mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into feat/dev_reload
This commit is contained in:
commit
54e844f7da
@ -170,6 +170,7 @@ class _InputField(BaseModel):
|
|||||||
ui_hidden: bool
|
ui_hidden: bool
|
||||||
ui_type: Optional[UIType]
|
ui_type: Optional[UIType]
|
||||||
ui_component: Optional[UIComponent]
|
ui_component: Optional[UIComponent]
|
||||||
|
ui_order: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class _OutputField(BaseModel):
|
class _OutputField(BaseModel):
|
||||||
@ -182,6 +183,7 @@ class _OutputField(BaseModel):
|
|||||||
|
|
||||||
ui_hidden: bool
|
ui_hidden: bool
|
||||||
ui_type: Optional[UIType]
|
ui_type: Optional[UIType]
|
||||||
|
ui_order: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
def InputField(
|
def InputField(
|
||||||
@ -215,6 +217,7 @@ def InputField(
|
|||||||
ui_type: Optional[UIType] = None,
|
ui_type: Optional[UIType] = None,
|
||||||
ui_component: Optional[UIComponent] = None,
|
ui_component: Optional[UIComponent] = None,
|
||||||
ui_hidden: bool = False,
|
ui_hidden: bool = False,
|
||||||
|
ui_order: Optional[int] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
@ -273,6 +276,7 @@ def InputField(
|
|||||||
ui_type=ui_type,
|
ui_type=ui_type,
|
||||||
ui_component=ui_component,
|
ui_component=ui_component,
|
||||||
ui_hidden=ui_hidden,
|
ui_hidden=ui_hidden,
|
||||||
|
ui_order=ui_order,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -306,6 +310,7 @@ def OutputField(
|
|||||||
repr: bool = True,
|
repr: bool = True,
|
||||||
ui_type: Optional[UIType] = None,
|
ui_type: Optional[UIType] = None,
|
||||||
ui_hidden: bool = False,
|
ui_hidden: bool = False,
|
||||||
|
ui_order: Optional[int] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
@ -352,6 +357,7 @@ def OutputField(
|
|||||||
repr=repr,
|
repr=repr,
|
||||||
ui_type=ui_type,
|
ui_type=ui_type,
|
||||||
ui_hidden=ui_hidden,
|
ui_hidden=ui_hidden,
|
||||||
|
ui_order=ui_order,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -380,7 +386,7 @@ class BaseInvocationOutput(BaseModel):
|
|||||||
"""Base class for all invocation outputs"""
|
"""Base class for all invocation outputs"""
|
||||||
|
|
||||||
# All outputs must include a type name like this:
|
# All outputs must include a type name like this:
|
||||||
# type: Literal['your_output_name']
|
# type: Literal['your_output_name'] # noqa f821
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_subclasses_tuple(cls):
|
def get_all_subclasses_tuple(cls):
|
||||||
@ -421,7 +427,7 @@ class BaseInvocation(ABC, BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# All invocations must include a type name like this:
|
# All invocations must include a type name like this:
|
||||||
# type: Literal['your_output_name']
|
# type: Literal['your_output_name'] # noqa f821
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_subclasses(cls):
|
def get_all_subclasses(cls):
|
||||||
@ -499,7 +505,7 @@ class BaseInvocation(ABC, BaseModel):
|
|||||||
raise MissingInputException(self.__fields__["type"].default, field_name)
|
raise MissingInputException(self.__fields__["type"].default, field_name)
|
||||||
return self.invoke(context)
|
return self.invoke(context)
|
||||||
|
|
||||||
id: str = InputField(description="The id of this node. Must be unique among all nodes.")
|
id: str = Field(description="The id of this node. Must be unique among all nodes.")
|
||||||
is_intermediate: bool = InputField(
|
is_intermediate: bool = InputField(
|
||||||
default=False, description="Whether or not this node is an intermediate node.", input=Input.Direct
|
default=False, description="Whether or not this node is an intermediate node.", input=Input.Direct
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ import numpy
|
|||||||
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
||||||
|
|
||||||
from invokeai.app.invocations.metadata import CoreMetadata
|
from invokeai.app.invocations.metadata import CoreMetadata
|
||||||
from invokeai.app.invocations.primitives import ImageField, ImageOutput
|
from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput
|
||||||
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
|
||||||
from invokeai.backend.image_util.safety_checker import SafetyChecker
|
from invokeai.backend.image_util.safety_checker import SafetyChecker
|
||||||
|
|
||||||
@ -41,6 +41,39 @@ class ShowImageInvocation(BaseInvocation):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@title("Blank Image")
|
||||||
|
@tags("image")
|
||||||
|
class BlankImageInvocation(BaseInvocation):
|
||||||
|
"""Creates a blank image and forwards it to the pipeline"""
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
type: Literal["blank_image"] = "blank_image"
|
||||||
|
|
||||||
|
# Inputs
|
||||||
|
width: int = InputField(default=512, description="The width of the image")
|
||||||
|
height: int = InputField(default=512, description="The height of the image")
|
||||||
|
mode: Literal["RGB", "RGBA"] = InputField(default="RGB", description="The mode of the image")
|
||||||
|
color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color of the image")
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
|
image = Image.new(mode=self.mode, size=(self.width, self.height), color=self.color.tuple())
|
||||||
|
|
||||||
|
image_dto = context.services.images.create(
|
||||||
|
image=image,
|
||||||
|
image_origin=ResourceOrigin.INTERNAL,
|
||||||
|
image_category=ImageCategory.GENERAL,
|
||||||
|
node_id=self.id,
|
||||||
|
session_id=context.graph_execution_state_id,
|
||||||
|
is_intermediate=self.is_intermediate,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ImageOutput(
|
||||||
|
image=ImageField(image_name=image_dto.image_name),
|
||||||
|
width=image_dto.width,
|
||||||
|
height=image_dto.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@title("Crop Image")
|
@title("Crop Image")
|
||||||
@tags("image", "crop")
|
@tags("image", "crop")
|
||||||
class ImageCropInvocation(BaseInvocation):
|
class ImageCropInvocation(BaseInvocation):
|
||||||
|
@ -107,12 +107,12 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
|||||||
|
|
||||||
# Inputs
|
# Inputs
|
||||||
positive_conditioning: ConditioningField = InputField(
|
positive_conditioning: ConditioningField = InputField(
|
||||||
description=FieldDescriptions.positive_cond, input=Input.Connection
|
description=FieldDescriptions.positive_cond, input=Input.Connection, ui_order=0
|
||||||
)
|
)
|
||||||
negative_conditioning: ConditioningField = InputField(
|
negative_conditioning: ConditioningField = InputField(
|
||||||
description=FieldDescriptions.negative_cond, input=Input.Connection
|
description=FieldDescriptions.negative_cond, input=Input.Connection, ui_order=1
|
||||||
)
|
)
|
||||||
noise: Optional[LatentsField] = InputField(description=FieldDescriptions.noise, input=Input.Connection)
|
noise: Optional[LatentsField] = InputField(description=FieldDescriptions.noise, input=Input.Connection, ui_order=3)
|
||||||
steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
|
steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
|
||||||
cfg_scale: Union[float, List[float]] = InputField(
|
cfg_scale: Union[float, List[float]] = InputField(
|
||||||
default=7.5, ge=1, description=FieldDescriptions.cfg_scale, ui_type=UIType.Float, title="CFG Scale"
|
default=7.5, ge=1, description=FieldDescriptions.cfg_scale, ui_type=UIType.Float, title="CFG Scale"
|
||||||
@ -122,11 +122,13 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
|||||||
scheduler: SAMPLER_NAME_VALUES = InputField(
|
scheduler: SAMPLER_NAME_VALUES = InputField(
|
||||||
default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler
|
default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler
|
||||||
)
|
)
|
||||||
unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection, title="UNet")
|
unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection, title="UNet", ui_order=2)
|
||||||
control: Union[ControlField, list[ControlField]] = InputField(
|
control: Union[ControlField, list[ControlField]] = InputField(
|
||||||
default=None, description=FieldDescriptions.control, input=Input.Connection
|
default=None, description=FieldDescriptions.control, input=Input.Connection, ui_order=5
|
||||||
|
)
|
||||||
|
latents: Optional[LatentsField] = InputField(
|
||||||
|
description=FieldDescriptions.latents, input=Input.Connection, ui_order=4
|
||||||
)
|
)
|
||||||
latents: Optional[LatentsField] = InputField(description=FieldDescriptions.latents, input=Input.Connection)
|
|
||||||
mask: Optional[ImageField] = InputField(
|
mask: Optional[ImageField] = InputField(
|
||||||
default=None,
|
default=None,
|
||||||
description=FieldDescriptions.mask,
|
description=FieldDescriptions.mask,
|
||||||
|
@ -9,8 +9,8 @@ module.exports = {
|
|||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:prettier/recommended',
|
|
||||||
'plugin:react/jsx-runtime',
|
'plugin:react/jsx-runtime',
|
||||||
|
'prettier',
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@ -39,7 +39,6 @@ module.exports = {
|
|||||||
'warn',
|
'warn',
|
||||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||||
],
|
],
|
||||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-empty-interface': [
|
'@typescript-eslint/no-empty-interface': [
|
||||||
|
@ -29,12 +29,13 @@
|
|||||||
"lint:eslint": "eslint --max-warnings=0 .",
|
"lint:eslint": "eslint --max-warnings=0 .",
|
||||||
"lint:prettier": "prettier --check .",
|
"lint:prettier": "prettier --check .",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:tsc && yarn run lint:madge",
|
"lint": "concurrently -g -n eslint,prettier,tsc,madge -c cyan,green,magenta,yellow \"yarn run lint:eslint\" \"yarn run lint:prettier\" \"yarn run lint:tsc\" \"yarn run lint:madge\"",
|
||||||
"fix": "eslint --fix . && prettier --loglevel warn --write . && tsc --noEmit",
|
"fix": "eslint --fix . && prettier --loglevel warn --write . && tsc --noEmit",
|
||||||
"lint-staged": "lint-staged",
|
"lint-staged": "lint-staged",
|
||||||
"postinstall": "patch-package && yarn run theme",
|
"postinstall": "patch-package && yarn run theme",
|
||||||
"theme": "chakra-cli tokens src/theme/theme.ts",
|
"theme": "chakra-cli tokens src/theme/theme.ts",
|
||||||
"theme:watch": "chakra-cli tokens src/theme/theme.ts --watch"
|
"theme:watch": "chakra-cli tokens src/theme/theme.ts --watch",
|
||||||
|
"up": "yarn upgrade-interactive --latest"
|
||||||
},
|
},
|
||||||
"madge": {
|
"madge": {
|
||||||
"detectiveOptions": {
|
"detectiveOptions": {
|
||||||
@ -54,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/anatomy": "^2.2.0",
|
"@chakra-ui/anatomy": "^2.2.0",
|
||||||
"@chakra-ui/icons": "^2.0.19",
|
"@chakra-ui/icons": "^2.1.0",
|
||||||
"@chakra-ui/react": "^2.8.0",
|
"@chakra-ui/react": "^2.8.0",
|
||||||
"@chakra-ui/styled-system": "^2.9.1",
|
"@chakra-ui/styled-system": "^2.9.1",
|
||||||
"@chakra-ui/theme-tools": "^2.1.0",
|
"@chakra-ui/theme-tools": "^2.1.0",
|
||||||
@ -65,56 +66,55 @@
|
|||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@floating-ui/react-dom": "^2.0.1",
|
"@floating-ui/react-dom": "^2.0.1",
|
||||||
"@fontsource-variable/inter": "^5.0.3",
|
"@fontsource-variable/inter": "^5.0.8",
|
||||||
"@fontsource/inter": "^5.0.3",
|
"@fontsource/inter": "^5.0.8",
|
||||||
"@mantine/core": "^6.0.14",
|
"@mantine/core": "^6.0.19",
|
||||||
"@mantine/form": "^6.0.15",
|
"@mantine/form": "^6.0.19",
|
||||||
"@mantine/hooks": "^6.0.14",
|
"@mantine/hooks": "^6.0.19",
|
||||||
"@nanostores/react": "^0.7.1",
|
"@nanostores/react": "^0.7.1",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@roarr/browser-log-writer": "^1.1.5",
|
"@roarr/browser-log-writer": "^1.1.5",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"downshift": "^7.6.0",
|
"formik": "^2.4.3",
|
||||||
"formik": "^2.4.2",
|
"framer-motion": "^10.16.1",
|
||||||
"framer-motion": "^10.12.17",
|
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"i18next": "^23.2.3",
|
"i18next": "^23.4.4",
|
||||||
"i18next-browser-languagedetector": "^7.0.2",
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
"i18next-http-backend": "^2.2.1",
|
"i18next-http-backend": "^2.2.1",
|
||||||
"konva": "^9.2.0",
|
"konva": "^9.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanostores": "^0.9.2",
|
"nanostores": "^0.9.2",
|
||||||
"new-github-issue-url": "^1.0.0",
|
"new-github-issue-url": "^1.0.0",
|
||||||
"openapi-fetch": "^0.6.1",
|
"openapi-fetch": "^0.7.4",
|
||||||
"overlayscrollbars": "^2.2.0",
|
"overlayscrollbars": "^2.2.0",
|
||||||
"overlayscrollbars-react": "^0.5.0",
|
"overlayscrollbars-react": "^0.5.0",
|
||||||
"patch-package": "^7.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"re-resizable": "^6.9.9",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-error-boundary": "^4.0.11",
|
"react-error-boundary": "^4.0.11",
|
||||||
"react-hotkeys-hook": "4.4.0",
|
"react-hotkeys-hook": "4.4.1",
|
||||||
"react-i18next": "^13.0.1",
|
"react-i18next": "^13.1.2",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-konva": "^18.2.10",
|
"react-konva": "^18.2.10",
|
||||||
"react-redux": "^8.1.1",
|
"react-redux": "^8.1.2",
|
||||||
"react-resizable-panels": "^0.0.52",
|
"react-resizable-panels": "^0.0.55",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"react-virtuoso": "^4.3.11",
|
"react-virtuoso": "^4.5.0",
|
||||||
"react-zoom-pan-pinch": "^3.0.8",
|
"react-zoom-pan-pinch": "^3.0.8",
|
||||||
"reactflow": "^11.7.4",
|
"reactflow": "^11.8.3",
|
||||||
"redux-dynamic-middlewares": "^2.2.0",
|
"redux-dynamic-middlewares": "^2.2.0",
|
||||||
"redux-remember": "^3.3.1",
|
"redux-remember": "^4.0.1",
|
||||||
"roarr": "^7.15.0",
|
"roarr": "^7.15.1",
|
||||||
"serialize-error": "^11.0.0",
|
"serialize-error": "^11.0.1",
|
||||||
"socket.io-client": "^4.7.0",
|
"socket.io-client": "^4.7.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"use-image": "^1.1.1",
|
"use-image": "^1.1.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.22.2",
|
||||||
|
"zod-validation-error": "^1.5.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@chakra-ui/cli": "^2.4.0",
|
"@chakra-ui/cli": "^2.4.0",
|
||||||
@ -127,38 +127,36 @@
|
|||||||
"@chakra-ui/cli": "^2.4.1",
|
"@chakra-ui/cli": "^2.4.1",
|
||||||
"@types/dateformat": "^5.0.0",
|
"@types/dateformat": "^5.0.0",
|
||||||
"@types/lodash-es": "^4.14.194",
|
"@types/lodash-es": "^4.14.194",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.5.1",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@types/react-redux": "^7.1.25",
|
"@types/react-redux": "^7.1.25",
|
||||||
"@types/react-transition-group": "^4.4.6",
|
"@types/react-transition-group": "^4.4.6",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"babel-plugin-transform-imports": "^2.0.0",
|
"babel-plugin-transform-imports": "^2.0.0",
|
||||||
"concurrently": "^8.2.0",
|
"concurrently": "^8.2.0",
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^14.0.1",
|
||||||
"madge": "^6.1.0",
|
"madge": "^6.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"openapi-typescript": "^6.2.8",
|
"openapi-typescript": "^6.5.2",
|
||||||
"openapi-typescript-codegen": "^0.24.0",
|
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.0.2",
|
||||||
"rollup-plugin-visualizer": "^5.9.2",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"terser": "^5.18.1",
|
|
||||||
"ts-toolbelt": "^9.6.0",
|
"ts-toolbelt": "^9.6.0",
|
||||||
"vite": "^4.3.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-css-injected-by-js": "^3.1.1",
|
"vite-plugin-css-injected-by-js": "^3.3.0",
|
||||||
"vite-plugin-dts": "^2.3.0",
|
"vite-plugin-dts": "^3.5.2",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"toggleAutoscroll": "Toggle autoscroll",
|
"toggleAutoscroll": "Toggle autoscroll",
|
||||||
"toggleLogViewer": "Toggle Log Viewer",
|
"toggleLogViewer": "Toggle Log Viewer",
|
||||||
"showGallery": "Show Gallery",
|
"showGallery": "Show Gallery",
|
||||||
"showOptionsPanel": "Show Options Panel",
|
"showOptionsPanel": "Show Side Panel",
|
||||||
"menu": "Menu"
|
"menu": "Menu"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@ -95,7 +95,6 @@
|
|||||||
"statusModelConverted": "Model Converted",
|
"statusModelConverted": "Model Converted",
|
||||||
"statusMergingModels": "Merging Models",
|
"statusMergingModels": "Merging Models",
|
||||||
"statusMergedModels": "Models Merged",
|
"statusMergedModels": "Models Merged",
|
||||||
"pinOptionsPanel": "Pin Options Panel",
|
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loadingInvokeAI": "Loading Invoke AI",
|
"loadingInvokeAI": "Loading Invoke AI",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
@ -116,7 +115,6 @@
|
|||||||
"maintainAspectRatio": "Maintain Aspect Ratio",
|
"maintainAspectRatio": "Maintain Aspect Ratio",
|
||||||
"autoSwitchNewImages": "Auto-Switch to New Images",
|
"autoSwitchNewImages": "Auto-Switch to New Images",
|
||||||
"singleColumnLayout": "Single Column Layout",
|
"singleColumnLayout": "Single Column Layout",
|
||||||
"pinGallery": "Pin Gallery",
|
|
||||||
"allImagesLoaded": "All Images Loaded",
|
"allImagesLoaded": "All Images Loaded",
|
||||||
"loadMore": "Load More",
|
"loadMore": "Load More",
|
||||||
"noImagesInGallery": "No Images to Display",
|
"noImagesInGallery": "No Images to Display",
|
||||||
@ -577,7 +575,7 @@
|
|||||||
"resetWebUI": "Reset Web UI",
|
"resetWebUI": "Reset Web UI",
|
||||||
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
|
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
|
||||||
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
|
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
|
||||||
"resetComplete": "Web UI has been reset. Refresh the page to reload.",
|
"resetComplete": "Web UI has been reset.",
|
||||||
"consoleLogLevel": "Log Level",
|
"consoleLogLevel": "Log Level",
|
||||||
"shouldLogToConsole": "Console Logging",
|
"shouldLogToConsole": "Console Logging",
|
||||||
"developer": "Developer",
|
"developer": "Developer",
|
||||||
@ -720,11 +718,12 @@
|
|||||||
"swapSizes": "Swap Sizes"
|
"swapSizes": "Swap Sizes"
|
||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"reloadSchema": "Reload Schema",
|
"reloadNodeTemplates": "Reload Node Templates",
|
||||||
"saveGraph": "Save Graph",
|
"saveWorkflow": "Save Workflow",
|
||||||
"loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)",
|
"loadWorkflow": "Load Workflow",
|
||||||
"clearGraph": "Clear Graph",
|
"resetWorkflow": "Reset Workflow",
|
||||||
"clearGraphDesc": "Are you sure you want to clear all nodes?",
|
"resetWorkflowDesc": "Are you sure you want to reset this workflow?",
|
||||||
|
"resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.",
|
||||||
"zoomInNodes": "Zoom In",
|
"zoomInNodes": "Zoom In",
|
||||||
"zoomOutNodes": "Zoom Out",
|
"zoomOutNodes": "Zoom Out",
|
||||||
"fitViewportNodes": "Fit View",
|
"fitViewportNodes": "Fit View",
|
||||||
|
@ -27,21 +27,10 @@ async function main() {
|
|||||||
* field accepts connection input. If it does, we can make the field optional.
|
* field accepts connection input. If it does, we can make the field optional.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Check if we are generating types for an invocation
|
if ('class' in schemaObject && schemaObject.class === 'invocation') {
|
||||||
const isInvocationPath = metadata.path.match(
|
|
||||||
/^#\/components\/schemas\/\w*Invocation$/
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasInvocationProperties =
|
|
||||||
schemaObject.properties &&
|
|
||||||
['id', 'is_intermediate', 'type'].every(
|
|
||||||
(prop) => prop in schemaObject.properties
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInvocationPath && hasInvocationProperties) {
|
|
||||||
// We only want to make fields optional if they are required
|
// We only want to make fields optional if they are required
|
||||||
if (!Array.isArray(schemaObject?.required)) {
|
if (!Array.isArray(schemaObject?.required)) {
|
||||||
schemaObject.required = ['id', 'type'];
|
schemaObject.required = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
schemaObject.required.forEach((prop) => {
|
schemaObject.required.forEach((prop) => {
|
||||||
@ -60,32 +49,12 @@ async function main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
schemaObject.required = [
|
|
||||||
...new Set(schemaObject.required.concat(['id', 'type'])),
|
|
||||||
];
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we are generating types for an invocation output
|
// Check if we are generating types for an invocation output
|
||||||
const isInvocationOutputPath = metadata.path.match(
|
if ('class' in schemaObject && schemaObject.class === 'output') {
|
||||||
/^#\/components\/schemas\/\w*Output$/
|
// modify output types
|
||||||
);
|
|
||||||
|
|
||||||
const hasOutputProperties =
|
|
||||||
schemaObject.properties && 'type' in schemaObject.properties;
|
|
||||||
|
|
||||||
if (isInvocationOutputPath && hasOutputProperties) {
|
|
||||||
if (!Array.isArray(schemaObject?.required)) {
|
|
||||||
schemaObject.required = ['type'];
|
|
||||||
}
|
|
||||||
schemaObject.required = [
|
|
||||||
...new Set(schemaObject.required.concat(['type'])),
|
|
||||||
];
|
|
||||||
console.log(
|
|
||||||
`Making output's "type" required: ${COLORS.fg.yellow}${schemaObject.title}${COLORS.reset}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, Grid, Portal } from '@chakra-ui/react';
|
import { Flex, Grid } from '@chakra-ui/react';
|
||||||
import { useLogger } from 'app/logging/useLogger';
|
import { useLogger } from 'app/logging/useLogger';
|
||||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -6,21 +6,17 @@ import { PartialAppConfig } from 'app/types/invokeai';
|
|||||||
import ImageUploader from 'common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
|
||||||
import SiteHeader from 'features/system/components/SiteHeader';
|
import SiteHeader from 'features/system/components/SiteHeader';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
|
||||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||||
import GlobalHotkeys from './GlobalHotkeys';
|
import GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {};
|
const DEFAULT_CONFIG = {};
|
||||||
|
|
||||||
@ -32,7 +28,7 @@ interface Props {
|
|||||||
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||||
const language = useAppSelector(languageSelector);
|
const language = useAppSelector(languageSelector);
|
||||||
|
|
||||||
const logger = useLogger();
|
const logger = useLogger('system');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@ -46,7 +42,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (size(config)) {
|
if (size(config)) {
|
||||||
logger.info({ namespace: 'App', config }, 'Received config');
|
logger.info({ config }, 'Received config');
|
||||||
dispatch(configChanged(config));
|
dispatch(configChanged(config));
|
||||||
}
|
}
|
||||||
}, [dispatch, config, logger]);
|
}, [dispatch, config, logger]);
|
||||||
@ -83,15 +79,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
|
|
||||||
<GalleryDrawer />
|
|
||||||
<ParametersDrawer />
|
|
||||||
<Portal>
|
|
||||||
<FloatingParametersPanelButtons />
|
|
||||||
</Portal>
|
|
||||||
<Portal>
|
|
||||||
<FloatingGalleryButton />
|
|
||||||
</Portal>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<DeleteImageModal />
|
<DeleteImageModal />
|
||||||
<ChangeBoardModal />
|
<ChangeBoardModal />
|
||||||
|
@ -1,30 +1,21 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import {
|
import {
|
||||||
ctrlKeyPressed,
|
ctrlKeyPressed,
|
||||||
metaKeyPressed,
|
metaKeyPressed,
|
||||||
shiftKeyPressed,
|
shiftKeyPressed,
|
||||||
} from 'features/ui/store/hotkeysSlice';
|
} from 'features/ui/store/hotkeysSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import {
|
|
||||||
setActiveTab,
|
|
||||||
toggleGalleryPanel,
|
|
||||||
toggleParametersPanel,
|
|
||||||
togglePinGalleryPanel,
|
|
||||||
togglePinParametersPanel,
|
|
||||||
} from 'features/ui/store/uiSlice';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
const globalHotkeysSelector = createSelector(
|
const globalHotkeysSelector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ hotkeys, ui }) => {
|
({ hotkeys }) => {
|
||||||
const { shift, ctrl, meta } = hotkeys;
|
const { shift, ctrl, meta } = hotkeys;
|
||||||
const { shouldPinParametersPanel, shouldPinGallery } = ui;
|
return { shift, ctrl, meta };
|
||||||
return { shift, ctrl, meta, shouldPinGallery, shouldPinParametersPanel };
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
@ -41,9 +32,7 @@ const globalHotkeysSelector = createSelector(
|
|||||||
*/
|
*/
|
||||||
const GlobalHotkeys: React.FC = () => {
|
const GlobalHotkeys: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { shift, ctrl, meta, shouldPinParametersPanel, shouldPinGallery } =
|
const { shift, ctrl, meta } = useAppSelector(globalHotkeysSelector);
|
||||||
useAppSelector(globalHotkeysSelector);
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'*',
|
'*',
|
||||||
@ -68,34 +57,6 @@ const GlobalHotkeys: React.FC = () => {
|
|||||||
[shift, ctrl, meta]
|
[shift, ctrl, meta]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys('o', () => {
|
|
||||||
dispatch(toggleParametersPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas' && shouldPinParametersPanel) {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys(['shift+o'], () => {
|
|
||||||
dispatch(togglePinParametersPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys('g', () => {
|
|
||||||
dispatch(toggleGalleryPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas' && shouldPinGallery) {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys(['shift+g'], () => {
|
|
||||||
dispatch(togglePinGalleryPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys('1', () => {
|
useHotkeys('1', () => {
|
||||||
dispatch(setActiveTab('txt2img'));
|
dispatch(setActiveTab('txt2img'));
|
||||||
});
|
});
|
||||||
@ -112,6 +73,10 @@ const GlobalHotkeys: React.FC = () => {
|
|||||||
dispatch(setActiveTab('nodes'));
|
dispatch(setActiveTab('nodes'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useHotkeys('5', () => {
|
||||||
|
dispatch(setActiveTab('modelManager'));
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export const log = Roarr.child(BASE_CONTEXT);
|
|||||||
|
|
||||||
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
|
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
|
||||||
|
|
||||||
type LoggerNamespace =
|
export type LoggerNamespace =
|
||||||
| 'images'
|
| 'images'
|
||||||
| 'models'
|
| 'models'
|
||||||
| 'config'
|
| 'config'
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { createLogWriter } from '@roarr/browser-log-writer';
|
import { createLogWriter } from '@roarr/browser-log-writer';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { ROARR, Roarr } from 'roarr';
|
import { ROARR, Roarr } from 'roarr';
|
||||||
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP } from './logger';
|
import {
|
||||||
|
$logger,
|
||||||
|
BASE_CONTEXT,
|
||||||
|
LOG_LEVEL_MAP,
|
||||||
|
LoggerNamespace,
|
||||||
|
logger,
|
||||||
|
} from './logger';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
systemSelector,
|
systemSelector,
|
||||||
@ -25,7 +30,7 @@ const selector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useLogger = () => {
|
export const useLogger = (namespace: LoggerNamespace) => {
|
||||||
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
|
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
|
||||||
|
|
||||||
// The provided Roarr browser log writer uses localStorage to config logging to console
|
// The provided Roarr browser log writer uses localStorage to config logging to console
|
||||||
@ -57,7 +62,7 @@ export const useLogger = () => {
|
|||||||
$logger.set(Roarr.child(newContext));
|
$logger.set(Roarr.child(newContext));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logger = useStore($logger);
|
const log = useMemo(() => logger(namespace), [namespace]);
|
||||||
|
|
||||||
return logger;
|
return log;
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import { modelsApi } from 'services/api/endpoints/models';
|
|||||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||||
import { appSocketConnected, socketConnected } from 'services/events/actions';
|
import { appSocketConnected, socketConnected } from 'services/events/actions';
|
||||||
import { startAppListening } from '../..';
|
import { startAppListening } from '../..';
|
||||||
|
import { size } from 'lodash-es';
|
||||||
|
|
||||||
export const addSocketConnectedEventListener = () => {
|
export const addSocketConnectedEventListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -18,7 +19,7 @@ export const addSocketConnectedEventListener = () => {
|
|||||||
|
|
||||||
const { disabledTabs } = config;
|
const { disabledTabs } = config;
|
||||||
|
|
||||||
if (!nodes.schema && !disabledTabs.includes('nodes')) {
|
if (!size(nodes.nodeTemplates) && !disabledTabs.includes('nodes')) {
|
||||||
dispatch(receivedOpenAPISchema());
|
dispatch(receivedOpenAPISchema());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,14 +34,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
borderTopRadius: 'base',
|
borderTopRadius: 'base',
|
||||||
borderBottomRadius: isOpen ? 0 : 'base',
|
borderBottomRadius: isOpen ? 0 : 'base',
|
||||||
bg: isOpen
|
bg: mode('base.250', 'base.750')(colorMode),
|
||||||
? mode('base.200', 'base.750')(colorMode)
|
|
||||||
: mode('base.150', 'base.800')(colorMode),
|
|
||||||
color: mode('base.900', 'base.100')(colorMode),
|
color: mode('base.900', 'base.100')(colorMode),
|
||||||
_hover: {
|
_hover: {
|
||||||
bg: isOpen
|
bg: mode('base.300', 'base.700')(colorMode),
|
||||||
? mode('base.250', 'base.700')(colorMode)
|
|
||||||
: mode('base.200', 'base.750')(colorMode),
|
|
||||||
},
|
},
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -90,9 +86,10 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
|
|||||||
<Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}>
|
<Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 4,
|
p: 2,
|
||||||
|
pt: 3,
|
||||||
borderBottomRadius: 'base',
|
borderBottomRadius: 'base',
|
||||||
bg: 'base.100',
|
bg: 'base.150',
|
||||||
_dark: {
|
_dark: {
|
||||||
bg: 'base.800',
|
bg: 'base.800',
|
||||||
},
|
},
|
||||||
|
@ -126,7 +126,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
bg: mode('base.200', 'base.800')(colorMode),
|
bg: mode('base.200', 'base.700')(colorMode),
|
||||||
_hover: {
|
_hover: {
|
||||||
bg: mode('base.300', 'base.650')(colorMode),
|
bg: mode('base.300', 'base.650')(colorMode),
|
||||||
color: mode('base.500', 'base.300')(colorMode),
|
color: mode('base.500', 'base.300')(colorMode),
|
||||||
|
@ -32,6 +32,10 @@ const selector = createSelector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'nodes' && nodes.shouldValidateGraph) {
|
if (activeTabName === 'nodes' && nodes.shouldValidateGraph) {
|
||||||
|
if (!nodes.nodes.length) {
|
||||||
|
reasons.push('No nodes in graph');
|
||||||
|
}
|
||||||
|
|
||||||
nodes.nodes.forEach((node) => {
|
nodes.nodes.forEach((node) => {
|
||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
return;
|
return;
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export const colorTokenToCssVar = (colorToken: string) =>
|
||||||
|
`var(--invokeai-colors-${colorToken.split('.').join('-')}`;
|
@ -1,6 +1,6 @@
|
|||||||
import { Box, chakra, Flex } from '@chakra-ui/react';
|
import { Box, chakra, Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { KonvaEventObject } from 'konva/lib/Node';
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import { Vector2d } from 'konva/lib/types';
|
import { Vector2d } from 'konva/lib/types';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||||
import { Layer, Stage } from 'react-konva';
|
import { Layer, Stage } from 'react-konva';
|
||||||
import useCanvasDragMove from '../hooks/useCanvasDragMove';
|
import useCanvasDragMove from '../hooks/useCanvasDragMove';
|
||||||
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
|
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
|
||||||
@ -18,6 +18,7 @@ import useCanvasMouseMove from '../hooks/useCanvasMouseMove';
|
|||||||
import useCanvasMouseOut from '../hooks/useCanvasMouseOut';
|
import useCanvasMouseOut from '../hooks/useCanvasMouseOut';
|
||||||
import useCanvasMouseUp from '../hooks/useCanvasMouseUp';
|
import useCanvasMouseUp from '../hooks/useCanvasMouseUp';
|
||||||
import useCanvasWheel from '../hooks/useCanvasZoom';
|
import useCanvasWheel from '../hooks/useCanvasZoom';
|
||||||
|
import { canvasResized } from '../store/canvasSlice';
|
||||||
import {
|
import {
|
||||||
setCanvasBaseLayer,
|
setCanvasBaseLayer,
|
||||||
setCanvasStage,
|
setCanvasStage,
|
||||||
@ -106,7 +107,8 @@ const IAICanvas = () => {
|
|||||||
shouldAntialias,
|
shouldAntialias,
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
useCanvasHotkeys();
|
useCanvasHotkeys();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
||||||
|
|
||||||
@ -137,8 +139,30 @@ const IAICanvas = () => {
|
|||||||
const { handleDragStart, handleDragMove, handleDragEnd } =
|
const { handleDragStart, handleDragMove, handleDragEnd } =
|
||||||
useCanvasDragMove();
|
useCanvasDragMove();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.contentBoxSize) {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
dispatch(canvasResized({ width, height }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
id="canvas-container"
|
||||||
|
ref={containerRef}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -146,13 +170,18 @@ const IAICanvas = () => {
|
|||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ position: 'relative' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
// top: 0,
|
||||||
|
// insetInlineStart: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChakraStage
|
<ChakraStage
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={canvasStageRefCallback}
|
ref={canvasStageRefCallback}
|
||||||
sx={{
|
sx={{
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
// boxShadow: '0px 0px 0px 1px var(--border-color-light)',
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
cursor: stageCursor ? stageCursor : undefined,
|
cursor: stageCursor ? stageCursor : undefined,
|
||||||
canvas: {
|
canvas: {
|
||||||
@ -213,9 +242,9 @@ const IAICanvas = () => {
|
|||||||
/>
|
/>
|
||||||
</Layer>
|
</Layer>
|
||||||
</ChakraStage>
|
</ChakraStage>
|
||||||
|
</Box>
|
||||||
<IAICanvasStatusText />
|
<IAICanvasStatusText />
|
||||||
<IAICanvasStagingAreaToolbar />
|
<IAICanvasStagingAreaToolbar />
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import { Flex, Spinner } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import {
|
|
||||||
canvasSelector,
|
|
||||||
initialCanvasImageSelector,
|
|
||||||
} from 'features/canvas/store/canvasSelectors';
|
|
||||||
import {
|
|
||||||
resizeAndScaleCanvas,
|
|
||||||
resizeCanvas,
|
|
||||||
setCanvasContainerDimensions,
|
|
||||||
setDoesCanvasNeedScaling,
|
|
||||||
} from 'features/canvas/store/canvasSlice';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { memo, useLayoutEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
const canvasResizerSelector = createSelector(
|
|
||||||
canvasSelector,
|
|
||||||
initialCanvasImageSelector,
|
|
||||||
activeTabNameSelector,
|
|
||||||
(canvas, initialCanvasImage, activeTabName) => {
|
|
||||||
const { doesCanvasNeedScaling, isCanvasInitialized } = canvas;
|
|
||||||
return {
|
|
||||||
doesCanvasNeedScaling,
|
|
||||||
activeTabName,
|
|
||||||
initialCanvasImage,
|
|
||||||
isCanvasInitialized,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const IAICanvasResizer = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const {
|
|
||||||
doesCanvasNeedScaling,
|
|
||||||
activeTabName,
|
|
||||||
initialCanvasImage,
|
|
||||||
isCanvasInitialized,
|
|
||||||
} = useAppSelector(canvasResizerSelector);
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (!ref.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { clientWidth, clientHeight } = ref.current;
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
setCanvasContainerDimensions({
|
|
||||||
width: clientWidth,
|
|
||||||
height: clientHeight,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isCanvasInitialized) {
|
|
||||||
dispatch(resizeAndScaleCanvas());
|
|
||||||
} else {
|
|
||||||
dispatch(resizeCanvas());
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setDoesCanvasNeedScaling(false));
|
|
||||||
}, 0);
|
|
||||||
}, [
|
|
||||||
dispatch,
|
|
||||||
initialCanvasImage,
|
|
||||||
doesCanvasNeedScaling,
|
|
||||||
activeTabName,
|
|
||||||
isCanvasInitialized,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
ref={ref}
|
|
||||||
sx={{
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 4,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spinner thickness="2px" size="xl" />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(IAICanvasResizer);
|
|
@ -140,11 +140,10 @@ const IAICanvasStagingAreaToolbar = () => {
|
|||||||
w="100%"
|
w="100%"
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
filter="drop-shadow(0 0.5rem 1rem rgba(0,0,0))"
|
|
||||||
onMouseOver={handleMouseOver}
|
onMouseOver={handleMouseOver}
|
||||||
onMouseOut={handleMouseOut}
|
onMouseOut={handleMouseOut}
|
||||||
>
|
>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached borderRadius="base" shadow="dark-lg">
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
||||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
resetCanvas,
|
resetCanvas,
|
||||||
resetCanvasView,
|
resetCanvasView,
|
||||||
resizeAndScaleCanvas,
|
|
||||||
setIsMaskEnabled,
|
setIsMaskEnabled,
|
||||||
setLayer,
|
setLayer,
|
||||||
setTool,
|
setTool,
|
||||||
@ -183,7 +182,6 @@ const IAICanvasToolbar = () => {
|
|||||||
|
|
||||||
const handleResetCanvas = () => {
|
const handleResetCanvas = () => {
|
||||||
dispatch(resetCanvas());
|
dispatch(resetCanvas());
|
||||||
dispatch(resizeAndScaleCanvas());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMergeVisible = () => {
|
const handleMergeVisible = () => {
|
||||||
|
@ -3,8 +3,4 @@ import { CanvasState } from './canvasTypes';
|
|||||||
/**
|
/**
|
||||||
* Canvas slice persist denylist
|
* Canvas slice persist denylist
|
||||||
*/
|
*/
|
||||||
export const canvasPersistDenylist: (keyof CanvasState)[] = [
|
export const canvasPersistDenylist: (keyof CanvasState)[] = ['cursorPosition'];
|
||||||
'cursorPosition',
|
|
||||||
'isCanvasInitialized',
|
|
||||||
'doesCanvasNeedScaling',
|
|
||||||
];
|
|
||||||
|
@ -5,10 +5,6 @@ import {
|
|||||||
roundToMultiple,
|
roundToMultiple,
|
||||||
} from 'common/util/roundDownToMultiple';
|
} from 'common/util/roundDownToMultiple';
|
||||||
import { setAspectRatio } from 'features/parameters/store/generationSlice';
|
import { setAspectRatio } from 'features/parameters/store/generationSlice';
|
||||||
import {
|
|
||||||
setActiveTab,
|
|
||||||
setShouldUseCanvasBetaLayout,
|
|
||||||
} from 'features/ui/store/uiSlice';
|
|
||||||
import { IRect, Vector2d } from 'konva/lib/types';
|
import { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp, cloneDeep } from 'lodash-es';
|
||||||
import { RgbaColor } from 'react-colorful';
|
import { RgbaColor } from 'react-colorful';
|
||||||
@ -50,12 +46,9 @@ export const initialCanvasState: CanvasState = {
|
|||||||
boundingBoxScaleMethod: 'none',
|
boundingBoxScaleMethod: 'none',
|
||||||
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||||
brushSize: 50,
|
brushSize: 50,
|
||||||
canvasContainerDimensions: { width: 0, height: 0 },
|
|
||||||
colorPickerColor: { r: 90, g: 90, b: 255, a: 1 },
|
colorPickerColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||||
cursorPosition: null,
|
cursorPosition: null,
|
||||||
doesCanvasNeedScaling: false,
|
|
||||||
futureLayerStates: [],
|
futureLayerStates: [],
|
||||||
isCanvasInitialized: false,
|
|
||||||
isDrawing: false,
|
isDrawing: false,
|
||||||
isMaskEnabled: true,
|
isMaskEnabled: true,
|
||||||
isMouseOverBoundingBox: false,
|
isMouseOverBoundingBox: false,
|
||||||
@ -208,7 +201,6 @@ export const canvasSlice = createSlice({
|
|||||||
};
|
};
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
|
|
||||||
state.isCanvasInitialized = false;
|
|
||||||
const newScale = calculateScale(
|
const newScale = calculateScale(
|
||||||
stageDimensions.width,
|
stageDimensions.width,
|
||||||
stageDimensions.height,
|
stageDimensions.height,
|
||||||
@ -228,7 +220,6 @@ export const canvasSlice = createSlice({
|
|||||||
);
|
);
|
||||||
state.stageScale = newScale;
|
state.stageScale = newScale;
|
||||||
state.stageCoordinates = newCoordinates;
|
state.stageCoordinates = newCoordinates;
|
||||||
state.doesCanvasNeedScaling = true;
|
|
||||||
},
|
},
|
||||||
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||||
const newDimensions = roundDimensionsTo64(action.payload);
|
const newDimensions = roundDimensionsTo64(action.payload);
|
||||||
@ -258,9 +249,6 @@ export const canvasSlice = createSlice({
|
|||||||
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
|
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
state.boundingBoxPreviewFill = action.payload;
|
state.boundingBoxPreviewFill = action.payload;
|
||||||
},
|
},
|
||||||
setDoesCanvasNeedScaling: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.doesCanvasNeedScaling = action.payload;
|
|
||||||
},
|
|
||||||
setStageScale: (state, action: PayloadAction<number>) => {
|
setStageScale: (state, action: PayloadAction<number>) => {
|
||||||
state.stageScale = action.payload;
|
state.stageScale = action.payload;
|
||||||
},
|
},
|
||||||
@ -493,97 +481,14 @@ export const canvasSlice = createSlice({
|
|||||||
state.layerState = initialLayerState;
|
state.layerState = initialLayerState;
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
},
|
},
|
||||||
setCanvasContainerDimensions: (
|
canvasResized: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<Dimensions>
|
action: PayloadAction<{ width: number; height: number }>
|
||||||
) => {
|
) => {
|
||||||
state.canvasContainerDimensions = action.payload;
|
const { width, height } = action.payload;
|
||||||
},
|
|
||||||
resizeAndScaleCanvas: (state) => {
|
|
||||||
const { width: containerWidth, height: containerHeight } =
|
|
||||||
state.canvasContainerDimensions;
|
|
||||||
|
|
||||||
const initialCanvasImage =
|
|
||||||
state.layerState.objects.find(isCanvasBaseImage);
|
|
||||||
|
|
||||||
const newStageDimensions = {
|
const newStageDimensions = {
|
||||||
width: Math.floor(containerWidth),
|
width: Math.floor(width),
|
||||||
height: Math.floor(containerHeight),
|
height: Math.floor(height),
|
||||||
};
|
|
||||||
|
|
||||||
if (!initialCanvasImage) {
|
|
||||||
const newScale = calculateScale(
|
|
||||||
newStageDimensions.width,
|
|
||||||
newStageDimensions.height,
|
|
||||||
512,
|
|
||||||
512,
|
|
||||||
STAGE_PADDING_PERCENTAGE
|
|
||||||
);
|
|
||||||
|
|
||||||
const newCoordinates = calculateCoordinates(
|
|
||||||
newStageDimensions.width,
|
|
||||||
newStageDimensions.height,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
512,
|
|
||||||
512,
|
|
||||||
newScale
|
|
||||||
);
|
|
||||||
|
|
||||||
const newBoundingBoxDimensions = { width: 512, height: 512 };
|
|
||||||
|
|
||||||
state.stageScale = newScale;
|
|
||||||
state.stageCoordinates = newCoordinates;
|
|
||||||
state.stageDimensions = newStageDimensions;
|
|
||||||
state.boundingBoxCoordinates = { x: 0, y: 0 };
|
|
||||||
state.boundingBoxDimensions = newBoundingBoxDimensions;
|
|
||||||
|
|
||||||
if (state.boundingBoxScaleMethod === 'auto') {
|
|
||||||
const scaledDimensions = getScaledBoundingBoxDimensions(
|
|
||||||
newBoundingBoxDimensions
|
|
||||||
);
|
|
||||||
state.scaledBoundingBoxDimensions = scaledDimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width: imageWidth, height: imageHeight } = initialCanvasImage;
|
|
||||||
|
|
||||||
const padding = 0.95;
|
|
||||||
|
|
||||||
const newScale = calculateScale(
|
|
||||||
containerWidth,
|
|
||||||
containerHeight,
|
|
||||||
imageWidth,
|
|
||||||
imageHeight,
|
|
||||||
padding
|
|
||||||
);
|
|
||||||
|
|
||||||
const newCoordinates = calculateCoordinates(
|
|
||||||
newStageDimensions.width,
|
|
||||||
newStageDimensions.height,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
imageWidth,
|
|
||||||
imageHeight,
|
|
||||||
newScale
|
|
||||||
);
|
|
||||||
|
|
||||||
state.minimumStageScale = newScale;
|
|
||||||
state.stageScale = newScale;
|
|
||||||
state.stageCoordinates = floorCoordinates(newCoordinates);
|
|
||||||
state.stageDimensions = newStageDimensions;
|
|
||||||
|
|
||||||
state.isCanvasInitialized = true;
|
|
||||||
},
|
|
||||||
resizeCanvas: (state) => {
|
|
||||||
const { width: containerWidth, height: containerHeight } =
|
|
||||||
state.canvasContainerDimensions;
|
|
||||||
|
|
||||||
const newStageDimensions = {
|
|
||||||
width: Math.floor(containerWidth),
|
|
||||||
height: Math.floor(containerHeight),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state.stageDimensions = newStageDimensions;
|
state.stageDimensions = newStageDimensions;
|
||||||
@ -876,14 +781,6 @@ export const canvasSlice = createSlice({
|
|||||||
state.layerState.stagingArea = initialLayerState.stagingArea;
|
state.layerState.stagingArea = initialLayerState.stagingArea;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(setShouldUseCanvasBetaLayout, (state) => {
|
|
||||||
state.doesCanvasNeedScaling = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addCase(setActiveTab, (state) => {
|
|
||||||
state.doesCanvasNeedScaling = true;
|
|
||||||
});
|
|
||||||
builder.addCase(setAspectRatio, (state, action) => {
|
builder.addCase(setAspectRatio, (state, action) => {
|
||||||
const ratio = action.payload;
|
const ratio = action.payload;
|
||||||
if (ratio) {
|
if (ratio) {
|
||||||
@ -915,8 +812,6 @@ export const {
|
|||||||
resetCanvas,
|
resetCanvas,
|
||||||
resetCanvasInteractionState,
|
resetCanvasInteractionState,
|
||||||
resetCanvasView,
|
resetCanvasView,
|
||||||
resizeAndScaleCanvas,
|
|
||||||
resizeCanvas,
|
|
||||||
setBoundingBoxCoordinates,
|
setBoundingBoxCoordinates,
|
||||||
setBoundingBoxDimensions,
|
setBoundingBoxDimensions,
|
||||||
setBoundingBoxPreviewFill,
|
setBoundingBoxPreviewFill,
|
||||||
@ -924,10 +819,8 @@ export const {
|
|||||||
flipBoundingBoxAxes,
|
flipBoundingBoxAxes,
|
||||||
setBrushColor,
|
setBrushColor,
|
||||||
setBrushSize,
|
setBrushSize,
|
||||||
setCanvasContainerDimensions,
|
|
||||||
setColorPickerColor,
|
setColorPickerColor,
|
||||||
setCursorPosition,
|
setCursorPosition,
|
||||||
setDoesCanvasNeedScaling,
|
|
||||||
setInitialCanvasImage,
|
setInitialCanvasImage,
|
||||||
setIsDrawing,
|
setIsDrawing,
|
||||||
setIsMaskEnabled,
|
setIsMaskEnabled,
|
||||||
@ -966,6 +859,7 @@ export const {
|
|||||||
stagingAreaInitialized,
|
stagingAreaInitialized,
|
||||||
canvasSessionIdChanged,
|
canvasSessionIdChanged,
|
||||||
setShouldAntialias,
|
setShouldAntialias,
|
||||||
|
canvasResized,
|
||||||
} = canvasSlice.actions;
|
} = canvasSlice.actions;
|
||||||
|
|
||||||
export default canvasSlice.reducer;
|
export default canvasSlice.reducer;
|
||||||
|
@ -126,12 +126,9 @@ export interface CanvasState {
|
|||||||
boundingBoxScaleMethod: BoundingBoxScale;
|
boundingBoxScaleMethod: BoundingBoxScale;
|
||||||
brushColor: RgbaColor;
|
brushColor: RgbaColor;
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
canvasContainerDimensions: Dimensions;
|
|
||||||
colorPickerColor: RgbaColor;
|
colorPickerColor: RgbaColor;
|
||||||
cursorPosition: Vector2d | null;
|
cursorPosition: Vector2d | null;
|
||||||
doesCanvasNeedScaling: boolean;
|
|
||||||
futureLayerStates: CanvasLayerState[];
|
futureLayerStates: CanvasLayerState[];
|
||||||
isCanvasInitialized: boolean;
|
|
||||||
isDrawing: boolean;
|
isDrawing: boolean;
|
||||||
isMaskEnabled: boolean;
|
isMaskEnabled: boolean;
|
||||||
isMouseOverBoundingBox: boolean;
|
isMouseOverBoundingBox: boolean;
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { AppDispatch, AppGetState } from 'app/store/store';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import { setDoesCanvasNeedScaling } from '../canvasSlice';
|
|
||||||
|
|
||||||
const debouncedCanvasScale = debounce((dispatch: AppDispatch) => {
|
|
||||||
dispatch(setDoesCanvasNeedScaling(true));
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
export const requestCanvasRescale =
|
|
||||||
() => (dispatch: AppDispatch, getState: AppGetState) => {
|
|
||||||
const activeTabName = activeTabNameSelector(getState());
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
debouncedCanvasScale(dispatch);
|
|
||||||
}
|
|
||||||
};
|
|
@ -80,12 +80,12 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
flexDir: 'column',
|
flexDir: 'column',
|
||||||
gap: 3,
|
gap: 3,
|
||||||
p: 3,
|
p: 2,
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
bg: 'base.200',
|
bg: 'base.250',
|
||||||
_dark: {
|
_dark: {
|
||||||
bg: 'base.850',
|
bg: 'base.750',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -194,7 +194,7 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ControlNetImagePreview controlNet={controlNet} height={28} />
|
<ControlNetImagePreview controlNet={controlNet} isSmall />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -207,7 +207,7 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<ControlNetImagePreview controlNet={controlNet} height="392px" />
|
<ControlNetImagePreview controlNet={controlNet} />
|
||||||
<ParamControlNetShouldAutoConfig controlNet={controlNet} />
|
<ParamControlNetShouldAutoConfig controlNet={controlNet} />
|
||||||
<ControlNetProcessorComponent controlNet={controlNet} />
|
<ControlNetProcessorComponent controlNet={controlNet} />
|
||||||
</>
|
</>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Box, Flex, Spinner, SystemStyleObject } from '@chakra-ui/react';
|
import { Box, Flex, Spinner } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import {
|
|
||||||
TypesafeDraggableData,
|
|
||||||
TypesafeDroppableData,
|
|
||||||
} from 'features/dnd/types';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import {
|
||||||
|
TypesafeDraggableData,
|
||||||
|
TypesafeDroppableData,
|
||||||
|
} from 'features/dnd/types';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FaUndo } from 'react-icons/fa';
|
import { FaUndo } from 'react-icons/fa';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlNet: ControlNetConfig;
|
controlNet: ControlNetConfig;
|
||||||
height: SystemStyleObject['h'];
|
isSmall?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
@ -36,15 +36,14 @@ const selector = createSelector(
|
|||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const ControlNetImagePreview = (props: Props) => {
|
const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
|
||||||
const { height } = props;
|
|
||||||
const {
|
const {
|
||||||
controlImage: controlImageName,
|
controlImage: controlImageName,
|
||||||
processedControlImage: processedControlImageName,
|
processedControlImage: processedControlImageName,
|
||||||
processorType,
|
processorType,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
controlNetId,
|
controlNetId,
|
||||||
} = props.controlNet;
|
} = controlNet;
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -109,7 +108,7 @@ const ControlNetImagePreview = (props: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: height,
|
h: isSmall ? 28 : 366, // magic no touch
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
pointerEvents: isEnabled ? 'auto' : 'none',
|
pointerEvents: isEnabled ? 'auto' : 'none',
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
|
||||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
|
||||||
import {
|
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import ImageGalleryContent from './ImageGalleryContent';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[activeTabNameSelector, uiSelector, gallerySelector, isStagingSelector],
|
|
||||||
(activeTabName, ui, gallery, isStaging) => {
|
|
||||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
|
||||||
const { galleryImageMinimumWidth } = gallery;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeTabName,
|
|
||||||
isStaging,
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGallery,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
isResizable: activeTabName !== 'unifiedCanvas',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const GalleryDrawer = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const {
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGallery,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
// activeTabName,
|
|
||||||
// isStaging,
|
|
||||||
// isResizable,
|
|
||||||
} = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleCloseGallery = () => {
|
|
||||||
dispatch(setShouldShowGallery(false));
|
|
||||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'esc',
|
|
||||||
() => {
|
|
||||||
dispatch(setShouldShowGallery(false));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: () => !shouldPinGallery,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[shouldPinGallery]
|
|
||||||
);
|
|
||||||
|
|
||||||
const IMAGE_SIZE_STEP = 32;
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+up',
|
|
||||||
() => {
|
|
||||||
if (galleryImageMinimumWidth < 256) {
|
|
||||||
const newMinWidth = clamp(
|
|
||||||
galleryImageMinimumWidth + IMAGE_SIZE_STEP,
|
|
||||||
32,
|
|
||||||
256
|
|
||||||
);
|
|
||||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[galleryImageMinimumWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+down',
|
|
||||||
() => {
|
|
||||||
if (galleryImageMinimumWidth > 32) {
|
|
||||||
const newMinWidth = clamp(
|
|
||||||
galleryImageMinimumWidth - IMAGE_SIZE_STEP,
|
|
||||||
32,
|
|
||||||
256
|
|
||||||
);
|
|
||||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[galleryImageMinimumWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldPinGallery) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizableDrawer
|
|
||||||
direction="right"
|
|
||||||
isResizable={true}
|
|
||||||
isOpen={shouldShowGallery}
|
|
||||||
onClose={handleCloseGallery}
|
|
||||||
minWidth={400}
|
|
||||||
>
|
|
||||||
<ImageGalleryContent />
|
|
||||||
</ResizableDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(GalleryDrawer);
|
|
@ -1,45 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
(state) => {
|
|
||||||
const { shouldPinGallery } = state.ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinGallery,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const GalleryPinButton = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { shouldPinGallery } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleSetShouldPinGallery = () => {
|
|
||||||
dispatch(togglePinGalleryPanel());
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<IAIIconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label={t('gallery.pinGallery')}
|
|
||||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
|
||||||
onClick={handleSetShouldPinGallery}
|
|
||||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(GalleryPinButton);
|
|
@ -2,10 +2,7 @@ import { MenuItem } from '@chakra-ui/react';
|
|||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import {
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
resizeAndScaleCanvas,
|
|
||||||
setInitialCanvasImage,
|
|
||||||
} from 'features/canvas/store/canvasSlice';
|
|
||||||
import {
|
import {
|
||||||
imagesToChangeSelected,
|
imagesToChangeSelected,
|
||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
@ -29,6 +26,7 @@ import {
|
|||||||
FaShare,
|
FaShare,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||||
import {
|
import {
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
useStarImagesMutation,
|
useStarImagesMutation,
|
||||||
@ -37,7 +35,6 @@ import {
|
|||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
@ -110,7 +107,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
dispatch(sentImageToCanvas());
|
dispatch(sentImageToCanvas());
|
||||||
dispatch(setInitialCanvasImage(imageDTO));
|
dispatch(setInitialCanvasImage(imageDTO));
|
||||||
dispatch(resizeAndScaleCanvas());
|
|
||||||
dispatch(setActiveTab('unifiedCanvas'));
|
dispatch(setActiveTab('unifiedCanvas'));
|
||||||
|
|
||||||
toaster({
|
toaster({
|
||||||
|
@ -18,7 +18,6 @@ import { FaImages, FaServer } from 'react-icons/fa';
|
|||||||
import { galleryViewChanged } from '../store/gallerySlice';
|
import { galleryViewChanged } from '../store/gallerySlice';
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GalleryPinButton from './GalleryPinButton';
|
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ const ImageGalleryContent = () => {
|
|||||||
onToggle={onToggleBoardList}
|
onToggle={onToggleBoardList}
|
||||||
/>
|
/>
|
||||||
<GallerySettingsPopover />
|
<GallerySettingsPopover />
|
||||||
<GalleryPinButton />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<BoardsList isOpen={isBoardListOpen} />
|
<BoardsList isOpen={isBoardListOpen} />
|
||||||
|
@ -8,10 +8,12 @@ type Props = {
|
|||||||
label: string;
|
label: string;
|
||||||
data: object | string;
|
data: object | string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
withDownload?: boolean;
|
||||||
|
withCopy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DataViewer = (props: Props) => {
|
const DataViewer = (props: Props) => {
|
||||||
const { label, data, fileName } = props;
|
const { label, data, fileName, withDownload = true, withCopy = true } = props;
|
||||||
const dataString = useMemo(
|
const dataString = useMemo(
|
||||||
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
|
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
|
||||||
[data]
|
[data]
|
||||||
@ -70,6 +72,7 @@ const DataViewer = (props: Props) => {
|
|||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
||||||
|
{withDownload && (
|
||||||
<Tooltip label={`Save ${label} JSON`}>
|
<Tooltip label={`Save ${label} JSON`}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`Save ${label} JSON`}
|
aria-label={`Save ${label} JSON`}
|
||||||
@ -79,6 +82,8 @@ const DataViewer = (props: Props) => {
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{withCopy && (
|
||||||
<Tooltip label={`Copy ${label} JSON`}>
|
<Tooltip label={`Copy ${label} JSON`}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`Copy ${label} JSON`}
|
aria-label={`Copy ${label} JSON`}
|
||||||
@ -88,6 +93,7 @@ const DataViewer = (props: Props) => {
|
|||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -1,38 +1,21 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
|
||||||
import { memo, useState } from 'react';
|
|
||||||
import { MdDeviceHub } from 'react-icons/md';
|
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
|
||||||
import 'reactflow/dist/style.css';
|
|
||||||
import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup';
|
|
||||||
import { Flow } from './flow/Flow';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { MdDeviceHub } from 'react-icons/md';
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||||
|
import { Flow } from './flow/Flow';
|
||||||
|
import TopLeftPanel from './flow/panels/TopLeftPanel/TopLeftPanel';
|
||||||
|
import TopCenterPanel from './flow/panels/TopCenterPanel/TopCenterPanel';
|
||||||
|
import TopRightPanel from './flow/panels/TopRightPanel/TopRightPanel';
|
||||||
|
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
||||||
|
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||||
|
|
||||||
const NodeEditor = () => {
|
const NodeEditor = () => {
|
||||||
const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
|
|
||||||
const isReady = useAppSelector((state) => state.nodes.isReady);
|
const isReady = useAppSelector((state) => state.nodes.isReady);
|
||||||
return (
|
return (
|
||||||
<PanelGroup
|
|
||||||
id="node-editor"
|
|
||||||
autoSaveId="node-editor"
|
|
||||||
direction="horizontal"
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
>
|
|
||||||
<Panel
|
|
||||||
id="node-editor-panel-group"
|
|
||||||
collapsible
|
|
||||||
onCollapse={setIsPanelCollapsed}
|
|
||||||
minSize={25}
|
|
||||||
>
|
|
||||||
<NodeEditorPanelGroup />
|
|
||||||
</Panel>
|
|
||||||
<ResizeHandle
|
|
||||||
collapsedDirection={isPanelCollapsed ? 'left' : undefined}
|
|
||||||
/>
|
|
||||||
<Panel id="node-editor-content">
|
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
sx={{
|
sx={{
|
||||||
@ -62,6 +45,11 @@ const NodeEditor = () => {
|
|||||||
>
|
>
|
||||||
<Flow />
|
<Flow />
|
||||||
<AddNodePopover />
|
<AddNodePopover />
|
||||||
|
<TopLeftPanel />
|
||||||
|
<TopCenterPanel />
|
||||||
|
<TopRightPanel />
|
||||||
|
<BottomLeftPanel />
|
||||||
|
<MinimapPanel />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@ -102,8 +90,6 @@ const NodeEditor = () => {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
|
||||||
</PanelGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ const AddNodePopover = () => {
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
useHotkeys(['space', '/'], handleHotkeyOpen);
|
useHotkeys(['shift+a', 'space'], handleHotkeyOpen);
|
||||||
useHotkeys(['escape'], handleHotkeyClose);
|
useHotkeys(['escape'], handleHotkeyClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useToken } from '@chakra-ui/react';
|
import { useToken } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -39,11 +42,6 @@ import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
|
|||||||
import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode';
|
import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode';
|
||||||
import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper';
|
import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper';
|
||||||
import NotesNode from './nodes/Notes/NotesNode';
|
import NotesNode from './nodes/Notes/NotesNode';
|
||||||
import BottomLeftPanel from './panels/BottomLeftPanel/BottomLeftPanel';
|
|
||||||
import MinimapPanel from './panels/MinimapPanel/MinimapPanel';
|
|
||||||
import TopCenterPanel from './panels/TopCenterPanel/TopCenterPanel';
|
|
||||||
import TopLeftPanel from './panels/TopLeftPanel/TopLeftPanel';
|
|
||||||
import TopRightPanel from './panels/TopRightPanel/TopRightPanel';
|
|
||||||
|
|
||||||
const DELETE_KEYS = ['Delete', 'Backspace'];
|
const DELETE_KEYS = ['Delete', 'Backspace'];
|
||||||
|
|
||||||
@ -61,14 +59,24 @@ const nodeTypes = {
|
|||||||
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
|
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
|
||||||
const proOptions: ProOptions = { hideAttribution: true };
|
const proOptions: ProOptions = { hideAttribution: true };
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const { shouldSnapToGrid, selectionMode } = nodes;
|
||||||
|
return {
|
||||||
|
shouldSnapToGrid,
|
||||||
|
selectionMode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
export const Flow = () => {
|
export const Flow = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const nodes = useAppSelector((state) => state.nodes.nodes);
|
const nodes = useAppSelector((state) => state.nodes.nodes);
|
||||||
const edges = useAppSelector((state) => state.nodes.edges);
|
const edges = useAppSelector((state) => state.nodes.edges);
|
||||||
const viewport = useAppSelector((state) => state.nodes.viewport);
|
const viewport = useAppSelector((state) => state.nodes.viewport);
|
||||||
const shouldSnapToGrid = useAppSelector(
|
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
|
||||||
(state) => state.nodes.shouldSnapToGrid
|
|
||||||
);
|
|
||||||
|
|
||||||
const isValidConnection = useIsValidConnection();
|
const isValidConnection = useIsValidConnection();
|
||||||
|
|
||||||
@ -181,12 +189,8 @@ export const Flow = () => {
|
|||||||
style={{ borderRadius }}
|
style={{ borderRadius }}
|
||||||
onPaneClick={handlePaneClick}
|
onPaneClick={handlePaneClick}
|
||||||
deleteKeyCode={DELETE_KEYS}
|
deleteKeyCode={DELETE_KEYS}
|
||||||
|
selectionMode={selectionMode}
|
||||||
>
|
>
|
||||||
<TopLeftPanel />
|
|
||||||
<TopCenterPanel />
|
|
||||||
<TopRightPanel />
|
|
||||||
<BottomLeftPanel />
|
|
||||||
<MinimapPanel />
|
|
||||||
<Background />
|
<Background />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow';
|
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||||
import { FIELDS, colorTokenToCssVar } from '../../../types/constants';
|
import { FIELDS } from 'features/nodes/types/constants';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow';
|
||||||
|
|
||||||
const selector = createSelector(stateSelector, ({ nodes }) => {
|
const selector = createSelector(stateSelector, ({ nodes }) => {
|
||||||
const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } =
|
const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } =
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { FIELDS, colorTokenToCssVar } from 'features/nodes/types/constants';
|
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||||
|
import { FIELDS } from 'features/nodes/types/constants';
|
||||||
import { isInvocationNode } from 'features/nodes/types/types';
|
import { isInvocationNode } from 'features/nodes/types/types';
|
||||||
|
|
||||||
export const makeEdgeSelector = (
|
export const makeEdgeSelector = (
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex, Grid, GridItem } from '@chakra-ui/react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import InvocationNodeFooter from './InvocationNodeFooter';
|
import InvocationNodeFooter from './InvocationNodeFooter';
|
||||||
import InvocationNodeHeader from './InvocationNodeHeader';
|
import InvocationNodeHeader from './InvocationNodeHeader';
|
||||||
import NodeWrapper from '../common/NodeWrapper';
|
import NodeWrapper from '../common/NodeWrapper';
|
||||||
import OutputField from './fields/OutputField';
|
import OutputField from './fields/OutputField';
|
||||||
import InputField from './fields/InputField';
|
import InputField from './fields/InputField';
|
||||||
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
|
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||||
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
||||||
|
import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
|
||||||
|
import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -17,8 +19,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||||
const inputFieldNames = useFieldNames(nodeId, 'input');
|
const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
|
||||||
const outputFieldNames = useFieldNames(nodeId, 'output');
|
const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
|
||||||
|
const outputFieldNames = useOutputFieldNames(nodeId);
|
||||||
const withFooter = useWithFooter(nodeId);
|
const withFooter = useWithFooter(nodeId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,14 +47,27 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}>
|
<Flex sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}>
|
||||||
{outputFieldNames.map((fieldName) => (
|
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
|
||||||
<OutputField
|
{inputConnectionFieldNames.map((fieldName, i) => (
|
||||||
key={`${nodeId}.${fieldName}.output-field`}
|
<GridItem
|
||||||
nodeId={nodeId}
|
gridColumnStart={1}
|
||||||
fieldName={fieldName}
|
gridRowStart={i + 1}
|
||||||
/>
|
key={`${nodeId}.${fieldName}.input-field`}
|
||||||
|
>
|
||||||
|
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||||
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
{inputFieldNames.map((fieldName) => (
|
{outputFieldNames.map((fieldName, i) => (
|
||||||
|
<GridItem
|
||||||
|
gridColumnStart={2}
|
||||||
|
gridRowStart={i + 1}
|
||||||
|
key={`${nodeId}.${fieldName}.output-field`}
|
||||||
|
>
|
||||||
|
<OutputField nodeId={nodeId} fieldName={fieldName} />
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
{inputAnyOrDirectFieldNames.map((fieldName) => (
|
||||||
<InputField
|
<InputField
|
||||||
key={`${nodeId}.${fieldName}.input-field`}
|
key={`${nodeId}.${fieldName}.input-field`}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { CSSProperties, memo, useMemo } from 'react';
|
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||||
import { Handle, HandleType, Position } from 'reactflow';
|
|
||||||
import {
|
import {
|
||||||
FIELDS,
|
FIELDS,
|
||||||
HANDLE_TOOLTIP_OPEN_DELAY,
|
HANDLE_TOOLTIP_OPEN_DELAY,
|
||||||
colorTokenToCssVar,
|
} from 'features/nodes/types/constants';
|
||||||
} from '../../../../../types/constants';
|
|
||||||
import {
|
import {
|
||||||
InputFieldTemplate,
|
InputFieldTemplate,
|
||||||
OutputFieldTemplate,
|
OutputFieldTemplate,
|
||||||
} from '../../../../../types/types';
|
} from 'features/nodes/types/types';
|
||||||
|
import { CSSProperties, memo, useMemo } from 'react';
|
||||||
|
import { Handle, HandleType, Position } from 'reactflow';
|
||||||
|
|
||||||
export const handleBaseStyles: CSSProperties = {
|
export const handleBaseStyles: CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -31,7 +31,7 @@ const FieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (newTitle: string) => {
|
async (newTitle: string) => {
|
||||||
if (newTitle === label || newTitle === fieldTemplateTitle) {
|
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field');
|
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field');
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
||||||
Flex,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Spacer,
|
|
||||||
Tooltip,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||||
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||||
@ -42,7 +36,6 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||||
<Spacer />
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={
|
label={
|
||||||
<FieldTooltipContent
|
<FieldTooltipContent
|
||||||
@ -90,6 +83,7 @@ const OutputFieldWrapper = memo(
|
|||||||
opacity: shouldDim ? 0.5 : 1,
|
opacity: shouldDim ? 0.5 : 1,
|
||||||
transitionProperty: 'opacity',
|
transitionProperty: 'opacity',
|
||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -4,13 +4,16 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useToken,
|
useToken,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
DRAG_HANDLE_CLASSNAME,
|
DRAG_HANDLE_CLASSNAME,
|
||||||
NODE_WIDTH,
|
NODE_WIDTH,
|
||||||
} from 'features/nodes/types/constants';
|
} from 'features/nodes/types/constants';
|
||||||
|
import { NodeStatus } from 'features/nodes/types/types';
|
||||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||||
import { PropsWithChildren, memo, useCallback } from 'react';
|
import { PropsWithChildren, memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
type NodeWrapperProps = PropsWithChildren & {
|
type NodeWrapperProps = PropsWithChildren & {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -19,25 +22,42 @@ type NodeWrapperProps = PropsWithChildren & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NodeWrapper = (props: NodeWrapperProps) => {
|
const NodeWrapper = (props: NodeWrapperProps) => {
|
||||||
const { width, children, selected } = props;
|
const { nodeId, width, children, selected } = props;
|
||||||
|
|
||||||
|
const selectIsInProgress = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) =>
|
||||||
|
nodes.nodeExecutionStates[nodeId]?.status === NodeStatus.IN_PROGRESS
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInProgress = useAppSelector(selectIsInProgress);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
nodeSelectedOutlineLight,
|
nodeSelectedLight,
|
||||||
nodeSelectedOutlineDark,
|
nodeSelectedDark,
|
||||||
|
nodeInProgressLight,
|
||||||
|
nodeInProgressDark,
|
||||||
shadowsXl,
|
shadowsXl,
|
||||||
shadowsBase,
|
shadowsBase,
|
||||||
] = useToken('shadows', [
|
] = useToken('shadows', [
|
||||||
'nodeSelectedOutline.light',
|
'nodeSelected.light',
|
||||||
'nodeSelectedOutline.dark',
|
'nodeSelected.dark',
|
||||||
|
'nodeInProgress.light',
|
||||||
|
'nodeInProgress.dark',
|
||||||
'shadows.xl',
|
'shadows.xl',
|
||||||
'shadows.base',
|
'shadows.base',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const shadow = useColorModeValue(
|
const selectedShadow = useColorModeValue(nodeSelectedLight, nodeSelectedDark);
|
||||||
nodeSelectedOutlineLight,
|
const inProgressShadow = useColorModeValue(
|
||||||
nodeSelectedOutlineDark
|
nodeInProgressLight,
|
||||||
|
nodeInProgressDark
|
||||||
);
|
);
|
||||||
|
|
||||||
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
|
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
|
||||||
@ -57,7 +77,11 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
w: width ?? NODE_WIDTH,
|
w: width ?? NODE_WIDTH,
|
||||||
transitionProperty: 'common',
|
transitionProperty: 'common',
|
||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
shadow: selected ? shadow : undefined,
|
shadow: selected
|
||||||
|
? isInProgress
|
||||||
|
? undefined
|
||||||
|
: selectedShadow
|
||||||
|
: undefined,
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
opacity,
|
opacity,
|
||||||
}}
|
}}
|
||||||
@ -75,6 +99,22 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
bottom: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
borderRadius: 'md',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
opacity: 0.7,
|
||||||
|
shadow: isInProgress ? inProgressShadow : undefined,
|
||||||
|
zIndex: -1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { Panel } from 'reactflow';
|
|
||||||
import ViewportControls from './ViewportControls';
|
|
||||||
import NodeOpacitySlider from './NodeOpacitySlider';
|
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import NodeOpacitySlider from './NodeOpacitySlider';
|
||||||
|
import ViewportControls from './ViewportControls';
|
||||||
|
|
||||||
const BottomLeftPanel = () => (
|
const BottomLeftPanel = () => (
|
||||||
<Panel position="bottom-left">
|
<Flex sx={{ gap: 2, position: 'absolute', bottom: 2, insetInlineStart: 2 }}>
|
||||||
<Flex sx={{ gap: 2 }}>
|
|
||||||
<ViewportControls />
|
<ViewportControls />
|
||||||
<NodeOpacitySlider />
|
<NodeOpacitySlider />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default memo(BottomLeftPanel);
|
export default memo(BottomLeftPanel);
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { ButtonGroup, Tooltip } from '@chakra-ui/react';
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
// shouldShowFieldTypeLegendChanged,
|
||||||
|
shouldShowMinimapPanelChanged,
|
||||||
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
FaExpand,
|
FaExpand,
|
||||||
// FaInfo,
|
// FaInfo,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
FaMinus,
|
|
||||||
FaPlus,
|
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { FaMagnifyingGlassMinus, FaMagnifyingGlassPlus } from 'react-icons/fa6';
|
||||||
import { useReactFlow } from 'reactflow';
|
import { useReactFlow } from 'reactflow';
|
||||||
import {
|
|
||||||
// shouldShowFieldTypeLegendChanged,
|
|
||||||
shouldShowMinimapPanelChanged,
|
|
||||||
} from 'features/nodes/store/nodesSlice';
|
|
||||||
|
|
||||||
const ViewportControls = () => {
|
const ViewportControls = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -49,27 +48,24 @@ const ViewportControls = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup isAttached orientation="vertical">
|
<ButtonGroup isAttached orientation="vertical">
|
||||||
<Tooltip label={t('nodes.zoomInNodes')}>
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Zoom in "
|
tooltip={t('nodes.zoomInNodes')}
|
||||||
|
aria-label={t('nodes.zoomInNodes')}
|
||||||
onClick={handleClickedZoomIn}
|
onClick={handleClickedZoomIn}
|
||||||
icon={<FaPlus />}
|
icon={<FaMagnifyingGlassPlus />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label={t('nodes.zoomOutNodes')}>
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Zoom out"
|
tooltip={t('nodes.zoomOutNodes')}
|
||||||
|
aria-label={t('nodes.zoomOutNodes')}
|
||||||
onClick={handleClickedZoomOut}
|
onClick={handleClickedZoomOut}
|
||||||
icon={<FaMinus />}
|
icon={<FaMagnifyingGlassMinus />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label={t('nodes.fitViewportNodes')}>
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Fit viewport"
|
tooltip={t('nodes.fitViewportNodes')}
|
||||||
|
aria-label={t('nodes.fitViewportNodes')}
|
||||||
onClick={handleClickedFitView}
|
onClick={handleClickedFitView}
|
||||||
icon={<FaExpand />}
|
icon={<FaExpand />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
{/* <Tooltip
|
{/* <Tooltip
|
||||||
label={
|
label={
|
||||||
shouldShowFieldTypeLegend
|
shouldShowFieldTypeLegend
|
||||||
@ -84,20 +80,21 @@ const ViewportControls = () => {
|
|||||||
icon={<FaInfo />}
|
icon={<FaInfo />}
|
||||||
/>
|
/>
|
||||||
</Tooltip> */}
|
</Tooltip> */}
|
||||||
<Tooltip
|
<IAIIconButton
|
||||||
label={
|
tooltip={
|
||||||
|
shouldShowMinimapPanel
|
||||||
|
? t('nodes.hideMinimapnodes')
|
||||||
|
: t('nodes.showMinimapnodes')
|
||||||
|
}
|
||||||
|
aria-label={
|
||||||
shouldShowMinimapPanel
|
shouldShowMinimapPanel
|
||||||
? t('nodes.hideMinimapnodes')
|
? t('nodes.hideMinimapnodes')
|
||||||
: t('nodes.showMinimapnodes')
|
: t('nodes.showMinimapnodes')
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<IAIIconButton
|
|
||||||
aria-label="Toggle minimap"
|
|
||||||
isChecked={shouldShowMinimapPanel}
|
isChecked={shouldShowMinimapPanel}
|
||||||
onClick={handleClickedToggleMiniMapPanel}
|
onClick={handleClickedToggleMiniMapPanel}
|
||||||
icon={<FaMapMarkerAlt />}
|
icon={<FaMapMarkerAlt />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
|
import { Flex, chakra, useColorModeValue } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useColorModeValue } from '@chakra-ui/react';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { MiniMap } from 'reactflow';
|
import { MiniMap } from 'reactflow';
|
||||||
|
|
||||||
const MinimapPanel = () => {
|
const ChakraMiniMap = chakra(MiniMap);
|
||||||
const miniMapStyle = useColorModeValue(
|
|
||||||
{
|
|
||||||
background: 'var(--invokeai-colors-base-200)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: 'var(--invokeai-colors-base-500)',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const MinimapPanel = () => {
|
||||||
const shouldShowMinimapPanel = useAppSelector(
|
const shouldShowMinimapPanel = useAppSelector(
|
||||||
(state: RootState) => state.nodes.shouldShowMinimapPanel
|
(state: RootState) => state.nodes.shouldShowMinimapPanel
|
||||||
);
|
);
|
||||||
@ -29,18 +22,28 @@ const MinimapPanel = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Flex sx={{ gap: 2, position: 'absolute', bottom: 2, insetInlineEnd: 2 }}>
|
||||||
{shouldShowMinimapPanel && (
|
{shouldShowMinimapPanel && (
|
||||||
<MiniMap
|
<ChakraMiniMap
|
||||||
pannable
|
pannable
|
||||||
zoomable
|
zoomable
|
||||||
nodeBorderRadius={15}
|
nodeBorderRadius={15}
|
||||||
style={miniMapStyle}
|
sx={{
|
||||||
|
m: '0 !important',
|
||||||
|
backgroundColor: 'base.200 !important',
|
||||||
|
borderRadius: 'base',
|
||||||
|
_dark: {
|
||||||
|
backgroundColor: 'base.500 !important',
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
},
|
||||||
|
}}
|
||||||
nodeColor={nodeColor}
|
nodeColor={nodeColor}
|
||||||
maskColor={maskColor}
|
maskColor={maskColor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { FileButton } from '@mantine/core';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
|
||||||
|
import { memo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaUpload } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const LoadWorkflowButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const resetRef = useRef<() => void>(null);
|
||||||
|
const loadWorkflowFromFile = useLoadWorkflowFromFile();
|
||||||
|
return (
|
||||||
|
<FileButton
|
||||||
|
resetRef={resetRef}
|
||||||
|
accept="application/json"
|
||||||
|
onChange={loadWorkflowFromFile}
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaUpload />}
|
||||||
|
tooltip={t('nodes.loadWorkflow')}
|
||||||
|
aria-label={t('nodes.loadWorkflow')}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(LoadWorkflowButton);
|
@ -1,81 +0,0 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import { userInvoked } from 'app/store/actions';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
|
|
||||||
import { IAIIconButtonProps } from 'common/components/IAIIconButton';
|
|
||||||
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
|
||||||
import { InvokeButtonTooltipContent } from 'features/parameters/components/ProcessButtons/InvokeButton';
|
|
||||||
import ProgressBar from 'features/system/components/ProgressBar';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaPlay } from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface InvokeButton
|
|
||||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {}
|
|
||||||
|
|
||||||
const NodeInvokeButton = (props: InvokeButton) => {
|
|
||||||
const { ...rest } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
const { isReady, isProcessing } = useIsReadyToInvoke();
|
|
||||||
const handleInvoke = useCallback(() => {
|
|
||||||
dispatch(userInvoked('nodes'));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
['ctrl+enter', 'meta+enter'],
|
|
||||||
handleInvoke,
|
|
||||||
{
|
|
||||||
enabled: () => isReady,
|
|
||||||
preventDefault: true,
|
|
||||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
|
||||||
},
|
|
||||||
[isReady, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box style={{ flexGrow: 4 }} position="relative">
|
|
||||||
<Box style={{ position: 'relative' }}>
|
|
||||||
{!isReady && (
|
|
||||||
<Box
|
|
||||||
borderRadius="base"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '0',
|
|
||||||
left: '0',
|
|
||||||
right: '0',
|
|
||||||
height: '100%',
|
|
||||||
overflow: 'clip',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProgressBar />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<IAIButton
|
|
||||||
tooltip={<InvokeButtonTooltipContent />}
|
|
||||||
aria-label={t('parameters.invoke')}
|
|
||||||
type="submit"
|
|
||||||
isDisabled={!isReady}
|
|
||||||
onClick={handleInvoke}
|
|
||||||
flexGrow={1}
|
|
||||||
w="100%"
|
|
||||||
colorScheme="accent"
|
|
||||||
id="invoke-button"
|
|
||||||
leftIcon={isProcessing ? undefined : <FaPlay />}
|
|
||||||
fontWeight={700}
|
|
||||||
isLoading={isProcessing}
|
|
||||||
loadingText={t('parameters.invoke')}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
Invoke
|
|
||||||
</IAIButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(NodeInvokeButton);
|
|
@ -1,11 +1,11 @@
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaSyncAlt } from 'react-icons/fa';
|
import { FaSyncAlt } from 'react-icons/fa';
|
||||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||||
|
|
||||||
const ReloadSchemaButton = () => {
|
const ReloadNodeTemplatesButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -14,13 +14,15 @@ const ReloadSchemaButton = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIButton
|
||||||
icon={<FaSyncAlt />}
|
leftIcon={<FaSyncAlt />}
|
||||||
tooltip={t('nodes.reloadSchema')}
|
tooltip={t('nodes.reloadNodeTemplates')}
|
||||||
aria-label={t('nodes.reloadSchema')}
|
aria-label={t('nodes.reloadNodeTemplates')}
|
||||||
onClick={handleReloadSchema}
|
onClick={handleReloadSchema}
|
||||||
/>
|
>
|
||||||
|
{t('nodes.reloadNodeTemplates')}
|
||||||
|
</IAIButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ReloadSchemaButton);
|
export default memo(ReloadNodeTemplatesButton);
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
Button,
|
Button,
|
||||||
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
@ -19,7 +20,7 @@ import { memo, useCallback, useRef } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaTrash } from 'react-icons/fa';
|
import { FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
const ClearGraphButton = () => {
|
const ResetWorkflowButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@ -48,10 +49,11 @@ const ClearGraphButton = () => {
|
|||||||
<>
|
<>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
tooltip={t('nodes.clearGraph')}
|
tooltip={t('nodes.resetWorkflow')}
|
||||||
aria-label={t('nodes.clearGraph')}
|
aria-label={t('nodes.resetWorkflow')}
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
isDisabled={!nodesCount}
|
isDisabled={!nodesCount}
|
||||||
|
colorScheme="error"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
@ -64,18 +66,21 @@ const ClearGraphButton = () => {
|
|||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
{t('nodes.clearGraph')}
|
{t('nodes.resetWorkflow')}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogBody>
|
<AlertDialogBody py={4}>
|
||||||
<Text>{t('nodes.clearGraphDesc')}</Text>
|
<Flex flexDir="column" gap={2}>
|
||||||
|
<Text>{t('nodes.resetWorkflowDesc')}</Text>
|
||||||
|
<Text variant="subtext">{t('nodes.resetWorkflowDesc2')}</Text>
|
||||||
|
</Flex>
|
||||||
</AlertDialogBody>
|
</AlertDialogBody>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button ref={cancelRef} onClick={onClose}>
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorScheme="red" ml={3} onClick={handleConfirmClear}>
|
<Button colorScheme="error" ml={3} onClick={handleConfirmClear}>
|
||||||
{t('common.accept')}
|
{t('common.accept')}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@ -85,4 +90,4 @@ const ClearGraphButton = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ClearGraphButton);
|
export default memo(ResetWorkflowButton);
|
@ -0,0 +1,29 @@
|
|||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaSave } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const SaveWorkflowButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const workflow = useWorkflow();
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `${workflow.name || 'My Workflow'}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}, [workflow]);
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaSave />}
|
||||||
|
tooltip={t('nodes.saveWorkflow')}
|
||||||
|
aria-label={t('nodes.saveWorkflow')}
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SaveWorkflowButton);
|
@ -1,23 +1,24 @@
|
|||||||
import { HStack } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Panel } from 'reactflow';
|
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||||
import NodeEditorSettings from './NodeEditorSettings';
|
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||||
import ClearGraphButton from './ClearGraphButton';
|
import SaveWorkflowButton from './SaveWorkflowButton';
|
||||||
import NodeInvokeButton from './NodeInvokeButton';
|
|
||||||
import ReloadSchemaButton from './ReloadSchemaButton';
|
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
return (
|
return (
|
||||||
<Panel position="top-center">
|
<Flex
|
||||||
<HStack>
|
sx={{
|
||||||
<NodeInvokeButton />
|
gap: 2,
|
||||||
<CancelButton />
|
position: 'absolute',
|
||||||
<ReloadSchemaButton />
|
top: 2,
|
||||||
<ClearGraphButton />
|
insetInlineStart: '50%',
|
||||||
<NodeEditorSettings />
|
transform: 'translate(-50%)',
|
||||||
</HStack>
|
}}
|
||||||
</Panel>
|
>
|
||||||
|
<SaveWorkflowButton />
|
||||||
|
<LoadWorkflowButton />
|
||||||
|
<ResetWorkflowButton />
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
||||||
|
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const WorkflowEditorControls = () => {
|
||||||
|
return (
|
||||||
|
<Flex layerStyle="first" sx={{ gap: 2, borderRadius: 'base', p: 2 }}>
|
||||||
|
<InvokeButton />
|
||||||
|
<CancelButton />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowEditorControls);
|
@ -1,9 +1,9 @@
|
|||||||
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { FaPlus } from 'react-icons/fa';
|
||||||
import { Panel } from 'reactflow';
|
|
||||||
|
|
||||||
const TopLeftPanel = () => {
|
const TopLeftPanel = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -12,16 +12,15 @@ const TopLeftPanel = () => {
|
|||||||
dispatch(addNodePopoverOpened());
|
dispatch(addNodePopoverOpened());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useHotkeys(['shift+a'], () => {
|
|
||||||
handleOpenAddNodePopover();
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel position="top-left">
|
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
|
||||||
<IAIButton aria-label="Add Node" onClick={handleOpenAddNodePopover}>
|
<IAIIconButton
|
||||||
Add Node
|
tooltip="Add Node (Shift+A, Space)"
|
||||||
</IAIButton>
|
aria-label="Add Node"
|
||||||
</Panel>
|
icon={<FaPlus />}
|
||||||
|
onClick={handleOpenAddNodePopover}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Panel } from 'reactflow';
|
|
||||||
import FieldTypeLegend from './FieldTypeLegend';
|
import FieldTypeLegend from './FieldTypeLegend';
|
||||||
|
import WorkflowEditorSettings from './WorkflowEditorSettings';
|
||||||
|
|
||||||
const TopRightPanel = () => {
|
const TopRightPanel = () => {
|
||||||
const shouldShowFieldTypeLegend = useAppSelector(
|
const shouldShowFieldTypeLegend = useAppSelector(
|
||||||
@ -9,9 +10,10 @@ const TopRightPanel = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel position="top-right">
|
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
|
||||||
|
<WorkflowEditorSettings />
|
||||||
{shouldShowFieldTypeLegend && <FieldTypeLegend />}
|
{shouldShowFieldTypeLegend && <FieldTypeLegend />}
|
||||||
</Panel>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
|
FormLabelProps,
|
||||||
Heading,
|
Heading,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@ -8,22 +9,30 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
|
forwardRef,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import IAISwitch from 'common/components/IAISwitch';
|
import IAISwitch from 'common/components/IAISwitch';
|
||||||
import { ChangeEvent, memo, useCallback } from 'react';
|
|
||||||
import { FaCog } from 'react-icons/fa';
|
|
||||||
import {
|
import {
|
||||||
|
selectionModeChanged,
|
||||||
shouldAnimateEdgesChanged,
|
shouldAnimateEdgesChanged,
|
||||||
shouldColorEdgesChanged,
|
shouldColorEdgesChanged,
|
||||||
shouldSnapToGridChanged,
|
shouldSnapToGridChanged,
|
||||||
shouldValidateGraphChanged,
|
shouldValidateGraphChanged,
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { ChangeEvent, memo, useCallback } from 'react';
|
||||||
|
import { FaCog } from 'react-icons/fa';
|
||||||
|
import { SelectionMode } from 'reactflow';
|
||||||
|
import ReloadNodeTemplatesButton from '../TopCenterPanel/ReloadSchemaButton';
|
||||||
|
|
||||||
|
const formLabelProps: FormLabelProps = {
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
stateSelector,
|
stateSelector,
|
||||||
@ -33,18 +42,20 @@ const selector = createSelector(
|
|||||||
shouldValidateGraph,
|
shouldValidateGraph,
|
||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
shouldColorEdges,
|
shouldColorEdges,
|
||||||
|
selectionMode,
|
||||||
} = nodes;
|
} = nodes;
|
||||||
return {
|
return {
|
||||||
shouldAnimateEdges,
|
shouldAnimateEdges,
|
||||||
shouldValidateGraph,
|
shouldValidateGraph,
|
||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
shouldColorEdges,
|
shouldColorEdges,
|
||||||
|
selectionModeIsChecked: selectionMode === SelectionMode.Full,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const NodeEditorSettings = () => {
|
const WorkflowEditorSettings = forwardRef((_, ref) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {
|
const {
|
||||||
@ -52,6 +63,7 @@ const NodeEditorSettings = () => {
|
|||||||
shouldValidateGraph,
|
shouldValidateGraph,
|
||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
shouldColorEdges,
|
shouldColorEdges,
|
||||||
|
selectionModeIsChecked,
|
||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
const handleChangeShouldValidate = useCallback(
|
const handleChangeShouldValidate = useCallback(
|
||||||
@ -82,10 +94,19 @@ const NodeEditorSettings = () => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeSelectionMode = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(selectionModeChanged(e.target.checked));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Node Editor Settings"
|
ref={ref}
|
||||||
|
aria-label="Workflow Editor Settings"
|
||||||
|
tooltip="Workflow Editor Settings"
|
||||||
icon={<FaCog />}
|
icon={<FaCog />}
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
/>
|
/>
|
||||||
@ -93,7 +114,7 @@ const NodeEditorSettings = () => {
|
|||||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Node Editor Settings</ModalHeader>
|
<ModalHeader>Workflow Editor Settings</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Flex
|
<Flex
|
||||||
@ -105,6 +126,7 @@ const NodeEditorSettings = () => {
|
|||||||
>
|
>
|
||||||
<Heading size="sm">General</Heading>
|
<Heading size="sm">General</Heading>
|
||||||
<IAISwitch
|
<IAISwitch
|
||||||
|
formLabelProps={formLabelProps}
|
||||||
onChange={handleChangeShouldAnimate}
|
onChange={handleChangeShouldAnimate}
|
||||||
isChecked={shouldAnimateEdges}
|
isChecked={shouldAnimateEdges}
|
||||||
label="Animated Edges"
|
label="Animated Edges"
|
||||||
@ -112,6 +134,7 @@ const NodeEditorSettings = () => {
|
|||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<IAISwitch
|
<IAISwitch
|
||||||
|
formLabelProps={formLabelProps}
|
||||||
isChecked={shouldSnapToGrid}
|
isChecked={shouldSnapToGrid}
|
||||||
onChange={handleChangeShouldSnap}
|
onChange={handleChangeShouldSnap}
|
||||||
label="Snap to Grid"
|
label="Snap to Grid"
|
||||||
@ -119,26 +142,36 @@ const NodeEditorSettings = () => {
|
|||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<IAISwitch
|
<IAISwitch
|
||||||
|
formLabelProps={formLabelProps}
|
||||||
isChecked={shouldColorEdges}
|
isChecked={shouldColorEdges}
|
||||||
onChange={handleChangeShouldColor}
|
onChange={handleChangeShouldColor}
|
||||||
label="Color-Code Edges"
|
label="Color-Code Edges"
|
||||||
helperText="Color-code edges according to their connected fields"
|
helperText="Color-code edges according to their connected fields"
|
||||||
/>
|
/>
|
||||||
|
<IAISwitch
|
||||||
|
formLabelProps={formLabelProps}
|
||||||
|
isChecked={selectionModeIsChecked}
|
||||||
|
onChange={handleChangeSelectionMode}
|
||||||
|
label="Fully Contain Nodes to Select"
|
||||||
|
helperText="Nodes must be fully inside the selection box to be selected"
|
||||||
|
/>
|
||||||
<Heading size="sm" pt={4}>
|
<Heading size="sm" pt={4}>
|
||||||
Advanced
|
Advanced
|
||||||
</Heading>
|
</Heading>
|
||||||
<IAISwitch
|
<IAISwitch
|
||||||
|
formLabelProps={formLabelProps}
|
||||||
isChecked={shouldValidateGraph}
|
isChecked={shouldValidateGraph}
|
||||||
onChange={handleChangeShouldValidate}
|
onChange={handleChangeShouldValidate}
|
||||||
label="Validate Connections and Graph"
|
label="Validate Connections and Graph"
|
||||||
helperText="Prevent invalid connections from being made, and invalid graphs from being invoked"
|
helperText="Prevent invalid connections from being made, and invalid graphs from being invoked"
|
||||||
/>
|
/>
|
||||||
|
<ReloadNodeTemplatesButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default memo(NodeEditorSettings);
|
export default memo(WorkflowEditorSettings);
|
@ -1,23 +1,41 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { memo, useState } from 'react';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { memo, useCallback, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ImperativePanelGroupHandle,
|
||||||
|
Panel,
|
||||||
|
PanelGroup,
|
||||||
|
} from 'react-resizable-panels';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
|
||||||
import InspectorPanel from './inspector/InspectorPanel';
|
import InspectorPanel from './inspector/InspectorPanel';
|
||||||
|
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||||
|
|
||||||
const NodeEditorPanelGroup = () => {
|
const NodeEditorPanelGroup = () => {
|
||||||
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
|
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
|
||||||
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
|
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
|
||||||
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
const handleDoubleClickHandle = useCallback(() => {
|
||||||
|
if (!panelGroupRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panelGroupRef.current.setLayout([50, 50]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Flex sx={{ flexDir: 'column', gap: 2, height: '100%', width: '100%' }}>
|
||||||
|
<ProcessButtons />
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
id="node-editor-panel_group"
|
ref={panelGroupRef}
|
||||||
autoSaveId="node-editor-panel_group"
|
id="workflow-panel-group"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
>
|
>
|
||||||
<Panel
|
<Panel
|
||||||
id="node-editor-panel_group_workflow"
|
id="workflow"
|
||||||
collapsible
|
collapsible
|
||||||
onCollapse={setIsTopPanelCollapsed}
|
onCollapse={setIsTopPanelCollapsed}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
@ -26,6 +44,7 @@ const NodeEditorPanelGroup = () => {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
|
onDoubleClick={handleDoubleClickHandle}
|
||||||
collapsedDirection={
|
collapsedDirection={
|
||||||
isTopPanelCollapsed
|
isTopPanelCollapsed
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -35,7 +54,7 @@ const NodeEditorPanelGroup = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
id="node-editor-panel_group_inspector"
|
id="inspector"
|
||||||
collapsible
|
collapsible
|
||||||
onCollapse={setIsBottomPanelCollapsed}
|
onCollapse={setIsBottomPanelCollapsed}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
@ -43,6 +62,7 @@ const NodeEditorPanelGroup = () => {
|
|||||||
<InspectorPanel />
|
<InspectorPanel />
|
||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const InspectorPanel = () => {
|
|||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
p: 4,
|
p: 2,
|
||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,26 +1,10 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
|
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
|
|
||||||
const useWatchWorkflow = () => {
|
|
||||||
const nodes = useAppSelector((state: RootState) => state.nodes);
|
|
||||||
const [debouncedNodes] = useDebounce(nodes, 300);
|
|
||||||
const workflow = useMemo(
|
|
||||||
() => buildWorkflow(debouncedNodes),
|
|
||||||
[debouncedNodes]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
workflow,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkflowJSONTab = () => {
|
const WorkflowJSONTab = () => {
|
||||||
const { workflow } = useWatchWorkflow();
|
const workflow = useWorkflow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -31,7 +15,7 @@ const WorkflowJSONTab = () => {
|
|||||||
h: 'full',
|
h: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DataViewer data={workflow} label="Workflow" fileName={workflow.name} />
|
<DataViewer data={workflow} label="Workflow" />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,8 +8,8 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import WorkflowGeneralTab from './WorkflowGeneralTab';
|
import WorkflowGeneralTab from './WorkflowGeneralTab';
|
||||||
import WorkflowLinearTab from './WorkflowLinearTab';
|
|
||||||
import WorkflowJSONTab from './WorkflowJSONTab';
|
import WorkflowJSONTab from './WorkflowJSONTab';
|
||||||
|
import WorkflowLinearTab from './WorkflowLinearTab';
|
||||||
|
|
||||||
const WorkflowPanel = () => {
|
const WorkflowPanel = () => {
|
||||||
return (
|
return (
|
||||||
@ -20,7 +20,8 @@ const WorkflowPanel = () => {
|
|||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
p: 4,
|
p: 2,
|
||||||
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/types';
|
||||||
|
|
||||||
|
export const useAnyOrDirectInputFieldNames = (nodeId: string) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const nodeTemplate = nodes.nodeTemplates[node.data.type];
|
||||||
|
if (!nodeTemplate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return map(nodeTemplate.inputs)
|
||||||
|
.filter((field) => ['any', 'direct'].includes(field.input))
|
||||||
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
|
.map((field) => field.name)
|
||||||
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldNames = useAppSelector(selector);
|
||||||
|
return fieldNames;
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/types';
|
||||||
|
|
||||||
|
export const useConnectionInputFieldNames = (nodeId: string) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const nodeTemplate = nodes.nodeTemplates[node.data.type];
|
||||||
|
if (!nodeTemplate) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return map(nodeTemplate.inputs)
|
||||||
|
.filter((field) => field.input === 'connection')
|
||||||
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
|
.map((field) => field.name)
|
||||||
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldNames = useAppSelector(selector);
|
||||||
|
return fieldNames;
|
||||||
|
};
|
@ -0,0 +1,103 @@
|
|||||||
|
import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||||
|
import { useLogger } from 'app/logging/useLogger';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { parseify } from 'common/util/serialize';
|
||||||
|
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { zWorkflow } from 'features/nodes/types/types';
|
||||||
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
|
import { makeToast } from 'features/system/util/makeToast';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
import { fromZodError, fromZodIssue } from 'zod-validation-error';
|
||||||
|
|
||||||
|
export const useLoadWorkflowFromFile = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const logger = useLogger('nodes');
|
||||||
|
const loadWorkflowFromFile = useCallback(
|
||||||
|
(file: File | null) => {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async () => {
|
||||||
|
const rawJSON = reader.result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedJSON = JSON.parse(String(rawJSON));
|
||||||
|
const result = zWorkflow.safeParse(parsedJSON);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const message = fromZodError(result.error, {
|
||||||
|
prefix: 'Workflow Validation Error',
|
||||||
|
}).toString();
|
||||||
|
logger.error({ error: parseify(result.error) }, message);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addToast(
|
||||||
|
makeToast({
|
||||||
|
title: 'Unable to Validate Workflow',
|
||||||
|
description: (
|
||||||
|
<WorkflowValidationErrorContent error={result.error} />
|
||||||
|
),
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(workflowLoaded(result.data));
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addToast(
|
||||||
|
makeToast({
|
||||||
|
title: 'Workflow Loaded',
|
||||||
|
status: 'success',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
reader.abort();
|
||||||
|
} catch (error) {
|
||||||
|
// file reader error
|
||||||
|
if (error) {
|
||||||
|
dispatch(
|
||||||
|
addToast(
|
||||||
|
makeToast({
|
||||||
|
title: 'Unable to Load Workflow',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
},
|
||||||
|
[dispatch, logger]
|
||||||
|
);
|
||||||
|
|
||||||
|
return loadWorkflowFromFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowValidationErrorContent = memo((props: { error: ZodError }) => {
|
||||||
|
if (props.error.issues[0]) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{fromZodIssue(props.error.issues[0], { prefix: null }).toString()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<UnorderedList>
|
||||||
|
{props.error.issues.map((issue, i) => (
|
||||||
|
<ListItem key={i}>
|
||||||
|
<Text>{fromZodIssue(issue, { prefix: null }).toString()}</Text>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</UnorderedList>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
WorkflowValidationErrorContent.displayName = 'WorkflowValidationErrorContent';
|
@ -4,10 +4,9 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { KIND_MAP } from '../types/constants';
|
|
||||||
import { isInvocationNode } from '../types/types';
|
import { isInvocationNode } from '../types/types';
|
||||||
|
|
||||||
export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => {
|
export const useOutputFieldNames = (nodeId: string) => {
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(
|
createSelector(
|
||||||
@ -17,13 +16,18 @@ export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => {
|
|||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return map(node.data[KIND_MAP[kind]], (field) => field.name).filter(
|
const nodeTemplate = nodes.nodeTemplates[node.data.type];
|
||||||
(fieldName) => fieldName !== 'is_intermediate'
|
if (!nodeTemplate) {
|
||||||
);
|
return [];
|
||||||
|
}
|
||||||
|
return map(nodeTemplate.outputs)
|
||||||
|
.sort((a, b) => (a.ui_order ?? 0) - (b.ui_order ?? 0))
|
||||||
|
.map((field) => field.name)
|
||||||
|
.filter((fieldName) => fieldName !== 'is_intermediate');
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
),
|
),
|
||||||
[kind, nodeId]
|
[nodeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldNames = useAppSelector(selector);
|
const fieldNames = useAppSelector(selector);
|
@ -0,0 +1,16 @@
|
|||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
export const useWorkflow = () => {
|
||||||
|
const nodes = useAppSelector((state: RootState) => state.nodes);
|
||||||
|
const [debouncedNodes] = useDebounce(nodes, 300);
|
||||||
|
const workflow = useMemo(
|
||||||
|
() => buildWorkflow(debouncedNodes),
|
||||||
|
[debouncedNodes]
|
||||||
|
);
|
||||||
|
|
||||||
|
return workflow;
|
||||||
|
};
|
@ -4,7 +4,6 @@ import { NodesState } from './types';
|
|||||||
* Nodes slice persist denylist
|
* Nodes slice persist denylist
|
||||||
*/
|
*/
|
||||||
export const nodesPersistDenylist: (keyof NodesState)[] = [
|
export const nodesPersistDenylist: (keyof NodesState)[] = [
|
||||||
'schema',
|
|
||||||
'nodeTemplates',
|
'nodeTemplates',
|
||||||
'connectionStartParams',
|
'connectionStartParams',
|
||||||
'currentConnectionFieldType',
|
'currentConnectionFieldType',
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
Node,
|
Node,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
OnConnectStartParams,
|
OnConnectStartParams,
|
||||||
|
SelectionMode,
|
||||||
Viewport,
|
Viewport,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||||
@ -56,6 +57,8 @@ import {
|
|||||||
import { NodesState } from './types';
|
import { NodesState } from './types';
|
||||||
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
|
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
|
||||||
|
|
||||||
|
export const WORKFLOW_FORMAT_VERSION = '1.0.0';
|
||||||
|
|
||||||
const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
|
const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
|
||||||
status: NodeStatus.PENDING,
|
status: NodeStatus.PENDING,
|
||||||
error: null,
|
error: null,
|
||||||
@ -64,10 +67,23 @@ const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
|
|||||||
outputs: [],
|
outputs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialWorkflow = {
|
||||||
|
meta: {
|
||||||
|
version: WORKFLOW_FORMAT_VERSION,
|
||||||
|
},
|
||||||
|
name: '',
|
||||||
|
author: '',
|
||||||
|
description: '',
|
||||||
|
notes: '',
|
||||||
|
tags: '',
|
||||||
|
contact: '',
|
||||||
|
version: '',
|
||||||
|
exposedFields: [],
|
||||||
|
};
|
||||||
|
|
||||||
export const initialNodesState: NodesState = {
|
export const initialNodesState: NodesState = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
schema: null,
|
|
||||||
nodeTemplates: {},
|
nodeTemplates: {},
|
||||||
isReady: false,
|
isReady: false,
|
||||||
connectionStartParams: null,
|
connectionStartParams: null,
|
||||||
@ -82,21 +98,13 @@ export const initialNodesState: NodesState = {
|
|||||||
nodeOpacity: 1,
|
nodeOpacity: 1,
|
||||||
selectedNodes: [],
|
selectedNodes: [],
|
||||||
selectedEdges: [],
|
selectedEdges: [],
|
||||||
workflow: {
|
workflow: initialWorkflow,
|
||||||
name: '',
|
|
||||||
author: '',
|
|
||||||
description: '',
|
|
||||||
notes: '',
|
|
||||||
tags: '',
|
|
||||||
contact: '',
|
|
||||||
version: '',
|
|
||||||
exposedFields: [],
|
|
||||||
},
|
|
||||||
nodeExecutionStates: {},
|
nodeExecutionStates: {},
|
||||||
viewport: { x: 0, y: 0, zoom: 1 },
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
mouseOverField: null,
|
mouseOverField: null,
|
||||||
nodesToCopy: [],
|
nodesToCopy: [],
|
||||||
edgesToCopy: [],
|
edgesToCopy: [],
|
||||||
|
selectionMode: SelectionMode.Partial,
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
||||||
@ -570,15 +578,6 @@ const nodesSlice = createSlice({
|
|||||||
nodeOpacityChanged: (state, action: PayloadAction<number>) => {
|
nodeOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.nodeOpacity = action.payload;
|
state.nodeOpacity = action.payload;
|
||||||
},
|
},
|
||||||
loadFileNodes: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<Node<InvocationNodeData>[]>
|
|
||||||
) => {
|
|
||||||
state.nodes = action.payload;
|
|
||||||
},
|
|
||||||
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
|
|
||||||
state.edges = action.payload;
|
|
||||||
},
|
|
||||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.workflow.name = action.payload;
|
state.workflow.name = action.payload;
|
||||||
},
|
},
|
||||||
@ -601,9 +600,9 @@ const nodesSlice = createSlice({
|
|||||||
state.workflow.contact = action.payload;
|
state.workflow.contact = action.payload;
|
||||||
},
|
},
|
||||||
workflowLoaded: (state, action: PayloadAction<Workflow>) => {
|
workflowLoaded: (state, action: PayloadAction<Workflow>) => {
|
||||||
// TODO: validation
|
|
||||||
const { nodes, edges, ...workflow } = action.payload;
|
const { nodes, edges, ...workflow } = action.payload;
|
||||||
state.workflow = workflow;
|
state.workflow = workflow;
|
||||||
|
|
||||||
state.nodes = applyNodeChanges(
|
state.nodes = applyNodeChanges(
|
||||||
nodes.map((node) => ({
|
nodes.map((node) => ({
|
||||||
item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` },
|
item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` },
|
||||||
@ -615,6 +614,19 @@ const nodesSlice = createSlice({
|
|||||||
edges.map((edge) => ({ item: edge, type: 'add' })),
|
edges.map((edge) => ({ item: edge, type: 'add' })),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
state.nodeExecutionStates = nodes.reduce<
|
||||||
|
Record<string, NodeExecutionState>
|
||||||
|
>((acc, node) => {
|
||||||
|
acc[node.id] = {
|
||||||
|
nodeId: node.id,
|
||||||
|
...initialNodeExecutionState,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
workflowReset: (state) => {
|
||||||
|
state.workflow = cloneDeep(initialWorkflow);
|
||||||
},
|
},
|
||||||
viewportChanged: (state, action: PayloadAction<Viewport>) => {
|
viewportChanged: (state, action: PayloadAction<Viewport>) => {
|
||||||
state.viewport = action.payload;
|
state.viewport = action.payload;
|
||||||
@ -721,14 +733,16 @@ const nodesSlice = createSlice({
|
|||||||
addNodePopoverToggled: (state) => {
|
addNodePopoverToggled: (state) => {
|
||||||
state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen;
|
state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen;
|
||||||
},
|
},
|
||||||
|
selectionModeChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.selectionMode = action.payload
|
||||||
|
? SelectionMode.Full
|
||||||
|
: SelectionMode.Partial;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||||
state.isReady = false;
|
state.isReady = false;
|
||||||
});
|
});
|
||||||
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
|
|
||||||
state.schema = action.payload;
|
|
||||||
});
|
|
||||||
builder.addCase(appSocketInvocationStarted, (state, action) => {
|
builder.addCase(appSocketInvocationStarted, (state, action) => {
|
||||||
const { source_node_id } = action.payload.data;
|
const { source_node_id } = action.payload.data;
|
||||||
const node = state.nodeExecutionStates[source_node_id];
|
const node = state.nodeExecutionStates[source_node_id];
|
||||||
@ -792,8 +806,6 @@ export const {
|
|||||||
nodeTemplatesBuilt,
|
nodeTemplatesBuilt,
|
||||||
nodeEditorReset,
|
nodeEditorReset,
|
||||||
imageCollectionFieldValueChanged,
|
imageCollectionFieldValueChanged,
|
||||||
loadFileNodes,
|
|
||||||
loadFileEdges,
|
|
||||||
fieldStringValueChanged,
|
fieldStringValueChanged,
|
||||||
fieldNumberValueChanged,
|
fieldNumberValueChanged,
|
||||||
fieldBooleanValueChanged,
|
fieldBooleanValueChanged,
|
||||||
@ -837,6 +849,7 @@ export const {
|
|||||||
addNodePopoverOpened,
|
addNodePopoverOpened,
|
||||||
addNodePopoverClosed,
|
addNodePopoverClosed,
|
||||||
addNodePopoverToggled,
|
addNodePopoverToggled,
|
||||||
|
selectionModeChanged,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { OpenAPIV3 } from 'openapi-types';
|
import {
|
||||||
import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow';
|
Edge,
|
||||||
|
Node,
|
||||||
|
OnConnectStartParams,
|
||||||
|
SelectionMode,
|
||||||
|
Viewport,
|
||||||
|
} from 'reactflow';
|
||||||
import {
|
import {
|
||||||
FieldIdentifier,
|
FieldIdentifier,
|
||||||
FieldType,
|
FieldType,
|
||||||
@ -13,7 +18,6 @@ import {
|
|||||||
export type NodesState = {
|
export type NodesState = {
|
||||||
nodes: Node<NodeData>[];
|
nodes: Node<NodeData>[];
|
||||||
edges: Edge<InvocationEdgeExtra>[];
|
edges: Edge<InvocationEdgeExtra>[];
|
||||||
schema: OpenAPIV3.Document | null;
|
|
||||||
nodeTemplates: Record<string, InvocationTemplate>;
|
nodeTemplates: Record<string, InvocationTemplate>;
|
||||||
connectionStartParams: OnConnectStartParams | null;
|
connectionStartParams: OnConnectStartParams | null;
|
||||||
currentConnectionFieldType: FieldType | null;
|
currentConnectionFieldType: FieldType | null;
|
||||||
@ -34,4 +38,5 @@ export type NodesState = {
|
|||||||
nodesToCopy: Node<NodeData>[];
|
nodesToCopy: Node<NodeData>[];
|
||||||
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
||||||
isAddNodePopoverOpen: boolean;
|
isAddNodePopoverOpen: boolean;
|
||||||
|
selectionMode: SelectionMode;
|
||||||
};
|
};
|
||||||
|
@ -23,9 +23,6 @@ export const COLLECTION_TYPES: FieldType[] = [
|
|||||||
'ImageCollection',
|
'ImageCollection',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const colorTokenToCssVar = (colorToken: string) =>
|
|
||||||
`var(--invokeai-colors-${colorToken.split('.').join('-')}`;
|
|
||||||
|
|
||||||
export const FIELDS: Record<FieldType, FieldUIConfig> = {
|
export const FIELDS: Record<FieldType, FieldUIConfig> = {
|
||||||
integer: {
|
integer: {
|
||||||
title: 'Integer',
|
title: 'Integer',
|
||||||
|
@ -1,23 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ControlNetModelParam,
|
|
||||||
LoRAModelParam,
|
|
||||||
MainModelParam,
|
|
||||||
OnnxModelParam,
|
|
||||||
SchedulerParam,
|
SchedulerParam,
|
||||||
VaeModelParam,
|
zBaseModel,
|
||||||
|
zMainOrOnnxModel,
|
||||||
|
zScheduler,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
import { RgbaColor } from 'react-colorful';
|
import { RgbaColor } from 'react-colorful';
|
||||||
import { Edge, Node } from 'reactflow';
|
import { Node } from 'reactflow';
|
||||||
import { components } from 'services/api/schema';
|
import { Graph, ImageDTO, _InputField, _OutputField } from 'services/api/types';
|
||||||
import {
|
|
||||||
Graph,
|
|
||||||
GraphExecutionState,
|
|
||||||
ImageDTO,
|
|
||||||
ImageField,
|
|
||||||
_InputField,
|
|
||||||
_OutputField,
|
|
||||||
} from 'services/api/types';
|
|
||||||
import {
|
import {
|
||||||
AnyInvocationType,
|
AnyInvocationType,
|
||||||
AnyResult,
|
AnyResult,
|
||||||
@ -53,6 +43,10 @@ export type InvocationTemplate = {
|
|||||||
* Array of the invocation outputs
|
* Array of the invocation outputs
|
||||||
*/
|
*/
|
||||||
outputs: Record<string, OutputFieldTemplate>;
|
outputs: Record<string, OutputFieldTemplate>;
|
||||||
|
/**
|
||||||
|
* The type of this node's output
|
||||||
|
*/
|
||||||
|
outputType: string; // TODO: generate a union of output types
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldUIConfig = {
|
export type FieldUIConfig = {
|
||||||
@ -114,40 +108,6 @@ export type FieldType = z.infer<typeof zFieldType>;
|
|||||||
export const isFieldType = (value: unknown): value is FieldType =>
|
export const isFieldType = (value: unknown): value is FieldType =>
|
||||||
zFieldType.safeParse(value).success;
|
zFieldType.safeParse(value).success;
|
||||||
|
|
||||||
/**
|
|
||||||
* An input field is persisted across reloads as part of the user's local state.
|
|
||||||
*
|
|
||||||
* An input field has three properties:
|
|
||||||
* - `id` a unique identifier
|
|
||||||
* - `name` the name of the field, which comes from the python dataclass
|
|
||||||
* - `value` the field's value
|
|
||||||
*/
|
|
||||||
export type InputFieldValue =
|
|
||||||
| IntegerInputFieldValue
|
|
||||||
| SeedInputFieldValue
|
|
||||||
| FloatInputFieldValue
|
|
||||||
| StringInputFieldValue
|
|
||||||
| BooleanInputFieldValue
|
|
||||||
| ImageInputFieldValue
|
|
||||||
| LatentsInputFieldValue
|
|
||||||
| ConditioningInputFieldValue
|
|
||||||
| UNetInputFieldValue
|
|
||||||
| ClipInputFieldValue
|
|
||||||
| VaeInputFieldValue
|
|
||||||
| ControlInputFieldValue
|
|
||||||
| EnumInputFieldValue
|
|
||||||
| MainModelInputFieldValue
|
|
||||||
| SDXLMainModelInputFieldValue
|
|
||||||
| SDXLRefinerModelInputFieldValue
|
|
||||||
| VaeModelInputFieldValue
|
|
||||||
| LoRAModelInputFieldValue
|
|
||||||
| ControlNetModelInputFieldValue
|
|
||||||
| CollectionInputFieldValue
|
|
||||||
| CollectionItemInputFieldValue
|
|
||||||
| ColorInputFieldValue
|
|
||||||
| ImageCollectionInputFieldValue
|
|
||||||
| SchedulerInputFieldValue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An input field template is generated on each page load from the OpenAPI schema.
|
* An input field template is generated on each page load from the OpenAPI schema.
|
||||||
*
|
*
|
||||||
@ -179,6 +139,19 @@ export type InputFieldTemplate =
|
|||||||
| ImageCollectionInputFieldTemplate
|
| ImageCollectionInputFieldTemplate
|
||||||
| SchedulerInputFieldTemplate;
|
| SchedulerInputFieldTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the kind of input(s) this field may have.
|
||||||
|
*/
|
||||||
|
export const zInputKind = z.enum(['connection', 'direct', 'any']);
|
||||||
|
export type InputKind = z.infer<typeof zInputKind>;
|
||||||
|
|
||||||
|
export const zFieldValueBase = z.object({
|
||||||
|
id: z.string().trim().min(1),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
type: zFieldType,
|
||||||
|
});
|
||||||
|
export type FieldValueBase = z.infer<typeof zFieldValueBase>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An output field is persisted across as part of the user's local state.
|
* An output field is persisted across as part of the user's local state.
|
||||||
*
|
*
|
||||||
@ -186,7 +159,11 @@ export type InputFieldTemplate =
|
|||||||
* - `id` a unique identifier
|
* - `id` a unique identifier
|
||||||
* - `name` the name of the field, which comes from the python dataclass
|
* - `name` the name of the field, which comes from the python dataclass
|
||||||
*/
|
*/
|
||||||
export type OutputFieldValue = FieldValueBase & { fieldKind: 'output' };
|
|
||||||
|
export const zOutputFieldValue = zFieldValueBase.extend({
|
||||||
|
fieldKind: z.literal('output'),
|
||||||
|
});
|
||||||
|
export type OutputFieldValue = z.infer<typeof zOutputFieldValue>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An output field template is generated on each page load from the OpenAPI schema.
|
* An output field template is generated on each page load from the OpenAPI schema.
|
||||||
@ -199,143 +176,309 @@ export type OutputFieldTemplate = {
|
|||||||
type: FieldType;
|
type: FieldType;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
} & _OutputField;
|
||||||
|
|
||||||
/**
|
export const zInputFieldValueBase = zFieldValueBase.extend({
|
||||||
* Indicates the kind of input(s) this field may have.
|
fieldKind: z.literal('input'),
|
||||||
*/
|
label: z.string(),
|
||||||
export type InputKind = 'connection' | 'direct' | 'any';
|
});
|
||||||
|
export type InputFieldValueBase = z.infer<typeof zInputFieldValueBase>;
|
||||||
|
|
||||||
export type FieldValueBase = {
|
export const zModelIdentifier = z.object({
|
||||||
id: string;
|
model_name: z.string().trim().min(1),
|
||||||
name: string;
|
base_model: zBaseModel,
|
||||||
type: FieldType;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export type InputFieldValueBase = FieldValueBase & {
|
export const zImageField = z.object({
|
||||||
fieldKind: 'input';
|
image_name: z.string().trim().min(1),
|
||||||
label: string;
|
});
|
||||||
};
|
export type ImageField = z.infer<typeof zImageField>;
|
||||||
|
|
||||||
export type IntegerInputFieldValue = InputFieldValueBase & {
|
export const zLatentsField = z.object({
|
||||||
type: 'integer';
|
latents_name: z.string().trim().min(1),
|
||||||
value?: number;
|
seed: z.number().int().optional(),
|
||||||
};
|
});
|
||||||
|
export type LatentsField = z.infer<typeof zLatentsField>;
|
||||||
|
|
||||||
export type FloatInputFieldValue = InputFieldValueBase & {
|
export const zConditioningField = z.object({
|
||||||
type: 'float';
|
conditioning_name: z.string().trim().min(1),
|
||||||
value?: number;
|
});
|
||||||
};
|
export type ConditioningField = z.infer<typeof zConditioningField>;
|
||||||
|
|
||||||
export type SeedInputFieldValue = InputFieldValueBase & {
|
export const zIntegerInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'Seed';
|
type: z.literal('integer'),
|
||||||
value?: number;
|
value: z.number().optional(),
|
||||||
};
|
});
|
||||||
|
export type IntegerInputFieldValue = z.infer<typeof zIntegerInputFieldValue>;
|
||||||
|
|
||||||
export type StringInputFieldValue = InputFieldValueBase & {
|
export const zFloatInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'string';
|
type: z.literal('float'),
|
||||||
value?: string;
|
value: z.number().optional(),
|
||||||
};
|
});
|
||||||
|
export type FloatInputFieldValue = z.infer<typeof zFloatInputFieldValue>;
|
||||||
|
|
||||||
export type BooleanInputFieldValue = InputFieldValueBase & {
|
export const zStringInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'boolean';
|
type: z.literal('string'),
|
||||||
value?: boolean;
|
value: z.string().optional(),
|
||||||
};
|
});
|
||||||
|
export type StringInputFieldValue = z.infer<typeof zStringInputFieldValue>;
|
||||||
|
|
||||||
export type EnumInputFieldValue = InputFieldValueBase & {
|
export const zBooleanInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'enum';
|
type: z.literal('boolean'),
|
||||||
value?: number | string;
|
value: z.boolean().optional(),
|
||||||
};
|
});
|
||||||
|
export type BooleanInputFieldValue = z.infer<typeof zBooleanInputFieldValue>;
|
||||||
|
|
||||||
export type LatentsInputFieldValue = InputFieldValueBase & {
|
export const zEnumInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'LatentsField';
|
type: z.literal('enum'),
|
||||||
value?: undefined;
|
value: z.union([z.string(), z.number()]).optional(),
|
||||||
};
|
});
|
||||||
|
export type EnumInputFieldValue = z.infer<typeof zEnumInputFieldValue>;
|
||||||
|
|
||||||
export type ConditioningInputFieldValue = InputFieldValueBase & {
|
export const zLatentsInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'ConditioningField';
|
type: z.literal('LatentsField'),
|
||||||
value?: string;
|
value: zLatentsField.optional(),
|
||||||
};
|
});
|
||||||
|
export type LatentsInputFieldValue = z.infer<typeof zLatentsInputFieldValue>;
|
||||||
|
|
||||||
export type ControlInputFieldValue = InputFieldValueBase & {
|
export const zConditioningInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'ControlField';
|
type: z.literal('ConditioningField'),
|
||||||
value?: undefined;
|
value: zConditioningField.optional(),
|
||||||
};
|
});
|
||||||
|
export type ConditioningInputFieldValue = z.infer<
|
||||||
|
typeof zConditioningInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
export type UNetInputFieldValue = InputFieldValueBase & {
|
export const zControlNetModel = zModelIdentifier;
|
||||||
type: 'UNetField';
|
export type ControlNetModel = z.infer<typeof zControlNetModel>;
|
||||||
value?: undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClipInputFieldValue = InputFieldValueBase & {
|
export const zControlField = zInputFieldValueBase.extend({
|
||||||
type: 'ClipField';
|
image: zImageField,
|
||||||
value?: undefined;
|
control_model: zControlNetModel,
|
||||||
};
|
control_weight: z.union([z.number(), z.array(z.number())]).optional(),
|
||||||
|
begin_step_percent: z.number().optional(),
|
||||||
|
end_step_percent: z.number().optional(),
|
||||||
|
control_mode: z
|
||||||
|
.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced'])
|
||||||
|
.optional(),
|
||||||
|
resize_mode: z
|
||||||
|
.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple'])
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
export type ControlField = z.infer<typeof zControlField>;
|
||||||
|
|
||||||
export type VaeInputFieldValue = InputFieldValueBase & {
|
export const zControlInputFieldTemplate = zInputFieldValueBase.extend({
|
||||||
type: 'VaeField';
|
type: z.literal('ControlField'),
|
||||||
value?: undefined;
|
value: zControlField.optional(),
|
||||||
};
|
});
|
||||||
|
export type ControlInputFieldValue = z.infer<typeof zControlInputFieldTemplate>;
|
||||||
|
|
||||||
export type ImageInputFieldValue = InputFieldValueBase & {
|
export const zModelType = z.enum([
|
||||||
type: 'ImageField';
|
'onnx',
|
||||||
value?: ImageField;
|
'main',
|
||||||
};
|
'vae',
|
||||||
|
'lora',
|
||||||
|
'controlnet',
|
||||||
|
'embedding',
|
||||||
|
]);
|
||||||
|
export type ModelType = z.infer<typeof zModelType>;
|
||||||
|
|
||||||
export type ImageCollectionInputFieldValue = InputFieldValueBase & {
|
export const zSubModelType = z.enum([
|
||||||
type: 'ImageCollection';
|
'unet',
|
||||||
value?: ImageField[];
|
'text_encoder',
|
||||||
};
|
'text_encoder_2',
|
||||||
|
'tokenizer',
|
||||||
|
'tokenizer_2',
|
||||||
|
'vae',
|
||||||
|
'vae_decoder',
|
||||||
|
'vae_encoder',
|
||||||
|
'scheduler',
|
||||||
|
'safety_checker',
|
||||||
|
]);
|
||||||
|
export type SubModelType = z.infer<typeof zSubModelType>;
|
||||||
|
|
||||||
export type MainModelInputFieldValue = InputFieldValueBase & {
|
export const zModelInfo = zModelIdentifier.extend({
|
||||||
type: 'MainModelField';
|
model_type: zModelType,
|
||||||
value?: MainModelParam | OnnxModelParam;
|
submodel: zSubModelType.optional(),
|
||||||
};
|
});
|
||||||
|
export type ModelInfo = z.infer<typeof zModelInfo>;
|
||||||
|
|
||||||
export type SDXLMainModelInputFieldValue = InputFieldValueBase & {
|
export const zLoraInfo = zModelInfo.extend({
|
||||||
type: 'SDXLMainModelField';
|
weight: z.number().optional(),
|
||||||
value?: MainModelParam | OnnxModelParam;
|
});
|
||||||
};
|
export type LoraInfo = z.infer<typeof zLoraInfo>;
|
||||||
|
|
||||||
export type SDXLRefinerModelInputFieldValue = InputFieldValueBase & {
|
export const zUNetField = z.object({
|
||||||
type: 'SDXLRefinerModelField';
|
unet: zModelInfo,
|
||||||
value?: MainModelParam | OnnxModelParam;
|
scheduler: zModelInfo,
|
||||||
};
|
loras: z.array(zLoraInfo),
|
||||||
|
});
|
||||||
|
export type UNetField = z.infer<typeof zUNetField>;
|
||||||
|
|
||||||
export type VaeModelInputFieldValue = InputFieldValueBase & {
|
export const zUNetInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'VaeModelField';
|
type: z.literal('UNetField'),
|
||||||
value?: VaeModelParam;
|
value: zUNetField.optional(),
|
||||||
};
|
});
|
||||||
|
export type UNetInputFieldValue = z.infer<typeof zUNetInputFieldValue>;
|
||||||
|
|
||||||
export type LoRAModelInputFieldValue = InputFieldValueBase & {
|
export const zClipField = z.object({
|
||||||
type: 'LoRAModelField';
|
tokenizer: zModelInfo,
|
||||||
value?: LoRAModelParam;
|
text_encoder: zModelInfo,
|
||||||
};
|
skipped_layers: z.number(),
|
||||||
|
loras: z.array(zLoraInfo),
|
||||||
|
});
|
||||||
|
export type ClipField = z.infer<typeof zClipField>;
|
||||||
|
|
||||||
export type ControlNetModelInputFieldValue = InputFieldValueBase & {
|
export const zClipInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'ControlNetModelField';
|
type: z.literal('ClipField'),
|
||||||
value?: ControlNetModelParam;
|
value: zClipField.optional(),
|
||||||
};
|
});
|
||||||
|
export type ClipInputFieldValue = z.infer<typeof zClipInputFieldValue>;
|
||||||
|
|
||||||
export type CollectionInputFieldValue = InputFieldValueBase & {
|
export const zVaeField = z.object({
|
||||||
type: 'Collection';
|
vae: zModelInfo,
|
||||||
value?: (string | number)[];
|
});
|
||||||
};
|
export type VaeField = z.infer<typeof zVaeField>;
|
||||||
|
|
||||||
export type CollectionItemInputFieldValue = InputFieldValueBase & {
|
export const zVaeInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'CollectionItem';
|
type: z.literal('VaeField'),
|
||||||
value?: undefined;
|
value: zVaeField.optional(),
|
||||||
};
|
});
|
||||||
|
export type VaeInputFieldValue = z.infer<typeof zVaeInputFieldValue>;
|
||||||
|
|
||||||
export type ColorInputFieldValue = InputFieldValueBase & {
|
export const zImageInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'ColorField';
|
type: z.literal('ImageField'),
|
||||||
value?: RgbaColor;
|
value: zImageField.optional(),
|
||||||
};
|
});
|
||||||
|
export type ImageInputFieldValue = z.infer<typeof zImageInputFieldValue>;
|
||||||
|
|
||||||
export type SchedulerInputFieldValue = InputFieldValueBase & {
|
export const zImageCollectionInputFieldValue = zInputFieldValueBase.extend({
|
||||||
type: 'Scheduler';
|
type: z.literal('ImageCollection'),
|
||||||
value?: SchedulerParam;
|
value: z.array(zImageField).optional(),
|
||||||
};
|
});
|
||||||
|
export type ImageCollectionInputFieldValue = z.infer<
|
||||||
|
typeof zImageCollectionInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zMainModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('MainModelField'),
|
||||||
|
value: zMainOrOnnxModel.optional(),
|
||||||
|
});
|
||||||
|
export type MainModelInputFieldValue = z.infer<
|
||||||
|
typeof zMainModelInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zSDXLMainModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('SDXLMainModelField'),
|
||||||
|
value: zMainOrOnnxModel.optional(),
|
||||||
|
});
|
||||||
|
export type SDXLMainModelInputFieldValue = z.infer<
|
||||||
|
typeof zSDXLMainModelInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zSDXLRefinerModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('SDXLRefinerModelField'),
|
||||||
|
value: zMainOrOnnxModel.optional(), // TODO: should narrow this down to a refiner model
|
||||||
|
});
|
||||||
|
export type SDXLRefinerModelInputFieldValue = z.infer<
|
||||||
|
typeof zSDXLRefinerModelInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zVaeModelField = zModelIdentifier;
|
||||||
|
|
||||||
|
export const zVaeModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('VaeModelField'),
|
||||||
|
value: zVaeModelField.optional(),
|
||||||
|
});
|
||||||
|
export type VaeModelInputFieldValue = z.infer<typeof zVaeModelInputFieldValue>;
|
||||||
|
|
||||||
|
export const zLoRAModelField = zModelIdentifier;
|
||||||
|
export type LoRAModelField = z.infer<typeof zLoRAModelField>;
|
||||||
|
|
||||||
|
export const zLoRAModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('LoRAModelField'),
|
||||||
|
value: zLoRAModelField.optional(),
|
||||||
|
});
|
||||||
|
export type LoRAModelInputFieldValue = z.infer<
|
||||||
|
typeof zLoRAModelInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zControlNetModelField = zModelIdentifier;
|
||||||
|
export type ControlNetModelField = z.infer<typeof zControlNetModelField>;
|
||||||
|
|
||||||
|
export const zControlNetModelInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('ControlNetModelField'),
|
||||||
|
value: zControlNetModelField.optional(),
|
||||||
|
});
|
||||||
|
export type ControlNetModelInputFieldValue = z.infer<
|
||||||
|
typeof zControlNetModelInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zCollectionInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('Collection'),
|
||||||
|
value: z.array(z.any()).optional(), // TODO: should this field ever have a value?
|
||||||
|
});
|
||||||
|
export type CollectionInputFieldValue = z.infer<
|
||||||
|
typeof zCollectionInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zCollectionItemInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('CollectionItem'),
|
||||||
|
value: z.any().optional(), // TODO: should this field ever have a value?
|
||||||
|
});
|
||||||
|
export type CollectionItemInputFieldValue = z.infer<
|
||||||
|
typeof zCollectionItemInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zColorField = z.object({
|
||||||
|
r: z.number().int().min(0).max(255),
|
||||||
|
g: z.number().int().min(0).max(255),
|
||||||
|
b: z.number().int().min(0).max(255),
|
||||||
|
a: z.number().int().min(0).max(255),
|
||||||
|
});
|
||||||
|
export type ColorField = z.infer<typeof zColorField>;
|
||||||
|
|
||||||
|
export const zColorInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('ColorField'),
|
||||||
|
value: zColorField.optional(),
|
||||||
|
});
|
||||||
|
export type ColorInputFieldValue = z.infer<typeof zColorInputFieldValue>;
|
||||||
|
|
||||||
|
export const zSchedulerInputFieldValue = zInputFieldValueBase.extend({
|
||||||
|
type: z.literal('Scheduler'),
|
||||||
|
value: zScheduler.optional(),
|
||||||
|
});
|
||||||
|
export type SchedulerInputFieldValue = z.infer<
|
||||||
|
typeof zSchedulerInputFieldValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const zInputFieldValue = z.discriminatedUnion('type', [
|
||||||
|
zIntegerInputFieldValue,
|
||||||
|
zFloatInputFieldValue,
|
||||||
|
zStringInputFieldValue,
|
||||||
|
zBooleanInputFieldValue,
|
||||||
|
zImageInputFieldValue,
|
||||||
|
zLatentsInputFieldValue,
|
||||||
|
zConditioningInputFieldValue,
|
||||||
|
zUNetInputFieldValue,
|
||||||
|
zClipInputFieldValue,
|
||||||
|
zVaeInputFieldValue,
|
||||||
|
zControlInputFieldTemplate,
|
||||||
|
zEnumInputFieldValue,
|
||||||
|
zMainModelInputFieldValue,
|
||||||
|
zSDXLMainModelInputFieldValue,
|
||||||
|
zSDXLRefinerModelInputFieldValue,
|
||||||
|
zVaeModelInputFieldValue,
|
||||||
|
zLoRAModelInputFieldValue,
|
||||||
|
zControlNetModelInputFieldValue,
|
||||||
|
zCollectionInputFieldValue,
|
||||||
|
zCollectionItemInputFieldValue,
|
||||||
|
zColorInputFieldValue,
|
||||||
|
zImageCollectionInputFieldValue,
|
||||||
|
zSchedulerInputFieldValue,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type InputFieldValue = z.infer<typeof zInputFieldValue>;
|
||||||
|
|
||||||
export type InputFieldTemplateBase = {
|
export type InputFieldTemplateBase = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -521,14 +664,15 @@ export type InvocationBaseSchemaObject = Omit<
|
|||||||
export type InvocationOutputSchemaObject = Omit<
|
export type InvocationOutputSchemaObject = Omit<
|
||||||
OpenAPIV3.SchemaObject,
|
OpenAPIV3.SchemaObject,
|
||||||
'properties'
|
'properties'
|
||||||
> &
|
> & {
|
||||||
OpenAPIV3.SchemaObject['properties'] & {
|
properties: OpenAPIV3.SchemaObject['properties'] & {
|
||||||
type: Omit<OpenAPIV3.SchemaObject, 'default'> & {
|
type: Omit<OpenAPIV3.SchemaObject, 'default'> & {
|
||||||
default: AnyInvocationType;
|
default: string;
|
||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
class: 'output';
|
class: 'output';
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type InvocationFieldSchema = OpenAPIV3.SchemaObject & _InputField;
|
export type InvocationFieldSchema = OpenAPIV3.SchemaObject & _InputField;
|
||||||
|
|
||||||
@ -571,24 +715,26 @@ export const isInvocationFieldSchema = (
|
|||||||
|
|
||||||
export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
|
export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
|
||||||
|
|
||||||
export const zInputFieldValue = z.object({
|
|
||||||
id: z.string().trim().min(1),
|
|
||||||
name: z.string().trim().min(1),
|
|
||||||
type: zFieldType,
|
|
||||||
label: z.string(),
|
|
||||||
isExposed: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zInvocationNodeData = z.object({
|
export const zInvocationNodeData = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
|
// no easy way to build this dynamically, and we don't want to anyways, because this will be used
|
||||||
|
// to validate incoming workflows, and we want to allow community nodes.
|
||||||
type: z.string().trim().min(1),
|
type: z.string().trim().min(1),
|
||||||
inputs: z.record(z.any()),
|
inputs: z.record(zInputFieldValue),
|
||||||
outputs: z.record(z.any()),
|
outputs: z.record(zOutputFieldValue),
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
isOpen: z.boolean(),
|
isOpen: z.boolean(),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Massage this to get better type safety while developing
|
||||||
|
export type InvocationNodeData = Omit<
|
||||||
|
z.infer<typeof zInvocationNodeData>,
|
||||||
|
'type'
|
||||||
|
> & {
|
||||||
|
type: AnyInvocationType;
|
||||||
|
};
|
||||||
|
|
||||||
export const zNotesNodeData = z.object({
|
export const zNotesNodeData = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
type: z.literal('notes'),
|
type: z.literal('notes'),
|
||||||
@ -597,75 +743,83 @@ export const zNotesNodeData = z.object({
|
|||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const zWorkflow = z.object({
|
export type NotesNodeData = z.infer<typeof zNotesNodeData>;
|
||||||
name: z.string().trim().min(1),
|
|
||||||
author: z.string(),
|
export const zWorkflowInvocationNode = z.object({
|
||||||
description: z.string(),
|
|
||||||
version: z.string(),
|
|
||||||
contact: z.string(),
|
|
||||||
tags: z.string(),
|
|
||||||
notes: z.string(),
|
|
||||||
nodes: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
type: z.string().trim().min(1),
|
type: z.literal('invocation'),
|
||||||
data: z.union([zInvocationNodeData, zNotesNodeData]),
|
data: zInvocationNodeData,
|
||||||
width: z.number().gt(0),
|
width: z.number().gt(0),
|
||||||
height: z.number().gt(0),
|
height: z.number().gt(0),
|
||||||
position: z.object({
|
position: z.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
y: z.number(),
|
y: z.number(),
|
||||||
}),
|
}),
|
||||||
})
|
});
|
||||||
),
|
|
||||||
edges: z.array(
|
export const zWorkflowNotesNode = z.object({
|
||||||
z.object({
|
id: z.string().trim().min(1),
|
||||||
|
type: z.literal('notes'),
|
||||||
|
data: zNotesNodeData,
|
||||||
|
width: z.number().gt(0),
|
||||||
|
height: z.number().gt(0),
|
||||||
|
position: z.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zWorkflowNode = z.discriminatedUnion('type', [
|
||||||
|
zWorkflowInvocationNode,
|
||||||
|
zWorkflowNotesNode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type WorkflowNode = z.infer<typeof zWorkflowNode>;
|
||||||
|
|
||||||
|
export const zWorkflowEdge = z.object({
|
||||||
source: z.string().trim().min(1),
|
source: z.string().trim().min(1),
|
||||||
sourceHandle: z.string().trim().min(1),
|
sourceHandle: z.string().trim().min(1),
|
||||||
target: z.string().trim().min(1),
|
target: z.string().trim().min(1),
|
||||||
targetHandle: z.string().trim().min(1),
|
targetHandle: z.string().trim().min(1),
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
type: z.string().trim().min(1),
|
type: z.enum(['default', 'collapsed']),
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Workflow = {
|
export const zFieldIdentifier = z.object({
|
||||||
name: string;
|
nodeId: z.string().trim().min(1),
|
||||||
author: string;
|
fieldName: z.string().trim().min(1),
|
||||||
description: string;
|
});
|
||||||
version: string;
|
|
||||||
contact: string;
|
|
||||||
tags: string;
|
|
||||||
notes: string;
|
|
||||||
nodes: Pick<
|
|
||||||
Node<InvocationNodeData | NotesNodeData>,
|
|
||||||
'id' | 'type' | 'data' | 'width' | 'height' | 'position'
|
|
||||||
>[];
|
|
||||||
edges: Pick<
|
|
||||||
Edge<InvocationEdgeExtra>,
|
|
||||||
'source' | 'sourceHandle' | 'target' | 'targetHandle' | 'id' | 'type'
|
|
||||||
>[];
|
|
||||||
exposedFields: FieldIdentifier[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InvocationNodeData = {
|
export type FieldIdentifier = z.infer<typeof zFieldIdentifier>;
|
||||||
id: string;
|
|
||||||
type: AnyInvocationType;
|
|
||||||
inputs: Record<string, InputFieldValue>;
|
|
||||||
outputs: Record<string, OutputFieldValue>;
|
|
||||||
label: string;
|
|
||||||
isOpen: boolean;
|
|
||||||
notes: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NotesNodeData = {
|
export const zSemVer = z.string().refine((val) => {
|
||||||
id: string;
|
const [major, minor, patch] = val.split('.');
|
||||||
type: 'notes';
|
return (
|
||||||
label: string;
|
major !== undefined &&
|
||||||
notes: string;
|
minor !== undefined &&
|
||||||
isOpen: boolean;
|
patch !== undefined &&
|
||||||
};
|
Number.isInteger(Number(major)) &&
|
||||||
|
Number.isInteger(Number(minor)) &&
|
||||||
|
Number.isInteger(Number(patch))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SemVer = z.infer<typeof zSemVer>;
|
||||||
|
|
||||||
|
export const zWorkflow = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
contact: z.string(),
|
||||||
|
tags: z.string(),
|
||||||
|
notes: z.string(),
|
||||||
|
nodes: z.array(zWorkflowNode),
|
||||||
|
edges: z.array(zWorkflowEdge),
|
||||||
|
exposedFields: z.array(zFieldIdentifier),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Workflow = z.infer<typeof zWorkflow>;
|
||||||
|
|
||||||
export type CurrentImageNodeData = {
|
export type CurrentImageNodeData = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -705,25 +859,6 @@ export enum NodeStatus {
|
|||||||
FAILED,
|
FAILED,
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedOutput =
|
|
||||||
| components['schemas']['StringOutput']
|
|
||||||
| components['schemas']['IntegerOutput']
|
|
||||||
| components['schemas']['FloatOutput']
|
|
||||||
| components['schemas']['ImageOutput'];
|
|
||||||
|
|
||||||
export const isSavedOutput = (
|
|
||||||
output: GraphExecutionState['results'][string]
|
|
||||||
): output is SavedOutput =>
|
|
||||||
Boolean(
|
|
||||||
output &&
|
|
||||||
[
|
|
||||||
'string_output',
|
|
||||||
'integer_output',
|
|
||||||
'float_output',
|
|
||||||
'image_output',
|
|
||||||
].includes(output?.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type NodeExecutionState = {
|
export type NodeExecutionState = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
status: NodeStatus;
|
status: NodeStatus;
|
||||||
@ -733,14 +868,9 @@ export type NodeExecutionState = {
|
|||||||
outputs: AnyResult[];
|
outputs: AnyResult[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldIdentifier = {
|
|
||||||
nodeId: string;
|
|
||||||
fieldName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FieldComponentProps<
|
export type FieldComponentProps<
|
||||||
V extends InputFieldValue,
|
V extends InputFieldValue,
|
||||||
T extends InputFieldTemplate
|
T extends InputFieldTemplate,
|
||||||
> = {
|
> = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
field: V;
|
field: V;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { pick } from 'lodash-es';
|
|
||||||
import { NodesState } from '../store/types';
|
import { NodesState } from '../store/types';
|
||||||
import { Workflow, isInvocationNode, isNotesNode } from '../types/types';
|
import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types';
|
||||||
|
|
||||||
export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
||||||
const { workflow: workflowMeta, nodes, edges } = nodesState;
|
const { workflow: workflowMeta, nodes, edges } = nodesState;
|
||||||
@ -13,25 +12,19 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
if (!isInvocationNode(node) && !isNotesNode(node)) {
|
const result = zWorkflowNode.safeParse(node);
|
||||||
|
if (!result.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workflow.nodes.push(
|
workflow.nodes.push(result.data);
|
||||||
pick(node, ['id', 'type', 'position', 'width', 'height', 'data'])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
workflow.edges.push(
|
const result = zWorkflowEdge.safeParse(edge);
|
||||||
pick(edge, [
|
if (!result.success) {
|
||||||
'source',
|
return;
|
||||||
'sourceHandle',
|
}
|
||||||
'target',
|
workflow.edges.push(result.data);
|
||||||
'targetHandle',
|
|
||||||
'id',
|
|
||||||
'type',
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
|
@ -467,7 +467,7 @@ export const buildInputFieldTemplate = (
|
|||||||
const fieldType = getFieldType(fieldSchema);
|
const fieldType = getFieldType(fieldSchema);
|
||||||
// console.log('input fieldType', fieldType);
|
// console.log('input fieldType', fieldType);
|
||||||
|
|
||||||
const { input, ui_hidden, ui_component, ui_type } = fieldSchema;
|
const { input, ui_hidden, ui_component, ui_type, ui_order } = fieldSchema;
|
||||||
|
|
||||||
const extra = {
|
const extra = {
|
||||||
input,
|
input,
|
||||||
@ -475,6 +475,7 @@ export const buildInputFieldTemplate = (
|
|||||||
ui_component,
|
ui_component,
|
||||||
ui_type,
|
ui_type,
|
||||||
required: nodeSchema.required?.includes(name) ?? false,
|
required: nodeSchema.required?.includes(name) ?? false,
|
||||||
|
ui_order,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseField = {
|
const baseField = {
|
||||||
|
@ -14,13 +14,34 @@ import {
|
|||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders';
|
import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders';
|
||||||
|
|
||||||
const RESERVED_FIELD_NAMES = ['id', 'type', 'metadata'];
|
const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'metadata'];
|
||||||
|
const RESERVED_OUTPUT_FIELD_NAMES = ['type'];
|
||||||
|
|
||||||
const invocationDenylist: AnyInvocationType[] = [
|
const invocationDenylist: AnyInvocationType[] = [
|
||||||
'graph',
|
'graph',
|
||||||
'metadata_accumulator',
|
'metadata_accumulator',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isAllowedInputField = (nodeType: string, fieldName: string) => {
|
||||||
|
if (RESERVED_INPUT_FIELD_NAMES.includes(fieldName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (nodeType === 'collect' && fieldName === 'collection') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (nodeType === 'iterate' && fieldName === 'index') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllowedOutputField = (nodeType: string, fieldName: string) => {
|
||||||
|
if (RESERVED_OUTPUT_FIELD_NAMES.includes(fieldName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const isNotInDenylist = (schema: InvocationSchemaObject) =>
|
const isNotInDenylist = (schema: InvocationSchemaObject) =>
|
||||||
!invocationDenylist.includes(schema.properties.type.default);
|
!invocationDenylist.includes(schema.properties.type.default);
|
||||||
|
|
||||||
@ -42,17 +63,28 @@ export const parseSchema = (
|
|||||||
const inputs = reduce(
|
const inputs = reduce(
|
||||||
schema.properties,
|
schema.properties,
|
||||||
(inputsAccumulator, property, propertyName) => {
|
(inputsAccumulator, property, propertyName) => {
|
||||||
if (
|
if (!isAllowedInputField(type, propertyName)) {
|
||||||
!RESERVED_FIELD_NAMES.includes(propertyName) &&
|
logger('nodes').trace(
|
||||||
isInvocationFieldSchema(property) &&
|
{ type, propertyName, property: parseify(property) },
|
||||||
!property.ui_hidden
|
'Skipped reserved input field'
|
||||||
) {
|
);
|
||||||
|
return inputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInvocationFieldSchema(property)) {
|
||||||
|
logger('nodes').warn(
|
||||||
|
{ type, propertyName, property: parseify(property) },
|
||||||
|
'Unhandled input property'
|
||||||
|
);
|
||||||
|
return inputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
const field = buildInputFieldTemplate(schema, property, propertyName);
|
const field = buildInputFieldTemplate(schema, property, propertyName);
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
inputsAccumulator[propertyName] = field;
|
inputsAccumulator[propertyName] = field;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return inputsAccumulator;
|
return inputsAccumulator;
|
||||||
},
|
},
|
||||||
{} as Record<string, InputFieldTemplate>
|
{} as Record<string, InputFieldTemplate>
|
||||||
@ -82,14 +114,27 @@ export const parseSchema = (
|
|||||||
throw 'Invalid output schema';
|
throw 'Invalid output schema';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outputType = outputSchema.properties.type.default;
|
||||||
|
|
||||||
const outputs = reduce(
|
const outputs = reduce(
|
||||||
outputSchema.properties as OpenAPIV3.SchemaObject,
|
outputSchema.properties,
|
||||||
(outputsAccumulator, property, propertyName) => {
|
(outputsAccumulator, property, propertyName) => {
|
||||||
if (
|
if (!isAllowedOutputField(type, propertyName)) {
|
||||||
!['type', 'id'].includes(propertyName) &&
|
logger('nodes').trace(
|
||||||
!['object'].includes(property.type) && // TODO: handle objects?
|
{ type, propertyName, property: parseify(property) },
|
||||||
isInvocationFieldSchema(property)
|
'Skipped reserved output field'
|
||||||
) {
|
);
|
||||||
|
return outputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInvocationFieldSchema(property)) {
|
||||||
|
logger('nodes').warn(
|
||||||
|
{ type, propertyName, property: parseify(property) },
|
||||||
|
'Unhandled output property'
|
||||||
|
);
|
||||||
|
return outputsAccumulator;
|
||||||
|
}
|
||||||
|
|
||||||
const fieldType = getFieldType(property);
|
const fieldType = getFieldType(property);
|
||||||
outputsAccumulator[propertyName] = {
|
outputsAccumulator[propertyName] = {
|
||||||
fieldKind: 'output',
|
fieldKind: 'output',
|
||||||
@ -97,10 +142,10 @@ export const parseSchema = (
|
|||||||
title: property.title ?? '',
|
title: property.title ?? '',
|
||||||
description: property.description ?? '',
|
description: property.description ?? '',
|
||||||
type: fieldType,
|
type: fieldType,
|
||||||
|
ui_hidden: property.ui_hidden ?? false,
|
||||||
|
ui_type: property.ui_type,
|
||||||
|
ui_order: property.ui_order,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
logger('nodes').warn({ property }, 'Unhandled output property');
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputsAccumulator;
|
return outputsAccumulator;
|
||||||
},
|
},
|
||||||
@ -114,6 +159,7 @@ export const parseSchema = (
|
|||||||
description,
|
description,
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
|
outputType,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(acc, { [type]: invocation });
|
Object.assign(acc, { [type]: invocation });
|
||||||
|
@ -69,31 +69,29 @@ const ParamControlNetCollapse = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IAICollapse label="ControlNet" activeLabel={activeLabel}>
|
<IAICollapse label="ControlNet" activeLabel={activeLabel}>
|
||||||
<Flex sx={{ flexDir: 'column', gap: 3 }}>
|
<Flex sx={{ flexDir: 'column', gap: 2 }}>
|
||||||
<Flex gap={2} alignItems="center">
|
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
flexDirection: 'column',
|
|
||||||
w: '100%',
|
w: '100%',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
px: 4,
|
p: 2,
|
||||||
py: 2,
|
ps: 3,
|
||||||
borderRadius: 4,
|
borderRadius: 'base',
|
||||||
bg: 'base.200',
|
alignItems: 'center',
|
||||||
|
bg: 'base.250',
|
||||||
_dark: {
|
_dark: {
|
||||||
bg: 'base.850',
|
bg: 'base.750',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ParamControlNetFeatureToggle />
|
<ParamControlNetFeatureToggle />
|
||||||
</Flex>
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Add ControlNet"
|
tooltip="Add ControlNet"
|
||||||
aria-label="Add ControlNet"
|
aria-label="Add ControlNet"
|
||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
isDisabled={!firstModel}
|
isDisabled={!firstModel}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
size="md"
|
size="sm"
|
||||||
onClick={handleClickedAddControlNet}
|
onClick={handleClickedAddControlNet}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -23,9 +23,8 @@ import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
|||||||
|
|
||||||
const promptInputSelector = createSelector(
|
const promptInputSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
({ generation, ui }, activeTabName) => {
|
({ generation }, activeTabName) => {
|
||||||
return {
|
return {
|
||||||
shouldPinParametersPanel: ui.shouldPinParametersPanel,
|
|
||||||
prompt: generation.positivePrompt,
|
prompt: generation.positivePrompt,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
};
|
};
|
||||||
@ -42,8 +41,7 @@ const promptInputSelector = createSelector(
|
|||||||
*/
|
*/
|
||||||
const ParamPositiveConditioning = () => {
|
const ParamPositiveConditioning = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { prompt, shouldPinParametersPanel, activeTabName } =
|
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||||
useAppSelector(promptInputSelector);
|
|
||||||
const isReady = useIsReadyToInvoke();
|
const isReady = useIsReadyToInvoke();
|
||||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
@ -148,7 +146,7 @@ const ParamPositiveConditioning = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: shouldPinParametersPanel ? 5 : 0,
|
top: 0,
|
||||||
insetInlineEnd: 0,
|
insetInlineEnd: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -49,7 +49,7 @@ const InitialImageDisplay = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
p: 4,
|
p: 2,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -64,6 +64,7 @@ const InitialImageDisplay = () => {
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
sx={{
|
sx={{
|
||||||
|
ps: 2,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
color: 'base.700',
|
color: 'base.700',
|
||||||
|
@ -27,6 +27,7 @@ import { MdCancel, MdCancelScheduleSend } from 'react-icons/md';
|
|||||||
|
|
||||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
import { sessionCanceled } from 'services/api/thunks/session';
|
import { sessionCanceled } from 'services/api/thunks/session';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
|
||||||
const cancelButtonSelector = createSelector(
|
const cancelButtonSelector = createSelector(
|
||||||
systemSelector,
|
systemSelector,
|
||||||
@ -49,15 +50,14 @@ const cancelButtonSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface CancelButtonProps {
|
type Props = Omit<ButtonProps, 'aria-label'> & {
|
||||||
btnGroupWidth?: string | number;
|
btnGroupWidth?: string | number;
|
||||||
}
|
asIconButton?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const CancelButton = (
|
const CancelButton = (props: Props) => {
|
||||||
props: CancelButtonProps & Omit<ButtonProps, 'aria-label'>
|
|
||||||
) => {
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { btnGroupWidth = 'auto', ...rest } = props;
|
const { btnGroupWidth = 'auto', asIconButton = false, ...rest } = props;
|
||||||
const {
|
const {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -124,6 +124,7 @@ const CancelButton = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup isAttached width={btnGroupWidth}>
|
<ButtonGroup isAttached width={btnGroupWidth}>
|
||||||
|
{asIconButton ? (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={cancelIcon}
|
icon={cancelIcon}
|
||||||
tooltip={cancelLabel}
|
tooltip={cancelLabel}
|
||||||
@ -134,6 +135,20 @@ const CancelButton = (
|
|||||||
id="cancel-button"
|
id="cancel-button"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<IAIButton
|
||||||
|
leftIcon={cancelIcon}
|
||||||
|
tooltip={cancelLabel}
|
||||||
|
aria-label={cancelLabel}
|
||||||
|
isDisabled={!isConnected || !isProcessing || !isCancelable}
|
||||||
|
onClick={handleClickCancel}
|
||||||
|
colorScheme="error"
|
||||||
|
id="cancel-button"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</IAIButton>
|
||||||
|
)}
|
||||||
<Menu closeOnSelect={false}>
|
<Menu closeOnSelect={false}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IAIIconButton}
|
as={IAIIconButton}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ChakraProps,
|
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
ListItem,
|
ListItem,
|
||||||
@ -19,7 +18,6 @@ import IAIIconButton, {
|
|||||||
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
||||||
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
|
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
|
||||||
import ProgressBar from 'features/system/components/ProgressBar';
|
import ProgressBar from 'features/system/components/ProgressBar';
|
||||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -27,42 +25,16 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { FaPlay } from 'react-icons/fa';
|
import { FaPlay } from 'react-icons/fa';
|
||||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
|
|
||||||
const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
|
|
||||||
_disabled: {
|
|
||||||
bg: 'none',
|
|
||||||
color: 'base.600',
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
_hover: {
|
|
||||||
color: 'base.600',
|
|
||||||
bg: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector, activeTabNameSelector, selectIsBusy],
|
|
||||||
({ gallery }, activeTabName, isBusy) => {
|
|
||||||
const { autoAddBoardId } = gallery;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isBusy,
|
|
||||||
autoAddBoardId,
|
|
||||||
activeTabName,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
interface InvokeButton
|
interface InvokeButton
|
||||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||||
iconButton?: boolean;
|
asIconButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvokeButton(props: InvokeButton) {
|
export default function InvokeButton(props: InvokeButton) {
|
||||||
const { iconButton = false, ...rest } = props;
|
const { asIconButton = false, sx, ...rest } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isReady, isProcessing } = useIsReadyToInvoke();
|
const { isReady, isProcessing } = useIsReadyToInvoke();
|
||||||
const { activeTabName } = useAppSelector(selector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
|
||||||
const handleInvoke = useCallback(() => {
|
const handleInvoke = useCallback(() => {
|
||||||
dispatch(clampSymmetrySteps());
|
dispatch(clampSymmetrySteps());
|
||||||
@ -87,21 +59,22 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
<Box style={{ position: 'relative' }}>
|
<Box style={{ position: 'relative' }}>
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<Box
|
<Box
|
||||||
borderRadius="base"
|
sx={{
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '0',
|
bottom: '0',
|
||||||
left: '0',
|
left: '0',
|
||||||
right: '0',
|
right: '0',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'clip',
|
overflow: 'clip',
|
||||||
|
borderRadius: 'base',
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{iconButton ? (
|
{asIconButton ? (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('parameters.invoke')}
|
aria-label={t('parameters.invoke')}
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -112,18 +85,20 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
id="invoke-button"
|
id="invoke-button"
|
||||||
{...rest}
|
data-progress={isProcessing}
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
w: 'full',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
...sx,
|
||||||
}}
|
}}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IAIButton
|
<IAIButton
|
||||||
tooltip={<InvokeButtonTooltipContent />}
|
tooltip={<InvokeButtonTooltipContent />}
|
||||||
aria-label={t('parameters.invoke')}
|
aria-label={t('parameters.invoke')}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-progress={isProcessing}
|
||||||
isDisabled={!isReady}
|
isDisabled={!isReady}
|
||||||
onClick={handleInvoke}
|
onClick={handleInvoke}
|
||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
@ -131,13 +106,13 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
leftIcon={isProcessing ? undefined : <FaPlay />}
|
leftIcon={isProcessing ? undefined : <FaPlay />}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('parameters.invoke')}
|
loadingText={t('parameters.invoke')}
|
||||||
{...rest}
|
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
w: 'full',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
...sx,
|
||||||
}}
|
}}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
Invoke
|
Invoke
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
@ -147,9 +122,21 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tooltipSelector = createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
({ gallery }) => {
|
||||||
|
const { autoAddBoardId } = gallery;
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoAddBoardId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
export const InvokeButtonTooltipContent = memo(() => {
|
export const InvokeButtonTooltipContent = memo(() => {
|
||||||
const { isReady, reasons } = useIsReadyToInvoke();
|
const { isReady, reasons } = useIsReadyToInvoke();
|
||||||
const { autoAddBoardId } = useAppSelector(selector);
|
const { autoAddBoardId } = useAppSelector(tooltipSelector);
|
||||||
const autoAddBoardName = useBoardName(autoAddBoardId);
|
const autoAddBoardName = useBoardName(autoAddBoardId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,7 +153,11 @@ export const InvokeButtonTooltipContent = memo(() => {
|
|||||||
))}
|
))}
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
)}
|
)}
|
||||||
<Divider opacity={0.2} />
|
<Divider
|
||||||
|
opacity={0.2}
|
||||||
|
borderColor="base.50"
|
||||||
|
_dark={{ borderColor: 'base.900' }}
|
||||||
|
/>
|
||||||
<Text fontWeight={400} fontStyle="oblique 10deg">
|
<Text fontWeight={400} fontStyle="oblique 10deg">
|
||||||
Adding images to{' '}
|
Adding images to{' '}
|
||||||
<Text as="span" fontWeight={600}>
|
<Text as="span" fontWeight={600}>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { memo } from 'react';
|
||||||
import CancelButton from './CancelButton';
|
import CancelButton from './CancelButton';
|
||||||
import InvokeButton from './InvokeButton';
|
import InvokeButton from './InvokeButton';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons to start and cancel image generation.
|
* Buttons to start and cancel image generation.
|
||||||
*/
|
*/
|
||||||
const ProcessButtons = () => {
|
const ProcessButtons = () => {
|
||||||
return (
|
return (
|
||||||
<Flex gap={2}>
|
<Flex layerStyle="first" sx={{ gap: 2, borderRadius: 'base', p: 2 }}>
|
||||||
<InvokeButton />
|
<InvokeButton />
|
||||||
<CancelButton />
|
<CancelButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -210,7 +210,7 @@ export type HeightParam = z.infer<typeof zHeight>;
|
|||||||
export const isValidHeight = (val: unknown): val is HeightParam =>
|
export const isValidHeight = (val: unknown): val is HeightParam =>
|
||||||
zHeight.safeParse(val).success;
|
zHeight.safeParse(val).success;
|
||||||
|
|
||||||
const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']);
|
export const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']);
|
||||||
|
|
||||||
export type BaseModelParam = z.infer<typeof zBaseModel>;
|
export type BaseModelParam = z.infer<typeof zBaseModel>;
|
||||||
|
|
||||||
|
@ -9,10 +9,6 @@ export default function ParamSDXLConcatButton() {
|
|||||||
(state: RootState) => state.sdxl.shouldConcatSDXLStylePrompt
|
(state: RootState) => state.sdxl.shouldConcatSDXLStylePrompt
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
|
||||||
(state: RootState) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleShouldConcatPromptChange = () => {
|
const handleShouldConcatPromptChange = () => {
|
||||||
@ -31,7 +27,7 @@ export default function ParamSDXLConcatButton() {
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
insetInlineEnd: 1,
|
insetInlineEnd: 1,
|
||||||
top: shouldPinParametersPanel ? 12 : 20,
|
top: 6,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: shouldConcatSDXLStylePrompt ? 'accent.500' : 'base.500',
|
color: shouldConcatSDXLStylePrompt ? 'accent.500' : 'base.500',
|
||||||
_hover: {
|
_hover: {
|
||||||
|
@ -2,17 +2,15 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/Para
|
|||||||
import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse';
|
import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse';
|
||||||
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
||||||
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
||||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
import { memo } from 'react';
|
||||||
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
||||||
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
||||||
import SDXLImageToImageTabCoreParameters from './SDXLImageToImageTabCoreParameters';
|
import SDXLImageToImageTabCoreParameters from './SDXLImageToImageTabCoreParameters';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
const SDXLImageToImageTabParameters = () => {
|
const SDXLImageToImageTabParameters = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ParamSDXLPromptArea />
|
<ParamSDXLPromptArea />
|
||||||
<ProcessButtons />
|
|
||||||
<SDXLImageToImageTabCoreParameters />
|
<SDXLImageToImageTabCoreParameters />
|
||||||
<ParamSDXLRefinerCollapse />
|
<ParamSDXLRefinerCollapse />
|
||||||
<ParamControlNetCollapse />
|
<ParamControlNetCollapse />
|
||||||
|
@ -2,17 +2,15 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/Para
|
|||||||
import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse';
|
import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse';
|
||||||
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
||||||
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
||||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
|
||||||
import TextToImageTabCoreParameters from 'features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters';
|
import TextToImageTabCoreParameters from 'features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters';
|
||||||
|
import { memo } from 'react';
|
||||||
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
||||||
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
const SDXLTextToImageTabParameters = () => {
|
const SDXLTextToImageTabParameters = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ParamSDXLPromptArea />
|
<ParamSDXLPromptArea />
|
||||||
<ProcessButtons />
|
|
||||||
<TextToImageTabCoreParameters />
|
<TextToImageTabCoreParameters />
|
||||||
<ParamSDXLRefinerCollapse />
|
<ParamSDXLRefinerCollapse />
|
||||||
<ParamControlNetCollapse />
|
<ParamControlNetCollapse />
|
||||||
|
@ -5,7 +5,6 @@ import ParamMaskAdjustmentCollapse from 'features/parameters/components/Paramete
|
|||||||
import ParamSeamPaintingCollapse from 'features/parameters/components/Parameters/Canvas/SeamPainting/ParamSeamPaintingCollapse';
|
import ParamSeamPaintingCollapse from 'features/parameters/components/Parameters/Canvas/SeamPainting/ParamSeamPaintingCollapse';
|
||||||
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
|
||||||
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse';
|
||||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
|
||||||
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
import ParamSDXLPromptArea from './ParamSDXLPromptArea';
|
||||||
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
import ParamSDXLRefinerCollapse from './ParamSDXLRefinerCollapse';
|
||||||
import SDXLUnifiedCanvasTabCoreParameters from './SDXLUnifiedCanvasTabCoreParameters';
|
import SDXLUnifiedCanvasTabCoreParameters from './SDXLUnifiedCanvasTabCoreParameters';
|
||||||
@ -14,7 +13,6 @@ export default function SDXLUnifiedCanvasTabParameters() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ParamSDXLPromptArea />
|
<ParamSDXLPromptArea />
|
||||||
<ProcessButtons />
|
|
||||||
<SDXLUnifiedCanvasTabCoreParameters />
|
<SDXLUnifiedCanvasTabCoreParameters />
|
||||||
<ParamSDXLRefinerCollapse />
|
<ParamSDXLRefinerCollapse />
|
||||||
<ParamControlNetCollapse />
|
<ParamControlNetCollapse />
|
||||||
|
@ -60,11 +60,6 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
desc: t('hotkeys.toggleOptions.desc'),
|
desc: t('hotkeys.toggleOptions.desc'),
|
||||||
hotkey: 'O',
|
hotkey: 'O',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('hotkeys.pinOptions.title'),
|
|
||||||
desc: t('hotkeys.pinOptions.desc'),
|
|
||||||
hotkey: 'Shift+O',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('hotkeys.toggleGallery.title'),
|
title: t('hotkeys.toggleGallery.title'),
|
||||||
desc: t('hotkeys.toggleGallery.desc'),
|
desc: t('hotkeys.toggleGallery.desc'),
|
||||||
@ -136,11 +131,6 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
desc: t('hotkeys.nextImage.desc'),
|
desc: t('hotkeys.nextImage.desc'),
|
||||||
hotkey: 'Arrow Right',
|
hotkey: 'Arrow Right',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('hotkeys.toggleGalleryPin.title'),
|
|
||||||
desc: t('hotkeys.toggleGalleryPin.desc'),
|
|
||||||
hotkey: 'Shift+G',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('hotkeys.increaseGalleryThumbSize.title'),
|
title: t('hotkeys.increaseGalleryThumbSize.title'),
|
||||||
desc: t('hotkeys.increaseGalleryThumbSize.desc'),
|
desc: t('hotkeys.increaseGalleryThumbSize.desc'),
|
||||||
@ -290,7 +280,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
{
|
{
|
||||||
title: t('hotkeys.addNodes.title'),
|
title: t('hotkeys.addNodes.title'),
|
||||||
desc: t('hotkeys.addNodes.desc'),
|
desc: t('hotkeys.addNodes.desc'),
|
||||||
hotkey: 'Shift + A',
|
hotkey: 'Shift + A / Space',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const InvokeAILogoComponent = ({ showVersion = true }: Props) => {
|
|||||||
const isHovered = useHoverDirty(ref);
|
const isHovered = useHoverDirty(ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex alignItems="center" gap={3} ps={1} ref={ref}>
|
<Flex alignItems="center" gap={5} ps={1} ref={ref}>
|
||||||
<Image
|
<Image
|
||||||
src={InvokeAILogoImage}
|
src={InvokeAILogoImage}
|
||||||
alt="invoke-ai-logo"
|
alt="invoke-ai-logo"
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSettingsModalClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResetWebUIButton = ({ onSettingsModalClose }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState(5);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: isRefreshModalOpen,
|
||||||
|
onOpen: onRefreshModalOpen,
|
||||||
|
onClose: onRefreshModalClose,
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const handleClickResetWebUI = useCallback(() => {
|
||||||
|
// Only remove our keys
|
||||||
|
Object.keys(window.localStorage).forEach((key) => {
|
||||||
|
if (
|
||||||
|
LOCALSTORAGE_KEYS.includes(key) ||
|
||||||
|
key.startsWith(LOCALSTORAGE_PREFIX)
|
||||||
|
) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onSettingsModalClose();
|
||||||
|
onRefreshModalOpen();
|
||||||
|
setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
||||||
|
}, [onSettingsModalClose, onRefreshModalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
|
||||||
|
{t('settings.resetWebUI')}
|
||||||
|
</IAIButton>
|
||||||
|
<Modal
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
isOpen={isRefreshModalOpen}
|
||||||
|
onClose={onRefreshModalClose}
|
||||||
|
isCentered
|
||||||
|
closeOnEsc={false}
|
||||||
|
>
|
||||||
|
<ModalOverlay backdropFilter="blur(40px)" />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<Text fontSize="lg">
|
||||||
|
<Text>{t('settings.resetComplete')}</Text>
|
||||||
|
<Text>Reloading in {countdown}...</Text>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ResetWebUIButton);
|
@ -31,7 +31,6 @@ import {
|
|||||||
} from 'features/system/store/systemSlice';
|
} from 'features/system/store/systemSlice';
|
||||||
import {
|
import {
|
||||||
setShouldShowProgressInViewer,
|
setShouldShowProgressInViewer,
|
||||||
setShouldUseCanvasBetaLayout,
|
|
||||||
setShouldUseSliders,
|
setShouldUseSliders,
|
||||||
} from 'features/ui/store/uiSlice';
|
} from 'features/ui/store/uiSlice';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
@ -42,6 +41,7 @@ import {
|
|||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LogLevelName } from 'roarr';
|
import { LogLevelName } from 'roarr';
|
||||||
@ -68,18 +68,13 @@ const selector = createSelector(
|
|||||||
shouldUseWatermarker,
|
shouldUseWatermarker,
|
||||||
} = system;
|
} = system;
|
||||||
|
|
||||||
const {
|
const { shouldUseSliders, shouldShowProgressInViewer } = ui;
|
||||||
shouldUseCanvasBetaLayout,
|
|
||||||
shouldUseSliders,
|
|
||||||
shouldShowProgressInViewer,
|
|
||||||
} = ui;
|
|
||||||
|
|
||||||
const { shouldShowAdvancedOptions } = generation;
|
const { shouldShowAdvancedOptions } = generation;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shouldConfirmOnDelete,
|
shouldConfirmOnDelete,
|
||||||
enableImageDebugging,
|
enableImageDebugging,
|
||||||
shouldUseCanvasBetaLayout,
|
|
||||||
shouldUseSliders,
|
shouldUseSliders,
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
consoleLogLevel,
|
consoleLogLevel,
|
||||||
@ -98,7 +93,6 @@ const selector = createSelector(
|
|||||||
type ConfigOptions = {
|
type ConfigOptions = {
|
||||||
shouldShowDeveloperSettings: boolean;
|
shouldShowDeveloperSettings: boolean;
|
||||||
shouldShowResetWebUiText: boolean;
|
shouldShowResetWebUiText: boolean;
|
||||||
shouldShowBetaLayout: boolean;
|
|
||||||
shouldShowAdvancedOptionsSettings: boolean;
|
shouldShowAdvancedOptionsSettings: boolean;
|
||||||
shouldShowClearIntermediates: boolean;
|
shouldShowClearIntermediates: boolean;
|
||||||
shouldShowLocalizationToggle: boolean;
|
shouldShowLocalizationToggle: boolean;
|
||||||
@ -113,8 +107,8 @@ type SettingsModalProps = {
|
|||||||
const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState(3);
|
||||||
|
|
||||||
const shouldShowBetaLayout = config?.shouldShowBetaLayout ?? true;
|
|
||||||
const shouldShowDeveloperSettings =
|
const shouldShowDeveloperSettings =
|
||||||
config?.shouldShowDeveloperSettings ?? true;
|
config?.shouldShowDeveloperSettings ?? true;
|
||||||
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
|
const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true;
|
||||||
@ -156,7 +150,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
const {
|
const {
|
||||||
shouldConfirmOnDelete,
|
shouldConfirmOnDelete,
|
||||||
enableImageDebugging,
|
enableImageDebugging,
|
||||||
shouldUseCanvasBetaLayout,
|
|
||||||
shouldUseSliders,
|
shouldUseSliders,
|
||||||
shouldShowProgressInViewer,
|
shouldShowProgressInViewer,
|
||||||
consoleLogLevel,
|
consoleLogLevel,
|
||||||
@ -179,8 +172,15 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
});
|
});
|
||||||
onSettingsModalClose();
|
onSettingsModalClose();
|
||||||
onRefreshModalOpen();
|
onRefreshModalOpen();
|
||||||
|
setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
||||||
}, [onSettingsModalClose, onRefreshModalOpen]);
|
}, [onSettingsModalClose, onRefreshModalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
const handleLogLevelChanged = useCallback(
|
const handleLogLevelChanged = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
dispatch(consoleLogLevelChanged(v as LogLevelName));
|
dispatch(consoleLogLevelChanged(v as LogLevelName));
|
||||||
@ -297,17 +297,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{shouldShowBetaLayout && (
|
|
||||||
<SettingSwitch
|
|
||||||
label={t('settings.alternateCanvasLayout')}
|
|
||||||
useBadge
|
|
||||||
badgeLabel={t('settings.beta')}
|
|
||||||
isChecked={shouldUseCanvasBetaLayout}
|
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shouldShowLocalizationToggle && (
|
{shouldShowLocalizationToggle && (
|
||||||
<IAIMantineSelect
|
<IAIMantineSelect
|
||||||
disabled={!isLocalizationEnabled}
|
disabled={!isLocalizationEnabled}
|
||||||
@ -381,6 +370,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
isOpen={isRefreshModalOpen}
|
isOpen={isRefreshModalOpen}
|
||||||
onClose={onRefreshModalClose}
|
onClose={onRefreshModalClose}
|
||||||
isCentered
|
isCentered
|
||||||
|
closeOnEsc={false}
|
||||||
>
|
>
|
||||||
<ModalOverlay backdropFilter="blur(40px)" />
|
<ModalOverlay backdropFilter="blur(40px)" />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
@ -388,7 +378,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Flex justifyContent="center">
|
<Flex justifyContent="center">
|
||||||
<Text fontSize="lg">
|
<Text fontSize="lg">
|
||||||
<Text>{t('settings.resetComplete')}</Text>
|
<Text>
|
||||||
|
{t('settings.resetComplete')} Reloading in {countdown}...
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { configSelector } from '../store/configSelectors';
|
|
||||||
import { systemSelector } from '../store/systemSelectors';
|
|
||||||
|
|
||||||
const isApplicationReadySelector = createSelector(
|
|
||||||
[systemSelector, configSelector],
|
|
||||||
(system, config) => {
|
|
||||||
const { wasSchemaParsed } = system;
|
|
||||||
|
|
||||||
const { disabledTabs } = config;
|
|
||||||
|
|
||||||
return {
|
|
||||||
disabledTabs,
|
|
||||||
wasSchemaParsed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the application is ready to be used, i.e. if the initial startup process is finished.
|
|
||||||
*/
|
|
||||||
export const useIsApplicationReady = () => {
|
|
||||||
const { disabledTabs, wasSchemaParsed } = useAppSelector(
|
|
||||||
isApplicationReadySelector
|
|
||||||
);
|
|
||||||
|
|
||||||
const isApplicationReady = useMemo(() => {
|
|
||||||
if (!disabledTabs.includes('nodes') && !wasSchemaParsed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, [disabledTabs, wasSchemaParsed]);
|
|
||||||
|
|
||||||
return isApplicationReady;
|
|
||||||
};
|
|
@ -16,7 +16,6 @@ export const systemPersistDenylist: (keyof SystemState)[] = [
|
|||||||
'isCancelScheduled',
|
'isCancelScheduled',
|
||||||
'progressImage',
|
'progressImage',
|
||||||
'wereModelsReceived',
|
'wereModelsReceived',
|
||||||
'wasSchemaParsed',
|
|
||||||
'isPersisted',
|
'isPersisted',
|
||||||
'isUploading',
|
'isUploading',
|
||||||
];
|
];
|
||||||
|
@ -2,9 +2,8 @@ import { UseToastOptions } from '@chakra-ui/react';
|
|||||||
import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import { InvokeLogLevel } from 'app/logging/logger';
|
import { InvokeLogLevel } from 'app/logging/logger';
|
||||||
import { userInvoked } from 'app/store/actions';
|
import { userInvoked } from 'app/store/actions';
|
||||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { startCase, upperFirst } from 'lodash-es';
|
import { get, startCase, upperFirst } from 'lodash-es';
|
||||||
import { LogLevelName } from 'roarr';
|
import { LogLevelName } from 'roarr';
|
||||||
import {
|
import {
|
||||||
isAnySessionRejected,
|
isAnySessionRejected,
|
||||||
@ -68,10 +67,6 @@ export interface SystemState {
|
|||||||
* Whether or not the available models were received
|
* Whether or not the available models were received
|
||||||
*/
|
*/
|
||||||
wereModelsReceived: boolean;
|
wereModelsReceived: boolean;
|
||||||
/**
|
|
||||||
* Whether or not the OpenAPI schema was received and parsed
|
|
||||||
*/
|
|
||||||
wasSchemaParsed: boolean;
|
|
||||||
/**
|
/**
|
||||||
* The console output logging level
|
* The console output logging level
|
||||||
*/
|
*/
|
||||||
@ -112,7 +107,6 @@ export const initialSystemState: SystemState = {
|
|||||||
isCancelScheduled: false,
|
isCancelScheduled: false,
|
||||||
subscribedNodeIds: [],
|
subscribedNodeIds: [],
|
||||||
wereModelsReceived: false,
|
wereModelsReceived: false,
|
||||||
wasSchemaParsed: false,
|
|
||||||
consoleLogLevel: 'debug',
|
consoleLogLevel: 'debug',
|
||||||
shouldLogToConsole: true,
|
shouldLogToConsole: true,
|
||||||
statusTranslationKey: 'common.statusDisconnected',
|
statusTranslationKey: 'common.statusDisconnected',
|
||||||
@ -339,13 +333,6 @@ export const systemSlice = createSlice({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenAPI schema was parsed
|
|
||||||
*/
|
|
||||||
builder.addCase(nodeTemplatesBuilt, (state) => {
|
|
||||||
state.wasSchemaParsed = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// *** Matchers - must be after all cases ***
|
// *** Matchers - must be after all cases ***
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -381,14 +368,14 @@ export const systemSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (action.payload?.error) {
|
} else if (action.payload?.error) {
|
||||||
errorDescription = action.payload?.error as string;
|
errorDescription = action.payload?.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.toastQueue.push(
|
state.toastQueue.push(
|
||||||
makeToast({
|
makeToast({
|
||||||
title: t('toast.serverError'),
|
title: t('toast.serverError'),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
description: errorDescription,
|
description: get(errorDescription, 'detail', 'Unknown Error'),
|
||||||
duration,
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,65 +1,57 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { Portal } from '@chakra-ui/portal';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { RefObject, memo } from 'react';
|
||||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MdPhotoLibrary } from 'react-icons/md';
|
import { MdPhotoLibrary } from 'react-icons/md';
|
||||||
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
|
import { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
import { NO_GALLERY_TABS } from './InvokeTabs';
|
|
||||||
|
|
||||||
const floatingGalleryButtonSelector = createSelector(
|
type Props = {
|
||||||
[activeTabNameSelector, uiSelector],
|
isGalleryCollapsed: boolean;
|
||||||
(activeTabName, ui) => {
|
galleryPanelRef: RefObject<ImperativePanelHandle>;
|
||||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGalleryButton: NO_GALLERY_TABS.includes(activeTabName)
|
|
||||||
? false
|
|
||||||
: !shouldShowGallery,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const FloatingGalleryButton = () => {
|
const FloatingGalleryButton = ({
|
||||||
|
isGalleryCollapsed,
|
||||||
|
galleryPanelRef,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { shouldPinGallery, shouldShowGalleryButton } = useAppSelector(
|
|
||||||
floatingGalleryButtonSelector
|
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const handleShowGallery = () => {
|
const handleShowGallery = () => {
|
||||||
dispatch(setShouldShowGallery(true));
|
galleryPanelRef.current?.expand();
|
||||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return shouldShowGalleryButton ? (
|
if (!isGalleryCollapsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Flex
|
||||||
|
pos="absolute"
|
||||||
|
transform="translate(0, -50%)"
|
||||||
|
minW={8}
|
||||||
|
top="50%"
|
||||||
|
insetInlineEnd="1.63rem"
|
||||||
|
>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Gallery (G)"
|
tooltip="Show Gallery (G)"
|
||||||
tooltipProps={{ placement: 'top' }}
|
tooltipProps={{ placement: 'top' }}
|
||||||
aria-label={t('accessibility.showGallery')}
|
aria-label={t('common.showGalleryPanel')}
|
||||||
onClick={handleShowGallery}
|
onClick={handleShowGallery}
|
||||||
|
icon={<MdPhotoLibrary />}
|
||||||
sx={{
|
sx={{
|
||||||
pos: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translate(0, -50%)',
|
|
||||||
p: 0,
|
p: 0,
|
||||||
insetInlineEnd: 0,
|
|
||||||
px: 3,
|
px: 3,
|
||||||
h: 48,
|
h: 48,
|
||||||
w: 8,
|
|
||||||
borderStartEndRadius: 0,
|
borderStartEndRadius: 0,
|
||||||
borderEndEndRadius: 0,
|
borderEndEndRadius: 0,
|
||||||
shadow: '2xl',
|
shadow: '2xl',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<MdPhotoLibrary />
|
</Flex>
|
||||||
</IAIIconButton>
|
</Portal>
|
||||||
) : null;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(FloatingGalleryButton);
|
export default memo(FloatingGalleryButton);
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
import { ChakraProps, Flex } from '@chakra-ui/react';
|
import { ChakraProps, Flex, Portal } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
||||||
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
||||||
import {
|
import { RefObject, memo } from 'react';
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FaSlidersH } from 'react-icons/fa';
|
import { FaSlidersH } from 'react-icons/fa';
|
||||||
|
import { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
const floatingButtonStyles: ChakraProps['sx'] = {
|
const floatingButtonStyles: ChakraProps['sx'] = {
|
||||||
borderStartStartRadius: 0,
|
borderStartStartRadius: 0,
|
||||||
@ -22,81 +14,48 @@ const floatingButtonStyles: ChakraProps['sx'] = {
|
|||||||
shadow: '2xl',
|
shadow: '2xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const floatingParametersPanelButtonSelector = createSelector(
|
type Props = {
|
||||||
[uiSelector, activeTabNameSelector],
|
isSidePanelCollapsed: boolean;
|
||||||
(ui, activeTabName) => {
|
sidePanelRef: RefObject<ImperativePanelHandle>;
|
||||||
const {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldUseCanvasBetaLayout,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
} = ui;
|
|
||||||
|
|
||||||
const canvasBetaLayoutCheck =
|
|
||||||
shouldUseCanvasBetaLayout && activeTabName === 'unifiedCanvas';
|
|
||||||
|
|
||||||
const shouldShowProcessButtons =
|
|
||||||
!canvasBetaLayoutCheck &&
|
|
||||||
(!shouldPinParametersPanel || !shouldShowParametersPanel);
|
|
||||||
|
|
||||||
const shouldShowParametersPanelButton =
|
|
||||||
!canvasBetaLayoutCheck &&
|
|
||||||
!shouldShowParametersPanel &&
|
|
||||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanelButton,
|
|
||||||
shouldShowProcessButtons,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const FloatingParametersPanelButtons = () => {
|
const FloatingSidePanelButtons = ({
|
||||||
const dispatch = useAppDispatch();
|
isSidePanelCollapsed,
|
||||||
|
sidePanelRef,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
shouldShowProcessButtons,
|
|
||||||
shouldShowParametersPanelButton,
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
} = useAppSelector(floatingParametersPanelButtonSelector);
|
|
||||||
|
|
||||||
const handleShowOptionsPanel = () => {
|
const handleShowSidePanel = () => {
|
||||||
dispatch(setShouldShowParametersPanel(true));
|
sidePanelRef.current?.expand();
|
||||||
shouldPinParametersPanel && dispatch(requestCanvasRescale());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!shouldShowParametersPanelButton) {
|
if (!isSidePanelCollapsed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Portal>
|
||||||
<Flex
|
<Flex
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
transform="translate(0, -50%)"
|
transform="translate(0, -50%)"
|
||||||
minW={8}
|
minW={8}
|
||||||
top="50%"
|
top="50%"
|
||||||
insetInlineStart="4.5rem"
|
insetInlineStart="5.13rem"
|
||||||
direction="column"
|
direction="column"
|
||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Options Panel (O)"
|
tooltip="Show Side Panel (O, T)"
|
||||||
tooltipProps={{ placement: 'top' }}
|
aria-label={t('common.showOptionsPanel')}
|
||||||
aria-label={t('accessibility.showOptionsPanel')}
|
onClick={handleShowSidePanel}
|
||||||
onClick={handleShowOptionsPanel}
|
|
||||||
sx={floatingButtonStyles}
|
sx={floatingButtonStyles}
|
||||||
>
|
icon={<FaSlidersH />}
|
||||||
<FaSlidersH />
|
/>
|
||||||
</IAIIconButton>
|
<InvokeButton asIconButton sx={floatingButtonStyles} />
|
||||||
{shouldShowProcessButtons && (
|
<CancelButton sx={floatingButtonStyles} asIconButton />
|
||||||
<>
|
|
||||||
<InvokeButton iconButton sx={floatingButtonStyles} />
|
|
||||||
<CancelButton sx={floatingButtonStyles} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(FloatingParametersPanelButtons);
|
export default memo(FloatingSidePanelButtons);
|
||||||
|
@ -11,12 +11,12 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
|
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
|
||||||
import { RootState, stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import { InvokeTabName, tabMap } from 'features/ui/store/tabMap';
|
import { InvokeTabName, tabMap } from 'features/ui/store/tabMap';
|
||||||
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { ResourceKey } from 'i18next';
|
import { ResourceKey } from 'i18next';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
||||||
@ -25,11 +25,15 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
|
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
|
||||||
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
|
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
|
import { usePanel } from '../hooks/usePanel';
|
||||||
|
import { usePanelStorage } from '../hooks/usePanelStorage';
|
||||||
import {
|
import {
|
||||||
activeTabIndexSelector,
|
activeTabIndexSelector,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
} from '../store/uiSelectors';
|
} from '../store/uiSelectors';
|
||||||
|
import FloatingGalleryButton from './FloatingGalleryButton';
|
||||||
|
import FloatingSidePanelButtons from './FloatingParametersPanelButtons';
|
||||||
|
import ParametersPanel from './ParametersPanel';
|
||||||
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
|
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
|
||||||
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
|
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
|
||||||
import NodesTab from './tabs/Nodes/NodesTab';
|
import NodesTab from './tabs/Nodes/NodesTab';
|
||||||
@ -89,38 +93,20 @@ const enabledTabsSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const MIN_GALLERY_WIDTH = 350;
|
const SIDE_PANEL_MIN_SIZE_PX = 448;
|
||||||
const DEFAULT_GALLERY_PCT = 20;
|
const MAIN_PANEL_MIN_SIZE_PX = 448;
|
||||||
|
const GALLERY_PANEL_MIN_SIZE_PX = 360;
|
||||||
|
|
||||||
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
||||||
|
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager'];
|
||||||
|
|
||||||
const InvokeTabs = () => {
|
const InvokeTabs = () => {
|
||||||
const activeTab = useAppSelector(activeTabIndexSelector);
|
const activeTab = useAppSelector(activeTabIndexSelector);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const enabledTabs = useAppSelector(enabledTabsSelector);
|
const enabledTabs = useAppSelector(enabledTabsSelector);
|
||||||
|
|
||||||
const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } =
|
|
||||||
useAppSelector((state: RootState) => state.ui);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'f',
|
|
||||||
() => {
|
|
||||||
dispatch(togglePanels());
|
|
||||||
(shouldPinGallery || shouldPinParametersPanel) &&
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
},
|
|
||||||
[shouldPinGallery, shouldPinParametersPanel]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResizeGallery = useCallback(() => {
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
}, [dispatch, activeTabName]);
|
|
||||||
|
|
||||||
const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => {
|
const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => {
|
||||||
if (e.target instanceof HTMLElement) {
|
if (e.target instanceof HTMLElement) {
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
@ -153,9 +139,6 @@ const InvokeTabs = () => {
|
|||||||
[enabledTabs]
|
[enabledTabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { ref: galleryPanelRef, minSizePct: galleryMinSizePct } =
|
|
||||||
useMinimumPanelSize(MIN_GALLERY_WIDTH, DEFAULT_GALLERY_PCT, 'app');
|
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
const handleTabChange = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const activeTabName = tabMap[index];
|
const activeTabName = tabMap[index];
|
||||||
@ -167,6 +150,60 @@ const InvokeTabs = () => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
minSize: sidePanelMinSize,
|
||||||
|
isCollapsed: isSidePanelCollapsed,
|
||||||
|
setIsCollapsed: setIsSidePanelCollapsed,
|
||||||
|
ref: sidePanelRef,
|
||||||
|
reset: resetSidePanel,
|
||||||
|
expand: expandSidePanel,
|
||||||
|
collapse: collapseSidePanel,
|
||||||
|
toggle: toggleSidePanel,
|
||||||
|
} = usePanel(SIDE_PANEL_MIN_SIZE_PX, 'pixels');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ref: galleryPanelRef,
|
||||||
|
minSize: galleryPanelMinSize,
|
||||||
|
isCollapsed: isGalleryPanelCollapsed,
|
||||||
|
setIsCollapsed: setIsGalleryPanelCollapsed,
|
||||||
|
reset: resetGalleryPanel,
|
||||||
|
expand: expandGalleryPanel,
|
||||||
|
collapse: collapseGalleryPanel,
|
||||||
|
toggle: toggleGalleryPanel,
|
||||||
|
} = usePanel(GALLERY_PANEL_MIN_SIZE_PX, 'pixels');
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'f',
|
||||||
|
() => {
|
||||||
|
if (isGalleryPanelCollapsed || isSidePanelCollapsed) {
|
||||||
|
expandGalleryPanel();
|
||||||
|
expandSidePanel();
|
||||||
|
} else {
|
||||||
|
collapseSidePanel();
|
||||||
|
collapseGalleryPanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, isGalleryPanelCollapsed, isSidePanelCollapsed]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['t', 'o'],
|
||||||
|
() => {
|
||||||
|
toggleSidePanel();
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'g',
|
||||||
|
() => {
|
||||||
|
toggleGalleryPanel();
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="appTabs"
|
variant="appTabs"
|
||||||
@ -195,33 +232,64 @@ const InvokeTabs = () => {
|
|||||||
autoSaveId="app"
|
autoSaveId="app"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
|
units="pixels"
|
||||||
>
|
>
|
||||||
<Panel id="main">
|
{!NO_SIDE_PANEL_TABS.includes(activeTabName) && (
|
||||||
|
<>
|
||||||
|
<Panel
|
||||||
|
order={0}
|
||||||
|
id="side"
|
||||||
|
ref={sidePanelRef}
|
||||||
|
defaultSize={sidePanelMinSize}
|
||||||
|
minSize={sidePanelMinSize}
|
||||||
|
onCollapse={setIsSidePanelCollapsed}
|
||||||
|
collapsible
|
||||||
|
>
|
||||||
|
{activeTabName === 'nodes' ? (
|
||||||
|
<NodeEditorPanelGroup />
|
||||||
|
) : (
|
||||||
|
<ParametersPanel />
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
<ResizeHandle
|
||||||
|
onDoubleClick={resetSidePanel}
|
||||||
|
// isCollapsed={isSidePanelCollapsed}
|
||||||
|
collapsedDirection={isSidePanelCollapsed ? 'left' : undefined}
|
||||||
|
/>
|
||||||
|
<FloatingSidePanelButtons
|
||||||
|
isSidePanelCollapsed={isSidePanelCollapsed}
|
||||||
|
sidePanelRef={sidePanelRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Panel id="main" order={1} minSize={MAIN_PANEL_MIN_SIZE_PX}>
|
||||||
<TabPanels style={{ height: '100%', width: '100%' }}>
|
<TabPanels style={{ height: '100%', width: '100%' }}>
|
||||||
{tabPanels}
|
{tabPanels}
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Panel>
|
</Panel>
|
||||||
{shouldPinGallery &&
|
{!NO_GALLERY_TABS.includes(activeTabName) && (
|
||||||
shouldShowGallery &&
|
|
||||||
!NO_GALLERY_TABS.includes(activeTabName) && (
|
|
||||||
<>
|
<>
|
||||||
<ResizeHandle />
|
<ResizeHandle
|
||||||
|
onDoubleClick={resetGalleryPanel}
|
||||||
|
// isCollapsed={isGalleryPanelCollapsed}
|
||||||
|
collapsedDirection={isGalleryPanelCollapsed ? 'right' : undefined}
|
||||||
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
ref={galleryPanelRef}
|
|
||||||
onResize={handleResizeGallery}
|
|
||||||
id="gallery"
|
id="gallery"
|
||||||
order={3}
|
ref={galleryPanelRef}
|
||||||
defaultSize={
|
order={2}
|
||||||
galleryMinSizePct > DEFAULT_GALLERY_PCT &&
|
defaultSize={galleryPanelMinSize}
|
||||||
galleryMinSizePct < 100 // prevent this error https://github.com/bvaughn/react-resizable-panels/blob/main/packages/react-resizable-panels/src/Panel.ts#L96
|
minSize={galleryPanelMinSize}
|
||||||
? galleryMinSizePct
|
onCollapse={setIsGalleryPanelCollapsed}
|
||||||
: DEFAULT_GALLERY_PCT
|
collapsible
|
||||||
}
|
|
||||||
minSize={galleryMinSizePct}
|
|
||||||
maxSize={50}
|
|
||||||
>
|
>
|
||||||
<ImageGalleryContent />
|
<ImageGalleryContent />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<FloatingGalleryButton
|
||||||
|
isGalleryCollapsed={isGalleryPanelCollapsed}
|
||||||
|
galleryPanelRef={galleryPanelRef}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
|
||||||
import SDXLTextToImageTabParameters from 'features/sdxl/components/SDXLTextToImageTabParameters';
|
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
|
||||||
import {
|
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
|
||||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
|
||||||
import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer';
|
|
||||||
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
|
||||||
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
|
||||||
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[uiSelector, activeTabNameSelector],
|
|
||||||
(ui, activeTabName) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeTabName,
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const ParametersDrawer = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel, activeTabName } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleClosePanel = () => {
|
|
||||||
dispatch(setShouldShowParametersPanel(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
|
||||||
|
|
||||||
const drawerContent = useMemo(() => {
|
|
||||||
if (activeTabName === 'txt2img') {
|
|
||||||
return model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLTextToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<TextToImageTabParameters />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'img2img') {
|
|
||||||
return model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLImageToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<ImageToImageTabParameters />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
return <UnifiedCanvasParameters />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [activeTabName, model]);
|
|
||||||
|
|
||||||
if (shouldPinParametersPanel) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizableDrawer
|
|
||||||
direction="left"
|
|
||||||
isResizable={false}
|
|
||||||
isOpen={shouldShowParametersPanel}
|
|
||||||
onClose={handleClosePanel}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
flexDir: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: PARAMETERS_PANEL_WIDTH,
|
|
||||||
gap: 2,
|
|
||||||
position: 'relative',
|
|
||||||
flexShrink: 0,
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
paddingBottom={4}
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<InvokeAILogoComponent />
|
|
||||||
<PinParametersPanelButton />
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drawerContent}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</ResizableDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ParametersDrawer);
|
|
@ -0,0 +1,130 @@
|
|||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||||
|
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
||||||
|
import SDXLTextToImageTabParameters from 'features/sdxl/components/SDXLTextToImageTabParameters';
|
||||||
|
import SDXLUnifiedCanvasTabParameters from 'features/sdxl/components/SDXLUnifiedCanvasTabParameters';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
import { activeTabNameSelector } from '../store/uiSelectors';
|
||||||
|
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
||||||
|
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
||||||
|
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
||||||
|
|
||||||
|
const ParametersPanel = () => {
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const model = useAppSelector((state: RootState) => state.generation.model);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLTextToImageTabParameters />
|
||||||
|
) : (
|
||||||
|
<TextToImageTabParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'img2img') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLImageToImageTabParameters />
|
||||||
|
) : (
|
||||||
|
<ImageToImageTabParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'unifiedCanvas') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLUnifiedCanvasTabParameters />
|
||||||
|
) : (
|
||||||
|
<UnifiedCanvasParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParametersPanel);
|
||||||
|
|
||||||
|
const ParametersPanelWrapper = memo((props: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
flexDir: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProcessButtons />
|
||||||
|
<Flex
|
||||||
|
layerStyle="first"
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: 'base',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
defer
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
options={{
|
||||||
|
scrollbars: {
|
||||||
|
visibility: 'auto',
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 800,
|
||||||
|
theme: 'os-theme-dark',
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
gap: 2,
|
||||||
|
flexDirection: 'column',
|
||||||
|
h: 'full',
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ParametersPanelWrapper.displayName = 'ParametersPanelWrapper';
|
@ -1,57 +0,0 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { PropsWithChildren, memo } from 'react';
|
|
||||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
|
||||||
import { uiSelector } from '../store/uiSelectors';
|
|
||||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
|
||||||
|
|
||||||
const selector = createSelector(uiSelector, (ui) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
type ParametersPinnedWrapperProps = PropsWithChildren;
|
|
||||||
|
|
||||||
const ParametersPinnedWrapper = (props: ParametersPinnedWrapperProps) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
if (!(shouldPinParametersPanel && shouldShowParametersPanel)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
h: 'full',
|
|
||||||
w: PARAMETERS_PANEL_WIDTH,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
position: 'absolute',
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<PinParametersPanelButton
|
|
||||||
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ParametersPinnedWrapper);
|
|
@ -1,59 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton, {
|
|
||||||
IAIIconButtonProps,
|
|
||||||
} from 'common/components/IAIIconButton';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
|
||||||
import { setShouldPinParametersPanel } from '../store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
type PinParametersPanelButtonProps = Omit<IAIIconButtonProps, 'aria-label'>;
|
|
||||||
|
|
||||||
const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
|
|
||||||
const { sx } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
|
||||||
(state) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleClickPinOptionsPanel = () => {
|
|
||||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IAIIconButton
|
|
||||||
{...props}
|
|
||||||
tooltip={t('common.pinOptionsPanel')}
|
|
||||||
aria-label={t('common.pinOptionsPanel')}
|
|
||||||
onClick={handleClickPinOptionsPanel}
|
|
||||||
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
sx={{
|
|
||||||
color: 'base.500',
|
|
||||||
_hover: {
|
|
||||||
color: 'base.600',
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
color: 'base.700',
|
|
||||||
},
|
|
||||||
_dark: {
|
|
||||||
color: 'base.500',
|
|
||||||
_hover: {
|
|
||||||
color: 'base.400',
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
color: 'base.300',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(PinParametersPanelButton);
|
|
@ -1,196 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
chakra,
|
|
||||||
ChakraProps,
|
|
||||||
Slide,
|
|
||||||
useOutsideClick,
|
|
||||||
useTheme,
|
|
||||||
SlideDirection,
|
|
||||||
useColorMode,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
Resizable,
|
|
||||||
ResizableProps,
|
|
||||||
ResizeCallback,
|
|
||||||
ResizeStartCallback,
|
|
||||||
} from 're-resizable';
|
|
||||||
import { ReactNode, memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { LangDirection } from './types';
|
|
||||||
import {
|
|
||||||
getHandleEnables,
|
|
||||||
getMinMaxDimensions,
|
|
||||||
getSlideDirection,
|
|
||||||
getStyles,
|
|
||||||
} from './util';
|
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
|
|
||||||
type ResizableDrawerProps = ResizableProps & {
|
|
||||||
children: ReactNode;
|
|
||||||
isResizable: boolean;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
direction?: SlideDirection;
|
|
||||||
initialWidth?: number;
|
|
||||||
minWidth?: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
initialHeight?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
onResizeStart?: ResizeStartCallback;
|
|
||||||
onResizeStop?: ResizeCallback;
|
|
||||||
onResize?: ResizeCallback;
|
|
||||||
handleWidth?: string | number;
|
|
||||||
handleInteractWidth?: string | number;
|
|
||||||
sx?: ChakraProps['sx'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChakraResizeable = chakra(Resizable, {
|
|
||||||
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ResizableDrawer = ({
|
|
||||||
direction = 'left',
|
|
||||||
isResizable,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
initialWidth,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
initialHeight,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
onResizeStart,
|
|
||||||
onResizeStop,
|
|
||||||
onResize,
|
|
||||||
sx = {},
|
|
||||||
}: ResizableDrawerProps) => {
|
|
||||||
const langDirection = useTheme().direction as LangDirection;
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const outsideClickRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const defaultWidth = useMemo(
|
|
||||||
() =>
|
|
||||||
initialWidth ??
|
|
||||||
minWidth ??
|
|
||||||
(['left', 'right'].includes(direction) ? 'auto' : '100%'),
|
|
||||||
[initialWidth, minWidth, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultHeight = useMemo(
|
|
||||||
() =>
|
|
||||||
initialHeight ??
|
|
||||||
minHeight ??
|
|
||||||
(['top', 'bottom'].includes(direction) ? 'auto' : '100%'),
|
|
||||||
[initialHeight, minHeight, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [width, setWidth] = useState<number | string>(defaultWidth);
|
|
||||||
|
|
||||||
const [height, setHeight] = useState<number | string>(defaultHeight);
|
|
||||||
|
|
||||||
useOutsideClick({
|
|
||||||
ref: outsideClickRef,
|
|
||||||
handler: () => {
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
enabled: isOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleEnables = useMemo(
|
|
||||||
() => (isResizable ? getHandleEnables({ direction, langDirection }) : {}),
|
|
||||||
[isResizable, langDirection, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const minMaxDimensions = useMemo(
|
|
||||||
() =>
|
|
||||||
getMinMaxDimensions({
|
|
||||||
direction,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
}),
|
|
||||||
[minWidth, maxWidth, minHeight, maxHeight, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { containerStyles, handleStyles } = useMemo(
|
|
||||||
() =>
|
|
||||||
getStyles({
|
|
||||||
isResizable,
|
|
||||||
direction,
|
|
||||||
}),
|
|
||||||
[isResizable, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const slideDirection = useMemo(
|
|
||||||
() => getSlideDirection(direction, langDirection),
|
|
||||||
[direction, langDirection]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (['left', 'right'].includes(direction)) {
|
|
||||||
setHeight('100vh');
|
|
||||||
// setHeight(isPinned ? '100%' : '100vh');
|
|
||||||
}
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
setWidth('100vw');
|
|
||||||
// setWidth(isPinned ? '100%' : '100vw');
|
|
||||||
}
|
|
||||||
}, [direction]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slide
|
|
||||||
direction={slideDirection}
|
|
||||||
in={isOpen}
|
|
||||||
motionProps={{ initial: false }}
|
|
||||||
style={{ width: 'full' }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
ref={outsideClickRef}
|
|
||||||
sx={{
|
|
||||||
width: 'full',
|
|
||||||
height: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChakraResizeable
|
|
||||||
size={{
|
|
||||||
width: isResizable ? width : defaultWidth,
|
|
||||||
height: isResizable ? height : defaultHeight,
|
|
||||||
}}
|
|
||||||
enable={handleEnables}
|
|
||||||
handleStyles={handleStyles}
|
|
||||||
{...minMaxDimensions}
|
|
||||||
sx={{
|
|
||||||
borderColor: mode('base.200', 'base.800')(colorMode),
|
|
||||||
p: 4,
|
|
||||||
bg: mode('base.50', 'base.900')(colorMode),
|
|
||||||
height: 'full',
|
|
||||||
shadow: isOpen ? 'dark-lg' : undefined,
|
|
||||||
...containerStyles,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
onResizeStart={(event, direction, elementRef) => {
|
|
||||||
onResizeStart && onResizeStart(event, direction, elementRef);
|
|
||||||
}}
|
|
||||||
onResize={(event, direction, elementRef, delta) => {
|
|
||||||
onResize && onResize(event, direction, elementRef, delta);
|
|
||||||
}}
|
|
||||||
onResizeStop={(event, direction, elementRef, delta) => {
|
|
||||||
if (['left', 'right'].includes(direction)) {
|
|
||||||
setWidth(Number(width) + delta.width);
|
|
||||||
}
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
setHeight(Number(height) + delta.height);
|
|
||||||
}
|
|
||||||
onResizeStop && onResizeStop(event, direction, elementRef, delta);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ChakraResizeable>
|
|
||||||
</Box>
|
|
||||||
</Slide>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ResizableDrawer);
|
|
@ -1,2 +0,0 @@
|
|||||||
export type Placement = 'top' | 'right' | 'bottom' | 'left';
|
|
||||||
export type LangDirection = 'ltr' | 'rtl' | undefined;
|
|
@ -1,283 +0,0 @@
|
|||||||
import { SlideDirection } from '@chakra-ui/react';
|
|
||||||
import { AnimationProps } from 'framer-motion';
|
|
||||||
import { HandleStyles } from 're-resizable';
|
|
||||||
import { CSSProperties } from 'react';
|
|
||||||
import { LangDirection } from './types';
|
|
||||||
|
|
||||||
export type GetHandleEnablesOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
langDirection: LangDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine handles to enable. `re-resizable` doesn't handle RTL, so we have to do that here.
|
|
||||||
*/
|
|
||||||
export const getHandleEnables = ({
|
|
||||||
direction,
|
|
||||||
langDirection,
|
|
||||||
}: GetHandleEnablesOptions) => {
|
|
||||||
const top = direction === 'bottom';
|
|
||||||
|
|
||||||
const right =
|
|
||||||
(langDirection !== 'rtl' && direction === 'left') ||
|
|
||||||
(langDirection === 'rtl' && direction === 'right');
|
|
||||||
|
|
||||||
const bottom = direction === 'top';
|
|
||||||
|
|
||||||
const left =
|
|
||||||
(langDirection !== 'rtl' && direction === 'right') ||
|
|
||||||
(langDirection === 'rtl' && direction === 'left');
|
|
||||||
|
|
||||||
return {
|
|
||||||
top,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
left,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetDefaultSizeOptions = {
|
|
||||||
initialWidth?: string | number;
|
|
||||||
initialHeight?: string | number;
|
|
||||||
direction: SlideDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get default sizes based on direction and initial values
|
|
||||||
export const getDefaultSize = ({
|
|
||||||
initialWidth,
|
|
||||||
initialHeight,
|
|
||||||
direction,
|
|
||||||
}: GetDefaultSizeOptions) => {
|
|
||||||
const width =
|
|
||||||
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
|
|
||||||
|
|
||||||
const height =
|
|
||||||
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetMinMaxDimensionsOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
minWidth?: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the min/max width/height based on direction and provided values
|
|
||||||
export const getMinMaxDimensions = ({
|
|
||||||
direction,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
}: GetMinMaxDimensionsOptions) => {
|
|
||||||
const minW =
|
|
||||||
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
|
|
||||||
|
|
||||||
const maxW =
|
|
||||||
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
|
|
||||||
|
|
||||||
const minH =
|
|
||||||
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
|
|
||||||
|
|
||||||
const maxH =
|
|
||||||
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(minW ? { minWidth: minW } : {}),
|
|
||||||
...(maxW ? { maxWidth: maxW } : {}),
|
|
||||||
...(minH ? { minHeight: minH } : {}),
|
|
||||||
...(maxH ? { maxHeight: maxH } : {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAnimationsOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
langDirection: LangDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the framer-motion animation props, taking into account language direction
|
|
||||||
export const getAnimations = ({
|
|
||||||
direction,
|
|
||||||
langDirection,
|
|
||||||
}: GetAnimationsOptions): AnimationProps => {
|
|
||||||
const baseAnimation = {
|
|
||||||
initial: { opacity: 0 },
|
|
||||||
animate: { opacity: 1 },
|
|
||||||
exit: { opacity: 0 },
|
|
||||||
// chakra consumes the transition prop, which, for it, is a string.
|
|
||||||
// however we know the transition prop will make it to framer motion,
|
|
||||||
// which wants it as an object. cast as string to satisfy TS.
|
|
||||||
transition: { duration: 0.2, ease: 'easeInOut' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
|
|
||||||
|
|
||||||
if (direction === 'top') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { y: -999 },
|
|
||||||
animate: { y: 0 },
|
|
||||||
exit: { y: -999 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { x: 999 * langDirectionFactor },
|
|
||||||
animate: { x: 0 },
|
|
||||||
exit: { x: 999 * langDirectionFactor },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { y: 999 },
|
|
||||||
animate: { y: 0 },
|
|
||||||
exit: { y: 999 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { x: -999 * langDirectionFactor },
|
|
||||||
animate: { x: 0 },
|
|
||||||
exit: { x: -999 * langDirectionFactor },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetResizableStylesProps = {
|
|
||||||
isResizable: boolean;
|
|
||||||
direction: SlideDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expand the handle hitbox
|
|
||||||
const HANDLE_INTERACT_PADDING = '0.75rem';
|
|
||||||
|
|
||||||
// Visible padding around handle
|
|
||||||
const HANDLE_PADDING = '1rem';
|
|
||||||
const HANDLE_WIDTH = '5px';
|
|
||||||
|
|
||||||
// Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS
|
|
||||||
export const getStyles = ({
|
|
||||||
isResizable,
|
|
||||||
direction,
|
|
||||||
}: GetResizableStylesProps): {
|
|
||||||
containerStyles: CSSProperties; // technically this could be ChakraProps['sx'], but we cannot use this for HandleStyles so leave it as CSSProperties to be consistent
|
|
||||||
handleStyles: HandleStyles;
|
|
||||||
} => {
|
|
||||||
// if (!isResizable) {
|
|
||||||
// return { containerStyles: {}, handleStyles: {} };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Calculate the positioning offset of the handle hitbox so it is centered over the handle
|
|
||||||
const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${HANDLE_WIDTH}) / -2)`;
|
|
||||||
|
|
||||||
if (direction === 'top') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderBottomWidth: HANDLE_WIDTH,
|
|
||||||
paddingBottom: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
top: {
|
|
||||||
paddingTop: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
|
||||||
bottom: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderInlineEndWidth: HANDLE_WIDTH,
|
|
||||||
paddingInlineEnd: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
right: {
|
|
||||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
|
||||||
insetInlineEnd: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderTopWidth: HANDLE_WIDTH,
|
|
||||||
paddingTop: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
bottom: {
|
|
||||||
paddingTop: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
|
||||||
top: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderInlineStartWidth: HANDLE_WIDTH,
|
|
||||||
paddingInlineStart: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
left: {
|
|
||||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
|
||||||
insetInlineStart: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { containerStyles: {}, handleStyles: {} };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chakra's Slide does not handle langDirection, so we need to do it here
|
|
||||||
export const getSlideDirection = (
|
|
||||||
direction: SlideDirection,
|
|
||||||
langDirection: LangDirection
|
|
||||||
) => {
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
return direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
if (langDirection === 'rtl') {
|
|
||||||
return 'right';
|
|
||||||
}
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
if (langDirection === 'rtl') {
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
return 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'left';
|
|
||||||
};
|
|
@ -1,48 +1,36 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import InitialImageDisplay from 'features/parameters/components/Parameters/ImageToImage/InitialImageDisplay';
|
import InitialImageDisplay from 'features/parameters/components/Parameters/ImageToImage/InitialImageDisplay';
|
||||||
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ImperativePanelGroupHandle,
|
ImperativePanelGroupHandle,
|
||||||
Panel,
|
Panel,
|
||||||
PanelGroup,
|
PanelGroup,
|
||||||
} from 'react-resizable-panels';
|
} from 'react-resizable-panels';
|
||||||
import ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
|
|
||||||
import ResizeHandle from '../ResizeHandle';
|
import ResizeHandle from '../ResizeHandle';
|
||||||
import TextToImageTabMain from '../TextToImage/TextToImageTabMain';
|
import TextToImageTabMain from '../TextToImage/TextToImageTabMain';
|
||||||
import ImageToImageTabParameters from './ImageToImageTabParameters';
|
|
||||||
|
|
||||||
const ImageToImageTab = () => {
|
const ImageToImageTab = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
|
||||||
|
|
||||||
const handleDoubleClickHandle = useCallback(() => {
|
const handleDoubleClickHandle = useCallback(() => {
|
||||||
if (!panelGroupRef.current) {
|
if (!panelGroupRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
panelGroupRef.current.setLayout([50, 50]);
|
panelGroupRef.current.setLayout([50, 50]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
|
|
||||||
<ParametersPinnedWrapper>
|
|
||||||
{model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLImageToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<ImageToImageTabParameters />
|
|
||||||
)}
|
|
||||||
</ParametersPinnedWrapper>
|
|
||||||
<Box sx={{ w: 'full', h: 'full' }}>
|
<Box sx={{ w: 'full', h: 'full' }}>
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
ref={panelGroupRef}
|
ref={panelGroupRef}
|
||||||
autoSaveId="imageTab.content"
|
autoSaveId="imageTab.content"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
|
units="percentages"
|
||||||
>
|
>
|
||||||
<Panel
|
<Panel
|
||||||
id="imageTab.content.initImage"
|
id="imageTab.content.initImage"
|
||||||
@ -59,15 +47,11 @@ const ImageToImageTab = () => {
|
|||||||
order={1}
|
order={1}
|
||||||
defaultSize={50}
|
defaultSize={50}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
onResize={() => {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TextToImageTabMain />
|
<TextToImageTabMain />
|
||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user