Merge branch 'main' into maryhipp/informational-popover

This commit is contained in:
chainchompa 2023-09-15 13:12:25 -04:00 committed by GitHub
commit 7bf7c16a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 2230 additions and 1047 deletions

View File

@ -34,12 +34,9 @@ body:
id: whatisexpected id: whatisexpected
attributes: attributes:
label: What should this feature add? label: What should this feature add?
description: Please try to explain the functionality this feature should add description: Explain the functionality this feature should add. Feature requests should be for single features. Please create multiple requests if you want to request multiple features.
placeholder: | placeholder: |
Instead of one huge text field, it would be nice to have forms for bug-reports, feature-requests, ... I'd like a button that creates an image of banana sushi every time I press it. Each image should be different. There should be a toggle next to the button that enables strawberry mode, in which the images are of strawberry sushi instead.
Great benefits with automatic labeling, assigning and other functionalitys not available in that form
via old-fashioned markdown-templates. I would also love to see the use of a moderator bot 🤖 like
https://github.com/marketplace/actions/issue-moderator-with-commands to auto close old issues and other things
validations: validations:
required: true required: true

View File

@ -22,6 +22,7 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|Divide Integers | Divides two numbers| |Divide Integers | Divides two numbers|
|Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator| |Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator|
|Upscale (RealESRGAN) | Upscales an image using RealESRGAN.| |Upscale (RealESRGAN) | Upscales an image using RealESRGAN.|
|Float Math | Perform basic math operations on two floats|
|Float Primitive Collection | A collection of float primitive values| |Float Primitive Collection | A collection of float primitive values|
|Float Primitive | A float primitive value| |Float Primitive | A float primitive value|
|Float Range | Creates a range| |Float Range | Creates a range|
@ -29,6 +30,7 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|Blur Image | Blurs an image| |Blur Image | Blurs an image|
|Extract Image Channel | Gets a channel from an image.| |Extract Image Channel | Gets a channel from an image.|
|Image Primitive Collection | A collection of image primitive values| |Image Primitive Collection | A collection of image primitive values|
|Integer Math | Perform basic math operations on two integers|
|Convert Image Mode | Converts an image to a different mode.| |Convert Image Mode | Converts an image to a different mode.|
|Crop Image | Crops an image to a specified box. The box can be outside of the image.| |Crop Image | Crops an image to a specified box. The box can be outside of the image.|
|Image Hue Adjustment | Adjusts the Hue of an image.| |Image Hue Adjustment | Adjusts the Hue of an image.|
@ -42,6 +44,8 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|Paste Image | Pastes an image into another image.| |Paste Image | Pastes an image into another image.|
|ImageProcessor | Base class for invocations that preprocess images for ControlNet| |ImageProcessor | Base class for invocations that preprocess images for ControlNet|
|Resize Image | Resizes an image to specific dimensions| |Resize Image | Resizes an image to specific dimensions|
|Round Float | Rounds a float to a specified number of decimal places|
|Float to Integer | Converts a float to an integer. Optionally rounds to an even multiple of a input number.|
|Scale Image | Scales an image by a factor| |Scale Image | Scales an image by a factor|
|Image to Latents | Encodes an image into latents.| |Image to Latents | Encodes an image into latents.|
|Add Invisible Watermark | Add an invisible watermark to an image| |Add Invisible Watermark | Add an invisible watermark to an image|

View File

@ -14,7 +14,7 @@ fi
VERSION=$(cd ..; python -c "from invokeai.version import __version__ as version; print(version)") VERSION=$(cd ..; python -c "from invokeai.version import __version__ as version; print(version)")
PATCH="" PATCH=""
VERSION="v${VERSION}${PATCH}" VERSION="v${VERSION}${PATCH}"
LATEST_TAG="v3.0-latest" LATEST_TAG="v3-latest"
echo Building installer for version $VERSION echo Building installer for version $VERSION
echo "Be certain that you're in the 'installer' directory before continuing." echo "Be certain that you're in the 'installer' directory before continuing."

View File

@ -198,6 +198,7 @@ class _InputField(BaseModel):
ui_type: Optional[UIType] ui_type: Optional[UIType]
ui_component: Optional[UIComponent] ui_component: Optional[UIComponent]
ui_order: Optional[int] ui_order: Optional[int]
ui_choice_labels: Optional[dict[str, str]]
item_default: Optional[Any] item_default: Optional[Any]
@ -246,6 +247,7 @@ def InputField(
ui_component: Optional[UIComponent] = None, ui_component: Optional[UIComponent] = None,
ui_hidden: bool = False, ui_hidden: bool = False,
ui_order: Optional[int] = None, ui_order: Optional[int] = None,
ui_choice_labels: Optional[dict[str, str]] = None,
item_default: Optional[Any] = None, item_default: Optional[Any] = None,
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
@ -312,6 +314,7 @@ def InputField(
ui_hidden=ui_hidden, ui_hidden=ui_hidden,
ui_order=ui_order, ui_order=ui_order,
item_default=item_default, item_default=item_default,
ui_choice_labels=ui_choice_labels,
**kwargs, **kwargs,
) )

View File

@ -38,14 +38,16 @@ class RangeInvocation(BaseInvocation):
version="1.0.0", version="1.0.0",
) )
class RangeOfSizeInvocation(BaseInvocation): class RangeOfSizeInvocation(BaseInvocation):
"""Creates a range from start to start + size with step""" """Creates a range from start to start + (size * step) incremented by step"""
start: int = InputField(default=0, description="The start of the range") start: int = InputField(default=0, description="The start of the range")
size: int = InputField(default=1, description="The number of values") size: int = InputField(default=1, gt=0, description="The number of values")
step: int = InputField(default=1, description="The step of the range") step: int = InputField(default=1, description="The step of the range")
def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
return IntegerCollectionOutput(collection=list(range(self.start, self.start + self.size, self.step))) return IntegerCollectionOutput(
collection=list(range(self.start, self.start + (self.step * self.size), self.step))
)
@invocation( @invocation(

View File

@ -98,7 +98,7 @@ class ImageCropInvocation(BaseInvocation):
) )
@invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.0") @invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.1")
class ImagePasteInvocation(BaseInvocation): class ImagePasteInvocation(BaseInvocation):
"""Pastes an image into another image.""" """Pastes an image into another image."""
@ -110,6 +110,7 @@ class ImagePasteInvocation(BaseInvocation):
) )
x: int = InputField(default=0, description="The left x coordinate at which to paste the image") x: int = InputField(default=0, description="The left x coordinate at which to paste the image")
y: int = InputField(default=0, description="The top y coordinate at which to paste the image") y: int = InputField(default=0, description="The top y coordinate at which to paste the image")
crop: bool = InputField(default=False, description="Crop to base image dimensions")
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
base_image = context.services.images.get_pil_image(self.base_image.image_name) base_image = context.services.images.get_pil_image(self.base_image.image_name)
@ -129,6 +130,10 @@ class ImagePasteInvocation(BaseInvocation):
new_image.paste(base_image, (abs(min_x), abs(min_y))) new_image.paste(base_image, (abs(min_x), abs(min_y)))
new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask)
if self.crop:
base_w, base_h = base_image.size
new_image = new_image.crop((abs(min_x), abs(min_y), abs(min_x) + base_w, abs(min_y) + base_h))
image_dto = context.services.images.create( image_dto = context.services.images.create(
image=new_image, image=new_image,
image_origin=ResourceOrigin.INTERNAL, image_origin=ResourceOrigin.INTERNAL,
@ -330,8 +335,8 @@ class ImageResizeInvocation(BaseInvocation):
"""Resizes an image to specific dimensions""" """Resizes an image to specific dimensions"""
image: ImageField = InputField(description="The image to resize") image: ImageField = InputField(description="The image to resize")
width: int = InputField(default=512, ge=64, multiple_of=8, description="The width to resize to (px)") width: int = InputField(default=512, gt=0, description="The width to resize to (px)")
height: int = InputField(default=512, ge=64, multiple_of=8, description="The height to resize to (px)") height: int = InputField(default=512, gt=0, description="The height to resize to (px)")
resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode")
metadata: Optional[CoreMetadata] = InputField( metadata: Optional[CoreMetadata] = InputField(
default=None, description=FieldDescriptions.core_metadata, ui_hidden=True default=None, description=FieldDescriptions.core_metadata, ui_hidden=True

View File

@ -63,6 +63,9 @@ from .compel import ConditioningField
from .controlnet_image_processors import ControlField from .controlnet_image_processors import ControlField
from .model import ModelInfo, UNetField, VaeField from .model import ModelInfo, UNetField, VaeField
if choose_torch_device() == torch.device("mps"):
from torch import mps
DEFAULT_PRECISION = choose_precision(choose_torch_device()) DEFAULT_PRECISION = choose_precision(choose_torch_device())
@ -541,6 +544,8 @@ class DenoiseLatentsInvocation(BaseInvocation):
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
result_latents = result_latents.to("cpu") result_latents = result_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
context.services.latents.save(name, result_latents) context.services.latents.save(name, result_latents)
@ -612,6 +617,8 @@ class LatentsToImageInvocation(BaseInvocation):
# clear memory as vae decode can request a lot # clear memory as vae decode can request a lot
torch.cuda.empty_cache() torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
with torch.inference_mode(): with torch.inference_mode():
# copied from diffusers pipeline # copied from diffusers pipeline
@ -624,6 +631,8 @@ class LatentsToImageInvocation(BaseInvocation):
image = VaeImageProcessor.numpy_to_pil(np_image)[0] image = VaeImageProcessor.numpy_to_pil(np_image)[0]
torch.cuda.empty_cache() torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
image_dto = context.services.images.create( image_dto = context.services.images.create(
image=image, image=image,
@ -683,6 +692,8 @@ class ResizeLatentsInvocation(BaseInvocation):
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
resized_latents = resized_latents.to("cpu") resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
# context.services.latents.set(name, resized_latents) # context.services.latents.set(name, resized_latents)
@ -719,6 +730,8 @@ class ScaleLatentsInvocation(BaseInvocation):
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
resized_latents = resized_latents.to("cpu") resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
# context.services.latents.set(name, resized_latents) # context.services.latents.set(name, resized_latents)
@ -875,6 +888,8 @@ class BlendLatentsInvocation(BaseInvocation):
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
blended_latents = blended_latents.to("cpu") blended_latents = blended_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
# context.services.latents.set(name, resized_latents) # context.services.latents.set(name, resized_latents)

View File

@ -1,8 +1,11 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
import numpy as np from typing import Literal
from invokeai.app.invocations.primitives import IntegerOutput import numpy as np
from pydantic import validator
from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput
from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation
@ -60,3 +63,201 @@ class RandomIntInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> IntegerOutput: def invoke(self, context: InvocationContext) -> IntegerOutput:
return IntegerOutput(value=np.random.randint(self.low, self.high)) return IntegerOutput(value=np.random.randint(self.low, self.high))
@invocation(
"float_to_int",
title="Float To Integer",
tags=["math", "round", "integer", "float", "convert"],
category="math",
version="1.0.0",
)
class FloatToIntegerInvocation(BaseInvocation):
"""Rounds a float number to (a multiple of) an integer."""
value: float = InputField(default=0, description="The value to round")
multiple: int = InputField(default=1, ge=1, title="Multiple of", description="The multiple to round to")
method: Literal["Nearest", "Floor", "Ceiling", "Truncate"] = InputField(
default="Nearest", description="The method to use for rounding"
)
def invoke(self, context: InvocationContext) -> IntegerOutput:
if self.method == "Nearest":
return IntegerOutput(value=round(self.value / self.multiple) * self.multiple)
elif self.method == "Floor":
return IntegerOutput(value=np.floor(self.value / self.multiple) * self.multiple)
elif self.method == "Ceiling":
return IntegerOutput(value=np.ceil(self.value / self.multiple) * self.multiple)
else: # self.method == "Truncate"
return IntegerOutput(value=int(self.value / self.multiple) * self.multiple)
@invocation("round_float", title="Round Float", tags=["math", "round"], category="math", version="1.0.0")
class RoundInvocation(BaseInvocation):
"""Rounds a float to a specified number of decimal places."""
value: float = InputField(default=0, description="The float value")
decimals: int = InputField(default=0, description="The number of decimal places")
def invoke(self, context: InvocationContext) -> FloatOutput:
return FloatOutput(value=round(self.value, self.decimals))
INTEGER_OPERATIONS = Literal[
"ADD",
"SUB",
"MUL",
"DIV",
"EXP",
"MOD",
"ABS",
"MIN",
"MAX",
]
INTEGER_OPERATIONS_LABELS = dict(
ADD="Add A+B",
SUB="Subtract A-B",
MUL="Multiply A*B",
DIV="Divide A/B",
EXP="Exponentiate A^B",
MOD="Modulus A%B",
ABS="Absolute Value of A",
MIN="Minimum(A,B)",
MAX="Maximum(A,B)",
)
@invocation(
"integer_math",
title="Integer Math",
tags=[
"math",
"integer",
"add",
"subtract",
"multiply",
"divide",
"modulus",
"power",
"absolute value",
"min",
"max",
],
category="math",
version="1.0.0",
)
class IntegerMathInvocation(BaseInvocation):
"""Performs integer math."""
operation: INTEGER_OPERATIONS = InputField(
default="ADD", description="The operation to perform", ui_choice_labels=INTEGER_OPERATIONS_LABELS
)
a: int = InputField(default=0, description=FieldDescriptions.num_1)
b: int = InputField(default=0, description=FieldDescriptions.num_2)
@validator("b")
def no_unrepresentable_results(cls, v, values):
if values["operation"] == "DIV" and v == 0:
raise ValueError("Cannot divide by zero")
elif values["operation"] == "MOD" and v == 0:
raise ValueError("Cannot divide by zero")
elif values["operation"] == "EXP" and v < 0:
raise ValueError("Result of exponentiation is not an integer")
return v
def invoke(self, context: InvocationContext) -> IntegerOutput:
# Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9
if self.operation == "ADD":
return IntegerOutput(value=self.a + self.b)
elif self.operation == "SUB":
return IntegerOutput(value=self.a - self.b)
elif self.operation == "MUL":
return IntegerOutput(value=self.a * self.b)
elif self.operation == "DIV":
return IntegerOutput(value=int(self.a / self.b))
elif self.operation == "EXP":
return IntegerOutput(value=self.a**self.b)
elif self.operation == "MOD":
return IntegerOutput(value=self.a % self.b)
elif self.operation == "ABS":
return IntegerOutput(value=abs(self.a))
elif self.operation == "MIN":
return IntegerOutput(value=min(self.a, self.b))
else: # self.operation == "MAX":
return IntegerOutput(value=max(self.a, self.b))
FLOAT_OPERATIONS = Literal[
"ADD",
"SUB",
"MUL",
"DIV",
"EXP",
"ABS",
"SQRT",
"MIN",
"MAX",
]
FLOAT_OPERATIONS_LABELS = dict(
ADD="Add A+B",
SUB="Subtract A-B",
MUL="Multiply A*B",
DIV="Divide A/B",
EXP="Exponentiate A^B",
ABS="Absolute Value of A",
SQRT="Square Root of A",
MIN="Minimum(A,B)",
MAX="Maximum(A,B)",
)
@invocation(
"float_math",
title="Float Math",
tags=["math", "float", "add", "subtract", "multiply", "divide", "power", "root", "absolute value", "min", "max"],
category="math",
version="1.0.0",
)
class FloatMathInvocation(BaseInvocation):
"""Performs floating point math."""
operation: FLOAT_OPERATIONS = InputField(
default="ADD", description="The operation to perform", ui_choice_labels=FLOAT_OPERATIONS_LABELS
)
a: float = InputField(default=0, description=FieldDescriptions.num_1)
b: float = InputField(default=0, description=FieldDescriptions.num_2)
@validator("b")
def no_unrepresentable_results(cls, v, values):
if values["operation"] == "DIV" and v == 0:
raise ValueError("Cannot divide by zero")
elif values["operation"] == "EXP" and values["a"] == 0 and v < 0:
raise ValueError("Cannot raise zero to a negative power")
elif values["operation"] == "EXP" and type(values["a"] ** v) is complex:
raise ValueError("Root operation resulted in a complex number")
return v
def invoke(self, context: InvocationContext) -> FloatOutput:
# Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9
if self.operation == "ADD":
return FloatOutput(value=self.a + self.b)
elif self.operation == "SUB":
return FloatOutput(value=self.a - self.b)
elif self.operation == "MUL":
return FloatOutput(value=self.a * self.b)
elif self.operation == "DIV":
return FloatOutput(value=self.a / self.b)
elif self.operation == "EXP":
return FloatOutput(value=self.a**self.b)
elif self.operation == "SQRT":
return FloatOutput(value=np.sqrt(self.a))
elif self.operation == "ABS":
return FloatOutput(value=abs(self.a))
elif self.operation == "MIN":
return FloatOutput(value=min(self.a, self.b))
else: # self.operation == "MAX":
return FloatOutput(value=max(self.a, self.b))

View File

@ -0,0 +1,139 @@
# 2023 skunkworxdark (https://github.com/skunkworxdark)
import re
from .baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
InputField,
InvocationContext,
OutputField,
UIComponent,
invocation,
invocation_output,
)
from .primitives import StringOutput
@invocation_output("string_pos_neg_output")
class StringPosNegOutput(BaseInvocationOutput):
"""Base class for invocations that output a positive and negative string"""
positive_string: str = OutputField(description="Positive string")
negative_string: str = OutputField(description="Negative string")
@invocation(
"string_split_neg",
title="String Split Negative",
tags=["string", "split", "negative"],
category="string",
version="1.0.0",
)
class StringSplitNegInvocation(BaseInvocation):
"""Splits string into two strings, inside [] goes into negative string everthing else goes into positive string. Each [ and ] character is replaced with a space"""
string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea)
def invoke(self, context: InvocationContext) -> StringPosNegOutput:
p_string = ""
n_string = ""
brackets_depth = 0
escaped = False
for char in self.string or "":
if char == "[" and not escaped:
n_string += " "
brackets_depth += 1
elif char == "]" and not escaped:
brackets_depth -= 1
char = " "
elif brackets_depth > 0:
n_string += char
else:
p_string += char
# keep track of the escape char but only if it isn't escaped already
if char == "\\" and not escaped:
escaped = True
else:
escaped = False
return StringPosNegOutput(positive_string=p_string, negative_string=n_string)
@invocation_output("string_2_output")
class String2Output(BaseInvocationOutput):
"""Base class for invocations that output two strings"""
string_1: str = OutputField(description="string 1")
string_2: str = OutputField(description="string 2")
@invocation("string_split", title="String Split", tags=["string", "split"], category="string", version="1.0.0")
class StringSplitInvocation(BaseInvocation):
"""Splits string into two strings, based on the first occurance of the delimiter. The delimiter will be removed from the string"""
string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea)
delimiter: str = InputField(
default="", description="Delimiter to spilt with. blank will split on the first whitespace"
)
def invoke(self, context: InvocationContext) -> String2Output:
result = self.string.split(self.delimiter, 1)
if len(result) == 2:
part1, part2 = result
else:
part1 = result[0]
part2 = ""
return String2Output(string_1=part1, string_2=part2)
@invocation("string_join", title="String Join", tags=["string", "join"], category="string", version="1.0.0")
class StringJoinInvocation(BaseInvocation):
"""Joins string left to string right"""
string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea)
string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea)
def invoke(self, context: InvocationContext) -> StringOutput:
return StringOutput(value=((self.string_left or "") + (self.string_right or "")))
@invocation("string_join_three", title="String Join Three", tags=["string", "join"], category="string", version="1.0.0")
class StringJoinThreeInvocation(BaseInvocation):
"""Joins string left to string middle to string right"""
string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea)
string_middle: str = InputField(default="", description="String Middle", ui_component=UIComponent.Textarea)
string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea)
def invoke(self, context: InvocationContext) -> StringOutput:
return StringOutput(value=((self.string_left or "") + (self.string_middle or "") + (self.string_right or "")))
@invocation(
"string_replace", title="String Replace", tags=["string", "replace", "regex"], category="string", version="1.0.0"
)
class StringReplaceInvocation(BaseInvocation):
"""Replaces the search string with the replace string"""
string: str = InputField(default="", description="String to work on", ui_component=UIComponent.Textarea)
search_string: str = InputField(default="", description="String to search for", ui_component=UIComponent.Textarea)
replace_string: str = InputField(
default="", description="String to replace the search", ui_component=UIComponent.Textarea
)
use_regex: bool = InputField(
default=False, description="Use search string as a regex expression (non regex is case insensitive)"
)
def invoke(self, context: InvocationContext) -> StringOutput:
pattern = self.search_string or ""
new_string = self.string or ""
if len(pattern) > 0:
if not self.use_regex:
# None regex so make case insensitve
pattern = "(?i)" + re.escape(pattern)
new_string = re.sub(pattern, (self.replace_string or ""), new_string)
return StringOutput(value=new_string)

