mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-21 05:58:05 -05:00
Compare commits
23 Commits
v4.2.9.dev
...
v4.2.9.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db7b48cd8 | ||
|
|
ea014c66ac | ||
|
|
25918c28aa | ||
|
|
0c60469401 | ||
|
|
f1aa50f447 | ||
|
|
a413b261f0 | ||
|
|
4a1a6639f6 | ||
|
|
201c370ca1 | ||
|
|
d070c7c726 | ||
|
|
e38e20a992 | ||
|
|
39a94ec70e | ||
|
|
c7bfae2d1e | ||
|
|
e7944c427d | ||
|
|
48ed4e120d | ||
|
|
a5b038a1b1 | ||
|
|
dc752c98b0 | ||
|
|
85a47cc6fe | ||
|
|
6450f42cfa | ||
|
|
3876f71ff4 | ||
|
|
cf819e8eab | ||
|
|
2217fb8485 | ||
|
|
43652e830a | ||
|
|
a3417bf81d |
@@ -20,7 +20,6 @@ from typing import (
|
|||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import semver
|
import semver
|
||||||
@@ -80,7 +79,7 @@ class UIConfigBase(BaseModel):
|
|||||||
version: str = Field(
|
version: str = Field(
|
||||||
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
|
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
|
||||||
)
|
)
|
||||||
node_pack: Optional[str] = Field(default=None, description="Whether or not this is a custom node")
|
node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes")
|
||||||
classification: Classification = Field(default=Classification.Stable, description="The node's classification")
|
classification: Classification = Field(default=Classification.Stable, description="The node's classification")
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
@@ -230,18 +229,16 @@ class BaseInvocation(ABC, BaseModel):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
|
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
|
||||||
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
|
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
|
||||||
uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None))
|
if title := model_class.UIConfig.title:
|
||||||
if uiconfig is not None:
|
schema["title"] = title
|
||||||
if uiconfig.title is not None:
|
if tags := model_class.UIConfig.tags:
|
||||||
schema["title"] = uiconfig.title
|
schema["tags"] = tags
|
||||||
if uiconfig.tags is not None:
|
if category := model_class.UIConfig.category:
|
||||||
schema["tags"] = uiconfig.tags
|
schema["category"] = category
|
||||||
if uiconfig.category is not None:
|
if node_pack := model_class.UIConfig.node_pack:
|
||||||
schema["category"] = uiconfig.category
|
schema["node_pack"] = node_pack
|
||||||
if uiconfig.node_pack is not None:
|
schema["classification"] = model_class.UIConfig.classification
|
||||||
schema["node_pack"] = uiconfig.node_pack
|
schema["version"] = model_class.UIConfig.version
|
||||||
schema["classification"] = uiconfig.classification
|
|
||||||
schema["version"] = uiconfig.version
|
|
||||||
if "required" not in schema or not isinstance(schema["required"], list):
|
if "required" not in schema or not isinstance(schema["required"], list):
|
||||||
schema["required"] = []
|
schema["required"] = []
|
||||||
schema["class"] = "invocation"
|
schema["class"] = "invocation"
|
||||||
@@ -312,7 +309,7 @@ class BaseInvocation(ABC, BaseModel):
|
|||||||
json_schema_extra={"field_kind": FieldKind.NodeAttribute},
|
json_schema_extra={"field_kind": FieldKind.NodeAttribute},
|
||||||
)
|
)
|
||||||
|
|
||||||
UIConfig: ClassVar[Type[UIConfigBase]]
|
UIConfig: ClassVar[UIConfigBase]
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
protected_namespaces=(),
|
protected_namespaces=(),
|
||||||
@@ -441,30 +438,25 @@ def invocation(
|
|||||||
validate_fields(cls.model_fields, invocation_type)
|
validate_fields(cls.model_fields, invocation_type)
|
||||||
|
|
||||||
# Add OpenAPI schema extras
|
# Add OpenAPI schema extras
|
||||||
uiconfig_name = cls.__qualname__ + ".UIConfig"
|
uiconfig: dict[str, Any] = {}
|
||||||
if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name:
|
uiconfig["title"] = title
|
||||||
cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {})
|
uiconfig["tags"] = tags
|
||||||
cls.UIConfig.title = title
|
uiconfig["category"] = category
|
||||||
cls.UIConfig.tags = tags
|
uiconfig["classification"] = classification
|
||||||
cls.UIConfig.category = category
|
# The node pack is the module name - will be "invokeai" for built-in nodes
|
||||||
cls.UIConfig.classification = classification
|
uiconfig["node_pack"] = cls.__module__.split(".")[0]
|
||||||
|
|
||||||
# Grab the node pack's name from the module name, if it's a custom node
|
|
||||||
is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations"
|
|
||||||
if is_custom_node:
|
|
||||||
cls.UIConfig.node_pack = cls.__module__.split(".")[0]
|
|
||||||
else:
|
|
||||||
cls.UIConfig.node_pack = None
|
|
||||||
|
|
||||||
if version is not None:
|
if version is not None:
|
||||||
try:
|
try:
|
||||||
semver.Version.parse(version)
|
semver.Version.parse(version)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
|
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
|
||||||
cls.UIConfig.version = version
|
uiconfig["version"] = version
|
||||||
else:
|
else:
|
||||||
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
|
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
|
||||||
cls.UIConfig.version = "1.0.0"
|
uiconfig["version"] = "1.0.0"
|
||||||
|
|
||||||
|
cls.UIConfig = UIConfigBase(**uiconfig)
|
||||||
|
|
||||||
if use_cache is not None:
|
if use_cache is not None:
|
||||||
cls.model_fields["use_cache"].default = use_cache
|
cls.model_fields["use_cache"].default = use_cache
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"@roarr/browser-log-writer": "^1.3.0",
|
"@roarr/browser-log-writer": "^1.3.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chakra-react-select": "^4.9.1",
|
"chakra-react-select": "^4.9.1",
|
||||||
|
"cmdk": "^1.0.0",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"fracturedjsonjs": "^4.0.2",
|
"fracturedjsonjs": "^4.0.2",
|
||||||
@@ -92,7 +93,6 @@
|
|||||||
"react-icons": "^5.2.1",
|
"react-icons": "^5.2.1",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
"react-resizable-panels": "^2.0.23",
|
"react-resizable-panels": "^2.0.23",
|
||||||
"react-select": "5.8.0",
|
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
"react-virtuoso": "^4.9.0",
|
"react-virtuoso": "^4.9.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
|
|||||||
329
invokeai/frontend/web/pnpm-lock.yaml
generated
329
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ dependencies:
|
|||||||
chakra-react-select:
|
chakra-react-select:
|
||||||
specifier: ^4.9.1
|
specifier: ^4.9.1
|
||||||
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
cmdk:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
compare-versions:
|
compare-versions:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1
|
version: 6.1.1
|
||||||
@@ -125,9 +128,6 @@ dependencies:
|
|||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^2.0.23
|
specifier: ^2.0.23
|
||||||
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
|
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
|
||||||
react-select:
|
|
||||||
specifier: 5.8.0
|
|
||||||
version: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
react-use:
|
react-use:
|
||||||
specifier: ^17.5.1
|
specifier: ^17.5.1
|
||||||
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
|
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
|
||||||
@@ -2053,7 +2053,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@chakra-ui/dom-utils': 2.1.0
|
'@chakra-ui/dom-utils': 2.1.0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1)
|
react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3784,6 +3784,288 @@ packages:
|
|||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/primitive@1.0.1:
|
||||||
|
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
aria-hidden: 1.2.4
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.4
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
/@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
|
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5210,7 +5492,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.3.3
|
'@types/react': 18.3.3
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/react-transition-group@4.4.10:
|
/@types/react-transition-group@4.4.10:
|
||||||
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
|
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
|
||||||
@@ -6268,6 +6549,21 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- '@types/react-dom'
|
||||||
|
dev: false
|
||||||
|
|
||||||
/color-convert@1.9.3:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9712,8 +10008,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1):
|
/react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==}
|
resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
@@ -9867,6 +10163,25 @@ packages:
|
|||||||
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
react: 18.3.1
|
||||||
|
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
tslib: 2.7.0
|
||||||
|
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
|
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
|
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -1734,6 +1734,8 @@
|
|||||||
"unlocked": "Unlocked",
|
"unlocked": "Unlocked",
|
||||||
"deleteSelected": "Delete Selected",
|
"deleteSelected": "Delete Selected",
|
||||||
"deleteAll": "Delete All",
|
"deleteAll": "Delete All",
|
||||||
|
"flipHorizontal": "Flip Horizontal",
|
||||||
|
"flipVertical": "Flip Vertical",
|
||||||
"fill": {
|
"fill": {
|
||||||
"fillStyle": "Fill Style",
|
"fillStyle": "Fill Style",
|
||||||
"solid": "Solid",
|
"solid": "Solid",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
|
|||||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
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 SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
|
||||||
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 { AppContent } from 'features/ui/components/AppContent';
|
import { AppContent } from 'features/ui/components/AppContent';
|
||||||
@@ -121,6 +123,8 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
|||||||
<StylePresetModal />
|
<StylePresetModal />
|
||||||
<ClearQueueConfirmationsAlertDialog />
|
<ClearQueueConfirmationsAlertDialog />
|
||||||
<PreselectedImage selectedImage={selectedImage} />
|
<PreselectedImage selectedImage={selectedImage} />
|
||||||
|
<SettingsModal />
|
||||||
|
<RefreshAfterResetModal />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const ChangeBoardModal = () => {
|
|||||||
acceptCallback={handleChangeBoard}
|
acceptCallback={handleChangeBoard}
|
||||||
acceptButtonText={t('boards.move')}
|
acceptButtonText={t('boards.move')}
|
||||||
cancelButtonText={t('boards.cancel')}
|
cancelButtonText={t('boards.cancel')}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex flexDir="column" gap={4}>
|
<Flex flexDir="column" gap={4}>
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const marks = [
|
|||||||
mapOpacityToSliderValue(1),
|
mapOpacityToSliderValue(1),
|
||||||
];
|
];
|
||||||
|
|
||||||
const sliderDefaultValue = mapOpacityToSliderValue(100);
|
const sliderDefaultValue = mapOpacityToSliderValue(1);
|
||||||
|
|
||||||
const snapCandidates = marks.slice(1, marks.length - 1);
|
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||||
|
|
||||||
@@ -134,7 +134,11 @@ export const SelectedEntityOpacity = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<FormControl w="min-content" gap={2}>
|
<FormControl
|
||||||
|
w="min-content"
|
||||||
|
gap={2}
|
||||||
|
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||||
|
>
|
||||||
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -152,7 +156,6 @@ export const SelectedEntityOpacity = memo(() => {
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
clampValueOnBlur={false}
|
clampValueOnBlur={false}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
|
||||||
>
|
>
|
||||||
<NumberInputField paddingInlineEnd={7} />
|
<NumberInputField paddingInlineEnd={7} />
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
@@ -164,6 +167,7 @@ export const SelectedEntityOpacity = memo(() => {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
insetInlineEnd={0}
|
insetInlineEnd={0}
|
||||||
h="full"
|
h="full"
|
||||||
|
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</NumberInput>
|
</NumberInput>
|
||||||
@@ -181,6 +185,7 @@ export const SelectedEntityOpacity = memo(() => {
|
|||||||
marks={marks}
|
marks={marks}
|
||||||
formatValue={formatSliderValue}
|
formatValue={formatSliderValue}
|
||||||
alwaysShowMarks
|
alwaysShowMarks
|
||||||
|
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||||
/>
|
/>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export const CanvasEditor = memo(() => {
|
|||||||
<Flex
|
<Flex
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
layerStyle="first"
|
|
||||||
p={2}
|
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
position="relative"
|
position="relative"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
@@ -32,14 +30,14 @@ export const CanvasEditor = memo(() => {
|
|||||||
>
|
>
|
||||||
<ControlLayersToolbar />
|
<ControlLayersToolbar />
|
||||||
<StageComponent />
|
<StageComponent />
|
||||||
<Flex position="absolute" bottom={16} gap={2} align="center" justify="center">
|
<Flex position="absolute" bottom={8} gap={2} align="center" justify="center">
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<StagingAreaIsStagingGate>
|
<StagingAreaIsStagingGate>
|
||||||
<StagingAreaToolbar />
|
<StagingAreaToolbar />
|
||||||
</StagingAreaIsStagingGate>
|
</StagingAreaIsStagingGate>
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex position="absolute" bottom={16}>
|
<Flex position="absolute" bottom={8}>
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<Filter />
|
<Filter />
|
||||||
<Transform />
|
<Transform />
|
||||||
|
|||||||
@@ -15,18 +15,6 @@ export const Filter = memo(() => {
|
|||||||
const isFiltering = useStore(canvasManager.filter.$isFiltering);
|
const isFiltering = useStore(canvasManager.filter.$isFiltering);
|
||||||
const isProcessing = useStore(canvasManager.filter.$isProcessing);
|
const isProcessing = useStore(canvasManager.filter.$isProcessing);
|
||||||
|
|
||||||
const previewFilter = useCallback(() => {
|
|
||||||
canvasManager.filter.previewFilter();
|
|
||||||
}, [canvasManager.filter]);
|
|
||||||
|
|
||||||
const applyFilter = useCallback(() => {
|
|
||||||
canvasManager.filter.applyFilter();
|
|
||||||
}, [canvasManager.filter]);
|
|
||||||
|
|
||||||
const cancelFilter = useCallback(() => {
|
|
||||||
canvasManager.filter.cancelFilter();
|
|
||||||
}, [canvasManager.filter]);
|
|
||||||
|
|
||||||
const onChangeFilterConfig = useCallback(
|
const onChangeFilterConfig = useCallback(
|
||||||
(filterConfig: FilterConfig) => {
|
(filterConfig: FilterConfig) => {
|
||||||
canvasManager.filter.$config.set(filterConfig);
|
canvasManager.filter.$config.set(filterConfig);
|
||||||
@@ -65,24 +53,27 @@ export const Filter = memo(() => {
|
|||||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||||
<Button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
leftIcon={<PiShootingStarBold />}
|
leftIcon={<PiShootingStarBold />}
|
||||||
onClick={previewFilter}
|
onClick={canvasManager.filter.previewFilter}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('controlLayers.filter.preview')}
|
loadingText={t('controlLayers.filter.preview')}
|
||||||
>
|
>
|
||||||
{t('controlLayers.filter.preview')}
|
{t('controlLayers.filter.preview')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
leftIcon={<PiCheckBold />}
|
leftIcon={<PiCheckBold />}
|
||||||
onClick={applyFilter}
|
onClick={canvasManager.filter.applyFilter}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('controlLayers.filter.apply')}
|
loadingText={t('controlLayers.filter.apply')}
|
||||||
>
|
>
|
||||||
{t('controlLayers.filter.apply')}
|
{t('controlLayers.filter.apply')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
leftIcon={<PiXBold />}
|
leftIcon={<PiXBold />}
|
||||||
onClick={cancelFilter}
|
onClick={canvasManager.filter.cancelFilter}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('controlLayers.filter.cancel')}
|
loadingText={t('controlLayers.filter.cancel')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
|
|||||||
return (
|
return (
|
||||||
<Tooltip label={t('controlLayers.deletePrompt')}>
|
<Tooltip label={t('controlLayers.deletePrompt')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="promptOverlay"
|
variant="link"
|
||||||
aria-label={t('controlLayers.deletePrompt')}
|
aria-label={t('controlLayers.deletePrompt')}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
|
flexGrow={0}
|
||||||
|
size="sm"
|
||||||
|
p={0}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Box, Flex, Textarea } 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 { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const _focusVisible: SystemStyleObject = {
|
||||||
|
outline: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
export const RegionalGuidanceNegativePrompt = memo(() => {
|
export const RegionalGuidanceNegativePrompt = memo(() => {
|
||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
const selectPrompt = useMemo(
|
const selectPrompt = useMemo(
|
||||||
@@ -49,14 +53,25 @@ export const RegionalGuidanceNegativePrompt = memo(() => {
|
|||||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
placeholder={t('parameters.negativePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
variant="darkFilled"
|
variant="outline"
|
||||||
paddingRight={30}
|
paddingInlineStart={2}
|
||||||
|
paddingInlineEnd={8}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
zIndex="0 !important"
|
||||||
|
_focusVisible={_focusVisible}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<Flex
|
||||||
|
flexDir="column"
|
||||||
|
gap={2}
|
||||||
|
position="absolute"
|
||||||
|
insetBlockStart={2}
|
||||||
|
insetInlineEnd={0}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Box, Flex, Textarea } 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 { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const _focusVisible: SystemStyleObject = {
|
||||||
|
outline: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
export const RegionalGuidancePositivePrompt = memo(() => {
|
export const RegionalGuidancePositivePrompt = memo(() => {
|
||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
const selectPrompt = useMemo(
|
const selectPrompt = useMemo(
|
||||||
@@ -49,14 +53,25 @@ export const RegionalGuidancePositivePrompt = memo(() => {
|
|||||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
variant="darkFilled"
|
variant="outline"
|
||||||
paddingRight={30}
|
paddingInlineStart={2}
|
||||||
|
paddingInlineEnd={8}
|
||||||
minH={28}
|
minH={28}
|
||||||
|
zIndex="0 !important"
|
||||||
|
_focusVisible={_focusVisible}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<Flex
|
||||||
|
flexDir="column"
|
||||||
|
gap={2}
|
||||||
|
position="absolute"
|
||||||
|
insetBlockStart={2}
|
||||||
|
insetInlineEnd={0}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const RegionalGuidanceSettings = memo(() => {
|
|||||||
{flags.hasPositivePrompt && (
|
{flags.hasPositivePrompt && (
|
||||||
<>
|
<>
|
||||||
<RegionalGuidancePositivePrompt />
|
<RegionalGuidancePositivePrompt />
|
||||||
{(flags.hasNegativePrompt || flags.hasIPAdapters) && <Divider />}
|
{!flags.hasNegativePrompt && flags.hasIPAdapters && <Divider />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{flags.hasNegativePrompt && (
|
{flags.hasNegativePrompt && (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const CanvasSettingsPopover = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
|
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} variant="ghost" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full" bg={dynamicGrid ? 'base.850' : 'base.900'}>
|
<Flex position="relative" w="full" h="full" bg={dynamicGrid ? 'base.850' : 'base.900'} borderRadius="base">
|
||||||
{!dynamicGrid && (
|
{!dynamicGrid && (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
borderRadius="base"
|
||||||
bgImage={TRANSPARENCY_CHECKER_PATTERN}
|
bgImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||||
top={0}
|
top={0}
|
||||||
right={0}
|
right={0}
|
||||||
@@ -102,9 +103,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
left={0}
|
left={0}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
border={1}
|
|
||||||
borderStyle="solid"
|
|
||||||
borderColor="base.700"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
data-testid="control-layers-canvas"
|
data-testid="control-layers-canvas"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
|
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import {
|
import {
|
||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
useEntityIdentifierContext,
|
useEntityIdentifierContext,
|
||||||
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold, PiXBold } from 'react-icons/pi';
|
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const TransformBox = memo(() => {
|
const TransformBox = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -16,14 +16,6 @@ const TransformBox = memo(() => {
|
|||||||
const adapter = useEntityAdapter(entityIdentifier);
|
const adapter = useEntityAdapter(entityIdentifier);
|
||||||
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||||
|
|
||||||
const applyTransform = useCallback(() => {
|
|
||||||
adapter.transformer.applyTransform();
|
|
||||||
}, [adapter.transformer]);
|
|
||||||
|
|
||||||
const cancelFilter = useCallback(() => {
|
|
||||||
adapter.transformer.stopTransform();
|
|
||||||
}, [adapter.transformer]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
bg="base.800"
|
bg="base.800"
|
||||||
@@ -40,16 +32,33 @@ const TransformBox = memo(() => {
|
|||||||
<Heading size="md" color="base.300" userSelect="none">
|
<Heading size="md" color="base.300" userSelect="none">
|
||||||
{t('controlLayers.tool.transform')}
|
{t('controlLayers.tool.transform')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||||
|
<Button
|
||||||
|
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||||
|
onClick={adapter.transformer.resetTransform}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
loadingText={t('controlLayers.reset')}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{t('accessibility.reset')}
|
||||||
|
</Button>
|
||||||
|
<Spacer />
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<PiCheckBold />}
|
leftIcon={<PiCheckBold />}
|
||||||
onClick={applyTransform}
|
onClick={adapter.transformer.applyTransform}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('common.apply')}
|
loadingText={t('common.apply')}
|
||||||
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{t('common.apply')}
|
{t('common.apply')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button leftIcon={<PiXBold />} onClick={cancelFilter} isLoading={isProcessing} loadingText={t('common.cancel')}>
|
<Button
|
||||||
|
leftIcon={<PiXBold />}
|
||||||
|
onClick={adapter.transformer.stopTransform}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
loadingText={t('common.cancel')}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ export const UndoRedoButtonGroup = memo(() => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup isAttached={false}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('unifiedCanvas.undo')}
|
aria-label={t('unifiedCanvas.undo')}
|
||||||
tooltip={t('unifiedCanvas.undo')}
|
tooltip={t('unifiedCanvas.undo')}
|
||||||
onClick={handleUndo}
|
onClick={handleUndo}
|
||||||
icon={<PiArrowCounterClockwiseBold />}
|
icon={<PiArrowCounterClockwiseBold />}
|
||||||
isDisabled={!mayUndo}
|
isDisabled={!mayUndo}
|
||||||
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('unifiedCanvas.redo')}
|
aria-label={t('unifiedCanvas.redo')}
|
||||||
@@ -43,6 +44,7 @@ export const UndoRedoButtonGroup = memo(() => {
|
|||||||
onClick={handleRedo}
|
onClick={handleRedo}
|
||||||
icon={<PiArrowClockwiseBold />}
|
icon={<PiArrowClockwiseBold />}
|
||||||
isDisabled={!mayRedo}
|
isDisabled={!mayRedo}
|
||||||
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useBoolean } from 'common/hooks/useBoolean';
|
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
||||||
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
|
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold } from 'react-icons/pi';
|
import { PiCircleBold, PiCircleFill } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasEntityEnabledToggle = memo(() => {
|
export const CanvasEntityEnabledToggle = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
|
||||||
const isEnabled = useEntityIsEnabled(entityIdentifier);
|
const isEnabled = useEntityIsEnabled(entityIdentifier);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(entityIsEnabledToggled({ entityIdentifier }));
|
dispatch(entityIsEnabledToggled({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
const isHovered = useBoolean(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={ref}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
onMouseOver={isHovered.setTrue}
|
|
||||||
onMouseOut={isHovered.setFalse}
|
|
||||||
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||||
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={isEnabled || isHovered.isTrue ? <PiCheckBold /> : undefined}
|
icon={isEnabled ? <PiCircleFill /> : <PiCircleBold />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useBoolean } from 'common/hooks/useBoolean';
|
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||||
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
|
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiLockSimpleFill } from 'react-icons/pi';
|
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasEntityIsLockedToggle = memo(() => {
|
export const CanvasEntityIsLockedToggle = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
|
||||||
const isLocked = useEntityIsLocked(entityIdentifier);
|
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(entityIsLockedToggled({ entityIdentifier }));
|
dispatch(entityIsLockedToggled({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
const isHovered = useBoolean(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={ref}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
onMouseOver={isHovered.setTrue}
|
|
||||||
onMouseOut={isHovered.setFalse}
|
|
||||||
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||||
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={isLocked || isHovered.isTrue ? <PiLockSimpleFill /> : undefined}
|
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { useSelector } from 'react-redux';
|
|||||||
|
|
||||||
const ChakraCanvas = chakra.canvas;
|
const ChakraCanvas = chakra.canvas;
|
||||||
|
|
||||||
const PADDING = 4;
|
const PADDING = 2;
|
||||||
|
|
||||||
|
const CONTAINER_WIDTH = 36; // this is size 12 in our theme - need it in px for the canvas
|
||||||
|
const CONTAINER_WIDTH_PX = `${CONTAINER_WIDTH}px`;
|
||||||
|
|
||||||
export const CanvasEntityPreviewImage = memo(() => {
|
export const CanvasEntityPreviewImage = memo(() => {
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
@@ -31,11 +34,10 @@ export const CanvasEntityPreviewImage = memo(() => {
|
|||||||
[entityIdentifier]
|
[entityIdentifier]
|
||||||
);
|
);
|
||||||
const maskColor = useSelector(selectMaskColor);
|
const maskColor = useSelector(selectMaskColor);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const cache = useStore(adapter.renderer.$canvasCache);
|
const cache = useStore(adapter.renderer.$canvasCache);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cache || !canvasRef.current || !containerRef.current) {
|
if (!cache || !canvasRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
@@ -49,7 +51,7 @@ export const CanvasEntityPreviewImage = memo(() => {
|
|||||||
canvasRef.current.width = rect.width;
|
canvasRef.current.width = rect.width;
|
||||||
canvasRef.current.height = rect.height;
|
canvasRef.current.height = rect.height;
|
||||||
|
|
||||||
const scale = containerRef.current.offsetWidth / rect.width;
|
const scale = CONTAINER_WIDTH / rect.width;
|
||||||
|
|
||||||
const sx = rect.x;
|
const sx = rect.x;
|
||||||
const sy = rect.y;
|
const sy = rect.y;
|
||||||
@@ -72,11 +74,10 @@ export const CanvasEntityPreviewImage = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
position="relative"
|
position="relative"
|
||||||
ref={containerRef}
|
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
w={12}
|
w={CONTAINER_WIDTH_PX}
|
||||||
h={12}
|
h={CONTAINER_WIDTH_PX}
|
||||||
borderRadius="sm"
|
borderRadius="sm"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
bg="base.900"
|
bg="base.900"
|
||||||
|
|||||||
@@ -112,12 +112,11 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC {
|
|||||||
this.renderer.updateOpacity(opacity);
|
this.renderer.updateOpacity(opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.type === 'control_layer' && this.state.type === 'control_layer') {
|
if (state.type === 'control_layer' && prevState.type === 'control_layer') {
|
||||||
if (this.isFirstRender || state.withTransparencyEffect !== this.state.withTransparencyEffect) {
|
if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) {
|
||||||
this.renderer.updateTransparencyEffect(state.withTransparencyEffect);
|
this.renderer.updateTransparencyEffect(state.withTransparencyEffect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this.transformer.syncInteractionState();
|
|
||||||
|
|
||||||
if (this.isFirstRender) {
|
if (this.isFirstRender) {
|
||||||
this.transformer.updateBbox();
|
this.transformer.updateBbox();
|
||||||
|
|||||||
@@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
|||||||
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
|
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
|
||||||
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
|
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
|
||||||
// updated attributes to the object group, propagating the transformation on down.
|
// updated attributes to the object group, propagating the transformation on down.
|
||||||
this.parent.renderer.konva.objectGroup.setAttrs({
|
this.syncObjectGroupWithProxyRect();
|
||||||
x: this.konva.proxyRect.x(),
|
|
||||||
y: this.konva.proxyRect.y(),
|
|
||||||
scaleX: this.konva.proxyRect.scaleX(),
|
|
||||||
scaleY: this.konva.proxyRect.scaleY(),
|
|
||||||
rotation: this.konva.proxyRect.rotation(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.konva.transformer.on('transformend', () => {
|
this.konva.transformer.on('transformend', () => {
|
||||||
@@ -395,6 +389,54 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
|||||||
this.parent.konva.layer.add(this.konva.transformer);
|
this.parent.konva.layer.add(this.konva.transformer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(psyche): These don't work when the entity is rotated, need to do some math to offset the flip after rotation
|
||||||
|
// flipHorizontal = () => {
|
||||||
|
// if (!this.isTransforming || this.$isProcessing.get()) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Flipping horizontally = flipping across the vertical axis:
|
||||||
|
// // - Flip by negating the x scale
|
||||||
|
// // - Restore position by translating the rect rightwards by the width of the rect
|
||||||
|
// const x = this.konva.proxyRect.x();
|
||||||
|
// const width = this.konva.proxyRect.width();
|
||||||
|
// const scaleX = this.konva.proxyRect.scaleX();
|
||||||
|
// this.konva.proxyRect.setAttrs({
|
||||||
|
// scaleX: -scaleX,
|
||||||
|
// x: x + width * scaleX,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// this.syncObjectGroupWithProxyRect();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// flipVertical = () => {
|
||||||
|
// if (!this.isTransforming || this.$isProcessing.get()) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Flipping vertically = flipping across the horizontal axis:
|
||||||
|
// // - Flip by negating the y scale
|
||||||
|
// // - Restore position by translating the rect downwards by the height of the rect
|
||||||
|
// const y = this.konva.proxyRect.y();
|
||||||
|
// const height = this.konva.proxyRect.height();
|
||||||
|
// const scaleY = this.konva.proxyRect.scaleY();
|
||||||
|
// this.konva.proxyRect.setAttrs({
|
||||||
|
// scaleY: -scaleY,
|
||||||
|
// y: y + height * scaleY,
|
||||||
|
// });
|
||||||
|
// this.syncObjectGroupWithProxyRect();
|
||||||
|
// };
|
||||||
|
|
||||||
|
syncObjectGroupWithProxyRect = () => {
|
||||||
|
this.parent.renderer.konva.objectGroup.setAttrs({
|
||||||
|
x: this.konva.proxyRect.x(),
|
||||||
|
y: this.konva.proxyRect.y(),
|
||||||
|
scaleX: this.konva.proxyRect.scaleX(),
|
||||||
|
scaleY: this.konva.proxyRect.scaleY(),
|
||||||
|
rotation: this.konva.proxyRect.rotation(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the transformer's visual components to match the parent entity's position and bounding box.
|
* Updates the transformer's visual components to match the parent entity's position and bounding box.
|
||||||
* @param position The position of the parent entity
|
* @param position The position of the parent entity
|
||||||
@@ -510,6 +552,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
|||||||
this.stopTransform();
|
this.stopTransform();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
resetTransform = () => {
|
||||||
|
this.resetScale();
|
||||||
|
this.updatePosition();
|
||||||
|
this.updateBbox();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its
|
* Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its
|
||||||
* original state.
|
* original state.
|
||||||
@@ -520,12 +568,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
|
|||||||
this.isTransforming = false;
|
this.isTransforming = false;
|
||||||
this.setInteractionMode('off');
|
this.setInteractionMode('off');
|
||||||
|
|
||||||
// Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or
|
// Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or
|
||||||
// canceled a transformation. In either case, the scale should be reset.
|
// canceled a transformation. In either case, the scale should be reset.
|
||||||
this.resetScale();
|
this.resetTransform();
|
||||||
|
|
||||||
this.updatePosition();
|
|
||||||
this.updateBbox();
|
|
||||||
this.syncInteractionState();
|
this.syncInteractionState();
|
||||||
this.manager.stateApi.$transformingEntity.set(null);
|
this.manager.stateApi.$transformingEntity.set(null);
|
||||||
this.$isProcessing.set(false);
|
this.$isProcessing.set(false);
|
||||||
|
|||||||
@@ -1045,10 +1045,7 @@ export const canvasSlice = createSlice({
|
|||||||
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
|
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
|
||||||
state.bbox.rect.width = bboxDims.width;
|
state.bbox.rect.width = bboxDims.width;
|
||||||
state.bbox.rect.height = bboxDims.height;
|
state.bbox.rect.height = bboxDims.height;
|
||||||
|
syncScaledSize(state);
|
||||||
if (state.bbox.scaleMethod === 'auto') {
|
|
||||||
state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ export const selectScheduler = createParamsSelector((params) => params.scheduler
|
|||||||
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
|
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
|
||||||
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
|
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
|
||||||
export const selectSeed = createParamsSelector((params) => params.seed);
|
export const selectSeed = createParamsSelector((params) => params.seed);
|
||||||
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldConcatPrompts);
|
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldRandomizeSeed);
|
||||||
export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision);
|
export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision);
|
||||||
export const selectIterations = createParamsSelector((params) => params.iterations);
|
export const selectIterations = createParamsSelector((params) => params.iterations);
|
||||||
export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise);
|
export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise);
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const DeleteImageModal = () => {
|
|||||||
cancelButtonText={t('boards.cancel')}
|
cancelButtonText={t('boards.cancel')}
|
||||||
acceptButtonText={t('controlnet.delete')}
|
acceptButtonText={t('controlnet.delete')}
|
||||||
acceptCallback={handleDelete}
|
acceptCallback={handleDelete}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap={3}>
|
<Flex direction="column" gap={3}>
|
||||||
<ImageUsageMessage imageUsage={imageUsageSummary} />
|
<ImageUsageMessage imageUsage={imageUsageSummary} />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const DynamicPromptsModal = memo(() => {
|
|||||||
const { isOpen, onClose } = useDynamicPromptsModal();
|
const { isOpen, onClose } = useDynamicPromptsModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} isCentered useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
|
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
|
||||||
<ModalHeader>{t('dynamicPrompts.dynamicPrompts')}</ModalHeader>
|
<ModalHeader>{t('dynamicPrompts.dynamicPrompts')}</ModalHeader>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const GalleryPanelContent = () => {
|
|||||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" pt={2} tabIndex={-1}>
|
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||||
<Flex alignItems="center" gap={0}>
|
<Flex alignItems="center" gap={0}>
|
||||||
<GalleryHeader />
|
<GalleryHeader />
|
||||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const ImageViewer = memo(() => {
|
|||||||
<Flex
|
<Flex
|
||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
layerStyle="first"
|
layerStyle="body"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
@@ -29,7 +29,6 @@ export const ImageViewer = memo(() => {
|
|||||||
right={0}
|
right={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
left={0}
|
left={0}
|
||||||
p={2}
|
|
||||||
rowGap={4}
|
rowGap={4}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import {
|
import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Icon,
|
|
||||||
Popover,
|
|
||||||
PopoverArrow,
|
|
||||||
PopoverBody,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Text,
|
|
||||||
} from '@invoke-ai/ui-library';
|
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ViewerToggleMenu = () => {
|
export const ViewerToggleMenu = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -21,49 +11,45 @@ export const ViewerToggleMenu = () => {
|
|||||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Flex gap={4} alignItems="center" justifyContent="center">
|
||||||
<PopoverTrigger>
|
<ButtonGroup size="md">
|
||||||
<Button variant="outline" data-testid="toggle-viewer-menu-button" pointerEvents="auto">
|
<Tooltip
|
||||||
<Flex gap={3} w="full" alignItems="center">
|
hasArrow
|
||||||
{imageViewer.isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
|
label={
|
||||||
<Text fontSize="md">{imageViewer.isOpen ? t('common.viewing') : t('common.editing')}</Text>
|
<Flex flexDir="column">
|
||||||
<Icon as={PiCaretDownBold} />
|
<Text fontWeight="semibold">{t('common.viewing')}</Text>
|
||||||
</Flex>
|
<Text fontWeight="normal">{t('common.viewingDesc')}</Text>
|
||||||
</Button>
|
</Flex>
|
||||||
</PopoverTrigger>
|
}
|
||||||
<PopoverContent p={2} pointerEvents="auto">
|
>
|
||||||
<PopoverArrow />
|
<IconButton
|
||||||
<PopoverBody>
|
icon={<PiEyeBold />}
|
||||||
<Flex flexDir="column">
|
onClick={imageViewer.onOpen}
|
||||||
<Button onClick={imageViewer.onOpen} variant="ghost" h="auto" w="auto" p={2}>
|
variant={imageViewer.isOpen ? 'solid' : 'outline'}
|
||||||
<Flex gap={2} w="full">
|
colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
||||||
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'visible' : 'hidden'} />
|
aria-label={t('common.viewing')}
|
||||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
w={12}
|
||||||
<Text fontWeight="semibold" color="base.100">
|
/>
|
||||||
{t('common.viewing')}
|
</Tooltip>
|
||||||
</Text>
|
<Tooltip
|
||||||
<Text fontWeight="normal" color="base.300">
|
hasArrow
|
||||||
{t('common.viewingDesc')}
|
label={
|
||||||
</Text>
|
<Flex flexDir="column">
|
||||||
</Flex>
|
<Text fontWeight="semibold">{t('common.editing')}</Text>
|
||||||
</Flex>
|
<Text fontWeight="normal">{t('common.editingDesc')}</Text>
|
||||||
</Button>
|
</Flex>
|
||||||
<Button onClick={imageViewer.onClose} variant="ghost" h="auto" w="auto" p={2}>
|
}
|
||||||
<Flex gap={2} w="full">
|
>
|
||||||
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'hidden' : 'visible'} />
|
<IconButton
|
||||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
icon={<PiPencilBold />}
|
||||||
<Text fontWeight="semibold" color="base.100">
|
onClick={imageViewer.onClose}
|
||||||
{t('common.editing')}
|
variant={!imageViewer.isOpen ? 'solid' : 'outline'}
|
||||||
</Text>
|
colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
||||||
<Text fontWeight="normal" color="base.300">
|
aria-label={t('common.editing')}
|
||||||
{t('common.editingDesc')}
|
w={12}
|
||||||
</Text>
|
/>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Flex>
|
</ButtonGroup>
|
||||||
</Button>
|
</Flex>
|
||||||
</Flex>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
|
|||||||
title={t('modelManager.deleteModel')}
|
title={t('modelManager.deleteModel')}
|
||||||
acceptCallback={handleModelDelete}
|
acceptCallback={handleModelDelete}
|
||||||
acceptButtonText={t('modelManager.delete')}
|
acceptButtonText={t('modelManager.delete')}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex rowGap={4} flexDirection="column">
|
<Flex rowGap={4} flexDirection="column">
|
||||||
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>
|
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => {
|
|||||||
acceptButtonText={`${t('modelManager.convert')}`}
|
acceptButtonText={`${t('modelManager.convert')}`}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex flexDirection="column" rowGap={4}>
|
<Flex flexDirection="column" rowGap={4}>
|
||||||
<Text fontSize="md">{t('modelManager.convertToDiffusersHelpText1')}</Text>
|
<Text fontSize="md">{t('modelManager.convertToDiffusersHelpText1')}</Text>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'reactflow/dist/style.css';
|
|||||||
|
|
||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
|
||||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||||
@@ -10,7 +11,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { MdDeviceHub } from 'react-icons/md';
|
import { MdDeviceHub } from 'react-icons/md';
|
||||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||||
|
|
||||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
|
||||||
import { Flow } from './flow/Flow';
|
import { Flow } from './flow/Flow';
|
||||||
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
||||||
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||||
@@ -31,7 +31,7 @@ const NodeEditor = () => {
|
|||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<Flow />
|
<Flow />
|
||||||
<AddNodePopover />
|
<AddNodeCmdk />
|
||||||
<TopPanel />
|
<TopPanel />
|
||||||
<BottomLeftPanel />
|
<BottomLeftPanel />
|
||||||
<MinimapPanel />
|
<MinimapPanel />
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalOverlay,
|
||||||
|
Spacer,
|
||||||
|
Text,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useAppStore } from 'app/store/storeHooks';
|
||||||
|
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
||||||
|
import {
|
||||||
|
$addNodeCmdk,
|
||||||
|
$cursorPos,
|
||||||
|
$edgePendingUpdate,
|
||||||
|
$pendingConnection,
|
||||||
|
$templates,
|
||||||
|
edgesChanged,
|
||||||
|
nodesChanged,
|
||||||
|
useAddNodeCmdk,
|
||||||
|
} from 'features/nodes/store/nodesSlice';
|
||||||
|
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||||
|
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
|
||||||
|
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
||||||
|
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||||
|
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
||||||
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { memoize } from 'lodash-es';
|
||||||
|
import { computed } from 'nanostores';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiFlaskBold, PiHammerBold } from 'react-icons/pi';
|
||||||
|
import type { EdgeChange, NodeChange } from 'reactflow';
|
||||||
|
import type { S } from 'services/api/types';
|
||||||
|
|
||||||
|
const useThrottle = <T,>(value: T, limit: number) => {
|
||||||
|
const [throttledValue, setThrottledValue] = useState(value);
|
||||||
|
const lastRan = useRef(Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(
|
||||||
|
function () {
|
||||||
|
if (Date.now() - lastRan.current >= limit) {
|
||||||
|
setThrottledValue(value);
|
||||||
|
lastRan.current = Date.now();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit - (Date.now() - lastRan.current)
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, limit]);
|
||||||
|
|
||||||
|
return throttledValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddNode = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const store = useAppStore();
|
||||||
|
const buildInvocation = useBuildNode();
|
||||||
|
const templates = useStore($templates);
|
||||||
|
const pendingConnection = useStore($pendingConnection);
|
||||||
|
|
||||||
|
const addNode = useCallback(
|
||||||
|
(nodeType: string): void => {
|
||||||
|
const node = buildInvocation(nodeType);
|
||||||
|
if (!node) {
|
||||||
|
const errorMessage = t('nodes.unknownNode', {
|
||||||
|
nodeType: nodeType,
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: errorMessage,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a cozy spot for the node
|
||||||
|
const cursorPos = $cursorPos.get();
|
||||||
|
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||||
|
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
|
||||||
|
node.selected = true;
|
||||||
|
|
||||||
|
// Deselect all other nodes and edges
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
|
||||||
|
const edgeChanges: EdgeChange[] = [];
|
||||||
|
nodes.forEach(({ id, selected }) => {
|
||||||
|
if (selected) {
|
||||||
|
nodeChanges.push({ type: 'select', id, selected: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edges.forEach(({ id, selected }) => {
|
||||||
|
if (selected) {
|
||||||
|
edgeChanges.push({ type: 'select', id, selected: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Onwards!
|
||||||
|
if (nodeChanges.length > 0) {
|
||||||
|
store.dispatch(nodesChanged(nodeChanges));
|
||||||
|
}
|
||||||
|
if (edgeChanges.length > 0) {
|
||||||
|
store.dispatch(edgesChanged(edgeChanges));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-connect an edge if we just added a node and have a pending connection
|
||||||
|
if (pendingConnection && isInvocationNode(node)) {
|
||||||
|
const edgePendingUpdate = $edgePendingUpdate.get();
|
||||||
|
const { handleType } = pendingConnection;
|
||||||
|
|
||||||
|
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
|
||||||
|
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
|
||||||
|
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
|
||||||
|
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
|
||||||
|
|
||||||
|
const { nodes, edges } = selectNodesSlice(store.getState());
|
||||||
|
const connection = getFirstValidConnection(
|
||||||
|
source,
|
||||||
|
sourceHandle,
|
||||||
|
target,
|
||||||
|
targetHandle,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
templates,
|
||||||
|
edgePendingUpdate
|
||||||
|
);
|
||||||
|
if (connection) {
|
||||||
|
const newEdge = connectionToEdge(connection);
|
||||||
|
store.dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[buildInvocation, pendingConnection, store, t, templates]
|
||||||
|
);
|
||||||
|
|
||||||
|
return addNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmdkRootSx: SystemStyleObject = {
|
||||||
|
'[cmdk-root]': {
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
},
|
||||||
|
'[cmdk-list]': {
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddNodeCmdk = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const addNodeCmdk = useAddNodeCmdk();
|
||||||
|
const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const addNode = useAddNode();
|
||||||
|
const throttledSearchTerm = useThrottle(searchTerm, 100);
|
||||||
|
|
||||||
|
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true });
|
||||||
|
|
||||||
|
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSelect = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
addNode(value);
|
||||||
|
$addNodeCmdk.set(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
},
|
||||||
|
[addNode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
addNodeCmdk.setFalse();
|
||||||
|
setSearchTerm('');
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
}, [addNodeCmdk]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={addNodeCmdkIsOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
useInert={false}
|
||||||
|
initialFocusRef={inputRef}
|
||||||
|
size="xl"
|
||||||
|
isCentered
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent h="512" maxH="70%">
|
||||||
|
<ModalBody p={2} h="full" sx={cmdkRootSx}>
|
||||||
|
<CommandRoot loop shouldFilter={false}>
|
||||||
|
<Flex flexDir="column" h="full" gap={2}>
|
||||||
|
<Input ref={inputRef} value={searchTerm} onChange={onChange} placeholder={t('nodes.nodeSearch')} />
|
||||||
|
<Box w="full" h="full">
|
||||||
|
<ScrollableContent>
|
||||||
|
<CommandEmpty>
|
||||||
|
<IAINoContentFallback
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
icon={null}
|
||||||
|
label="No matching items"
|
||||||
|
/>
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandList>
|
||||||
|
<NodeCommandList searchTerm={throttledSearchTerm} onSelect={onSelect} />
|
||||||
|
</CommandList>
|
||||||
|
</ScrollableContent>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</CommandRoot>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddNodeCmdk.displayName = 'AddNodeCmdk';
|
||||||
|
|
||||||
|
const cmdkItemSx: SystemStyleObject = {
|
||||||
|
'&[data-selected="true"]': {
|
||||||
|
bg: 'base.700',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeCommandItemData = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
classification: S['Classification'];
|
||||||
|
nodePack: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $templatesArray = computed($templates, (templates) => Object.values(templates));
|
||||||
|
|
||||||
|
const createRegex = memoize(
|
||||||
|
(inputValue: string) =>
|
||||||
|
new RegExp(
|
||||||
|
inputValue
|
||||||
|
.trim()
|
||||||
|
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
||||||
|
.split(' ')
|
||||||
|
.join('.*'),
|
||||||
|
'gi'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filterable items are a subset of Invocation template - we also want to filter for notes or current image node,
|
||||||
|
// so we are using a less specific type instead of `InvocationTemplate`
|
||||||
|
type FilterableItem = {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
classification: S['Classification'];
|
||||||
|
nodePack: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter = memoize(
|
||||||
|
(item: FilterableItem, searchTerm: string) => {
|
||||||
|
const regex = createRegex(searchTerm);
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.title.includes(searchTerm) || regex.test(item.title)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type.includes(searchTerm) || regex.test(item.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.description.includes(searchTerm) || regex.test(item.description)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.nodePack.includes(searchTerm) || regex.test(item.nodePack)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.classification.includes(searchTerm) || regex.test(item.classification)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of item.tags) {
|
||||||
|
if (tag.includes(searchTerm) || regex.test(tag)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(item: FilterableItem, searchTerm: string) => `${item.type}-${searchTerm}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; onSelect: (value: string) => void }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const templatesArray = useStore($templatesArray);
|
||||||
|
const pendingConnection = useStore($pendingConnection);
|
||||||
|
const currentImageFilterItem = useMemo<FilterableItem>(
|
||||||
|
() => ({
|
||||||
|
type: 'current_image',
|
||||||
|
title: t('nodes.currentImage'),
|
||||||
|
description: t('nodes.currentImageDescription'),
|
||||||
|
tags: ['progress', 'image', 'current'],
|
||||||
|
classification: 'stable',
|
||||||
|
nodePack: 'invokeai',
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
const notesFilterItem = useMemo<FilterableItem>(
|
||||||
|
() => ({
|
||||||
|
type: 'notes',
|
||||||
|
title: t('nodes.notes'),
|
||||||
|
description: t('nodes.notesDescription'),
|
||||||
|
tags: ['notes'],
|
||||||
|
classification: 'stable',
|
||||||
|
nodePack: 'invokeai',
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = useMemo<NodeCommandItemData[]>(() => {
|
||||||
|
// If we have a connection in progress, we need to filter the node choices
|
||||||
|
const _items: NodeCommandItemData[] = [];
|
||||||
|
|
||||||
|
if (!pendingConnection) {
|
||||||
|
for (const template of templatesArray) {
|
||||||
|
if (filter(template, searchTerm)) {
|
||||||
|
_items.push({
|
||||||
|
label: template.title,
|
||||||
|
value: template.type,
|
||||||
|
description: template.description,
|
||||||
|
classification: template.classification,
|
||||||
|
nodePack: template.nodePack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of [currentImageFilterItem, notesFilterItem]) {
|
||||||
|
if (filter(item, searchTerm)) {
|
||||||
|
_items.push({
|
||||||
|
label: item.title,
|
||||||
|
value: item.type,
|
||||||
|
description: item.description,
|
||||||
|
classification: item.classification,
|
||||||
|
nodePack: item.nodePack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const template of templatesArray) {
|
||||||
|
if (filter(template, searchTerm)) {
|
||||||
|
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
|
||||||
|
|
||||||
|
for (const field of Object.values(candidateFields)) {
|
||||||
|
const sourceType =
|
||||||
|
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
|
||||||
|
const targetType =
|
||||||
|
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
|
||||||
|
|
||||||
|
if (validateConnectionTypes(sourceType, targetType)) {
|
||||||
|
_items.push({
|
||||||
|
label: template.title,
|
||||||
|
value: template.type,
|
||||||
|
description: template.description,
|
||||||
|
classification: template.classification,
|
||||||
|
nodePack: template.nodePack,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _items;
|
||||||
|
}, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item) => (
|
||||||
|
<CommandItem key={item.value} value={item.value} onSelect={onSelect} asChild>
|
||||||
|
<Flex role="button" flexDir="column" sx={cmdkItemSx} py={1} px={2} borderRadius="base">
|
||||||
|
<Flex alignItems="center" gap={2}>
|
||||||
|
{item.classification === 'beta' && <Icon boxSize={4} color="invokeYellow.300" as={PiHammerBold} />}
|
||||||
|
{item.classification === 'prototype' && <Icon boxSize={4} color="invokeRed.300" as={PiFlaskBold} />}
|
||||||
|
<Text fontWeight="semibold">{item.label}</Text>
|
||||||
|
<Spacer />
|
||||||
|
<Text variant="subtext" fontWeight="semibold">
|
||||||
|
{item.nodePack}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
{item.description && <Text color="base.200">{item.description}</Text>}
|
||||||
|
</Flex>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NodeCommandList.displayName = 'CommandListItems';
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import 'reactflow/dist/style.css';
|
|
||||||
|
|
||||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
|
||||||
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
|
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
|
|
||||||
import type { SelectInstance } from 'chakra-react-select';
|
|
||||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
|
||||||
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
|
||||||
import {
|
|
||||||
$cursorPos,
|
|
||||||
$edgePendingUpdate,
|
|
||||||
$isAddNodePopoverOpen,
|
|
||||||
$pendingConnection,
|
|
||||||
$templates,
|
|
||||||
closeAddNodePopover,
|
|
||||||
edgesChanged,
|
|
||||||
nodesChanged,
|
|
||||||
openAddNodePopover,
|
|
||||||
} from 'features/nodes/store/nodesSlice';
|
|
||||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
|
||||||
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
|
|
||||||
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
|
||||||
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
|
||||||
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
|
||||||
import type { AnyNode } from 'features/nodes/types/invocation';
|
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
||||||
import { toast } from 'features/toast/toast';
|
|
||||||
import { filter, map, memoize, some } from 'lodash-es';
|
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
|
|
||||||
import type { EdgeChange, NodeChange } from 'reactflow';
|
|
||||||
|
|
||||||
const createRegex = memoize(
|
|
||||||
(inputValue: string) =>
|
|
||||||
new RegExp(
|
|
||||||
inputValue
|
|
||||||
.trim()
|
|
||||||
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
|
||||||
.split(' ')
|
|
||||||
.join('.*'),
|
|
||||||
'gi'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterOption = memoize((option: FilterOptionOption<ComboboxOption>, inputValue: string) => {
|
|
||||||
if (!inputValue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const regex = createRegex(inputValue);
|
|
||||||
return (
|
|
||||||
regex.test(option.label) ||
|
|
||||||
regex.test(option.data.description ?? '') ||
|
|
||||||
(option.data.tags ?? []).some((tag) => regex.test(tag))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AddNodePopover = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const buildInvocation = useBuildNode();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const templates = useStore($templates);
|
|
||||||
const pendingConnection = useStore($pendingConnection);
|
|
||||||
const isOpen = useStore($isAddNodePopoverOpen);
|
|
||||||
const store = useAppStore();
|
|
||||||
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
|
||||||
|
|
||||||
const filteredTemplates = useMemo(() => {
|
|
||||||
// If we have a connection in progress, we need to filter the node choices
|
|
||||||
const templatesArray = map(templates);
|
|
||||||
if (!pendingConnection) {
|
|
||||||
return templatesArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter(templates, (template) => {
|
|
||||||
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
|
|
||||||
return some(candidateFields, (field) => {
|
|
||||||
const sourceType =
|
|
||||||
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
|
|
||||||
const targetType =
|
|
||||||
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
|
|
||||||
return validateConnectionTypes(sourceType, targetType);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [templates, pendingConnection]);
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const _options: ComboboxOption[] = map(filteredTemplates, (template) => {
|
|
||||||
return {
|
|
||||||
label: template.title,
|
|
||||||
value: template.type,
|
|
||||||
description: template.description,
|
|
||||||
tags: template.tags,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
//We only want these nodes if we're not filtered
|
|
||||||
if (!pendingConnection) {
|
|
||||||
_options.push({
|
|
||||||
label: t('nodes.currentImage'),
|
|
||||||
value: 'current_image',
|
|
||||||
description: t('nodes.currentImageDescription'),
|
|
||||||
tags: ['progress'],
|
|
||||||
});
|
|
||||||
|
|
||||||
_options.push({
|
|
||||||
label: t('nodes.notes'),
|
|
||||||
value: 'notes',
|
|
||||||
description: t('nodes.notesDescription'),
|
|
||||||
tags: ['notes'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_options.sort((a, b) => a.label.localeCompare(b.label));
|
|
||||||
|
|
||||||
return _options;
|
|
||||||
}, [filteredTemplates, pendingConnection, t]);
|
|
||||||
|
|
||||||
const addNode = useCallback(
|
|
||||||
(nodeType: string): AnyNode | null => {
|
|
||||||
const node = buildInvocation(nodeType);
|
|
||||||
if (!node) {
|
|
||||||
const errorMessage = t('nodes.unknownNode', {
|
|
||||||
nodeType: nodeType,
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
status: 'error',
|
|
||||||
title: errorMessage,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a cozy spot for the node
|
|
||||||
const cursorPos = $cursorPos.get();
|
|
||||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
|
||||||
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
|
|
||||||
node.selected = true;
|
|
||||||
|
|
||||||
// Deselect all other nodes and edges
|
|
||||||
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
|
|
||||||
const edgeChanges: EdgeChange[] = [];
|
|
||||||
nodes.forEach(({ id, selected }) => {
|
|
||||||
if (selected) {
|
|
||||||
nodeChanges.push({ type: 'select', id, selected: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
edges.forEach(({ id, selected }) => {
|
|
||||||
if (selected) {
|
|
||||||
edgeChanges.push({ type: 'select', id, selected: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Onwards!
|
|
||||||
if (nodeChanges.length > 0) {
|
|
||||||
dispatch(nodesChanged(nodeChanges));
|
|
||||||
}
|
|
||||||
if (edgeChanges.length > 0) {
|
|
||||||
dispatch(edgesChanged(edgeChanges));
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
[buildInvocation, store, dispatch, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChange = useCallback<ComboboxOnChange>(
|
|
||||||
(v) => {
|
|
||||||
if (!v) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const node = addNode(v.value);
|
|
||||||
|
|
||||||
// Auto-connect an edge if we just added a node and have a pending connection
|
|
||||||
if (pendingConnection && isInvocationNode(node)) {
|
|
||||||
const edgePendingUpdate = $edgePendingUpdate.get();
|
|
||||||
const { handleType } = pendingConnection;
|
|
||||||
|
|
||||||
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
|
|
||||||
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
|
|
||||||
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
|
|
||||||
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
|
|
||||||
|
|
||||||
const { nodes, edges } = selectNodesSlice(store.getState());
|
|
||||||
const connection = getFirstValidConnection(
|
|
||||||
source,
|
|
||||||
sourceHandle,
|
|
||||||
target,
|
|
||||||
targetHandle,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
templates,
|
|
||||||
edgePendingUpdate
|
|
||||||
);
|
|
||||||
if (connection) {
|
|
||||||
const newEdge = connectionToEdge(connection);
|
|
||||||
dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAddNodePopover();
|
|
||||||
},
|
|
||||||
[addNode, dispatch, pendingConnection, store, templates]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleHotkeyOpen: HotkeyCallback = useCallback((e) => {
|
|
||||||
if (!$isAddNodePopoverOpen.get()) {
|
|
||||||
e.preventDefault();
|
|
||||||
openAddNodePopover();
|
|
||||||
flushSync(() => {
|
|
||||||
selectRef.current?.inputRef?.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
|
|
||||||
|
|
||||||
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeAddNodePopover}
|
|
||||||
placement="bottom"
|
|
||||||
openDelay={0}
|
|
||||||
closeDelay={0}
|
|
||||||
closeOnBlur={true}
|
|
||||||
returnFocusOnClose={true}
|
|
||||||
initialFocusRef={inputRef}
|
|
||||||
isLazy
|
|
||||||
>
|
|
||||||
<PopoverAnchor>
|
|
||||||
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />
|
|
||||||
</PopoverAnchor>
|
|
||||||
<PopoverContent
|
|
||||||
p={0}
|
|
||||||
top={-1}
|
|
||||||
shadow="dark-lg"
|
|
||||||
borderColor="invokeBlue.400"
|
|
||||||
borderWidth="2px"
|
|
||||||
borderStyle="solid"
|
|
||||||
>
|
|
||||||
<PopoverBody w="32rem" p={0}>
|
|
||||||
<Combobox
|
|
||||||
menuIsOpen={isOpen}
|
|
||||||
selectRef={selectRef}
|
|
||||||
value={null}
|
|
||||||
placeholder={t('nodes.nodeSearch')}
|
|
||||||
options={options}
|
|
||||||
noOptionsMessage={noOptionsMessage}
|
|
||||||
filterOption={filterOption}
|
|
||||||
onChange={onChange}
|
|
||||||
onMenuClose={closeAddNodePopover}
|
|
||||||
inputRef={inputRef}
|
|
||||||
closeMenuOnSelect={false}
|
|
||||||
/>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(AddNodePopover);
|
|
||||||
@@ -8,10 +8,10 @@ import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
|||||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||||
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||||
import {
|
import {
|
||||||
|
$addNodeCmdk,
|
||||||
$cursorPos,
|
$cursorPos,
|
||||||
$didUpdateEdge,
|
$didUpdateEdge,
|
||||||
$edgePendingUpdate,
|
$edgePendingUpdate,
|
||||||
$isAddNodePopoverOpen,
|
|
||||||
$lastEdgeUpdateMouseEvent,
|
$lastEdgeUpdateMouseEvent,
|
||||||
$pendingConnection,
|
$pendingConnection,
|
||||||
$viewport,
|
$viewport,
|
||||||
@@ -281,7 +281,7 @@ export const Flow = memo(() => {
|
|||||||
const onEscapeHotkey = useCallback(() => {
|
const onEscapeHotkey = useCallback(() => {
|
||||||
if (!$edgePendingUpdate.get()) {
|
if (!$edgePendingUpdate.get()) {
|
||||||
$pendingConnection.set(null);
|
$pendingConnection.set(null);
|
||||||
$isAddNodePopoverOpen.set(false);
|
$addNodeCmdk.set(false);
|
||||||
cancelConnection();
|
cancelConnection();
|
||||||
}
|
}
|
||||||
}, [cancelConnection]);
|
}, [cancelConnection]);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { openAddNodePopover } from 'features/nodes/store/nodesSlice';
|
import { useAddNodeCmdk } from 'features/nodes/store/nodesSlice';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const AddNodeButton = () => {
|
const AddNodeButton = () => {
|
||||||
|
const addNodeCmdk = useAddNodeCmdk();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +13,7 @@ const AddNodeButton = () => {
|
|||||||
tooltip={t('nodes.addNodeToolTip')}
|
tooltip={t('nodes.addNodeToolTip')}
|
||||||
aria-label={t('nodes.addNode')}
|
aria-label={t('nodes.addNode')}
|
||||||
icon={<PiPlusBold />}
|
icon={<PiPlusBold />}
|
||||||
onClick={openAddNodePopover}
|
onClick={addNodeCmdk.setTrue}
|
||||||
pointerEvents="auto"
|
pointerEvents="auto"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const ClearFlowButton = () => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t('nodes.clearWorkflow')}
|
title={t('nodes.clearWorkflow')}
|
||||||
acceptCallback={handleNewWorkflow}
|
acceptCallback={handleNewWorkflow}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
<Text>{t('nodes.clearWorkflowDesc')}</Text>
|
<Text>{t('nodes.clearWorkflowDesc')}</Text>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const WorkflowEditorSettings = ({ children }: Props) => {
|
|||||||
<>
|
<>
|
||||||
{children({ onOpen })}
|
{children({ onOpen })}
|
||||||
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
|
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { useAppStore } from 'app/store/storeHooks';
|
import { useAppStore } from 'app/store/storeHooks';
|
||||||
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
import {
|
import {
|
||||||
|
$addNodeCmdk,
|
||||||
$didUpdateEdge,
|
$didUpdateEdge,
|
||||||
$edgePendingUpdate,
|
$edgePendingUpdate,
|
||||||
$isAddNodePopoverOpen,
|
|
||||||
$pendingConnection,
|
$pendingConnection,
|
||||||
$templates,
|
$templates,
|
||||||
edgesChanged,
|
edgesChanged,
|
||||||
@@ -107,7 +107,7 @@ export const useConnection = () => {
|
|||||||
$pendingConnection.set(null);
|
$pendingConnection.set(null);
|
||||||
} else {
|
} else {
|
||||||
// The mouse is not over a node - we should open the add node popover
|
// The mouse is not over a node - we should open the add node popover
|
||||||
$isAddNodePopoverOpen.set(true);
|
$addNodeCmdk.set(true);
|
||||||
}
|
}
|
||||||
}, [store, templates, updateNodeInternals]);
|
}, [store, templates, updateNodeInternals]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig } from 'app/store/store';
|
import type { PersistConfig } from 'app/store/store';
|
||||||
|
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||||
import type {
|
import type {
|
||||||
@@ -431,14 +432,8 @@ export const $didUpdateEdge = atom(false);
|
|||||||
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
|
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
|
||||||
|
|
||||||
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||||
export const $isAddNodePopoverOpen = atom(false);
|
export const $addNodeCmdk = atom(false);
|
||||||
export const closeAddNodePopover = () => {
|
export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk);
|
||||||
$isAddNodePopoverOpen.set(false);
|
|
||||||
$pendingConnection.set(null);
|
|
||||||
};
|
|
||||||
export const openAddNodePopover = () => {
|
|
||||||
$isAddNodePopoverOpen.set(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const migrateNodesState = (state: any): any => {
|
const migrateNodesState = (state: any): any => {
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export const collect: InvocationTemplate = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
useCache: true,
|
useCache: true,
|
||||||
|
nodePack: 'invokeai',
|
||||||
classification: 'stable',
|
classification: 'stable',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -480,6 +481,7 @@ const iterate: InvocationTemplate = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
useCache: true,
|
useCache: true,
|
||||||
|
nodePack: 'invokeai',
|
||||||
classification: 'stable',
|
classification: 'stable',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1152,6 +1154,7 @@ export const schema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['type', 'id'],
|
required: ['type', 'id'],
|
||||||
title: 'CollectInvocation',
|
title: 'CollectInvocation',
|
||||||
|
node_pack: 'invokeai',
|
||||||
description: 'Collects values into a collection',
|
description: 'Collects values into a collection',
|
||||||
classification: 'stable',
|
classification: 'stable',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
@@ -1513,6 +1516,7 @@ export const schema = {
|
|||||||
title: 'IterateInvocation',
|
title: 'IterateInvocation',
|
||||||
description: 'Iterates over a list of items',
|
description: 'Iterates over a list of items',
|
||||||
classification: 'stable',
|
classification: 'stable',
|
||||||
|
node_pack: 'invokeai',
|
||||||
version: '1.1.0',
|
version: '1.1.0',
|
||||||
output: {
|
output: {
|
||||||
$ref: '#/components/schemas/IterateInvocationOutput',
|
$ref: '#/components/schemas/IterateInvocationOutput',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const zInvocationTemplate = z.object({
|
|||||||
outputType: z.string().min(1),
|
outputType: z.string().min(1),
|
||||||
version: zSemVer,
|
version: zSemVer,
|
||||||
useCache: z.boolean(),
|
useCache: z.boolean(),
|
||||||
nodePack: z.string().min(1).nullish(),
|
nodePack: z.string().min(1).default('invokeai'),
|
||||||
classification: zClassification,
|
classification: zClassification,
|
||||||
});
|
});
|
||||||
export type InvocationTemplate = z.infer<typeof zInvocationTemplate>;
|
export type InvocationTemplate = z.infer<typeof zInvocationTemplate>;
|
||||||
@@ -26,7 +26,7 @@ export type InvocationTemplate = z.infer<typeof zInvocationTemplate>;
|
|||||||
export const zInvocationNodeData = z.object({
|
export const zInvocationNodeData = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
version: zSemVer,
|
version: zSemVer,
|
||||||
nodePack: z.string().min(1).nullish(),
|
nodePack: z.string().min(1).default('invokeai'),
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
type: z.string().trim().min(1),
|
type: z.string().trim().min(1),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
isIntermediate: type === 'save_image' ? false : true,
|
isIntermediate: type === 'save_image' ? false : true,
|
||||||
useCache: template.useCache,
|
useCache: template.useCache,
|
||||||
|
nodePack: template.nodePack,
|
||||||
inputs,
|
inputs,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
isIntermediate: node.is_intermediate ?? false,
|
isIntermediate: node.is_intermediate ?? false,
|
||||||
useCache: node.use_cache ?? true,
|
useCache: node.use_cache ?? true,
|
||||||
|
nodePack: template.nodePack,
|
||||||
inputs,
|
inputs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('validateWorkflow', () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
isIntermediate: true,
|
isIntermediate: true,
|
||||||
useCache: true,
|
useCache: true,
|
||||||
|
nodePack: 'invokeai',
|
||||||
inputs: {
|
inputs: {
|
||||||
model: {
|
model: {
|
||||||
name: 'model',
|
name: 'model',
|
||||||
@@ -56,6 +57,7 @@ describe('validateWorkflow', () => {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
isIntermediate: true,
|
isIntermediate: true,
|
||||||
useCache: true,
|
useCache: true,
|
||||||
|
nodePack: 'invokeai',
|
||||||
inputs: {
|
inputs: {
|
||||||
board: {
|
board: {
|
||||||
name: 'board',
|
name: 'board',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ClearQueueConfirmationsAlertDialog = memo(() => {
|
|||||||
title={t('queue.clearTooltip')}
|
title={t('queue.clearTooltip')}
|
||||||
acceptCallback={clearQueue}
|
acceptCallback={clearQueue}
|
||||||
acceptButtonText={t('queue.clear')}
|
acceptButtonText={t('queue.clear')}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Text>{t('queue.clearQueueAlertDialog')}</Text>
|
<Text>{t('queue.clearQueueAlertDialog')}</Text>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const InvokeQueueBackButton = memo(() => {
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
rightIcon={<RiSparkling2Fill />}
|
rightIcon={<RiSparkling2Fill />}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
zIndex={1}
|
|
||||||
colorScheme="invokeYellow"
|
colorScheme="invokeYellow"
|
||||||
size="lg"
|
size="lg"
|
||||||
w="calc(100% - 60px)"
|
w="calc(100% - 60px)"
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import {
|
|||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
|
Portal,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import type { Coordinate } from 'features/controlLayers/store/types';
|
||||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||||
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri';
|
import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri';
|
||||||
@@ -26,6 +28,8 @@ export const QueueActionsMenuButton = memo(() => {
|
|||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [badgePos, setBadgePos] = useState<Coordinate | null>(null);
|
||||||
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||||
@@ -49,10 +53,17 @@ export const QueueActionsMenuButton = memo(() => {
|
|||||||
dispatch(setActiveTab('queue'));
|
dispatch(setActiveTab('queue'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuButtonRef.current) {
|
||||||
|
const { x, y } = menuButtonRef.current.getBoundingClientRect();
|
||||||
|
setBadgePos({ x: x - 10, y: y - 10 });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative">
|
<Box pos="relative">
|
||||||
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose} placement="bottom-end">
|
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose} placement="bottom-end">
|
||||||
<MenuButton as={IconButton} aria-label="Queue Actions Menu" icon={<RiListCheck />} />
|
<MenuButton ref={menuButtonRef} as={IconButton} aria-label="Queue Actions Menu" icon={<RiListCheck />} />
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isDestructive
|
isDestructive
|
||||||
@@ -89,10 +100,18 @@ export const QueueActionsMenuButton = memo(() => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
{queueSize > 0 && (
|
{queueSize > 0 && badgePos !== null && (
|
||||||
<Badge pos="absolute" insetInlineStart={-3} insetBlockStart={-1.5} colorScheme="invokeYellow" zIndex="docked">
|
<Portal>
|
||||||
{queueSize}
|
<Badge
|
||||||
</Badge>
|
pos="absolute"
|
||||||
|
insetInlineStart={badgePos.x}
|
||||||
|
insetBlockStart={badgePos.y}
|
||||||
|
colorScheme="invokeYellow"
|
||||||
|
zIndex="docked"
|
||||||
|
>
|
||||||
|
{queueSize}
|
||||||
|
</Badge>
|
||||||
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { QueueActionsMenuButton } from './QueueActionsMenuButton';
|
|||||||
const QueueControls = () => {
|
const QueueControls = () => {
|
||||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||||
return (
|
return (
|
||||||
<Flex w="full" position="relative" borderRadius="base" gap={2} pt={2} flexDir="column">
|
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||||
<ButtonGroup size="lg" isAttached={false}>
|
<ButtonGroup size="lg" isAttached={false}>
|
||||||
{isPrependEnabled && <QueueFrontButton />}
|
{isPrependEnabled && <QueueFrontButton />}
|
||||||
<InvokeQueueBackButton />
|
<InvokeQueueBackButton />
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const StylePresetModal = () => {
|
|||||||
}, [stylePresetModalState.prefilledFormData]);
|
}, [stylePresetModalState.prefilledFormData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={stylePresetModalState.isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
|
<Modal isOpen={stylePresetModalState.isModalOpen} onClose={handleCloseModal} isCentered size="2xl" useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>{modalTitle}</ModalHeader>
|
<ModalHeader>{modalTitle}</ModalHeader>
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
|||||||
acceptCallback={handleDeletePreset}
|
acceptCallback={handleDeletePreset}
|
||||||
acceptButtonText={t('common.delete')}
|
acceptButtonText={t('common.delete')}
|
||||||
cancelButtonText={t('common.cancel')}
|
cancelButtonText={t('common.cancel')}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<p>{t('stylePresets.deleteTemplate2')}</p>
|
<p>{t('stylePresets.deleteTemplate2')}</p>
|
||||||
</ConfirmationAlertDialog>
|
</ConfirmationAlertDialog>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const AboutModal = ({ children }: AboutModalProps) => {
|
|||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
onClick: onOpen,
|
onClick: onOpen,
|
||||||
})}
|
})}
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered size="2xl">
|
<Modal isOpen={isOpen} onClose={onClose} isCentered size="2xl" useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent maxH="80vh" h="34rem">
|
<ModalContent maxH="80vh" h="34rem">
|
||||||
<ModalHeader>{t('accessibility.about')}</ModalHeader>
|
<ModalHeader>{t('accessibility.about')}</ModalHeader>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
|
|||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
onClick: onOpen,
|
onClick: onOpen,
|
||||||
})}
|
})}
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered size="2xl">
|
<Modal isOpen={isOpen} onClose={onClose} isCentered size="2xl" useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent maxH="80vh" h="80vh">
|
<ModalContent maxH="80vh" h="80vh">
|
||||||
<ModalHeader>{t('hotkeys.keyboardShortcuts')}</ModalHeader>
|
<ModalHeader>{t('hotkeys.keyboardShortcuts')}</ModalHeader>
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Text,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
import { memo, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const $refreshAfterResetModalState = atom(false);
|
||||||
|
export const useRefreshAfterResetModal = buildUseBoolean($refreshAfterResetModalState);
|
||||||
|
|
||||||
|
const RefreshAfterResetModal = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState(3);
|
||||||
|
|
||||||
|
const refreshModal = useRefreshAfterResetModal();
|
||||||
|
const isOpen = useStore(refreshModal.$boolean);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const i = window.setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(i);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={refreshModal.setFalse}
|
||||||
|
isCentered
|
||||||
|
closeOnEsc={false}
|
||||||
|
useInert={false}
|
||||||
|
>
|
||||||
|
<ModalOverlay backdropFilter="blur(40px)" />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<Text fontSize="lg">
|
||||||
|
<Text>
|
||||||
|
{t('settings.resetComplete')} {t('settings.reloadingIn')} {countdown}...
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(RefreshAfterResetModal);
|
||||||
@@ -25,12 +25,13 @@ import {
|
|||||||
} from 'react-icons/pi';
|
} from 'react-icons/pi';
|
||||||
import { RiDiscordFill, RiGithubFill, RiSettings4Line } from 'react-icons/ri';
|
import { RiDiscordFill, RiGithubFill, RiSettings4Line } from 'react-icons/ri';
|
||||||
|
|
||||||
import SettingsModal from './SettingsModal';
|
import { useSettingsModal } from './SettingsModal';
|
||||||
import { SettingsUpsellMenuItem } from './SettingsUpsellMenuItem';
|
import { SettingsUpsellMenuItem } from './SettingsUpsellMenuItem';
|
||||||
const SettingsMenu = () => {
|
const SettingsMenu = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
useGlobalMenuClose(onClose);
|
useGlobalMenuClose(onClose);
|
||||||
|
const settingsModal = useSettingsModal();
|
||||||
|
|
||||||
const isBugLinkEnabled = useFeatureStatus('bugLink');
|
const isBugLinkEnabled = useFeatureStatus('bugLink');
|
||||||
const isDiscordLinkEnabled = useFeatureStatus('discordLink');
|
const isDiscordLinkEnabled = useFeatureStatus('discordLink');
|
||||||
@@ -75,11 +76,9 @@ const SettingsMenu = () => {
|
|||||||
{t('common.hotkeysLabel')}
|
{t('common.hotkeysLabel')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</HotkeysModal>
|
</HotkeysModal>
|
||||||
<SettingsModal>
|
<MenuItem onClick={settingsModal.setTrue} as="button" icon={<PiToggleRightFill />}>
|
||||||
<MenuItem as="button" icon={<PiToggleRightFill />}>
|
{t('common.settingsLabel')}
|
||||||
{t('common.settingsLabel')}
|
</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
</SettingsModal>
|
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
<MenuGroup title={t('accessibility.about')}>
|
<MenuGroup title={t('accessibility.about')}>
|
||||||
<AboutModal>
|
<AboutModal>
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ import {
|
|||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||||
import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice';
|
import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice';
|
||||||
|
import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||||
import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
|
import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
|
||||||
import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
|
import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
|
||||||
import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces';
|
import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces';
|
||||||
@@ -40,8 +42,9 @@ import {
|
|||||||
} from 'features/system/store/systemSlice';
|
} from 'features/system/store/systemSlice';
|
||||||
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||||
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
||||||
import type { ChangeEvent, ReactElement } from 'react';
|
import { atom } from 'nanostores';
|
||||||
import { cloneElement, memo, useCallback, useEffect, useState } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { memo, useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
|
import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
|
||||||
|
|
||||||
@@ -54,27 +57,29 @@ type ConfigOptions = {
|
|||||||
shouldShowLocalizationToggle?: boolean;
|
shouldShowLocalizationToggle?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultConfig: ConfigOptions = {
|
||||||
|
shouldShowDeveloperSettings: true,
|
||||||
|
shouldShowResetWebUiText: true,
|
||||||
|
shouldShowClearIntermediates: true,
|
||||||
|
shouldShowLocalizationToggle: true,
|
||||||
|
};
|
||||||
|
|
||||||
type SettingsModalProps = {
|
type SettingsModalProps = {
|
||||||
/* The button to open the Settings Modal */
|
|
||||||
children: ReactElement;
|
|
||||||
config?: ConfigOptions;
|
config?: ConfigOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
const $settingsModal = atom(false);
|
||||||
|
export const useSettingsModal = buildUseBoolean($settingsModal);
|
||||||
|
|
||||||
|
const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [countdown, setCountdown] = useState(3);
|
|
||||||
|
|
||||||
const shouldShowDeveloperSettings = config?.shouldShowDeveloperSettings ?? true;
|
|
||||||
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
|
|
||||||
const shouldShowClearIntermediates = config?.shouldShowClearIntermediates ?? true;
|
|
||||||
const shouldShowLocalizationToggle = config?.shouldShowLocalizationToggle ?? true;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldShowDeveloperSettings) {
|
if (!config?.shouldShowDeveloperSettings) {
|
||||||
dispatch(logIsEnabledChanged(false));
|
dispatch(logIsEnabledChanged(false));
|
||||||
}
|
}
|
||||||
}, [shouldShowDeveloperSettings, dispatch]);
|
}, [dispatch, config?.shouldShowDeveloperSettings]);
|
||||||
|
|
||||||
const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, {
|
const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, {
|
||||||
selectFromResult: ({ data }) => ({
|
selectFromResult: ({ data }) => ({
|
||||||
@@ -89,11 +94,10 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
intermediatesCount,
|
intermediatesCount,
|
||||||
isLoading: isLoadingClearIntermediates,
|
isLoading: isLoadingClearIntermediates,
|
||||||
refetchIntermediatesCount,
|
refetchIntermediatesCount,
|
||||||
} = useClearIntermediates(shouldShowClearIntermediates);
|
} = useClearIntermediates(Boolean(config?.shouldShowClearIntermediates));
|
||||||
|
const settingsModal = useSettingsModal();
|
||||||
const { isOpen: isSettingsModalOpen, onOpen: _onSettingsModalOpen, onClose: onSettingsModalClose } = useDisclosure();
|
const settingsModalIsOpen = useStore(settingsModal.$boolean);
|
||||||
|
const refreshModal = useRefreshAfterResetModal();
|
||||||
const { isOpen: isRefreshModalOpen, onOpen: onRefreshModalOpen, onClose: onRefreshModalClose } = useDisclosure();
|
|
||||||
|
|
||||||
const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise);
|
const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise);
|
||||||
const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
|
const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
|
||||||
@@ -105,25 +109,17 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
|
|
||||||
const clearStorage = useClearStorage();
|
const clearStorage = useClearStorage();
|
||||||
|
|
||||||
const handleOpenSettingsModel = useCallback(() => {
|
useEffect(() => {
|
||||||
if (shouldShowClearIntermediates) {
|
if (settingsModalIsOpen && Boolean(config?.shouldShowClearIntermediates)) {
|
||||||
refetchIntermediatesCount();
|
refetchIntermediatesCount();
|
||||||
}
|
}
|
||||||
_onSettingsModalOpen();
|
}, [config?.shouldShowClearIntermediates, refetchIntermediatesCount, settingsModalIsOpen]);
|
||||||
}, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]);
|
|
||||||
|
|
||||||
const handleClickResetWebUI = useCallback(() => {
|
const handleClickResetWebUI = useCallback(() => {
|
||||||
clearStorage();
|
clearStorage();
|
||||||
onSettingsModalClose();
|
settingsModal.setFalse();
|
||||||
onRefreshModalOpen();
|
refreshModal.setTrue();
|
||||||
setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
}, [clearStorage, settingsModal, refreshModal]);
|
||||||
}, [clearStorage, onSettingsModalClose, onRefreshModalOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (countdown <= 0) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}, [countdown]);
|
|
||||||
|
|
||||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -169,139 +165,107 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal isOpen={settingsModalIsOpen} onClose={settingsModal.setFalse} size="2xl" isCentered useInert={false}>
|
||||||
{cloneElement(children, {
|
<ModalOverlay />
|
||||||
onClick: handleOpenSettingsModel,
|
<ModalContent maxH="80vh" h="68rem">
|
||||||
})}
|
<ModalHeader bg="none">{t('common.settingsLabel')}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody display="flex" flexDir="column" gap={4}>
|
||||||
|
<ScrollableContent>
|
||||||
|
<Flex flexDir="column" gap={4}>
|
||||||
|
<FormControlGroup formLabelProps={{ flexGrow: 1 }}>
|
||||||
|
<StickyScrollable title={t('settings.general')}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{t('settings.confirmOnDelete')}</FormLabel>
|
||||||
|
<Switch isChecked={shouldConfirmOnDelete} onChange={handleChangeShouldConfirmOnDelete} />
|
||||||
|
</FormControl>
|
||||||
|
</StickyScrollable>
|
||||||
|
|
||||||
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose} size="2xl" isCentered>
|
<StickyScrollable title={t('settings.generation')}>
|
||||||
<ModalOverlay />
|
<FormControl isDisabled={!isNSFWCheckerAvailable}>
|
||||||
<ModalContent maxH="80vh" h="68rem">
|
<FormLabel>{t('settings.enableNSFWChecker')}</FormLabel>
|
||||||
<ModalHeader bg="none">{t('common.settingsLabel')}</ModalHeader>
|
<Switch isChecked={shouldUseNSFWChecker} onChange={handleChangeShouldUseNSFWChecker} />
|
||||||
<ModalCloseButton />
|
</FormControl>
|
||||||
<ModalBody display="flex" flexDir="column" gap={4}>
|
<FormControl isDisabled={!isWatermarkerAvailable}>
|
||||||
<ScrollableContent>
|
<FormLabel>{t('settings.enableInvisibleWatermark')}</FormLabel>
|
||||||
<Flex flexDir="column" gap={4}>
|
<Switch isChecked={shouldUseWatermarker} onChange={handleChangeShouldUseWatermarker} />
|
||||||
<FormControlGroup formLabelProps={{ flexGrow: 1 }}>
|
</FormControl>
|
||||||
<StickyScrollable title={t('settings.general')}>
|
</StickyScrollable>
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('settings.confirmOnDelete')}</FormLabel>
|
<StickyScrollable title={t('settings.ui')}>
|
||||||
<Switch isChecked={shouldConfirmOnDelete} onChange={handleChangeShouldConfirmOnDelete} />
|
<FormControl>
|
||||||
</FormControl>
|
<FormLabel>{t('settings.showProgressInViewer')}</FormLabel>
|
||||||
|
<Switch isChecked={shouldShowProgressInViewer} onChange={handleChangeShouldShowProgressInViewer} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{t('settings.antialiasProgressImages')}</FormLabel>
|
||||||
|
<Switch
|
||||||
|
isChecked={shouldAntialiasProgressImage}
|
||||||
|
onChange={handleChangeShouldAntialiasProgressImage}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<InformationalPopover feature="noiseUseCPU" inPortal={false}>
|
||||||
|
<FormLabel>{t('parameters.useCpuNoise')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Switch isChecked={shouldUseCpuNoise} onChange={handleChangeShouldUseCpuNoise} />
|
||||||
|
</FormControl>
|
||||||
|
{Boolean(config?.shouldShowLocalizationToggle) && <SettingsLanguageSelect />}
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{t('settings.enableInformationalPopovers')}</FormLabel>
|
||||||
|
<Switch
|
||||||
|
isChecked={shouldEnableInformationalPopovers}
|
||||||
|
onChange={handleChangeShouldEnableInformationalPopovers}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</StickyScrollable>
|
||||||
|
|
||||||
|
{Boolean(config?.shouldShowDeveloperSettings) && (
|
||||||
|
<StickyScrollable title={t('settings.developer')}>
|
||||||
|
<SettingsDeveloperLogIsEnabled />
|
||||||
|
<SettingsDeveloperLogLevel />
|
||||||
|
<SettingsDeveloperLogNamespaces />
|
||||||
</StickyScrollable>
|
</StickyScrollable>
|
||||||
|
)}
|
||||||
|
|
||||||
<StickyScrollable title={t('settings.generation')}>
|
{Boolean(config?.shouldShowClearIntermediates) && (
|
||||||
<FormControl isDisabled={!isNSFWCheckerAvailable}>
|
<StickyScrollable title={t('settings.clearIntermediates')}>
|
||||||
<FormLabel>{t('settings.enableNSFWChecker')}</FormLabel>
|
<Button
|
||||||
<Switch isChecked={shouldUseNSFWChecker} onChange={handleChangeShouldUseNSFWChecker} />
|
tooltip={hasPendingItems ? t('settings.clearIntermediatesDisabled') : undefined}
|
||||||
</FormControl>
|
colorScheme="warning"
|
||||||
<FormControl isDisabled={!isWatermarkerAvailable}>
|
onClick={clearIntermediates}
|
||||||
<FormLabel>{t('settings.enableInvisibleWatermark')}</FormLabel>
|
isLoading={isLoadingClearIntermediates}
|
||||||
<Switch isChecked={shouldUseWatermarker} onChange={handleChangeShouldUseWatermarker} />
|
isDisabled={!intermediatesCount || hasPendingItems}
|
||||||
</FormControl>
|
>
|
||||||
</StickyScrollable>
|
{t('settings.clearIntermediatesWithCount', {
|
||||||
|
count: intermediatesCount ?? 0,
|
||||||
<StickyScrollable title={t('settings.ui')}>
|
})}
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('settings.showProgressInViewer')}</FormLabel>
|
|
||||||
<Switch
|
|
||||||
isChecked={shouldShowProgressInViewer}
|
|
||||||
onChange={handleChangeShouldShowProgressInViewer}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('settings.antialiasProgressImages')}</FormLabel>
|
|
||||||
<Switch
|
|
||||||
isChecked={shouldAntialiasProgressImage}
|
|
||||||
onChange={handleChangeShouldAntialiasProgressImage}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<InformationalPopover feature="noiseUseCPU" inPortal={false}>
|
|
||||||
<FormLabel>{t('parameters.useCpuNoise')}</FormLabel>
|
|
||||||
</InformationalPopover>
|
|
||||||
<Switch isChecked={shouldUseCpuNoise} onChange={handleChangeShouldUseCpuNoise} />
|
|
||||||
</FormControl>
|
|
||||||
{shouldShowLocalizationToggle && <SettingsLanguageSelect />}
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>{t('settings.enableInformationalPopovers')}</FormLabel>
|
|
||||||
<Switch
|
|
||||||
isChecked={shouldEnableInformationalPopovers}
|
|
||||||
onChange={handleChangeShouldEnableInformationalPopovers}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</StickyScrollable>
|
|
||||||
|
|
||||||
{shouldShowDeveloperSettings && (
|
|
||||||
<StickyScrollable title={t('settings.developer')}>
|
|
||||||
<SettingsDeveloperLogIsEnabled />
|
|
||||||
<SettingsDeveloperLogLevel />
|
|
||||||
<SettingsDeveloperLogNamespaces />
|
|
||||||
</StickyScrollable>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldShowClearIntermediates && (
|
|
||||||
<StickyScrollable title={t('settings.clearIntermediates')}>
|
|
||||||
<Button
|
|
||||||
tooltip={hasPendingItems ? t('settings.clearIntermediatesDisabled') : undefined}
|
|
||||||
colorScheme="warning"
|
|
||||||
onClick={clearIntermediates}
|
|
||||||
isLoading={isLoadingClearIntermediates}
|
|
||||||
isDisabled={!intermediatesCount || hasPendingItems}
|
|
||||||
>
|
|
||||||
{t('settings.clearIntermediatesWithCount', {
|
|
||||||
count: intermediatesCount ?? 0,
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
<Text fontWeight="bold">{t('settings.clearIntermediatesDesc1')}</Text>
|
|
||||||
<Text variant="subtext">{t('settings.clearIntermediatesDesc2')}</Text>
|
|
||||||
<Text variant="subtext">{t('settings.clearIntermediatesDesc3')}</Text>
|
|
||||||
</StickyScrollable>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<StickyScrollable title={t('settings.resetWebUI')}>
|
|
||||||
<Button colorScheme="error" onClick={handleClickResetWebUI}>
|
|
||||||
{t('settings.resetWebUI')}
|
|
||||||
</Button>
|
</Button>
|
||||||
{shouldShowResetWebUiText && (
|
<Text fontWeight="bold">{t('settings.clearIntermediatesDesc1')}</Text>
|
||||||
<>
|
<Text variant="subtext">{t('settings.clearIntermediatesDesc2')}</Text>
|
||||||
<Text variant="subtext">{t('settings.resetWebUIDesc1')}</Text>
|
<Text variant="subtext">{t('settings.clearIntermediatesDesc3')}</Text>
|
||||||
<Text variant="subtext">{t('settings.resetWebUIDesc2')}</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</StickyScrollable>
|
</StickyScrollable>
|
||||||
</FormControlGroup>
|
)}
|
||||||
</Flex>
|
|
||||||
</ScrollableContent>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter />
|
<StickyScrollable title={t('settings.resetWebUI')}>
|
||||||
</ModalContent>
|
<Button colorScheme="error" onClick={handleClickResetWebUI}>
|
||||||
</Modal>
|
{t('settings.resetWebUI')}
|
||||||
|
</Button>
|
||||||
<Modal
|
{Boolean(config?.shouldShowResetWebUiText) && (
|
||||||
closeOnOverlayClick={false}
|
<>
|
||||||
isOpen={isRefreshModalOpen}
|
<Text variant="subtext">{t('settings.resetWebUIDesc1')}</Text>
|
||||||
onClose={onRefreshModalClose}
|
<Text variant="subtext">{t('settings.resetWebUIDesc2')}</Text>
|
||||||
isCentered
|
</>
|
||||||
closeOnEsc={false}
|
)}
|
||||||
>
|
</StickyScrollable>
|
||||||
<ModalOverlay backdropFilter="blur(40px)" />
|
</FormControlGroup>
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader />
|
|
||||||
<ModalBody>
|
|
||||||
<Flex justifyContent="center">
|
|
||||||
<Text fontSize="lg">
|
|
||||||
<Text>
|
|
||||||
{t('settings.resetComplete')} {t('settings.reloadingIn')} {countdown}...
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</ScrollableContent>
|
||||||
<ModalFooter />
|
</ModalBody>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
<ModalFooter />
|
||||||
</>
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const VerticalNavBar = memo(() => {
|
|||||||
const customNavComponent = useStore($customNavComponent);
|
const customNavComponent = useStore($customNavComponent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
|
<Flex flexDir="column" alignItems="center" py={2} gap={4}>
|
||||||
<InvokeAILogoComponent />
|
<InvokeAILogoComponent />
|
||||||
<Flex gap={4} pt={6} h="full" flexDir="column">
|
<Flex gap={4} pt={6} h="full" flexDir="column">
|
||||||
<TabMountGate tab="generation">
|
<TabMountGate tab="generation">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const LoadWorkflowFromGraphModal = () => {
|
|||||||
onClose();
|
onClose();
|
||||||
}, [dispatch, onClose, workflowRaw]);
|
}, [dispatch, onClose, workflowRaw]);
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} isCentered useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
|
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
|
||||||
<ModalHeader>{t('workflows.loadFromGraph')}</ModalHeader>
|
<ModalHeader>{t('workflows.loadFromGraph')}</ModalHeader>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t('nodes.newWorkflow')}
|
title={t('nodes.newWorkflow')}
|
||||||
acceptCallback={handleNewWorkflow}
|
acceptCallback={handleNewWorkflow}
|
||||||
|
useInert={false}
|
||||||
>
|
>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
<Text>{t('nodes.newWorkflowDesc')}</Text>
|
<Text>{t('nodes.newWorkflowDesc')}</Text>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const WorkflowLibraryModal = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onClose } = useWorkflowLibraryModalContext();
|
const { isOpen, onClose } = useWorkflowLibraryModalContext();
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} isCentered useInert={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w="80%" h="80%" minW="unset" minH="unset" maxW="1200px" maxH="664px">
|
<ModalContent w="80%" h="80%" minW="unset" minH="unset" maxW="1200px" maxH="664px">
|
||||||
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
||||||
|
|||||||
@@ -15241,10 +15241,9 @@ export type components = {
|
|||||||
version: string;
|
version: string;
|
||||||
/**
|
/**
|
||||||
* Node Pack
|
* Node Pack
|
||||||
* @description Whether or not this is a custom node
|
* @description The node pack that this node belongs to, will be 'invokeai' for built-in nodes
|
||||||
* @default null
|
|
||||||
*/
|
*/
|
||||||
node_pack: string | null;
|
node_pack: string;
|
||||||
/**
|
/**
|
||||||
* @description The node's classification
|
* @description The node's classification
|
||||||
* @default stable
|
* @default stable
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.2.9.dev5"
|
__version__ = "4.2.9.dev6"
|
||||||
|
|||||||
Reference in New Issue
Block a user