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
attributes:
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: |
Instead of one huge text field, it would be nice to have forms for bug-reports, feature-requests, ...
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
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.
validations:
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|
|Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator|
|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 | A float primitive value|
|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|
|Extract Image Channel | Gets a channel from an image.|
|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.|
|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.|
@ -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.|
|ImageProcessor | Base class for invocations that preprocess images for ControlNet|
|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|
|Image to Latents | Encodes an image into latents.|
|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)")
PATCH=""
VERSION="v${VERSION}${PATCH}"
LATEST_TAG="v3.0-latest"
LATEST_TAG="v3-latest"
echo Building installer for version $VERSION
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_component: Optional[UIComponent]
ui_order: Optional[int]
ui_choice_labels: Optional[dict[str, str]]
item_default: Optional[Any]
@ -246,6 +247,7 @@ def InputField(
ui_component: Optional[UIComponent] = None,
ui_hidden: bool = False,
ui_order: Optional[int] = None,
ui_choice_labels: Optional[dict[str, str]] = None,
item_default: Optional[Any] = None,
**kwargs: Any,
) -> Any:
@ -312,6 +314,7 @@ def InputField(
ui_hidden=ui_hidden,
ui_order=ui_order,
item_default=item_default,
ui_choice_labels=ui_choice_labels,
**kwargs,
)

View File