View File

@ -29,8 +29,12 @@ import torch
import invokeai.backend.util.logging as logger import invokeai.backend.util.logging as logger
from ..util.devices import choose_torch_device
from .models import BaseModelType, ModelBase, ModelType, SubModelType from .models import BaseModelType, ModelBase, ModelType, SubModelType
if choose_torch_device() == torch.device("mps"):
from torch import mps
# Maximum size of the cache, in gigs # Maximum size of the cache, in gigs
# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously # Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously
DEFAULT_MAX_CACHE_SIZE = 6.0 DEFAULT_MAX_CACHE_SIZE = 6.0
@ -406,6 +410,8 @@ class ModelCache(object):
gc.collect() gc.collect()
torch.cuda.empty_cache() torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
self.logger.debug(f"After unloading: cached_models={len(self._cached_models)}") self.logger.debug(f"After unloading: cached_models={len(self._cached_models)}")
@ -426,6 +432,8 @@ class ModelCache(object):
gc.collect() gc.collect()
torch.cuda.empty_cache() torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
def _local_model_hash(self, model_path: Union[str, Path]) -> str: def _local_model_hash(self, model_path: Union[str, Path]) -> str:
sha = hashlib.sha256() sha = hashlib.sha256()

View File

@ -772,11 +772,13 @@ diffusers.models.controlnet.ControlNetModel = ControlNetModel
# NOTE: with this patch, torch.compile crashes on 2.0 torch(already fixed in nightly) # NOTE: with this patch, torch.compile crashes on 2.0 torch(already fixed in nightly)
# https://github.com/huggingface/diffusers/pull/4315 # https://github.com/huggingface/diffusers/pull/4315
# https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/lora.py#L96C18-L96C18 # https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/lora.py#L96C18-L96C18
def new_LoRACompatibleConv_forward(self, x): def new_LoRACompatibleConv_forward(self, hidden_states, scale: float = 1.0):
if self.lora_layer is None: if self.lora_layer is None:
return super(diffusers.models.lora.LoRACompatibleConv, self).forward(x) return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states)
else: else:
return super(diffusers.models.lora.LoRACompatibleConv, self).forward(x) + self.lora_layer(x) return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states) + (
scale * self.lora_layer(hidden_states)
)
diffusers.models.lora.LoRACompatibleConv.forward = new_LoRACompatibleConv_forward diffusers.models.lora.LoRACompatibleConv.forward = new_LoRACompatibleConv_forward

File diff suppressed because it is too large Load Diff

View File

