mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 03:18:05 -05:00
Compare commits
123 Commits
controlnet
...
v6.0.0a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c09ffa283 | ||
|
|
283a50567e | ||
|
|
2f94952af8 | ||
|
|
2f3b2f1fce | ||
|
|
9295e3a914 | ||
|
|
bcd1a64ce5 | ||
|
|
3363b0b54e | ||
|
|
ea057d7d9d | ||
|
|
2039cee108 | ||
|
|
ce5f48e01c | ||
|
|
ce0488d995 | ||
|
|
91cfafdda5 | ||
|
|
6a16873a77 | ||
|
|
07b16fbd06 | ||
|
|
19ff73d717 | ||
|
|
665f394394 | ||
|
|
7ed909a8b7 | ||
|
|
101e8de161 | ||
|
|
15ea1dc6f1 | ||
|
|
bdb292fb2b | ||
|
|
086c5ed75c | ||
|
|
e37b007bbb | ||
|
|
9733e07f3c | ||
|
|
09b19dc30d | ||
|
|
94bad3ea9f | ||
|
|
769a65b72b | ||
|
|
2fb1d65475 | ||
|
|
8880979f98 | ||
|
|
e9bce78c56 | ||
|
|
4a1dbb4fd2 | ||
|
|
8a98fbf0ad | ||
|
|
fcfbb6c6cd | ||
|
|
45c48454f3 | ||
|
|
39b34585b2 | ||
|
|
104f3f859e | ||
|
|
23cc356579 | ||
|
|
8243865e33 | ||
|
|
cdfcf33c17 | ||
|
|
2e917ff829 | ||
|
|
aefa8967e4 | ||
|
|
cc60a431fb | ||
|
|
8ad847e3e8 | ||
|
|
825b5979e1 | ||
|
|
2703924906 | ||
|
|
25f9c8d256 | ||
|
|
5fbefc63e3 | ||
|
|
8853c909b7 | ||
|
|
1e9d441143 | ||
|
|
473614b335 | ||
|
|
ccaef27280 | ||
|
|
cd56c30935 | ||
|
|
8f4ba3d91f | ||
|
|
d9bb65d9bd | ||
|
|
40079377bf | ||
|
|
8718e898f4 | ||
|
|
665b344a23 | ||
|
|
687d388014 | ||
|
|
935d0677f4 | ||
|
|
0dbda6cfb7 | ||
|
|
c3ecd63b50 | ||
|
|
537a2e3197 | ||
|
|
9582ac5d50 | ||
|
|
8c7842cd1f | ||
|
|
61215fff8e | ||
|
|
92331d3b2f | ||
|
|
5eb31831a9 | ||
|
|
7824934eb5 | ||
|
|
64f953309a | ||
|
|
cad260a96d | ||
|
|
84aced5f46 | ||
|
|
539fe6ec07 | ||
|
|
20fb65d0a7 | ||
|
|
b2f1082fdf | ||
|
|
3fd8f4a67b | ||
|
|
366901c08d | ||
|
|
c149587046 | ||
|
|
e5cb4ab113 | ||
|
|
5492a0a3a9 | ||
|
|
59f8fe4a96 | ||
|
|
815d308f91 | ||
|
|
3f6e575137 | ||
|
|
86afdd96a1 | ||
|
|
aae8f57480 | ||
|
|
ad3f745c98 | ||
|
|
d8dbae5633 | ||
|
|
8ae6b9ddaf | ||
|
|
f51ef7b4f4 | ||
|
|
cd5bb65f30 | ||
|
|
3d30509b83 | ||
|
|
b8059a6b95 | ||
|
|
a19c6d9a3c | ||
|
|
d386e623d9 | ||
|
|
827c3d9f4b | ||
|
|
ddbec86db8 | ||
|
|
3383a6c63a | ||
|
|
3a636d9181 | ||
|
|
2012f8fd7f | ||
|
|
7058f1f43a | ||
|
|
99c6668d7e | ||
|
|
d183a1e0e0 | ||
|
|
abd9979f43 | ||
|
|
3cfa9b45e6 | ||
|
|
77c7ff0f2d | ||
|
|
9343fc6f43 | ||
|
|
daa8bdff72 | ||
|
|
4ce182ee4b | ||
|
|
177b22725c | ||
|
|
d8d3d832d6 | ||
|
|
dd91ed4495 | ||
|
|
6fd5b0dc12 | ||
|
|
7fcc3958e1 | ||
|
|
858d96ce63 | ||
|
|
51f6af1660 | ||
|
|
8f523ab981 | ||
|
|
335c7e4cce | ||
|
|
881ea1a36e | ||
|
|
5506717e68 | ||
|
|
9fb5cbd309 | ||
|
|
e9fc77dae9 | ||
|
|
2b60f0e8c8 | ||
|
|
056fd352c2 | ||
|
|
cb30be643e | ||
|
|
9c8af8c39d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -180,6 +180,7 @@ cython_debug/
|
|||||||
# Scratch folder
|
# Scratch folder
|
||||||
.scratch/
|
.scratch/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.zed/
|
||||||
|
|
||||||
# source installer files
|
# source installer files
|
||||||
installer/*zip
|
installer/*zip
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
|||||||
CancelByBatchIDsResult,
|
CancelByBatchIDsResult,
|
||||||
CancelByDestinationResult,
|
CancelByDestinationResult,
|
||||||
ClearResult,
|
ClearResult,
|
||||||
|
DeleteAllExceptCurrentResult,
|
||||||
|
DeleteByDestinationResult,
|
||||||
EnqueueBatchResult,
|
EnqueueBatchResult,
|
||||||
FieldIdentifier,
|
FieldIdentifier,
|
||||||
PruneResult,
|
PruneResult,
|
||||||
RetryItemsResult,
|
RetryItemsResult,
|
||||||
SessionQueueCountsByDestination,
|
SessionQueueCountsByDestination,
|
||||||
SessionQueueItem,
|
SessionQueueItem,
|
||||||
SessionQueueItemDTO,
|
|
||||||
SessionQueueStatus,
|
SessionQueueStatus,
|
||||||
)
|
)
|
||||||
from invokeai.app.services.shared.pagination import CursorPaginatedResults
|
from invokeai.app.services.shared.pagination import CursorPaginatedResults
|
||||||
@@ -68,7 +69,7 @@ async def enqueue_batch(
|
|||||||
"/{queue_id}/list",
|
"/{queue_id}/list",
|
||||||
operation_id="list_queue_items",
|
operation_id="list_queue_items",
|
||||||
responses={
|
responses={
|
||||||
200: {"model": CursorPaginatedResults[SessionQueueItemDTO]},
|
200: {"model": CursorPaginatedResults[SessionQueueItem]},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def list_queue_items(
|
async def list_queue_items(
|
||||||
@@ -77,11 +78,36 @@ async def list_queue_items(
|
|||||||
status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
|
status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
|
||||||
cursor: Optional[int] = Query(default=None, description="The pagination cursor"),
|
cursor: Optional[int] = Query(default=None, description="The pagination cursor"),
|
||||||
priority: int = Query(default=0, description="The pagination cursor priority"),
|
priority: int = Query(default=0, description="The pagination cursor priority"),
|
||||||
) -> CursorPaginatedResults[SessionQueueItemDTO]:
|
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
|
||||||
"""Gets all queue items (without graphs)"""
|
) -> CursorPaginatedResults[SessionQueueItem]:
|
||||||
|
"""Gets cursor-paginated queue items"""
|
||||||
|
|
||||||
return ApiDependencies.invoker.services.session_queue.list_queue_items(
|
return ApiDependencies.invoker.services.session_queue.list_queue_items(
|
||||||
queue_id=queue_id, limit=limit, status=status, cursor=cursor, priority=priority
|
queue_id=queue_id,
|
||||||
|
limit=limit,
|
||||||
|
status=status,
|
||||||
|
cursor=cursor,
|
||||||
|
priority=priority,
|
||||||
|
destination=destination,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@session_queue_router.get(
|
||||||
|
"/{queue_id}/list_all",
|
||||||
|
operation_id="list_all_queue_items",
|
||||||
|
responses={
|
||||||
|
200: {"model": list[SessionQueueItem]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def list_all_queue_items(
|
||||||
|
queue_id: str = Path(description="The queue id to perform this operation on"),
|
||||||
|
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
|
||||||
|
) -> list[SessionQueueItem]:
|
||||||
|
"""Gets all queue items"""
|
||||||
|
|
||||||
|
return ApiDependencies.invoker.services.session_queue.list_all_queue_items(
|
||||||
|
queue_id=queue_id,
|
||||||
|
destination=destination,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +147,18 @@ async def cancel_all_except_current(
|
|||||||
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
|
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
|
||||||
|
|
||||||
|
|
||||||
|
@session_queue_router.put(
|
||||||
|
"/{queue_id}/delete_all_except_current",
|
||||||
|
operation_id="delete_all_except_current",
|
||||||
|
responses={200: {"model": DeleteAllExceptCurrentResult}},
|
||||||
|
)
|
||||||
|
async def delete_all_except_current(
|
||||||
|
queue_id: str = Path(description="The queue id to perform this operation on"),
|
||||||
|
) -> DeleteAllExceptCurrentResult:
|
||||||
|
"""Immediately deletes all queue items except in-processing items"""
|
||||||
|
return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id)
|
||||||
|
|
||||||
|
|
||||||
@session_queue_router.put(
|
@session_queue_router.put(
|
||||||
"/{queue_id}/cancel_by_batch_ids",
|
"/{queue_id}/cancel_by_batch_ids",
|
||||||
operation_id="cancel_by_batch_ids",
|
operation_id="cancel_by_batch_ids",
|
||||||
@@ -269,6 +307,18 @@ async def get_queue_item(
|
|||||||
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
|
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
|
||||||
|
|
||||||
|
|
||||||
|
@session_queue_router.delete(
|
||||||
|
"/{queue_id}/i/{item_id}",
|
||||||
|
operation_id="delete_queue_item",
|
||||||
|
)
|
||||||
|
async def delete_queue_item(
|
||||||
|
queue_id: str = Path(description="The queue id to perform this operation on"),
|
||||||
|
item_id: int = Path(description="The queue item to delete"),
|
||||||
|
) -> None:
|
||||||
|
"""Deletes a queue item"""
|
||||||
|
ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id)
|
||||||
|
|
||||||
|
|
||||||
@session_queue_router.put(
|
@session_queue_router.put(
|
||||||
"/{queue_id}/i/{item_id}/cancel",
|
"/{queue_id}/i/{item_id}/cancel",
|
||||||
operation_id="cancel_queue_item",
|
operation_id="cancel_queue_item",
|
||||||
@@ -298,3 +348,18 @@ async def counts_by_destination(
|
|||||||
return ApiDependencies.invoker.services.session_queue.get_counts_by_destination(
|
return ApiDependencies.invoker.services.session_queue.get_counts_by_destination(
|
||||||
queue_id=queue_id, destination=destination
|
queue_id=queue_id, destination=destination
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@session_queue_router.delete(
|
||||||
|
"/{queue_id}/d/{destination}",
|
||||||
|
operation_id="delete_by_destination",
|
||||||
|
responses={200: {"model": DeleteByDestinationResult}},
|
||||||
|
)
|
||||||
|
async def delete_by_destination(
|
||||||
|
queue_id: str = Path(description="The queue id to query"),
|
||||||
|
destination: str = Path(description="The destination to query"),
|
||||||
|
) -> DeleteByDestinationResult:
|
||||||
|
"""Deletes all items with the given destination"""
|
||||||
|
return ApiDependencies.invoker.services.session_queue.delete_by_destination(
|
||||||
|
queue_id=queue_id, destination=destination
|
||||||
|
)
|
||||||
|
|||||||
@@ -582,6 +582,8 @@ def invocation(
|
|||||||
|
|
||||||
fields: dict[str, tuple[Any, FieldInfo]] = {}
|
fields: dict[str, tuple[Any, FieldInfo]] = {}
|
||||||
|
|
||||||
|
original_model_fields: dict[str, OriginalModelField] = {}
|
||||||
|
|
||||||
for field_name, field_info in cls.model_fields.items():
|
for field_name, field_info in cls.model_fields.items():
|
||||||
annotation = field_info.annotation
|
annotation = field_info.annotation
|
||||||
assert annotation is not None, f"{field_name} on invocation {invocation_type} has no type annotation."
|
assert annotation is not None, f"{field_name} on invocation {invocation_type} has no type annotation."
|
||||||
@@ -589,7 +591,7 @@ def invocation(
|
|||||||
f"{field_name} on invocation {invocation_type} has a non-dict json_schema_extra, did you forget to use InputField?"
|
f"{field_name} on invocation {invocation_type} has a non-dict json_schema_extra, did you forget to use InputField?"
|
||||||
)
|
)
|
||||||
|
|
||||||
cls._original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
|
original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
|
||||||
|
|
||||||
validate_field_default(cls.__name__, field_name, invocation_type, annotation, field_info)
|
validate_field_default(cls.__name__, field_name, invocation_type, annotation, field_info)
|
||||||
|
|
||||||
@@ -676,6 +678,7 @@ def invocation(
|
|||||||
docstring = cls.__doc__
|
docstring = cls.__doc__
|
||||||
new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields) # type: ignore
|
new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields) # type: ignore
|
||||||
new_class.__doc__ = docstring
|
new_class.__doc__ = docstring
|
||||||
|
new_class._original_model_fields = original_model_fields
|
||||||
|
|
||||||
InvocationRegistry.register_invocation(new_class)
|
InvocationRegistry.register_invocation(new_class)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
|||||||
CancelByDestinationResult,
|
CancelByDestinationResult,
|
||||||
CancelByQueueIDResult,
|
CancelByQueueIDResult,
|
||||||
ClearResult,
|
ClearResult,
|
||||||
|
DeleteAllExceptCurrentResult,
|
||||||
|
DeleteByDestinationResult,
|
||||||
EnqueueBatchResult,
|
EnqueueBatchResult,
|
||||||
IsEmptyResult,
|
IsEmptyResult,
|
||||||
IsFullResult,
|
IsFullResult,
|
||||||
@@ -17,7 +19,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
|||||||
RetryItemsResult,
|
RetryItemsResult,
|
||||||
SessionQueueCountsByDestination,
|
SessionQueueCountsByDestination,
|
||||||
SessionQueueItem,
|
SessionQueueItem,
|
||||||
SessionQueueItemDTO,
|
|
||||||
SessionQueueStatus,
|
SessionQueueStatus,
|
||||||
)
|
)
|
||||||
from invokeai.app.services.shared.graph import GraphExecutionState
|
from invokeai.app.services.shared.graph import GraphExecutionState
|
||||||
@@ -92,6 +93,11 @@ class SessionQueueBase(ABC):
|
|||||||
"""Cancels a session queue item"""
|
"""Cancels a session queue item"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_queue_item(self, item_id: int) -> None:
|
||||||
|
"""Deletes a session queue item"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fail_queue_item(
|
def fail_queue_item(
|
||||||
self, item_id: int, error_type: str, error_message: str, error_traceback: str
|
self, item_id: int, error_type: str, error_message: str, error_traceback: str
|
||||||
@@ -109,6 +115,11 @@ class SessionQueueBase(ABC):
|
|||||||
"""Cancels all queue items with the given batch destination"""
|
"""Cancels all queue items with the given batch destination"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult:
|
||||||
|
"""Deletes all queue items with the given batch destination"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
||||||
"""Cancels all queue items with matching queue ID"""
|
"""Cancels all queue items with matching queue ID"""
|
||||||
@@ -119,6 +130,11 @@ class SessionQueueBase(ABC):
|
|||||||
"""Cancels all queue items except in-progress items"""
|
"""Cancels all queue items except in-progress items"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
|
||||||
|
"""Deletes all queue items except in-progress items"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def list_queue_items(
|
def list_queue_items(
|
||||||
self,
|
self,
|
||||||
@@ -127,10 +143,20 @@ class SessionQueueBase(ABC):
|
|||||||
priority: int,
|
priority: int,
|
||||||
cursor: Optional[int] = None,
|
cursor: Optional[int] = None,
|
||||||
status: Optional[QUEUE_ITEM_STATUS] = None,
|
status: Optional[QUEUE_ITEM_STATUS] = None,
|
||||||
) -> CursorPaginatedResults[SessionQueueItemDTO]:
|
destination: Optional[str] = None,
|
||||||
|
) -> CursorPaginatedResults[SessionQueueItem]:
|
||||||
"""Gets a page of session queue items"""
|
"""Gets a page of session queue items"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_all_queue_items(
|
||||||
|
self,
|
||||||
|
queue_id: str,
|
||||||
|
destination: Optional[str] = None,
|
||||||
|
) -> list[SessionQueueItem]:
|
||||||
|
"""Gets all queue items that match the given parameters"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
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"""
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class FieldIdentifier(BaseModel):
|
|||||||
field_name: str = Field(description="The name of the field")
|
field_name: str = Field(description="The name of the field")
|
||||||
|
|
||||||
|
|
||||||
class SessionQueueItemWithoutGraph(BaseModel):
|
class SessionQueueItem(BaseModel):
|
||||||
"""Session queue item without the full graph. Used for serialization."""
|
"""Session queue item without the full graph. Used for serialization."""
|
||||||
|
|
||||||
item_id: int = Field(description="The identifier of the session queue item")
|
item_id: int = Field(description="The identifier of the session queue item")
|
||||||
@@ -251,42 +251,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
description="The ID of the published workflow associated with this queue item",
|
description="The ID of the published workflow associated with this queue item",
|
||||||
)
|
)
|
||||||
api_input_fields: Optional[list[FieldIdentifier]] = Field(
|
|
||||||
default=None, description="The fields that were used as input to the API"
|
|
||||||
)
|
|
||||||
api_output_fields: Optional[list[FieldIdentifier]] = Field(
|
|
||||||
default=None, description="The nodes that were used as output from the API"
|
|
||||||
)
|
|
||||||
credits: Optional[float] = Field(default=None, description="The total credits used for this queue item")
|
credits: Optional[float] = Field(default=None, description="The total credits used for this queue item")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def queue_item_dto_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO":
|
|
||||||
# must parse these manually
|
|
||||||
queue_item_dict["field_values"] = get_field_values(queue_item_dict)
|
|
||||||
return SessionQueueItemDTO(**queue_item_dict)
|
|
||||||
|
|
||||||
model_config = ConfigDict(
|
|
||||||
json_schema_extra={
|
|
||||||
"required": [
|
|
||||||
"item_id",
|
|
||||||
"status",
|
|
||||||
"batch_id",
|
|
||||||
"queue_id",
|
|
||||||
"session_id",
|
|
||||||
"priority",
|
|
||||||
"session_id",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SessionQueueItemDTO(SessionQueueItemWithoutGraph):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SessionQueueItem(SessionQueueItemWithoutGraph):
|
|
||||||
session: GraphExecutionState = Field(description="The fully-populated session to be executed")
|
session: GraphExecutionState = Field(description="The fully-populated session to be executed")
|
||||||
workflow: Optional[WorkflowWithoutID] = Field(
|
workflow: Optional[WorkflowWithoutID] = Field(
|
||||||
default=None, description="The workflow associated with this queue item"
|
default=None, description="The workflow associated with this queue item"
|
||||||
@@ -397,6 +362,18 @@ class CancelByDestinationResult(CancelByBatchIDsResult):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteByDestinationResult(BaseModel):
|
||||||
|
"""Result of deleting by a destination"""
|
||||||
|
|
||||||
|
deleted: int = Field(..., description="Number of queue items deleted")
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAllExceptCurrentResult(DeleteByDestinationResult):
|
||||||
|
"""Result of deleting all except current"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CancelByQueueIDResult(CancelByBatchIDsResult):
|
class CancelByQueueIDResult(CancelByBatchIDsResult):
|
||||||
"""Result of canceling by queue id"""
|
"""Result of canceling by queue id"""
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
|||||||
CancelByDestinationResult,
|
CancelByDestinationResult,
|
||||||
CancelByQueueIDResult,
|
CancelByQueueIDResult,
|
||||||
ClearResult,
|
ClearResult,
|
||||||
|
DeleteAllExceptCurrentResult,
|
||||||
|
DeleteByDestinationResult,
|
||||||
EnqueueBatchResult,
|
EnqueueBatchResult,
|
||||||
IsEmptyResult,
|
IsEmptyResult,
|
||||||
IsFullResult,
|
IsFullResult,
|
||||||
@@ -24,7 +26,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
|||||||
RetryItemsResult,
|
RetryItemsResult,
|
||||||
SessionQueueCountsByDestination,
|
SessionQueueCountsByDestination,
|
||||||
SessionQueueItem,
|
SessionQueueItem,
|
||||||
SessionQueueItemDTO,
|
|
||||||
SessionQueueItemNotFoundError,
|
SessionQueueItemNotFoundError,
|
||||||
SessionQueueStatus,
|
SessionQueueStatus,
|
||||||
ValueToInsertTuple,
|
ValueToInsertTuple,
|
||||||
@@ -46,10 +47,6 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
clear_result = self.clear(DEFAULT_QUEUE_ID)
|
clear_result = self.clear(DEFAULT_QUEUE_ID)
|
||||||
if clear_result.deleted > 0:
|
if clear_result.deleted > 0:
|
||||||
self.__invoker.services.logger.info(f"Cleared all {clear_result.deleted} queue items")
|
self.__invoker.services.logger.info(f"Cleared all {clear_result.deleted} queue items")
|
||||||
else:
|
|
||||||
prune_result = self.prune(DEFAULT_QUEUE_ID)
|
|
||||||
if prune_result.deleted > 0:
|
|
||||||
self.__invoker.services.logger.info(f"Pruned {prune_result.deleted} finished queue items")
|
|
||||||
|
|
||||||
def __init__(self, db: SqliteDatabase) -> None:
|
def __init__(self, db: SqliteDatabase) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -217,6 +214,19 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
) -> SessionQueueItem:
|
) -> SessionQueueItem:
|
||||||
try:
|
try:
|
||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
SELECT status FROM session_queue WHERE item_id = ?
|
||||||
|
""",
|
||||||
|
(item_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}")
|
||||||
|
current_status = row[0]
|
||||||
|
# Only update if not already finished (completed, failed or canceled)
|
||||||
|
if current_status in ("completed", "failed", "canceled"):
|
||||||
|
return self.get_queue_item(item_id)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""--sql
|
"""--sql
|
||||||
UPDATE session_queue
|
UPDATE session_queue
|
||||||
@@ -328,6 +338,27 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
queue_item = self._set_queue_item_status(item_id=item_id, status="canceled")
|
queue_item = self._set_queue_item_status(item_id=item_id, status="canceled")
|
||||||
return queue_item
|
return queue_item
|
||||||
|
|
||||||
|
def delete_queue_item(self, item_id: int) -> None:
|
||||||
|
"""Deletes a session queue item"""
|
||||||
|
try:
|
||||||
|
self.cancel_queue_item(item_id)
|
||||||
|
except SessionQueueItemNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
cursor = self._conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
DELETE
|
||||||
|
FROM session_queue
|
||||||
|
WHERE item_id = ?
|
||||||
|
""",
|
||||||
|
(item_id,),
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
def complete_queue_item(self, item_id: int) -> SessionQueueItem:
|
def complete_queue_item(self, item_id: int) -> SessionQueueItem:
|
||||||
queue_item = self._set_queue_item_status(item_id=item_id, status="completed")
|
queue_item = self._set_queue_item_status(item_id=item_id, status="completed")
|
||||||
return queue_item
|
return queue_item
|
||||||
@@ -425,6 +456,71 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
raise
|
raise
|
||||||
return CancelByDestinationResult(canceled=count)
|
return CancelByDestinationResult(canceled=count)
|
||||||
|
|
||||||
|
def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult:
|
||||||
|
try:
|
||||||
|
cursor = self._conn.cursor()
|
||||||
|
current_queue_item = self.get_current(queue_id)
|
||||||
|
if current_queue_item is not None and current_queue_item.destination == destination:
|
||||||
|
self.cancel_queue_item(current_queue_item.item_id)
|
||||||
|
params = (queue_id, destination)
|
||||||
|
cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM session_queue
|
||||||
|
WHERE
|
||||||
|
queue_id = ?
|
||||||
|
AND destination = ?;
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
DELETE
|
||||||
|
FROM session_queue
|
||||||
|
WHERE
|
||||||
|
queue_id = ?
|
||||||
|
AND destination = ?;
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
return DeleteByDestinationResult(deleted=count)
|
||||||
|
|
||||||
|
def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
|
||||||
|
try:
|
||||||
|
cursor = self._conn.cursor()
|
||||||
|
where = """--sql
|
||||||
|
WHERE
|
||||||
|
queue_id == ?
|
||||||
|
AND status == 'pending'
|
||||||
|
"""
|
||||||
|
cursor.execute(
|
||||||
|
f"""--sql
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM session_queue
|
||||||
|
{where};
|
||||||
|
""",
|
||||||
|
(queue_id,),
|
||||||
|
)
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
cursor.execute(
|
||||||
|
f"""--sql
|
||||||
|
DELETE
|
||||||
|
FROM session_queue
|
||||||
|
{where};
|
||||||
|
""",
|
||||||
|
(queue_id,),
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
return DeleteAllExceptCurrentResult(deleted=count)
|
||||||
|
|
||||||
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
||||||
try:
|
try:
|
||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
@@ -540,26 +636,12 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
priority: int,
|
priority: int,
|
||||||
cursor: Optional[int] = None,
|
cursor: Optional[int] = None,
|
||||||
status: Optional[QUEUE_ITEM_STATUS] = None,
|
status: Optional[QUEUE_ITEM_STATUS] = None,
|
||||||
) -> CursorPaginatedResults[SessionQueueItemDTO]:
|
destination: Optional[str] = None,
|
||||||
|
) -> CursorPaginatedResults[SessionQueueItem]:
|
||||||
cursor_ = self._conn.cursor()
|
cursor_ = self._conn.cursor()
|
||||||
item_id = cursor
|
item_id = cursor
|
||||||
query = """--sql
|
query = """--sql
|
||||||
SELECT item_id,
|
SELECT *
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
field_values,
|
|
||||||
error_type,
|
|
||||||
error_message,
|
|
||||||
error_traceback,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
completed_at,
|
|
||||||
started_at,
|
|
||||||
session_id,
|
|
||||||
batch_id,
|
|
||||||
queue_id,
|
|
||||||
origin,
|
|
||||||
destination
|
|
||||||
FROM session_queue
|
FROM session_queue
|
||||||
WHERE queue_id = ?
|
WHERE queue_id = ?
|
||||||
"""
|
"""
|
||||||
@@ -571,6 +653,12 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
"""
|
"""
|
||||||
params.append(status)
|
params.append(status)
|
||||||
|
|
||||||
|
if destination is not None:
|
||||||
|
query += """---sql
|
||||||
|
AND destination = ?
|
||||||
|
"""
|
||||||
|
params.append(destination)
|
||||||
|
|
||||||
if item_id is not None:
|
if item_id is not None:
|
||||||
query += """--sql
|
query += """--sql
|
||||||
AND (priority < ?) OR (priority = ? AND item_id > ?)
|
AND (priority < ?) OR (priority = ? AND item_id > ?)
|
||||||
@@ -586,7 +674,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
params.append(limit + 1)
|
params.append(limit + 1)
|
||||||
cursor_.execute(query, params)
|
cursor_.execute(query, params)
|
||||||
results = cast(list[sqlite3.Row], cursor_.fetchall())
|
results = cast(list[sqlite3.Row], cursor_.fetchall())
|
||||||
items = [SessionQueueItemDTO.queue_item_dto_from_dict(dict(result)) for result in results]
|
items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
|
||||||
has_more = False
|
has_more = False
|
||||||
if len(items) > limit:
|
if len(items) > limit:
|
||||||
# remove the extra item
|
# remove the extra item
|
||||||
@@ -594,6 +682,37 @@ class SqliteSessionQueue(SessionQueueBase):
|
|||||||
has_more = True
|
has_more = True
|
||||||
return CursorPaginatedResults(items=items, limit=limit, has_more=has_more)
|
return CursorPaginatedResults(items=items, limit=limit, has_more=has_more)
|
||||||
|
|
||||||
|
def list_all_queue_items(
|
||||||
|
self,
|
||||||
|
queue_id: str,
|
||||||
|
destination: Optional[str] = None,
|
||||||
|
) -> list[SessionQueueItem]:
|
||||||
|
"""Gets all queue items that match the given parameters"""
|
||||||
|
cursor_ = self._conn.cursor()
|
||||||
|
query = """--sql
|
||||||
|
SELECT *
|
||||||
|
FROM session_queue
|
||||||
|
WHERE queue_id = ?
|
||||||
|
"""
|
||||||
|
params: list[Union[str, int]] = [queue_id]
|
||||||
|
|
||||||
|
if destination is not None:
|
||||||
|
query += """---sql
|
||||||
|
AND destination = ?
|
||||||
|
"""
|
||||||
|
params.append(destination)
|
||||||
|
|
||||||
|
query += """--sql
|
||||||
|
ORDER BY
|
||||||
|
priority DESC,
|
||||||
|
item_id ASC
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
cursor_.execute(query, params)
|
||||||
|
results = cast(list[sqlite3.Row], cursor_.fetchall())
|
||||||
|
items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
|
||||||
|
return items
|
||||||
|
|
||||||
def get_queue_status(self, queue_id: str) -> SessionQueueStatus:
|
def get_queue_status(self, queue_id: str) -> SessionQueueStatus:
|
||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
GetCoreSchemaHandler,
|
GetCoreSchemaHandler,
|
||||||
GetJsonSchemaHandler,
|
GetJsonSchemaHandler,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
@@ -787,6 +788,22 @@ class GraphExecutionState(BaseModel):
|
|||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"graph",
|
||||||
|
"execution_graph",
|
||||||
|
"executed",
|
||||||
|
"executed_history",
|
||||||
|
"results",
|
||||||
|
"errors",
|
||||||
|
"prepared_source_mapping",
|
||||||
|
"source_prepared_mapping",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("graph")
|
@field_validator("graph")
|
||||||
def graph_is_valid(cls, v: Graph):
|
def graph_is_valid(cls, v: Graph):
|
||||||
"""Validates that the graph is valid"""
|
"""Validates that the graph is valid"""
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ module.exports = {
|
|||||||
// https://github.com/qdanik/eslint-plugin-path
|
// https://github.com/qdanik/eslint-plugin-path
|
||||||
'path/no-relative-imports': ['error', { maxDepth: 0 }],
|
'path/no-relative-imports': ['error', { maxDepth: 0 }],
|
||||||
// https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md
|
// https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md
|
||||||
'i18next/no-literal-string': 'error',
|
// TODO: ENABLE THIS RULE BEFORE v6.0.0
|
||||||
|
// 'i18next/no-literal-string': 'error',
|
||||||
// https://eslint.org/docs/latest/rules/no-console
|
// https://eslint.org/docs/latest/rules/no-console
|
||||||
'no-console': 'error',
|
'no-console': 'error',
|
||||||
// https://eslint.org/docs/latest/rules/no-promise-executor-return
|
// https://eslint.org/docs/latest/rules/no-promise-executor-return
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { KnipConfig } from 'knip';
|
|||||||
const config: KnipConfig = {
|
const config: KnipConfig = {
|
||||||
project: ['src/**/*.{ts,tsx}!'],
|
project: ['src/**/*.{ts,tsx}!'],
|
||||||
ignore: [
|
ignore: [
|
||||||
|
// TODO(psyche): temporarily ignored all files for test build purposes
|
||||||
|
'src/**',
|
||||||
// This file is only used during debugging
|
// This file is only used during debugging
|
||||||
'src/app/store/middleware/debugLoggerMiddleware.ts',
|
'src/app/store/middleware/debugLoggerMiddleware.ts',
|
||||||
// Autogenerated types - shouldn't ever touch these
|
// Autogenerated types - shouldn't ever touch these
|
||||||
|
|||||||
@@ -60,13 +60,14 @@
|
|||||||
"@fontsource-variable/inter": "^5.2.5",
|
"@fontsource-variable/inter": "^5.2.5",
|
||||||
"@invoke-ai/ui-library": "^0.0.46",
|
"@invoke-ai/ui-library": "^0.0.46",
|
||||||
"@nanostores/react": "^1.0.0",
|
"@nanostores/react": "^1.0.0",
|
||||||
"@reduxjs/toolkit": "2.7.0",
|
"@reduxjs/toolkit": "2.8.2",
|
||||||
"@roarr/browser-log-writer": "^1.3.0",
|
"@roarr/browser-log-writer": "^1.3.0",
|
||||||
"@xyflow/react": "^12.6.0",
|
"@xyflow/react": "^12.6.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chakra-react-select": "^4.9.2",
|
"chakra-react-select": "^4.9.2",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
|
"dockview": "^4.3.1",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"fracturedjsonjs": "^4.1.0",
|
"fracturedjsonjs": "^4.1.0",
|
||||||
"framer-motion": "^11.10.0",
|
"framer-motion": "^11.10.0",
|
||||||
|
|||||||
24
invokeai/frontend/web/pnpm-lock.yaml
generated
24
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -30,8 +30,8 @@ dependencies:
|
|||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(nanostores@1.0.1)(react@18.3.1)
|
version: 1.0.0(nanostores@1.0.1)(react@18.3.1)
|
||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: 2.7.0
|
specifier: 2.8.2
|
||||||
version: 2.7.0(react-redux@9.2.0)(react@18.3.1)
|
version: 2.8.2(react-redux@9.2.0)(react@18.3.1)
|
||||||
'@roarr/browser-log-writer':
|
'@roarr/browser-log-writer':
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
@@ -50,6 +50,9 @@ dependencies:
|
|||||||
compare-versions:
|
compare-versions:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1
|
version: 6.1.1
|
||||||
|
dockview:
|
||||||
|
specifier: ^4.3.1
|
||||||
|
version: 4.3.1(react@18.3.1)
|
||||||
filesize:
|
filesize:
|
||||||
specifier: ^10.1.6
|
specifier: ^10.1.6
|
||||||
version: 10.1.6
|
version: 10.1.6
|
||||||
@@ -2161,8 +2164,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@reduxjs/toolkit@2.7.0(react-redux@9.2.0)(react@18.3.1):
|
/@reduxjs/toolkit@2.8.2(react-redux@9.2.0)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==}
|
resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||||
@@ -4492,6 +4495,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
|
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dockview-core@4.3.1:
|
||||||
|
resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/dockview@4.3.1(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
dependencies:
|
||||||
|
dockview-core: 4.3.1
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/doctrine@2.1.0:
|
/doctrine@2.1.0:
|
||||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|||||||
@@ -2015,7 +2015,9 @@
|
|||||||
"resetGenerationSettings": "Reset Generation Settings",
|
"resetGenerationSettings": "Reset Generation Settings",
|
||||||
"replaceCurrent": "Replace Current",
|
"replaceCurrent": "Replace Current",
|
||||||
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
|
||||||
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, or <PullBboxButton>pull the bounding box into this layer</PullBboxButton> to get started.",
|
"referenceImageEmptyStateWithCanvasOptions": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image or <PullBboxButton>pull the bounding box into this Reference Image</PullBboxButton> to get started.",
|
||||||
|
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton> or drag an image from the <GalleryButton>gallery</GalleryButton> onto this Reference Image to get started.",
|
||||||
|
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
|
||||||
"imageNoise": "Image Noise",
|
"imageNoise": "Image Noise",
|
||||||
"denoiseLimit": "Denoise Limit",
|
"denoiseLimit": "Denoise Limit",
|
||||||
"warnings": {
|
"warnings": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator';
|
import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator';
|
||||||
import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator';
|
import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator';
|
||||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||||
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
import { $globalIsLoading } from 'app/store/nanostores/globalIsLoading';
|
||||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||||
import Loading from 'common/components/Loading/Loading';
|
import Loading from 'common/components/Loading/Loading';
|
||||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||||
@@ -20,7 +20,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||||
const didStudioInit = useStore($didStudioInit);
|
const globalIsLoading = useStore($globalIsLoading);
|
||||||
const clearStorage = useClearStorage();
|
const clearStorage = useClearStorage();
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
@@ -33,7 +33,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
|||||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||||
<Box id="invoke-app-wrapper" w="100dvw" h="100dvh" position="relative" overflow="hidden">
|
<Box id="invoke-app-wrapper" w="100dvw" h="100dvh" position="relative" overflow="hidden">
|
||||||
<AppContent />
|
<AppContent />
|
||||||
{!didStudioInit && <Loading />}
|
{globalIsLoading && <Loading />}
|
||||||
</Box>
|
</Box>
|
||||||
<GlobalHookIsolator config={config} studioInitAction={studioInitAction} />
|
<GlobalHookIsolator config={config} studioInitAction={studioInitAction} />
|
||||||
<GlobalModalIsolator />
|
<GlobalModalIsolator />
|
||||||
|
|||||||
@@ -8,19 +8,25 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||||
|
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
|
||||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||||
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
||||||
|
import { toggleImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||||
|
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { memo, useEffect } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||||
|
import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue';
|
||||||
import { useSocketIO } from 'services/events/useSocketIO';
|
import { useSocketIO } from 'services/events/useSocketIO';
|
||||||
|
|
||||||
|
const queueCountArg = { destination: 'canvas' };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GlobalHookIsolator is a logical component that runs global hooks in an isolated component, so that they do not
|
* GlobalHookIsolator is a logical component that runs global hooks in an isolated component, so that they do not
|
||||||
* cause needless re-renders of any other components.
|
* cause needless re-renders of any other components.
|
||||||
@@ -38,6 +44,11 @@ export const GlobalHookIsolator = memo(
|
|||||||
useGlobalHotkeys();
|
useGlobalHotkeys();
|
||||||
useGetOpenAPISchemaQuery();
|
useGetOpenAPISchemaQuery();
|
||||||
useSyncLoggingConfig();
|
useSyncLoggingConfig();
|
||||||
|
useCloseChakraTooltipsOnDragFix();
|
||||||
|
|
||||||
|
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
|
||||||
|
// and/or in progress canvas sessions.
|
||||||
|
useGetQueueCountsByDestinationQuery(queueCountArg);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(language);
|
i18n.changeLanguage(language);
|
||||||
@@ -61,6 +72,12 @@ export const GlobalHookIsolator = memo(
|
|||||||
useWorkflowBuilderWatcher();
|
useWorkflowBuilderWatcher();
|
||||||
useDynamicPromptsWatcher();
|
useDynamicPromptsWatcher();
|
||||||
|
|
||||||
|
useRegisteredHotkeys({
|
||||||
|
id: 'toggleViewer',
|
||||||
|
category: 'viewer',
|
||||||
|
callback: toggleImageViewer,
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import {
|
|||||||
NewGallerySessionDialog,
|
NewGallerySessionDialog,
|
||||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
|
import { ImageViewerModal } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
|
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||||
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
|
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
|
||||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||||
@@ -39,6 +41,7 @@ export const GlobalModalIsolator = memo(() => {
|
|||||||
<StylePresetModal />
|
<StylePresetModal />
|
||||||
<WorkflowLibraryModal />
|
<WorkflowLibraryModal />
|
||||||
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
|
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
|
||||||
|
<DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />
|
||||||
<ClearQueueConfirmationsAlertDialog />
|
<ClearQueueConfirmationsAlertDialog />
|
||||||
<NewWorkflowConfirmationAlertDialog />
|
<NewWorkflowConfirmationAlertDialog />
|
||||||
<LoadWorkflowConfirmationAlertDialog />
|
<LoadWorkflowConfirmationAlertDialog />
|
||||||
@@ -58,6 +61,7 @@ export const GlobalModalIsolator = memo(() => {
|
|||||||
<CanvasPasteModal />
|
<CanvasPasteModal />
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
<LoadWorkflowFromGraphModal />
|
<LoadWorkflowFromGraphModal />
|
||||||
|
<ImageViewerModal />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useAppStore } from 'app/store/storeHooks';
|
|||||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
import { withResultAsync } from 'common/util/result';
|
import { withResultAsync } from 'common/util/result';
|
||||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||||
import { settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||||
|
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
|
||||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
@@ -93,8 +93,6 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
|||||||
};
|
};
|
||||||
store.dispatch(canvasReset());
|
store.dispatch(canvasReset());
|
||||||
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||||
store.dispatch(settingsSendToCanvasChanged(true));
|
|
||||||
store.dispatch(setActiveTab('canvas'));
|
|
||||||
store.dispatch(sentImageToCanvas());
|
store.dispatch(sentImageToCanvas());
|
||||||
$imageViewer.set(false);
|
$imageViewer.set(false);
|
||||||
toast({
|
toast({
|
||||||
@@ -118,9 +116,9 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const metadata = getImageMetadataResult.value;
|
const metadata = getImageMetadataResult.value;
|
||||||
|
store.dispatch(canvasReset());
|
||||||
// This shows a toast
|
// This shows a toast
|
||||||
await parseAndRecallAllMetadata(metadata, true);
|
await parseAndRecallAllMetadata(metadata, true);
|
||||||
store.dispatch(setActiveTab('canvas'));
|
|
||||||
},
|
},
|
||||||
[store, t]
|
[store, t]
|
||||||
);
|
);
|
||||||
@@ -164,15 +162,13 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
|||||||
switch (destination) {
|
switch (destination) {
|
||||||
case 'generation':
|
case 'generation':
|
||||||
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
|
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
|
||||||
store.dispatch(setActiveTab('canvas'));
|
store.dispatch(paramsReset());
|
||||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
store.dispatch(settingsSendToCanvasChanged(false));
|
|
||||||
$imageViewer.set(true);
|
$imageViewer.set(true);
|
||||||
break;
|
break;
|
||||||
case 'canvas':
|
case 'canvas':
|
||||||
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
|
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
|
||||||
store.dispatch(setActiveTab('canvas'));
|
store.dispatch(canvasReset());
|
||||||
store.dispatch(settingsSendToCanvasChanged(true));
|
|
||||||
$imageViewer.set(false);
|
$imageViewer.set(false);
|
||||||
break;
|
break;
|
||||||
case 'workflows':
|
case 'workflows':
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { TypedStartListening } from '@reduxjs/toolkit';
|
import type { TypedStartListening } from '@reduxjs/toolkit';
|
||||||
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
|
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||||
import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
|
||||||
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
||||||
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
||||||
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||||
@@ -10,15 +9,14 @@ import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/l
|
|||||||
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
|
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
|
||||||
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
|
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
|
||||||
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
||||||
|
import { addEnsureImageIsSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener';
|
||||||
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||||
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
|
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
|
||||||
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
||||||
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
||||||
import { addImageDeletionListeners } from 'app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners';
|
|
||||||
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
||||||
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
||||||
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
|
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
|
||||||
import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected';
|
|
||||||
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
|
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
|
||||||
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
|
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
|
||||||
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
|
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
|
||||||
@@ -47,9 +45,7 @@ export const addAppListener = addListener.withTypes<RootState, AppDispatch>();
|
|||||||
addImageUploadedFulfilledListener(startAppListening);
|
addImageUploadedFulfilledListener(startAppListening);
|
||||||
|
|
||||||
// Image deleted
|
// Image deleted
|
||||||
addImageDeletionListeners(startAppListening);
|
|
||||||
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
||||||
addImageToDeleteSelectedListener(startAppListening);
|
|
||||||
|
|
||||||
// Image starred
|
// Image starred
|
||||||
addImagesStarredListener(startAppListening);
|
addImagesStarredListener(startAppListening);
|
||||||
@@ -65,9 +61,6 @@ addEnqueueRequestedUpscale(startAppListening);
|
|||||||
addAnyEnqueuedListener(startAppListening);
|
addAnyEnqueuedListener(startAppListening);
|
||||||
addBatchEnqueuedListener(startAppListening);
|
addBatchEnqueuedListener(startAppListening);
|
||||||
|
|
||||||
// Canvas actions
|
|
||||||
addStagingListeners(startAppListening);
|
|
||||||
|
|
||||||
// Socket.IO
|
// Socket.IO
|
||||||
addSocketConnectedEventListener(startAppListening);
|
addSocketConnectedEventListener(startAppListening);
|
||||||
|
|
||||||
@@ -95,3 +88,5 @@ addAppConfigReceivedListener(startAppListening);
|
|||||||
addAdHocPostProcessingRequestedListener(startAppListening);
|
addAdHocPostProcessingRequestedListener(startAppListening);
|
||||||
|
|
||||||
addSetDefaultSettingsListener(startAppListening);
|
addSetDefaultSettingsListener(startAppListening);
|
||||||
|
|
||||||
|
addEnsureImageIsSelectedListener(startAppListening);
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { isAnyOf } from '@reduxjs/toolkit';
|
|
||||||
import { logger } from 'app/logging/logger';
|
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
|
||||||
import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
|
|
||||||
import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
|
||||||
import { toast } from 'features/toast/toast';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
|
||||||
|
|
||||||
const log = logger('canvas');
|
|
||||||
|
|
||||||
const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset, newSessionRequested);
|
|
||||||
|
|
||||||
export const addStagingListeners = (startAppListening: AppStartListening) => {
|
|
||||||
startAppListening({
|
|
||||||
matcher: matchCanvasOrStagingAreaReset,
|
|
||||||
effect: async (_, { dispatch }) => {
|
|
||||||
try {
|
|
||||||
const req = dispatch(
|
|
||||||
queueApi.endpoints.cancelByBatchDestination.initiate(
|
|
||||||
{ destination: 'canvas' },
|
|
||||||
{ fixedCacheKey: 'cancelByBatchOrigin' }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const { canceled } = await req.unwrap();
|
|
||||||
req.reset();
|
|
||||||
|
|
||||||
if (canceled > 0) {
|
|
||||||
log.debug(`Canceled ${canceled} canvas batches`);
|
|
||||||
toast({
|
|
||||||
id: 'CANCEL_BATCH_SUCCEEDED',
|
|
||||||
title: t('queue.cancelBatchSucceeded'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log.error('Failed to cancel canvas batches');
|
|
||||||
toast({
|
|
||||||
id: 'CANCEL_BATCH_FAILED',
|
|
||||||
title: t('queue.cancelBatchFailed'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||||
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
import { getImageUsage } from 'features/deleteImageModal/store/state';
|
||||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||||
@@ -20,9 +21,10 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
|
|||||||
const nodes = selectNodesSlice(state);
|
const nodes = selectNodesSlice(state);
|
||||||
const canvas = selectCanvasSlice(state);
|
const canvas = selectCanvasSlice(state);
|
||||||
const upscale = selectUpscaleSlice(state);
|
const upscale = selectUpscaleSlice(state);
|
||||||
|
const refImages = selectRefImagesSlice(state);
|
||||||
|
|
||||||
deleted_images.forEach((image_name) => {
|
deleted_images.forEach((image_name) => {
|
||||||
const imageUsage = getImageUsage(nodes, canvas, upscale, image_name);
|
const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name);
|
||||||
|
|
||||||
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
|
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
|
||||||
dispatch(nodeEditorReset());
|
dispatch(nodeEditorReset());
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
|||||||
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
|
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
|
||||||
import { withResult, withResultAsync } from 'common/util/result';
|
import { withResult, withResultAsync } from 'common/util/result';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
|
import {
|
||||||
|
canvasSessionIdCreated,
|
||||||
|
generateSessionIdCreated,
|
||||||
|
selectCanvasSessionId,
|
||||||
|
selectGenerateSessionId,
|
||||||
|
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
|
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
|
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
|
||||||
@@ -17,6 +23,7 @@ import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Grap
|
|||||||
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
|
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
|
||||||
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
|
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||||
import { serializeError } from 'serialize-error';
|
import { serializeError } from 'serialize-error';
|
||||||
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
|
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
|
||||||
import { assert, AssertionError } from 'tsafe';
|
import { assert, AssertionError } from 'tsafe';
|
||||||
@@ -30,11 +37,34 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
actionCreator: enqueueRequestedCanvas,
|
actionCreator: enqueueRequestedCanvas,
|
||||||
effect: async (action, { getState, dispatch }) => {
|
effect: async (action, { getState, dispatch }) => {
|
||||||
log.debug('Enqueue requested');
|
log.debug('Enqueue requested');
|
||||||
|
|
||||||
|
const tab = selectActiveTab(getState());
|
||||||
|
let sessionId = null;
|
||||||
|
if (tab === 'generate') {
|
||||||
|
sessionId = selectGenerateSessionId(getState());
|
||||||
|
if (!sessionId) {
|
||||||
|
dispatch(generateSessionIdCreated());
|
||||||
|
sessionId = selectGenerateSessionId(getState());
|
||||||
|
}
|
||||||
|
} else if (tab === 'canvas') {
|
||||||
|
sessionId = selectCanvasSessionId(getState());
|
||||||
|
if (!sessionId) {
|
||||||
|
dispatch(canvasSessionIdCreated());
|
||||||
|
sessionId = selectCanvasSessionId(getState());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(`Enqueue requested in unsupported tab ${tab}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const destination = sessionId;
|
||||||
|
assert(destination !== null);
|
||||||
|
|
||||||
const { prepend } = action.payload;
|
const { prepend } = action.payload;
|
||||||
|
|
||||||
const manager = $canvasManager.get();
|
const manager = $canvasManager.get();
|
||||||
assert(manager, 'No canvas manager');
|
// assert(manager, 'No canvas manager');
|
||||||
|
|
||||||
const model = state.params.model;
|
const model = state.params.model;
|
||||||
assert(model, 'No model found in state');
|
assert(model, 'No model found in state');
|
||||||
@@ -87,8 +117,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value;
|
const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value;
|
||||||
|
|
||||||
const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
|
|
||||||
|
|
||||||
const prepareBatchResult = withResult(() =>
|
const prepareBatchResult = withResult(() =>
|
||||||
prepareLinearUIBatch({
|
prepareLinearUIBatch({
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
export const addEnsureImageIsSelectedListener = (startAppListening: AppStartListening) => {
|
||||||
|
// When we list images, if no images is selected, select the first one.
|
||||||
|
startAppListening({
|
||||||
|
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
||||||
|
effect: (action, { dispatch, getState }) => {
|
||||||
|
const selection = getState().gallery.selection;
|
||||||
|
if (selection.length === 0) {
|
||||||
|
dispatch(imageSelected(action.payload.items[0] ?? null));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
|
||||||
import type { AppDispatch, RootState } from 'app/store/store';
|
|
||||||
import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
|
||||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
|
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
||||||
import { forEach, intersectionBy } from 'lodash-es';
|
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
|
||||||
import type { Param0 } from 'tsafe';
|
|
||||||
|
|
||||||
const log = logger('gallery');
|
|
||||||
|
|
||||||
//TODO(psyche): handle image deletion (canvas staging area?)
|
|
||||||
|
|
||||||
// Some utils to delete images from different parts of the app
|
|
||||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
|
||||||
const actions: Param0<typeof dispatch>[] = [];
|
|
||||||
state.nodes.present.nodes.forEach((node) => {
|
|
||||||
if (!isInvocationNode(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(node.data.inputs, (input) => {
|
|
||||||
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
|
|
||||||
actions.push(
|
|
||||||
fieldImageValueChanged({
|
|
||||||
nodeId: node.data.id,
|
|
||||||
fieldName: input.name,
|
|
||||||
value: undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isImageFieldCollectionInputInstance(input)) {
|
|
||||||
actions.push(
|
|
||||||
fieldImageCollectionValueChanged({
|
|
||||||
nodeId: node.data.id,
|
|
||||||
fieldName: input.name,
|
|
||||||
value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.forEach(dispatch);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
|
||||||
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
|
|
||||||
let shouldDelete = false;
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
|
|
||||||
shouldDelete = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldDelete) {
|
|
||||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
|
||||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
|
||||||
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
|
|
||||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
|
||||||
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
|
|
||||||
let shouldDelete = false;
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
|
|
||||||
shouldDelete = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldDelete) {
|
|
||||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addImageDeletionListeners = (startAppListening: AppStartListening) => {
|
|
||||||
// Handle single image deletion
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: imageDeletionConfirmed,
|
|
||||||
effect: async (action, { dispatch, getState }) => {
|
|
||||||
const { imageDTOs, imagesUsage } = action.payload;
|
|
||||||
|
|
||||||
if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
|
|
||||||
// handle multiples in separate listener
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageDTO = imageDTOs[0];
|
|
||||||
const imageUsage = imagesUsage[0];
|
|
||||||
|
|
||||||
if (!imageDTO || !imageUsage) {
|
|
||||||
// satisfy noUncheckedIndexedAccess
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = getState();
|
|
||||||
await dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO)).unwrap();
|
|
||||||
|
|
||||||
if (state.gallery.selection.some((i) => i.image_name === imageDTO.image_name)) {
|
|
||||||
// The deleted image was a selected image, we need to select the next image
|
|
||||||
const newSelection = state.gallery.selection.filter((i) => i.image_name !== imageDTO.image_name);
|
|
||||||
|
|
||||||
if (newSelection.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current list of images and select the same index
|
|
||||||
const baseQueryArgs = selectListImagesQueryArgs(state);
|
|
||||||
const data = imagesApi.endpoints.listImages.select(baseQueryArgs)(state).data;
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const deletedImageIndex = data.items.findIndex((i) => i.image_name === imageDTO.image_name);
|
|
||||||
const nextImage = data.items[deletedImageIndex + 1] ?? data.items[0] ?? null;
|
|
||||||
if (nextImage?.image_name === imageDTO.image_name) {
|
|
||||||
// If the next image is the same as the deleted one, it means it was the last image, reset selection
|
|
||||||
dispatch(imageSelected(null));
|
|
||||||
} else {
|
|
||||||
dispatch(imageSelected(nextImage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteNodesImages(state, dispatch, imageDTO);
|
|
||||||
deleteReferenceImages(state, dispatch, imageDTO);
|
|
||||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
|
||||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
} finally {
|
|
||||||
dispatch(isModalOpenChanged(false));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle multiple image deletion
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: imageDeletionConfirmed,
|
|
||||||
effect: async (action, { dispatch, getState }) => {
|
|
||||||
const { imageDTOs, imagesUsage } = action.payload;
|
|
||||||
|
|
||||||
if (imageDTOs.length <= 1 || imagesUsage.length <= 1) {
|
|
||||||
// handle singles in separate listener
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = getState();
|
|
||||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
|
||||||
|
|
||||||
if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
|
|
||||||
// Some selected images were deleted, need to select the next image
|
|
||||||
const queryArgs = selectListImagesQueryArgs(state);
|
|
||||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
|
||||||
if (data) {
|
|
||||||
// When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
|
|
||||||
// select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
|
|
||||||
dispatch(imageSelected(null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
|
||||||
|
|
||||||
imageDTOs.forEach((imageDTO) => {
|
|
||||||
deleteNodesImages(state, dispatch, imageDTO);
|
|
||||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
|
||||||
deleteReferenceImages(state, dispatch, imageDTO);
|
|
||||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
} finally {
|
|
||||||
dispatch(isModalOpenChanged(false));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// When we list images, if no images is selected, select the first one.
|
|
||||||
startAppListening({
|
|
||||||
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
|
||||||
effect: (action, { dispatch, getState }) => {
|
|
||||||
const selection = getState().gallery.selection;
|
|
||||||
if (selection.length === 0) {
|
|
||||||
dispatch(imageSelected(action.payload.items[0] ?? null));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
startAppListening({
|
|
||||||
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
|
|
||||||
effect: (action) => {
|
|
||||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
startAppListening({
|
|
||||||
matcher: imagesApi.endpoints.deleteImage.matchRejected,
|
|
||||||
effect: (action) => {
|
|
||||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
|
||||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
|
||||||
import { selectImageUsage } from 'features/deleteImageModal/store/selectors';
|
|
||||||
import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
|
||||||
|
|
||||||
export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
|
|
||||||
startAppListening({
|
|
||||||
actionCreator: imagesToDeleteSelected,
|
|
||||||
effect: (action, { dispatch, getState }) => {
|
|
||||||
const imageDTOs = action.payload;
|
|
||||||
const state = getState();
|
|
||||||
const { shouldConfirmOnDelete } = state.system;
|
|
||||||
const imagesUsage = selectImageUsage(getState());
|
|
||||||
|
|
||||||
const isImageInUse =
|
|
||||||
imagesUsage.some((i) => i.isRasterLayerImage) ||
|
|
||||||
imagesUsage.some((i) => i.isControlLayerImage) ||
|
|
||||||
imagesUsage.some((i) => i.isReferenceImage) ||
|
|
||||||
imagesUsage.some((i) => i.isInpaintMaskImage) ||
|
|
||||||
imagesUsage.some((i) => i.isUpscaleImage) ||
|
|
||||||
imagesUsage.some((i) => i.isNodesImage) ||
|
|
||||||
imagesUsage.some((i) => i.isRegionalGuidanceImage);
|
|
||||||
|
|
||||||
if (shouldConfirmOnDelete || isImageInUse) {
|
|
||||||
dispatch(isModalOpenChanged(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(imageDeletionConfirmed({ imageDTOs, imagesUsage }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
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 type { AppDispatch, RootState } from 'app/store/store';
|
import type { AppDispatch, RootState } from 'app/store/store';
|
||||||
import {
|
import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
|
||||||
controlLayerModelChanged,
|
|
||||||
referenceImageIPAdapterModelChanged,
|
|
||||||
rgIPAdapterModelChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||||
import {
|
import {
|
||||||
clipEmbedModelSelected,
|
clipEmbedModelSelected,
|
||||||
@@ -15,8 +11,9 @@ import {
|
|||||||
t5EncoderModelSelected,
|
t5EncoderModelSelected,
|
||||||
vaeSelected,
|
vaeSelected,
|
||||||
} from 'features/controlLayers/store/paramsSlice';
|
} from 'features/controlLayers/store/paramsSlice';
|
||||||
|
import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
import { getEntityIdentifier, isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||||
import { modelSelected } from 'features/parameters/store/actions';
|
import { modelSelected } from 'features/parameters/store/actions';
|
||||||
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
||||||
import {
|
import {
|
||||||
@@ -210,12 +207,12 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log)
|
|||||||
|
|
||||||
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
||||||
const ipaModels = models.filter(isIPAdapterModelConfig);
|
const ipaModels = models.filter(isIPAdapterModelConfig);
|
||||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
selectRefImagesSlice(state).entities.forEach((entity) => {
|
||||||
if (entity.ipAdapter.type !== 'ip_adapter') {
|
if (!isIPAdapterConfig(entity.config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIPAdapterModel = entity.ipAdapter.model;
|
const selectedIPAdapterModel = entity.config.model;
|
||||||
// `null` is a valid IP adapter model - no need to do anything.
|
// `null` is a valid IP adapter model - no need to do anything.
|
||||||
if (!selectedIPAdapterModel) {
|
if (!selectedIPAdapterModel) {
|
||||||
return;
|
return;
|
||||||
@@ -225,16 +222,16 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
|
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
|
||||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
|
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
|
||||||
});
|
});
|
||||||
|
|
||||||
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
||||||
entity.referenceImages.forEach(({ id: referenceImageId, ipAdapter }) => {
|
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
|
||||||
if (ipAdapter.type !== 'ip_adapter') {
|
if (!isIPAdapterConfig(config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIPAdapterModel = ipAdapter.model;
|
const selectedIPAdapterModel = config.model;
|
||||||
// `null` is a valid IP adapter model - no need to do anything.
|
// `null` is a valid IP adapter model - no need to do anything.
|
||||||
if (!selectedIPAdapterModel) {
|
if (!selectedIPAdapterModel) {
|
||||||
return;
|
return;
|
||||||
@@ -245,7 +242,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
}
|
}
|
||||||
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
|
log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing');
|
||||||
dispatch(
|
dispatch(
|
||||||
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
|
rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -254,11 +251,11 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
|
const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
|
||||||
const fluxReduxModels = models.filter(isFluxReduxModelConfig);
|
const fluxReduxModels = models.filter(isFluxReduxModelConfig);
|
||||||
|
|
||||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
selectRefImagesSlice(state).entities.forEach((entity) => {
|
||||||
if (entity.ipAdapter.type !== 'flux_redux') {
|
if (!isFLUXReduxConfig(entity.config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedFLUXReduxModel = entity.ipAdapter.model;
|
const selectedFLUXReduxModel = entity.config.model;
|
||||||
// `null` is a valid FLUX Redux model - no need to do anything.
|
// `null` is a valid FLUX Redux model - no need to do anything.
|
||||||
if (!selectedFLUXReduxModel) {
|
if (!selectedFLUXReduxModel) {
|
||||||
return;
|
return;
|
||||||
@@ -268,16 +265,16 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
|
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
|
||||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
|
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
|
||||||
});
|
});
|
||||||
|
|
||||||
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
||||||
entity.referenceImages.forEach(({ id: referenceImageId, ipAdapter }) => {
|
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
|
||||||
if (ipAdapter.type !== 'flux_redux') {
|
if (!isFLUXReduxConfig(config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedFLUXReduxModel = ipAdapter.model;
|
const selectedFLUXReduxModel = config.model;
|
||||||
// `null` is a valid FLUX Redux model - no need to do anything.
|
// `null` is a valid FLUX Redux model - no need to do anything.
|
||||||
if (!selectedFLUXReduxModel) {
|
if (!selectedFLUXReduxModel) {
|
||||||
return;
|
return;
|
||||||
@@ -288,7 +285,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
}
|
}
|
||||||
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
|
log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing');
|
||||||
dispatch(
|
dispatch(
|
||||||
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
|
rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
||||||
|
import { atom, computed } from 'nanostores';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
|
||||||
|
export const $isLayoutLoading = atom(false);
|
||||||
|
export const setIsLayoutLoading = (isLoading: boolean) => {
|
||||||
|
flushSync(() => {
|
||||||
|
$isLayoutLoading.set(isLoading);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const $globalIsLoading = computed([$didStudioInit, $isLayoutLoading], (didStudioInit, isLayoutLoading) => {
|
||||||
|
return !didStudioInit || isLayoutLoading;
|
||||||
|
});
|
||||||
@@ -8,12 +8,12 @@ import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
|
|||||||
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
||||||
import {
|
import {
|
||||||
|
canvasSessionSlice,
|
||||||
canvasStagingAreaPersistConfig,
|
canvasStagingAreaPersistConfig,
|
||||||
canvasStagingAreaSlice,
|
|
||||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
||||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
import { refImagesPersistConfig, refImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||||
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
||||||
@@ -54,7 +54,6 @@ const allReducers = {
|
|||||||
[configSlice.name]: configSlice.reducer,
|
[configSlice.name]: configSlice.reducer,
|
||||||
[uiSlice.name]: uiSlice.reducer,
|
[uiSlice.name]: uiSlice.reducer,
|
||||||
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
|
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
|
||||||
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
|
|
||||||
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
|
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
|
||||||
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
|
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
|
||||||
[queueSlice.name]: queueSlice.reducer,
|
[queueSlice.name]: queueSlice.reducer,
|
||||||
@@ -65,9 +64,10 @@ const allReducers = {
|
|||||||
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||||
[paramsSlice.name]: paramsSlice.reducer,
|
[paramsSlice.name]: paramsSlice.reducer,
|
||||||
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
|
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
|
||||||
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
|
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
|
||||||
[lorasSlice.name]: lorasSlice.reducer,
|
[lorasSlice.name]: lorasSlice.reducer,
|
||||||
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
|
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
|
||||||
|
[refImagesSlice.name]: refImagesSlice.reducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootReducer = combineReducers(allReducers);
|
const rootReducer = combineReducers(allReducers);
|
||||||
@@ -113,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
|
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
|
||||||
[lorasPersistConfig.name]: lorasPersistConfig,
|
[lorasPersistConfig.name]: lorasPersistConfig,
|
||||||
[workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig,
|
[workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig,
|
||||||
|
[refImagesSlice.name]: refImagesPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
@@ -175,6 +176,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
|||||||
.concat(api.middleware)
|
.concat(api.middleware)
|
||||||
.concat(dynamicMiddlewares)
|
.concat(dynamicMiddlewares)
|
||||||
.concat(authToastMiddleware)
|
.concat(authToastMiddleware)
|
||||||
|
// .concat(getDebugLoggerMiddleware())
|
||||||
.prepend(listenerMiddleware.middleware),
|
.prepend(listenerMiddleware.middleware),
|
||||||
enhancers: (getDefaultEnhancers) => {
|
enhancers: (getDefaultEnhancers) => {
|
||||||
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
|
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
|
||||||
@@ -209,3 +211,4 @@ export type RootState = ReturnType<AppStore['getState']>;
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
|
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
|
||||||
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];
|
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];
|
||||||
|
export type AppGetState = ReturnType<typeof createStore>['getState'];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const Loading = () => {
|
|||||||
right={0}
|
right={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
left={0}
|
left={0}
|
||||||
|
zIndex={99999}
|
||||||
>
|
>
|
||||||
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
|
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
|
||||||
<Spinner
|
<Spinner
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import { memo, useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
type Props = PropsWithChildren & {
|
type Props = PropsWithChildren & {
|
||||||
maxHeight?: ChakraProps['maxHeight'];
|
maxHeight?: ChakraProps['maxHeight'];
|
||||||
|
maxWidth?: ChakraProps['maxWidth'];
|
||||||
overflowX?: 'hidden' | 'scroll';
|
overflowX?: 'hidden' | 'scroll';
|
||||||
overflowY?: 'hidden' | 'scroll';
|
overflowY?: 'hidden' | 'scroll';
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles: CSSProperties = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
|
const styles: CSSProperties = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
|
||||||
|
|
||||||
const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflowY = 'scroll' }: Props) => {
|
const ScrollableContent = ({ children, maxHeight, maxWidth, overflowX = 'hidden', overflowY = 'scroll' }: Props) => {
|
||||||
const overlayscrollbarsOptions = useMemo(
|
const overlayscrollbarsOptions = useMemo(
|
||||||
() => getOverlayScrollbarsParams({ overflowX, overflowY }).options,
|
() => getOverlayScrollbarsParams({ overflowX, overflowY }).options,
|
||||||
[overflowX, overflowY]
|
[overflowX, overflowY]
|
||||||
@@ -44,7 +45,7 @@ const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflow
|
|||||||
}, [os]);
|
}, [os]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" maxHeight={maxHeight} position="relative">
|
<Flex w="full" h="full" maxHeight={maxHeight} maxWidth={maxWidth} position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||||
<OverlayScrollbarsComponent ref={osRef} style={styles} options={overlayscrollbarsOptions}>
|
<OverlayScrollbarsComponent ref={osRef} style={styles} options={overlayscrollbarsOptions}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const useBoolean = (initialValue: boolean): UseBoolean => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseDisclosure = {
|
export type UseDisclosure = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||||
|
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||||
|
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||||
|
import { useTimeoutCallback } from 'common/hooks/useTimeoutCallback';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useCallbackOnDragEnter = (cb: () => void, ref: RefObject<HTMLElement>, delay = 300) => {
|
||||||
|
const [run, cancel] = useTimeoutCallback(cb, delay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return combine(
|
||||||
|
dropTargetForElements({
|
||||||
|
element,
|
||||||
|
onDragEnter: run,
|
||||||
|
onDragLeave: cancel,
|
||||||
|
}),
|
||||||
|
dropTargetForExternal({
|
||||||
|
element,
|
||||||
|
onDragEnter: run,
|
||||||
|
onDragLeave: cancel,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [cancel, ref, run]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
// Chakra tooltips sometimes open during a drag operation. We can fix it by dispatching an event that chakra listens
|
||||||
|
// for to close tooltips. It's reaching into the internals but it seems to work.
|
||||||
|
|
||||||
|
const closeEventName = 'chakra-ui:close-tooltip';
|
||||||
|
|
||||||
|
export const useCloseChakraTooltipsOnDragFix = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const closeTooltips = () => {
|
||||||
|
document.dispatchEvent(new window.CustomEvent(closeEventName));
|
||||||
|
};
|
||||||
|
document.addEventListener('drag', closeTooltips);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('drag', closeTooltips);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from https://github.com/chakra-ui/chakra-ui/blob/v2/packages/hooks/src/use-outside-click.ts
|
||||||
|
*
|
||||||
|
* The main change here is to support filtering of outside clicks via a `filter` function.
|
||||||
|
*
|
||||||
|
* This lets us work around issues with portals and components like popovers, which typically close on an outside click.
|
||||||
|
*
|
||||||
|
* For example, consider a popover that has a custom drop-down component inside it, which uses a portal to render
|
||||||
|
* the drop-down options. The original outside click handler would close the popover when clicking on the drop-down options,
|
||||||
|
* because the click is outside the popover - but we expect the popover to stay open in this case.
|
||||||
|
*
|
||||||
|
* A filter function like this can fix that:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const filter = (el: HTMLElement) => el.className.includes('chakra-portal') || el.id.includes('react-select')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This ignores clicks on react-select-based drop-downs and Chakra UI portals and is used as the default filter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
type FilterFunction = (el: HTMLElement | SVGElement) => boolean;
|
||||||
|
|
||||||
|
export function useCallbackRef<T extends (...args: any[]) => any>(
|
||||||
|
callback: T | undefined,
|
||||||
|
deps: React.DependencyList = []
|
||||||
|
) {
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
return useCallback(((...args) => callbackRef.current?.(...args)) as T, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseOutsideClickProps {
|
||||||
|
/**
|
||||||
|
* Whether the hook is enabled
|
||||||
|
*/
|
||||||
|
enabled?: boolean;
|
||||||
|
/**
|
||||||
|
* The reference to a DOM element.
|
||||||
|
*/
|
||||||
|
ref: React.RefObject<HTMLElement | null>;
|
||||||
|
/**
|
||||||
|
* Function invoked when a click is triggered outside the referenced element.
|
||||||
|
*/
|
||||||
|
handler?: (e: Event) => void;
|
||||||
|
/**
|
||||||
|
* A function that filters the elements that should be considered as outside clicks.
|
||||||
|
*
|
||||||
|
* If omitted, a default filter function that ignores clicks in Chakra UI portals and react-select components is used.
|
||||||
|
*/
|
||||||
|
filter?: FilterFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_FILTER: FilterFunction = (el) => {
|
||||||
|
if (el instanceof SVGElement) {
|
||||||
|
// SVGElement's type appears to be incorrect. Its className is not a string, which causes `includes` to fail.
|
||||||
|
// Let's assume that SVG elements with a class name are not part of the portal and should not be filtered.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.className.includes('chakra-portal') || el.id.includes('react-select');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example, used in components like Dialogs and Popovers, so they can close
|
||||||
|
* when a user clicks outside them.
|
||||||
|
*/
|
||||||
|
export function useFilterableOutsideClick(props: UseOutsideClickProps) {
|
||||||
|
const { ref, handler, enabled = true, filter = DEFAULT_FILTER } = props;
|
||||||
|
const savedHandler = useCallbackRef(handler);
|
||||||
|
|
||||||
|
const stateRef = useRef({
|
||||||
|
isPointerDown: false,
|
||||||
|
ignoreEmulatedMouseEvents: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = stateRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onPointerDown: any = (e: PointerEvent) => {
|
||||||
|
if (isValidEvent(e, ref, filter)) {
|
||||||
|
state.isPointerDown = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp: any = (event: MouseEvent) => {
|
||||||
|
if (state.ignoreEmulatedMouseEvents) {
|
||||||
|
state.ignoreEmulatedMouseEvents = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isPointerDown && handler && isValidEvent(event, ref)) {
|
||||||
|
state.isPointerDown = false;
|
||||||
|
savedHandler(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = (event: TouchEvent) => {
|
||||||
|
state.ignoreEmulatedMouseEvents = true;
|
||||||
|
if (handler && state.isPointerDown && isValidEvent(event, ref)) {
|
||||||
|
state.isPointerDown = false;
|
||||||
|
savedHandler(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doc = getOwnerDocument(ref.current);
|
||||||
|
doc.addEventListener('mousedown', onPointerDown, true);
|
||||||
|
doc.addEventListener('mouseup', onMouseUp, true);
|
||||||
|
doc.addEventListener('touchstart', onPointerDown, true);
|
||||||
|
doc.addEventListener('touchend', onTouchEnd, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
doc.removeEventListener('mousedown', onPointerDown, true);
|
||||||
|
doc.removeEventListener('mouseup', onMouseUp, true);
|
||||||
|
doc.removeEventListener('touchstart', onPointerDown, true);
|
||||||
|
doc.removeEventListener('touchend', onTouchEnd, true);
|
||||||
|
};
|
||||||
|
}, [handler, ref, savedHandler, state, enabled, filter]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEvent(event: Event, ref: React.RefObject<HTMLElement | null>, filter?: FilterFunction): boolean {
|
||||||
|
const target = (event.composedPath?.()[0] ?? event.target) as HTMLElement;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
const doc = getOwnerDocument(target);
|
||||||
|
if (!doc.contains(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.current?.contains(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main logic change from the original hook.
|
||||||
|
if (filter) {
|
||||||
|
// Check if the click is inside an element matching the filter.
|
||||||
|
// This is used for portal-awareness or other general exclusion cases.
|
||||||
|
let currentElement: HTMLElement | null = target;
|
||||||
|
// Traverse up the DOM tree from the target element.
|
||||||
|
while (currentElement && currentElement !== document.body) {
|
||||||
|
if (filter(currentElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the click is not inside the ref and not inside a portal, it's a valid outside click.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnerDocument(node?: Element | null): Document {
|
||||||
|
return node?.ownerDocument ?? document;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
|
||||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||||
|
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
|
||||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
@@ -35,34 +35,39 @@ export const useGlobalHotkeys = () => {
|
|||||||
dependencies: [queue],
|
dependencies: [queue],
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
|
||||||
cancelQueueItem,
|
|
||||||
isDisabled: isDisabledCancelQueueItem,
|
|
||||||
isLoading: isLoadingCancelQueueItem,
|
|
||||||
} = useCancelCurrentQueueItem();
|
|
||||||
|
|
||||||
useRegisteredHotkeys({
|
useRegisteredHotkeys({
|
||||||
id: 'cancelQueueItem',
|
id: 'cancelQueueItem',
|
||||||
category: 'app',
|
category: 'app',
|
||||||
callback: cancelQueueItem,
|
callback: deleteCurrentQueueItem.trigger,
|
||||||
options: {
|
options: {
|
||||||
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
dependencies: [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem],
|
dependencies: [deleteCurrentQueueItem],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { clearQueue, isDisabled: isDisabledClearQueue, isLoading: isLoadingClearQueue } = useClearQueue();
|
const clearQueue = useClearQueue();
|
||||||
|
|
||||||
useRegisteredHotkeys({
|
useRegisteredHotkeys({
|
||||||
id: 'clearQueue',
|
id: 'clearQueue',
|
||||||
category: 'app',
|
category: 'app',
|
||||||
callback: clearQueue,
|
callback: clearQueue.trigger,
|
||||||
options: {
|
options: {
|
||||||
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
|
enabled: !clearQueue.isDisabled && !clearQueue.isLoading,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
dependencies: [clearQueue, isDisabledClearQueue, isLoadingClearQueue],
|
dependencies: [clearQueue],
|
||||||
|
});
|
||||||
|
|
||||||
|
useRegisteredHotkeys({
|
||||||
|
id: 'selectGenerateTab',
|
||||||
|
category: 'app',
|
||||||
|
callback: () => {
|
||||||
|
dispatch(setActiveTab('generate'));
|
||||||
|
},
|
||||||
|
dependencies: [dispatch],
|
||||||
});
|
});
|
||||||
|
|
||||||
useRegisteredHotkeys({
|
useRegisteredHotkeys({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { Button, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||||
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import type { FileRejection } from 'react-dropzone';
|
import type { FileRejection } from 'react-dropzone';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -163,7 +163,8 @@ const sx = {
|
|||||||
},
|
},
|
||||||
} satisfies SystemStyleObject;
|
} satisfies SystemStyleObject;
|
||||||
|
|
||||||
export const UploadImageButton = ({
|
export const UploadImageIconButton = memo(
|
||||||
|
({
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
onUpload,
|
onUpload,
|
||||||
isError = false,
|
isError = false,
|
||||||
@@ -188,7 +189,37 @@ export const UploadImageButton = ({
|
|||||||
<input {...uploadApi.getUploadInputProps()} />
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
UploadImageIconButton.displayName = 'UploadImageIconButton';
|
||||||
|
|
||||||
|
type UploadImageButtonProps = {
|
||||||
|
onUpload?: (imageDTO: ImageDTO) => void;
|
||||||
|
isError?: boolean;
|
||||||
|
} & ButtonProps;
|
||||||
|
|
||||||
|
const UploadImageButton = memo((props: UploadImageButtonProps) => {
|
||||||
|
const { children, isDisabled = false, onUpload, isError = false, ...rest } = props;
|
||||||
|
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
aria-label="Upload image"
|
||||||
|
variant="outline"
|
||||||
|
sx={sx}
|
||||||
|
data-error={isError}
|
||||||
|
rightIcon={<PiUploadBold />}
|
||||||
|
isLoading={uploadApi.request.isLoading}
|
||||||
|
{...rest}
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
>
|
||||||
|
{children ?? 'Upload'}
|
||||||
|
</Button>
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
UploadImageButton.displayName = 'UploadImageButton';
|
||||||
|
|
||||||
export const UploadMultipleImageButton = ({
|
export const UploadMultipleImageButton = ({
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||||
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import type { GroupBase } from 'chakra-react-select';
|
import type { GroupBase } from 'chakra-react-select';
|
||||||
|
import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
|
||||||
|
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||||
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
|
||||||
import type { AnyModelConfig } from 'services/api/types';
|
import type { AnyModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
import { useGroupedModelCombobox } from './useGroupedModelCombobox';
|
import { useGroupedModelCombobox } from './useGroupedModelCombobox';
|
||||||
import { useRelatedModelKeys } from './useRelatedModelKeys';
|
|
||||||
import { useSelectedModelKeys } from './useSelectedModelKeys';
|
|
||||||
|
|
||||||
type UseRelatedGroupedModelComboboxArg<T extends AnyModelConfig> = {
|
type UseRelatedGroupedModelComboboxArg<T extends AnyModelConfig> = {
|
||||||
modelConfigs: T[];
|
modelConfigs: T[];
|
||||||
@@ -29,6 +35,32 @@ type UseRelatedGroupedModelComboboxReturn = {
|
|||||||
noOptionsMessage: () => string;
|
noOptionsMessage: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => {
|
||||||
|
const keys: string[] = [];
|
||||||
|
const main = params.model;
|
||||||
|
const vae = params.vae;
|
||||||
|
const refiner = params.refinerModel;
|
||||||
|
const controlnet = params.controlLora;
|
||||||
|
|
||||||
|
if (main) {
|
||||||
|
keys.push(main.key);
|
||||||
|
}
|
||||||
|
if (vae) {
|
||||||
|
keys.push(vae.key);
|
||||||
|
}
|
||||||
|
if (refiner) {
|
||||||
|
keys.push(refiner.key);
|
||||||
|
}
|
||||||
|
if (controlnet) {
|
||||||
|
keys.push(controlnet.key);
|
||||||
|
}
|
||||||
|
for (const { model } of loras.loras) {
|
||||||
|
keys.push(model.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniq(keys);
|
||||||
|
});
|
||||||
|
|
||||||
export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
||||||
modelConfigs,
|
modelConfigs,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
@@ -39,9 +71,15 @@ export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
|||||||
}: UseRelatedGroupedModelComboboxArg<T>): UseRelatedGroupedModelComboboxReturn {
|
}: UseRelatedGroupedModelComboboxArg<T>): UseRelatedGroupedModelComboboxReturn {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const selectedKeys = useSelectedModelKeys();
|
const selectedKeys = useAppSelector(selectSelectedModelKeys);
|
||||||
|
const { relatedKeys } = useGetRelatedModelIdsBatchQuery(selectedKeys, {
|
||||||
const relatedKeys = useRelatedModelKeys(selectedKeys);
|
selectFromResult: ({ data }) => {
|
||||||
|
if (!data) {
|
||||||
|
return { relatedKeys: EMPTY_ARRAY };
|
||||||
|
}
|
||||||
|
return { relatedKeys: data };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Base grouped options
|
// Base grouped options
|
||||||
const base = useGroupedModelCombobox({
|
const base = useGroupedModelCombobox({
|
||||||
@@ -53,9 +91,9 @@ export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
|||||||
groupByType,
|
groupByType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no related models selected, just return base
|
const options = useMemo(() => {
|
||||||
if (relatedKeys.size === 0) {
|
if (relatedKeys.length === 0) {
|
||||||
return base;
|
return base.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOptions: ComboboxOption[] = [];
|
const relatedOptions: ComboboxOption[] = [];
|
||||||
@@ -65,7 +103,7 @@ export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
|||||||
const remainingOptions: ComboboxOption[] = [];
|
const remainingOptions: ComboboxOption[] = [];
|
||||||
|
|
||||||
for (const option of group.options) {
|
for (const option of group.options) {
|
||||||
if (relatedKeys.has(option.value)) {
|
if (relatedKeys.includes(option.value)) {
|
||||||
relatedOptions.push({ ...option, label: `* ${option.label}` });
|
relatedOptions.push({ ...option, label: `* ${option.label}` });
|
||||||
} else {
|
} else {
|
||||||
remainingOptions.push(option);
|
remainingOptions.push(option);
|
||||||
@@ -80,13 +118,15 @@ export function useRelatedGroupedModelCombobox<T extends AnyModelConfig>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalOptions: GroupBase<ComboboxOption>[] =
|
if (relatedOptions.length > 0) {
|
||||||
relatedOptions.length > 0
|
return [{ label: t('modelManager.relatedModels'), options: relatedOptions }, ...updatedGroups];
|
||||||
? [{ label: t('modelManager.relatedModels'), options: relatedOptions }, ...updatedGroups]
|
} else {
|
||||||
: updatedGroups;
|
return updatedGroups;
|
||||||
|
}
|
||||||
|
}, [base.options, relatedKeys, t]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
options: finalOptions,
|
options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches related model keys for a given set of selected model keys.
|
|
||||||
* Returns a Set<string> for fast lookup.
|
|
||||||
*/
|
|
||||||
export const useRelatedModelKeys = (selectedKeys: Set<string>) => {
|
|
||||||
const { data: related = [] } = useGetRelatedModelIdsBatchQuery([...selectedKeys], {
|
|
||||||
skip: selectedKeys.size === 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
return useMemo(() => new Set(related), [related]);
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gathers all currently selected model keys from parameters and loras.
|
|
||||||
* This includes the main model, VAE, refiner model, controlnet, and loras.
|
|
||||||
*/
|
|
||||||
export const useSelectedModelKeys = () => {
|
|
||||||
return useAppSelector((state) => {
|
|
||||||
const keys = new Set<string>();
|
|
||||||
const main = state.params.model;
|
|
||||||
const vae = state.params.vae;
|
|
||||||
const refiner = state.params.refinerModel;
|
|
||||||
const controlnet = state.params.controlLora;
|
|
||||||
const loras = state.loras.loras.map((l) => l.model);
|
|
||||||
|
|
||||||
if (main) {
|
|
||||||
keys.add(main.key);
|
|
||||||
}
|
|
||||||
if (vae) {
|
|
||||||
keys.add(vae.key);
|
|
||||||
}
|
|
||||||
if (refiner) {
|
|
||||||
keys.add(refiner.key);
|
|
||||||
}
|
|
||||||
if (controlnet) {
|
|
||||||
keys.add(controlnet.key);
|
|
||||||
}
|
|
||||||
for (const lora of loras) {
|
|
||||||
keys.add(lora.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
21
invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts
Normal file
21
invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useTimeoutCallback = (callback: () => void, delay: number, onCancel?: () => void) => {
|
||||||
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
}, [onCancel]);
|
||||||
|
const callWithTimeout = useCallback(() => {
|
||||||
|
cancel();
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}, delay);
|
||||||
|
}, [callback, cancel, delay]);
|
||||||
|
const api = useMemo(() => [callWithTimeout, cancel] as const, [callWithTimeout, cancel]);
|
||||||
|
return api;
|
||||||
|
};
|
||||||
10
invokeai/frontend/web/src/common/util/zodUtils.ts
Normal file
10
invokeai/frontend/web/src/common/util/zodUtils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a type guard from a zod schema. The type guard will infer the schema's TS type.
|
||||||
|
* @param schema The zod schema to create a type guard from.
|
||||||
|
* @returns A type guard function for the schema.
|
||||||
|
*/
|
||||||
|
export const buildZodTypeGuard = <T extends z.ZodTypeAny>(schema: T) => {
|
||||||
|
return (val: unknown): val is z.infer<T> => schema.safeParse(val).success;
|
||||||
|
};
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||||
|
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||||
|
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||||
|
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||||
|
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
|
||||||
|
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
|
||||||
|
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||||
|
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||||
|
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||||
|
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||||
|
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
||||||
|
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
|
||||||
|
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||||
|
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||||
|
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||||
|
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||||
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
|
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||||
|
width: 'full',
|
||||||
|
height: 'full',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuContent = memo(() => {
|
||||||
|
return (
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<MenuList>
|
||||||
|
<CanvasContextMenuSelectedEntityMenuItems />
|
||||||
|
<CanvasContextMenuGlobalMenuItems />
|
||||||
|
</MenuList>
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MenuContent.displayName = 'MenuContent';
|
||||||
|
|
||||||
|
const canvasBgSx = {
|
||||||
|
position: 'relative',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
borderRadius: 'base',
|
||||||
|
overflow: 'hidden',
|
||||||
|
bg: 'base.900',
|
||||||
|
'&[data-dynamic-grid="true"]': {
|
||||||
|
bg: 'base.850',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdvancedSession = memo(({ id }: { id: string | null }) => {
|
||||||
|
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||||
|
const showHUD = useAppSelector(selectShowHUD);
|
||||||
|
|
||||||
|
const renderMenu = useCallback(() => {
|
||||||
|
return <MenuContent />;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs w="full" h="full">
|
||||||
|
<TabList>
|
||||||
|
<Tab>Welcome</Tab>
|
||||||
|
<Tab>Workspace</Tab>
|
||||||
|
<Tab>Viewer</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels w="full" h="full">
|
||||||
|
<TabPanel w="full" h="full" justifyContent="center">
|
||||||
|
<GenerateLaunchpadPanel />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel w="full" h="full">
|
||||||
|
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
|
||||||
|
<Flex
|
||||||
|
tabIndex={-1}
|
||||||
|
borderRadius="base"
|
||||||
|
position="relative"
|
||||||
|
flexDirection="column"
|
||||||
|
height="full"
|
||||||
|
width="full"
|
||||||
|
gap={2}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<CanvasToolbar />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
<Divider />
|
||||||
|
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
||||||
|
{(ref) => (
|
||||||
|
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
|
||||||
|
<InvokeCanvasComponent />
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
flexDir="column"
|
||||||
|
top={1}
|
||||||
|
insetInlineStart={1}
|
||||||
|
pointerEvents="none"
|
||||||
|
gap={2}
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
{showHUD && <CanvasHUD />}
|
||||||
|
<CanvasAlertsSelectedEntityStatus />
|
||||||
|
<CanvasAlertsPreserveMask />
|
||||||
|
<CanvasAlertsInvocationProgress />
|
||||||
|
</Flex>
|
||||||
|
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
||||||
|
<MenuContent />
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
|
{id !== null && (
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<CanvasSessionContextProvider type="advanced" id={id}>
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
flexDir="column"
|
||||||
|
bottom={4}
|
||||||
|
gap={2}
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
left={4}
|
||||||
|
right={4}
|
||||||
|
>
|
||||||
|
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||||
|
<StagingAreaItemsList />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={2}>
|
||||||
|
<StagingAreaToolbar />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</CanvasSessionContextProvider>
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
)}
|
||||||
|
<Flex position="absolute" bottom={4}>
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<Filter />
|
||||||
|
<Transform />
|
||||||
|
<SelectObject />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
</Flex>
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<CanvasDropArea />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
</Flex>
|
||||||
|
</FocusRegionWrapper>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel w="full" h="full">
|
||||||
|
<Flex flexDir="column" w="full" h="full">
|
||||||
|
<ViewerToolbar />
|
||||||
|
<ImageViewer />
|
||||||
|
</Flex>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
AdvancedSession.displayName = 'AdvancedSession';
|
||||||
@@ -2,11 +2,10 @@ import { Button, Flex, Heading } from '@invoke-ai/ui-library';
|
|||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import {
|
import {
|
||||||
useAddControlLayer,
|
useAddControlLayer,
|
||||||
useAddGlobalReferenceImage,
|
|
||||||
useAddInpaintMask,
|
useAddInpaintMask,
|
||||||
|
useAddNewRegionalGuidanceWithARefImage,
|
||||||
useAddRasterLayer,
|
useAddRasterLayer,
|
||||||
useAddRegionalGuidance,
|
useAddRegionalGuidance,
|
||||||
useAddRegionalReferenceImage,
|
|
||||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
|
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -19,9 +18,7 @@ export const CanvasAddEntityButtons = memo(() => {
|
|||||||
const addRegionalGuidance = useAddRegionalGuidance();
|
const addRegionalGuidance = useAddRegionalGuidance();
|
||||||
const addRasterLayer = useAddRasterLayer();
|
const addRasterLayer = useAddRasterLayer();
|
||||||
const addControlLayer = useAddControlLayer();
|
const addControlLayer = useAddControlLayer();
|
||||||
const addGlobalReferenceImage = useAddGlobalReferenceImage();
|
const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage();
|
||||||
const addRegionalReferenceImage = useAddRegionalReferenceImage();
|
|
||||||
const isReferenceImageEnabled = useIsEntityTypeEnabled('reference_image');
|
|
||||||
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
|
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
|
||||||
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
|
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
|
||||||
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
|
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
|
||||||
@@ -29,21 +26,6 @@ export const CanvasAddEntityButtons = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" justifyContent="center" gap={4}>
|
<Flex w="full" h="full" justifyContent="center" gap={4}>
|
||||||
<Flex position="relative" flexDir="column" gap={4} top="20%">
|
<Flex position="relative" flexDir="column" gap={4} top="20%">
|
||||||
<Flex flexDir="column" justifyContent="flex-start" gap={2}>
|
|
||||||
<Heading size="xs">{t('controlLayers.global')}</Heading>
|
|
||||||
<InformationalPopover feature="globalReferenceImage">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
leftIcon={<PiPlusBold />}
|
|
||||||
onClick={addGlobalReferenceImage}
|
|
||||||
isDisabled={!isReferenceImageEnabled}
|
|
||||||
>
|
|
||||||
{t('controlLayers.globalReferenceImage')}
|
|
||||||
</Button>
|
|
||||||
</InformationalPopover>
|
|
||||||
</Flex>
|
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
<Heading size="xs">{t('controlLayers.regional')}</Heading>
|
<Heading size="xs">{t('controlLayers.regional')}</Heading>
|
||||||
<InformationalPopover feature="inpainting">
|
<InformationalPopover feature="inpainting">
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { selectIsLocal } from 'features/system/store/configSlice';
|
|||||||
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
|
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { $invocationProgressMessage } from 'services/events/stores';
|
import { $lastProgressMessage } from 'services/events/stores';
|
||||||
|
|
||||||
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
|
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const invocationProgressMessage = useStore($invocationProgressMessage);
|
const invocationProgressMessage = useStore($lastProgressMessage);
|
||||||
|
|
||||||
if (!invocationProgressMessage) {
|
if (!invocationProgressMessage) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, Flex } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useBoolean } from 'common/hooks/useBoolean';
|
|
||||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
|
|
||||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
|
||||||
import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const ActivateImageViewerButton = (props: PropsWithChildren) => {
|
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
imageViewer.open();
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
|
||||||
}, [imageViewer, dispatch]);
|
|
||||||
return (
|
|
||||||
<Button onClick={onClick} size="sm" variant="link" color="base.50">
|
|
||||||
{props.children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasAlertsSendingToGallery = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const destination = useCurrentDestination();
|
|
||||||
const tab = useAppSelector(selectActiveTab);
|
|
||||||
const isVisible = useMemo(() => {
|
|
||||||
// This alert should only be visible when the destination is gallery and the tab is canvas
|
|
||||||
if (tab !== 'canvas') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!destination) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return destination === 'gallery';
|
|
||||||
}, [destination, tab]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertWrapper
|
|
||||||
title={t('controlLayers.sendingToGallery')}
|
|
||||||
description={
|
|
||||||
<Trans i18nKey="controlLayers.viewProgressInViewer" components={{ Btn: <ActivateImageViewerButton /> }} />
|
|
||||||
}
|
|
||||||
isVisible={isVisible}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ActivateCanvasButton = (props: PropsWithChildren) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
dispatch(setActiveTab('canvas'));
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
|
||||||
imageViewer.close();
|
|
||||||
}, [dispatch, imageViewer]);
|
|
||||||
return (
|
|
||||||
<Button onClick={onClick} size="sm" variant="link" color="base.50">
|
|
||||||
{props.children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasAlertsSendingToCanvas = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const destination = useCurrentDestination();
|
|
||||||
const isStaging = useAppSelector(selectIsStaging);
|
|
||||||
const tab = useAppSelector(selectActiveTab);
|
|
||||||
const isVisible = useMemo(() => {
|
|
||||||
// When we are on a non-canvas tab, and the current generation's destination is not the canvas, we don't show the alert
|
|
||||||
// For example, on the workflows tab, when the destinatin is gallery, we don't show the alert
|
|
||||||
if (tab !== 'canvas' && destination !== 'canvas') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isStaging) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!destination) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return destination === 'canvas';
|
|
||||||
}, [destination, isStaging, tab]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertWrapper
|
|
||||||
title={t('controlLayers.sendingToCanvas')}
|
|
||||||
description={
|
|
||||||
<Trans i18nKey="controlLayers.viewProgressOnCanvas" components={{ Btn: <ActivateCanvasButton /> }} />
|
|
||||||
}
|
|
||||||
isVisible={isVisible}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AlertWrapper = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
isVisible,
|
|
||||||
}: {
|
|
||||||
title: ReactNode;
|
|
||||||
description: ReactNode;
|
|
||||||
isVisible: boolean;
|
|
||||||
}) => {
|
|
||||||
const isHovered = useBoolean(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{(isVisible || isHovered.isTrue) && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1, transition: { duration: 0.1, ease: 'easeOut' } }}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.1, delay: !isHovered.isTrue ? 1 : 0.1, ease: 'easeIn' },
|
|
||||||
}}
|
|
||||||
onMouseEnter={isHovered.setTrue}
|
|
||||||
onMouseLeave={isHovered.setFalse}
|
|
||||||
>
|
|
||||||
<Alert
|
|
||||||
status="warning"
|
|
||||||
flexDir="column"
|
|
||||||
pointerEvents="auto"
|
|
||||||
borderRadius="base"
|
|
||||||
fontSize="sm"
|
|
||||||
shadow="md"
|
|
||||||
w="fit-content"
|
|
||||||
>
|
|
||||||
<Flex w="full" alignItems="center">
|
|
||||||
<AlertIcon />
|
|
||||||
<AlertTitle>{title}</AlertTitle>
|
|
||||||
</Flex>
|
|
||||||
<AlertDescription>{description}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,8 +2,8 @@ import { MenuGroup } from '@invoke-ai/ui-library';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
|
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
|
||||||
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
|
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
|
||||||
import { IPAdapterMenuItems } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItems';
|
|
||||||
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
|
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
|
||||||
|
import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems';
|
||||||
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
|
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
|
||||||
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
|
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Grid, GridItem } from '@invoke-ai/ui-library';
|
|||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd';
|
import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -13,19 +12,11 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.
|
|||||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||||
type: 'regional_guidance_with_reference_image',
|
type: 'regional_guidance_with_reference_image',
|
||||||
});
|
});
|
||||||
const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
|
||||||
type: 'reference_image',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CanvasDropArea = memo(() => {
|
export const CanvasDropArea = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const isBusy = useCanvasIsBusy();
|
const isBusy = useCanvasIsBusy();
|
||||||
|
|
||||||
if (imageViewer.isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -63,14 +54,6 @@ export const CanvasDropArea = memo(() => {
|
|||||||
isDisabled={isBusy}
|
isDisabled={isBusy}
|
||||||
/>
|
/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem position="relative">
|
|
||||||
<DndDropTarget
|
|
||||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
|
||||||
dndTargetData={addGlobalReferenceImageFromImageDndTargetData}
|
|
||||||
label={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
|
|
||||||
isDisabled={isBusy}
|
|
||||||
/>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/
|
|||||||
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
||||||
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
|
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { isRenderableEntityType } from 'features/controlLayers/store/types';
|
|
||||||
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
|
import { singleCanvasEntityDndSource } from 'features/dnd/dnd';
|
||||||
import { triggerPostMoveFlash } from 'features/dnd/util';
|
import { triggerPostMoveFlash } from 'features/dnd/util';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
@@ -165,8 +164,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
|
|||||||
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</Flex>
|
</Flex>
|
||||||
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
|
<CanvasEntityMergeVisibleButton type={type} />
|
||||||
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
<CanvasEntityTypeIsHiddenToggle type={type} />
|
||||||
<CanvasEntityAddOfTypeButton type={type} />
|
<CanvasEntityAddOfTypeButton type={type} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Collapse in={collapse.isTrue} style={fixTooltipCloseOnScrollStyles}>
|
<Collapse in={collapse.isTrue} style={fixTooltipCloseOnScrollStyles}>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Flex } from '@invoke-ai/ui-library';
|
|||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
||||||
import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList';
|
import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList';
|
||||||
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
|
|
||||||
import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList';
|
import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList';
|
||||||
import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList';
|
import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -11,7 +10,6 @@ export const CanvasEntityList = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list" w="full" h="full">
|
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list" w="full" h="full">
|
||||||
<IPAdapterList />
|
|
||||||
<InpaintMaskList />
|
<InpaintMaskList />
|
||||||
<RegionalGuidanceEntityList />
|
<RegionalGuidanceEntityList />
|
||||||
<ControlLayerEntityList />
|
<ControlLayerEntityList />
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
import {
|
import {
|
||||||
useAddControlLayer,
|
useAddControlLayer,
|
||||||
useAddGlobalReferenceImage,
|
|
||||||
useAddInpaintMask,
|
useAddInpaintMask,
|
||||||
|
useAddNewRegionalGuidanceWithARefImage,
|
||||||
useAddRasterLayer,
|
useAddRasterLayer,
|
||||||
useAddRegionalGuidance,
|
useAddRegionalGuidance,
|
||||||
useAddRegionalReferenceImage,
|
|
||||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
|
import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled';
|
||||||
@@ -16,13 +15,11 @@ import { PiPlusBold } from 'react-icons/pi';
|
|||||||
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isBusy = useCanvasIsBusy();
|
const isBusy = useCanvasIsBusy();
|
||||||
const addGlobalReferenceImage = useAddGlobalReferenceImage();
|
|
||||||
const addInpaintMask = useAddInpaintMask();
|
const addInpaintMask = useAddInpaintMask();
|
||||||
const addRegionalGuidance = useAddRegionalGuidance();
|
const addRegionalGuidance = useAddRegionalGuidance();
|
||||||
const addRegionalReferenceImage = useAddRegionalReferenceImage();
|
const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage();
|
||||||
const addRasterLayer = useAddRasterLayer();
|
const addRasterLayer = useAddRasterLayer();
|
||||||
const addControlLayer = useAddControlLayer();
|
const addControlLayer = useAddControlLayer();
|
||||||
const isReferenceImageEnabled = useIsEntityTypeEnabled('reference_image');
|
|
||||||
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
|
const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance');
|
||||||
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
|
const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer');
|
||||||
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
|
const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask');
|
||||||
@@ -41,11 +38,6 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
|||||||
isDisabled={isBusy}
|
isDisabled={isBusy}
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuGroup title={t('controlLayers.global')}>
|
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addGlobalReferenceImage} isDisabled={!isReferenceImageEnabled}>
|
|
||||||
{t('controlLayers.globalReferenceImage')}
|
|
||||||
</MenuItem>
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuGroup title={t('controlLayers.regional')}>
|
<MenuGroup title={t('controlLayers.regional')}>
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask} isDisabled={!isInpaintLayerEnabled}>
|
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask} isDisabled={!isInpaintLayerEnabled}>
|
||||||
{t('controlLayers.inpaintMask')}
|
{t('controlLayers.inpaintMask')}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
selectEntity,
|
selectEntity,
|
||||||
selectSelectedEntityIdentifier,
|
selectSelectedEntityIdentifier,
|
||||||
} from 'features/controlLayers/store/selectors';
|
} from 'features/controlLayers/store/selectors';
|
||||||
import { isRenderableEntity } from 'features/controlLayers/store/types';
|
|
||||||
import { clamp, round } from 'lodash-es';
|
import { clamp, round } from 'lodash-es';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -70,9 +69,6 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
|
|||||||
if (!selectedEntity) {
|
if (!selectedEntity) {
|
||||||
return 1; // fallback to 100% opacity
|
return 1; // fallback to 100% opacity
|
||||||
}
|
}
|
||||||
if (!isRenderableEntity(selectedEntity)) {
|
|
||||||
return 1; // fallback to 100% opacity
|
|
||||||
}
|
|
||||||
// Opacity is a float from 0-1, but we want to display it as a percentage
|
// Opacity is a float from 0-1, but we want to display it as a percentage
|
||||||
return selectedEntity.opacity;
|
return selectedEntity.opacity;
|
||||||
});
|
});
|
||||||
@@ -134,11 +130,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<FormControl
|
<FormControl w="min-content" gap={2} isDisabled={selectedEntityIdentifier === null}>
|
||||||
w="min-content"
|
|
||||||
gap={2}
|
|
||||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
|
||||||
>
|
|
||||||
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -167,7 +159,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
insetInlineEnd={0}
|
insetInlineEnd={0}
|
||||||
h="full"
|
h="full"
|
||||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
isDisabled={selectedEntityIdentifier === null}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</NumberInput>
|
</NumberInput>
|
||||||
@@ -185,7 +177,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
|||||||
marks={marks}
|
marks={marks}
|
||||||
formatValue={formatSliderValue}
|
formatValue={formatSliderValue}
|
||||||
alwaysShowMarks
|
alwaysShowMarks
|
||||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
isDisabled={selectedEntityIdentifier === null}
|
||||||
/>
|
/>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library';
|
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
|
||||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||||
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
|
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
|
||||||
|
|
||||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
export const CanvasLayersPanel = memo(() => {
|
||||||
width: 'full',
|
|
||||||
height: 'full',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasLayersPanelContent = memo(() => {
|
|
||||||
const hasEntities = useAppSelector(selectHasEntities);
|
const hasEntities = useAppSelector(selectHasEntities);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusRegionWrapper region="layers" sx={FOCUS_REGION_STYLES}>
|
<CanvasManagerProviderGate>
|
||||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
<Flex flexDir="column" gap={2} w="full" h="full" p={2}>
|
||||||
<EntityListSelectedEntityActionBar />
|
<EntityListSelectedEntityActionBar />
|
||||||
<Divider py={0} />
|
<Divider py={0} />
|
||||||
<ParamDenoisingStrength />
|
<ParamDenoisingStrength />
|
||||||
@@ -27,8 +22,8 @@ export const CanvasLayersPanelContent = memo(() => {
|
|||||||
{!hasEntities && <CanvasAddEntityButtons />}
|
{!hasEntities && <CanvasAddEntityButtons />}
|
||||||
{hasEntities && <CanvasEntityList />}
|
{hasEntities && <CanvasEntityList />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</FocusRegionWrapper>
|
</CanvasManagerProviderGate>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasLayersPanelContent.displayName = 'CanvasLayersPanelContent';
|
CanvasLayersPanel.displayName = 'CanvasLayersPanel';
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
Flex,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuList,
|
|
||||||
type SystemStyleObject,
|
|
||||||
} from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
|
||||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
|
||||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
|
||||||
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
|
||||||
import { CanvasBusySpinner } from 'features/controlLayers/components/CanvasBusySpinner';
|
|
||||||
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
|
|
||||||
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
|
|
||||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
|
||||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
|
||||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
|
||||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
|
||||||
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
|
||||||
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
|
||||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
|
||||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
|
||||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
|
||||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
|
|
||||||
|
|
||||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
|
||||||
width: 'full',
|
|
||||||
height: 'full',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuContent = () => {
|
|
||||||
return (
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<MenuList>
|
|
||||||
<CanvasContextMenuSelectedEntityMenuItems />
|
|
||||||
<CanvasContextMenuGlobalMenuItems />
|
|
||||||
</MenuList>
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasMainPanelContent = memo(() => {
|
|
||||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
|
||||||
const showHUD = useAppSelector(selectShowHUD);
|
|
||||||
|
|
||||||
const renderMenu = useCallback(() => {
|
|
||||||
return <MenuContent />;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
|
|
||||||
<Flex
|
|
||||||
tabIndex={-1}
|
|
||||||
borderRadius="base"
|
|
||||||
position="relative"
|
|
||||||
flexDirection="column"
|
|
||||||
height="full"
|
|
||||||
width="full"
|
|
||||||
gap={2}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<CanvasToolbar />
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
|
||||||
{(ref) => (
|
|
||||||
<Flex
|
|
||||||
ref={ref}
|
|
||||||
position="relative"
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
bg={dynamicGrid ? 'base.850' : 'base.900'}
|
|
||||||
borderRadius="base"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<InvokeCanvasComponent />
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<Flex
|
|
||||||
position="absolute"
|
|
||||||
flexDir="column"
|
|
||||||
top={1}
|
|
||||||
insetInlineStart={1}
|
|
||||||
pointerEvents="none"
|
|
||||||
gap={2}
|
|
||||||
alignItems="flex-start"
|
|
||||||
>
|
|
||||||
{showHUD && <CanvasHUD />}
|
|
||||||
<CanvasAlertsSelectedEntityStatus />
|
|
||||||
<CanvasAlertsPreserveMask />
|
|
||||||
<CanvasAlertsSendingToGallery />
|
|
||||||
<CanvasAlertsInvocationProgress />
|
|
||||||
</Flex>
|
|
||||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
|
||||||
<MenuContent />
|
|
||||||
</Menu>
|
|
||||||
</Flex>
|
|
||||||
<Flex position="absolute" bottom={4} insetInlineEnd={4}>
|
|
||||||
<CanvasBusySpinner />
|
|
||||||
</Flex>
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</ContextMenu>
|
|
||||||
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<StagingAreaIsStagingGate>
|
|
||||||
<StagingAreaToolbar />
|
|
||||||
</StagingAreaIsStagingGate>
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
</Flex>
|
|
||||||
<Flex position="absolute" bottom={4}>
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<Filter />
|
|
||||||
<Transform />
|
|
||||||
<SelectObject />
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
</Flex>
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<CanvasDropArea />
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
<GatedImageViewer />
|
|
||||||
</Flex>
|
|
||||||
</FocusRegionWrapper>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
||||||
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
||||||
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
|
||||||
import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
|
||||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
|
||||||
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
|
||||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
|
||||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
|
||||||
import type { DndTargetState } from 'features/dnd/types';
|
|
||||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
|
||||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
|
||||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export const CanvasRightPanel = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const tabIndex = useMemo(() => {
|
|
||||||
if (activeTab === 'gallery') {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
const onClickViewerToggleButton = useCallback(() => {
|
|
||||||
if (activeTab !== 'gallery') {
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
|
||||||
}
|
|
||||||
imageViewer.toggle();
|
|
||||||
}, [imageViewer, activeTab, dispatch]);
|
|
||||||
|
|
||||||
const onChangeTab = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === 0) {
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
|
||||||
} else {
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useRegisteredHotkeys({
|
|
||||||
id: 'toggleViewer',
|
|
||||||
category: 'viewer',
|
|
||||||
callback: imageViewer.toggle,
|
|
||||||
dependencies: [imageViewer],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs index={tabIndex} onChange={onChangeTab} w="full" h="full" display="flex" flexDir="column">
|
|
||||||
<TabList alignItems="center">
|
|
||||||
<PanelTabs />
|
|
||||||
<Spacer />
|
|
||||||
<Button size="sm" variant="ghost" onClick={onClickViewerToggleButton}>
|
|
||||||
{imageViewer.isOpen ? t('gallery.closeViewer') : t('gallery.openViewer')}
|
|
||||||
</Button>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels w="full" h="full">
|
|
||||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<CanvasLayersPanelContent />
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
|
||||||
<GalleryPanelContent />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasRightPanel.displayName = 'CanvasRightPanel';
|
|
||||||
|
|
||||||
const PanelTabs = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const store = useAppStore();
|
|
||||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
|
||||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
|
||||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
|
||||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
|
||||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
|
||||||
const timeoutRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const layersTabLabel = useMemo(() => {
|
|
||||||
if (activeEntityCount === 0) {
|
|
||||||
return t('controlLayers.layer_other');
|
|
||||||
}
|
|
||||||
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
|
||||||
}, [activeEntityCount, t]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!layersTabRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
|
||||||
|
|
||||||
const onDragEnter = () => {
|
|
||||||
// If we are already on the layers tab, do nothing
|
|
||||||
if (getIsOnLayersTab()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else set the state to active and switch to the layers tab after a timeout
|
|
||||||
setLayersTabDndState('over');
|
|
||||||
timeoutRef.current = window.setTimeout(() => {
|
|
||||||
timeoutRef.current = null;
|
|
||||||
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
|
||||||
// When we switch tabs, the other tab should be pending
|
|
||||||
setLayersTabDndState('idle');
|
|
||||||
setGalleryTabDndState('potential');
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
const onDragLeave = () => {
|
|
||||||
// Set the state to idle or pending depending on the current tab
|
|
||||||
if (getIsOnLayersTab()) {
|
|
||||||
setLayersTabDndState('idle');
|
|
||||||
} else {
|
|
||||||
setLayersTabDndState('potential');
|
|
||||||
}
|
|
||||||
// Abort the tab switch if it hasn't happened yet
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onDragStart = () => {
|
|
||||||
// Set the state to pending when a drag starts
|
|
||||||
setLayersTabDndState('potential');
|
|
||||||
};
|
|
||||||
return combine(
|
|
||||||
dropTargetForElements({
|
|
||||||
element: layersTabRef.current,
|
|
||||||
onDragEnter,
|
|
||||||
onDragLeave,
|
|
||||||
}),
|
|
||||||
monitorForElements({
|
|
||||||
canMonitor: ({ source }) => {
|
|
||||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Only monitor if we are not already on the gallery tab
|
|
||||||
return !getIsOnLayersTab();
|
|
||||||
},
|
|
||||||
onDragStart,
|
|
||||||
}),
|
|
||||||
dropTargetForExternal({
|
|
||||||
element: layersTabRef.current,
|
|
||||||
onDragEnter,
|
|
||||||
onDragLeave,
|
|
||||||
}),
|
|
||||||
monitorForExternal({
|
|
||||||
canMonitor: () => !getIsOnLayersTab(),
|
|
||||||
onDragStart,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [store]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!galleryTabRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
|
||||||
|
|
||||||
const onDragEnter = () => {
|
|
||||||
// If we are already on the gallery tab, do nothing
|
|
||||||
if (getIsOnGalleryTab()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else set the state to active and switch to the gallery tab after a timeout
|
|
||||||
setGalleryTabDndState('over');
|
|
||||||
timeoutRef.current = window.setTimeout(() => {
|
|
||||||
timeoutRef.current = null;
|
|
||||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
|
||||||
// When we switch tabs, the other tab should be pending
|
|
||||||
setGalleryTabDndState('idle');
|
|
||||||
setLayersTabDndState('potential');
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragLeave = () => {
|
|
||||||
// Set the state to idle or pending depending on the current tab
|
|
||||||
if (getIsOnGalleryTab()) {
|
|
||||||
setGalleryTabDndState('idle');
|
|
||||||
} else {
|
|
||||||
setGalleryTabDndState('potential');
|
|
||||||
}
|
|
||||||
// Abort the tab switch if it hasn't happened yet
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragStart = () => {
|
|
||||||
// Set the state to pending when a drag starts
|
|
||||||
setGalleryTabDndState('potential');
|
|
||||||
};
|
|
||||||
|
|
||||||
return combine(
|
|
||||||
dropTargetForElements({
|
|
||||||
element: galleryTabRef.current,
|
|
||||||
onDragEnter,
|
|
||||||
onDragLeave,
|
|
||||||
}),
|
|
||||||
monitorForElements({
|
|
||||||
canMonitor: ({ source }) => {
|
|
||||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Only monitor if we are not already on the gallery tab
|
|
||||||
return !getIsOnGalleryTab();
|
|
||||||
},
|
|
||||||
onDragStart,
|
|
||||||
}),
|
|
||||||
dropTargetForExternal({
|
|
||||||
element: galleryTabRef.current,
|
|
||||||
onDragEnter,
|
|
||||||
onDragLeave,
|
|
||||||
}),
|
|
||||||
monitorForExternal({
|
|
||||||
canMonitor: () => !getIsOnGalleryTab(),
|
|
||||||
onDragStart,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [store]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onDrop = () => {
|
|
||||||
// Reset the dnd state when a drop happens
|
|
||||||
setGalleryTabDndState('idle');
|
|
||||||
setLayersTabDndState('idle');
|
|
||||||
};
|
|
||||||
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cleanup();
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tab ref={layersTabRef} position="relative" w={32}>
|
|
||||||
<Box as="span" w="full">
|
|
||||||
{layersTabLabel}
|
|
||||||
</Box>
|
|
||||||
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
|
||||||
</Tab>
|
|
||||||
<Tab ref={galleryTabRef} position="relative" w={32}>
|
|
||||||
<Box as="span" w="full">
|
|
||||||
{t('gallery.gallery')}
|
|
||||||
</Box>
|
|
||||||
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
|
||||||
</Tab>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
PanelTabs.displayName = 'PanelTabs';
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Spacer } from '@invoke-ai/ui-library';
|
|
||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
|
||||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
|
||||||
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
|
|
||||||
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
|
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IPAdapter = memo(({ id }: Props) => {
|
|
||||||
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'reference_image' }), [id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
|
||||||
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
|
|
||||||
<CanvasEntityContainer>
|
|
||||||
<CanvasEntityHeader ps={4} py={5}>
|
|
||||||
<CanvasEntityEditableTitle />
|
|
||||||
<Spacer />
|
|
||||||
<CanvasEntityHeaderCommonActions />
|
|
||||||
</CanvasEntityHeader>
|
|
||||||
<IPAdapterSettings />
|
|
||||||
</CanvasEntityContainer>
|
|
||||||
</CanvasEntityStateGate>
|
|
||||||
</EntityIdentifierContext.Provider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
IPAdapter.displayName = 'IPAdapter';
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
|
||||||
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
|
|
||||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
|
||||||
return canvas.referenceImages.entities.map(getEntityIdentifier).toReversed();
|
|
||||||
});
|
|
||||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
|
||||||
return selectedEntityIdentifier?.type === 'reference_image';
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IPAdapterList = memo(() => {
|
|
||||||
const isSelected = useAppSelector(selectIsSelected);
|
|
||||||
const entityIdentifiers = useAppSelector(selectEntityIdentifiers);
|
|
||||||
|
|
||||||
if (entityIdentifiers.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityIdentifiers.length > 0) {
|
|
||||||
return (
|
|
||||||
<CanvasEntityGroupList type="reference_image" isSelected={isSelected} entityIdentifiers={entityIdentifiers}>
|
|
||||||
{entityIdentifiers.map((entityIdentifiers) => (
|
|
||||||
<IPAdapter key={entityIdentifiers.id} id={entityIdentifiers.id} />
|
|
||||||
))}
|
|
||||||
</CanvasEntityGroupList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
IPAdapterList.displayName = 'IPAdapterList';
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
|
||||||
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
|
||||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
|
||||||
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
|
|
||||||
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
|
|
||||||
import { GlobalReferenceImageModel } from 'features/controlLayers/components/IPAdapter/GlobalReferenceImageModel';
|
|
||||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
|
||||||
import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
|
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
|
||||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import {
|
|
||||||
referenceImageIPAdapterBeginEndStepPctChanged,
|
|
||||||
referenceImageIPAdapterCLIPVisionModelChanged,
|
|
||||||
referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
|
|
||||||
referenceImageIPAdapterImageChanged,
|
|
||||||
referenceImageIPAdapterMethodChanged,
|
|
||||||
referenceImageIPAdapterModelChanged,
|
|
||||||
referenceImageIPAdapterWeightChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
|
||||||
import { selectCanvasSlice, selectEntity, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
|
||||||
import type {
|
|
||||||
CanvasEntityIdentifier,
|
|
||||||
CLIPVisionModelV2,
|
|
||||||
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
|
||||||
IPMethodV2,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
|
||||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
|
||||||
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
|
||||||
|
|
||||||
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
|
|
||||||
|
|
||||||
const buildSelectIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
|
|
||||||
createSelector(
|
|
||||||
selectCanvasSlice,
|
|
||||||
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'IPAdapterSettings').ipAdapter
|
|
||||||
);
|
|
||||||
|
|
||||||
const IPAdapterSettingsContent = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const entityIdentifier = useEntityIdentifierContext('reference_image');
|
|
||||||
const selectIPAdapter = useMemo(() => buildSelectIPAdapter(entityIdentifier), [entityIdentifier]);
|
|
||||||
const ipAdapter = useAppSelector(selectIPAdapter);
|
|
||||||
|
|
||||||
const onChangeBeginEndStepPct = useCallback(
|
|
||||||
(beginEndStepPct: [number, number]) => {
|
|
||||||
dispatch(referenceImageIPAdapterBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeWeight = useCallback(
|
|
||||||
(weight: number) => {
|
|
||||||
dispatch(referenceImageIPAdapterWeightChanged({ entityIdentifier, weight }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeIPMethod = useCallback(
|
|
||||||
(method: IPMethodV2) => {
|
|
||||||
dispatch(referenceImageIPAdapterMethodChanged({ entityIdentifier, method }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeFLUXReduxImageInfluence = useCallback(
|
|
||||||
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
|
||||||
dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, imageInfluence }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeModel = useCallback(
|
|
||||||
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => {
|
|
||||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier, modelConfig }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeCLIPVisionModel = useCallback(
|
|
||||||
(clipVisionModel: CLIPVisionModelV2) => {
|
|
||||||
dispatch(referenceImageIPAdapterCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeImage = useCallback(
|
|
||||||
(imageDTO: ImageDTO | null) => {
|
|
||||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
|
||||||
},
|
|
||||||
[dispatch, entityIdentifier]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
|
||||||
() => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }, ipAdapter.image?.image_name),
|
|
||||||
[entityIdentifier, ipAdapter.image?.image_name]
|
|
||||||
);
|
|
||||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
|
|
||||||
const isFLUX = useAppSelector(selectIsFLUX);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CanvasEntitySettingsWrapper>
|
|
||||||
<Flex flexDir="column" gap={2} position="relative" w="full">
|
|
||||||
<Flex gap={2} alignItems="center" w="full">
|
|
||||||
<GlobalReferenceImageModel modelKey={ipAdapter.model?.key ?? null} onChangeModel={onChangeModel} />
|
|
||||||
{ipAdapter.type === 'ip_adapter' && (
|
|
||||||
<CLIPVisionModel model={ipAdapter.clipVisionModel} onChange={onChangeCLIPVisionModel} />
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
onClick={pullBboxIntoIPAdapter}
|
|
||||||
isDisabled={isBusy}
|
|
||||||
variant="ghost"
|
|
||||||
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}
|
|
||||||
tooltip={t('controlLayers.pullBboxIntoReferenceImage')}
|
|
||||||
icon={<PiBoundingBoxBold />}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex gap={2} w="full">
|
|
||||||
{ipAdapter.type === 'ip_adapter' && (
|
|
||||||
<Flex flexDir="column" gap={2} w="full">
|
|
||||||
{!isFLUX && <IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />}
|
|
||||||
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
|
|
||||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{ipAdapter.type === 'flux_redux' && (
|
|
||||||
<Flex flexDir="column" gap={2} w="full" alignItems="flex-start">
|
|
||||||
<FLUXReduxImageInfluence
|
|
||||||
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
|
|
||||||
onChange={onChangeFLUXReduxImageInfluence}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
|
||||||
<IPAdapterImagePreview
|
|
||||||
image={ipAdapter.image}
|
|
||||||
onChangeImage={onChangeImage}
|
|
||||||
dndTarget={setGlobalReferenceImageDndTarget}
|
|
||||||
dndTargetData={dndTargetData}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</CanvasEntitySettingsWrapper>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
|
|
||||||
|
|
||||||
const buildSelectIPAdapterHasImage = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
|
|
||||||
createSelector(selectCanvasSlice, (canvas) => {
|
|
||||||
const referenceImage = selectEntity(canvas, entityIdentifier);
|
|
||||||
return !!referenceImage && referenceImage.ipAdapter.image !== null;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IPAdapterSettings = memo(() => {
|
|
||||||
const entityIdentifier = useEntityIdentifierContext('reference_image');
|
|
||||||
|
|
||||||
const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(entityIdentifier), [entityIdentifier]);
|
|
||||||
const hasImage = useAppSelector(selectIPAdapterHasImage);
|
|
||||||
|
|
||||||
if (!hasImage) {
|
|
||||||
return <IPAdapterSettingsEmptyState />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <IPAdapterSettingsContent />;
|
|
||||||
});
|
|
||||||
|
|
||||||
IPAdapterSettings.displayName = 'IPAdapterSettings';
|
|
||||||
@@ -2,8 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import {
|
import {
|
||||||
selectSystemShouldConfirmOnNewSession,
|
selectSystemShouldConfirmOnNewSession,
|
||||||
shouldConfirmOnNewSessionToggled,
|
shouldConfirmOnNewSessionToggled,
|
||||||
@@ -17,15 +16,13 @@ const [useNewCanvasSessionDialog] = buildUseBoolean(false);
|
|||||||
|
|
||||||
export const useNewGallerySession = () => {
|
export const useNewGallerySession = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||||
const newSessionDialog = useNewGallerySessionDialog();
|
const newSessionDialog = useNewGallerySessionDialog();
|
||||||
|
|
||||||
const newGallerySessionImmediate = useCallback(() => {
|
const newGallerySessionImmediate = useCallback(() => {
|
||||||
dispatch(newGallerySessionRequested());
|
dispatch(generateSessionReset());
|
||||||
imageViewer.open();
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
}, [dispatch, imageViewer]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const newGallerySessionWithDialog = useCallback(() => {
|
const newGallerySessionWithDialog = useCallback(() => {
|
||||||
if (shouldConfirmOnNewSession) {
|
if (shouldConfirmOnNewSession) {
|
||||||
@@ -40,15 +37,13 @@ export const useNewGallerySession = () => {
|
|||||||
|
|
||||||
export const useNewCanvasSession = () => {
|
export const useNewCanvasSession = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||||
const newSessionDialog = useNewCanvasSessionDialog();
|
const newSessionDialog = useNewCanvasSessionDialog();
|
||||||
|
|
||||||
const newCanvasSessionImmediate = useCallback(() => {
|
const newCanvasSessionImmediate = useCallback(() => {
|
||||||
dispatch(newCanvasSessionRequested());
|
dispatch(canvasSessionReset());
|
||||||
imageViewer.close();
|
|
||||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||||
}, [dispatch, imageViewer]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const newCanvasSessionWithDialog = useCallback(() => {
|
const newCanvasSessionWithDialog = useCallback(() => {
|
||||||
if (shouldConfirmOnNewSession) {
|
if (shouldConfirmOnNewSession) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -8,8 +8,8 @@ import { PiBoundingBoxBold } from 'react-icons/pi';
|
|||||||
|
|
||||||
export const IPAdapterMenuItemPullBbox = memo(() => {
|
export const IPAdapterMenuItemPullBbox = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext('reference_image');
|
const id = useRefImageIdContext();
|
||||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||||
const isBusy = useCanvasIsBusy();
|
const isBusy = useCanvasIsBusy();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -3,7 +3,7 @@ import { IconMenuItemGroup } from 'common/components/IconMenuItem';
|
|||||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||||
import { IPAdapterMenuItemPullBbox } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItemPullBbox';
|
import { IPAdapterMenuItemPullBbox } from 'features/controlLayers/components/RefImage/IPAdapterMenuItemPullBbox';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const IPAdapterMenuItems = memo(() => {
|
export const IPAdapterMenuItems = memo(() => {
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Image,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
Portal,
|
||||||
|
Skeleton,
|
||||||
|
Text,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
import { POPPER_MODIFIERS } from 'common/components/InformationalPopover/constants';
|
||||||
|
import type { UseDisclosure } from 'common/hooks/useBoolean';
|
||||||
|
import { useDisclosure } from 'common/hooks/useBoolean';
|
||||||
|
import { DEFAULT_FILTER, useFilterableOutsideClick } from 'common/hooks/useFilterableOutsideClick';
|
||||||
|
import { RefImageHeader } from 'features/controlLayers/components/RefImage/RefImageHeader';
|
||||||
|
import { RefImageSettings } from 'features/controlLayers/components/RefImage/RefImageSettings';
|
||||||
|
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||||
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
|
import { isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||||
|
import { round } from 'lodash-es';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { PiImageBold } from 'react-icons/pi';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
// There is some awkwardness here with closing the popover when clicking outside of it, related to Chakra's
|
||||||
|
// handling of refs, portals, outside clicks, and a race condition with framer-motion animations that can leave
|
||||||
|
// the popover closed when its internal state is still open.
|
||||||
|
//
|
||||||
|
// We have to manually manage the popover open state to work around the race condition, and then have to do special
|
||||||
|
// handling to close the popover when clicking outside of it.
|
||||||
|
|
||||||
|
// We have to reach outside react to identify the popover trigger element instead of using refs, thanks to how Chakra
|
||||||
|
// handles refs for PopoverAnchor internally. Maybe there is some way to merge them but I couldn't figure it out.
|
||||||
|
const getRefImagePopoverTriggerId = (id: string) => `ref-image-popover-trigger-${id}`;
|
||||||
|
|
||||||
|
export const RefImage = memo(() => {
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const disclosure = useDisclosure(false);
|
||||||
|
// This filter prevents the popover from closing when clicking on a sibling portal element, like the dropdown menu
|
||||||
|
// inside the ref image settings popover. It also prevents the popover from closing when clicking on the popover's
|
||||||
|
// own trigger element.
|
||||||
|
const filter = useCallback(
|
||||||
|
(el: HTMLElement | SVGElement) => {
|
||||||
|
return DEFAULT_FILTER(el) || el.id === getRefImagePopoverTriggerId(id);
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
useFilterableOutsideClick({ ref, handler: disclosure.close, filter });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
// The popover contains a react-select component, which uses a portal to render its options. This portal
|
||||||
|
// is itself not lazy. As a result, if we do not unmount the popover when it is closed, the react-select
|
||||||
|
// component still exists but is invisible, and intercepts clicks!
|
||||||
|
isLazy
|
||||||
|
lazyBehavior="unmount"
|
||||||
|
isOpen={disclosure.isOpen}
|
||||||
|
closeOnBlur={false}
|
||||||
|
modifiers={POPPER_MODIFIERS}
|
||||||
|
>
|
||||||
|
<Thumbnail disclosure={disclosure} />
|
||||||
|
<Portal>
|
||||||
|
<PopoverContent ref={ref} w={400}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||||
|
<RefImageHeader />
|
||||||
|
<Divider />
|
||||||
|
<RefImageSettings />
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RefImage.displayName = 'RefImage';
|
||||||
|
|
||||||
|
const baseSx: SystemStyleObject = {
|
||||||
|
opacity: 0.7,
|
||||||
|
transitionProperty: 'opacity',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
position: 'relative',
|
||||||
|
_hover: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
'&[data-is-open="true"]': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const weightDisplaySx: SystemStyleObject = {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
transitionProperty: 'opacity',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
opacity: 0,
|
||||||
|
'&[data-visible="true"]': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageSxWithWeight = (weight: number): SystemStyleObject => {
|
||||||
|
const fillPercentage = Math.max(0, Math.min(100, weight * 100));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSx,
|
||||||
|
_after: {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: `linear-gradient(to top, transparent ${fillPercentage}%, rgba(0, 0, 0, 0.8) ${fillPercentage}%)`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
borderRadius: 'base',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Thumbnail = memo(({ disclosure }: { disclosure: UseDisclosure }) => {
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const entity = useRefImageEntity(id);
|
||||||
|
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
|
||||||
|
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
|
||||||
|
|
||||||
|
const sx = useMemo(() => {
|
||||||
|
if (!isIPAdapterConfig(entity.config)) {
|
||||||
|
return baseSx;
|
||||||
|
}
|
||||||
|
return getImageSxWithWeight(entity.config.weight);
|
||||||
|
}, [entity.config]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isIPAdapterConfig(entity.config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowWeightDisplay(true);
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
setShowWeightDisplay(false);
|
||||||
|
}, 1000);
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [entity.config]);
|
||||||
|
|
||||||
|
if (!entity.config.image) {
|
||||||
|
return (
|
||||||
|
<PopoverAnchor>
|
||||||
|
<IconButton
|
||||||
|
id={getRefImagePopoverTriggerId(id)}
|
||||||
|
aria-label="Open Reference Image Settings"
|
||||||
|
h="full"
|
||||||
|
variant="ghost"
|
||||||
|
aspectRatio="1/1"
|
||||||
|
borderWidth="2px !important"
|
||||||
|
borderStyle="dashed !important"
|
||||||
|
borderColor="errorAlpha.500"
|
||||||
|
borderRadius="base"
|
||||||
|
icon={<PiImageBold />}
|
||||||
|
colorScheme="error"
|
||||||
|
onClick={disclosure.toggle}
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
|
</PopoverAnchor>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PopoverAnchor>
|
||||||
|
<Flex
|
||||||
|
position="relative"
|
||||||
|
borderWidth={1}
|
||||||
|
borderStyle="solid"
|
||||||
|
borderRadius="base"
|
||||||
|
aspectRatio="1/1"
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
flexShrink={0}
|
||||||
|
sx={sx}
|
||||||
|
data-is-open={disclosure.isOpen}
|
||||||
|
id={getRefImagePopoverTriggerId(id)}
|
||||||
|
role="button"
|
||||||
|
onClick={disclosure.toggle}
|
||||||
|
cursor="pointer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageDTO?.thumbnail_url}
|
||||||
|
objectFit="contain"
|
||||||
|
aspectRatio="1/1"
|
||||||
|
height={imageDTO?.height}
|
||||||
|
fallback={<Skeleton h="full" aspectRatio="1/1" />}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
borderRadius="base"
|
||||||
|
/>
|
||||||
|
{isIPAdapterConfig(entity.config) && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
inset={0}
|
||||||
|
fontWeight="semibold"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
zIndex={1}
|
||||||
|
data-visible={showWeightDisplay}
|
||||||
|
sx={weightDisplaySx}
|
||||||
|
>
|
||||||
|
<Text filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))">
|
||||||
|
{`${round(entity.config.weight * 100, 2)}%`}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</PopoverAnchor>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Thumbnail.displayName = 'Thumbnail';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||||
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
|
import { refImageDeleted } from 'features/controlLayers/store/refImagesSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { PiTrashBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const RefImageHeader = memo(() => {
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const entity = useRefImageEntity(id);
|
||||||
|
const deleteRefImage = useCallback(() => {
|
||||||
|
dispatch(refImageDeleted({ id }));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="space-between" alignItems="center" w="full">
|
||||||
|
{entity.config.image !== null && (
|
||||||
|
<Text fontWeight="semibold" color="base.300">
|
||||||
|
Reference Image
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{entity.config.image === null && (
|
||||||
|
<Text fontWeight="semibold" color="base.300">
|
||||||
|
No Reference Image Selected
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
variant="link"
|
||||||
|
alignSelf="stretch"
|
||||||
|
icon={<PiTrashBold />}
|
||||||
|
onClick={deleteRefImage}
|
||||||
|
aria-label="Delete reference image"
|
||||||
|
colorScheme="error"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RefImageHeader.displayName = 'RefImageHeader';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
@@ -21,7 +21,7 @@ type Props<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegiona
|
|||||||
dndTargetData: ReturnType<T['getData']>;
|
dndTargetData: ReturnType<T['getData']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IPAdapterImagePreview = memo(
|
export const RefImageImage = memo(
|
||||||
<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget>({
|
<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget>({
|
||||||
image,
|
image,
|
||||||
onChangeImage,
|
onChangeImage,
|
||||||
@@ -51,7 +51,7 @@ export const IPAdapterImagePreview = memo(
|
|||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||||
{!imageDTO && (
|
{!imageDTO && (
|
||||||
<UploadImageButton
|
<UploadImageIconButton
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
h="full"
|
||||||
isError={!imageDTO && !image?.image_name}
|
isError={!imageDTO && !image?.image_name}
|
||||||
@@ -77,4 +77,4 @@ export const IPAdapterImagePreview = memo(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
|
RefImageImage.displayName = 'RefImageImage';
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { FlexProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { RefImage } from 'features/controlLayers/components/RefImage/RefImage';
|
||||||
|
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
|
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
|
import { refImageAdded, selectRefImageEntityIds } from 'features/controlLayers/store/refImagesSlice';
|
||||||
|
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||||
|
import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { PiUploadBold } from 'react-icons/pi';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export const RefImageList = memo((props: FlexProps) => {
|
||||||
|
const ids = useAppSelector(selectRefImageEntityIds);
|
||||||
|
return (
|
||||||
|
<Flex gap={2} h={16} {...props}>
|
||||||
|
{ids.map((id) => (
|
||||||
|
<RefImageIdContext.Provider key={id} value={id}>
|
||||||
|
<RefImage />
|
||||||
|
</RefImageIdContext.Provider>
|
||||||
|
))}
|
||||||
|
{ids.length < 5 && <AddRefImageDropTargetAndButton />}
|
||||||
|
{ids.length >= 5 && <MaxRefImages />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RefImageList.displayName = 'RefImageList';
|
||||||
|
|
||||||
|
const dndTargetData = addGlobalReferenceImageDndTarget.getData();
|
||||||
|
|
||||||
|
const MaxRefImages = memo(() => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
position="relative"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
h="full"
|
||||||
|
w="full"
|
||||||
|
borderWidth="2px !important"
|
||||||
|
borderStyle="dashed !important"
|
||||||
|
borderRadius="base"
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
Max Ref Images
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MaxRefImages.displayName = 'MaxRefImages';
|
||||||
|
|
||||||
|
const AddRefImageDropTargetAndButton = memo(() => {
|
||||||
|
const { dispatch, getState } = useAppStore();
|
||||||
|
|
||||||
|
const uploadOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
onUpload: (imageDTO: ImageDTO) => {
|
||||||
|
const config = getDefaultRefImageConfig(getState);
|
||||||
|
config.image = imageDTOToImageWithDims(imageDTO);
|
||||||
|
dispatch(refImageAdded({ overrides: { config } }));
|
||||||
|
},
|
||||||
|
allowMultiple: false,
|
||||||
|
}) as const,
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadApi = useImageUploadButton(uploadOptions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
position="relative"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
h="full"
|
||||||
|
w="full"
|
||||||
|
borderWidth="2px !important"
|
||||||
|
borderStyle="dashed !important"
|
||||||
|
borderRadius="base"
|
||||||
|
leftIcon={<PiUploadBold />}
|
||||||
|
{...uploadApi.getUploadButtonProps()}
|
||||||
|
>
|
||||||
|
Reference Image
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
<DndDropTarget label="Drop" dndTarget={addGlobalReferenceImageDndTarget} dndTargetData={dndTargetData} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
AddRefImageDropTargetAndButton.displayName = 'AddRefImageDropTargetAndButton';
|
||||||
@@ -12,7 +12,7 @@ type Props = {
|
|||||||
onChangeModel: (modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => void;
|
onChangeModel: (modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalReferenceImageModel = memo(({ modelKey, onChangeModel }: Props) => {
|
export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentBaseModel = useAppSelector(selectBase);
|
const currentBaseModel = useAppSelector(selectBase);
|
||||||
const [modelConfigs, { isLoading }] = useGlobalReferenceImageModels();
|
const [modelConfigs, { isLoading }] = useGlobalReferenceImageModels();
|
||||||
@@ -47,7 +47,7 @@ export const GlobalReferenceImageModel = memo(({ modelKey, onChangeModel }: Prop
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={selectedModel?.description}>
|
<Tooltip label={selectedModel?.description}>
|
||||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
|
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full" minW={0}>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={t('common.placeholderSelectAModel')}
|
placeholder={t('common.placeholderSelectAModel')}
|
||||||
@@ -60,4 +60,4 @@ export const GlobalReferenceImageModel = memo(({ modelKey, onChangeModel }: Prop
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
GlobalReferenceImageModel.displayName = 'GlobalReferenceImageModel';
|
RefImageModel.displayName = 'RefImageModel';
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
|
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||||
|
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { setGlobalReferenceImage } from 'features/imageActions/actions';
|
||||||
|
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export const RefImageNoImageState = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onUpload = useCallback(
|
||||||
|
(imageDTO: ImageDTO) => {
|
||||||
|
setGlobalReferenceImage({ imageDTO, id, dispatch });
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||||
|
const onClickGalleryButton = useCallback(() => {
|
||||||
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||||
|
() => setGlobalReferenceImageDndTarget.getData({ id }),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const components = useMemo(
|
||||||
|
() => ({
|
||||||
|
UploadButton: <Button size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />,
|
||||||
|
GalleryButton: <Button onClick={onClickGalleryButton} size="sm" variant="link" color="base.300" />,
|
||||||
|
}),
|
||||||
|
[onClickGalleryButton, uploadApi]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
|
||||||
|
<Text textAlign="center" color="base.300">
|
||||||
|
<Trans i18nKey="controlLayers.referenceImageEmptyState" components={components} />
|
||||||
|
</Text>
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
<DndDropTarget
|
||||||
|
dndTarget={setGlobalReferenceImageDndTarget}
|
||||||
|
dndTargetData={dndTargetData}
|
||||||
|
label={t('controlLayers.useImage')}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RefImageNoImageState.displayName = 'RefImageNoImageState';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||||
@@ -13,26 +13,26 @@ import { memo, useCallback, useMemo } from 'react';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
export const IPAdapterSettingsEmptyState = memo(() => {
|
export const RefImageNoImageStateWithCanvasOptions = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext('reference_image');
|
const id = useRefImageIdContext();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isBusy = useCanvasIsBusy();
|
const isBusy = useCanvasIsBusy();
|
||||||
const onUpload = useCallback(
|
const onUpload = useCallback(
|
||||||
(imageDTO: ImageDTO) => {
|
(imageDTO: ImageDTO) => {
|
||||||
setGlobalReferenceImage({ imageDTO, entityIdentifier, dispatch });
|
setGlobalReferenceImage({ imageDTO, id, dispatch });
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier]
|
[dispatch, id]
|
||||||
);
|
);
|
||||||
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
|
||||||
const onClickGalleryButton = useCallback(() => {
|
const onClickGalleryButton = useCallback(() => {
|
||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(id);
|
||||||
|
|
||||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||||
() => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }),
|
() => setGlobalReferenceImageDndTarget.getData({ id }),
|
||||||
[entityIdentifier]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = useMemo(
|
const components = useMemo(
|
||||||
@@ -53,7 +53,7 @@ export const IPAdapterSettingsEmptyState = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
|
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
|
||||||
<Text textAlign="center" color="base.300">
|
<Text textAlign="center" color="base.300">
|
||||||
<Trans i18nKey="controlLayers.referenceImageEmptyState" components={components} />
|
<Trans i18nKey="controlLayers.referenceImageEmptyStateWithCanvasOptions" components={components} />
|
||||||
</Text>
|
</Text>
|
||||||
<input {...uploadApi.getUploadInputProps()} />
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
<DndDropTarget
|
<DndDropTarget
|
||||||
@@ -66,4 +66,4 @@ export const IPAdapterSettingsEmptyState = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
IPAdapterSettingsEmptyState.displayName = 'IPAdapterSettingsEmptyState';
|
RefImageNoImageStateWithCanvasOptions.displayName = 'RefImageNoImageStateWithCanvasOptions';
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
||||||
|
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
|
||||||
|
import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
|
||||||
|
import { PullBboxIntoRefImageIconButton } from 'features/controlLayers/components/common/PullBboxIntoRefImageIconButton';
|
||||||
|
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||||
|
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
|
||||||
|
import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
|
||||||
|
import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
|
||||||
|
import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions';
|
||||||
|
import {
|
||||||
|
CanvasManagerProviderGate,
|
||||||
|
useCanvasManagerSafe,
|
||||||
|
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||||
|
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||||
|
import {
|
||||||
|
refImageFLUXReduxImageInfluenceChanged,
|
||||||
|
refImageImageChanged,
|
||||||
|
refImageIPAdapterBeginEndStepPctChanged,
|
||||||
|
refImageIPAdapterCLIPVisionModelChanged,
|
||||||
|
refImageIPAdapterMethodChanged,
|
||||||
|
refImageIPAdapterWeightChanged,
|
||||||
|
refImageModelChanged,
|
||||||
|
selectRefImageEntity,
|
||||||
|
selectRefImageEntityOrThrow,
|
||||||
|
selectRefImagesSlice,
|
||||||
|
} from 'features/controlLayers/store/refImagesSlice';
|
||||||
|
import type {
|
||||||
|
CLIPVisionModelV2,
|
||||||
|
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||||
|
IPMethodV2,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
|
import { isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||||
|
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||||
|
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
|
import { RefImageImage } from './RefImageImage';
|
||||||
|
|
||||||
|
const buildSelectConfig = (id: string) =>
|
||||||
|
createSelector(
|
||||||
|
selectRefImagesSlice,
|
||||||
|
(refImages) => selectRefImageEntityOrThrow(refImages, id, 'IPAdapterSettings').config
|
||||||
|
);
|
||||||
|
|
||||||
|
const RefImageSettingsContent = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
|
||||||
|
const config = useAppSelector(selectConfig);
|
||||||
|
const tab = useAppSelector(selectActiveTab);
|
||||||
|
|
||||||
|
const onChangeBeginEndStepPct = useCallback(
|
||||||
|
(beginEndStepPct: [number, number]) => {
|
||||||
|
dispatch(refImageIPAdapterBeginEndStepPctChanged({ id, beginEndStepPct }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeWeight = useCallback(
|
||||||
|
(weight: number) => {
|
||||||
|
dispatch(refImageIPAdapterWeightChanged({ id, weight }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeIPMethod = useCallback(
|
||||||
|
(method: IPMethodV2) => {
|
||||||
|
dispatch(refImageIPAdapterMethodChanged({ id, method }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeFLUXReduxImageInfluence = useCallback(
|
||||||
|
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
||||||
|
dispatch(refImageFLUXReduxImageInfluenceChanged({ id, imageInfluence }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeModel = useCallback(
|
||||||
|
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ApiModelConfig) => {
|
||||||
|
dispatch(refImageModelChanged({ id, modelConfig }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeCLIPVisionModel = useCallback(
|
||||||
|
(clipVisionModel: CLIPVisionModelV2) => {
|
||||||
|
dispatch(refImageIPAdapterCLIPVisionModelChanged({ id, clipVisionModel }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeImage = useCallback(
|
||||||
|
(imageDTO: ImageDTO | null) => {
|
||||||
|
dispatch(refImageImageChanged({ id, imageDTO }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||||
|
() => setGlobalReferenceImageDndTarget.getData({ id }, config.image?.image_name),
|
||||||
|
[id, config.image?.image_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFLUX = useAppSelector(selectIsFLUX);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={2} position="relative" w="full">
|
||||||
|
<Flex gap={2} alignItems="center" w="full">
|
||||||
|
<RefImageModel modelKey={config.model?.key ?? null} onChangeModel={onChangeModel} />
|
||||||
|
{isIPAdapterConfig(config) && (
|
||||||
|
<IPAdapterCLIPVisionModel model={config.clipVisionModel} onChange={onChangeCLIPVisionModel} />
|
||||||
|
)}
|
||||||
|
{tab === 'canvas' && (
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<PullBboxIntoRefImageIconButton />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
{isIPAdapterConfig(config) && (
|
||||||
|
<Flex flexDir="column" gap={2} w="full">
|
||||||
|
{!isFLUX && <IPAdapterMethod method={config.method} onChange={onChangeIPMethod} />}
|
||||||
|
<Weight weight={config.weight} onChange={onChangeWeight} />
|
||||||
|
<BeginEndStepPct beginEndStepPct={config.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{isFLUXReduxConfig(config) && (
|
||||||
|
<Flex flexDir="column" gap={2} w="full" alignItems="flex-start">
|
||||||
|
<FLUXReduxImageInfluence
|
||||||
|
imageInfluence={config.imageInfluence ?? 'lowest'}
|
||||||
|
onChange={onChangeFLUXReduxImageInfluence}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
||||||
|
<RefImageImage
|
||||||
|
image={config.image}
|
||||||
|
onChangeImage={onChangeImage}
|
||||||
|
dndTarget={setGlobalReferenceImageDndTarget}
|
||||||
|
dndTargetData={dndTargetData}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RefImageSettingsContent.displayName = 'RefImageSettingsContent';
|
||||||
|
|
||||||
|
const buildSelectIPAdapterHasImage = (id: string) =>
|
||||||
|
createSelector(selectRefImagesSlice, (refImages) => {
|
||||||
|
const referenceImage = selectRefImageEntity(refImages, id);
|
||||||
|
return !!referenceImage && referenceImage.config.image !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RefImageSettings = memo(() => {
|
||||||
|
const id = useRefImageIdContext();
|
||||||
|
const tab = useAppSelector(selectActiveTab);
|
||||||
|
const canvasManager = useCanvasManagerSafe();
|
||||||
|
const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(id), [id]);
|
||||||
|
const hasImage = useAppSelector(selectIPAdapterHasImage);
|
||||||
|
|
||||||
|
if (!hasImage && canvasManager && tab === 'canvas') {
|
||||||
|
return (
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<RefImageNoImageStateWithCanvasOptions />
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasImage) {
|
||||||
|
return <RefImageNoImageState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RefImageSettingsContent />;
|
||||||
|
});
|
||||||
|
|
||||||
|
RefImageSettings.displayName = 'RefImageSettings';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectRefImageEntityOrThrow, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useRefImageEntity = (id: string) => {
|
||||||
|
const selectEntity = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectRefImagesSlice, (refImages) =>
|
||||||
|
selectRefImageEntityOrThrow(refImages, id, `useRefImageState(${id})`)
|
||||||
|
),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const entity = useAppSelector(selectEntity);
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import {
|
import {
|
||||||
buildSelectValidRegionalGuidanceActions,
|
buildSelectValidRegionalGuidanceActions,
|
||||||
useAddRegionalGuidanceIPAdapter,
|
useAddNegativePromptToExistingRegionalGuidance,
|
||||||
useAddRegionalGuidanceNegativePrompt,
|
useAddPositivePromptToExistingRegionalGuidance,
|
||||||
useAddRegionalGuidancePositivePrompt,
|
useAddRefImageToExistingRegionalGuidance,
|
||||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -14,9 +14,9 @@ import { PiPlusBold } from 'react-icons/pi';
|
|||||||
export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
|
export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
|
||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
|
const addRegionalGuidanceIPAdapter = useAddRefImageToExistingRegionalGuidance(entityIdentifier);
|
||||||
const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
|
const addRegionalGuidancePositivePrompt = useAddPositivePromptToExistingRegionalGuidance(entityIdentifier);
|
||||||
const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
|
const addRegionalGuidanceNegativePrompt = useAddNegativePromptToExistingRegionalGuidance(entityIdentifier);
|
||||||
|
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
|
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
|
||||||
|
|||||||
@@ -2,25 +2,25 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
||||||
|
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/common/FLUXReduxImageInfluence';
|
||||||
|
import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
|
||||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||||
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
|
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
|
||||||
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
|
import { RefImageImage } from 'features/controlLayers/components/RefImage/RefImageImage';
|
||||||
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
|
|
||||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
|
||||||
import { RegionalReferenceImageModel } from 'features/controlLayers/components/IPAdapter/RegionalReferenceImageModel';
|
|
||||||
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
|
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
|
||||||
|
import { RegionalReferenceImageModel } from 'features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import {
|
import {
|
||||||
rgIPAdapterBeginEndStepPctChanged,
|
rgRefImageDeleted,
|
||||||
rgIPAdapterCLIPVisionModelChanged,
|
rgRefImageFLUXReduxImageInfluenceChanged,
|
||||||
rgIPAdapterDeleted,
|
rgRefImageImageChanged,
|
||||||
rgIPAdapterFLUXReduxImageInfluenceChanged,
|
rgRefImageIPAdapterBeginEndStepPctChanged,
|
||||||
rgIPAdapterImageChanged,
|
rgRefImageIPAdapterCLIPVisionModelChanged,
|
||||||
rgIPAdapterMethodChanged,
|
rgRefImageIPAdapterMethodChanged,
|
||||||
rgIPAdapterModelChanged,
|
rgRefImageIPAdapterWeightChanged,
|
||||||
rgIPAdapterWeightChanged,
|
rgRefImageModelChanged,
|
||||||
} from 'features/controlLayers/store/canvasSlice';
|
} from 'features/controlLayers/store/canvasSlice';
|
||||||
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
||||||
import type {
|
import type {
|
||||||
@@ -46,64 +46,64 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onDeleteIPAdapter = useCallback(() => {
|
const onDeleteIPAdapter = useCallback(() => {
|
||||||
dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
|
dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
|
||||||
}, [dispatch, entityIdentifier, referenceImageId]);
|
}, [dispatch, entityIdentifier, referenceImageId]);
|
||||||
const selectIPAdapter = useMemo(
|
const selectConfig = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(selectCanvasSlice, (canvas) => {
|
createSelector(selectCanvasSlice, (canvas) => {
|
||||||
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
|
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
|
||||||
assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`);
|
assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`);
|
||||||
return referenceImage.ipAdapter;
|
return referenceImage.config;
|
||||||
}),
|
}),
|
||||||
[entityIdentifier, referenceImageId]
|
[entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
const ipAdapter = useAppSelector(selectIPAdapter);
|
const config = useAppSelector(selectConfig);
|
||||||
|
|
||||||
const onChangeBeginEndStepPct = useCallback(
|
const onChangeBeginEndStepPct = useCallback(
|
||||||
(beginEndStepPct: [number, number]) => {
|
(beginEndStepPct: [number, number]) => {
|
||||||
dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, referenceImageId, beginEndStepPct }));
|
dispatch(rgRefImageIPAdapterBeginEndStepPctChanged({ entityIdentifier, referenceImageId, beginEndStepPct }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeWeight = useCallback(
|
const onChangeWeight = useCallback(
|
||||||
(weight: number) => {
|
(weight: number) => {
|
||||||
dispatch(rgIPAdapterWeightChanged({ entityIdentifier, referenceImageId, weight }));
|
dispatch(rgRefImageIPAdapterWeightChanged({ entityIdentifier, referenceImageId, weight }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeIPMethod = useCallback(
|
const onChangeIPMethod = useCallback(
|
||||||
(method: IPMethodV2) => {
|
(method: IPMethodV2) => {
|
||||||
dispatch(rgIPAdapterMethodChanged({ entityIdentifier, referenceImageId, method }));
|
dispatch(rgRefImageIPAdapterMethodChanged({ entityIdentifier, referenceImageId, method }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeFLUXReduxImageInfluence = useCallback(
|
const onChangeFLUXReduxImageInfluence = useCallback(
|
||||||
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
||||||
dispatch(rgIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
|
dispatch(rgRefImageFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeModel = useCallback(
|
const onChangeModel = useCallback(
|
||||||
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
|
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
|
||||||
dispatch(rgIPAdapterModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
|
dispatch(rgRefImageModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeCLIPVisionModel = useCallback(
|
const onChangeCLIPVisionModel = useCallback(
|
||||||
(clipVisionModel: CLIPVisionModelV2) => {
|
(clipVisionModel: CLIPVisionModelV2) => {
|
||||||
dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, referenceImageId, clipVisionModel }));
|
dispatch(rgRefImageIPAdapterCLIPVisionModelChanged({ entityIdentifier, referenceImageId, clipVisionModel }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeImage = useCallback(
|
const onChangeImage = useCallback(
|
||||||
(imageDTO: ImageDTO | null) => {
|
(imageDTO: ImageDTO | null) => {
|
||||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
dispatch(rgRefImageImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier, referenceImageId]
|
[dispatch, entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
@@ -112,9 +112,9 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
|||||||
() =>
|
() =>
|
||||||
setRegionalGuidanceReferenceImageDndTarget.getData(
|
setRegionalGuidanceReferenceImageDndTarget.getData(
|
||||||
{ entityIdentifier, referenceImageId },
|
{ entityIdentifier, referenceImageId },
|
||||||
ipAdapter.image?.image_name
|
config.image?.image_name
|
||||||
),
|
),
|
||||||
[entityIdentifier, ipAdapter.image?.image_name, referenceImageId]
|
[entityIdentifier, config.image?.image_name, referenceImageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
||||||
@@ -140,9 +140,9 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flexDir="column" gap={2} position="relative" w="full">
|
<Flex flexDir="column" gap={2} position="relative" w="full">
|
||||||
<Flex gap={2} alignItems="center" w="full">
|
<Flex gap={2} alignItems="center" w="full">
|
||||||
<RegionalReferenceImageModel modelKey={ipAdapter.model?.key ?? null} onChangeModel={onChangeModel} />
|
<RegionalReferenceImageModel modelKey={config.model?.key ?? null} onChangeModel={onChangeModel} />
|
||||||
{ipAdapter.type === 'ip_adapter' && (
|
{config.type === 'ip_adapter' && (
|
||||||
<CLIPVisionModel model={ipAdapter.clipVisionModel} onChange={onChangeCLIPVisionModel} />
|
<IPAdapterCLIPVisionModel model={config.clipVisionModel} onChange={onChangeCLIPVisionModel} />
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={pullBboxIntoIPAdapter}
|
onClick={pullBboxIntoIPAdapter}
|
||||||
@@ -154,24 +154,24 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap={2} w="full">
|
<Flex gap={2} w="full">
|
||||||
{ipAdapter.type === 'ip_adapter' && (
|
{config.type === 'ip_adapter' && (
|
||||||
<Flex flexDir="column" gap={2} w="full">
|
<Flex flexDir="column" gap={2} w="full">
|
||||||
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
|
<IPAdapterMethod method={config.method} onChange={onChangeIPMethod} />
|
||||||
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
|
<Weight weight={config.weight} onChange={onChangeWeight} />
|
||||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
<BeginEndStepPct beginEndStepPct={config.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{ipAdapter.type === 'flux_redux' && (
|
{config.type === 'flux_redux' && (
|
||||||
<Flex flexDir="column" gap={2} w="full">
|
<Flex flexDir="column" gap={2} w="full">
|
||||||
<FLUXReduxImageInfluence
|
<FLUXReduxImageInfluence
|
||||||
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
|
imageInfluence={config.imageInfluence ?? 'lowest'}
|
||||||
onChange={onChangeFLUXReduxImageInfluence}
|
onChange={onChangeFLUXReduxImageInfluence}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
||||||
<IPAdapterImagePreview
|
<RefImageImage
|
||||||
image={ipAdapter.image}
|
image={config.image}
|
||||||
onChangeImage={onChangeImage}
|
onChangeImage={onChangeImage}
|
||||||
dndTarget={setRegionalGuidanceReferenceImageDndTarget}
|
dndTarget={setRegionalGuidanceReferenceImageDndTarget}
|
||||||
dndTargetData={dndTargetData}
|
dndTargetData={dndTargetData}
|
||||||
@@ -191,17 +191,16 @@ const buildSelectIPAdapterHasImage = (
|
|||||||
) =>
|
) =>
|
||||||
createSelector(selectCanvasSlice, (canvas) => {
|
createSelector(selectCanvasSlice, (canvas) => {
|
||||||
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
|
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
|
||||||
return !!referenceImage && referenceImage.ipAdapter.image !== null;
|
return !!referenceImage && referenceImage.config.image !== null;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
|
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
|
||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
|
const selectHasImage = useMemo(
|
||||||
const selectIPAdapterHasImage = useMemo(
|
|
||||||
() => buildSelectIPAdapterHasImage(entityIdentifier, referenceImageId),
|
() => buildSelectIPAdapterHasImage(entityIdentifier, referenceImageId),
|
||||||
[entityIdentifier, referenceImageId]
|
[entityIdentifier, referenceImageId]
|
||||||
);
|
);
|
||||||
const hasImage = useAppSelector(selectIPAdapterHasImage);
|
const hasImage = useAppSelector(selectHasImage);
|
||||||
|
|
||||||
if (!hasImage) {
|
if (!hasImage) {
|
||||||
return <RegionalGuidanceIPAdapterSettingsEmptyState referenceImageId={referenceImageId} />;
|
return <RegionalGuidanceIPAdapterSettingsEmptyState referenceImageId={referenceImageId} />;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
|||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import { rgIPAdapterDeleted } from 'features/controlLayers/store/canvasSlice';
|
import { rgRefImageDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||||
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
|
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||||
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
@@ -35,7 +35,7 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
|
|||||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
const onDeleteIPAdapter = useCallback(() => {
|
const onDeleteIPAdapter = useCallback(() => {
|
||||||
dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
|
dispatch(rgRefImageDeleted({ entityIdentifier, referenceImageId }));
|
||||||
}, [dispatch, entityIdentifier, referenceImageId]);
|
}, [dispatch, entityIdentifier, referenceImageId]);
|
||||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImag
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex alignItems="center" gap={2} p={4}>
|
<Flex alignItems="center" gap={2} p={4}>
|
||||||
<Text textAlign="center" color="base.300">
|
<Text textAlign="center" color="base.300">
|
||||||
<Trans i18nKey="controlLayers.referenceImageEmptyState" components={components} />
|
<Trans i18nKey="controlLayers.referenceImageEmptyStateWithCanvasTab" components={components} />
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<input {...uploadApi.getUploadInputProps()} />
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import {
|
import {
|
||||||
buildSelectValidRegionalGuidanceActions,
|
buildSelectValidRegionalGuidanceActions,
|
||||||
useAddRegionalGuidanceIPAdapter,
|
useAddNegativePromptToExistingRegionalGuidance,
|
||||||
useAddRegionalGuidanceNegativePrompt,
|
useAddPositivePromptToExistingRegionalGuidance,
|
||||||
useAddRegionalGuidancePositivePrompt,
|
useAddRefImageToExistingRegionalGuidance,
|
||||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
@@ -15,9 +15,9 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
|||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isBusy = useCanvasIsBusy();
|
const isBusy = useCanvasIsBusy();
|
||||||
const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
|
const addRegionalGuidanceIPAdapter = useAddRefImageToExistingRegionalGuidance(entityIdentifier);
|
||||||
const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
|
const addRegionalGuidancePositivePrompt = useAddPositivePromptToExistingRegionalGuidance(entityIdentifier);
|
||||||
const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
|
const addRegionalGuidanceNegativePrompt = useAddNegativePromptToExistingRegionalGuidance(entityIdentifier);
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
|
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
|
||||||
[entityIdentifier]
|
[entityIdentifier]
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { InitialStateMainModelPicker } from './InitialStateMainModelPicker';
|
||||||
|
import { LaunchpadAddStyleReference } from './LaunchpadAddStyleReference';
|
||||||
|
import { LaunchpadEditImageButton } from './LaunchpadEditImageButton';
|
||||||
|
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||||
|
import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton';
|
||||||
|
|
||||||
|
export const CanvasLaunchpadPanel = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||||
|
<Flex flexDir="column" w="full" h="full" gap={4} px={14} maxW={768} pt="20%">
|
||||||
|
<Heading mb={4}>Edit and refine on Canvas.</Heading>
|
||||||
|
<Flex flexDir="column" gap={8}>
|
||||||
|
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||||
|
<InitialStateMainModelPicker />
|
||||||
|
<Flex flexDir="column" gap={2} justifyContent="center">
|
||||||
|
<Text>
|
||||||
|
Want to learn what prompts work best for each model?{' '}
|
||||||
|
<Button as="a" variant="link" href="#" size="sm">
|
||||||
|
Check our our Model Guide.
|
||||||
|
</Button>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Grid>
|
||||||
|
<LaunchpadGenerateFromTextButton />
|
||||||
|
<LaunchpadAddStyleReference />
|
||||||
|
<LaunchpadEditImageButton />
|
||||||
|
<LaunchpadUseALayoutImageButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CanvasLaunchpadPanel.displayName = 'CanvasLaunchpadPanel';
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
|
||||||
|
import { LaunchpadAddStyleReference } from 'features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference';
|
||||||
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||||
|
|
||||||
|
export const GenerateLaunchpadPanel = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const newCanvasSession = useCallback(() => {
|
||||||
|
dispatch(setActiveTab('canvas'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
|
||||||
|
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={14} maxW={768}>
|
||||||
|
<Heading mb={4}>Generate images from text prompts.</Heading>
|
||||||
|
<Flex flexDir="column" gap={8}>
|
||||||
|
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||||
|
<InitialStateMainModelPicker />
|
||||||
|
<Flex flexDir="column" gap={2} justifyContent="center">
|
||||||
|
<Text>
|
||||||
|
Want to learn what prompts work best for each model?{' '}
|
||||||
|
<Button as="a" variant="link" href="#" size="sm">
|
||||||
|
Check our our Model Guide.
|
||||||
|
</Button>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Grid>
|
||||||
|
<LaunchpadGenerateFromTextButton />
|
||||||
|
<LaunchpadAddStyleReference />
|
||||||
|
<Alert status="info" borderRadius="base" flexDir="column" gap={2} overflow="unset">
|
||||||
|
<Text fontSize="md" fontWeight="semibold">
|
||||||
|
Looking to get more control, edit, and iterate on your images?
|
||||||
|
</Text>
|
||||||
|
<Button variant="link" onClick={newCanvasSession}>
|
||||||
|
Navigate to Canvas for more capabilities.
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
GenerateLaunchpadPanel.displayName = 'GenerateLaunchpad';
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ButtonGroupProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
|
||||||
|
const edit = useCallback(() => {
|
||||||
|
newCanvasFromImage({
|
||||||
|
imageDTO,
|
||||||
|
type: 'raster_layer',
|
||||||
|
withInpaintMask: true,
|
||||||
|
getState,
|
||||||
|
dispatch,
|
||||||
|
});
|
||||||
|
}, [dispatch, getState, imageDTO]);
|
||||||
|
return (
|
||||||
|
<ButtonGroup isAttached={false} size="sm" {...rest}>
|
||||||
|
<Button onClick={edit} tooltip="Edit parts of this image with Inpainting">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ImageActions.displayName = 'ImageActions';
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Flex, FormControl, FormLabel, Icon } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
|
import { ModelPicker } from 'features/parameters/components/ModelPicker';
|
||||||
|
import { modelSelected } from 'features/parameters/store/actions';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { MdMoneyOff } from 'react-icons/md';
|
||||||
|
import { useMainModels } from 'services/api/hooks/modelsByType';
|
||||||
|
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
|
||||||
|
import { type AnyModelConfig, isCheckpointMainModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
|
export const InitialStateMainModelPicker = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [modelConfigs] = useMainModels();
|
||||||
|
const selectedModelConfig = useSelectedModelConfig();
|
||||||
|
const onChange = useCallback(
|
||||||
|
(modelConfig: AnyModelConfig) => {
|
||||||
|
dispatch(modelSelected(modelConfig));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFluxDevSelected = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedModelConfig &&
|
||||||
|
isCheckpointMainModelConfig(selectedModelConfig) &&
|
||||||
|
selectedModelConfig.config_path === 'flux-dev',
|
||||||
|
[selectedModelConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl orientation="vertical" alignItems="unset">
|
||||||
|
<FormLabel display="flex" fontSize="md" gap={2}>
|
||||||
|
Select your Model{' '}
|
||||||
|
{isFluxDevSelected && (
|
||||||
|
<InformationalPopover feature="fluxDevLicense" hideDisable={true}>
|
||||||
|
<Flex justifyContent="flex-start">
|
||||||
|
<Icon as={MdMoneyOff} />
|
||||||
|
</Flex>
|
||||||
|
</InformationalPopover>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<ModelPicker modelConfigs={modelConfigs} selectedModelConfig={selectedModelConfig} onChange={onChange} grouped />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
InitialStateMainModelPicker.displayName = 'InitialStateMainModelPicker';
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||||
|
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
|
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||||
|
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||||
|
import { addGlobalReferenceImageDndTarget, newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { PiUploadBold, PiUserCircleGearBold } from 'react-icons/pi';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
const dndTargetData = addGlobalReferenceImageDndTarget.getData();
|
||||||
|
|
||||||
|
export const LaunchpadAddStyleReference = memo(() => {
|
||||||
|
const { dispatch, getState } = useAppStore();
|
||||||
|
|
||||||
|
const uploadOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
onUpload: (imageDTO: ImageDTO) => {
|
||||||
|
const config = getDefaultRefImageConfig(getState);
|
||||||
|
config.image = imageDTOToImageWithDims(imageDTO);
|
||||||
|
dispatch(refImageAdded({ overrides: { config } }));
|
||||||
|
},
|
||||||
|
allowMultiple: false,
|
||||||
|
}) as const,
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadApi = useImageUploadButton(uploadOptions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||||
|
<Icon as={PiUserCircleGearBold} boxSize={8} color="base.500" />
|
||||||
|
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||||
|
<Heading size="sm">Add a Style Reference</Heading>
|
||||||
|
<Text color="base.300">Add an image to transfer its look.</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex position="absolute" right={3} bottom={3}>
|
||||||
|
<PiUploadBold />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Flex>
|
||||||
|
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||||
|
</LaunchpadButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
LaunchpadAddStyleReference.displayName = 'LaunchpadAddStyleReference';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Button, forwardRef } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const LaunchpadButton = memo(
|
||||||
|
forwardRef(({ children, ...rest }: ButtonProps, ref) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="outline"
|
||||||
|
display="flex"
|
||||||
|
position="relative"
|
||||||
|
alignItems="center"
|
||||||
|
borderWidth={1}
|
||||||
|
borderRadius="base"
|
||||||
|
p={4}
|
||||||
|
pt={6}
|
||||||
|
gap={2}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
LaunchpadButton.displayName = 'LaunchpadButton';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||||
|
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { PiPencilBold, PiUploadBold } from 'react-icons/pi';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
const NEW_CANVAS_OPTIONS = { type: 'raster_layer', withInpaintMask: true } as const;
|
||||||
|
|
||||||
|
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||||
|
|
||||||
|
export const LaunchpadEditImageButton = memo(() => {
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
|
||||||
|
const onUpload = useCallback(
|
||||||
|
(imageDTO: ImageDTO) => {
|
||||||
|
newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
|
||||||
|
},
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||||
|
<Icon as={PiPencilBold} boxSize={8} color="base.500" />
|
||||||
|
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||||
|
<Heading size="sm">Edit Image</Heading>
|
||||||
|
<Text color="base.300">Add an image to refine.</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex position="absolute" right={3} bottom={3}>
|
||||||
|
<PiUploadBold />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Flex>
|
||||||
|
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||||
|
</LaunchpadButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
LaunchpadEditImageButton.displayName = 'LaunchpadEditImageButton';
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { LaunchpadButton } from 'features/controlLayers/components/SimpleSession/LaunchpadButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const focusOnPrompt = () => {
|
||||||
|
const promptElement = document.getElementById('prompt');
|
||||||
|
if (promptElement instanceof HTMLTextAreaElement) {
|
||||||
|
promptElement.focus();
|
||||||
|
promptElement.select();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LaunchpadGenerateFromTextButton = memo(() => {
|
||||||
|
return (
|
||||||
|
<LaunchpadButton onClick={focusOnPrompt} position="relative" gap={8}>
|
||||||
|
<Icon as={PiTextAaBold} boxSize={8} color="base.500" />
|
||||||
|
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||||
|
<Heading size="sm">Generate from Text</Heading>
|
||||||
|
<Text color="base.300">Enter a prompt and Invoke.</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex position="absolute" right={3} bottom={3}>
|
||||||
|
<PiCursorTextBold />
|
||||||
|
</Flex>
|
||||||
|
</LaunchpadButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
LaunchpadGenerateFromTextButton.displayName = 'LaunchpadGenerateFromTextButton';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||||
|
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||||
|
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { PiRectangleDashedBold, PiUploadBold } from 'react-icons/pi';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
import { LaunchpadButton } from './LaunchpadButton';
|
||||||
|
|
||||||
|
const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
|
||||||
|
|
||||||
|
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||||
|
|
||||||
|
export const LaunchpadUseALayoutImageButton = memo(() => {
|
||||||
|
const { getState, dispatch } = useAppStore();
|
||||||
|
|
||||||
|
const onUpload = useCallback(
|
||||||
|
(imageDTO: ImageDTO) => {
|
||||||
|
newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
|
||||||
|
},
|
||||||
|
[dispatch, getState]
|
||||||
|
);
|
||||||
|
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||||
|
<Icon as={PiRectangleDashedBold} boxSize={8} color="base.500" />
|
||||||
|
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||||
|
<Heading size="sm">Use a Layout Image</Heading>
|
||||||
|
<Text color="base.300">Add an image to control composition.</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex position="absolute" right={3} bottom={3}>
|
||||||
|
<PiUploadBold />
|
||||||
|
<input {...uploadApi.getUploadInputProps()} />
|
||||||
|
</Flex>
|
||||||
|
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||||
|
</LaunchpadButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
LaunchpadUseALayoutImageButton.displayName = 'LaunchpadUseALayoutImageButton';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
const circleStyles: SystemStyleObject = {
|
||||||
|
circle: {
|
||||||
|
transitionProperty: 'none',
|
||||||
|
transitionDuration: '0s',
|
||||||
|
},
|
||||||
|
position: 'absolute',
|
||||||
|
top: 2,
|
||||||
|
right: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = { itemId: number; status: S['SessionQueueItem']['status'] } & CircularProgressProps;
|
||||||
|
|
||||||
|
export const QueueItemCircularProgress = memo(({ itemId, status, ...rest }: Props) => {
|
||||||
|
const { $progressData } = useCanvasSessionContext();
|
||||||
|
const { progressEvent } = useProgressData($progressData, itemId);
|
||||||
|
|
||||||
|
if (status !== 'in_progress') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={getProgressMessage(progressEvent)}>
|
||||||
|
<CircularProgress
|
||||||
|
size="14px"
|
||||||
|
color="invokeBlue.500"
|
||||||
|
thickness={14}
|
||||||
|
isIndeterminate={!progressEvent || progressEvent.percentage === null}
|
||||||
|
value={progressEvent?.percentage ? progressEvent.percentage * 100 : undefined}
|
||||||
|
sx={circleStyles}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
QueueItemCircularProgress.displayName = 'QueueItemCircularProgress';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { TextProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
import { DROP_SHADOW } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const QueueItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
|
||||||
|
return <Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>{`#${number}`}</Text>;
|
||||||
|
});
|
||||||
|
QueueItemNumber.displayName = 'QueueItemNumber';
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
useCanvasSessionContext,
|
||||||
|
useOutputImageDTO,
|
||||||
|
useProgressData,
|
||||||
|
} from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { ImageActions } from 'features/controlLayers/components/SimpleSession/ImageActions';
|
||||||
|
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
|
||||||
|
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
|
||||||
|
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
|
||||||
|
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||||
|
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { DndImage } from 'features/dnd/DndImage';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: S['SessionQueueItem'];
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sx = {
|
||||||
|
userSelect: 'none',
|
||||||
|
pos: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
h: 'full',
|
||||||
|
w: 'full',
|
||||||
|
} satisfies SystemStyleObject;
|
||||||
|
|
||||||
|
export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const imageDTO = useOutputImageDTO(item);
|
||||||
|
const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex id={getQueueItemElementId(item.item_id)} sx={sx}>
|
||||||
|
<QueueItemStatusLabel item={item} position="absolute" margin="auto" />
|
||||||
|
{imageDTO && <DndImage imageDTO={imageDTO} />}
|
||||||
|
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}
|
||||||
|
{imageDTO && <ImageActions imageDTO={imageDTO} position="absolute" top={1} right={2} />}
|
||||||
|
<QueueItemNumber number={number} position="absolute" top={1} left={2} />
|
||||||
|
<QueueItemCircularProgress
|
||||||
|
itemId={item.item_id}
|
||||||
|
status={item.status}
|
||||||
|
position="absolute"
|
||||||
|
top={1}
|
||||||
|
right={2}
|
||||||
|
size={8}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
QueueItemPreviewFull.displayName = 'QueueItemPreviewFull';
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
useCanvasSessionContext,
|
||||||
|
useOutputImageDTO,
|
||||||
|
useProgressData,
|
||||||
|
} from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
|
||||||
|
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
|
||||||
|
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
|
||||||
|
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||||
|
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { DndImage } from 'features/dnd/DndImage';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
const sx = {
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
pos: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
h: 108,
|
||||||
|
w: 108,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 'base',
|
||||||
|
bg: 'base.900',
|
||||||
|
'&[data-selected="true"]': {
|
||||||
|
borderColor: 'invokeBlue.300',
|
||||||
|
},
|
||||||
|
} satisfies SystemStyleObject;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: S['SessionQueueItem'];
|
||||||
|
number: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const { imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
|
||||||
|
const imageDTO = useOutputImageDTO(item);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
ctx.$selectedItemId.set(item.item_id);
|
||||||
|
}, [ctx.$selectedItemId, item.item_id]);
|
||||||
|
|
||||||
|
const onLoad = useCallback(() => {
|
||||||
|
ctx.onImageLoad(item.item_id);
|
||||||
|
}, [ctx, item.item_id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex id={getQueueItemElementId(item.item_id)} sx={sx} data-selected={isSelected} onClick={onClick}>
|
||||||
|
<QueueItemStatusLabel item={item} position="absolute" margin="auto" />
|
||||||
|
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} asThumbnail />}
|
||||||
|
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}
|
||||||
|
<QueueItemNumber number={number} position="absolute" top={0} left={1} />
|
||||||
|
<QueueItemCircularProgress itemId={item.item_id} status={item.status} position="absolute" top={1} right={2} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
QueueItemPreviewMini.displayName = 'QueueItemPreviewMini';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ImageProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Image } from '@invoke-ai/ui-library';
|
||||||
|
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
type Props = { itemId: number } & ImageProps;
|
||||||
|
|
||||||
|
export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const { progressImage } = useProgressData(ctx.$progressData, itemId);
|
||||||
|
|
||||||
|
if (!progressImage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
objectFit="contain"
|
||||||
|
maxH="full"
|
||||||
|
maxW="full"
|
||||||
|
draggable={false}
|
||||||
|
src={progressImage.dataURL}
|
||||||
|
width={progressImage.width}
|
||||||
|
height={progressImage.height}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
QueueItemProgressImage.displayName = 'QueueItemProgressImage';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { TextProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { DROP_SHADOW, getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = { itemId: number; status: S['SessionQueueItem']['status'] } & TextProps;
|
||||||
|
|
||||||
|
export const QueueItemProgressMessage = memo(({ itemId, status, ...rest }: Props) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const { progressEvent } = useProgressData(ctx.$progressData, itemId);
|
||||||
|
|
||||||
|
if (status === 'completed' || status === 'failed' || status === 'canceled') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>
|
||||||
|
Waiting to start...
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>
|
||||||
|
{getProgressMessage(progressEvent)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
QueueItemProgressMessage.displayName = 'QueueItemProgressMessage';
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { TextProps } from '@invoke-ai/ui-library';
|
||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = { item: S['SessionQueueItem'] } & TextProps;
|
||||||
|
|
||||||
|
export const QueueItemStatusLabel = memo(({ item, ...rest }: Props) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const { progressImage, imageLoaded } = useProgressData(ctx.$progressData, item.item_id);
|
||||||
|
|
||||||
|
if (progressImage || imageLoaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.status === 'pending') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="base.300" {...rest}>
|
||||||
|
Pending
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (item.status === 'canceled') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="warning.300" {...rest}>
|
||||||
|
Canceled
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (item.status === 'failed') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="error.300" {...rest}>
|
||||||
|
Failed
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.status === 'in_progress') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeBlue.300" {...rest}>
|
||||||
|
In Progress
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.status === 'completed') {
|
||||||
|
return (
|
||||||
|
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeGreen.300" {...rest}>
|
||||||
|
Completed
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
QueueItemStatusLabel.displayName = 'QueueItemStatusLabel';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
|
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
|
||||||
|
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
|
||||||
|
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const SimpleSession = memo(() => {
|
||||||
|
return (
|
||||||
|
<Tabs w="full" h="full" px={2}>
|
||||||
|
<TabList>
|
||||||
|
<Tab>Launchpad</Tab>
|
||||||
|
<Tab>Viewer</Tab>
|
||||||
|
<Tab>Generation Progress</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels w="full" h="full">
|
||||||
|
<TabPanel w="full" h="full" justifyContent="center">
|
||||||
|
<GenerateLaunchpadPanel />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel w="full" h="full">
|
||||||
|
<Flex flexDir="column" w="full" h="full">
|
||||||
|
<ViewerToolbar />
|
||||||
|
<ImageViewer />
|
||||||
|
</Flex>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel w="full" h="full">
|
||||||
|
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
|
||||||
|
<ProgressImage />
|
||||||
|
</Flex>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SimpleSession.displayName = 'SimpleSession';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
|
||||||
|
import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const SimpleSessionNoId = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={2} w="full" h="full" minW={0} minH={0}>
|
||||||
|
<StagingAreaHeader />
|
||||||
|
<Divider />
|
||||||
|
<StagingAreaNoItems />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import { StagingAreaContent } from 'features/controlLayers/components/SimpleSession/StagingAreaContent';
|
||||||
|
import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
|
||||||
|
import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
|
||||||
|
import { useStagingAreaKeyboardNav } from 'features/controlLayers/components/SimpleSession/use-staging-keyboard-nav';
|
||||||
|
import { memo, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const StagingArea = memo(() => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const hasItems = useStore(ctx.$hasItems);
|
||||||
|
useStagingAreaKeyboardNav();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return ctx.$selectedItemId.listen((id) => {
|
||||||
|
if (id !== null) {
|
||||||
|
document.getElementById(getQueueItemElementId(id))?.scrollIntoView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [ctx.$selectedItemId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={2} w="full" h="full" minW={0} minH={0}>
|
||||||
|
<StagingAreaHeader />
|
||||||
|
<Divider />
|
||||||
|
{hasItems && <StagingAreaContent />}
|
||||||
|
{!hasItems && <StagingAreaNoItems />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
StagingArea.displayName = 'StagingArea';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
|
||||||
|
import { StagingAreaSelectedItem } from 'features/controlLayers/components/SimpleSession/StagingAreaSelectedItem';
|
||||||
|
import { SimpleStagingAreaToolbar } from 'features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const StagingAreaContent = memo(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex position="relative" w="full" h="full" maxH="full" alignItems="center" justifyContent="center" minH={0}>
|
||||||
|
<StagingAreaSelectedItem />
|
||||||
|
</Flex>
|
||||||
|
<Divider />
|
||||||
|
<Flex position="relative" maxW="full" w="full" h={108} flexShrink={0}>
|
||||||
|
<StagingAreaItemsList />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={2} w="full" justifyContent="safe center">
|
||||||
|
<SimpleStagingAreaToolbar />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
StagingAreaContent.displayName = 'StagingAreaContent';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const StagingAreaHeader = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex gap={2} w="full" alignItems="center" px={2}>
|
||||||
|
<Heading size="sm">Review Session</Heading>
|
||||||
|
<Spacer />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
StagingAreaHeader.displayName = 'StagingAreaHeader';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
|
||||||
|
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { memo, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const StagingAreaItemsList = memo(() => {
|
||||||
|
const canvasManager = useCanvasManagerSafe();
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const items = useStore(ctx.$items);
|
||||||
|
const selectedItemId = useStore(ctx.$selectedItemId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasManager.stagingArea.connectToSession(ctx.$selectedItemId, ctx.$progressData);
|
||||||
|
}, [canvasManager, ctx.$progressData, ctx.$selectedItemId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollableContent overflowX="scroll" overflowY="hidden">
|
||||||
|
<Flex gap={2} w="full" h="full" justifyContent="safe center">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<QueueItemPreviewMini
|
||||||
|
key={`${item.item_id}-mini`}
|
||||||
|
item={item}
|
||||||
|
number={i + 1}
|
||||||
|
isSelected={selectedItemId === item.item_id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ScrollableContent>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
StagingAreaItemsList.displayName = 'StagingAreaItemsList';
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const StagingAreaNoItems = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
|
<Text>No generations</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
StagingAreaNoItems.displayName = 'StagingAreaNoItems';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||||
|
import { QueueItemPreviewFull } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewFull';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const StagingAreaSelectedItem = memo(() => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const selectedItem = useStore(ctx.$selectedItem);
|
||||||
|
const selectedItemIndex = useStore(ctx.$selectedItemIndex);
|
||||||
|
|
||||||
|
if (selectedItem && selectedItemIndex !== null) {
|
||||||
|
return (
|
||||||
|
<QueueItemPreviewFull key={`${selectedItem.item_id}-full`} item={selectedItem} number={selectedItemIndex + 1} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text>No generation selected</Text>;
|
||||||
|
});
|
||||||
|
StagingAreaSelectedItem.displayName = 'StagingAreaSelectedItem';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const UpscalingLaunchpadPanel = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
|
||||||
|
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={14} maxW={768}>
|
||||||
|
<Heading mb={4}>Upscale and add detail.</Heading>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
UpscalingLaunchpadPanel.displayName = 'UpscalingLaunchpadPanel';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const WorkflowsLaunchpadPanel = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
|
||||||
|
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={14} maxW={768}>
|
||||||
|
<Heading mb={4}>Go deep with Workflows.</Heading>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
WorkflowsLaunchpadPanel.displayName = 'WorkflowsLaunchpadPanel';
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared';
|
||||||
|
import type { ProgressImage } from 'features/nodes/types/common';
|
||||||
|
import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores';
|
||||||
|
import { atom, computed, effect, map, subscribeKeys } from 'nanostores';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { getImageDTOSafe } from 'services/api/endpoints/images';
|
||||||
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
|
import type { ImageDTO, S } from 'services/api/types';
|
||||||
|
import { $socket } from 'services/events/stores';
|
||||||
|
import { assert, objectEntries } from 'tsafe';
|
||||||
|
|
||||||
|
export type ProgressData = {
|
||||||
|
itemId: number;
|
||||||
|
progressEvent: S['InvocationProgressEvent'] | null;
|
||||||
|
progressImage: ProgressImage | null;
|
||||||
|
imageDTO: ImageDTO | null;
|
||||||
|
imageLoaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitialProgressData = (itemId: number): ProgressData => ({
|
||||||
|
itemId,
|
||||||
|
progressEvent: null,
|
||||||
|
progressImage: null,
|
||||||
|
imageDTO: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useProgressData = ($progressData: ProgressDataMap, itemId: number): ProgressData => {
|
||||||
|
const getInitialValue = useCallback(
|
||||||
|
() => $progressData.get()[itemId] ?? getInitialProgressData(itemId),
|
||||||
|
[$progressData, itemId]
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState(getInitialValue);
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = subscribeKeys($progressData, [itemId], (data) => {
|
||||||
|
const progressData = data[itemId];
|
||||||
|
if (!progressData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(progressData);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsub();
|
||||||
|
};
|
||||||
|
}, [$progressData, itemId]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProgress = ($progressData: ProgressDataMap, data: S['InvocationProgressEvent']) => {
|
||||||
|
const progressData = $progressData.get();
|
||||||
|
const current = progressData[data.item_id];
|
||||||
|
if (current) {
|
||||||
|
const next = { ...current };
|
||||||
|
next.progressEvent = data;
|
||||||
|
if (data.image) {
|
||||||
|
next.progressImage = data.image;
|
||||||
|
}
|
||||||
|
$progressData.set({
|
||||||
|
...progressData,
|
||||||
|
[data.item_id]: next,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$progressData.set({
|
||||||
|
...progressData,
|
||||||
|
[data.item_id]: {
|
||||||
|
itemId: data.item_id,
|
||||||
|
progressEvent: data,
|
||||||
|
progressImage: data.image ?? null,
|
||||||
|
imageDTO: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProgressDataMap = MapStore<Record<number, ProgressData | undefined>>;
|
||||||
|
|
||||||
|
type CanvasSessionContextValue = {
|
||||||
|
session: { id: string; type: 'simple' | 'advanced' };
|
||||||
|
$items: Atom<S['SessionQueueItem'][]>;
|
||||||
|
$itemCount: Atom<number>;
|
||||||
|
$hasItems: Atom<boolean>;
|
||||||
|
$progressData: ProgressDataMap;
|
||||||
|
$selectedItemId: WritableAtom<number | null>;
|
||||||
|
$selectedItem: Atom<S['SessionQueueItem'] | null>;
|
||||||
|
$selectedItemIndex: Atom<number | null>;
|
||||||
|
$selectedItemOutputImageDTO: Atom<ImageDTO | null>;
|
||||||
|
$autoSwitch: WritableAtom<boolean>;
|
||||||
|
selectNext: () => void;
|
||||||
|
selectPrev: () => void;
|
||||||
|
selectFirst: () => void;
|
||||||
|
selectLast: () => void;
|
||||||
|
onImageLoad: (itemId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CanvasSessionContext = createContext<CanvasSessionContextValue | null>(null);
|
||||||
|
|
||||||
|
export const CanvasSessionContextProvider = memo(
|
||||||
|
({ id, type, children }: PropsWithChildren<{ id: string; type: 'simple' | 'advanced' }>) => {
|
||||||
|
/**
|
||||||
|
* For best performance and interop with the Canvas, which is outside react but needs to interact with the react
|
||||||
|
* app, all canvas session state is packaged as nanostores atoms. The trickiest part is syncing the queue items
|
||||||
|
* with a nanostores atom.
|
||||||
|
*/
|
||||||
|
const session = useMemo(() => ({ type, id }), [type, id]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App store
|
||||||
|
*/
|
||||||
|
const store = useAppStore();
|
||||||
|
|
||||||
|
const socket = useStore($socket);
|
||||||
|
const $lastCompletedItemId = useState(() => atom<number | null>(null))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
|
||||||
|
* and kept in sync with it via a redux subscription.
|
||||||
|
*/
|
||||||
|
const $items = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether auto-switch is enabled.
|
||||||
|
*/
|
||||||
|
const $autoSwitch = useState(() => atom(true))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
|
||||||
|
* output images have fully loaded.
|
||||||
|
*/
|
||||||
|
const $lastLoadedItemId = useState(() => atom<number | null>(null))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ephemeral store of progress events and images for all items in the current session.
|
||||||
|
*/
|
||||||
|
const $progressData = useState(() => map<StoreValue<ProgressDataMap>>({}))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected queue item's ID, or null if one is not selected.
|
||||||
|
*/
|
||||||
|
const $selectedItemId = useState(() => atom<number | null>(null))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of items. Computed from the queue items array.
|
||||||
|
*/
|
||||||
|
const $itemCount = useState(() => computed([$items], (items) => items.length))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there are any items. Computed from the queue items array.
|
||||||
|
*/
|
||||||
|
const $hasItems = useState(() => computed([$items], (items) => items.length > 0))[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected queue item, or null if one is not selected.
|
||||||
|
*/
|
||||||
|
const $selectedItem = useState(() =>
|
||||||
|
computed([$items, $selectedItemId], (items, selectedItemId) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (selectedItemId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
|
||||||
|
})
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected queue item's index in the list of items, or null if one is not selected.
|
||||||
|
*/
|
||||||
|
const $selectedItemIndex = useState(() =>
|
||||||
|
computed([$items, $selectedItemId], (items, selectedItemId) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (selectedItemId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
|
||||||
|
})
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected queue item's output image name, or null if one is not selected or there is no output
|
||||||
|
* image recorded.
|
||||||
|
*/
|
||||||
|
const $selectedItemOutputImageDTO = useState(() =>
|
||||||
|
computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
|
||||||
|
if (selectedItemId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const datum = progressData[selectedItemId];
|
||||||
|
if (!datum) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return datum.imageDTO;
|
||||||
|
})
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A redux selector to select all queue items from the RTK Query cache. It's important that this returns stable
|
||||||
|
* references if possible to reduce re-renders. All derivations of the queue items (e.g. filtering out canceled
|
||||||
|
* items) should be done in a nanostores computed.
|
||||||
|
*/
|
||||||
|
const selectQueueItems = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
queueApi.endpoints.listAllQueueItems.select({ destination: session.id }),
|
||||||
|
({ data }) => data ?? EMPTY_ARRAY
|
||||||
|
),
|
||||||
|
[session.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectNext = useCallback(() => {
|
||||||
|
const selectedItemId = $selectedItemId.get();
|
||||||
|
if (selectedItemId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = $items.get();
|
||||||
|
const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
|
||||||
|
const nextIndex = (currentIndex + 1) % items.length;
|
||||||
|
const nextItem = items[nextIndex];
|
||||||
|
if (!nextItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$selectedItemId.set(nextItem.item_id);
|
||||||
|
}, [$items, $selectedItemId]);
|
||||||
|
|
||||||
|
const selectPrev = useCallback(() => {
|
||||||
|
const selectedItemId = $selectedItemId.get();
|
||||||
|
if (selectedItemId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = $items.get();
|
||||||
|
const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
|
||||||
|
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
||||||
|
const prevItem = items[prevIndex];
|
||||||
|
if (!prevItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$selectedItemId.set(prevItem.item_id);
|
||||||
|
}, [$items, $selectedItemId]);
|
||||||
|
|
||||||
|
const selectFirst = useCallback(() => {
|
||||||
|
const items = $items.get();
|
||||||
|
const first = items.at(0);
|
||||||
|
if (!first) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$selectedItemId.set(first.item_id);
|
||||||
|
}, [$items, $selectedItemId]);
|
||||||
|
|
||||||
|
const selectLast = useCallback(() => {
|
||||||
|
const items = $items.get();
|
||||||
|
const last = items.at(-1);
|
||||||
|
if (!last) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$selectedItemId.set(last.item_id);
|
||||||
|
}, [$items, $selectedItemId]);
|
||||||
|
|
||||||
|
const onImageLoad = useCallback(
|
||||||
|
(itemId: number) => {
|
||||||
|
const progressData = $progressData.get();
|
||||||
|
const current = progressData[itemId];
|
||||||
|
if (current) {
|
||||||
|
const next = { ...current, imageLoaded: true };
|
||||||
|
$progressData.setKey(itemId, next);
|
||||||
|
} else {
|
||||||
|
$progressData.setKey(itemId, {
|
||||||
|
...getInitialProgressData(itemId),
|
||||||
|
imageLoaded: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($lastCompletedItemId.get() === itemId) {
|
||||||
|
$selectedItemId.set(itemId);
|
||||||
|
$lastCompletedItemId.set(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[$lastCompletedItemId, $progressData, $selectedItemId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up socket listeners
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProgress = (data: S['InvocationProgressEvent']) => {
|
||||||
|
if (data.destination !== session.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProgress($progressData, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
|
||||||
|
if (data.destination !== session.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
$lastCompletedItemId.set(data.item_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('invocation_progress', onProgress);
|
||||||
|
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off('invocation_progress', onProgress);
|
||||||
|
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
|
||||||
|
};
|
||||||
|
}, [$autoSwitch, $lastCompletedItemId, $progressData, $selectedItemId, session.id, socket]);
|
||||||
|
|
||||||
|
// Set up state subscriptions and effects
|
||||||
|
useEffect(() => {
|
||||||
|
let _prevItems: readonly S['SessionQueueItem'][] = [];
|
||||||
|
// Seed the $items atom with the initial query cache state
|
||||||
|
$items.set(selectQueueItems(store.getState()));
|
||||||
|
|
||||||
|
// Manually keep the $items atom in sync as the query cache is updated
|
||||||
|
const unsubReduxSyncToItemsAtom = store.subscribe(() => {
|
||||||
|
const prevItems = $items.get();
|
||||||
|
const items = selectQueueItems(store.getState());
|
||||||
|
if (items !== prevItems) {
|
||||||
|
_prevItems = prevItems;
|
||||||
|
$items.set(items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle cases that could result in a nonexistent queue item being selected.
|
||||||
|
const unsubEnsureSelectedItemIdExists = effect([$items, $selectedItemId], (items, selectedItemId) => {
|
||||||
|
// If there are no items, cannot have a selected item.
|
||||||
|
if (items.length === 0) {
|
||||||
|
$selectedItemId.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If there is no selected item but there are items, select the first one.
|
||||||
|
if (selectedItemId === null && items.length > 0) {
|
||||||
|
$selectedItemId.set(items[0]?.item_id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
|
||||||
|
// the above case, selecting the first item if there are any.
|
||||||
|
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
|
||||||
|
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
|
||||||
|
if (prevIndex >= items.length) {
|
||||||
|
prevIndex = items.length - 1;
|
||||||
|
}
|
||||||
|
const nextItem = items[prevIndex];
|
||||||
|
$selectedItemId.set(nextItem?.item_id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up the progress data when a queue item is discarded.
|
||||||
|
const unsubCleanUpProgressData = $items.subscribe(async (items) => {
|
||||||
|
const progressData = $progressData.get();
|
||||||
|
|
||||||
|
const toDelete: number[] = [];
|
||||||
|
const toUpdate: ProgressData[] = [];
|
||||||
|
|
||||||
|
for (const [id, datum] of objectEntries(progressData)) {
|
||||||
|
if (!datum) {
|
||||||
|
toDelete.push(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item = items.find(({ item_id }) => item_id === datum.itemId);
|
||||||
|
if (!item) {
|
||||||
|
toDelete.push(datum.itemId);
|
||||||
|
} else if (item.status === 'canceled' || item.status === 'failed') {
|
||||||
|
toUpdate[datum.itemId] = {
|
||||||
|
...datum,
|
||||||
|
progressEvent: null,
|
||||||
|
progressImage: null,
|
||||||
|
imageDTO: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const datum = progressData[item.item_id];
|
||||||
|
|
||||||
|
if (datum) {
|
||||||
|
if (datum.imageDTO) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const outputImageName = getOutputImageName(item);
|
||||||
|
if (!outputImageName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||||
|
if (!imageDTO) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toUpdate.push({
|
||||||
|
...datum,
|
||||||
|
imageDTO,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const outputImageName = getOutputImageName(item);
|
||||||
|
if (!outputImageName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const imageDTO = await getImageDTOSafe(outputImageName);
|
||||||
|
if (!imageDTO) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toUpdate.push({
|
||||||
|
...getInitialProgressData(item.item_id),
|
||||||
|
imageDTO,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const itemId of toDelete) {
|
||||||
|
$progressData.setKey(itemId, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const datum of toUpdate) {
|
||||||
|
$progressData.setKey(datum.itemId, datum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
|
||||||
|
// of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
|
||||||
|
// is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
|
||||||
|
// `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
|
||||||
|
//
|
||||||
|
// TODO: This isn't perfect... we set $lastLoadedItemId in the mini preview component, but the full view
|
||||||
|
// component still needs to retrieve the image from the browser cache... can result in a flash of the progress
|
||||||
|
// image...
|
||||||
|
const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
|
||||||
|
if (lastLoadedItemId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($autoSwitch.get()) {
|
||||||
|
$selectedItemId.set(lastLoadedItemId);
|
||||||
|
}
|
||||||
|
$lastLoadedItemId.set(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
|
||||||
|
// doesn't know we care about it.
|
||||||
|
const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
|
||||||
|
queueApi.endpoints.listAllQueueItems.initiate({ destination: session.id })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
|
||||||
|
return () => {
|
||||||
|
unsubHandleAutoSwitch();
|
||||||
|
unsubQueueItemsQuery();
|
||||||
|
unsubReduxSyncToItemsAtom();
|
||||||
|
unsubEnsureSelectedItemIdExists();
|
||||||
|
unsubCleanUpProgressData();
|
||||||
|
$items.set([]);
|
||||||
|
$progressData.set({});
|
||||||
|
$selectedItemId.set(null);
|
||||||
|
};
|
||||||
|
}, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
|
||||||
|
|
||||||
|
const value = useMemo<CanvasSessionContextValue>(
|
||||||
|
() => ({
|
||||||
|
session,
|
||||||
|
$items,
|
||||||
|
$hasItems,
|
||||||
|
$progressData,
|
||||||
|
$selectedItemId,
|
||||||
|
$autoSwitch,
|
||||||
|
$selectedItem,
|
||||||
|
$selectedItemIndex,
|
||||||
|
$selectedItemOutputImageDTO,
|
||||||
|
$itemCount,
|
||||||
|
selectNext,
|
||||||
|
selectPrev,
|
||||||
|
selectFirst,
|
||||||
|
selectLast,
|
||||||
|
onImageLoad,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
$autoSwitch,
|
||||||
|
$items,
|
||||||
|
$hasItems,
|
||||||
|
$progressData,
|
||||||
|
$selectedItem,
|
||||||
|
$selectedItemId,
|
||||||
|
$selectedItemIndex,
|
||||||
|
session,
|
||||||
|
$selectedItemOutputImageDTO,
|
||||||
|
$itemCount,
|
||||||
|
selectNext,
|
||||||
|
selectPrev,
|
||||||
|
selectFirst,
|
||||||
|
selectLast,
|
||||||
|
onImageLoad,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <CanvasSessionContext.Provider value={value}>{children}</CanvasSessionContext.Provider>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
|
||||||
|
|
||||||
|
export const useCanvasSessionContext = () => {
|
||||||
|
const ctx = useContext(CanvasSessionContext);
|
||||||
|
assert(ctx !== null, "'useCanvasSessionContext' must be used within a CanvasSessionContextProvider");
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
|
||||||
|
const ctx = useCanvasSessionContext();
|
||||||
|
const $imageDTO = useState(() =>
|
||||||
|
computed([ctx.$progressData], (progressData) => progressData[item.item_id]?.imageDTO ?? null)
|
||||||
|
)[0];
|
||||||
|
const imageDTO = useStore($imageDTO);
|
||||||
|
|
||||||
|
return imageDTO;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user