@ -38,14 +38,16 @@ class RangeInvocation(BaseInvocation):
version="1.0.0",
)
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")
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")
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(

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):
"""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")
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:
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(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=new_image,
image_origin=ResourceOrigin.INTERNAL,
@ -330,8 +335,8 @@ class ImageResizeInvocation(BaseInvocation):
"""Resizes an image to specific dimensions"""
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)")
height: int = InputField(default=512, ge=64, multiple_of=8, description="The height to resize to (px)")
width: int = InputField(default=512, gt=0, description="The width 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")
metadata: Optional[CoreMetadata] = InputField(
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 .model import ModelInfo, UNetField, VaeField
if choose_torch_device() == torch.device("mps"):
from torch import mps
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
result_latents = result_latents.to("cpu")
torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}"
context.services.latents.save(name, result_latents)
@ -612,6 +617,8 @@ class LatentsToImageInvocation(BaseInvocation):
# clear memory as vae decode can request a lot
torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
with torch.inference_mode():
# copied from diffusers pipeline
@ -624,6 +631,8 @@ class LatentsToImageInvocation(BaseInvocation):
image = VaeImageProcessor.numpy_to_pil(np_image)[0]
torch.cuda.empty_cache()
if choose_torch_device() == torch.device("mps"):
mps.empty_cache()
image_dto = context.services.images.create(
image=image,
@ -683,6 +692,8 @@ class ResizeLatentsInvocation(BaseInvocation):
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}"
# 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
resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}"
# 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
blended_latents = blended_latents.to("cpu")
torch.cuda.empty_cache()
if device == torch.device("mps"):
mps.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}"
# context.services.latents.set(name, resized_latents)

View File

@ -1,8 +1,11 @@
# 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
@ -60,3 +63,201 @@ class RandomIntInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> IntegerOutput:
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
from ..util.devices import choose_torch_device
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
# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously
DEFAULT_MAX_CACHE_SIZE = 6.0
@ -406,6 +410,8 @@ class ModelCache(object):
gc.collect()
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)}")
@ -426,6 +432,8 @@ class ModelCache(object):
gc.collect()
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:
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)
# https://github.com/huggingface/diffusers/pull/4315
# 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:
return super(diffusers.models.lora.LoRACompatibleConv, self).forward(x)
return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states)
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

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

View File

@ -15,6 +15,8 @@ import { socketMiddleware } from 'services/events/middleware';
import Loading from '../../common/components/Loading/Loading';
import '../../i18n';
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 ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -30,6 +32,7 @@ interface Props extends PropsWithChildren {
imageName: string;
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
customStarUi?: CustomStarUi;
}
const InvokeAIUI = ({
@ -40,6 +43,7 @@ const InvokeAIUI = ({
middleware,
projectId,
selectedImage,
customStarUi,
}: Props) => {
useEffect(() => {
// configure API client token
@ -80,17 +84,33 @@ const InvokeAIUI = ({
};
}, [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 (
<React.StrictMode>
<Provider store={store}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<AppDndContext>
<App
config={config}
headerComponent={headerComponent}
selectedImage={selectedImage}
/>
<App config={config} selectedImage={selectedImage} />
</AppDndContext>
</ThemeLocaleProvider>
</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());
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
})
getDefaultMiddleware({ immutableCheck: false })
.concat(api.middleware)
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import {
} from 'features/controlNet/store/controlNetSlice';
import { ControlNetBeginEndPopover } from 'features/informationalPopovers/components/controlNetBeginEnd';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
controlNet: ControlNetConfig;
@ -28,6 +29,7 @@ const ParamControlNetBeginEnd = (props: Props) => {
const { beginStepPct, endStepPct, isEnabled, controlNetId } =
props.controlNet;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleStepPctChanged = useCallback(
(v: number[]) => {
@ -50,10 +52,10 @@ const ParamControlNetBeginEnd = (props: Props) => {
return (
<ControlNetBeginEndPopover>
<FormControl isDisabled={!isEnabled}>
<FormLabel>Begin / End Step Percentage</FormLabel>
<FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel>
<HStack w="100%" gap={2} alignItems="center">
<RangeSlider
aria-label={['Begin Step %', 'End Step %']}
aria-label={['Begin Step %', 'End Step %!']}
value={[beginStepPct, endStepPct]}
onChange={handleStepPctChanged}
min={0}
@ -62,6 +64,22 @@ const ParamControlNetBeginEnd = (props: Props) => {
minStepsBetweenThumbs={5}
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>
<RangeSliderFilledTrack />
</RangeSliderTrack>

View File

@ -7,23 +7,25 @@ import {
} from 'features/controlNet/store/controlNetSlice';
import { ControlNetControlModePopover } from 'features/informationalPopovers/components/controlNetControlMode';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetControlModeProps = {
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(
props: ParamControlNetControlModeProps
) {
const { controlMode, isEnabled, controlNetId } = props.controlNet;
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(
(controlMode: ControlModes) => {
@ -36,7 +38,7 @@ export default function ParamControlNetControlMode(
<ControlNetControlModePopover>
<IAIMantineSelect
disabled={!isEnabled}
label="Control Mode"
label={t('controlnet.controlMode')}
data={CONTROL_MODE_DATA}
value={String(controlMode)}
onChange={handleControlModeChange}

View File

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

View File

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

View File

@ -7,22 +7,24 @@ import {
} from 'features/controlNet/store/controlNetSlice';
import { ControlNetResizeModePopover } from 'features/informationalPopovers/components/controlNetResizeMode';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetResizeModeProps = {
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(
props: ParamControlNetResizeModeProps
) {
const { resizeMode, isEnabled, controlNetId } = props.controlNet;
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(
(resizeMode: ResizeModes) => {
@ -35,7 +37,7 @@ export default function ParamControlNetResizeMode(
<ControlNetResizeModePopover>
<IAIMantineSelect
disabled={!isEnabled}
label="Resize Mode"
label={t('controlnet.resizeMode')}
data={RESIZE_MODE_DATA}
value={String(resizeMode)}
onChange={handleResizeModeChange}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import {
ControlNetProcessorType,
RequiredControlNetProcessorNode,
} from './types';
import i18n from 'i18next';
type ControlNetProcessorsDict = Record<
ControlNetProcessorType,
@ -12,7 +13,6 @@ type ControlNetProcessorsDict = Record<
default: RequiredControlNetProcessorNode | { type: 'none' };
}
>;
/**
* A dict of ControlNet processors, including:
* - type
@ -25,16 +25,24 @@ type ControlNetProcessorsDict = Record<
export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
none: {
type: 'none',
label: 'none',
description: '',
get label() {
return i18n.t('controlnet.none');
},
get description() {
return i18n.t('controlnet.noneDescription');
},
default: {
type: 'none',
},
},
canny_image_processor: {
type: 'canny_image_processor',
label: 'Canny',
description: '',
get label() {
return i18n.t('controlnet.canny');
},
get description() {
return i18n.t('controlnet.cannyDescription');
},
default: {
id: 'canny_image_processor',
type: 'canny_image_processor',
@ -44,8 +52,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
content_shuffle_image_processor: {
type: 'content_shuffle_image_processor',
label: 'Content Shuffle',
description: '',
get label() {
return i18n.t('controlnet.contentShuffle');
},
get description() {
return i18n.t('controlnet.contentShuffleDescription');
},
default: {
id: 'content_shuffle_image_processor',
type: 'content_shuffle_image_processor',
@ -58,8 +70,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
hed_image_processor: {
type: 'hed_image_processor',
label: 'HED',
description: '',
get label() {
return i18n.t('controlnet.hed');
},
get description() {
return i18n.t('controlnet.hedDescription');
},
default: {
id: 'hed_image_processor',
type: 'hed_image_processor',
@ -70,8 +86,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
lineart_anime_image_processor: {
type: 'lineart_anime_image_processor',
label: 'Lineart Anime',
description: '',
get label() {
return i18n.t('controlnet.lineartAnime');
},
get description() {
return i18n.t('controlnet.lineartAnimeDescription');
},
default: {
id: 'lineart_anime_image_processor',
type: 'lineart_anime_image_processor',
@ -81,8 +101,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
lineart_image_processor: {
type: 'lineart_image_processor',
label: 'Lineart',
description: '',
get label() {
return i18n.t('controlnet.lineart');
},
get description() {
return i18n.t('controlnet.lineartDescription');
},
default: {
id: 'lineart_image_processor',
type: 'lineart_image_processor',
@ -93,8 +117,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
mediapipe_face_processor: {
type: 'mediapipe_face_processor',
label: 'Mediapipe Face',
description: '',
get label() {
return i18n.t('controlnet.mediapipeFace');
},
get description() {
return i18n.t('controlnet.mediapipeFaceDescription');
},
default: {
id: 'mediapipe_face_processor',
type: 'mediapipe_face_processor',
@ -104,8 +132,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
midas_depth_image_processor: {
type: 'midas_depth_image_processor',
label: 'Depth (Midas)',
description: '',
get label() {
return i18n.t('controlnet.depthMidas');
},
get description() {
return i18n.t('controlnet.depthMidasDescription');
},
default: {
id: 'midas_depth_image_processor',
type: 'midas_depth_image_processor',
@ -115,8 +147,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
mlsd_image_processor: {
type: 'mlsd_image_processor',
label: 'M-LSD',
description: '',
get label() {
return i18n.t('controlnet.mlsd');
},
get description() {
return i18n.t('controlnet.mlsdDescription');
},
default: {
id: 'mlsd_image_processor',
type: 'mlsd_image_processor',
@ -128,8 +164,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
normalbae_image_processor: {
type: 'normalbae_image_processor',
label: 'Normal BAE',
description: '',
get label() {
return i18n.t('controlnet.normalBae');
},
get description() {
return i18n.t('controlnet.normalBaeDescription');
},
default: {
id: 'normalbae_image_processor',
type: 'normalbae_image_processor',
@ -139,8 +179,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
openpose_image_processor: {
type: 'openpose_image_processor',
label: 'Openpose',
description: '',
get label() {
return i18n.t('controlnet.openPose');
},
get description() {
return i18n.t('controlnet.openPoseDescription');
},
default: {
id: 'openpose_image_processor',
type: 'openpose_image_processor',
@ -151,8 +195,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
pidi_image_processor: {
type: 'pidi_image_processor',
label: 'PIDI',
description: '',
get label() {
return i18n.t('controlnet.pidi');
},
get description() {
return i18n.t('controlnet.pidiDescription');
},
default: {
id: 'pidi_image_processor',
type: 'pidi_image_processor',
@ -164,8 +212,12 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
},
zoe_depth_image_processor: {
type: 'zoe_depth_image_processor',
label: 'Depth (Zoe)',
description: '',
get label() {
return i18n.t('controlnet.depthZoe');
},
get description() {
return i18n.t('controlnet.depthZoeDescription');
},
default: {
id: 'zoe_depth_image_processor',
type: 'zoe_depth_image_processor',
@ -186,4 +238,6 @@ export const CONTROLNET_MODEL_DEFAULT_PROCESSORS: {
shuffle: 'content_shuffle_image_processor',
openpose: 'openpose_image_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 { memo } from 'react';
import { ImageUsage } from '../store/types';
import { useTranslation } from 'react-i18next';
type Props = {
imageUsage?: ImageUsage;
topMessage?: string;
bottomMessage?: string;
};
const ImageUsageMessage = (props: Props) => {
const { t } = useTranslation();
const {
imageUsage,
topMessage = 'This image is currently in use in the following features:',
bottomMessage = 'If you delete this image, those features will immediately be reset.',
topMessage = t('gallery.currentlyInUse'),
bottomMessage = t('gallery.featuresWillReset'),
} = props;
if (!imageUsage) {
@ -26,10 +29,18 @@ const ImageUsageMessage = (props: Props) => {
<>
<Text>{topMessage}</Text>
<UnorderedList sx={{ paddingInlineStart: 6 }}>
{imageUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
{imageUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
{imageUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
{imageUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
{imageUsage.isInitialImage && (
<ListItem>{t('common.img2img')}</ListItem>
)}
{imageUsage.isCanvasImage && (
<ListItem>{t('common.unifiedCanvas')}</ListItem>
)}
{imageUsage.isControlNetImage && (
<ListItem>{t('common.controlNet')}</ListItem>
)}
{imageUsage.isNodesImage && (
<ListItem>{t('common.nodeEditor')}</ListItem>
)}
</UnorderedList>
<Text>{bottomMessage}</Text>
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -132,8 +132,8 @@ const DeleteBoardModal = (props: Props) => {
) : (
<ImageUsageMessage
imageUsage={imageUsageSummary}
topMessage="This board contains images used in the following features:"
bottomMessage="Deleting this board and its images will reset any features currently using them."
topMessage={t('boards.topMessage')}
bottomMessage={t('boards.bottomMessage')}
/>
)}
<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 ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from '../NextPrevImageButtons';
import { useTranslation } from 'react-i18next';
export const imagesSelector = createSelector(
[stateSelector, selectLastSelectedImage],
@ -117,6 +118,8 @@ const CurrentImagePreview = () => {
const timeoutId = useRef(0);
const { t } = useTranslation();
const handleMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current);
@ -164,7 +167,7 @@ const CurrentImagePreview = () => {
isUploadDisabled={true}
fitContainer
useThumbailFallback
dropLabel="Set as Current Image"
dropLabel={t('gallery.setCurrentImage')}
noContentFallback={
<IAINoContentFallback icon={FaImage} label="No image selected" />
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,11 @@ const EnumInputFieldComponent = (
value={field.value}
>
{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>
);

View File

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

View File

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

View File

@ -89,7 +89,7 @@ const RefinerModelInputFieldComponent = (
return isLoading ? (
<IAIMantineSearchableSelect
label={t('modelManager.model')}
placeholder="Loading..."
placeholder={t('models.loading')}
disabled={true}
data={[]}
/>
@ -99,7 +99,11 @@ const RefinerModelInputFieldComponent = (
className="nowheel nodrag"
tooltip={selectedModel?.description}
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}
error={!selectedModel}
disabled={data.length === 0}

View File

@ -116,7 +116,7 @@ const ModelInputFieldComponent = (
return isLoading ? (
<IAIMantineSearchableSelect
label={t('modelManager.model')}
placeholder="Loading..."
placeholder={t('models.loading')}
disabled={true}
data={[]}
/>
@ -126,7 +126,11 @@ const ModelInputFieldComponent = (
className="nowheel nodrag"
tooltip={selectedModel?.description}
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}
error={!selectedModel}
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 { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
@ -22,16 +23,17 @@ const NodeTitle = ({ nodeId, title }: Props) => {
const dispatch = useAppDispatch();
const label = useNodeLabel(nodeId);
const templateTitle = useNodeTemplateTitle(nodeId);
const { t } = useTranslation();
const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback(
async (newTitle: string) => {
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
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) => {
@ -40,8 +42,10 @@ const NodeTitle = ({ nodeId, title }: Props) => {
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(label || title || templateTitle || 'Problem Setting Title');
}, [label, templateTitle, title]);
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')
);
}, [label, templateTitle, title, t]);
return (
<Flex

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { memo } from 'react';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
import NodeTitle from '../../flow/nodes/common/NodeTitle';
import ScrollableContent from '../ScrollableContent';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
stateSelector,
@ -34,9 +35,12 @@ const selector = createSelector(
const InspectorDetailsTab = () => {
const { data, template } = useAppSelector(selector);
const { t } = useTranslation();
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} />;

View File

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

View File

@ -5,6 +5,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
stateSelector,
@ -29,12 +30,15 @@ const selector = createSelector(
const NodeTemplateInspector = () => {
const { template } = useAppSelector(selector);
const { t } = useTranslation();
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -656,8 +656,8 @@ const buildEnumInputFieldTemplate = ({
const template: EnumInputFieldTemplate = {
...baseField,
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],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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