@ -12,29 +12,26 @@ import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs'; import InvokeTabs from 'features/ui/components/InvokeTabs';
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 { memo, useCallback, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { usePreselectedImage } from '../../features/parameters/hooks/usePreselectedImage'; import { usePreselectedImage } from '../../features/parameters/hooks/usePreselectedImage';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
import GlobalHotkeys from './GlobalHotkeys'; import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster'; import Toaster from './Toaster';
import { useStore } from '@nanostores/react';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
interface Props { interface Props {
config?: PartialAppConfig; config?: PartialAppConfig;
headerComponent?: ReactNode;
selectedImage?: { selectedImage?: {
imageName: string; imageName: string;
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
}; };
} }
const App = ({ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
config = DEFAULT_CONFIG,
headerComponent,
selectedImage,
}: Props) => {
const language = useAppSelector(languageSelector); const language = useAppSelector(languageSelector);
const logger = useLogger('system'); const logger = useLogger('system');
@ -65,6 +62,8 @@ const App = ({
handlePreselectedImage(selectedImage); handlePreselectedImage(selectedImage);
}, [handlePreselectedImage, selectedImage]); }, [handlePreselectedImage, selectedImage]);
const headerComponent = useStore($headerComponent);
return ( return (
<ErrorBoundary <ErrorBoundary
onReset={handleReset} onReset={handleReset}

View File

@ -15,6 +15,8 @@ import { socketMiddleware } from 'services/events/middleware';
import Loading from '../../common/components/Loading/Loading'; import Loading from '../../common/components/Loading/Loading';
import '../../i18n'; import '../../i18n';
import AppDndContext from '../../features/dnd/components/AppDndContext'; import AppDndContext from '../../features/dnd/components/AppDndContext';
import { $customStarUI, CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
const App = lazy(() => import('./App')); const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -30,6 +32,7 @@ interface Props extends PropsWithChildren {
imageName: string; imageName: string;
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
}; };
customStarUi?: CustomStarUi;
} }
const InvokeAIUI = ({ const InvokeAIUI = ({
@ -40,6 +43,7 @@ const InvokeAIUI = ({
middleware, middleware,
projectId, projectId,
selectedImage, selectedImage,
customStarUi,
}: Props) => { }: Props) => {
useEffect(() => { useEffect(() => {
// configure API client token // configure API client token
@ -80,17 +84,33 @@ const InvokeAIUI = ({
}; };
}, [apiUrl, token, middleware, projectId]); }, [apiUrl, token, middleware, projectId]);
useEffect(() => {
if (customStarUi) {
$customStarUI.set(customStarUi);
}
return () => {
$customStarUI.set(undefined);
};
}, [customStarUi]);
useEffect(() => {
if (headerComponent) {
$headerComponent.set(headerComponent);
}
return () => {
$headerComponent.set(undefined);
};
}, [headerComponent]);
return ( return (
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<React.Suspense fallback={<Loading />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<AppDndContext> <AppDndContext>
<App <App config={config} selectedImage={selectedImage} />
config={config}
headerComponent={headerComponent}
selectedImage={selectedImage}
/>
</AppDndContext> </AppDndContext>
</ThemeLocaleProvider> </ThemeLocaleProvider>
</React.Suspense> </React.Suspense>

View File

@ -0,0 +1,14 @@
import { MenuItemProps } from '@chakra-ui/react';
import { atom } from 'nanostores';
export type CustomStarUi = {
on: {
icon: MenuItemProps['icon'];
text: string;
};
off: {
icon: MenuItemProps['icon'];
text: string;
};
};
export const $customStarUI = atom<CustomStarUi | undefined>(undefined);

View File

@ -0,0 +1,4 @@
import { atom } from 'nanostores';
import { ReactNode } from 'react';
export const $headerComponent = atom<ReactNode | undefined>(undefined);

View File

@ -0,0 +1,3 @@
/**
* For non-serializable data that needs to be available throughout the app, or when redux is not appropriate, use nanostores.
*/

View File

@ -86,10 +86,7 @@ export const store = configureStore({
.concat(autoBatchEnhancer()); .concat(autoBatchEnhancer());
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({ immutableCheck: false })
immutableCheck: false,
serializableCheck: false,
})
.concat(api.middleware) .concat(api.middleware)
.concat(dynamicMiddlewares) .concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware), .prepend(listenerMiddleware.middleware),

View File

@ -6,6 +6,7 @@ import { isInvocationNode } from 'features/nodes/types/types';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { forEach, map } from 'lodash-es'; import { forEach, map } from 'lodash-es';
import { getConnectedEdges } from 'reactflow'; import { getConnectedEdges } from 'reactflow';
import i18n from 'i18next';
const selector = createSelector( const selector = createSelector(
[stateSelector, activeTabNameSelector], [stateSelector, activeTabNameSelector],
@ -19,22 +20,22 @@ const selector = createSelector(
// Cannot generate if already processing an image // Cannot generate if already processing an image
if (isProcessing) { if (isProcessing) {
reasons.push('System busy'); reasons.push(i18n.t('parameters.invoke.systemBusy'));
} }
// Cannot generate if not connected // Cannot generate if not connected
if (!isConnected) { if (!isConnected) {
reasons.push('System disconnected'); reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
} }
if (activeTabName === 'img2img' && !initialImage) { if (activeTabName === 'img2img' && !initialImage) {
reasons.push('No initial image selected'); reasons.push(i18n.t('parameters.invoke.noInitialImageSelected'));
} }
if (activeTabName === 'nodes') { if (activeTabName === 'nodes') {
if (nodes.shouldValidateGraph) { if (nodes.shouldValidateGraph) {
if (!nodes.nodes.length) { if (!nodes.nodes.length) {
reasons.push('No nodes in graph'); reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
} }
nodes.nodes.forEach((node) => { nodes.nodes.forEach((node) => {
@ -46,7 +47,7 @@ const selector = createSelector(
if (!nodeTemplate) { if (!nodeTemplate) {
// Node type not found // Node type not found
reasons.push('Missing node template'); reasons.push(i18n.t('parameters.invoke.missingNodeTemplate'));
return; return;
} }
@ -60,7 +61,7 @@ const selector = createSelector(
); );
if (!fieldTemplate) { if (!fieldTemplate) {
reasons.push('Missing field template'); reasons.push(i18n.t('parameters.invoke.missingFieldTemplate'));
return; return;
} }
@ -70,9 +71,10 @@ const selector = createSelector(
!hasConnection !hasConnection
) { ) {
reasons.push( reasons.push(
`${node.data.label || nodeTemplate.title} -> ${ i18n.t('parameters.invoke.missingInputForField', {
field.label || fieldTemplate.title nodeLabel: node.data.label || nodeTemplate.title,
} missing input` fieldLabel: field.label || fieldTemplate.title,
})
); );
return; return;
} }
@ -81,7 +83,7 @@ const selector = createSelector(
} }
} else { } else {
if (!model) { if (!model) {
reasons.push('No model selected'); reasons.push(i18n.t('parameters.invoke.noModelSelected'));
} }
if (state.controlNet.isEnabled) { if (state.controlNet.isEnabled) {
@ -90,7 +92,9 @@ const selector = createSelector(
return; return;
} }
if (!controlNet.model) { if (!controlNet.model) {
reasons.push(`ControlNet ${i + 1} has no model selected.`); reasons.push(
i18n.t('parameters.invoke.noModelForControlNet', { index: i + 1 })
);
} }
if ( if (
@ -98,7 +102,11 @@ const selector = createSelector(
(!controlNet.processedControlImage && (!controlNet.processedControlImage &&
controlNet.processorType !== 'none') controlNet.processorType !== 'none')
) { ) {
reasons.push(`ControlNet ${i + 1} has no control image`); reasons.push(
i18n.t('parameters.invoke.noControlImageForControlNet', {
index: i + 1,
})
);
} }
}); });
} }

View File

@ -154,6 +154,8 @@ const IAICanvas = () => {
resizeObserver.observe(containerRef.current); resizeObserver.observe(containerRef.current);
dispatch(canvasResized(containerRef.current.getBoundingClientRect()));
return () => { return () => {
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };

View File

@ -8,7 +8,7 @@ const calculateScale = (
const scaleX = (containerWidth * padding) / contentWidth; const scaleX = (containerWidth * padding) / contentWidth;
const scaleY = (containerHeight * padding) / contentHeight; const scaleY = (containerHeight * padding) / contentHeight;
const scaleFit = Math.min(1, Math.min(scaleX, scaleY)); const scaleFit = Math.min(1, Math.min(scaleX, scaleY));
return scaleFit; return scaleFit ? scaleFit : 1;
}; };
export default calculateScale; export default calculateScale;

View File

@ -21,6 +21,7 @@ import {
useRemoveImagesFromBoardMutation, useRemoveImagesFromBoardMutation,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import { changeBoardReset, isModalOpenChanged } from '../store/slice'; import { changeBoardReset, isModalOpenChanged } from '../store/slice';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -42,10 +43,11 @@ const ChangeBoardModal = () => {
const { imagesToChange, isModalOpen } = useAppSelector(selector); const { imagesToChange, isModalOpen } = useAppSelector(selector);
const [addImagesToBoard] = useAddImagesToBoardMutation(); const [addImagesToBoard] = useAddImagesToBoardMutation();
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation(); const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
const { t } = useTranslation();
const data = useMemo(() => { const data = useMemo(() => {
const data: { label: string; value: string }[] = [ const data: { label: string; value: string }[] = [
{ label: 'Uncategorized', value: 'none' }, { label: t('boards.uncategorized'), value: 'none' },
]; ];
(boards ?? []).forEach((board) => (boards ?? []).forEach((board) =>
data.push({ data.push({
@ -55,7 +57,7 @@ const ChangeBoardModal = () => {
); );
return data; return data;
}, [boards]); }, [boards, t]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
dispatch(changeBoardReset()); dispatch(changeBoardReset());
@ -97,7 +99,7 @@ const ChangeBoardModal = () => {
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold"> <AlertDialogHeader fontSize="lg" fontWeight="bold">
Change Board {t('boards.changeBoard')}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogBody> <AlertDialogBody>
@ -107,7 +109,9 @@ const ChangeBoardModal = () => {
{`${imagesToChange.length > 1 ? 's' : ''}`} to board: {`${imagesToChange.length > 1 ? 's' : ''}`} to board:
</Text> </Text>
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
placeholder={isFetching ? 'Loading...' : 'Select Board'} placeholder={
isFetching ? t('boards.loading') : t('boards.selectBoard')
}
disabled={isFetching} disabled={isFetching}
onChange={(v) => setSelectedBoard(v)} onChange={(v) => setSelectedBoard(v)}
value={selectedBoard} value={selectedBoard}
@ -117,10 +121,10 @@ const ChangeBoardModal = () => {
</AlertDialogBody> </AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={handleClose}> <IAIButton ref={cancelRef} onClick={handleClose}>
Cancel {t('boards.cancel')}
</IAIButton> </IAIButton>
<IAIButton colorScheme="accent" onClick={handleChangeBoard} ml={3}> <IAIButton colorScheme="accent" onClick={handleChangeBoard} ml={3}>
Move {t('boards.move')}
</IAIButton> </IAIButton>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View File

@ -28,6 +28,7 @@ import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
import ParamControlNetControlMode from './parameters/ParamControlNetControlMode'; import ParamControlNetControlMode from './parameters/ParamControlNetControlMode';
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
import ParamControlNetResizeMode from './parameters/ParamControlNetResizeMode'; import ParamControlNetResizeMode from './parameters/ParamControlNetResizeMode';
import { useTranslation } from 'react-i18next';
type ControlNetProps = { type ControlNetProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -37,6 +38,7 @@ const ControlNet = (props: ControlNetProps) => {
const { controlNet } = props; const { controlNet } = props;
const { controlNetId } = controlNet; const { controlNetId } = controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
@ -95,8 +97,8 @@ const ControlNet = (props: ControlNetProps) => {
> >
<Flex sx={{ gap: 2, alignItems: 'center' }}> <Flex sx={{ gap: 2, alignItems: 'center' }}>
<IAISwitch <IAISwitch
tooltip="Toggle this ControlNet" tooltip={t('controlnet.toggleControlNet')}
aria-label="Toggle this ControlNet" aria-label={t('controlnet.toggleControlNet')}
isChecked={isEnabled} isChecked={isEnabled}
onChange={handleToggleIsEnabled} onChange={handleToggleIsEnabled}
/> />
@ -117,23 +119,31 @@ const ControlNet = (props: ControlNetProps) => {
)} )}
<IAIIconButton <IAIIconButton
size="sm" size="sm"
tooltip="Duplicate" tooltip={t('controlnet.duplicate')}
aria-label="Duplicate" aria-label={t('controlnet.duplicate')}
onClick={handleDuplicate} onClick={handleDuplicate}
icon={<FaCopy />} icon={<FaCopy />}
/> />
<IAIIconButton <IAIIconButton
size="sm" size="sm"
tooltip="Delete" tooltip={t('controlnet.delete')}
aria-label="Delete" aria-label={t('controlnet.delete')}
colorScheme="error" colorScheme="error"
onClick={handleDelete} onClick={handleDelete}
icon={<FaTrash />} icon={<FaTrash />}
/> />
<IAIIconButton <IAIIconButton
size="sm" size="sm"
tooltip={isExpanded ? 'Hide Advanced' : 'Show Advanced'} tooltip={
aria-label={isExpanded ? 'Hide Advanced' : 'Show Advanced'} isExpanded
? t('controlnet.hideAdvanced')
: t('controlnet.showAdvanced')
}
aria-label={
isExpanded
? t('controlnet.hideAdvanced')
: t('controlnet.showAdvanced')
}
onClick={toggleIsExpanded} onClick={toggleIsExpanded}
variant="ghost" variant="ghost"
sx={{ sx={{

View File

@ -26,6 +26,7 @@ import {
ControlNetConfig, ControlNetConfig,
controlNetImageChanged, controlNetImageChanged,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -56,6 +57,7 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
} = controlNet; } = controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const { pendingControlImages, autoAddBoardId } = useAppSelector(selector); const { pendingControlImages, autoAddBoardId } = useAppSelector(selector);
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
@ -208,18 +210,18 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
<IAIDndImageIcon <IAIDndImageIcon
onClick={handleResetControlImage} onClick={handleResetControlImage}
icon={controlImage ? <FaUndo /> : undefined} icon={controlImage ? <FaUndo /> : undefined}
tooltip="Reset Control Image" tooltip={t('controlnet.resetControlImage')}
/> />
<IAIDndImageIcon <IAIDndImageIcon
onClick={handleSaveControlImage} onClick={handleSaveControlImage}
icon={controlImage ? <FaSave size={16} /> : undefined} icon={controlImage ? <FaSave size={16} /> : undefined}
tooltip="Save Control Image" tooltip={t('controlnet.saveControlImage')}
styleOverrides={{ marginTop: 6 }} styleOverrides={{ marginTop: 6 }}
/> />
<IAIDndImageIcon <IAIDndImageIcon
onClick={handleSetControlImageToDimensions} onClick={handleSetControlImageToDimensions}
icon={controlImage ? <FaRulerVertical size={16} /> : undefined} icon={controlImage ? <FaRulerVertical size={16} /> : undefined}
tooltip="Set Control Image Dimensions To W/H" tooltip={t('controlnet.setControlImageDimensions')}
styleOverrides={{ marginTop: 12 }} styleOverrides={{ marginTop: 12 }}
/> />
</> </>

View File

@ -6,6 +6,7 @@ import {
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { selectIsBusy } from 'features/system/store/systemSelectors'; import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -15,6 +16,7 @@ const ParamControlNetShouldAutoConfig = (props: Props) => {
const { controlNetId, isEnabled, shouldAutoConfig } = props.controlNet; const { controlNetId, isEnabled, shouldAutoConfig } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleShouldAutoConfigChanged = useCallback(() => { const handleShouldAutoConfigChanged = useCallback(() => {
dispatch(controlNetAutoConfigToggled({ controlNetId })); dispatch(controlNetAutoConfigToggled({ controlNetId }));
@ -22,8 +24,8 @@ const ParamControlNetShouldAutoConfig = (props: Props) => {
return ( return (
<IAISwitch <IAISwitch
label="Auto configure processor" label={t('controlnet.autoConfigure')}
aria-label="Auto configure processor" aria-label={t('controlnet.autoConfigure')}
isChecked={shouldAutoConfig} isChecked={shouldAutoConfig}
onChange={handleShouldAutoConfigChanged} onChange={handleShouldAutoConfigChanged}
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}

View File

@ -8,6 +8,7 @@ import {
import { ControlNetConfig } from 'features/controlNet/store/controlNetSlice'; import { ControlNetConfig } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaImage, FaMask } from 'react-icons/fa'; import { FaImage, FaMask } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
type ControlNetCanvasImageImportsProps = { type ControlNetCanvasImageImportsProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -18,6 +19,7 @@ const ControlNetCanvasImageImports = (
) => { ) => {
const { controlNet } = props; const { controlNet } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleImportImageFromCanvas = useCallback(() => { const handleImportImageFromCanvas = useCallback(() => {
dispatch(canvasImageToControlNet({ controlNet })); dispatch(canvasImageToControlNet({ controlNet }));
@ -36,15 +38,15 @@ const ControlNetCanvasImageImports = (
<IAIIconButton <IAIIconButton
size="sm" size="sm"
icon={<FaImage />} icon={<FaImage />}
tooltip="Import Image From Canvas" tooltip={t('controlnet.importImageFromCanvas')}
aria-label="Import Image From Canvas" aria-label={t('controlnet.importImageFromCanvas')}
onClick={handleImportImageFromCanvas} onClick={handleImportImageFromCanvas}
/> />
<IAIIconButton <IAIIconButton
size="sm" size="sm"
icon={<FaMask />} icon={<FaMask />}
tooltip="Import Mask From Canvas" tooltip={t('controlnet.importMaskFromCanvas')}
aria-label="Import Mask From Canvas" aria-label={t('controlnet.importMaskFromCanvas')}
onClick={handleImportMaskFromCanvas} onClick={handleImportMaskFromCanvas}
/> />
</Flex> </Flex>

View File

@ -17,6 +17,7 @@ import {
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { ControlNetBeginEndPopover } from 'features/informationalPopovers/components/controlNetBeginEnd'; import { ControlNetBeginEndPopover } from 'features/informationalPopovers/components/controlNetBeginEnd';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -28,6 +29,7 @@ const ParamControlNetBeginEnd = (props: Props) => {
const { beginStepPct, endStepPct, isEnabled, controlNetId } = const { beginStepPct, endStepPct, isEnabled, controlNetId } =
props.controlNet; props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleStepPctChanged = useCallback( const handleStepPctChanged = useCallback(
(v: number[]) => { (v: number[]) => {
@ -50,10 +52,10 @@ const ParamControlNetBeginEnd = (props: Props) => {
return ( return (
<ControlNetBeginEndPopover> <ControlNetBeginEndPopover>
<FormControl isDisabled={!isEnabled}> <FormControl isDisabled={!isEnabled}>
<FormLabel>Begin / End Step Percentage</FormLabel> <FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel>
<HStack w="100%" gap={2} alignItems="center"> <HStack w="100%" gap={2} alignItems="center">
<RangeSlider <RangeSlider
aria-label={['Begin Step %', 'End Step %']} aria-label={['Begin Step %', 'End Step %!']}
value={[beginStepPct, endStepPct]} value={[beginStepPct, endStepPct]}
onChange={handleStepPctChanged} onChange={handleStepPctChanged}
min={0} min={0}
@ -61,6 +63,22 @@ const ParamControlNetBeginEnd = (props: Props) => {
step={0.01} step={0.01}
minStepsBetweenThumbs={5} minStepsBetweenThumbs={5}
isDisabled={!isEnabled} isDisabled={!isEnabled}
>
<RangeSliderTrack>
<RangeSliderFilledTrack />
</RangeSliderTrack>
<Tooltip label={formatPct(beginStepPct)} placement="top" hasArrow>
<RangeSliderThumb index={0} />
</Tooltip>
<Tooltip label={formatPct(endStepPct)} placement="top" hasArrow>
<RangeSliderThumb index={1} />
</Tooltip>
<RangeSliderMark
value={0}
sx={{
insetInlineStart: '0 !important',
insetInlineEnd: 'unset !important',
}}
> >
<RangeSliderTrack> <RangeSliderTrack>
<RangeSliderFilledTrack /> <RangeSliderFilledTrack />

View File

@ -7,23 +7,25 @@ import {
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { ControlNetControlModePopover } from 'features/informationalPopovers/components/controlNetControlMode'; import { ControlNetControlModePopover } from 'features/informationalPopovers/components/controlNetControlMode';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetControlModeProps = { type ParamControlNetControlModeProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
}; };
const CONTROL_MODE_DATA = [
{ label: 'Balanced', value: 'balanced' },
{ label: 'Prompt', value: 'more_prompt' },
{ label: 'Control', value: 'more_control' },
{ label: 'Mega Control', value: 'unbalanced' },
];
export default function ParamControlNetControlMode( export default function ParamControlNetControlMode(
props: ParamControlNetControlModeProps props: ParamControlNetControlModeProps
) { ) {
const { controlMode, isEnabled, controlNetId } = props.controlNet; const { controlMode, isEnabled, controlNetId } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const CONTROL_MODE_DATA = [
{ label: t('controlnet.balanced'), value: 'balanced' },
{ label: t('controlnet.prompt'), value: 'more_prompt' },
{ label: t('controlnet.control'), value: 'more_control' },
{ label: t('controlnet.megaControl'), value: 'unbalanced' },
];
const handleControlModeChange = useCallback( const handleControlModeChange = useCallback(
(controlMode: ControlModes) => { (controlMode: ControlModes) => {
@ -36,7 +38,7 @@ export default function ParamControlNetControlMode(
<ControlNetControlModePopover> <ControlNetControlModePopover>
<IAIMantineSelect <IAIMantineSelect
disabled={!isEnabled} disabled={!isEnabled}
label="Control Mode" label={t('controlnet.controlMode')}
data={CONTROL_MODE_DATA} data={CONTROL_MODE_DATA}
value={String(controlMode)} value={String(controlMode)}
onChange={handleControlModeChange} onChange={handleControlModeChange}

View File

@ -15,6 +15,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useGetControlNetModelsQuery } from 'services/api/endpoints/models'; import { useGetControlNetModelsQuery } from 'services/api/endpoints/models';
import { useTranslation } from 'react-i18next';
type ParamControlNetModelProps = { type ParamControlNetModelProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -35,6 +36,7 @@ const ParamControlNetModel = (props: ParamControlNetModelProps) => {
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { mainModel } = useAppSelector(selector); const { mainModel } = useAppSelector(selector);
const { t } = useTranslation();
const { data: controlNetModels } = useGetControlNetModelsQuery(); const { data: controlNetModels } = useGetControlNetModelsQuery();
@ -58,13 +60,13 @@ const ParamControlNetModel = (props: ParamControlNetModelProps) => {
group: MODEL_TYPE_MAP[model.base_model], group: MODEL_TYPE_MAP[model.base_model],
disabled, disabled,
tooltip: disabled tooltip: disabled
? `Incompatible base model: ${model.base_model}` ? `${t('controlnet.incompatibleBaseModel')} ${model.base_model}`
: undefined, : undefined,
}); });
}); });
return data; return data;
}, [controlNetModels, mainModel?.base_model]); }, [controlNetModels, mainModel?.base_model, t]);
// grab the full model entity from the RTK Query cache // grab the full model entity from the RTK Query cache
const selectedModel = useMemo( const selectedModel = useMemo(
@ -105,7 +107,7 @@ const ParamControlNetModel = (props: ParamControlNetModelProps) => {
error={ error={
!selectedModel || mainModel?.base_model !== selectedModel.base_model !selectedModel || mainModel?.base_model !== selectedModel.base_model
} }
placeholder="Select a model" placeholder={t('controlnet.selectModel')}
value={selectedModel?.id ?? null} value={selectedModel?.id ?? null}
onChange={handleModelChanged} onChange={handleModelChanged}
disabled={isBusy || !isEnabled} disabled={isBusy || !isEnabled}

View File

@ -15,6 +15,7 @@ import {
controlNetProcessorTypeChanged, controlNetProcessorTypeChanged,
} from '../../store/controlNetSlice'; } from '../../store/controlNetSlice';
import { ControlNetProcessorType } from '../../store/types'; import { ControlNetProcessorType } from '../../store/types';
import { useTranslation } from 'react-i18next';
type ParamControlNetProcessorSelectProps = { type ParamControlNetProcessorSelectProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -57,6 +58,7 @@ const ParamControlNetProcessorSelect = (
const { controlNetId, isEnabled, processorNode } = props.controlNet; const { controlNetId, isEnabled, processorNode } = props.controlNet;
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const controlNetProcessors = useAppSelector(selector); const controlNetProcessors = useAppSelector(selector);
const { t } = useTranslation();
const handleProcessorTypeChanged = useCallback( const handleProcessorTypeChanged = useCallback(
(v: string | null) => { (v: string | null) => {
@ -72,7 +74,7 @@ const ParamControlNetProcessorSelect = (
return ( return (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label="Processor" label={t('controlnet.processor')}
value={processorNode.type ?? 'canny_image_processor'} value={processorNode.type ?? 'canny_image_processor'}
data={controlNetProcessors} data={controlNetProcessors}
onChange={handleProcessorTypeChanged} onChange={handleProcessorTypeChanged}

View File

@ -7,22 +7,24 @@ import {
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { ControlNetResizeModePopover } from 'features/informationalPopovers/components/controlNetResizeMode'; import { ControlNetResizeModePopover } from 'features/informationalPopovers/components/controlNetResizeMode';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetResizeModeProps = { type ParamControlNetResizeModeProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
}; };
const RESIZE_MODE_DATA = [
{ label: 'Resize', value: 'just_resize' },
{ label: 'Crop', value: 'crop_resize' },
{ label: 'Fill', value: 'fill_resize' },
];
export default function ParamControlNetResizeMode( export default function ParamControlNetResizeMode(
props: ParamControlNetResizeModeProps props: ParamControlNetResizeModeProps
) { ) {
const { resizeMode, isEnabled, controlNetId } = props.controlNet; const { resizeMode, isEnabled, controlNetId } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const RESIZE_MODE_DATA = [
{ label: t('controlnet.resize'), value: 'just_resize' },
{ label: t('controlnet.crop'), value: 'crop_resize' },
{ label: t('controlnet.fill'), value: 'fill_resize' },
];
const handleResizeModeChange = useCallback( const handleResizeModeChange = useCallback(
(resizeMode: ResizeModes) => { (resizeMode: ResizeModes) => {
@ -35,7 +37,7 @@ export default function ParamControlNetResizeMode(
<ControlNetResizeModePopover> <ControlNetResizeModePopover>
<IAIMantineSelect <IAIMantineSelect
disabled={!isEnabled} disabled={!isEnabled}
label="Resize Mode" label={t('controlnet.resizeMode')}
data={RESIZE_MODE_DATA} data={RESIZE_MODE_DATA}
value={String(resizeMode)} value={String(resizeMode)}
onChange={handleResizeModeChange} onChange={handleResizeModeChange}

View File

@ -6,6 +6,7 @@ import {
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { ControlNetWeightPopover } from 'features/informationalPopovers/components/controlNetWeight'; import { ControlNetWeightPopover } from 'features/informationalPopovers/components/controlNetWeight';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetWeightProps = { type ParamControlNetWeightProps = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -14,6 +15,7 @@ type ParamControlNetWeightProps = {
const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
const { weight, isEnabled, controlNetId } = props.controlNet; const { weight, isEnabled, controlNetId } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleWeightChanged = useCallback( const handleWeightChanged = useCallback(
(weight: number) => { (weight: number) => {
dispatch(controlNetWeightChanged({ controlNetId, weight })); dispatch(controlNetWeightChanged({ controlNetId, weight }));
@ -25,7 +27,7 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
<ControlNetWeightPopover> <ControlNetWeightPopover>
<IAISlider <IAISlider
isDisabled={!isEnabled} isDisabled={!isEnabled}
label="Weight" label={t('controlnet.weight')}
value={weight} value={weight}
onChange={handleWeightChanged} onChange={handleWeightChanged}
min={0} min={0}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor
.default as RequiredCannyImageProcessorInvocation; .default as RequiredCannyImageProcessorInvocation;
@ -21,6 +22,7 @@ const CannyProcessor = (props: CannyProcessorProps) => {
const { low_threshold, high_threshold } = processorNode; const { low_threshold, high_threshold } = processorNode;
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const { t } = useTranslation();
const handleLowThresholdChanged = useCallback( const handleLowThresholdChanged = useCallback(
(v: number) => { (v: number) => {
@ -52,7 +54,7 @@ const CannyProcessor = (props: CannyProcessorProps) => {
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
label="Low Threshold" label={t('controlnet.lowThreshold')}
value={low_threshold} value={low_threshold}
onChange={handleLowThresholdChanged} onChange={handleLowThresholdChanged}
handleReset={handleLowThresholdReset} handleReset={handleLowThresholdReset}
@ -64,7 +66,7 @@ const CannyProcessor = (props: CannyProcessorProps) => {
/> />
<IAISlider <IAISlider
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
label="High Threshold" label={t('controlnet.highThreshold')}
value={high_threshold} value={high_threshold}
onChange={handleHighThresholdChanged} onChange={handleHighThresholdChanged}
handleReset={handleHighThresholdReset} handleReset={handleHighThresholdReset}

View File

@ -6,6 +6,7 @@ import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectIsBusy } from 'features/system/store/systemSelectors'; import { selectIsBusy } from 'features/system/store/systemSelectors';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor
.default as RequiredContentShuffleImageProcessorInvocation; .default as RequiredContentShuffleImageProcessorInvocation;
@ -21,6 +22,7 @@ const ContentShuffleProcessor = (props: Props) => {
const { image_resolution, detect_resolution, w, h, f } = processorNode; const { image_resolution, detect_resolution, w, h, f } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -90,7 +92,7 @@ const ContentShuffleProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -102,7 +104,7 @@ const ContentShuffleProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -114,7 +116,7 @@ const ContentShuffleProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="W" label={t('controlnet.w')}
value={w} value={w}
onChange={handleWChanged} onChange={handleWChanged}
handleReset={handleWReset} handleReset={handleWReset}
@ -126,7 +128,7 @@ const ContentShuffleProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="H" label={t('controlnet.h')}
value={h} value={h}
onChange={handleHChanged} onChange={handleHChanged}
handleReset={handleHReset} handleReset={handleHReset}
@ -138,7 +140,7 @@ const ContentShuffleProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="F" label={t('controlnet.f')}
value={f} value={f}
onChange={handleFChanged} onChange={handleFChanged}
handleReset={handleFReset} handleReset={handleFReset}

View File

@ -7,6 +7,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor
.default as RequiredHedImageProcessorInvocation; .default as RequiredHedImageProcessorInvocation;
@ -25,6 +26,7 @@ const HedPreprocessor = (props: HedProcessorProps) => {
} = props; } = props;
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -62,7 +64,7 @@ const HedPreprocessor = (props: HedProcessorProps) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -74,7 +76,7 @@ const HedPreprocessor = (props: HedProcessorProps) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -86,7 +88,7 @@ const HedPreprocessor = (props: HedProcessorProps) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISwitch <IAISwitch
label="Scribble" label={t('controlnet.scribble')}
isChecked={scribble} isChecked={scribble}
onChange={handleScribbleChanged} onChange={handleScribbleChanged}
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor
.default as RequiredLineartAnimeImageProcessorInvocation; .default as RequiredLineartAnimeImageProcessorInvocation;
@ -21,6 +22,7 @@ const LineartAnimeProcessor = (props: Props) => {
const { image_resolution, detect_resolution } = processorNode; const { image_resolution, detect_resolution } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -51,7 +53,7 @@ const LineartAnimeProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -63,7 +65,7 @@ const LineartAnimeProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}

View File

@ -7,6 +7,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor
.default as RequiredLineartImageProcessorInvocation; .default as RequiredLineartImageProcessorInvocation;
@ -22,6 +23,7 @@ const LineartProcessor = (props: LineartProcessorProps) => {
const { image_resolution, detect_resolution, coarse } = processorNode; const { image_resolution, detect_resolution, coarse } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -59,7 +61,7 @@ const LineartProcessor = (props: LineartProcessorProps) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -71,7 +73,7 @@ const LineartProcessor = (props: LineartProcessorProps) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -83,7 +85,7 @@ const LineartProcessor = (props: LineartProcessorProps) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISwitch <IAISwitch
label="Coarse" label={t('controlnet.coarse')}
isChecked={coarse} isChecked={coarse}
onChange={handleCoarseChanged} onChange={handleCoarseChanged}
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor
.default as RequiredMediapipeFaceProcessorInvocation; .default as RequiredMediapipeFaceProcessorInvocation;
@ -21,6 +22,7 @@ const MediapipeFaceProcessor = (props: Props) => {
const { max_faces, min_confidence } = processorNode; const { max_faces, min_confidence } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleMaxFacesChanged = useCallback( const handleMaxFacesChanged = useCallback(
(v: number) => { (v: number) => {
@ -47,7 +49,7 @@ const MediapipeFaceProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Max Faces" label={t('controlnet.maxFaces')}
value={max_faces} value={max_faces}
onChange={handleMaxFacesChanged} onChange={handleMaxFacesChanged}
handleReset={handleMaxFacesReset} handleReset={handleMaxFacesReset}
@ -59,7 +61,7 @@ const MediapipeFaceProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Min Confidence" label={t('controlnet.minConfidence')}
value={min_confidence} value={min_confidence}
onChange={handleMinConfidenceChanged} onChange={handleMinConfidenceChanged}
handleReset={handleMinConfidenceReset} handleReset={handleMinConfidenceReset}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor
.default as RequiredMidasDepthImageProcessorInvocation; .default as RequiredMidasDepthImageProcessorInvocation;
@ -21,6 +22,7 @@ const MidasDepthProcessor = (props: Props) => {
const { a_mult, bg_th } = processorNode; const { a_mult, bg_th } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleAMultChanged = useCallback( const handleAMultChanged = useCallback(
(v: number) => { (v: number) => {
@ -47,7 +49,7 @@ const MidasDepthProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="a_mult" label={t('controlnet.amult')}
value={a_mult} value={a_mult}
onChange={handleAMultChanged} onChange={handleAMultChanged}
handleReset={handleAMultReset} handleReset={handleAMultReset}
@ -60,7 +62,7 @@ const MidasDepthProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="bg_th" label={t('controlnet.bgth')}
value={bg_th} value={bg_th}
onChange={handleBgThChanged} onChange={handleBgThChanged}
handleReset={handleBgThReset} handleReset={handleBgThReset}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor
.default as RequiredMlsdImageProcessorInvocation; .default as RequiredMlsdImageProcessorInvocation;
@ -21,6 +22,7 @@ const MlsdImageProcessor = (props: Props) => {
const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode; const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -73,7 +75,7 @@ const MlsdImageProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -85,7 +87,7 @@ const MlsdImageProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -97,7 +99,7 @@ const MlsdImageProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="W" label={t('controlnet.w')}
value={thr_d} value={thr_d}
onChange={handleThrDChanged} onChange={handleThrDChanged}
handleReset={handleThrDReset} handleReset={handleThrDReset}
@ -110,7 +112,7 @@ const MlsdImageProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="H" label={t('controlnet.h')}
value={thr_v} value={thr_v}
onChange={handleThrVChanged} onChange={handleThrVChanged}
handleReset={handleThrVReset} handleReset={handleThrVReset}

View File

@ -6,6 +6,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor
.default as RequiredNormalbaeImageProcessorInvocation; .default as RequiredNormalbaeImageProcessorInvocation;
@ -21,6 +22,7 @@ const NormalBaeProcessor = (props: Props) => {
const { image_resolution, detect_resolution } = processorNode; const { image_resolution, detect_resolution } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -51,7 +53,7 @@ const NormalBaeProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -63,7 +65,7 @@ const NormalBaeProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}

View File

@ -7,6 +7,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor
.default as RequiredOpenposeImageProcessorInvocation; .default as RequiredOpenposeImageProcessorInvocation;
@ -22,6 +23,7 @@ const OpenposeProcessor = (props: Props) => {
const { image_resolution, detect_resolution, hand_and_face } = processorNode; const { image_resolution, detect_resolution, hand_and_face } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -59,7 +61,7 @@ const OpenposeProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -71,7 +73,7 @@ const OpenposeProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -83,7 +85,7 @@ const OpenposeProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISwitch <IAISwitch
label="Hand and Face" label={t('controlnet.handAndFace')}
isChecked={hand_and_face} isChecked={hand_and_face}
onChange={handleHandAndFaceChanged} onChange={handleHandAndFaceChanged}
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}

View File

@ -7,6 +7,7 @@ import { selectIsBusy } from 'features/system/store/systemSelectors';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper'; import ProcessorWrapper from './common/ProcessorWrapper';
import { useTranslation } from 'react-i18next';
const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor
.default as RequiredPidiImageProcessorInvocation; .default as RequiredPidiImageProcessorInvocation;
@ -22,6 +23,7 @@ const PidiProcessor = (props: Props) => {
const { image_resolution, detect_resolution, scribble, safe } = processorNode; const { image_resolution, detect_resolution, scribble, safe } = processorNode;
const processorChanged = useProcessorNodeChanged(); const processorChanged = useProcessorNodeChanged();
const isBusy = useAppSelector(selectIsBusy); const isBusy = useAppSelector(selectIsBusy);
const { t } = useTranslation();
const handleDetectResolutionChanged = useCallback( const handleDetectResolutionChanged = useCallback(
(v: number) => { (v: number) => {
@ -66,7 +68,7 @@ const PidiProcessor = (props: Props) => {
return ( return (
<ProcessorWrapper> <ProcessorWrapper>
<IAISlider <IAISlider
label="Detect Resolution" label={t('controlnet.detectResolution')}
value={detect_resolution} value={detect_resolution}
onChange={handleDetectResolutionChanged} onChange={handleDetectResolutionChanged}
handleReset={handleDetectResolutionReset} handleReset={handleDetectResolutionReset}
@ -78,7 +80,7 @@ const PidiProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISlider <IAISlider
label="Image Resolution" label={t('controlnet.imageResolution')}
value={image_resolution} value={image_resolution}
onChange={handleImageResolutionChanged} onChange={handleImageResolutionChanged}
handleReset={handleImageResolutionReset} handleReset={handleImageResolutionReset}
@ -90,12 +92,12 @@ const PidiProcessor = (props: Props) => {
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}
/> />
<IAISwitch <IAISwitch
label="Scribble" label={t('controlnet.scribble')}
isChecked={scribble} isChecked={scribble}
onChange={handleScribbleChanged} onChange={handleScribbleChanged}
/> />
<IAISwitch <IAISwitch
label="Safe" label={t('controlnet.safe')}
isChecked={safe} isChecked={safe}
onChange={handleSafeChanged} onChange={handleSafeChanged}
isDisabled={isBusy || !isEnabled} isDisabled={isBusy || !isEnabled}

View File

@ -2,6 +2,7 @@ import {
ControlNetProcessorType, ControlNetProcessorType,
RequiredControlNetProcessorNode, RequiredControlNetProcessorNode,
} from './types'; } from './types';
import i18n from 'i18next';
type ControlNetProcessorsDict = Record< type ControlNetProcessorsDict = Record<
ControlNetProcessorType, ControlNetProcessorType,
@ -12,7 +13,6 @@ type ControlNetProcessorsDict = Record<
default: RequiredControlNetProcessorNode | { type: 'none' }; default: RequiredControlNetProcessorNode | { type: 'none' };
} }
>; >;
/** /**
* A dict of ControlNet processors, including: * A dict of ControlNet processors, including:
* - type * - type
@ -25,16 +25,24 @@ type ControlNetProcessorsDict = Record<
export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
none: { none: {
type: 'none', type: 'none',
label: 'none', get label() {
description: '', return i18n.t('controlnet.none');
},
get description() {
return i18n.t('controlnet.noneDescription');
},
default: { default: {
type: 'none', type: 'none',
}, },
}, },
canny_image_processor: { canny_image_processor: {
type: 'canny_image_processor', type: 'canny_image_processor',
label: 'Canny', get label() {
description: '', return i18n.t('controlnet.canny');
},
get description() {
return i18n.t('controlnet.cannyDescription');
},
default: { default: {
id: 'canny_image_processor', id: 'canny_image_processor',
type: 'canny_image_processor', type: 'canny_image_processor',
@ -44,8 +52,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
content_shuffle_image_processor: { content_shuffle_image_processor: {
type: 'content_shuffle_image_processor', type: 'content_shuffle_image_processor',
label: 'Content Shuffle', get label() {
description: '', return i18n.t('controlnet.contentShuffle');
},
get description() {
return i18n.t('controlnet.contentShuffleDescription');
},
default: { default: {
id: 'content_shuffle_image_processor', id: 'content_shuffle_image_processor',
type: 'content_shuffle_image_processor', type: 'content_shuffle_image_processor',
@ -58,8 +70,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
hed_image_processor: { hed_image_processor: {
type: 'hed_image_processor', type: 'hed_image_processor',
label: 'HED', get label() {
description: '', return i18n.t('controlnet.hed');
},
get description() {
return i18n.t('controlnet.hedDescription');
},
default: { default: {
id: 'hed_image_processor', id: 'hed_image_processor',
type: 'hed_image_processor', type: 'hed_image_processor',
@ -70,8 +86,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
lineart_anime_image_processor: { lineart_anime_image_processor: {
type: 'lineart_anime_image_processor', type: 'lineart_anime_image_processor',
label: 'Lineart Anime', get label() {
description: '', return i18n.t('controlnet.lineartAnime');
},
get description() {
return i18n.t('controlnet.lineartAnimeDescription');
},
default: { default: {
id: 'lineart_anime_image_processor', id: 'lineart_anime_image_processor',
type: 'lineart_anime_image_processor', type: 'lineart_anime_image_processor',
@ -81,8 +101,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
lineart_image_processor: { lineart_image_processor: {
type: 'lineart_image_processor', type: 'lineart_image_processor',
label: 'Lineart', get label() {
description: '', return i18n.t('controlnet.lineart');
},
get description() {
return i18n.t('controlnet.lineartDescription');
},
default: { default: {
id: 'lineart_image_processor', id: 'lineart_image_processor',
type: 'lineart_image_processor', type: 'lineart_image_processor',
@ -93,8 +117,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
mediapipe_face_processor: { mediapipe_face_processor: {
type: 'mediapipe_face_processor', type: 'mediapipe_face_processor',
label: 'Mediapipe Face', get label() {
description: '', return i18n.t('controlnet.mediapipeFace');
},
get description() {
return i18n.t('controlnet.mediapipeFaceDescription');
},
default: { default: {
id: 'mediapipe_face_processor', id: 'mediapipe_face_processor',
type: 'mediapipe_face_processor', type: 'mediapipe_face_processor',
@ -104,8 +132,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
midas_depth_image_processor: { midas_depth_image_processor: {
type: 'midas_depth_image_processor', type: 'midas_depth_image_processor',
label: 'Depth (Midas)', get label() {
description: '', return i18n.t('controlnet.depthMidas');
},
get description() {
return i18n.t('controlnet.depthMidasDescription');
},
default: { default: {
id: 'midas_depth_image_processor', id: 'midas_depth_image_processor',
type: 'midas_depth_image_processor', type: 'midas_depth_image_processor',
@ -115,8 +147,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
mlsd_image_processor: { mlsd_image_processor: {
type: 'mlsd_image_processor', type: 'mlsd_image_processor',
label: 'M-LSD', get label() {
description: '', return i18n.t('controlnet.mlsd');
},
get description() {
return i18n.t('controlnet.mlsdDescription');
},
default: { default: {
id: 'mlsd_image_processor', id: 'mlsd_image_processor',
type: 'mlsd_image_processor', type: 'mlsd_image_processor',
@ -128,8 +164,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
normalbae_image_processor: { normalbae_image_processor: {
type: 'normalbae_image_processor', type: 'normalbae_image_processor',
label: 'Normal BAE', get label() {
description: '', return i18n.t('controlnet.normalBae');
},
get description() {
return i18n.t('controlnet.normalBaeDescription');
},
default: { default: {
id: 'normalbae_image_processor', id: 'normalbae_image_processor',
type: 'normalbae_image_processor', type: 'normalbae_image_processor',
@ -139,8 +179,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
openpose_image_processor: { openpose_image_processor: {
type: 'openpose_image_processor', type: 'openpose_image_processor',
label: 'Openpose', get label() {
description: '', return i18n.t('controlnet.openPose');
},
get description() {
return i18n.t('controlnet.openPoseDescription');
},
default: { default: {
id: 'openpose_image_processor', id: 'openpose_image_processor',
type: 'openpose_image_processor', type: 'openpose_image_processor',
@ -151,8 +195,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
pidi_image_processor: { pidi_image_processor: {
type: 'pidi_image_processor', type: 'pidi_image_processor',
label: 'PIDI', get label() {
description: '', return i18n.t('controlnet.pidi');
},
get description() {
return i18n.t('controlnet.pidiDescription');
},
default: { default: {
id: 'pidi_image_processor', id: 'pidi_image_processor',
type: 'pidi_image_processor', type: 'pidi_image_processor',
@ -164,8 +212,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
}, },
zoe_depth_image_processor: { zoe_depth_image_processor: {
type: 'zoe_depth_image_processor', type: 'zoe_depth_image_processor',
label: 'Depth (Zoe)', get label() {
description: '', return i18n.t('controlnet.depthZoe');
},
get description() {
return i18n.t('controlnet.depthZoeDescription');
},
default: { default: {
id: 'zoe_depth_image_processor', id: 'zoe_depth_image_processor',
type: 'zoe_depth_image_processor', type: 'zoe_depth_image_processor',
@ -186,4 +238,6 @@ export const CONTROLNET_MODEL_DEFAULT_PROCESSORS: {
shuffle: 'content_shuffle_image_processor', shuffle: 'content_shuffle_image_processor',
openpose: 'openpose_image_processor', openpose: 'openpose_image_processor',
mediapipe: 'mediapipe_face_processor', mediapipe: 'mediapipe_face_processor',
pidi: 'pidi_image_processor',
zoe: 'zoe_depth_image_processor',
}; };

View File

@ -2,16 +2,19 @@ import { ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { some } from 'lodash-es'; import { some } from 'lodash-es';
import { memo } from 'react'; import { memo } from 'react';
import { ImageUsage } from '../store/types'; import { ImageUsage } from '../store/types';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
imageUsage?: ImageUsage; imageUsage?: ImageUsage;
topMessage?: string; topMessage?: string;
bottomMessage?: string; bottomMessage?: string;
}; };
const ImageUsageMessage = (props: Props) => { const ImageUsageMessage = (props: Props) => {
const { t } = useTranslation();
const { const {
imageUsage, imageUsage,
topMessage = 'This image is currently in use in the following features:', topMessage = t('gallery.currentlyInUse'),
bottomMessage = 'If you delete this image, those features will immediately be reset.', bottomMessage = t('gallery.featuresWillReset'),
} = props; } = props;
if (!imageUsage) { if (!imageUsage) {
@ -26,10 +29,18 @@ const ImageUsageMessage = (props: Props) => {
<> <>
<Text>{topMessage}</Text> <Text>{topMessage}</Text>
<UnorderedList sx={{ paddingInlineStart: 6 }}> <UnorderedList sx={{ paddingInlineStart: 6 }}>
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>} {imageUsage.isInitialImage && (
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>} <ListItem>{t('common.img2img')}</ListItem>
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>} )}
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>} {imageUsage.isCanvasImage && (
<ListItem>{t('common.unifiedCanvas')}</ListItem>
)}
{imageUsage.isControlNetImage && (
<ListItem>{t('common.controlNet')}</ListItem>
)}
{imageUsage.isNodesImage && (
<ListItem>{t('common.nodeEditor')}</ListItem>
)}
</UnorderedList> </UnorderedList>
<Text>{bottomMessage}</Text> <Text>{bottomMessage}</Text>
</> </>

View File

@ -9,6 +9,7 @@ import { useFeatureStatus } from '../../system/hooks/useFeatureStatus';
import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial'; import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial';
import ParamDynamicPromptsToggle from './ParamDynamicPromptsEnabled'; import ParamDynamicPromptsToggle from './ParamDynamicPromptsEnabled';
import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts'; import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -22,6 +23,7 @@ const selector = createSelector(
const ParamDynamicPromptsCollapse = () => { const ParamDynamicPromptsCollapse = () => {
const { activeLabel } = useAppSelector(selector); const { activeLabel } = useAppSelector(selector);
const { t } = useTranslation();
const isDynamicPromptingEnabled = const isDynamicPromptingEnabled =
useFeatureStatus('dynamicPrompting').isFeatureEnabled; useFeatureStatus('dynamicPrompting').isFeatureEnabled;
@ -31,7 +33,7 @@ const ParamDynamicPromptsCollapse = () => {
} }
return ( return (
<IAICollapse label="Dynamic Prompts" activeLabel={activeLabel}> <IAICollapse label={t('prompt.dynamicPrompts')} activeLabel={activeLabel}>
<Flex sx={{ gap: 2, flexDir: 'column' }}> <Flex sx={{ gap: 2, flexDir: 'column' }}>
<ParamDynamicPromptsToggle /> <ParamDynamicPromptsToggle />
<ParamDynamicPromptsCombinatorial /> <ParamDynamicPromptsCombinatorial />

View File

@ -6,6 +6,7 @@ import IAISwitch from 'common/components/IAISwitch';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { combinatorialToggled } from '../store/dynamicPromptsSlice'; import { combinatorialToggled } from '../store/dynamicPromptsSlice';
import { DynamicPromptsCombinatorialPopover } from 'features/informationalPopovers/components/dynamicPromptsCombinatorial'; import { DynamicPromptsCombinatorialPopover } from 'features/informationalPopovers/components/dynamicPromptsCombinatorial';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -20,6 +21,7 @@ const selector = createSelector(
const ParamDynamicPromptsCombinatorial = () => { const ParamDynamicPromptsCombinatorial = () => {
const { combinatorial, isDisabled } = useAppSelector(selector); const { combinatorial, isDisabled } = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChange = useCallback(() => { const handleChange = useCallback(() => {
dispatch(combinatorialToggled()); dispatch(combinatorialToggled());
@ -29,7 +31,7 @@ const ParamDynamicPromptsCombinatorial = () => {
<DynamicPromptsCombinatorialPopover> <DynamicPromptsCombinatorialPopover>
<IAISwitch <IAISwitch
isDisabled={isDisabled} isDisabled={isDisabled}
label="Combinatorial Generation" label={t('prompt.combinatorial')}
isChecked={combinatorial} isChecked={combinatorial}
onChange={handleChange} onChange={handleChange}
/> />

View File

@ -6,6 +6,7 @@ import IAISwitch from 'common/components/IAISwitch';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { isEnabledToggled } from '../store/dynamicPromptsSlice'; import { isEnabledToggled } from '../store/dynamicPromptsSlice';
import { DynamicPromptsTogglePopover } from 'features/informationalPopovers/components/dynamicPromptsToggle'; import { DynamicPromptsTogglePopover } from 'features/informationalPopovers/components/dynamicPromptsToggle';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -20,6 +21,7 @@ const selector = createSelector(
const ParamDynamicPromptsToggle = () => { const ParamDynamicPromptsToggle = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isEnabled } = useAppSelector(selector); const { isEnabled } = useAppSelector(selector);
const { t } = useTranslation();
const handleToggleIsEnabled = useCallback(() => { const handleToggleIsEnabled = useCallback(() => {
dispatch(isEnabledToggled()); dispatch(isEnabledToggled());
@ -28,11 +30,12 @@ const ParamDynamicPromptsToggle = () => {
return ( return (
<DynamicPromptsTogglePopover> <DynamicPromptsTogglePopover>
<IAISwitch <IAISwitch
label="Enable Dynamic Prompts" label={t('prompt.enableDynamicPrompts')}
isChecked={isEnabled} isChecked={isEnabled}
onChange={handleToggleIsEnabled} onChange={handleToggleIsEnabled}
/> />
</DynamicPromptsTogglePopover> </DynamicPromptsTogglePopover>
); );
}; };

View File

@ -8,6 +8,7 @@ import {
maxPromptsChanged, maxPromptsChanged,
maxPromptsReset, maxPromptsReset,
} from '../store/dynamicPromptsSlice'; } from '../store/dynamicPromptsSlice';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -31,6 +32,7 @@ const ParamDynamicPromptsMaxPrompts = () => {
const { maxPrompts, min, sliderMax, inputMax, isDisabled } = const { maxPrompts, min, sliderMax, inputMax, isDisabled } =
useAppSelector(selector); useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChange = useCallback( const handleChange = useCallback(
(v: number) => { (v: number) => {
@ -45,7 +47,7 @@ const ParamDynamicPromptsMaxPrompts = () => {
return ( return (
<IAISlider <IAISlider
label="Max Prompts" label={t('prompt.maxPrompts')}
isDisabled={isDisabled} isDisabled={isDisabled}
min={min} min={min}
max={sliderMax} max={sliderMax}

View File

@ -1,6 +1,7 @@
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { memo } from 'react'; import { memo } from 'react';
import { FaCode } from 'react-icons/fa'; import { FaCode } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
onClick: () => void; onClick: () => void;
@ -8,11 +9,12 @@ type Props = {
const AddEmbeddingButton = (props: Props) => { const AddEmbeddingButton = (props: Props) => {
const { onClick } = props; const { onClick } = props;
const { t } = useTranslation();
return ( return (
<IAIIconButton <IAIIconButton
size="sm" size="sm"
aria-label="Add Embedding" aria-label={t('embedding.addEmbedding')}
tooltip="Add Embedding" tooltip={t('embedding.addEmbedding')}
icon={<FaCode />} icon={<FaCode />}
sx={{ sx={{
p: 2, p: 2,

View File

@ -16,6 +16,7 @@ import { forEach } from 'lodash-es';
import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react'; import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react';
import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren & { type Props = PropsWithChildren & {
onSelect: (v: string) => void; onSelect: (v: string) => void;
@ -27,6 +28,7 @@ const ParamEmbeddingPopover = (props: Props) => {
const { onSelect, isOpen, onClose, children } = props; const { onSelect, isOpen, onClose, children } = props;
const { data: embeddingQueryData } = useGetTextualInversionModelsQuery(); const { data: embeddingQueryData } = useGetTextualInversionModelsQuery();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const currentMainModel = useAppSelector( const currentMainModel = useAppSelector(
(state: RootState) => state.generation.model (state: RootState) => state.generation.model
@ -52,7 +54,7 @@ const ParamEmbeddingPopover = (props: Props) => {
group: MODEL_TYPE_MAP[embedding.base_model], group: MODEL_TYPE_MAP[embedding.base_model],
disabled, disabled,
tooltip: disabled tooltip: disabled
? `Incompatible base model: ${embedding.base_model}` ? `${t('embedding.incompatibleModel')} ${embedding.base_model}`
: undefined, : undefined,
}); });
}); });
@ -63,7 +65,7 @@ const ParamEmbeddingPopover = (props: Props) => {
); );
return data.sort((a, b) => (a.disabled && !b.disabled ? 1 : -1)); return data.sort((a, b) => (a.disabled && !b.disabled ? 1 : -1));
}, [embeddingQueryData, currentMainModel?.base_model]); }, [embeddingQueryData, currentMainModel?.base_model, t]);
const handleChange = useCallback( const handleChange = useCallback(
(v: string | null) => { (v: string | null) => {
@ -118,10 +120,10 @@ const ParamEmbeddingPopover = (props: Props) => {
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
inputRef={inputRef} inputRef={inputRef}
autoFocus autoFocus
placeholder="Add Embedding" placeholder={t('embedding.addEmbedding')}
value={null} value={null}
data={data} data={data}
nothingFound="No matching Embeddings" nothingFound={t('embedding.noMatchingEmbedding')}
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0} disabled={data.length === 0}
onDropdownClose={onClose} onDropdownClose={onClose}

View File

@ -8,6 +8,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -26,6 +27,7 @@ const selector = createSelector(
const BoardAutoAddSelect = () => { const BoardAutoAddSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const { autoAddBoardId, autoAssignBoardOnClick, isProcessing } = const { autoAddBoardId, autoAssignBoardOnClick, isProcessing } =
useAppSelector(selector); useAppSelector(selector);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -63,13 +65,13 @@ const BoardAutoAddSelect = () => {
return ( return (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label="Auto-Add Board" label={t('boards.autoAddBoard')}
inputRef={inputRef} inputRef={inputRef}
autoFocus autoFocus
placeholder="Select a Board" placeholder={t('boards.selectBoard')}
value={autoAddBoardId} value={autoAddBoardId}
data={boards} data={boards}
nothingFound="No matching Boards" nothingFound={t('boards.noMatching')}
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={!hasBoards || autoAssignBoardOnClick || isProcessing} disabled={!hasBoards || autoAssignBoardOnClick || isProcessing}
filter={(value, item: SelectItem) => filter={(value, item: SelectItem) =>

View File

@ -16,6 +16,7 @@ import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems'; import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import NoBoardContextMenuItems from './NoBoardContextMenuItems'; import NoBoardContextMenuItems from './NoBoardContextMenuItems';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
board?: BoardDTO; board?: BoardDTO;
@ -59,6 +60,8 @@ const BoardContextMenu = ({
e.preventDefault(); e.preventDefault();
}, []); }, []);
const { t } = useTranslation();
return ( return (
<IAIContextMenu<HTMLDivElement> <IAIContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
@ -78,7 +81,7 @@ const BoardContextMenu = ({
isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick} isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick}
onClick={handleSetAutoAdd} onClick={handleSetAutoAdd}
> >
Auto-add to this Board {t('boards.menuItemAutoAdd')}
</MenuItem> </MenuItem>
{!board && <NoBoardContextMenuItems />} {!board && <NoBoardContextMenuItems />}
{board && ( {board && (

View File

@ -2,22 +2,22 @@ import IAIIconButton from 'common/components/IAIIconButton';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import { useCreateBoardMutation } from 'services/api/endpoints/boards'; import { useCreateBoardMutation } from 'services/api/endpoints/boards';
import { useTranslation } from 'react-i18next';
const DEFAULT_BOARD_NAME = 'My Board';
const AddBoardButton = () => { const AddBoardButton = () => {
const { t } = useTranslation();
const [createBoard, { isLoading }] = useCreateBoardMutation(); const [createBoard, { isLoading }] = useCreateBoardMutation();
const DEFAULT_BOARD_NAME = t('boards.myBoard');
const handleCreateBoard = useCallback(() => { const handleCreateBoard = useCallback(() => {
createBoard(DEFAULT_BOARD_NAME); createBoard(DEFAULT_BOARD_NAME);
}, [createBoard]); }, [createBoard, DEFAULT_BOARD_NAME]);
return ( return (
<IAIIconButton <IAIIconButton
icon={<FaPlus />} icon={<FaPlus />}
isLoading={isLoading} isLoading={isLoading}
tooltip="Add Board" tooltip={t('boards.addBoard')}
aria-label="Add Board" aria-label={t('boards.addBoard')}
onClick={handleCreateBoard} onClick={handleCreateBoard}
size="sm" size="sm"
/> />

View File

@ -18,6 +18,7 @@ import {
useEffect, useEffect,
useRef, useRef,
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -32,6 +33,7 @@ const BoardsSearch = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { boardSearchText } = useAppSelector(selector); const { boardSearchText } = useAppSelector(selector);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const handleBoardSearch = useCallback( const handleBoardSearch = useCallback(
(searchTerm: string) => { (searchTerm: string) => {
@ -73,7 +75,7 @@ const BoardsSearch = () => {
<InputGroup> <InputGroup>
<Input <Input
ref={inputRef} ref={inputRef}
placeholder="Search Boards..." placeholder={t('boards.searchBoard')}
value={boardSearchText} value={boardSearchText}
onKeyDown={handleKeydown} onKeyDown={handleKeydown}
onChange={handleChange} onChange={handleChange}
@ -84,7 +86,7 @@ const BoardsSearch = () => {
onClick={clearBoardSearch} onClick={clearBoardSearch}
size="xs" size="xs"
variant="ghost" variant="ghost"
aria-label="Clear Search" aria-label={t('boards.clearSearch')}
opacity={0.5} opacity={0.5}
icon={<CloseIcon boxSize={2} />} icon={<CloseIcon boxSize={2} />}
/> />

View File

@ -132,8 +132,8 @@ const DeleteBoardModal = (props: Props) => {
) : ( ) : (
<ImageUsageMessage <ImageUsageMessage
imageUsage={imageUsageSummary} imageUsage={imageUsageSummary}
topMessage="This board contains images used in the following features:" topMessage={t('boards.topMessage')}
bottomMessage="Deleting this board and its images will reset any features currently using them." bottomMessage={t('boards.bottomMessage')}
/> />
)} )}
<Text>Deleted boards cannot be restored.</Text> <Text>Deleted boards cannot be restored.</Text>

View File

@ -19,6 +19,7 @@ import { FaImage } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer'; import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from '../NextPrevImageButtons'; import NextPrevImageButtons from '../NextPrevImageButtons';
import { useTranslation } from 'react-i18next';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[stateSelector, selectLastSelectedImage], [stateSelector, selectLastSelectedImage],
@ -117,6 +118,8 @@ const CurrentImagePreview = () => {
const timeoutId = useRef(0); const timeoutId = useRef(0);
const { t } = useTranslation();
const handleMouseOver = useCallback(() => { const handleMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true); setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current); window.clearTimeout(timeoutId.current);
@ -164,7 +167,7 @@ const CurrentImagePreview = () => {
isUploadDisabled={true} isUploadDisabled={true}
fitContainer fitContainer
useThumbailFallback useThumbailFallback
dropLabel="Set as Current Image" dropLabel={t('gallery.setCurrentImage')}
noContentFallback={ noContentFallback={
<IAINoContentFallback icon={FaImage} label="No image selected" /> <IAINoContentFallback icon={FaImage} label="No image selected" />
} }

View File

@ -1,4 +1,6 @@
import { MenuItem } from '@chakra-ui/react'; import { MenuItem } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
imagesToChangeSelected, imagesToChangeSelected,
@ -16,6 +18,7 @@ import {
const MultipleSelectionMenuItems = () => { const MultipleSelectionMenuItems = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selection = useAppSelector((state) => state.gallery.selection); const selection = useAppSelector((state) => state.gallery.selection);
const customStarUi = useStore($customStarUI);
const [starImages] = useStarImagesMutation(); const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation(); const [unstarImages] = useUnstarImagesMutation();
@ -49,15 +52,18 @@ const MultipleSelectionMenuItems = () => {
<> <>
{areAllStarred && ( {areAllStarred && (
<MenuItem <MenuItem
icon={<MdStarBorder />} icon={customStarUi ? customStarUi.on.icon : <MdStarBorder />}
onClickCapture={handleUnstarSelection} onClickCapture={handleUnstarSelection}
> >
Unstar All {customStarUi ? customStarUi.off.text : `Unstar All`}
</MenuItem> </MenuItem>
)} )}
{(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && ( {(areAllUnstarred || (!areAllStarred && !areAllUnstarred)) && (
<MenuItem icon={<MdStar />} onClickCapture={handleStarSelection}> <MenuItem
Star All icon={customStarUi ? customStarUi.on.icon : <MdStar />}
onClickCapture={handleStarSelection}
>
{customStarUi ? customStarUi.on.text : `Star All`}
</MenuItem> </MenuItem>
)} )}
<MenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}> <MenuItem icon={<FaFolder />} onClickCapture={handleChangeBoard}>

View File

@ -1,5 +1,7 @@
import { Flex, MenuItem, Spinner } from '@chakra-ui/react'; import { Flex, MenuItem, Spinner } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { import {
@ -7,6 +9,7 @@ import {
isModalOpenChanged, isModalOpenChanged,
} from 'features/changeBoardModal/store/slice'; } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@ -32,9 +35,9 @@ import {
useUnstarImagesMutation, useUnstarImagesMutation,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { configSelector } from '../../../system/store/configSelectors'; import { configSelector } from '../../../system/store/configSelectors';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { flushSync } from 'react-dom';
type SingleSelectionMenuItemsProps = { type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO; imageDTO: ImageDTO;
@ -50,6 +53,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); const { shouldFetchMetadataFromApi } = useAppSelector(configSelector);
const customStarUi = useStore($customStarUI);
const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery( const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery(
{ image: imageDTO, shouldFetchMetadataFromApi }, { image: imageDTO, shouldFetchMetadataFromApi },
@ -112,8 +116,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const handleSendToCanvas = useCallback(() => { const handleSendToCanvas = useCallback(() => {
dispatch(sentImageToCanvas()); dispatch(sentImageToCanvas());
dispatch(setInitialCanvasImage(imageDTO)); flushSync(() => {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('unifiedCanvas'));
});
dispatch(setInitialCanvasImage(imageDTO));
toaster({ toaster({
title: t('toast.sentToUnifiedCanvas'), title: t('toast.sentToUnifiedCanvas'),
@ -225,12 +231,18 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
Change Board Change Board
</MenuItem> </MenuItem>
{imageDTO.starred ? ( {imageDTO.starred ? (
<MenuItem icon={<MdStar />} onClickCapture={handleUnstarImage}> <MenuItem
Unstar Image icon={customStarUi ? customStarUi.off.icon : <MdStar />}
onClickCapture={handleUnstarImage}
>
{customStarUi ? customStarUi.off.text : `Unstar Image`}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem icon={<MdStarBorder />} onClickCapture={handleStarImage}> <MenuItem
Star Image icon={customStarUi ? customStarUi.on.icon : <MdStarBorder />}
onClickCapture={handleStarImage}
>
{customStarUi ? customStarUi.on.text : `Star Image`}
</MenuItem> </MenuItem>
)} )}
<MenuItem <MenuItem

View File

@ -1,4 +1,6 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
@ -10,6 +12,7 @@ import {
} from 'features/dnd/types'; } from 'features/dnd/types';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { MouseEvent, memo, useCallback, useMemo, useState } from 'react'; import { MouseEvent, memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
import { MdStar, MdStarBorder } from 'react-icons/md'; import { MdStar, MdStarBorder } from 'react-icons/md';
import { import {
@ -28,10 +31,13 @@ const GalleryImage = (props: HoverableImageProps) => {
const { imageName } = props; const { imageName } = props;
const { currentData: imageDTO } = useGetImageDTOQuery(imageName); const { currentData: imageDTO } = useGetImageDTOQuery(imageName);
const shift = useAppSelector((state) => state.hotkeys.shift); const shift = useAppSelector((state) => state.hotkeys.shift);
const { t } = useTranslation();
const { handleClick, isSelected, selection, selectionCount } = const { handleClick, isSelected, selection, selectionCount } =
useMultiselect(imageDTO); useMultiselect(imageDTO);
const customStarUi = useStore($customStarUI);
const handleDelete = useCallback( const handleDelete = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
@ -89,12 +95,22 @@ const GalleryImage = (props: HoverableImageProps) => {
const starIcon = useMemo(() => { const starIcon = useMemo(() => {
if (imageDTO?.starred) { if (imageDTO?.starred) {
return <MdStar size="20" />; return customStarUi ? customStarUi.on.icon : <MdStar size="20" />;
} }
if (!imageDTO?.starred && isHovered) { if (!imageDTO?.starred && isHovered) {
return <MdStarBorder size="20" />; return customStarUi ? customStarUi.off.icon : <MdStarBorder size="20" />;
} }
}, [imageDTO?.starred, isHovered]); }, [imageDTO?.starred, isHovered, customStarUi]);
const starTooltip = useMemo(() => {
if (imageDTO?.starred) {
return customStarUi ? customStarUi.off.text : 'Unstar';
}
if (!imageDTO?.starred) {
return customStarUi ? customStarUi.on.text : 'Star';
}
return '';
}, [imageDTO?.starred, customStarUi]);
if (!imageDTO) { if (!imageDTO) {
return <IAIFillSkeleton />; return <IAIFillSkeleton />;
@ -129,14 +145,14 @@ const GalleryImage = (props: HoverableImageProps) => {
<IAIDndImageIcon <IAIDndImageIcon
onClick={toggleStarredState} onClick={toggleStarredState}
icon={starIcon} icon={starIcon}
tooltip={imageDTO.starred ? 'Unstar' : 'Star'} tooltip={starTooltip}
/> />
{isHovered && shift && ( {isHovered && shift && (
<IAIDndImageIcon <IAIDndImageIcon
onClick={handleDelete} onClick={handleDelete}
icon={<FaTrash />} icon={<FaTrash />}
tooltip="Delete" tooltip={t('gallery.deleteImage')}
styleOverrides={{ styleOverrides={{
bottom: 2, bottom: 2,
top: 'auto', top: 'auto',

View File

@ -95,7 +95,7 @@ const GalleryImageGrid = () => {
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<IAINoContentFallback label="Loading..." icon={FaImage} /> <IAINoContentFallback label={t('gallery.loading')} icon={FaImage} />
</Flex> </Flex>
); );
} }
@ -140,7 +140,7 @@ const GalleryImageGrid = () => {
onClick={handleLoadMoreImages} onClick={handleLoadMoreImages}
isDisabled={!areMoreAvailable} isDisabled={!areMoreAvailable}
isLoading={isFetching} isLoading={isFetching}
loadingText="Loading" loadingText={t('gallery.loading')}
flexShrink={0} flexShrink={0}
> >
{`Load More (${currentData.ids.length} of ${currentViewTotal})`} {`Load More (${currentData.ids.length} of ${currentViewTotal})`}
@ -153,7 +153,7 @@ const GalleryImageGrid = () => {
return ( return (
<Box sx={{ w: 'full', h: 'full' }}> <Box sx={{ w: 'full', h: 'full' }}>
<IAINoContentFallback <IAINoContentFallback
label="Unable to load Gallery" label={t('gallery.unableToLoad')}
icon={FaExclamationCircle} icon={FaExclamationCircle}
/> />
</Box> </Box>

View File

@ -3,6 +3,7 @@ import { isString } from 'lodash-es';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { FaCopy, FaDownload } from 'react-icons/fa'; import { FaCopy, FaDownload } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
label: string; label: string;
@ -33,6 +34,8 @@ const DataViewer = (props: Props) => {
a.remove(); a.remove();
}, [dataString, label, fileName]); }, [dataString, label, fileName]);
const { t } = useTranslation();
return ( return (
<Flex <Flex
layerStyle="second" layerStyle="second"
@ -73,9 +76,9 @@ const DataViewer = (props: Props) => {
</Box> </Box>
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}> <Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
{withDownload && ( {withDownload && (
<Tooltip label={`Download ${label} JSON`}> <Tooltip label={`${t('gallery.download')} ${label} JSON`}>
<IconButton <IconButton
aria-label={`Download ${label} JSON`} aria-label={`${t('gallery.download')} ${label} JSON`}
icon={<FaDownload />} icon={<FaDownload />}
variant="ghost" variant="ghost"
opacity={0.7} opacity={0.7}
@ -84,9 +87,9 @@ const DataViewer = (props: Props) => {
</Tooltip> </Tooltip>
)} )}
{withCopy && ( {withCopy && (
<Tooltip label={`Copy ${label} JSON`}> <Tooltip label={`${t('gallery.copy')} ${label} JSON`}>
<IconButton <IconButton
aria-label={`Copy ${label} JSON`} aria-label={`${t('gallery.copy')} ${label} JSON`}
icon={<FaCopy />} icon={<FaCopy />}
variant="ghost" variant="ghost"
opacity={0.7} opacity={0.7}

View File

@ -2,6 +2,7 @@ import { CoreMetadata } from 'features/nodes/types/types';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import ImageMetadataItem from './ImageMetadataItem'; import ImageMetadataItem from './ImageMetadataItem';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
metadata?: CoreMetadata; metadata?: CoreMetadata;
@ -10,6 +11,8 @@ type Props = {
const ImageMetadataActions = (props: Props) => { const ImageMetadataActions = (props: Props) => {
const { metadata } = props; const { metadata } = props;
const { t } = useTranslation();
const { const {
recallPositivePrompt, recallPositivePrompt,
recallNegativePrompt, recallNegativePrompt,
@ -70,17 +73,20 @@ const ImageMetadataActions = (props: Props) => {
return ( return (
<> <>
{metadata.created_by && ( {metadata.created_by && (
<ImageMetadataItem label="Created By" value={metadata.created_by} /> <ImageMetadataItem
label={t('metadata.createdBy')}
value={metadata.created_by}
/>
)} )}
{metadata.generation_mode && ( {metadata.generation_mode && (
<ImageMetadataItem <ImageMetadataItem
label="Generation Mode" label={t('metadata.generationMode')}
value={metadata.generation_mode} value={metadata.generation_mode}
/> />
)} )}
{metadata.positive_prompt && ( {metadata.positive_prompt && (
<ImageMetadataItem <ImageMetadataItem
label="Positive Prompt" label={t('metadata.positivePrompt')}
labelPosition="top" labelPosition="top"
value={metadata.positive_prompt} value={metadata.positive_prompt}
onClick={handleRecallPositivePrompt} onClick={handleRecallPositivePrompt}
@ -88,7 +94,7 @@ const ImageMetadataActions = (props: Props) => {
)} )}
{metadata.negative_prompt && ( {metadata.negative_prompt && (
<ImageMetadataItem <ImageMetadataItem
label="Negative Prompt" label={t('metadata.NegativePrompt')}
labelPosition="top" labelPosition="top"
value={metadata.negative_prompt} value={metadata.negative_prompt}
onClick={handleRecallNegativePrompt} onClick={handleRecallNegativePrompt}
@ -96,7 +102,7 @@ const ImageMetadataActions = (props: Props) => {
)} )}
{metadata.seed !== undefined && metadata.seed !== null && ( {metadata.seed !== undefined && metadata.seed !== null && (
<ImageMetadataItem <ImageMetadataItem
label="Seed" label={t('metadata.seed')}
value={metadata.seed} value={metadata.seed}
onClick={handleRecallSeed} onClick={handleRecallSeed}
/> />
@ -105,63 +111,63 @@ const ImageMetadataActions = (props: Props) => {
metadata.model !== null && metadata.model !== null &&
metadata.model.model_name && ( metadata.model.model_name && (
<ImageMetadataItem <ImageMetadataItem
label="Model" label={t('metadata.model')}
value={metadata.model.model_name} value={metadata.model.model_name}
onClick={handleRecallModel} onClick={handleRecallModel}
/> />
)} )}
{metadata.width && ( {metadata.width && (
<ImageMetadataItem <ImageMetadataItem
label="Width" label={t('metadata.width')}
value={metadata.width} value={metadata.width}
onClick={handleRecallWidth} onClick={handleRecallWidth}
/> />
)} )}
{metadata.height && ( {metadata.height && (
<ImageMetadataItem <ImageMetadataItem
label="Height" label={t('metadata.height')}
value={metadata.height} value={metadata.height}
onClick={handleRecallHeight} onClick={handleRecallHeight}
/> />
)} )}
{/* {metadata.threshold !== undefined && ( {/* {metadata.threshold !== undefined && (
<MetadataItem <MetadataItem
label="Noise Threshold" label={t('metadata.threshold')}
value={metadata.threshold} value={metadata.threshold}
onClick={() => dispatch(setThreshold(Number(metadata.threshold)))} onClick={() => dispatch(setThreshold(Number(metadata.threshold)))}
/> />
)} )}
{metadata.perlin !== undefined && ( {metadata.perlin !== undefined && (
<MetadataItem <MetadataItem
label="Perlin Noise" label={t('metadata.perlin')}
value={metadata.perlin} value={metadata.perlin}
onClick={() => dispatch(setPerlin(Number(metadata.perlin)))} onClick={() => dispatch(setPerlin(Number(metadata.perlin)))}
/> />
)} */} )} */}
{metadata.scheduler && ( {metadata.scheduler && (
<ImageMetadataItem <ImageMetadataItem
label="Scheduler" label={t('metadata.scheduler')}
value={metadata.scheduler} value={metadata.scheduler}
onClick={handleRecallScheduler} onClick={handleRecallScheduler}
/> />
)} )}
{metadata.steps && ( {metadata.steps && (
<ImageMetadataItem <ImageMetadataItem
label="Steps" label={t('metadata.steps')}
value={metadata.steps} value={metadata.steps}
onClick={handleRecallSteps} onClick={handleRecallSteps}
/> />
)} )}
{metadata.cfg_scale !== undefined && metadata.cfg_scale !== null && ( {metadata.cfg_scale !== undefined && metadata.cfg_scale !== null && (
<ImageMetadataItem <ImageMetadataItem
label="CFG scale" label={t('metadata.cfgScale')}
value={metadata.cfg_scale} value={metadata.cfg_scale}
onClick={handleRecallCfgScale} onClick={handleRecallCfgScale}
/> />
)} )}
{/* {metadata.variations && metadata.variations.length > 0 && ( {/* {metadata.variations && metadata.variations.length > 0 && (
<MetadataItem <MetadataItem
label="Seed-weight pairs" label="{t('metadata.variations')}
value={seedWeightsToString(metadata.variations)} value={seedWeightsToString(metadata.variations)}
onClick={() => onClick={() =>
dispatch( dispatch(
@ -172,14 +178,14 @@ const ImageMetadataActions = (props: Props) => {
)} )}
{metadata.seamless && ( {metadata.seamless && (
<MetadataItem <MetadataItem
label="Seamless" label={t('metadata.seamless')}
value={metadata.seamless} value={metadata.seamless}
onClick={() => dispatch(setSeamless(metadata.seamless))} onClick={() => dispatch(setSeamless(metadata.seamless))}
/> />
)} )}
{metadata.hires_fix && ( {metadata.hires_fix && (
<MetadataItem <MetadataItem
label="High Resolution Optimization" label={t('metadata.hiresFix')}
value={metadata.hires_fix} value={metadata.hires_fix}
onClick={() => dispatch(setHiresFix(metadata.hires_fix))} onClick={() => dispatch(setHiresFix(metadata.hires_fix))}
/> />
@ -187,7 +193,7 @@ const ImageMetadataActions = (props: Props) => {
{/* {init_image_path && ( {/* {init_image_path && (
<MetadataItem <MetadataItem
label="Initial image" label={t('metadata.initImage')}
value={init_image_path} value={init_image_path}
isLink isLink
onClick={() => dispatch(setInitialImage(init_image_path))} onClick={() => dispatch(setInitialImage(init_image_path))}
@ -195,14 +201,14 @@ const ImageMetadataActions = (props: Props) => {
)} */} )} */}
{metadata.strength && ( {metadata.strength && (
<ImageMetadataItem <ImageMetadataItem
label="Image to image strength" label={t('metadata.strength')}
value={metadata.strength} value={metadata.strength}
onClick={handleRecallStrength} onClick={handleRecallStrength}
/> />
)} )}
{/* {metadata.fit && ( {/* {metadata.fit && (
<MetadataItem <MetadataItem
label="Image to image fit" label={t('metadata.fit')}
value={metadata.fit} value={metadata.fit}
onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))} onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))}
/> />

View File

@ -17,6 +17,7 @@ import DataViewer from './DataViewer';
import ImageMetadataActions from './ImageMetadataActions'; import ImageMetadataActions from './ImageMetadataActions';
import { useAppSelector } from '../../../../app/store/storeHooks'; import { useAppSelector } from '../../../../app/store/storeHooks';
import { configSelector } from '../../../system/store/configSelectors'; import { configSelector } from '../../../system/store/configSelectors';
import { useTranslation } from 'react-i18next';
type ImageMetadataViewerProps = { type ImageMetadataViewerProps = {
image: ImageDTO; image: ImageDTO;
@ -28,6 +29,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
// useHotkeys('esc', () => { // useHotkeys('esc', () => {
// dispatch(setShouldShowImageDetails(false)); // dispatch(setShouldShowImageDetails(false));
// }); // });
const { t } = useTranslation();
const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); const { shouldFetchMetadataFromApi } = useAppSelector(configSelector);
@ -70,31 +72,31 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }} sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
> >
<TabList> <TabList>
<Tab>Metadata</Tab> <Tab>{t('metadata.metadata')}</Tab>
<Tab>Image Details</Tab> <Tab>{t('metadata.imageDetails')}</Tab>
<Tab>Workflow</Tab> <Tab>{t('metadata.workflow')}</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
{metadata ? ( {metadata ? (
<DataViewer data={metadata} label="Metadata" /> <DataViewer data={metadata} label={t('metadata.metadata')} />
) : ( ) : (
<IAINoContentFallback label="No metadata found" /> <IAINoContentFallback label={t('metadata.noMetaData')} />
)} )}
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
{image ? ( {image ? (
<DataViewer data={image} label="Image Details" /> <DataViewer data={image} label={t('metadata.imageDetails')} />
) : ( ) : (
<IAINoContentFallback label="No image details found" /> <IAINoContentFallback label={t('metadata.noImageDetails')} />
)} )}
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
{workflow ? ( {workflow ? (
<DataViewer data={workflow} label="Workflow" /> <DataViewer data={workflow} label={t('metadata.workflow')} />
) : ( ) : (
<IAINoContentFallback label="No workflow found" /> <IAINoContentFallback label={t('metadata.noWorkFlow')} />
)} )}
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>

View File

@ -12,9 +12,11 @@ import TopCenterPanel from './flow/panels/TopCenterPanel/TopCenterPanel';
import TopRightPanel from './flow/panels/TopRightPanel/TopRightPanel'; import TopRightPanel from './flow/panels/TopRightPanel/TopRightPanel';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel'; import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel'; import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
import { useTranslation } from 'react-i18next';
const NodeEditor = () => { const NodeEditor = () => {
const isReady = useAppSelector((state) => state.nodes.isReady); const isReady = useAppSelector((state) => state.nodes.isReady);
const { t } = useTranslation();
return ( return (
<Flex <Flex
layerStyle="first" layerStyle="first"
@ -82,7 +84,7 @@ const NodeEditor = () => {
}} }}
> >
<IAINoContentFallback <IAINoContentFallback
label="Loading Nodes..." label={t('nodes.loadingNodes')}
icon={MdDeviceHub} icon={MdDeviceHub}
/> />
</Flex> </Flex>

View File

@ -24,6 +24,7 @@ import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { AnyInvocationType } from 'services/events/types'; import { AnyInvocationType } from 'services/events/types';
import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem'; import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem';
import { useTranslation } from 'react-i18next';
type NodeTemplate = { type NodeTemplate = {
label: string; label: string;
@ -48,6 +49,12 @@ const filter = (value: string, item: NodeTemplate) => {
); );
}; };
const AddNodePopover = () => {
const dispatch = useAppDispatch();
const buildInvocation = useBuildNodeData();
const toaster = useAppToaster();
const { t } = useTranslation();
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
({ nodes }) => { ({ nodes }) => {
@ -61,30 +68,26 @@ const selector = createSelector(
}); });
data.push({ data.push({
label: 'Progress Image', label: t('nodes.currentImage'),
value: 'current_image', value: 'current_image',
description: 'Displays the current image in the Node Editor', description: t('nodes.currentImageDescription'),
tags: ['progress'], tags: ['progress'],
}); });
data.push({ data.push({
label: 'Notes', label: t('nodes.notes'),
value: 'notes', value: 'notes',
description: 'Add notes about your workflow', description: t('nodes.notesDescription'),
tags: ['notes'], tags: ['notes'],
}); });
data.sort((a, b) => a.label.localeCompare(b.label)); data.sort((a, b) => a.label.localeCompare(b.label));
return { data }; return { data, t };
}, },
defaultSelectorOptions defaultSelectorOptions
); );
const AddNodePopover = () => {
const dispatch = useAppDispatch();
const buildInvocation = useBuildNodeData();
const toaster = useAppToaster();
const { data } = useAppSelector(selector); const { data } = useAppSelector(selector);
const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen); const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -92,18 +95,20 @@ const AddNodePopover = () => {
const addNode = useCallback( const addNode = useCallback(
(nodeType: AnyInvocationType) => { (nodeType: AnyInvocationType) => {
const invocation = buildInvocation(nodeType); const invocation = buildInvocation(nodeType);
if (!invocation) { if (!invocation) {
const errorMessage = t('nodes.unknownInvocation', {
nodeType: nodeType,
});
toaster({ toaster({
status: 'error', status: 'error',
title: `Unknown Invocation type ${nodeType}`, title: errorMessage,
}); });
return; return;
} }
dispatch(nodeAdded(invocation)); dispatch(nodeAdded(invocation));
}, },
[dispatch, buildInvocation, toaster] [dispatch, buildInvocation, toaster, t]
); );
const handleChange = useCallback( const handleChange = useCallback(
@ -179,11 +184,11 @@ const AddNodePopover = () => {
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
inputRef={inputRef} inputRef={inputRef}
selectOnBlur={false} selectOnBlur={false}
placeholder="Search for nodes" placeholder={t('nodes.nodeSearch')}
value={null} value={null}
data={data} data={data}
maxDropdownHeight={400} maxDropdownHeight={400}
nothingFound="No matching nodes" nothingFound={t('nodes.noMatchingNodes')}
itemComponent={AddNodePopoverSelectItem} itemComponent={AddNodePopoverSelectItem}
filter={filter} filter={filter}
onChange={handleChange} onChange={handleChange}

View File

@ -22,6 +22,7 @@ import { memo, useMemo } from 'react';
import { FaInfoCircle } from 'react-icons/fa'; import { FaInfoCircle } from 'react-icons/fa';
import NotesTextarea from './NotesTextarea'; import NotesTextarea from './NotesTextarea';
import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch'; import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
nodeId: string; nodeId: string;
@ -32,6 +33,7 @@ const InvocationNodeNotes = ({ nodeId }: Props) => {
const label = useNodeLabel(nodeId); const label = useNodeLabel(nodeId);
const title = useNodeTemplateTitle(nodeId); const title = useNodeTemplateTitle(nodeId);
const doVersionsMatch = useDoNodeVersionsMatch(nodeId); const doVersionsMatch = useDoNodeVersionsMatch(nodeId);
const { t } = useTranslation();
return ( return (
<> <>
@ -65,7 +67,7 @@ const InvocationNodeNotes = ({ nodeId }: Props) => {
<Modal isOpen={isOpen} onClose={onClose} isCentered> <Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader>{label || title || 'Unknown Node'}</ModalHeader> <ModalHeader>{label || title || t('nodes.unknownNode')}</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<NotesTextarea nodeId={nodeId} /> <NotesTextarea nodeId={nodeId} />
@ -82,6 +84,7 @@ export default memo(InvocationNodeNotes);
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
const data = useNodeData(nodeId); const data = useNodeData(nodeId);
const nodeTemplate = useNodeTemplate(nodeId); const nodeTemplate = useNodeTemplate(nodeId);
const { t } = useTranslation();
const title = useMemo(() => { const title = useMemo(() => {
if (data?.label && nodeTemplate?.title) { if (data?.label && nodeTemplate?.title) {
@ -96,8 +99,8 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
return nodeTemplate.title; return nodeTemplate.title;
} }
return 'Unknown Node'; return t('nodes.unknownNode');
}, [data, nodeTemplate]); }, [data, nodeTemplate, t]);
const versionComponent = useMemo(() => { const versionComponent = useMemo(() => {
if (!isInvocationNodeData(data) || !nodeTemplate) { if (!isInvocationNodeData(data) || !nodeTemplate) {
@ -107,7 +110,7 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (!data.version) { if (!data.version) {
return ( return (
<Text as="span" sx={{ color: 'error.500' }}> <Text as="span" sx={{ color: 'error.500' }}>
Version unknown {t('nodes.versionUnknown')}
</Text> </Text>
); );
} }
@ -115,7 +118,7 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (!nodeTemplate.version) { if (!nodeTemplate.version) {
return ( return (
<Text as="span" sx={{ color: 'error.500' }}> <Text as="span" sx={{ color: 'error.500' }}>
Version {data.version} (unknown template) {t('nodes.version')} {data.version} ({t('nodes.unknownTemplate')})
</Text> </Text>
); );
} }
@ -123,7 +126,7 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (compare(data.version, nodeTemplate.version, '<')) { if (compare(data.version, nodeTemplate.version, '<')) {
return ( return (
<Text as="span" sx={{ color: 'error.500' }}> <Text as="span" sx={{ color: 'error.500' }}>
Version {data.version} (update node) {t('nodes.version')} {data.version} ({t('nodes.updateNode')})
</Text> </Text>
); );
} }
@ -131,16 +134,20 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (compare(data.version, nodeTemplate.version, '>')) { if (compare(data.version, nodeTemplate.version, '>')) {
return ( return (
<Text as="span" sx={{ color: 'error.500' }}> <Text as="span" sx={{ color: 'error.500' }}>
Version {data.version} (update app) {t('nodes.version')} {data.version} ({t('nodes.updateApp')})
</Text> </Text>
); );
} }
return <Text as="span">Version {data.version}</Text>; return (
}, [data, nodeTemplate]); <Text as="span">
{t('nodes.version')} {data.version}
</Text>
);
}, [data, nodeTemplate, t]);
if (!isInvocationNodeData(data)) { if (!isInvocationNodeData(data)) {
return <Text sx={{ fontWeight: 600 }}>Unknown Node</Text>; return <Text sx={{ fontWeight: 600 }}>{t('nodes.unknownNode')}</Text>;
} }
return ( return (

View File

@ -14,6 +14,7 @@ import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types'; import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa'; import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
nodeId: string; nodeId: string;
@ -72,10 +73,10 @@ type TooltipLabelProps = {
const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => { const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
const { status, progress, progressImage } = nodeExecutionState; const { status, progress, progressImage } = nodeExecutionState;
const { t } = useTranslation();
if (status === NodeStatus.PENDING) { if (status === NodeStatus.PENDING) {
return <Text>Pending</Text>; return <Text>Pending</Text>;
} }
if (status === NodeStatus.IN_PROGRESS) { if (status === NodeStatus.IN_PROGRESS) {
if (progressImage) { if (progressImage) {
return ( return (
@ -97,18 +98,22 @@ const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
} }
if (progress !== null) { if (progress !== null) {
return <Text>In Progress ({Math.round(progress * 100)}%)</Text>; return (
<Text>
{t('nodes.executionStateInProgress')} ({Math.round(progress * 100)}%)
</Text>
);
} }
return <Text>In Progress</Text>; return <Text>{t('nodes.executionStateInProgress')}</Text>;
} }
if (status === NodeStatus.COMPLETED) { if (status === NodeStatus.COMPLETED) {
return <Text>Completed</Text>; return <Text>{t('nodes.executionStateCompleted')}</Text>;
} }
if (status === NodeStatus.FAILED) { if (status === NodeStatus.FAILED) {
return <Text>nodeExecutionState.error</Text>; return <Text>{t('nodes.executionStateError')}</Text>;
} }
return null; return null;

View File

@ -5,10 +5,12 @@ import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice'; import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNodeData } from 'features/nodes/types/types'; import { isInvocationNodeData } from 'features/nodes/types/types';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => { const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const data = useNodeData(nodeId); const data = useNodeData(nodeId);
const { t } = useTranslation();
const handleNotesChanged = useCallback( const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => { (e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value })); dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
@ -20,7 +22,7 @@ const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
} }
return ( return (
<FormControl> <FormControl>
<FormLabel>Notes</FormLabel> <FormLabel>{t('nodes.notes')}</FormLabel>
<IAITextarea <IAITextarea
value={data?.notes} value={data?.notes}
onChange={handleNotesChanged} onChange={handleNotesChanged}

View File

@ -14,6 +14,7 @@ import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
import FieldTooltipContent from './FieldTooltipContent'; import FieldTooltipContent from './FieldTooltipContent';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
nodeId: string; nodeId: string;
@ -33,10 +34,11 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
} = props; } = props;
const label = useFieldLabel(nodeId, fieldName); const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [localTitle, setLocalTitle] = useState( const [localTitle, setLocalTitle] = useState(
label || fieldTemplateTitle || 'Unknown Field' label || fieldTemplateTitle || t('nodes.unknownFeild')
); );
const handleSubmit = useCallback( const handleSubmit = useCallback(
@ -44,10 +46,10 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) { if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
return; return;
} }
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field'); setLocalTitle(newTitle || fieldTemplateTitle || t('nodes.unknownField'));
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
}, },
[label, fieldTemplateTitle, dispatch, nodeId, fieldName] [label, fieldTemplateTitle, dispatch, nodeId, fieldName, t]
); );
const handleChange = useCallback((newTitle: string) => { const handleChange = useCallback((newTitle: string) => {
@ -56,8 +58,8 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
useEffect(() => { useEffect(() => {
// Another component may change the title; sync local title with global state // Another component may change the title; sync local title with global state
setLocalTitle(label || fieldTemplateTitle || 'Unknown Field'); setLocalTitle(label || fieldTemplateTitle || t('nodes.unknownField'));
}, [label, fieldTemplateTitle]); }, [label, fieldTemplateTitle, t]);
return ( return (
<Tooltip <Tooltip

View File

@ -17,6 +17,7 @@ import {
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react'; import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus } from 'react-icons/fa'; import { FaMinus, FaPlus } from 'react-icons/fa';
import { menuListMotionProps } from 'theme/components/menu'; import { menuListMotionProps } from 'theme/components/menu';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
nodeId: string; nodeId: string;
@ -30,6 +31,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
const label = useFieldLabel(nodeId, fieldName); const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
const input = useFieldInputKind(nodeId, fieldName); const input = useFieldInputKind(nodeId, fieldName);
const { t } = useTranslation();
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => { const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
@ -119,7 +121,9 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
motionProps={menuListMotionProps} motionProps={menuListMotionProps}
onContextMenu={skipEvent} onContextMenu={skipEvent}
> >
<MenuGroup title={label || fieldTemplateTitle || 'Unknown Field'}> <MenuGroup
title={label || fieldTemplateTitle || t('nodes.unknownField')}
>
{menuItems} {menuItems}
</MenuGroup> </MenuGroup>
</MenuList> </MenuList>

View File

@ -8,6 +8,7 @@ import {
} from 'features/nodes/types/types'; } from 'features/nodes/types/types';
import { startCase } from 'lodash-es'; import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
nodeId: string; nodeId: string;
@ -19,6 +20,7 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
const field = useFieldData(nodeId, fieldName); const field = useFieldData(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const isInputTemplate = isInputFieldTemplate(fieldTemplate); const isInputTemplate = isInputFieldTemplate(fieldTemplate);
const { t } = useTranslation();
const fieldTitle = useMemo(() => { const fieldTitle = useMemo(() => {
if (isInputFieldValue(field)) { if (isInputFieldValue(field)) {
if (field.label && fieldTemplate?.title) { if (field.label && fieldTemplate?.title) {
@ -33,11 +35,11 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
return fieldTemplate.title; return fieldTemplate.title;
} }
return 'Unknown Field'; return t('nodes.unknownField');
} else { } else {
return fieldTemplate?.title || 'Unknown Field'; return fieldTemplate?.title || t('nodes.unknownField');
} }
}, [field, fieldTemplate]); }, [field, fieldTemplate, t]);
return ( return (
<Flex sx={{ flexDir: 'column' }}> <Flex sx={{ flexDir: 'column' }}>

View File

@ -17,6 +17,7 @@ import { FaInfoCircle, FaTrash } from 'react-icons/fa';
import EditableFieldTitle from './EditableFieldTitle'; import EditableFieldTitle from './EditableFieldTitle';
import FieldTooltipContent from './FieldTooltipContent'; import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer'; import InputFieldRenderer from './InputFieldRenderer';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
nodeId: string; nodeId: string;
@ -27,7 +28,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = const { isMouseOverNode, handleMouseOut, handleMouseOver } =
useMouseOverNode(nodeId); useMouseOverNode(nodeId);
const { t } = useTranslation();
const handleRemoveField = useCallback(() => { const handleRemoveField = useCallback(() => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]); }, [dispatch, fieldName, nodeId]);
@ -75,8 +76,8 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
</Flex> </Flex>
</Tooltip> </Tooltip>
<IAIIconButton <IAIIconButton
aria-label="Remove from Linear View" aria-label={t('nodes.removeLinearView')}
tooltip="Remove from Linear View" tooltip={t('nodes.removeLinearView')}
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={handleRemoveField} onClick={handleRemoveField}

View File

@ -35,7 +35,11 @@ const EnumInputFieldComponent = (
value={field.value} value={field.value}
> >
{fieldTemplate.options.map((option) => ( {fieldTemplate.options.map((option) => (
<option key={option}>{option}</option> <option key={option} value={option}>
{fieldTemplate.ui_choice_labels
? fieldTemplate.ui_choice_labels[option]
: option}
</option>
))} ))}
</Select> </Select>
); );

View File

@ -14,6 +14,7 @@ import { modelIdToLoRAModelParam } from 'features/parameters/util/modelIdToLoRAM
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models';
import { useTranslation } from 'react-i18next';
const LoRAModelInputFieldComponent = ( const LoRAModelInputFieldComponent = (
props: FieldComponentProps< props: FieldComponentProps<
@ -25,6 +26,7 @@ const LoRAModelInputFieldComponent = (
const lora = field.value; const lora = field.value;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { data: loraModels } = useGetLoRAModelsQuery(); const { data: loraModels } = useGetLoRAModelsQuery();
const { t } = useTranslation();
const data = useMemo(() => { const data = useMemo(() => {
if (!loraModels) { if (!loraModels) {
@ -92,9 +94,11 @@ const LoRAModelInputFieldComponent = (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
className="nowheel nodrag" className="nowheel nodrag"
value={selectedLoRAModel?.id ?? null} value={selectedLoRAModel?.id ?? null}
placeholder={data.length > 0 ? 'Select a LoRA' : 'No LoRAs available'} placeholder={
data.length > 0 ? t('models.selectLoRA') : t('models.noLoRAsAvailable')
}
data={data} data={data}
nothingFound="No matching LoRAs" nothingFound={t('models.noMatchingLoRAs')}
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0} disabled={data.length === 0}
filter={(value, item: SelectItem) => filter={(value, item: SelectItem) =>

View File

@ -19,6 +19,7 @@ import {
useGetMainModelsQuery, useGetMainModelsQuery,
useGetOnnxModelsQuery, useGetOnnxModelsQuery,
} from 'services/api/endpoints/models'; } from 'services/api/endpoints/models';
import { useTranslation } from 'react-i18next';
const MainModelInputFieldComponent = ( const MainModelInputFieldComponent = (
props: FieldComponentProps< props: FieldComponentProps<
@ -29,7 +30,7 @@ const MainModelInputFieldComponent = (
const { nodeId, field } = props; const { nodeId, field } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
const { t } = useTranslation();
const { data: onnxModels, isLoading: isLoadingOnnxModels } = const { data: onnxModels, isLoading: isLoadingOnnxModels } =
useGetOnnxModelsQuery(NON_SDXL_MAIN_MODELS); useGetOnnxModelsQuery(NON_SDXL_MAIN_MODELS);
const { data: mainModels, isLoading: isLoadingMainModels } = const { data: mainModels, isLoading: isLoadingMainModels } =
@ -127,7 +128,9 @@ const MainModelInputFieldComponent = (
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
value={selectedModel?.id} value={selectedModel?.id}
placeholder={ placeholder={
data.length > 0 ? 'Select a model' : 'No models available' data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
} }
data={data} data={data}
error={!selectedModel} error={!selectedModel}

View File

@ -89,7 +89,7 @@ const RefinerModelInputFieldComponent = (
return isLoading ? ( return isLoading ? (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('modelManager.model')} label={t('modelManager.model')}
placeholder="Loading..." placeholder={t('models.loading')}
disabled={true} disabled={true}
data={[]} data={[]}
/> />
@ -99,7 +99,11 @@ const RefinerModelInputFieldComponent = (
className="nowheel nodrag" className="nowheel nodrag"
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
value={selectedModel?.id} value={selectedModel?.id}
placeholder={data.length > 0 ? 'Select a model' : 'No models available'} placeholder={
data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
}
data={data} data={data}
error={!selectedModel} error={!selectedModel}
disabled={data.length === 0} disabled={data.length === 0}

View File

@ -116,7 +116,7 @@ const ModelInputFieldComponent = (
return isLoading ? ( return isLoading ? (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('modelManager.model')} label={t('modelManager.model')}
placeholder="Loading..." placeholder={t('models.loading')}
disabled={true} disabled={true}
data={[]} data={[]}
/> />
@ -126,7 +126,11 @@ const ModelInputFieldComponent = (
className="nowheel nodrag" className="nowheel nodrag"
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
value={selectedModel?.id} value={selectedModel?.id}
placeholder={data.length > 0 ? 'Select a model' : 'No models available'} placeholder={
data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
}
data={data} data={data}
error={!selectedModel} error={!selectedModel}
disabled={data.length === 0} disabled={data.length === 0}

View File

@ -12,6 +12,7 @@ import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle'
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice'; import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
nodeId: string; nodeId: string;
@ -22,16 +23,17 @@ const NodeTitle = ({ nodeId, title }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const label = useNodeLabel(nodeId); const label = useNodeLabel(nodeId);
const templateTitle = useNodeTemplateTitle(nodeId); const templateTitle = useNodeTemplateTitle(nodeId);
const { t } = useTranslation();
const [localTitle, setLocalTitle] = useState(''); const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (newTitle: string) => { async (newTitle: string) => {
dispatch(nodeLabelChanged({ nodeId, label: newTitle })); dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
setLocalTitle( setLocalTitle(
newTitle || title || templateTitle || 'Problem Setting Title' label || title || templateTitle || t('nodes.problemSettingTitle')
); );
}, },
[dispatch, nodeId, title, templateTitle] [dispatch, nodeId, title, templateTitle, label, t]
); );
const handleChange = useCallback((newTitle: string) => { const handleChange = useCallback((newTitle: string) => {
@ -40,8 +42,10 @@ const NodeTitle = ({ nodeId, title }: Props) => {
useEffect(() => { useEffect(() => {
// Another component may change the title; sync local title with global state // Another component may change the title; sync local title with global state
setLocalTitle(label || title || templateTitle || 'Problem Setting Title'); setLocalTitle(
}, [label, templateTitle, title]); label || title || templateTitle || t('nodes.problemSettingTitle')
);
}, [label, templateTitle, title, t]);
return ( return (
<Flex <Flex

View File

@ -8,10 +8,12 @@ import {
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice'; import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function NodeOpacitySlider() { export default function NodeOpacitySlider() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const nodeOpacity = useAppSelector((state) => state.nodes.nodeOpacity); const nodeOpacity = useAppSelector((state) => state.nodes.nodeOpacity);
const { t } = useTranslation();
const handleChange = useCallback( const handleChange = useCallback(
(v: number) => { (v: number) => {
@ -23,7 +25,7 @@ export default function NodeOpacitySlider() {
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<Slider <Slider
aria-label="Node Opacity" aria-label={t('nodes.nodeOpacity')}
value={nodeOpacity} value={nodeOpacity}
min={0.5} min={0.5}
max={1} max={1}

View File

@ -4,10 +4,11 @@ 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 { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
const TopLeftPanel = () => { const TopLeftPanel = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleOpenAddNodePopover = useCallback(() => { const handleOpenAddNodePopover = useCallback(() => {
dispatch(addNodePopoverOpened()); dispatch(addNodePopoverOpened());
}, [dispatch]); }, [dispatch]);
@ -15,8 +16,8 @@ const TopLeftPanel = () => {
return ( return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}> <Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
<IAIIconButton <IAIIconButton
tooltip="Add Node (Shift+A, Space)" tooltip={t('nodes.addNodeToolTip')}
aria-label="Add Node" aria-label={t('nodes.addNode')}
icon={<FaPlus />} icon={<FaPlus />}
onClick={handleOpenAddNodePopover} onClick={handleOpenAddNodePopover}
/> />

View File

@ -29,6 +29,7 @@ import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa'; import { FaCog } from 'react-icons/fa';
import { SelectionMode } from 'reactflow'; import { SelectionMode } from 'reactflow';
import ReloadNodeTemplatesButton from '../TopCenterPanel/ReloadSchemaButton'; import ReloadNodeTemplatesButton from '../TopCenterPanel/ReloadSchemaButton';
import { useTranslation } from 'react-i18next';
const formLabelProps: FormLabelProps = { const formLabelProps: FormLabelProps = {
fontWeight: 600, fontWeight: 600,
@ -101,12 +102,14 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
[dispatch] [dispatch]
); );
const { t } = useTranslation();
return ( return (
<> <>
<IAIIconButton <IAIIconButton
ref={ref} ref={ref}
aria-label="Workflow Editor Settings" aria-label={t('nodes.workflowSettings')}
tooltip="Workflow Editor Settings" tooltip={t('nodes.workflowSettings')}
icon={<FaCog />} icon={<FaCog />}
onClick={onOpen} onClick={onOpen}
/> />
@ -114,7 +117,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered> <Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader>Workflow Editor Settings</ModalHeader> <ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Flex <Flex
@ -129,31 +132,31 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
formLabelProps={formLabelProps} formLabelProps={formLabelProps}
onChange={handleChangeShouldAnimate} onChange={handleChangeShouldAnimate}
isChecked={shouldAnimateEdges} isChecked={shouldAnimateEdges}
label="Animated Edges" label={t('nodes.animatedEdges')}
helperText="Animate selected edges and edges connected to selected nodes" helperText={t('nodes.animatedEdgesHelp')}
/> />
<Divider /> <Divider />
<IAISwitch <IAISwitch
formLabelProps={formLabelProps} formLabelProps={formLabelProps}
isChecked={shouldSnapToGrid} isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnap} onChange={handleChangeShouldSnap}
label="Snap to Grid" label={t('nodes.snapToGrid')}
helperText="Snap nodes to grid when moved" helperText={t('nodes.snapToGridHelp')}
/> />
<Divider /> <Divider />
<IAISwitch <IAISwitch
formLabelProps={formLabelProps} formLabelProps={formLabelProps}
isChecked={shouldColorEdges} isChecked={shouldColorEdges}
onChange={handleChangeShouldColor} onChange={handleChangeShouldColor}
label="Color-Code Edges" label={t('nodes.colorCodeEdges')}
helperText="Color-code edges according to their connected fields" helperText={t('nodes.colorCodeEdgesHelp')}
/> />
<IAISwitch <IAISwitch
formLabelProps={formLabelProps} formLabelProps={formLabelProps}
isChecked={selectionModeIsChecked} isChecked={selectionModeIsChecked}
onChange={handleChangeSelectionMode} onChange={handleChangeSelectionMode}
label="Fully Contain Nodes to Select" label={t('nodes.fullyContainNodes')}
helperText="Nodes must be fully inside the selection box to be selected" helperText={t('nodes.fullyContainNodesHelp')}
/> />
<Heading size="sm" pt={4}> <Heading size="sm" pt={4}>
Advanced Advanced
@ -162,8 +165,8 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
formLabelProps={formLabelProps} formLabelProps={formLabelProps}
isChecked={shouldValidateGraph} isChecked={shouldValidateGraph}
onChange={handleChangeShouldValidate} onChange={handleChangeShouldValidate}
label="Validate Connections and Graph" label={t('nodes.validateConnections')}
helperText="Prevent invalid connections from being made, and invalid graphs from being invoked" helperText={t('nodes.validateConnectionsHelp')}
/> />
<ReloadNodeTemplatesButton /> <ReloadNodeTemplatesButton />
</Flex> </Flex>

View File

@ -9,6 +9,7 @@ import { memo } from 'react';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea'; import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
import NodeTitle from '../../flow/nodes/common/NodeTitle'; import NodeTitle from '../../flow/nodes/common/NodeTitle';
import ScrollableContent from '../ScrollableContent'; import ScrollableContent from '../ScrollableContent';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -34,9 +35,12 @@ const selector = createSelector(
const InspectorDetailsTab = () => { const InspectorDetailsTab = () => {
const { data, template } = useAppSelector(selector); const { data, template } = useAppSelector(selector);
const { t } = useTranslation();
if (!template || !data) { if (!template || !data) {
return <IAINoContentFallback label="No node selected" icon={null} />; return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
} }
return <Content data={data} template={template} />; return <Content data={data} template={template} />;

View File

@ -11,6 +11,7 @@ import { ImageOutput } from 'services/api/types';
import { AnyResult } from 'services/events/types'; import { AnyResult } from 'services/events/types';
import ScrollableContent from '../ScrollableContent'; import ScrollableContent from '../ScrollableContent';
import ImageOutputPreview from './outputs/ImageOutputPreview'; import ImageOutputPreview from './outputs/ImageOutputPreview';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -40,13 +41,18 @@ const selector = createSelector(
const InspectorOutputsTab = () => { const InspectorOutputsTab = () => {
const { node, template, nes } = useAppSelector(selector); const { node, template, nes } = useAppSelector(selector);
const { t } = useTranslation();
if (!node || !nes || !isInvocationNode(node)) { if (!node || !nes || !isInvocationNode(node)) {
return <IAINoContentFallback label="No node selected" icon={null} />; return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
} }
if (nes.outputs.length === 0) { if (nes.outputs.length === 0) {
return <IAINoContentFallback label="No outputs recorded" icon={null} />; return (
<IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} />
);
} }
return ( return (
@ -77,7 +83,7 @@ const InspectorOutputsTab = () => {
/> />
)) ))
) : ( ) : (
<DataViewer data={nes.outputs} label="Node Outputs" /> <DataViewer data={nes.outputs} label={t('nodes.nodesOutputs')} />
)} )}
</Flex> </Flex>
</ScrollableContent> </ScrollableContent>

View File

@ -5,6 +5,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -29,12 +30,15 @@ const selector = createSelector(
const NodeTemplateInspector = () => { const NodeTemplateInspector = () => {
const { template } = useAppSelector(selector); const { template } = useAppSelector(selector);
const { t } = useTranslation();
if (!template) { if (!template) {
return <IAINoContentFallback label="No node selected" icon={null} />; return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
} }
return <DataViewer data={template} label="Node Template" />; return <DataViewer data={template} label={t('nodes.NodeTemplate')} />;
}; };
export default memo(NodeTemplateInspector); export default memo(NodeTemplateInspector);

View File

@ -16,6 +16,7 @@ import {
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import ScrollableContent from '../ScrollableContent'; import ScrollableContent from '../ScrollableContent';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -85,6 +86,8 @@ const WorkflowGeneralTab = () => {
[dispatch] [dispatch]
); );
const { t } = useTranslation();
return ( return (
<ScrollableContent> <ScrollableContent>
<Flex <Flex
@ -96,28 +99,36 @@ const WorkflowGeneralTab = () => {
}} }}
> >
<Flex sx={{ gap: 2, w: 'full' }}> <Flex sx={{ gap: 2, w: 'full' }}>
<IAIInput label="Name" value={name} onChange={handleChangeName} />
<IAIInput <IAIInput
label="Version" label={t('nodes.workflowName')}
value={name}
onChange={handleChangeName}
/>
<IAIInput
label={t('nodes.workflowVersion')}
value={version} value={version}
onChange={handleChangeVersion} onChange={handleChangeVersion}
/> />
</Flex> </Flex>
<Flex sx={{ gap: 2, w: 'full' }}> <Flex sx={{ gap: 2, w: 'full' }}>
<IAIInput <IAIInput
label="Author" label={t('nodes.workflowAuthor')}
value={author} value={author}
onChange={handleChangeAuthor} onChange={handleChangeAuthor}
/> />
<IAIInput <IAIInput
label="Contact" label={t('nodes.workflowContact')}
value={contact} value={contact}
onChange={handleChangeContact} onChange={handleChangeContact}
/> />
</Flex> </Flex>
<IAIInput label="Tags" value={tags} onChange={handleChangeTags} /> <IAIInput
label={t('nodes.workflowTags')}
value={tags}
onChange={handleChangeTags}
/>
<FormControl as={Flex} sx={{ flexDir: 'column' }}> <FormControl as={Flex} sx={{ flexDir: 'column' }}>
<FormLabel>Short Description</FormLabel> <FormLabel>{t('nodes.workflowDescription')}</FormLabel>
<IAITextarea <IAITextarea
onChange={handleChangeDescription} onChange={handleChangeDescription}
value={description} value={description}
@ -126,7 +137,7 @@ const WorkflowGeneralTab = () => {
/> />
</FormControl> </FormControl>
<FormControl as={Flex} sx={{ flexDir: 'column', h: 'full' }}> <FormControl as={Flex} sx={{ flexDir: 'column', h: 'full' }}>
<FormLabel>Notes</FormLabel> <FormLabel>{t('nodes.workflowNotes')}</FormLabel>
<IAITextarea <IAITextarea
onChange={handleChangeNotes} onChange={handleChangeNotes}
value={notes} value={notes}

View File

@ -2,9 +2,11 @@ import { Flex } from '@chakra-ui/react';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const WorkflowJSONTab = () => { const WorkflowJSONTab = () => {
const workflow = useWorkflow(); const workflow = useWorkflow();
const { t } = useTranslation();
return ( return (
<Flex <Flex
@ -15,7 +17,7 @@ const WorkflowJSONTab = () => {
h: 'full', h: 'full',
}} }}
> >
<DataViewer data={workflow} label="Workflow" /> <DataViewer data={workflow} label={t('nodes.workflow')} />
</Flex> </Flex>
); );
}; };

View File

@ -7,6 +7,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { memo } from 'react'; import { memo } from 'react';
import LinearViewField from '../../flow/nodes/Invocation/fields/LinearViewField'; import LinearViewField from '../../flow/nodes/Invocation/fields/LinearViewField';
import ScrollableContent from '../ScrollableContent'; import ScrollableContent from '../ScrollableContent';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -20,6 +21,7 @@ const selector = createSelector(
const WorkflowLinearTab = () => { const WorkflowLinearTab = () => {
const { fields } = useAppSelector(selector); const { fields } = useAppSelector(selector);
const { t } = useTranslation();
return ( return (
<Box <Box
@ -51,7 +53,7 @@ const WorkflowLinearTab = () => {
)) ))
) : ( ) : (
<IAINoContentFallback <IAINoContentFallback
label="No fields added to Linear View" label={t('nodes.noFieldsLinearview')}
icon={null} icon={null}
/> />
)} )}

View File

@ -9,10 +9,12 @@ import { memo, useCallback } from 'react';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { fromZodError, fromZodIssue } from 'zod-validation-error'; import { fromZodError, fromZodIssue } from 'zod-validation-error';
import { workflowLoadRequested } from '../store/actions'; import { workflowLoadRequested } from '../store/actions';
import { useTranslation } from 'react-i18next';
export const useLoadWorkflowFromFile = () => { export const useLoadWorkflowFromFile = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const logger = useLogger('nodes'); const logger = useLogger('nodes');
const { t } = useTranslation();
const loadWorkflowFromFile = useCallback( const loadWorkflowFromFile = useCallback(
(file: File | null) => { (file: File | null) => {
if (!file) { if (!file) {
@ -28,7 +30,7 @@ export const useLoadWorkflowFromFile = () => {
if (!result.success) { if (!result.success) {
const { message } = fromZodError(result.error, { const { message } = fromZodError(result.error, {
prefix: 'Workflow Validation Error', prefix: t('nodes.workflowValidation'),
}); });
logger.error({ error: parseify(result.error) }, message); logger.error({ error: parseify(result.error) }, message);
@ -36,7 +38,7 @@ export const useLoadWorkflowFromFile = () => {
dispatch( dispatch(
addToast( addToast(
makeToast({ makeToast({
title: 'Unable to Validate Workflow', title: t('nodes.unableToValidateWorkflow'),
status: 'error', status: 'error',
duration: 5000, duration: 5000,
}) })
@ -54,7 +56,7 @@ export const useLoadWorkflowFromFile = () => {
dispatch( dispatch(
addToast( addToast(
makeToast({ makeToast({
title: 'Unable to Load Workflow', title: t('nodes.unableToLoadWorkflow'),
status: 'error', status: 'error',
}) })
) )
@ -64,7 +66,7 @@ export const useLoadWorkflowFromFile = () => {
reader.readAsText(file); reader.readAsText(file);
}, },
[dispatch, logger] [dispatch, logger, t]
); );
return loadWorkflowFromFile; return loadWorkflowFromFile;

View File

@ -9,6 +9,7 @@ import {
} from 'features/nodes/types/constants'; } from 'features/nodes/types/constants';
import { FieldType } from 'features/nodes/types/types'; import { FieldType } from 'features/nodes/types/types';
import { HandleType } from 'reactflow'; import { HandleType } from 'reactflow';
import i18n from 'i18next';
/** /**
* NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts`
@ -20,17 +21,17 @@ export const makeConnectionErrorSelector = (
fieldName: string, fieldName: string,
handleType: HandleType, handleType: HandleType,
fieldType?: FieldType fieldType?: FieldType
) => ) => {
createSelector(stateSelector, (state) => { return createSelector(stateSelector, (state) => {
if (!fieldType) { if (!fieldType) {
return 'No field type'; return i18n.t('nodes.noFieldType');
} }
const { currentConnectionFieldType, connectionStartParams, nodes, edges } = const { currentConnectionFieldType, connectionStartParams, nodes, edges } =
state.nodes; state.nodes;
if (!connectionStartParams || !currentConnectionFieldType) { if (!connectionStartParams || !currentConnectionFieldType) {
return 'No connection in progress'; return i18n.t('nodes.noConnectionInProgress');
} }
const { const {
@ -40,7 +41,7 @@ export const makeConnectionErrorSelector = (
} = connectionStartParams; } = connectionStartParams;
if (!connectionHandleType || !connectionNodeId || !connectionFieldName) { if (!connectionHandleType || !connectionNodeId || !connectionFieldName) {
return 'No connection data'; return i18n.t('nodes.noConnectionData');
} }
const targetType = const targetType =
@ -49,14 +50,14 @@ export const makeConnectionErrorSelector = (
handleType === 'source' ? fieldType : currentConnectionFieldType; handleType === 'source' ? fieldType : currentConnectionFieldType;
if (nodeId === connectionNodeId) { if (nodeId === connectionNodeId) {
return 'Cannot connect to self'; return i18n.t('nodes.cannotConnectToSelf');
} }
if (handleType === connectionHandleType) { if (handleType === connectionHandleType) {
if (handleType === 'source') { if (handleType === 'source') {
return 'Cannot connect output to output'; return i18n.t('nodes.cannotConnectOutputToOutput');
} }
return 'Cannot connect input to input'; return i18n.t('nodes.cannotConnectInputToInput');
} }
if ( if (
@ -66,7 +67,7 @@ export const makeConnectionErrorSelector = (
// except CollectionItem inputs can have multiples // except CollectionItem inputs can have multiples
targetType !== 'CollectionItem' targetType !== 'CollectionItem'
) { ) {
return 'Input may only have one connection'; return i18n.t('nodes.inputMayOnlyHaveOneConnection');
} }
/** /**
@ -125,7 +126,7 @@ export const makeConnectionErrorSelector = (
isIntToFloat isIntToFloat
) )
) { ) {
return 'Field types must match'; return i18n.t('nodes.fieldTypesMustMatch');
} }
} }
@ -137,8 +138,9 @@ export const makeConnectionErrorSelector = (
); );
if (!isGraphAcyclic) { if (!isGraphAcyclic) {
return 'Connection would create a cycle'; return i18n.t('nodes.connectionWouldCreateCycle');
} }
return null; return null;
}); });
};

View File

@ -286,7 +286,7 @@ export type BooleanPolymorphicInputFieldValue = z.infer<
export const zEnumInputFieldValue = zInputFieldValueBase.extend({ export const zEnumInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('enum'), type: z.literal('enum'),
value: z.union([z.string(), z.number()]).optional(), value: z.string().optional(),
}); });
export type EnumInputFieldValue = z.infer<typeof zEnumInputFieldValue>; export type EnumInputFieldValue = z.infer<typeof zEnumInputFieldValue>;
@ -822,10 +822,10 @@ export type ControlPolymorphicInputFieldTemplate = Omit<
}; };
export type EnumInputFieldTemplate = InputFieldTemplateBase & { export type EnumInputFieldTemplate = InputFieldTemplateBase & {
default: string | number; default: string;
type: 'enum'; type: 'enum';
enumType: 'string' | 'number'; options: string[];
options: Array<string | number>; labels?: { [key: string]: string };
}; };
export type MainModelInputFieldTemplate = InputFieldTemplateBase & { export type MainModelInputFieldTemplate = InputFieldTemplateBase & {

View File

@ -656,8 +656,8 @@ const buildEnumInputFieldTemplate = ({
const template: EnumInputFieldTemplate = { const template: EnumInputFieldTemplate = {
...baseField, ...baseField,
type: 'enum', type: 'enum',
enumType: (schemaObject.type as 'string' | 'number') ?? 'string', // TODO: dangerous? options,
options: options, ui_choice_labels: schemaObject.ui_choice_labels,
default: schemaObject.default ?? options[0], default: schemaObject.default ?? options[0],
}; };

View File

@ -1,8 +1,7 @@
import { InputFieldTemplate, InputFieldValue } from '../types/types'; import { InputFieldTemplate, InputFieldValue } from '../types/types';
const FIELD_VALUE_FALLBACK_MAP = { const FIELD_VALUE_FALLBACK_MAP = {
'enum.number': 0, enum: '',
'enum.string': '',
boolean: false, boolean: false,
BooleanCollection: [], BooleanCollection: [],
BooleanPolymorphic: false, BooleanPolymorphic: false,
@ -62,19 +61,8 @@ export const buildInputFieldValue = (
fieldKind: 'input', fieldKind: 'input',
} as InputFieldValue; } as InputFieldValue;
if (template.type === 'enum') {
if (template.enumType === 'number') {
fieldValue.value =
template.default ?? FIELD_VALUE_FALLBACK_MAP['enum.number'];
}
if (template.enumType === 'string') {
fieldValue.value =
template.default ?? FIELD_VALUE_FALLBACK_MAP['enum.string'];
}
} else {
fieldValue.value = fieldValue.value =
template.default ?? FIELD_VALUE_FALLBACK_MAP[template.type]; template.default ?? FIELD_VALUE_FALLBACK_MAP[template.type];
}
return fieldValue; return fieldValue;
}; };

View File

@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICollapse from 'common/components/IAICollapse'; import IAICollapse from 'common/components/IAICollapse';
import ParamClipSkip from './ParamClipSkip'; import ParamClipSkip from './ParamClipSkip';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -22,13 +23,13 @@ export default function ParamAdvancedCollapse() {
const shouldShowAdvancedOptions = useAppSelector( const shouldShowAdvancedOptions = useAppSelector(
(state: RootState) => state.generation.shouldShowAdvancedOptions (state: RootState) => state.generation.shouldShowAdvancedOptions
); );
const { t } = useTranslation();
if (!shouldShowAdvancedOptions) { if (!shouldShowAdvancedOptions) {
return null; return null;
} }
return ( return (
<IAICollapse label="Advanced" activeLabel={activeLabel}> <IAICollapse label={t('common.advanced')} activeLabel={activeLabel}>
<Flex sx={{ flexDir: 'column', gap: 2 }}> <Flex sx={{ flexDir: 'column', gap: 2 }}>
<ParamClipSkip /> <ParamClipSkip />
</Flex> </Flex>

View File

@ -6,6 +6,7 @@ import IAISwitch from 'common/components/IAISwitch';
import { NoiseUseCPUPopover } from 'features/informationalPopovers/components/noiseUseCPU'; import { NoiseUseCPUPopover } from 'features/informationalPopovers/components/noiseUseCPU';
import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice'; import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -22,6 +23,7 @@ const selector = createSelector(
export const ParamCpuNoiseToggle = () => { export const ParamCpuNoiseToggle = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isDisabled, shouldUseCpuNoise } = useAppSelector(selector); const { isDisabled, shouldUseCpuNoise } = useAppSelector(selector);
const { t } = useTranslation();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(shouldUseCpuNoiseChanged(e.target.checked)); dispatch(shouldUseCpuNoiseChanged(e.target.checked));
@ -30,7 +32,7 @@ export const ParamCpuNoiseToggle = () => {
<NoiseUseCPUPopover> <NoiseUseCPUPopover>
<IAISwitch <IAISwitch
isDisabled={isDisabled} isDisabled={isDisabled}
label="Use CPU Noise" label={t('parameters.useCpuNoise')}
isChecked={shouldUseCpuNoise} isChecked={shouldUseCpuNoise}
onChange={handleChange} onChange={handleChange}
/> />

View File

@ -4,9 +4,11 @@ import IAISwitch from 'common/components/IAISwitch';
import { NoiseEnablePopover } from 'features/informationalPopovers/components/noiseEnable'; import { NoiseEnablePopover } from 'features/informationalPopovers/components/noiseEnable';
import { setShouldUseNoiseSettings } from 'features/parameters/store/generationSlice'; import { setShouldUseNoiseSettings } from 'features/parameters/store/generationSlice';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
export const ParamNoiseToggle = () => { export const ParamNoiseToggle = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const shouldUseNoiseSettings = useAppSelector( const shouldUseNoiseSettings = useAppSelector(
(state: RootState) => state.generation.shouldUseNoiseSettings (state: RootState) => state.generation.shouldUseNoiseSettings
@ -18,7 +20,7 @@ export const ParamNoiseToggle = () => {
return ( return (
<NoiseEnablePopover> <NoiseEnablePopover>
<IAISwitch <IAISwitch
label="Enable Noise Settings" label={t('parameters.enableNoiseSettings')}
isChecked={shouldUseNoiseSettings} isChecked={shouldUseNoiseSettings}
onChange={handleChange} onChange={handleChange}
/> />

View File

@ -146,7 +146,7 @@ const CancelButton = (props: Props) => {
id="cancel-button" id="cancel-button"
{...rest} {...rest}
> >
Cancel {t('parameters.cancel.cancel')}
</IAIButton> </IAIButton>
)} )}
<Menu closeOnSelect={false}> <Menu closeOnSelect={false}>

View File

@ -76,7 +76,7 @@ export default function InvokeButton(props: InvokeButton) {
)} )}
{asIconButton ? ( {asIconButton ? (
<IAIIconButton <IAIIconButton
aria-label={t('parameters.invoke')} aria-label={t('parameters.invoke.invoke')}
type="submit" type="submit"
icon={<FaPlay />} icon={<FaPlay />}
isDisabled={!isReady} isDisabled={!isReady}
@ -96,7 +96,7 @@ export default function InvokeButton(props: InvokeButton) {
) : ( ) : (
<IAIButton <IAIButton
tooltip={<InvokeButtonTooltipContent />} tooltip={<InvokeButtonTooltipContent />}
aria-label={t('parameters.invoke')} aria-label={t('parameters.invoke.invoke')}
type="submit" type="submit"
data-progress={isProcessing} data-progress={isProcessing}
isDisabled={!isReady} isDisabled={!isReady}
@ -105,7 +105,7 @@ export default function InvokeButton(props: InvokeButton) {
id="invoke-button" id="invoke-button"
leftIcon={isProcessing ? undefined : <FaPlay />} leftIcon={isProcessing ? undefined : <FaPlay />}
isLoading={isProcessing} isLoading={isProcessing}
loadingText={t('parameters.invoke')} loadingText={t('parameters.invoke.invoke')}
sx={{ sx={{
w: 'full', w: 'full',
flexGrow: 1, flexGrow: 1,
@ -138,11 +138,14 @@ export const InvokeButtonTooltipContent = memo(() => {
const { isReady, reasons } = useIsReadyToInvoke(); const { isReady, reasons } = useIsReadyToInvoke();
const { autoAddBoardId } = useAppSelector(tooltipSelector); const { autoAddBoardId } = useAppSelector(tooltipSelector);
const autoAddBoardName = useBoardName(autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId);
const { t } = useTranslation();
return ( return (
<Flex flexDir="column" gap={1}> <Flex flexDir="column" gap={1}>
<Text fontWeight={600}> <Text fontWeight={600}>
{isReady ? 'Ready to Invoke' : 'Unable to Invoke'} {isReady
? t('parameters.invoke.readyToInvoke')
: t('parameters.invoke.unableToInvoke')}
</Text> </Text>
{reasons.length > 0 && ( {reasons.length > 0 && (
<UnorderedList> <UnorderedList>
@ -159,7 +162,7 @@ export const InvokeButtonTooltipContent = memo(() => {
_dark={{ borderColor: 'base.900' }} _dark={{ borderColor: 'base.900' }}
/> />
<Text fontWeight={400} fontStyle="oblique 10deg"> <Text fontWeight={400} fontStyle="oblique 10deg">
Adding images to{' '} {t('parameters.invoke.addingImagesTo')}{' '}
<Text as="span" fontWeight={600}> <Text as="span" fontWeight={600}>
{autoAddBoardName || 'Uncategorized'} {autoAddBoardName || 'Uncategorized'}
</Text> </Text>

View File

@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { FaLink } from 'react-icons/fa'; import { FaLink } from 'react-icons/fa';
import { setShouldConcatSDXLStylePrompt } from '../store/sdxlSlice'; import { setShouldConcatSDXLStylePrompt } from '../store/sdxlSlice';
import { useTranslation } from 'react-i18next';
export default function ParamSDXLConcatButton() { export default function ParamSDXLConcatButton() {
const shouldConcatSDXLStylePrompt = useAppSelector( const shouldConcatSDXLStylePrompt = useAppSelector(
@ -10,6 +11,7 @@ export default function ParamSDXLConcatButton() {
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleShouldConcatPromptChange = () => { const handleShouldConcatPromptChange = () => {
dispatch(setShouldConcatSDXLStylePrompt(!shouldConcatSDXLStylePrompt)); dispatch(setShouldConcatSDXLStylePrompt(!shouldConcatSDXLStylePrompt));
@ -17,8 +19,8 @@ export default function ParamSDXLConcatButton() {
return ( return (
<IAIIconButton <IAIIconButton
aria-label="Concatenate Prompt & Style" aria-label={t('sdxl.concatPromptStyle')}
tooltip="Concatenate Prompt & Style" tooltip={t('sdxl.concatPromptStyle')}
variant="outline" variant="outline"
isChecked={shouldConcatSDXLStylePrompt} isChecked={shouldConcatSDXLStylePrompt}
onClick={handleShouldConcatPromptChange} onClick={handleShouldConcatPromptChange}

View File

@ -39,7 +39,7 @@ const ParamSDXLImg2ImgDenoisingStrength = () => {
<SubParametersWrapper> <SubParametersWrapper>
<ParamDenoisingStrengthPopover> <ParamDenoisingStrengthPopover>
<IAISlider <IAISlider
label={`${t('parameters.denoisingStrength')}`} label={t('sdxl.denoisingStrength')}
step={0.01} step={0.01}
min={0} min={0}
max={1} max={1}

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