Merge branch 'main' into feat/ui/allow-number-to-string

This commit is contained in:
Jonathan 2023-09-22 21:02:28 -05:00 committed by GitHub
commit 4c9344b0ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1288 additions and 760 deletions

View File

@ -88,6 +88,9 @@ class FieldDescriptions:
num_1 = "The first number" num_1 = "The first number"
num_2 = "The second number" num_2 = "The second number"
mask = "The mask to use for the operation" mask = "The mask to use for the operation"
board = "The board to save the image to"
image = "The image to process"
tile_size = "Tile size"
class Input(str, Enum): class Input(str, Enum):
@ -173,6 +176,7 @@ class UIType(str, Enum):
WorkflowField = "WorkflowField" WorkflowField = "WorkflowField"
IsIntermediate = "IsIntermediate" IsIntermediate = "IsIntermediate"
MetadataField = "MetadataField" MetadataField = "MetadataField"
BoardField = "BoardField"
# endregion # endregion
@ -656,6 +660,8 @@ def invocation(
:param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None. :param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None.
:param Optional[list[str]] tags: Adds tags to the invocation. Invocations may be searched for by their tags. Defaults to None. :param Optional[list[str]] tags: Adds tags to the invocation. Invocations may be searched for by their tags. Defaults to None.
:param Optional[str] category: Adds a category to the invocation. Used to group the invocations in the UI. Defaults to None. :param Optional[str] category: Adds a category to the invocation. Used to group the invocations in the UI. Defaults to None.
:param Optional[str] version: Adds a version to the invocation. Must be a valid semver string. Defaults to None.
:param Optional[bool] use_cache: Whether or not to use the invocation cache. Defaults to True. The user may override this in the workflow editor.
""" """
def wrapper(cls: Type[GenericBaseInvocation]) -> Type[GenericBaseInvocation]: def wrapper(cls: Type[GenericBaseInvocation]) -> Type[GenericBaseInvocation]:

View File

@ -559,3 +559,33 @@ class SamDetectorReproducibleColors(SamDetector):
img[:, :] = ann_color img[:, :] = ann_color
final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255))) final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255)))
return np.array(final_img, dtype=np.uint8) return np.array(final_img, dtype=np.uint8)
@invocation(
"color_map_image_processor",
title="Color Map Processor",
tags=["controlnet"],
category="controlnet",
version="1.0.0",
)
class ColorMapImageProcessorInvocation(ImageProcessorInvocation):
"""Generates a color map from the provided image"""
color_map_tile_size: int = InputField(default=64, ge=0, description=FieldDescriptions.tile_size)
def run_processor(self, image: Image.Image):
image = image.convert("RGB")
image = np.array(image, dtype=np.uint8)
height, width = image.shape[:2]
width_tile_size = min(self.color_map_tile_size, width)
height_tile_size = min(self.color_map_tile_size, height)
color_map = cv2.resize(
image,
(width // width_tile_size, height // height_tile_size),
interpolation=cv2.INTER_CUBIC,
)
color_map = cv2.resize(color_map, (width, height), interpolation=cv2.INTER_NEAREST)
color_map = Image.fromarray(color_map)
return color_map

View File

@ -8,12 +8,12 @@ import numpy
from PIL import Image, ImageChops, ImageFilter, ImageOps from PIL import Image, ImageChops, ImageFilter, ImageOps
from invokeai.app.invocations.metadata import CoreMetadata from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
from invokeai.backend.image_util.safety_checker import SafetyChecker from invokeai.backend.image_util.safety_checker import SafetyChecker
from ..models.image import ImageCategory, ResourceOrigin from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation from .baseinvocation import BaseInvocation, FieldDescriptions, Input, InputField, InvocationContext, invocation
@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") @invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0")
@ -972,13 +972,14 @@ class ImageChannelMultiplyInvocation(BaseInvocation):
title="Save Image", title="Save Image",
tags=["primitives", "image"], tags=["primitives", "image"],
category="primitives", category="primitives",
version="1.0.0", version="1.0.1",
use_cache=False, use_cache=False,
) )
class SaveImageInvocation(BaseInvocation): class SaveImageInvocation(BaseInvocation):
"""Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" """Saves an image. Unlike an image primitive, this invocation stores a copy of the image."""
image: ImageField = InputField(description="The image to load") image: ImageField = InputField(description=FieldDescriptions.image)
board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct)
metadata: CoreMetadata = InputField( metadata: CoreMetadata = InputField(
default=None, default=None,
description=FieldDescriptions.core_metadata, description=FieldDescriptions.core_metadata,
@ -992,6 +993,7 @@ class SaveImageInvocation(BaseInvocation):
image=image, image=image,
image_origin=ResourceOrigin.INTERNAL, image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL, image_category=ImageCategory.GENERAL,
board_id=self.board.board_id if self.board else None,
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,

View File

@ -226,6 +226,12 @@ class ImageField(BaseModel):
image_name: str = Field(description="The name of the image") image_name: str = Field(description="The name of the image")
class BoardField(BaseModel):
"""A board primitive field"""
board_id: str = Field(description="The id of the board")
@invocation_output("image_output") @invocation_output("image_output")
class ImageOutput(BaseInvocationOutput): class ImageOutput(BaseInvocationOutput):
"""Base class for nodes that output a single image""" """Base class for nodes that output a single image"""

View File

@ -8,97 +8,97 @@ from invokeai.app.services.invoker import Invoker
class MemoryInvocationCache(InvocationCacheBase): class MemoryInvocationCache(InvocationCacheBase):
__cache: dict[Union[int, str], tuple[BaseInvocationOutput, str]] _cache: dict[Union[int, str], tuple[BaseInvocationOutput, str]]
__max_cache_size: int _max_cache_size: int
__disabled: bool _disabled: bool
__hits: int _hits: int
__misses: int _misses: int
__cache_ids: Queue _cache_ids: Queue
__invoker: Invoker _invoker: Invoker
def __init__(self, max_cache_size: int = 0) -> None: def __init__(self, max_cache_size: int = 0) -> None:
self.__cache = dict() self._cache = dict()
self.__max_cache_size = max_cache_size self._max_cache_size = max_cache_size
self.__disabled = False self._disabled = False
self.__hits = 0 self._hits = 0
self.__misses = 0 self._misses = 0
self.__cache_ids = Queue() self._cache_ids = Queue()
def start(self, invoker: Invoker) -> None: def start(self, invoker: Invoker) -> None:
self.__invoker = invoker self._invoker = invoker
if self.__max_cache_size == 0: if self._max_cache_size == 0:
return return
self.__invoker.services.images.on_deleted(self._delete_by_match) self._invoker.services.images.on_deleted(self._delete_by_match)
self.__invoker.services.latents.on_deleted(self._delete_by_match) self._invoker.services.latents.on_deleted(self._delete_by_match)
def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]: def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]:
if self.__max_cache_size == 0 or self.__disabled: if self._max_cache_size == 0 or self._disabled:
return return
item = self.__cache.get(key, None) item = self._cache.get(key, None)
if item is not None: if item is not None:
self.__hits += 1 self._hits += 1
return item[0] return item[0]
self.__misses += 1 self._misses += 1
def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None: def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None:
if self.__max_cache_size == 0 or self.__disabled: if self._max_cache_size == 0 or self._disabled:
return return
if key not in self.__cache: if key not in self._cache:
self.__cache[key] = (invocation_output, invocation_output.json()) self._cache[key] = (invocation_output, invocation_output.json())
self.__cache_ids.put(key) self._cache_ids.put(key)
if self.__cache_ids.qsize() > self.__max_cache_size: if self._cache_ids.qsize() > self._max_cache_size:
try: try:
self.__cache.pop(self.__cache_ids.get()) self._cache.pop(self._cache_ids.get())
except KeyError: except KeyError:
# this means the cache_ids are somehow out of sync w/ the cache # this means the cache_ids are somehow out of sync w/ the cache
pass pass
def delete(self, key: Union[int, str]) -> None: def delete(self, key: Union[int, str]) -> None:
if self.__max_cache_size == 0 or self.__disabled: if self._max_cache_size == 0:
return return
if key in self.__cache: if key in self._cache:
del self.__cache[key] del self._cache[key]
def clear(self, *args, **kwargs) -> None: def clear(self, *args, **kwargs) -> None:
if self.__max_cache_size == 0 or self.__disabled: if self._max_cache_size == 0:
return return
self.__cache.clear() self._cache.clear()
self.__cache_ids = Queue() self._cache_ids = Queue()
self.__misses = 0 self._misses = 0
self.__hits = 0 self._hits = 0
def create_key(self, invocation: BaseInvocation) -> int: def create_key(self, invocation: BaseInvocation) -> int:
return hash(invocation.json(exclude={"id"})) return hash(invocation.json(exclude={"id"}))
def disable(self) -> None: def disable(self) -> None:
if self.__max_cache_size == 0: if self._max_cache_size == 0:
return return
self.__disabled = True self._disabled = True
def enable(self) -> None: def enable(self) -> None:
if self.__max_cache_size == 0: if self._max_cache_size == 0:
return return
self.__disabled = False self._disabled = False
def get_status(self) -> InvocationCacheStatus: def get_status(self) -> InvocationCacheStatus:
return InvocationCacheStatus( return InvocationCacheStatus(
hits=self.__hits, hits=self._hits,
misses=self.__misses, misses=self._misses,
enabled=not self.__disabled and self.__max_cache_size > 0, enabled=not self._disabled and self._max_cache_size > 0,
size=len(self.__cache), size=len(self._cache),
max_size=self.__max_cache_size, max_size=self._max_cache_size,
) )
def _delete_by_match(self, to_match: str) -> None: def _delete_by_match(self, to_match: str) -> None:
if self.__max_cache_size == 0 or self.__disabled: if self._max_cache_size == 0:
return return
keys_to_delete = set() keys_to_delete = set()
for key, value_tuple in self.__cache.items(): for key, value_tuple in self._cache.items():
if to_match in value_tuple[1]: if to_match in value_tuple[1]:
keys_to_delete.add(key) keys_to_delete.add(key)
@ -108,4 +108,4 @@ class MemoryInvocationCache(InvocationCacheBase):
for key in keys_to_delete: for key in keys_to_delete:
self.delete(key) self.delete(key)
self.__invoker.services.logger.debug(f"Deleted {len(keys_to_delete)} cached invocation outputs for {to_match}") self._invoker.services.logger.debug(f"Deleted {len(keys_to_delete)} cached invocation outputs for {to_match}")

View File

@ -81,6 +81,7 @@
"load": "Load", "load": "Load",
"loading": "Loading", "loading": "Loading",
"loadingInvokeAI": "Loading Invoke AI", "loadingInvokeAI": "Loading Invoke AI",
"learnMore": "Learn More",
"modelManager": "Model Manager", "modelManager": "Model Manager",
"nodeEditor": "Node Editor", "nodeEditor": "Node Editor",
"nodes": "Workflow Editor", "nodes": "Workflow Editor",
@ -135,6 +136,8 @@
"bgth": "bg_th", "bgth": "bg_th",
"canny": "Canny", "canny": "Canny",
"cannyDescription": "Canny edge detection", "cannyDescription": "Canny edge detection",
"colorMap": "Color",
"colorMapDescription": "Generates a color map from the image",
"coarse": "Coarse", "coarse": "Coarse",
"contentShuffle": "Content Shuffle", "contentShuffle": "Content Shuffle",
"contentShuffleDescription": "Shuffles the content in an image", "contentShuffleDescription": "Shuffles the content in an image",
@ -158,6 +161,7 @@
"hideAdvanced": "Hide Advanced", "hideAdvanced": "Hide Advanced",
"highThreshold": "High Threshold", "highThreshold": "High Threshold",
"imageResolution": "Image Resolution", "imageResolution": "Image Resolution",
"colorMapTileSize": "Tile Size",
"importImageFromCanvas": "Import Image From Canvas", "importImageFromCanvas": "Import Image From Canvas",
"importMaskFromCanvas": "Import Mask From Canvas", "importMaskFromCanvas": "Import Mask From Canvas",
"incompatibleBaseModel": "Incompatible base model:", "incompatibleBaseModel": "Incompatible base model:",
@ -701,6 +705,8 @@
"addNodeToolTip": "Add Node (Shift+A, Space)", "addNodeToolTip": "Add Node (Shift+A, Space)",
"animatedEdges": "Animated Edges", "animatedEdges": "Animated Edges",
"animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes", "animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes",
"boardField": "Board",
"boardFieldDescription": "A gallery board",
"boolean": "Booleans", "boolean": "Booleans",
"booleanCollection": "Boolean Collection", "booleanCollection": "Boolean Collection",
"booleanCollectionDescription": "A collection of booleans.", "booleanCollectionDescription": "A collection of booleans.",
@ -888,7 +894,7 @@
"zoomOutNodes": "Zoom Out" "zoomOutNodes": "Zoom Out"
}, },
"parameters": { "parameters": {
"aspectRatio": "Ratio", "aspectRatio": "Aspect Ratio",
"boundingBoxHeader": "Bounding Box", "boundingBoxHeader": "Bounding Box",
"boundingBoxHeight": "Bounding Box Height", "boundingBoxHeight": "Bounding Box Height",
"boundingBoxWidth": "Bounding Box Width", "boundingBoxWidth": "Bounding Box Width",
@ -1020,8 +1026,8 @@
"label": "Seed Behaviour", "label": "Seed Behaviour",
"perIterationLabel": "Seed per Iteration", "perIterationLabel": "Seed per Iteration",
"perIterationDesc": "Use a different seed for each iteration", "perIterationDesc": "Use a different seed for each iteration",
"perPromptLabel": "Seed per Prompt", "perPromptLabel": "Seed per Image",
"perPromptDesc": "Use a different seed for each prompt" "perPromptDesc": "Use a different seed for each image"
} }
}, },
"sdxl": { "sdxl": {
@ -1173,131 +1179,205 @@
"popovers": { "popovers": {
"clipSkip": { "clipSkip": {
"heading": "CLIP Skip", "heading": "CLIP Skip",
"paragraph": "Choose how many layers of the CLIP model to skip. Certain models are better suited to be used with CLIP Skip." "paragraphs": [
}, "Choose how many layers of the CLIP model to skip.",
"compositingBlur": { "Some models work better with certain CLIP Skip settings.",
"heading": "Blur", "A higher value typically results in a less detailed image."
"paragraph": "The blur radius of the mask." ]
},
"compositingBlurMethod": {
"heading": "Blur Method",
"paragraph": "The method of blur applied to the masked area."
},
"compositingCoherencePass": {
"heading": "Coherence Pass",
"paragraph": "Composite the Inpainted/Outpainted images."
},
"compositingCoherenceMode": {
"heading": "Mode",
"paragraph": "The mode of the Coherence Pass."
},
"compositingCoherenceSteps": {
"heading": "Steps",
"paragraph": "Number of steps in the Coherence Pass. Similar to Denoising Steps."
},
"compositingStrength": {
"heading": "Strength",
"paragraph": "Amount of noise added for the Coherence Pass. Similar to Denoising Strength."
},
"compositingMaskAdjustments": {
"heading": "Mask Adjustments",
"paragraph": "Adjust the mask."
},
"controlNetBeginEnd": {
"heading": "Begin / End Step Percentage",
"paragraph": "Which parts of the denoising process will have the ControlNet applied. ControlNets applied at the start of the process guide composition, and ControlNets applied at the end guide details."
},
"controlNetControlMode": {
"heading": "Control Mode",
"paragraph": "Lends more weight to either the prompt or ControlNet."
},
"controlNetResizeMode": {
"heading": "Resize Mode",
"paragraph": "How the ControlNet image will be fit to the image generation Ratio"
},
"controlNetToggle": {
"heading": "Enable ControlNet",
"paragraph": "ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected."
},
"controlNetWeight": {
"heading": "Weight",
"paragraph": "How strongly the ControlNet will impact the generated image."
},
"dynamicPromptsToggle": {
"heading": "Enable Dynamic Prompts",
"paragraph": "Dynamic prompts allow multiple options within a prompt. Dynamic prompts can be used by: {option1|option2|option3}. Combinations of prompts will be randomly generated until the “Images” number has been reached."
},
"dynamicPromptsCombinatorial": {
"heading": "Combinatorial Generation",
"paragraph": "Generate an image for every possible combination of Dynamic Prompts until the Max Prompts is reached."
},
"infillMethod": {
"heading": "Infill Method",
"paragraph": "Method to infill the selected area."
},
"lora": {
"heading": "LoRA Weight",
"paragraph": "Weight of the LoRA. Higher weight will lead to larger impacts on the final image."
},
"noiseEnable": {
"heading": "Enable Noise Settings",
"paragraph": "Advanced control over noise generation."
},
"noiseUseCPU": {
"heading": "Use CPU Noise",
"paragraph": "Uses the CPU to generate random noise."
},
"paramCFGScale": {
"heading": "CFG Scale",
"paragraph": "Controls how much your prompt influences the generation process."
},
"paramDenoisingStrength": {
"heading": "Denoising Strength",
"paragraph": "How much noise is added to the input image. 0 will result in an identical image, while 1 will result in a completely new image."
},
"paramIterations": {
"heading": "Iterations",
"paragraph": "The number of images to generate. If Dynamic Prompts is enabled, each of the prompts will be generated this many times."
},
"paramModel": {
"heading": "Model",
"paragraph": "Model used for the denoising steps. Different models are trained to specialize in producing different aesthetic results and content."
}, },
"paramNegativeConditioning": { "paramNegativeConditioning": {
"heading": "Negative Prompt", "heading": "Negative Prompt",
"paragraph": "The generation process avoids the concepts in the negative prompt. Use this to exclude qualities or objects from the output. Supports Compel syntax and embeddings." "paragraphs": [
"The generation process avoids the concepts in the negative prompt. Use this to exclude qualities or objects from the output.",
"Supports Compel syntax and embeddings."
]
}, },
"paramPositiveConditioning": { "paramPositiveConditioning": {
"heading": "Positive Prompt", "heading": "Positive Prompt",
"paragraph": "Guides the generation process. You may use any words or phrases. Supports Compel and Dynamic Prompts syntaxes and embeddings." "paragraphs": [
}, "Guides the generation process. You may use any words or phrases.",
"paramRatio": { "Compel and Dynamic Prompts syntaxes and embeddings."
"heading": "Ratio", ]
"paragraph": "The ratio of the dimensions of the image generated. An image size (in number of pixels) equivalent to 512x512 is recommended for SD1.5 models and a size equivalent to 1024x1024 is recommended for SDXL models."
}, },
"paramScheduler": { "paramScheduler": {
"heading": "Scheduler", "heading": "Scheduler",
"paragraph": "Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." "paragraphs": [
"Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output."
]
},
"compositingBlur": {
"heading": "Blur",
"paragraphs": ["The blur radius of the mask."]
},
"compositingBlurMethod": {
"heading": "Blur Method",
"paragraphs": ["The method of blur applied to the masked area."]
},
"compositingCoherencePass": {
"heading": "Coherence Pass",
"paragraphs": [
"A second round of denoising helps to composite the Inpainted/Outpainted image."
]
},
"compositingCoherenceMode": {
"heading": "Mode",
"paragraphs": ["The mode of the Coherence Pass."]
},
"compositingCoherenceSteps": {
"heading": "Steps",
"paragraphs": [
"Number of denoising steps used in the Coherence Pass.",
"Same as the main Steps parameter."
]
},
"compositingStrength": {
"heading": "Strength",
"paragraphs": [
"Denoising strength for the Coherence Pass.",
"Same as the Image to Image Denoising Strength parameter."
]
},
"compositingMaskAdjustments": {
"heading": "Mask Adjustments",
"paragraphs": ["Adjust the mask."]
},
"controlNetBeginEnd": {
"heading": "Begin / End Step Percentage",
"paragraphs": [
"Which steps of the denoising process will have the ControlNet applied.",
"ControlNets applied at the beginning of the process guide composition, and ControlNets applied at the end guide details."
]
},
"controlNetControlMode": {
"heading": "Control Mode",
"paragraphs": [
"Lends more weight to either the prompt or ControlNet."
]
},
"controlNetResizeMode": {
"heading": "Resize Mode",
"paragraphs": [
"How the ControlNet image will be fit to the image output size."
]
},
"controlNet": {
"heading": "ControlNet",
"paragraphs": [
"ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected."
]
},
"controlNetWeight": {
"heading": "Weight",
"paragraphs": [
"How strongly the ControlNet will impact the generated image."
]
},
"dynamicPrompts": {
"heading": "Dynamic Prompts",
"paragraphs": [
"Dynamic Prompts parses a single prompt into many.",
"The basic syntax is \"a {red|green|blue} ball\". This will produce three prompts: \"a red ball\", \"a green ball\" and \"a blue ball\".",
"You can use the syntax as many times as you like in a single prompt, but be sure to keep the number of prompts generated in check with the Max Prompts setting."
]
},
"dynamicPromptsMaxPrompts": {
"heading": "Max Prompts",
"paragraphs": [
"Limits the number of prompts that can be generated by Dynamic Prompts."
]
},
"dynamicPromptsSeedBehaviour": {
"heading": "Seed Behaviour",
"paragraphs": [
"Controls how the seed is used when generating prompts.",
"Per Iteration will use a unique seed for each iteration. Use this to explore prompt variations on a single seed.",
"For example, if you have 5 prompts, each image will use the same seed.",
"Per Image will use a unique seed for each image. This provides more variation."
]
},
"infillMethod": {
"heading": "Infill Method",
"paragraphs": ["Method to infill the selected area."]
},
"lora": {
"heading": "LoRA Weight",
"paragraphs": [
"Higher LoRA weight will lead to larger impacts on the final image."
]
},
"noiseUseCPU": {
"heading": "Use CPU Noise",
"paragraphs": [
"Controls whether noise is generated on the CPU or GPU.",
"With CPU Noise enabled, a particular seed will produce the same image on any machine.",
"There is no performance impact to enabling CPU Noise."
]
},
"paramCFGScale": {
"heading": "CFG Scale",
"paragraphs": [
"Controls how much your prompt influences the generation process."
]
},
"paramDenoisingStrength": {
"heading": "Denoising Strength",
"paragraphs": [
"How much noise is added to the input image.",
"0 will result in an identical image, while 1 will result in a completely new image."
]
},
"paramIterations": {
"heading": "Iterations",
"paragraphs": [
"The number of images to generate.",
"If Dynamic Prompts is enabled, each of the prompts will be generated this many times."
]
},
"paramModel": {
"heading": "Model",
"paragraphs": [
"Model used for the denoising steps.",
"Different models are typically trained to specialize in producing particular aesthetic results and content."
]
},
"paramRatio": {
"heading": "Aspect Ratio",
"paragraphs": [
"The aspect ratio of the dimensions of the image generated.",
"An image size (in number of pixels) equivalent to 512x512 is recommended for SD1.5 models and a size equivalent to 1024x1024 is recommended for SDXL models."
]
}, },
"paramSeed": { "paramSeed": {
"heading": "Seed", "heading": "Seed",
"paragraph": "Controls the starting noise used for generation. Disable “Random Seed” to produce identical results with the same generation settings." "paragraphs": [
"Controls the starting noise used for generation.",
"Disable “Random Seed” to produce identical results with the same generation settings."
]
}, },
"paramSteps": { "paramSteps": {
"heading": "Steps", "heading": "Steps",
"paragraph": "Number of steps that will be performed in each generation. Higher step counts will typically create better images but will require more generation time." "paragraphs": [
"Number of steps that will be performed in each generation.",
"Higher step counts will typically create better images but will require more generation time."
]
}, },
"paramVAE": { "paramVAE": {
"heading": "VAE", "heading": "VAE",
"paragraph": "Model used for translating AI output into the final image." "paragraphs": [
"Model used for translating AI output into the final image."
]
}, },
"paramVAEPrecision": { "paramVAEPrecision": {
"heading": "VAE Precision", "heading": "VAE Precision",
"paragraph": "The precision used during VAE encoding and decoding. Fp16/Half precision is more efficient, at the expense of minor image variations." "paragraphs": [
"The precision used during VAE encoding and decoding. FP16/half precision is more efficient, at the expense of minor image variations."
]
}, },
"scaleBeforeProcessing": { "scaleBeforeProcessing": {
"heading": "Scale Before Processing", "heading": "Scale Before Processing",
"paragraph": "Scales the selected area to the size best suited for the model before the image generation process." "paragraphs": [
"Scales the selected area to the size best suited for the model before the image generation process."
]
} }
}, },
"ui": { "ui": {

View File

@ -17,7 +17,8 @@ import {
} from 'services/events/actions'; } from 'services/events/actions';
import { startAppListening } from '../..'; import { startAppListening } from '../..';
const nodeDenylist = ['load_image']; // These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
const nodeDenylist = ['load_image', 'image'];
export const addInvocationCompleteEventListener = () => { export const addInvocationCompleteEventListener = () => {
startAppListening({ startAppListening({
@ -37,6 +38,7 @@ export const addInvocationCompleteEventListener = () => {
const { image_name } = result.image; const { image_name } = result.image;
const { canvas, gallery } = getState(); const { canvas, gallery } = getState();
// This populates the `getImageDTO` cache
const imageDTO = await dispatch( const imageDTO = await dispatch(
imagesApi.endpoints.getImageDTO.initiate(image_name) imagesApi.endpoints.getImageDTO.initiate(image_name)
).unwrap(); ).unwrap();
@ -52,54 +54,36 @@ export const addInvocationCompleteEventListener = () => {
if (!imageDTO.is_intermediate) { if (!imageDTO.is_intermediate) {
/** /**
* Cache updates for when an image result is received * Cache updates for when an image result is received
* - *add* to getImageDTO * - add it to the no_board/images
* - IF `autoAddBoardId` is set:
* - THEN add it to the board_id/images
* - ELSE (`autoAddBoardId` is not set):
* - THEN add it to the no_board/images
*/ */
const { autoAddBoardId } = gallery; dispatch(
if (autoAddBoardId && autoAddBoardId !== 'none') { imagesApi.util.updateQueryData(
dispatch( 'listImages',
imagesApi.endpoints.addImageToBoard.initiate({ {
board_id: autoAddBoardId, board_id: imageDTO.board_id ?? 'none',
imageDTO, categories: IMAGE_CATEGORIES,
}) },
); (draft) => {
} else { imagesAdapter.addOne(draft, imageDTO);
dispatch( }
imagesApi.util.updateQueryData( )
'listImages', );
{
board_id: 'none',
categories: IMAGE_CATEGORIES,
},
(draft) => {
imagesAdapter.addOne(draft, imageDTO);
}
)
);
}
dispatch( dispatch(
imagesApi.util.invalidateTags([ imagesApi.util.invalidateTags([
{ type: 'BoardImagesTotal', id: autoAddBoardId }, { type: 'BoardImagesTotal', id: imageDTO.board_id },
{ type: 'BoardAssetsTotal', id: autoAddBoardId }, { type: 'BoardAssetsTotal', id: imageDTO.board_id },
]) ])
); );
const { selectedBoardId, shouldAutoSwitch } = gallery; const { shouldAutoSwitch } = gallery;
// If auto-switch is enabled, select the new image // If auto-switch is enabled, select the new image
if (shouldAutoSwitch) { if (shouldAutoSwitch) {
// if auto-add is enabled, switch the board as the image comes in // if auto-add is enabled, switch the board as the image comes in
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) { dispatch(galleryViewChanged('images'));
dispatch(boardIdSelected(autoAddBoardId)); dispatch(boardIdSelected(imageDTO.board_id ?? 'none'));
dispatch(galleryViewChanged('images'));
} else if (!autoAddBoardId) {
dispatch(galleryViewChanged('images'));
}
dispatch(imageSelected(imageDTO)); dispatch(imageSelected(imageDTO));
} }
} }

View File

@ -18,11 +18,14 @@ export const addUpscaleRequestedListener = () => {
const log = logger('session'); const log = logger('session');
const { image_name } = action.payload; const { image_name } = action.payload;
const { esrganModelName } = getState().postprocessing; const state = getState();
const { esrganModelName } = state.postprocessing;
const { autoAddBoardId } = state.gallery;
const graph = buildAdHocUpscaleGraph({ const graph = buildAdHocUpscaleGraph({
image_name, image_name,
esrganModelName, esrganModelName,
autoAddBoardId,
}); });
try { try {

View File

@ -1,124 +0,0 @@
import {
Box,
Button,
Divider,
Flex,
Heading,
Image,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverProps,
PopoverTrigger,
Portal,
Text,
} from '@chakra-ui/react';
import { ReactNode, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../app/store/storeHooks';
const OPEN_DELAY = 1500;
type Props = Omit<PopoverProps, 'children'> & {
details: string;
children: ReactNode;
image?: string;
buttonLabel?: string;
buttonHref?: string;
placement?: PopoverProps['placement'];
};
const IAIInformationalPopover = ({
details,
image,
buttonLabel,
buttonHref,
children,
placement,
}: Props) => {
const shouldEnableInformationalPopovers = useAppSelector(
(state) => state.system.shouldEnableInformationalPopovers
);
const { t } = useTranslation();
const heading = t(`popovers.${details}.heading`);
const paragraph = t(`popovers.${details}.paragraph`);
if (!shouldEnableInformationalPopovers) {
return <>{children}</>;
}
return (
<Popover
placement={placement || 'top'}
closeOnBlur={false}
trigger="hover"
variant="informational"
openDelay={OPEN_DELAY}
>
<PopoverTrigger>
<Box w="full">{children}</Box>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
<Flex
sx={{
gap: 3,
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
{image && (
<Image
sx={{
objectFit: 'contain',
maxW: '60%',
maxH: '60%',
backgroundColor: 'white',
}}
src={image}
alt="Optional Image"
/>
)}
<Flex
sx={{
gap: 3,
flexDirection: 'column',
width: '100%',
}}
>
{heading && (
<>
<Heading size="sm">{heading}</Heading>
<Divider />
</>
)}
<Text>{paragraph}</Text>
{buttonLabel && (
<Flex justifyContent="flex-end">
<Button
onClick={() => window.open(buttonHref)}
size="sm"
variant="invokeAIOutline"
>
{buttonLabel}
</Button>
</Flex>
)}
</Flex>
</Flex>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
export default memo(IAIInformationalPopover);

View File

@ -0,0 +1,155 @@
import {
Box,
BoxProps,
Button,
Divider,
Flex,
Heading,
Image,
Popover,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverProps,
PopoverTrigger,
Portal,
Text,
forwardRef,
} from '@chakra-ui/react';
import { merge, omit } from 'lodash-es';
import { PropsWithChildren, memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExternalLinkAlt } from 'react-icons/fa';
import { useAppSelector } from '../../../app/store/storeHooks';
import {
Feature,
OPEN_DELAY,
POPOVER_DATA,
POPPER_MODIFIERS,
} from './constants';
type Props = PropsWithChildren & {
feature: Feature;
wrapperProps?: BoxProps;
popoverProps?: PopoverProps;
};
const IAIInformationalPopover = forwardRef(
({ feature, children, wrapperProps, ...rest }: Props, ref) => {
const { t } = useTranslation();
const shouldEnableInformationalPopovers = useAppSelector(
(state) => state.system.shouldEnableInformationalPopovers
);
const data = useMemo(() => POPOVER_DATA[feature], [feature]);
const popoverProps = useMemo(
() => merge(omit(data, ['image', 'href', 'buttonLabel']), rest),
[data, rest]
);
const heading = useMemo<string | undefined>(
() => t(`popovers.${feature}.heading`),
[feature, t]
);
const paragraphs = useMemo<string[]>(
() =>
t(`popovers.${feature}.paragraphs`, {
returnObjects: true,
}) ?? [],
[feature, t]
);
const handleClick = useCallback(() => {
if (!data?.href) {
return;
}
window.open(data.href);
}, [data?.href]);
if (!shouldEnableInformationalPopovers) {
return (
<Box ref={ref} w="full" {...wrapperProps}>
{children}
</Box>
);
}
return (
<Popover
isLazy
closeOnBlur={false}
trigger="hover"
variant="informational"
openDelay={OPEN_DELAY}
modifiers={POPPER_MODIFIERS}
placement="top"
{...popoverProps}
>
<PopoverTrigger>
<Box ref={ref} w="full" {...wrapperProps}>
{children}
</Box>
</PopoverTrigger>
<Portal>
<PopoverContent w={96}>
<PopoverCloseButton />
<PopoverBody>
<Flex
sx={{
gap: 2,
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
{heading && (
<>
<Heading size="sm">{heading}</Heading>
<Divider />
</>
)}
{data?.image && (
<>
<Image
sx={{
objectFit: 'contain',
maxW: '60%',
maxH: '60%',
backgroundColor: 'white',
}}
src={data.image}
alt="Optional Image"
/>
<Divider />
</>
)}
{paragraphs.map((p) => (
<Text key={p}>{p}</Text>
))}
{data?.href && (
<>
<Divider />
<Button
pt={1}
onClick={handleClick}
leftIcon={<FaExternalLinkAlt />}
alignSelf="flex-end"
variant="link"
>
{t('common.learnMore') ?? heading}
</Button>
</>
)}
</Flex>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
}
);
IAIInformationalPopover.displayName = 'IAIInformationalPopover';
export default memo(IAIInformationalPopover);

View File

@ -0,0 +1,98 @@
import { PopoverProps } from '@chakra-ui/react';
export type Feature =
| 'clipSkip'
| 'paramNegativeConditioning'
| 'paramPositiveConditioning'
| 'paramScheduler'
| 'compositingBlur'
| 'compositingBlurMethod'
| 'compositingCoherencePass'
| 'compositingCoherenceMode'
| 'compositingCoherenceSteps'
| 'compositingStrength'
| 'compositingMaskAdjustments'
| 'controlNetBeginEnd'
| 'controlNetControlMode'
| 'controlNetResizeMode'
| 'controlNet'
| 'controlNetWeight'
| 'dynamicPrompts'
| 'dynamicPromptsMaxPrompts'
| 'dynamicPromptsSeedBehaviour'
| 'infillMethod'
| 'lora'
| 'noiseUseCPU'
| 'paramCFGScale'
| 'paramDenoisingStrength'
| 'paramIterations'
| 'paramModel'
| 'paramRatio'
| 'paramSeed'
| 'paramSteps'
| 'paramVAE'
| 'paramVAEPrecision'
| 'scaleBeforeProcessing';
export type PopoverData = PopoverProps & {
image?: string;
href?: string;
buttonLabel?: string;
};
export const POPOVER_DATA: { [key in Feature]?: PopoverData } = {
paramNegativeConditioning: {
placement: 'right',
},
controlNet: {
href: 'https://support.invoke.ai/support/solutions/articles/151000105880',
},
lora: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159072',
},
compositingCoherenceMode: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838',
},
infillMethod: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158841',
},
scaleBeforeProcessing: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158841',
},
paramIterations: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159073',
},
paramPositiveConditioning: {
href: 'https://support.invoke.ai/support/solutions/articles/151000096606-tips-on-crafting-prompts',
placement: 'right',
},
paramScheduler: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000159073',
},
paramModel: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000096601-what-is-a-model-which-should-i-use-',
},
paramRatio: {
gutter: 16,
},
controlNetControlMode: {
placement: 'right',
},
controlNetResizeMode: {
placement: 'right',
},
paramVAE: {
placement: 'right',
},
paramVAEPrecision: {
placement: 'right',
},
} as const;
export const OPEN_DELAY = 1000; // in milliseconds
export const POPPER_MODIFIERS: PopoverProps['modifiers'] = [
{ name: 'preventOverflow', options: { padding: 10 } },
];

View File

@ -44,23 +44,19 @@ const IAIMantineMultiSelect = forwardRef((props: IAIMultiSelectProps, ref) => {
return ( return (
<Tooltip label={tooltip} placement="top" hasArrow isOpen={true}> <Tooltip label={tooltip} placement="top" hasArrow isOpen={true}>
<MultiSelect <FormControl ref={ref} isDisabled={disabled}>
label={ {label && <FormLabel>{label}</FormLabel>}
label ? ( <MultiSelect
<FormControl ref={ref} isDisabled={disabled}> ref={inputRef}
<FormLabel>{label}</FormLabel> disabled={disabled}
</FormControl> onKeyDown={handleKeyDown}
) : undefined onKeyUp={handleKeyUp}
} searchable={searchable}
ref={inputRef} maxDropdownHeight={300}
disabled={disabled} styles={styles}
onKeyDown={handleKeyDown} {...rest}
onKeyUp={handleKeyUp} />
searchable={searchable} </FormControl>
maxDropdownHeight={300}
styles={styles}
{...rest}
/>
</Tooltip> </Tooltip>
); );
}); });

View File

@ -70,26 +70,23 @@ const IAIMantineSearchableSelect = forwardRef((props: IAISelectProps, ref) => {
return ( return (
<Tooltip label={tooltip} placement="top" hasArrow> <Tooltip label={tooltip} placement="top" hasArrow>
<Select <FormControl ref={ref} isDisabled={disabled}>
ref={inputRef} {label && <FormLabel>{label}</FormLabel>}
label={ <Select
label ? ( ref={inputRef}
<FormControl ref={ref} isDisabled={disabled}> withinPortal
<FormLabel>{label}</FormLabel> disabled={disabled}
</FormControl> searchValue={searchValue}
) : undefined onSearchChange={setSearchValue}
} onChange={handleChange}
disabled={disabled} onKeyDown={handleKeyDown}
searchValue={searchValue} onKeyUp={handleKeyUp}
onSearchChange={setSearchValue} searchable={searchable}
onChange={handleChange} maxDropdownHeight={300}
onKeyDown={handleKeyDown} styles={styles}
onKeyUp={handleKeyUp} {...rest}
searchable={searchable} />
maxDropdownHeight={300} </FormControl>
styles={styles}
{...rest}
/>
</Tooltip> </Tooltip>
); );
}); });

View File

@ -22,19 +22,10 @@ const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => {
return ( return (
<Tooltip label={tooltip} placement="top" hasArrow> <Tooltip label={tooltip} placement="top" hasArrow>
<Select <FormControl ref={ref} isRequired={required} isDisabled={disabled}>
label={ <FormLabel>{label}</FormLabel>
label ? ( <Select disabled={disabled} ref={inputRef} styles={styles} {...rest} />
<FormControl ref={ref} isRequired={required} isDisabled={disabled}> </FormControl>
<FormLabel>{label}</FormLabel>
</FormControl>
) : undefined
}
disabled={disabled}
ref={inputRef}
styles={styles}
{...rest}
/>
</Tooltip> </Tooltip>
); );
}); });

View File

@ -1,6 +1,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { ControlNetConfig } from '../store/controlNetSlice'; import { ControlNetConfig } from '../store/controlNetSlice';
import CannyProcessor from './processors/CannyProcessor'; import CannyProcessor from './processors/CannyProcessor';
import ColorMapProcessor from './processors/ColorMapProcessor';
import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; import ContentShuffleProcessor from './processors/ContentShuffleProcessor';
import HedProcessor from './processors/HedProcessor'; import HedProcessor from './processors/HedProcessor';
import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; import LineartAnimeProcessor from './processors/LineartAnimeProcessor';
@ -30,6 +31,16 @@ const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => {
); );
} }
if (processorNode.type === 'color_map_image_processor') {
return (
<ColorMapProcessor
controlNetId={controlNetId}
processorNode={processorNode}
isEnabled={isEnabled}
/>
);
}
if (processorNode.type === 'hed_image_processor') { if (processorNode.type === 'hed_image_processor') {
return ( return (
<HedProcessor <HedProcessor

View File

@ -10,7 +10,7 @@ import {
Tooltip, Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { import {
ControlNetConfig, ControlNetConfig,
controlNetBeginStepPctChanged, controlNetBeginStepPctChanged,
@ -50,7 +50,7 @@ const ParamControlNetBeginEnd = (props: Props) => {
); );
return ( return (
<IAIInformationalPopover details="controlNetBeginEnd"> <IAIInformationalPopover feature="controlNetBeginEnd">
<FormControl isDisabled={!isEnabled}> <FormControl isDisabled={!isEnabled}>
<FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel> <FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel>
<HStack w="100%" gap={2} alignItems="center"> <HStack w="100%" gap={2} alignItems="center">

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { import {
ControlModes, ControlModes,
@ -35,7 +35,7 @@ export default function ParamControlNetControlMode(
); );
return ( return (
<IAIInformationalPopover details="controlNetControlMode"> <IAIInformationalPopover feature="controlNetControlMode">
<IAIMantineSelect <IAIMantineSelect
disabled={!isEnabled} disabled={!isEnabled}
label={t('controlnet.controlMode')} label={t('controlnet.controlMode')}

View File

@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { isControlNetEnabledToggled } from 'features/controlNet/store/controlNetSlice'; import { isControlNetEnabledToggled } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
@ -28,7 +28,7 @@ const ParamControlNetFeatureToggle = () => {
return ( return (
<Box width="100%"> <Box width="100%">
<IAIInformationalPopover details="controlNetToggle"> <IAIInformationalPopover feature="controlNet">
<IAISwitch <IAISwitch
label="Enable ControlNet" label="Enable ControlNet"
isChecked={isEnabled} isChecked={isEnabled}

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { import {
ControlNetConfig, ControlNetConfig,
@ -34,7 +34,7 @@ export default function ParamControlNetResizeMode(
); );
return ( return (
<IAIInformationalPopover details="controlNetResizeMode"> <IAIInformationalPopover feature="controlNetResizeMode">
<IAIMantineSelect <IAIMantineSelect
disabled={!isEnabled} disabled={!isEnabled}
label={t('controlnet.resizeMode')} label={t('controlnet.resizeMode')}

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { import {
ControlNetConfig, ControlNetConfig,
@ -24,7 +24,7 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
); );
return ( return (
<IAIInformationalPopover details="controlNetWeight"> <IAIInformationalPopover feature="controlNetWeight">
<IAISlider <IAISlider
isDisabled={!isEnabled} isDisabled={!isEnabled}
label={t('controlnet.weight')} label={t('controlnet.weight')}

View File

@ -0,0 +1,59 @@
import IAISlider from 'common/components/IAISlider';
import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants';
import { RequiredColorMapImageProcessorInvocation } from 'features/controlNet/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged';
import ProcessorWrapper from './common/ProcessorWrapper';
const DEFAULTS = CONTROLNET_PROCESSORS.color_map_image_processor
.default as RequiredColorMapImageProcessorInvocation;
type ColorMapProcessorProps = {
controlNetId: string;
processorNode: RequiredColorMapImageProcessorInvocation;
isEnabled: boolean;
};
const ColorMapProcessor = (props: ColorMapProcessorProps) => {
const { controlNetId, processorNode, isEnabled } = props;
const { color_map_tile_size } = processorNode;
const processorChanged = useProcessorNodeChanged();
const { t } = useTranslation();
const handleColorMapTileSizeChanged = useCallback(
(v: number) => {
processorChanged(controlNetId, { color_map_tile_size: v });
},
[controlNetId, processorChanged]
);
const handleColorMapTileSizeReset = useCallback(() => {
processorChanged(controlNetId, {
color_map_tile_size: DEFAULTS.color_map_tile_size,
});
}, [controlNetId, processorChanged]);
return (
<ProcessorWrapper>
<IAISlider
isDisabled={!isEnabled}
label={t('controlnet.colorMapTileSize')}
value={color_map_tile_size}
onChange={handleColorMapTileSizeChanged}
handleReset={handleColorMapTileSizeReset}
withReset
min={1}
max={256}
step={1}
withInput
withSliderMarks
sliderNumberInputProps={{
max: 4096,
}}
/>
</ProcessorWrapper>
);
};
export default memo(ColorMapProcessor);

View File

@ -4,5 +4,9 @@ import { PropsWithChildren } from 'react';
type Props = PropsWithChildren; type Props = PropsWithChildren;
export default function ProcessorWrapper(props: Props) { export default function ProcessorWrapper(props: Props) {
return <Flex sx={{ flexDirection: 'column', gap: 2 }}>{props.children}</Flex>; return (
<Flex sx={{ flexDirection: 'column', gap: 2, pb: 2 }}>
{props.children}
</Flex>
);
} }

View File

@ -1,8 +1,8 @@
import i18n from 'i18next';
import { import {
ControlNetProcessorType, ControlNetProcessorType,
RequiredControlNetProcessorNode, RequiredControlNetProcessorNode,
} from './types'; } from './types';
import i18n from 'i18next';
type ControlNetProcessorsDict = Record< type ControlNetProcessorsDict = Record<
ControlNetProcessorType, ControlNetProcessorType,
@ -50,6 +50,20 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
high_threshold: 200, high_threshold: 200,
}, },
}, },
color_map_image_processor: {
type: 'color_map_image_processor',
get label() {
return i18n.t('controlnet.colorMap');
},
get description() {
return i18n.t('controlnet.colorMapDescription');
},
default: {
id: 'color_map_image_processor',
type: 'color_map_image_processor',
color_map_tile_size: 64,
},
},
content_shuffle_image_processor: { content_shuffle_image_processor: {
type: 'content_shuffle_image_processor', type: 'content_shuffle_image_processor',
get label() { get label() {

View File

@ -1,6 +1,7 @@
import { isObject } from 'lodash-es'; import { isObject } from 'lodash-es';
import { import {
CannyImageProcessorInvocation, CannyImageProcessorInvocation,
ColorMapImageProcessorInvocation,
ContentShuffleImageProcessorInvocation, ContentShuffleImageProcessorInvocation,
HedImageProcessorInvocation, HedImageProcessorInvocation,
LineartAnimeImageProcessorInvocation, LineartAnimeImageProcessorInvocation,
@ -20,6 +21,7 @@ import { O } from 'ts-toolbelt';
*/ */
export type ControlNetProcessorNode = export type ControlNetProcessorNode =
| CannyImageProcessorInvocation | CannyImageProcessorInvocation
| ColorMapImageProcessorInvocation
| ContentShuffleImageProcessorInvocation | ContentShuffleImageProcessorInvocation
| HedImageProcessorInvocation | HedImageProcessorInvocation
| LineartAnimeImageProcessorInvocation | LineartAnimeImageProcessorInvocation
@ -47,6 +49,14 @@ export type RequiredCannyImageProcessorInvocation = O.Required<
'type' | 'low_threshold' | 'high_threshold' 'type' | 'low_threshold' | 'high_threshold'
>; >;
/**
* The Color Map processor node, with parameters flagged as required
*/
export type RequiredColorMapImageProcessorInvocation = O.Required<
ColorMapImageProcessorInvocation,
'type' | 'color_map_tile_size'
>;
/** /**
* The ContentShuffle processor node, with parameters flagged as required * The ContentShuffle processor node, with parameters flagged as required
*/ */
@ -140,6 +150,7 @@ export type RequiredZoeDepthImageProcessorInvocation = O.Required<
*/ */
export type RequiredControlNetProcessorNode = O.Required< export type RequiredControlNetProcessorNode = O.Required<
| RequiredCannyImageProcessorInvocation | RequiredCannyImageProcessorInvocation
| RequiredColorMapImageProcessorInvocation
| RequiredContentShuffleImageProcessorInvocation | RequiredContentShuffleImageProcessorInvocation
| RequiredHedImageProcessorInvocation | RequiredHedImageProcessorInvocation
| RequiredLineartAnimeImageProcessorInvocation | RequiredLineartAnimeImageProcessorInvocation
@ -166,6 +177,22 @@ export const isCannyImageProcessorInvocation = (
return false; return false;
}; };
/**
* Type guard for ColorMapImageProcessorInvocation
*/
export const isColorMapImageProcessorInvocation = (
obj: unknown
): obj is ColorMapImageProcessorInvocation => {
if (
isObject(obj) &&
'type' in obj &&
obj.type === 'color_map_image_processor'
) {
return true;
}
return false;
};
/** /**
* Type guard for ContentShuffleImageProcessorInvocation * Type guard for ContentShuffleImageProcessorInvocation
*/ */

View File

@ -43,8 +43,8 @@ const ParamDynamicPromptsCollapse = () => {
activeLabel={activeLabel} activeLabel={activeLabel}
> >
<Flex sx={{ gap: 2, flexDir: 'column' }}> <Flex sx={{ gap: 2, flexDir: 'column' }}>
<ParamDynamicPromptsSeedBehaviour />
<ParamDynamicPromptsPreview /> <ParamDynamicPromptsPreview />
<ParamDynamicPromptsSeedBehaviour />
<ParamDynamicPromptsMaxPrompts /> <ParamDynamicPromptsMaxPrompts />
</Flex> </Flex>
</IAICollapse> </IAICollapse>

View File

@ -4,9 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { combinatorialToggled } from '../store/dynamicPromptsSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import { combinatorialToggled } from '../store/dynamicPromptsSlice';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -28,13 +27,11 @@ const ParamDynamicPromptsCombinatorial = () => {
}, [dispatch]); }, [dispatch]);
return ( return (
<IAIInformationalPopover details="dynamicPromptsCombinatorial"> <IAISwitch
<IAISwitch label={t('dynamicPrompts.combinatorial')}
label={t('dynamicPrompts.combinatorial')} isChecked={combinatorial}
isChecked={combinatorial} onChange={handleChange}
onChange={handleChange} />
/>
</IAIInformationalPopover>
); );
}; };

View File

@ -9,6 +9,7 @@ import {
maxPromptsReset, maxPromptsReset,
} from '../store/dynamicPromptsSlice'; } from '../store/dynamicPromptsSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -46,19 +47,21 @@ const ParamDynamicPromptsMaxPrompts = () => {
}, [dispatch]); }, [dispatch]);
return ( return (
<IAISlider <IAIInformationalPopover feature="dynamicPromptsMaxPrompts">
label={t('dynamicPrompts.maxPrompts')} <IAISlider
isDisabled={isDisabled} label={t('dynamicPrompts.maxPrompts')}
min={min} isDisabled={isDisabled}
max={sliderMax} min={min}
value={maxPrompts} max={sliderMax}
onChange={handleChange} value={maxPrompts}
sliderNumberInputProps={{ max: inputMax }} onChange={handleChange}
withSliderMarks sliderNumberInputProps={{ max: inputMax }}
withInput withSliderMarks
withReset withInput
handleReset={handleReset} withReset
/> handleReset={handleReset}
/>
</IAIInformationalPopover>
); );
}; };

View File

@ -13,6 +13,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { memo } from 'react'; import { memo } from 'react';
import { FaCircleExclamation } from 'react-icons/fa6'; import { FaCircleExclamation } from 'react-icons/fa6';
@ -42,58 +43,73 @@ const ParamDynamicPromptsPreview = () => {
if (isError) { if (isError) {
return ( return (
<Flex <IAIInformationalPopover feature="dynamicPrompts">
w="full" <Flex
h="full" w="full"
layerStyle="second" h="full"
alignItems="center" layerStyle="second"
justifyContent="center" alignItems="center"
p={8} justifyContent="center"
> p={8}
<IAINoContentFallback >
icon={FaCircleExclamation} <IAINoContentFallback
label="Problem generating prompts" icon={FaCircleExclamation}
/> label="Problem generating prompts"
</Flex> />
</Flex>
</IAIInformationalPopover>
); );
} }
return ( return (
<FormControl isInvalid={Boolean(parsingError)}> <IAIInformationalPopover feature="dynamicPrompts">
<FormLabel whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis"> <FormControl isInvalid={Boolean(parsingError)}>
Prompts Preview ({prompts.length}){parsingError && ` - ${parsingError}`} <FormLabel
</FormLabel> whiteSpace="nowrap"
<Flex h={64} pos="relative" layerStyle="third" borderRadius="base" p={2}> overflow="hidden"
<ScrollableContent> textOverflow="ellipsis"
<OrderedList stylePosition="inside" ms={0}> >
{prompts.map((prompt, i) => ( Prompts Preview ({prompts.length})
<ListItem {parsingError && ` - ${parsingError}`}
fontSize="sm" </FormLabel>
key={`${prompt}.${i}`} <Flex
sx={listItemStyles} h={64}
> pos="relative"
<Text as="span">{prompt}</Text> layerStyle="third"
</ListItem> borderRadius="base"
))} p={2}
</OrderedList> >
</ScrollableContent> <ScrollableContent>
{isLoading && ( <OrderedList stylePosition="inside" ms={0}>
<Flex {prompts.map((prompt, i) => (
pos="absolute" <ListItem
w="full" fontSize="sm"
h="full" key={`${prompt}.${i}`}
top={0} sx={listItemStyles}
insetInlineStart={0} >
layerStyle="second" <Text as="span">{prompt}</Text>
opacity={0.7} </ListItem>
alignItems="center" ))}
justifyContent="center" </OrderedList>
> </ScrollableContent>
<Spinner /> {isLoading && (
</Flex> <Flex
)} pos="absolute"
</Flex> w="full"
</FormControl> h="full"
top={0}
insetInlineStart={0}
layerStyle="second"
opacity={0.7}
alignItems="center"
justifyContent="center"
>
<Spinner />
</Flex>
)}
</Flex>
</FormControl>
</IAIInformationalPopover>
); );
}; };

View File

@ -7,6 +7,7 @@ import {
seedBehaviourChanged, seedBehaviourChanged,
} from '../store/dynamicPromptsSlice'; } from '../store/dynamicPromptsSlice';
import IAIMantineSelectItemWithDescription from 'common/components/IAIMantineSelectItemWithDescription'; import IAIMantineSelectItemWithDescription from 'common/components/IAIMantineSelectItemWithDescription';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
type Item = { type Item = {
label: string; label: string;
@ -47,13 +48,15 @@ const ParamDynamicPromptsSeedBehaviour = () => {
); );
return ( return (
<IAIMantineSelect <IAIInformationalPopover feature="dynamicPromptsSeedBehaviour">
label={t('dynamicPrompts.seedBehaviour.label')} <IAIMantineSelect
value={seedBehaviour} label={t('dynamicPrompts.seedBehaviour.label')}
data={data} value={seedBehaviour}
itemComponent={IAIMantineSelectItemWithDescription} data={data}
onChange={handleChange} itemComponent={IAIMantineSelectItemWithDescription}
/> onChange={handleChange}
/>
</IAIInformationalPopover>
); );
}; };

View File

@ -10,7 +10,7 @@ import {
loraWeightChanged, loraWeightChanged,
loraWeightReset, loraWeightReset,
} from '../store/loraSlice'; } from '../store/loraSlice';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
type Props = { type Props = {
lora: LoRA; lora: LoRA;
@ -36,7 +36,7 @@ const ParamLora = (props: Props) => {
}, [dispatch, lora.id]); }, [dispatch, lora.id]);
return ( return (
<IAIInformationalPopover details="lora"> <IAIInformationalPopover feature="lora">
<Flex sx={{ gap: 2.5, alignItems: 'flex-end' }}> <Flex sx={{ gap: 2.5, alignItems: 'flex-end' }}>
<IAISlider <IAISlider
label={lora.model_name} label={lora.model_name}

View File

@ -25,8 +25,8 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
> >
{hasImageOutput && <EmbedWorkflowCheckbox nodeId={nodeId} />}
<UseCacheCheckbox nodeId={nodeId} /> <UseCacheCheckbox nodeId={nodeId} />
{hasImageOutput && <EmbedWorkflowCheckbox nodeId={nodeId} />}
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />} {hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
</Flex> </Flex>
); );

View File

@ -16,6 +16,7 @@ import SchedulerInputField from './inputs/SchedulerInputField';
import StringInputField from './inputs/StringInputField'; import StringInputField from './inputs/StringInputField';
import VaeModelInputField from './inputs/VaeModelInputField'; import VaeModelInputField from './inputs/VaeModelInputField';
import IPAdapterModelInputField from './inputs/IPAdapterModelInputField'; import IPAdapterModelInputField from './inputs/IPAdapterModelInputField';
import BoardInputField from './inputs/BoardInputField';
type InputFieldProps = { type InputFieldProps = {
nodeId: string; nodeId: string;
@ -99,6 +100,16 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
); );
} }
if (field?.type === 'BoardField' && fieldTemplate?.type === 'BoardField') {
return (
<BoardInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if ( if (
field?.type === 'MainModelField' && field?.type === 'MainModelField' &&
fieldTemplate?.type === 'MainModelField' fieldTemplate?.type === 'MainModelField'

View File

@ -0,0 +1,64 @@
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { fieldBoardValueChanged } from 'features/nodes/store/nodesSlice';
import {
BoardInputFieldTemplate,
BoardInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo, useCallback } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
const BoardInputFieldComponent = (
props: FieldComponentProps<BoardInputFieldValue, BoardInputFieldTemplate>
) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { data, hasBoards } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
const boards: SelectItem[] = [
{
label: 'None',
value: 'none',
},
];
data?.forEach(({ board_id, board_name }) => {
boards.push({
label: board_name,
value: board_id,
});
});
return {
data: boards,
hasBoards: boards.length > 1,
};
},
});
const handleChange = useCallback(
(v: string | null) => {
dispatch(
fieldBoardValueChanged({
nodeId,
fieldName: field.name,
value: v && v !== 'none' ? { board_id: v } : undefined,
})
);
},
[dispatch, field.name, nodeId]
);
return (
<IAIMantineSearchableSelect
className="nowheel nodrag"
value={field.value?.board_id ?? 'none'}
data={data}
onChange={handleChange}
disabled={!hasBoards}
/>
);
};
export default memo(BoardInputFieldComponent);

View File

@ -65,11 +65,6 @@ const SchedulerInputField = (
return ( return (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
className="nowheel nodrag" className="nowheel nodrag"
sx={{
'.mantine-Select-dropdown': {
width: '14rem !important',
},
}}
value={field.value} value={field.value}
data={data} data={data}
onChange={handleChange} onChange={handleChange}

View File

@ -143,7 +143,7 @@ export const useBuildNodeData = () => {
notes: '', notes: '',
isOpen: true, isOpen: true,
embedWorkflow: false, embedWorkflow: false,
isIntermediate: true, isIntermediate: type === 'save_image' ? false : true,
inputs, inputs,
outputs, outputs,
useCache: template.useCache, useCache: template.useCache,

View File

@ -17,8 +17,12 @@ export const useHasImageOutput = (nodeId: string) => {
if (!isInvocationNode(node)) { if (!isInvocationNode(node)) {
return false; return false;
} }
return some(node.data.outputs, (output) => return some(
IMAGE_FIELDS.includes(output.type) node.data.outputs,
(output) =>
IMAGE_FIELDS.includes(output.type) &&
// the image primitive node does not actually save the image, do not show the image-saving checkboxes
node.data.type !== 'image'
); );
}, },
defaultSelectorOptions defaultSelectorOptions

View File

@ -30,6 +30,7 @@ import {
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { DRAG_HANDLE_CLASSNAME } from '../types/constants'; import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
import { import {
BoardInputFieldValue,
BooleanInputFieldValue, BooleanInputFieldValue,
ColorInputFieldValue, ColorInputFieldValue,
ControlNetModelInputFieldValue, ControlNetModelInputFieldValue,
@ -494,6 +495,12 @@ const nodesSlice = createSlice({
) => { ) => {
fieldValueReducer(state, action); fieldValueReducer(state, action);
}, },
fieldBoardValueChanged: (
state,
action: FieldValueAction<BoardInputFieldValue>
) => {
fieldValueReducer(state, action);
},
fieldImageValueChanged: ( fieldImageValueChanged: (
state, state,
action: FieldValueAction<ImageInputFieldValue> action: FieldValueAction<ImageInputFieldValue>
@ -871,7 +878,7 @@ const nodesSlice = createSlice({
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => { builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
if (['in_progress'].includes(action.payload.data.status)) { if (['in_progress'].includes(action.payload.data.status)) {
forEach(state.nodeExecutionStates, (nes) => { forEach(state.nodeExecutionStates, (nes) => {
nes.status = NodeStatus.IN_PROGRESS; nes.status = NodeStatus.PENDING;
nes.error = null; nes.error = null;
nes.progress = null; nes.progress = null;
nes.progressImage = null; nes.progressImage = null;
@ -897,6 +904,7 @@ export const {
imageCollectionFieldValueChanged, imageCollectionFieldValueChanged,
fieldStringValueChanged, fieldStringValueChanged,
fieldNumberValueChanged, fieldNumberValueChanged,
fieldBoardValueChanged,
fieldBooleanValueChanged, fieldBooleanValueChanged,
fieldImageValueChanged, fieldImageValueChanged,
fieldColorValueChanged, fieldColorValueChanged,

View File

@ -1,4 +1,9 @@
import { FieldType, FieldUIConfig } from './types'; import {
FieldType,
FieldTypeMap,
FieldTypeMapWithNumber,
FieldUIConfig,
} from './types';
import { t } from 'i18next'; import { t } from 'i18next';
export const HANDLE_TOOLTIP_OPEN_DELAY = 500; export const HANDLE_TOOLTIP_OPEN_DELAY = 500;
@ -28,7 +33,7 @@ export const COLLECTION_TYPES: FieldType[] = [
'ColorCollection', 'ColorCollection',
]; ];
export const POLYMORPHIC_TYPES = [ export const POLYMORPHIC_TYPES: FieldType[] = [
'IntegerPolymorphic', 'IntegerPolymorphic',
'BooleanPolymorphic', 'BooleanPolymorphic',
'FloatPolymorphic', 'FloatPolymorphic',
@ -40,7 +45,7 @@ export const POLYMORPHIC_TYPES = [
'ColorPolymorphic', 'ColorPolymorphic',
]; ];
export const MODEL_TYPES = [ export const MODEL_TYPES: FieldType[] = [
'IPAdapterModelField', 'IPAdapterModelField',
'ControlNetModelField', 'ControlNetModelField',
'LoRAModelField', 'LoRAModelField',
@ -54,7 +59,7 @@ export const MODEL_TYPES = [
'ClipField', 'ClipField',
]; ];
export const COLLECTION_MAP = { export const COLLECTION_MAP: FieldTypeMapWithNumber = {
integer: 'IntegerCollection', integer: 'IntegerCollection',
boolean: 'BooleanCollection', boolean: 'BooleanCollection',
number: 'FloatCollection', number: 'FloatCollection',
@ -71,7 +76,7 @@ export const isCollectionItemType = (
): itemType is keyof typeof COLLECTION_MAP => ): itemType is keyof typeof COLLECTION_MAP =>
Boolean(itemType && itemType in COLLECTION_MAP); Boolean(itemType && itemType in COLLECTION_MAP);
export const SINGLE_TO_POLYMORPHIC_MAP = { export const SINGLE_TO_POLYMORPHIC_MAP: FieldTypeMapWithNumber = {
integer: 'IntegerPolymorphic', integer: 'IntegerPolymorphic',
boolean: 'BooleanPolymorphic', boolean: 'BooleanPolymorphic',
number: 'FloatPolymorphic', number: 'FloatPolymorphic',
@ -84,7 +89,7 @@ export const SINGLE_TO_POLYMORPHIC_MAP = {
ColorField: 'ColorPolymorphic', ColorField: 'ColorPolymorphic',
}; };
export const POLYMORPHIC_TO_SINGLE_MAP = { export const POLYMORPHIC_TO_SINGLE_MAP: FieldTypeMap = {
IntegerPolymorphic: 'integer', IntegerPolymorphic: 'integer',
BooleanPolymorphic: 'boolean', BooleanPolymorphic: 'boolean',
FloatPolymorphic: 'float', FloatPolymorphic: 'float',
@ -96,7 +101,7 @@ export const POLYMORPHIC_TO_SINGLE_MAP = {
ColorPolymorphic: 'ColorField', ColorPolymorphic: 'ColorField',
}; };
export const TYPES_WITH_INPUT_COMPONENTS = [ export const TYPES_WITH_INPUT_COMPONENTS: FieldType[] = [
'string', 'string',
'StringPolymorphic', 'StringPolymorphic',
'boolean', 'boolean',
@ -117,6 +122,7 @@ export const TYPES_WITH_INPUT_COMPONENTS = [
'SDXLMainModelField', 'SDXLMainModelField',
'Scheduler', 'Scheduler',
'IPAdapterModelField', 'IPAdapterModelField',
'BoardField',
]; ];
export const isPolymorphicItemType = ( export const isPolymorphicItemType = (
@ -240,6 +246,11 @@ export const FIELDS: Record<FieldType, FieldUIConfig> = {
description: t('nodes.imageFieldDescription'), description: t('nodes.imageFieldDescription'),
title: t('nodes.imageField'), title: t('nodes.imageField'),
}, },
BoardField: {
color: 'purple.500',
description: t('nodes.imageFieldDescription'),
title: t('nodes.imageField'),
},
ImagePolymorphic: { ImagePolymorphic: {
color: 'purple.500', color: 'purple.500',
description: t('nodes.imagePolymorphicDescription'), description: t('nodes.imagePolymorphicDescription'),

View File

@ -72,6 +72,7 @@ export type FieldUIConfig = {
// TODO: Get this from the OpenAPI schema? may be tricky... // TODO: Get this from the OpenAPI schema? may be tricky...
export const zFieldType = z.enum([ export const zFieldType = z.enum([
'BoardField',
'boolean', 'boolean',
'BooleanCollection', 'BooleanCollection',
'BooleanPolymorphic', 'BooleanPolymorphic',
@ -119,6 +120,10 @@ export const zFieldType = z.enum([
]); ]);
export type FieldType = z.infer<typeof zFieldType>; export type FieldType = z.infer<typeof zFieldType>;
export type FieldTypeMap = { [key in FieldType]?: FieldType };
export type FieldTypeMapWithNumber = {
[key in FieldType | 'number']?: FieldType;
};
export const zReservedFieldType = z.enum([ export const zReservedFieldType = z.enum([
'WorkflowField', 'WorkflowField',
@ -187,6 +192,11 @@ export const zImageField = z.object({
}); });
export type ImageField = z.infer<typeof zImageField>; export type ImageField = z.infer<typeof zImageField>;
export const zBoardField = z.object({
board_id: z.string().trim().min(1),
});
export type BoardField = z.infer<typeof zBoardField>;
export const zLatentsField = z.object({ export const zLatentsField = z.object({
latents_name: z.string().trim().min(1), latents_name: z.string().trim().min(1),
seed: z.number().int().optional(), seed: z.number().int().optional(),
@ -494,6 +504,12 @@ export const zImageInputFieldValue = zInputFieldValueBase.extend({
}); });
export type ImageInputFieldValue = z.infer<typeof zImageInputFieldValue>; export type ImageInputFieldValue = z.infer<typeof zImageInputFieldValue>;
export const zBoardInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('BoardField'),
value: zBoardField.optional(),
});
export type BoardInputFieldValue = z.infer<typeof zBoardInputFieldValue>;
export const zImagePolymorphicInputFieldValue = zInputFieldValueBase.extend({ export const zImagePolymorphicInputFieldValue = zInputFieldValueBase.extend({
type: z.literal('ImagePolymorphic'), type: z.literal('ImagePolymorphic'),
value: zImageField.optional(), value: zImageField.optional(),
@ -630,6 +646,7 @@ export type SchedulerInputFieldValue = z.infer<
>; >;
export const zInputFieldValue = z.discriminatedUnion('type', [ export const zInputFieldValue = z.discriminatedUnion('type', [
zBoardInputFieldValue,
zBooleanCollectionInputFieldValue, zBooleanCollectionInputFieldValue,
zBooleanInputFieldValue, zBooleanInputFieldValue,
zBooleanPolymorphicInputFieldValue, zBooleanPolymorphicInputFieldValue,
@ -770,6 +787,11 @@ export type BooleanPolymorphicInputFieldTemplate = Omit<
type: 'BooleanPolymorphic'; type: 'BooleanPolymorphic';
}; };
export type BoardInputFieldTemplate = InputFieldTemplateBase & {
default: BoardField;
type: 'BoardField';
};
export type ImageInputFieldTemplate = InputFieldTemplateBase & { export type ImageInputFieldTemplate = InputFieldTemplateBase & {
default: ImageField; default: ImageField;
type: 'ImageField'; type: 'ImageField';
@ -952,6 +974,7 @@ export type WorkflowInputFieldTemplate = InputFieldTemplateBase & {
* maximum length, pattern to match, etc). * maximum length, pattern to match, etc).
*/ */
export type InputFieldTemplate = export type InputFieldTemplate =
| BoardInputFieldTemplate
| BooleanCollectionInputFieldTemplate | BooleanCollectionInputFieldTemplate
| BooleanPolymorphicInputFieldTemplate | BooleanPolymorphicInputFieldTemplate
| BooleanInputFieldTemplate | BooleanInputFieldTemplate

View File

@ -62,6 +62,8 @@ import {
ConditioningField, ConditioningField,
IPAdapterInputFieldTemplate, IPAdapterInputFieldTemplate,
IPAdapterModelInputFieldTemplate, IPAdapterModelInputFieldTemplate,
BoardInputFieldTemplate,
InputFieldTemplate,
} from '../types/types'; } from '../types/types';
import { ControlField } from 'services/api/types'; import { ControlField } from 'services/api/types';
@ -450,6 +452,19 @@ const buildIPAdapterModelInputFieldTemplate = ({
return template; return template;
}; };
const buildBoardInputFieldTemplate = ({
schemaObject,
baseField,
}: BuildInputFieldArg): BoardInputFieldTemplate => {
const template: BoardInputFieldTemplate = {
...baseField,
type: 'BoardField',
default: schemaObject.default ?? undefined,
};
return template;
};
const buildImageInputFieldTemplate = ({ const buildImageInputFieldTemplate = ({
schemaObject, schemaObject,
baseField, baseField,
@ -851,7 +866,10 @@ export const getFieldType = (
return; return;
}; };
const TEMPLATE_BUILDER_MAP = { const TEMPLATE_BUILDER_MAP: {
[key in FieldType]?: (arg: BuildInputFieldArg) => InputFieldTemplate;
} = {
BoardField: buildBoardInputFieldTemplate,
boolean: buildBooleanInputFieldTemplate, boolean: buildBooleanInputFieldTemplate,
BooleanCollection: buildBooleanCollectionInputFieldTemplate, BooleanCollection: buildBooleanCollectionInputFieldTemplate,
BooleanPolymorphic: buildBooleanPolymorphicInputFieldTemplate, BooleanPolymorphic: buildBooleanPolymorphicInputFieldTemplate,
@ -937,7 +955,13 @@ export const buildInputFieldTemplate = (
return; return;
} }
return TEMPLATE_BUILDER_MAP[fieldType]({ const builder = TEMPLATE_BUILDER_MAP[fieldType];
if (!builder) {
return;
}
return builder({
schemaObject: fieldSchema, schemaObject: fieldSchema,
baseField, baseField,
}); });

View File

@ -1,7 +1,10 @@
import { InputFieldTemplate, InputFieldValue } from '../types/types'; import { FieldType, InputFieldTemplate, InputFieldValue } from '../types/types';
const FIELD_VALUE_FALLBACK_MAP = { const FIELD_VALUE_FALLBACK_MAP: {
[key in FieldType]: InputFieldValue['value'];
} = {
enum: '', enum: '',
BoardField: undefined,
boolean: false, boolean: false,
BooleanCollection: [], BooleanCollection: [],
BooleanPolymorphic: false, BooleanPolymorphic: false,

View File

@ -24,12 +24,14 @@ export const addSaveImageNode = (
const activeTabName = activeTabNameSelector(state); const activeTabName = activeTabNameSelector(state);
const is_intermediate = const is_intermediate =
activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false; activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false;
const { autoAddBoardId } = state.gallery;
const saveImageNode: SaveImageInvocation = { const saveImageNode: SaveImageInvocation = {
id: SAVE_IMAGE, id: SAVE_IMAGE,
type: 'save_image', type: 'save_image',
is_intermediate, is_intermediate,
use_cache: false, use_cache: false,
board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId },
}; };
graph.nodes[SAVE_IMAGE] = saveImageNode; graph.nodes[SAVE_IMAGE] = saveImageNode;

View File

@ -6,15 +6,18 @@ import {
SaveImageInvocation, SaveImageInvocation,
} from 'services/api/types'; } from 'services/api/types';
import { REALESRGAN as ESRGAN, SAVE_IMAGE } from './constants'; import { REALESRGAN as ESRGAN, SAVE_IMAGE } from './constants';
import { BoardId } from 'features/gallery/store/types';
type Arg = { type Arg = {
image_name: string; image_name: string;
esrganModelName: ESRGANModelName; esrganModelName: ESRGANModelName;
autoAddBoardId: BoardId;
}; };
export const buildAdHocUpscaleGraph = ({ export const buildAdHocUpscaleGraph = ({
image_name, image_name,
esrganModelName, esrganModelName,
autoAddBoardId,
}: Arg): Graph => { }: Arg): Graph => {
const realesrganNode: ESRGANInvocation = { const realesrganNode: ESRGANInvocation = {
id: ESRGAN, id: ESRGAN,
@ -28,6 +31,8 @@ export const buildAdHocUpscaleGraph = ({
id: SAVE_IMAGE, id: SAVE_IMAGE,
type: 'save_image', type: 'save_image',
use_cache: false, use_cache: false,
is_intermediate: false,
board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId },
}; };
const graph: NonNullableGraph = { const graph: NonNullableGraph = {

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setClipSkip } from 'features/parameters/store/generationSlice'; import { setClipSkip } from 'features/parameters/store/generationSlice';
import { clipSkipMap } from 'features/parameters/types/constants'; import { clipSkipMap } from 'features/parameters/types/constants';
@ -47,7 +47,7 @@ export default function ParamClipSkip() {
} }
return ( return (
<IAIInformationalPopover details="clipSkip"> <IAIInformationalPopover feature="clipSkip" placement="top">
<IAISlider <IAISlider
label={t('parameters.clipSkip')} label={t('parameters.clipSkip')}
aria-label={t('parameters.clipSkip')} aria-label={t('parameters.clipSkip')}

View File

@ -1,7 +1,8 @@
import { Box, Flex, Spacer, Text } from '@chakra-ui/react'; import { Flex, FormControl, FormLabel, Spacer } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice'; import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
@ -18,7 +19,6 @@ import ParamAspectRatio, {
} from '../../Core/ParamAspectRatio'; } from '../../Core/ParamAspectRatio';
import ParamBoundingBoxHeight from './ParamBoundingBoxHeight'; import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
import ParamBoundingBoxWidth from './ParamBoundingBoxWidth'; import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover';
const sizeOptsSelector = createSelector( const sizeOptsSelector = createSelector(
[generationSelector, canvasSelector], [generationSelector, canvasSelector],
@ -93,42 +93,29 @@ export default function ParamBoundingBoxSize() {
}, },
}} }}
> >
<Flex alignItems="center" gap={2}> <IAIInformationalPopover feature="paramRatio">
<Box width="full"> <FormControl as={Flex} flexDir="row" alignItems="center" gap={2}>
<IAIInformationalPopover details="paramRatio"> <FormLabel>{t('parameters.aspectRatio')}</FormLabel>
<Text <Spacer />
sx={{ <ParamAspectRatio />
fontSize: 'sm', <IAIIconButton
width: 'full', tooltip={t('ui.swapSizes')}
color: 'base.700', aria-label={t('ui.swapSizes')}
_dark: { size="sm"
color: 'base.300', icon={<MdOutlineSwapVert />}
}, fontSize={20}
}} onClick={handleToggleSize}
> />
{t('parameters.aspectRatio')} <IAIIconButton
</Text> tooltip={t('ui.lockRatio')}
</IAIInformationalPopover> aria-label={t('ui.lockRatio')}
</Box> size="sm"
<Spacer /> icon={<FaLock />}
<ParamAspectRatio /> isChecked={shouldLockAspectRatio}
<IAIIconButton onClick={handleLockRatio}
tooltip={t('ui.swapSizes')} />
aria-label={t('ui.swapSizes')} </FormControl>
size="sm" </IAIInformationalPopover>
icon={<MdOutlineSwapVert />}
fontSize={20}
onClick={handleToggleSize}
/>
<IAIIconButton
tooltip={t('ui.lockRatio')}
aria-label={t('ui.lockRatio')}
size="sm"
icon={<FaLock />}
isChecked={shouldLockAspectRatio}
onClick={handleLockRatio}
/>
</Flex>
<ParamBoundingBoxWidth /> <ParamBoundingBoxWidth />
<ParamBoundingBoxHeight /> <ParamBoundingBoxHeight />
</Flex> </Flex>

View File

@ -1,6 +1,6 @@
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { IAISelectDataType } from 'common/components/IAIMantineSearchableSelect'; import { IAISelectDataType } from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice';
@ -31,7 +31,7 @@ const ParamCanvasCoherenceMode = () => {
}; };
return ( return (
<IAIInformationalPopover details="compositingCoherenceMode"> <IAIInformationalPopover feature="compositingCoherenceMode">
<IAIMantineSelect <IAIMantineSelect
label={t('parameters.coherenceMode')} label={t('parameters.coherenceMode')}
data={coherenceModeSelectData} data={coherenceModeSelectData}

View File

@ -1,6 +1,6 @@
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setCanvasCoherenceSteps } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceSteps } from 'features/parameters/store/generationSlice';
import { memo } from 'react'; import { memo } from 'react';
@ -14,7 +14,7 @@ const ParamCanvasCoherenceSteps = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<IAIInformationalPopover details="compositingCoherenceSteps"> <IAIInformationalPopover feature="compositingCoherenceSteps">
<IAISlider <IAISlider
label={t('parameters.coherenceSteps')} label={t('parameters.coherenceSteps')}
min={1} min={1}

View File

@ -1,6 +1,6 @@
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setCanvasCoherenceStrength } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceStrength } from 'features/parameters/store/generationSlice';
import { memo } from 'react'; import { memo } from 'react';
@ -14,7 +14,7 @@ const ParamCanvasCoherenceStrength = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<IAIInformationalPopover details="compositingStrength"> <IAIInformationalPopover feature="compositingStrength">
<IAISlider <IAISlider
label={t('parameters.coherenceStrength')} label={t('parameters.coherenceStrength')}
min={0} min={0}

View File

@ -1,6 +1,6 @@
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setMaskBlur } from 'features/parameters/store/generationSlice'; import { setMaskBlur } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -13,7 +13,7 @@ export default function ParamMaskBlur() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<IAIInformationalPopover details="compositingBlur"> <IAIInformationalPopover feature="compositingBlur">
<IAISlider <IAISlider
label={t('parameters.maskBlur')} label={t('parameters.maskBlur')}
min={0} min={0}

View File

@ -2,7 +2,7 @@ import { SelectItem } from '@mantine/core';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { setMaskBlurMethod } from 'features/parameters/store/generationSlice'; import { setMaskBlurMethod } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -29,7 +29,7 @@ export default function ParamMaskBlurMethod() {
}; };
return ( return (
<IAIInformationalPopover details="compositingBlurMethod"> <IAIInformationalPopover feature="compositingBlurMethod">
<IAIMantineSelect <IAIMantineSelect
value={maskBlurMethod} value={maskBlurMethod}
onChange={handleMaskBlurMethodChange} onChange={handleMaskBlurMethodChange}

View File

@ -15,19 +15,13 @@ const ParamCompositingSettingsCollapse = () => {
return ( return (
<IAICollapse label={t('parameters.compositingSettingsHeader')}> <IAICollapse label={t('parameters.compositingSettingsHeader')}>
<Flex sx={{ flexDirection: 'column', gap: 2 }}> <Flex sx={{ flexDirection: 'column', gap: 2 }}>
<SubParametersWrapper <SubParametersWrapper label={t('parameters.coherencePassHeader')}>
label={t('parameters.coherencePassHeader')}
headerInfoPopover="compositingCoherencePass"
>
<ParamCanvasCoherenceMode /> <ParamCanvasCoherenceMode />
<ParamCanvasCoherenceSteps /> <ParamCanvasCoherenceSteps />
<ParamCanvasCoherenceStrength /> <ParamCanvasCoherenceStrength />
</SubParametersWrapper> </SubParametersWrapper>
<Divider /> <Divider />
<SubParametersWrapper <SubParametersWrapper label={t('parameters.maskAdjustmentsHeader')}>
label={t('parameters.maskAdjustmentsHeader')}
headerInfoPopover="compositingMaskAdjustments"
>
<ParamMaskBlur /> <ParamMaskBlur />
<ParamMaskBlurMethod /> <ParamMaskBlurMethod />
</SubParametersWrapper> </SubParametersWrapper>

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { setInfillMethod } from 'features/parameters/store/generationSlice'; import { setInfillMethod } from 'features/parameters/store/generationSlice';
@ -40,7 +40,7 @@ const ParamInfillMethod = () => {
); );
return ( return (
<IAIInformationalPopover details="infillMethod"> <IAIInformationalPopover feature="infillMethod">
<IAIMantineSelect <IAIMantineSelect
disabled={infill_methods?.length === 0} disabled={infill_methods?.length === 0}
placeholder={isLoading ? 'Loading...' : undefined} placeholder={isLoading ? 'Loading...' : undefined}

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice';
@ -36,7 +36,7 @@ const ParamScaleBeforeProcessing = () => {
}; };
return ( return (
<IAIInformationalPopover details="scaleBeforeProcessing"> <IAIInformationalPopover feature="scaleBeforeProcessing">
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('parameters.scaleBeforeProcessing')} label={t('parameters.scaleBeforeProcessing')}
data={BOUNDING_BOX_SCALES_DICT} data={BOUNDING_BOX_SCALES_DICT}

View File

@ -1,4 +1,4 @@
import { ButtonGroup, Flex } from '@chakra-ui/react'; import { ButtonGroup } from '@chakra-ui/react';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
@ -29,25 +29,23 @@ export default function ParamAspectRatio() {
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
return ( return (
<Flex gap={2} flexGrow={1}> <ButtonGroup isAttached>
<ButtonGroup isAttached> {aspectRatios.map((ratio) => (
{aspectRatios.map((ratio) => ( <IAIButton
<IAIButton key={ratio.name}
key={ratio.name} size="sm"
size="sm" isChecked={aspectRatio === ratio.value}
isChecked={aspectRatio === ratio.value} isDisabled={
isDisabled={ activeTabName === 'img2img' ? !shouldFitToWidthHeight : false
activeTabName === 'img2img' ? !shouldFitToWidthHeight : false }
} onClick={() => {
onClick={() => { dispatch(setAspectRatio(ratio.value));
dispatch(setAspectRatio(ratio.value)); dispatch(setShouldLockAspectRatio(false));
dispatch(setShouldLockAspectRatio(false)); }}
}} >
> {ratio.name}
{ratio.name} </IAIButton>
</IAIButton> ))}
))} </ButtonGroup>
</ButtonGroup>
</Flex>
); );
} }

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setCfgScale } from 'features/parameters/store/generationSlice'; import { setCfgScale } from 'features/parameters/store/generationSlice';
@ -54,7 +54,7 @@ const ParamCFGScale = () => {
); );
return shouldUseSliders ? ( return shouldUseSliders ? (
<IAIInformationalPopover details="paramCFGScale"> <IAIInformationalPopover feature="paramCFGScale">
<IAISlider <IAISlider
label={t('parameters.cfgScale')} label={t('parameters.cfgScale')}
step={shift ? 0.1 : 0.5} step={shift ? 0.1 : 0.5}
@ -71,7 +71,7 @@ const ParamCFGScale = () => {
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
) : ( ) : (
<IAIInformationalPopover details="paramCFGScale"> <IAIInformationalPopover feature="paramCFGScale">
<IAINumberInput <IAINumberInput
label={t('parameters.cfgScale')} label={t('parameters.cfgScale')}
step={0.5} step={0.5}

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setIterations } from 'features/parameters/store/generationSlice'; import { setIterations } from 'features/parameters/store/generationSlice';
@ -61,7 +61,7 @@ const ParamIterations = ({ asSlider }: Props) => {
}, [dispatch, initial]); }, [dispatch, initial]);
return asSlider || shouldUseSliders ? ( return asSlider || shouldUseSliders ? (
<IAIInformationalPopover details="paramIterations"> <IAIInformationalPopover feature="paramIterations">
<IAISlider <IAISlider
label={t('parameters.iterations')} label={t('parameters.iterations')}
step={step} step={step}
@ -77,7 +77,7 @@ const ParamIterations = ({ asSlider }: Props) => {
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
) : ( ) : (
<IAIInformationalPopover details="paramIterations"> <IAIInformationalPopover feature="paramIterations">
<IAINumberInput <IAINumberInput
label={t('parameters.iterations')} label={t('parameters.iterations')}
step={step} step={step}

View File

@ -1,7 +1,7 @@
import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import { Box, FormControl, useDisclosure } from '@chakra-ui/react';
import type { RootState } from 'app/store/store'; import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAITextarea from 'common/components/IAITextarea'; import IAITextarea from 'common/components/IAITextarea';
import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton';
import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover';
@ -76,15 +76,15 @@ const ParamNegativeConditioning = () => {
const isEmbeddingEnabled = useFeatureStatus('embedding').isFeatureEnabled; const isEmbeddingEnabled = useFeatureStatus('embedding').isFeatureEnabled;
return ( return (
<IAIInformationalPopover <FormControl>
placement="right" <ParamEmbeddingPopover
details="paramNegativeConditioning" isOpen={isOpen}
> onClose={onClose}
<FormControl> onSelect={handleSelectEmbedding}
<ParamEmbeddingPopover >
isOpen={isOpen} <IAIInformationalPopover
onClose={onClose} feature="paramNegativeConditioning"
onSelect={handleSelectEmbedding} placement="right"
> >
<IAITextarea <IAITextarea
id="negativePrompt" id="negativePrompt"
@ -98,20 +98,20 @@ const ParamNegativeConditioning = () => {
minH={16} minH={16}
{...(isEmbeddingEnabled && { onKeyDown: handleKeyDown })} {...(isEmbeddingEnabled && { onKeyDown: handleKeyDown })}
/> />
</ParamEmbeddingPopover> </IAIInformationalPopover>
{!isOpen && isEmbeddingEnabled && ( </ParamEmbeddingPopover>
<Box {!isOpen && isEmbeddingEnabled && (
sx={{ <Box
position: 'absolute', sx={{
top: 0, position: 'absolute',
insetInlineEnd: 0, top: 0,
}} insetInlineEnd: 0,
> }}
<AddEmbeddingButton onClick={onOpen} /> >
</Box> <AddEmbeddingButton onClick={onOpen} />
)} </Box>
</FormControl> )}
</IAIInformationalPopover> </FormControl>
); );
}; };

View File

@ -2,6 +2,7 @@ import { Box, FormControl, useDisclosure } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAITextarea from 'common/components/IAITextarea'; import IAITextarea from 'common/components/IAITextarea';
import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton';
import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover';
@ -12,7 +13,6 @@ import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
import IAIInformationalPopover from '../../../../../common/components/IAIInformationalPopover';
const promptInputSelector = createSelector( const promptInputSelector = createSelector(
[stateSelector], [stateSelector],
@ -104,15 +104,15 @@ const ParamPositiveConditioning = () => {
return ( return (
<Box position="relative"> <Box position="relative">
<IAIInformationalPopover <FormControl>
placement="right" <ParamEmbeddingPopover
details="paramPositiveConditioning" isOpen={isOpen}
> onClose={onClose}
<FormControl> onSelect={handleSelectEmbedding}
<ParamEmbeddingPopover >
isOpen={isOpen} <IAIInformationalPopover
onClose={onClose} feature="paramPositiveConditioning"
onSelect={handleSelectEmbedding} placement="right"
> >
<IAITextarea <IAITextarea
id="prompt" id="prompt"
@ -125,9 +125,9 @@ const ParamPositiveConditioning = () => {
resize="vertical" resize="vertical"
minH={32} minH={32}
/> />
</ParamEmbeddingPopover> </IAIInformationalPopover>
</FormControl> </ParamEmbeddingPopover>
</IAIInformationalPopover> </FormControl>
{!isOpen && isEmbeddingEnabled && ( {!isOpen && isEmbeddingEnabled && (
<Box <Box
sx={{ sx={{

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setScheduler } from 'features/parameters/store/generationSlice'; import { setScheduler } from 'features/parameters/store/generationSlice';
@ -52,7 +52,7 @@ const ParamScheduler = () => {
); );
return ( return (
<IAIInformationalPopover details="paramScheduler"> <IAIInformationalPopover feature="paramScheduler">
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('parameters.scheduler')} label={t('parameters.scheduler')}
value={scheduler} value={scheduler}

View File

@ -1,7 +1,9 @@
import { Box, Flex, Spacer, Text } from '@chakra-ui/react'; import { Flex, FormControl, FormLabel, Spacer } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { import {
setAspectRatio, setAspectRatio,
@ -16,8 +18,6 @@ import { activeTabNameSelector } from '../../../../ui/store/uiSelectors';
import ParamAspectRatio, { mappedAspectRatios } from './ParamAspectRatio'; import ParamAspectRatio, { mappedAspectRatios } from './ParamAspectRatio';
import ParamHeight from './ParamHeight'; import ParamHeight from './ParamHeight';
import ParamWidth from './ParamWidth'; import ParamWidth from './ParamWidth';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
const sizeOptsSelector = createSelector( const sizeOptsSelector = createSelector(
[generationSelector, activeTabNameSelector], [generationSelector, activeTabNameSelector],
@ -83,47 +83,35 @@ export default function ParamSize() {
}, },
}} }}
> >
<Flex alignItems="center" gap={2}> <IAIInformationalPopover feature="paramRatio">
<Box width="full"> <FormControl as={Flex} flexDir="row" alignItems="center" gap={2}>
<IAIInformationalPopover details="paramRatio"> <FormLabel>{t('parameters.aspectRatio')}</FormLabel>
<Text <Spacer />
sx={{ <ParamAspectRatio />
fontSize: 'sm', <IAIIconButton
color: 'base.700', tooltip={t('ui.swapSizes')}
_dark: { aria-label={t('ui.swapSizes')}
color: 'base.300', size="sm"
}, icon={<MdOutlineSwapVert />}
}} fontSize={20}
> isDisabled={
{t('parameters.aspectRatio')} activeTabName === 'img2img' ? !shouldFitToWidthHeight : false
</Text> }
</IAIInformationalPopover> onClick={handleToggleSize}
</Box> />
<Spacer /> <IAIIconButton
<ParamAspectRatio /> tooltip={t('ui.lockRatio')}
<IAIIconButton aria-label={t('ui.lockRatio')}
tooltip={t('ui.swapSizes')} size="sm"
aria-label={t('ui.swapSizes')} icon={<FaLock />}
size="sm" isChecked={shouldLockAspectRatio}
icon={<MdOutlineSwapVert />} isDisabled={
fontSize={20} activeTabName === 'img2img' ? !shouldFitToWidthHeight : false
isDisabled={ }
activeTabName === 'img2img' ? !shouldFitToWidthHeight : false onClick={handleLockRatio}
} />
onClick={handleToggleSize} </FormControl>
/> </IAIInformationalPopover>
<IAIIconButton
tooltip={t('ui.lockRatio')}
aria-label={t('ui.lockRatio')}
size="sm"
icon={<FaLock />}
isChecked={shouldLockAspectRatio}
isDisabled={
activeTabName === 'img2img' ? !shouldFitToWidthHeight : false
}
onClick={handleLockRatio}
/>
</Flex>
<Flex gap={2} alignItems="center"> <Flex gap={2} alignItems="center">
<Flex gap={2} flexDirection="column" width="full"> <Flex gap={2} flexDirection="column" width="full">
<ParamWidth <ParamWidth

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
@ -57,7 +57,7 @@ const ParamSteps = () => {
}, [dispatch]); }, [dispatch]);
return shouldUseSliders ? ( return shouldUseSliders ? (
<IAIInformationalPopover details="paramSteps"> <IAIInformationalPopover feature="paramSteps">
<IAISlider <IAISlider
label={t('parameters.steps')} label={t('parameters.steps')}
min={min} min={min}
@ -73,7 +73,7 @@ const ParamSteps = () => {
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
) : ( ) : (
<IAIInformationalPopover details="paramSteps"> <IAIInformationalPopover feature="paramSteps">
<IAINumberInput <IAINumberInput
label={t('parameters.steps')} label={t('parameters.steps')}
min={min} min={min}

View File

@ -7,7 +7,7 @@ import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SubParametersWrapper from '../SubParametersWrapper'; import SubParametersWrapper from '../SubParametersWrapper';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -46,8 +46,8 @@ const ImageToImageStrength = () => {
}, [dispatch, initial]); }, [dispatch, initial]);
return ( return (
<SubParametersWrapper> <IAIInformationalPopover feature="paramDenoisingStrength">
<IAIInformationalPopover details="paramDenoisingStrength"> <SubParametersWrapper>
<IAISlider <IAISlider
label={`${t('parameters.denoisingStrength')}`} label={`${t('parameters.denoisingStrength')}`}
step={step} step={step}
@ -62,8 +62,8 @@ const ImageToImageStrength = () => {
withReset withReset
sliderNumberInputProps={{ max: inputMax }} sliderNumberInputProps={{ max: inputMax }}
/> />
</IAIInformationalPopover> </SubParametersWrapper>
</SubParametersWrapper> </IAIInformationalPopover>
); );
}; };

View File

@ -21,7 +21,7 @@ import {
useGetOnnxModelsQuery, useGetOnnxModelsQuery,
} from 'services/api/endpoints/models'; } from 'services/api/endpoints/models';
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -120,7 +120,7 @@ const ParamMainModelSelect = () => {
/> />
) : ( ) : (
<Flex w="100%" alignItems="center" gap={3}> <Flex w="100%" alignItems="center" gap={3}>
<IAIInformationalPopover details="paramModel" placement="bottom"> <IAIInformationalPopover feature="paramModel">
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
label={t('modelManager.model')} label={t('modelManager.model')}
@ -136,7 +136,7 @@ const ParamMainModelSelect = () => {
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
{isSyncModelEnabled && ( {isSyncModelEnabled && (
<Box mt={7}> <Box mt={6}>
<SyncModelsButton iconMode /> <SyncModelsButton iconMode />
</Box> </Box>
)} )}

View File

@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice'; import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice';
import { ChangeEvent, useCallback } from 'react'; import { ChangeEvent, useCallback } from 'react';
@ -20,7 +20,7 @@ export const ParamCpuNoiseToggle = () => {
); );
return ( return (
<IAIInformationalPopover details="noiseUseCPU"> <IAIInformationalPopover feature="noiseUseCPU">
<IAISwitch <IAISwitch
label={t('parameters.useCpuNoise')} label={t('parameters.useCpuNoise')}
isChecked={shouldUseCpuNoise} isChecked={shouldUseCpuNoise}

View File

@ -3,11 +3,11 @@ import { memo } from 'react';
import ParamSeed from './ParamSeed'; import ParamSeed from './ParamSeed';
import ParamSeedShuffle from './ParamSeedShuffle'; import ParamSeedShuffle from './ParamSeedShuffle';
import ParamSeedRandomize from './ParamSeedRandomize'; import ParamSeedRandomize from './ParamSeedRandomize';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const ParamSeedFull = () => { const ParamSeedFull = () => {
return ( return (
<IAIInformationalPopover details="paramSeed"> <IAIInformationalPopover feature="paramSeed">
<Flex sx={{ gap: 3, alignItems: 'flex-end' }}> <Flex sx={{ gap: 3, alignItems: 'flex-end' }}>
<ParamSeed /> <ParamSeed />
<ParamSeedShuffle /> <ParamSeedShuffle />

View File

@ -1,40 +1,28 @@
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text, forwardRef } from '@chakra-ui/react';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover';
import { ReactNode, memo } from 'react'; import { ReactNode, memo } from 'react';
type SubParameterWrapperProps = { type SubParameterWrapperProps = {
children: ReactNode | ReactNode[]; children: ReactNode;
label?: string; label?: string;
headerInfoPopover?: string;
}; };
const SubParametersWrapper = (props: SubParameterWrapperProps) => ( const SubParametersWrapper = forwardRef(
<Flex (props: SubParameterWrapperProps, ref) => (
sx={{ <Flex
flexDir: 'column', ref={ref}
gap: 2, sx={{
bg: 'base.100', flexDir: 'column',
px: 4, gap: 2,
pt: 2, bg: 'base.100',
pb: 4, px: 4,
borderRadius: 'base', pt: 2,
_dark: { pb: 4,
bg: 'base.750', borderRadius: 'base',
}, _dark: {
}} bg: 'base.750',
> },
{props.headerInfoPopover && props.label && ( }}
<IAIInformationalPopover details={props.headerInfoPopover}> >
<Text
fontSize="sm"
fontWeight="bold"
sx={{ color: 'base.600', _dark: { color: 'base.300' } }}
>
{props.label}
</Text>
</IAIInformationalPopover>
)}
{!props.headerInfoPopover && props.label && (
<Text <Text
fontSize="sm" fontSize="sm"
fontWeight="bold" fontWeight="bold"
@ -42,9 +30,9 @@ const SubParametersWrapper = (props: SubParameterWrapperProps) => (
> >
{props.label} {props.label}
</Text> </Text>
)} {props.children}
{props.children} </Flex>
</Flex> )
); );
SubParametersWrapper.displayName = 'SubSettingsWrapper'; SubParametersWrapper.displayName = 'SubSettingsWrapper';

View File

@ -15,7 +15,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI
import { vaeSelected } from 'features/parameters/store/generationSlice'; import { vaeSelected } from 'features/parameters/store/generationSlice';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam'; import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -94,7 +94,7 @@ const ParamVAEModelSelect = () => {
); );
return ( return (
<IAIInformationalPopover details="paramVAE"> <IAIInformationalPopover feature="paramVAE">
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
tooltip={selectedVaeModel?.description} tooltip={selectedVaeModel?.description}

View File

@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { vaePrecisionChanged } from 'features/parameters/store/generationSlice'; import { vaePrecisionChanged } from 'features/parameters/store/generationSlice';
import { PrecisionParam } from 'features/parameters/types/parameterSchemas'; import { PrecisionParam } from 'features/parameters/types/parameterSchemas';
@ -35,7 +35,7 @@ const ParamVAEModelSelect = () => {
); );
return ( return (
<IAIInformationalPopover details="paramVAEPrecision"> <IAIInformationalPopover feature="paramVAEPrecision">
<IAIMantineSelect <IAIMantineSelect
label="VAE Precision" label="VAE Precision"
value={vaePrecision} value={vaePrecision}

View File

@ -7,7 +7,7 @@ import SubParametersWrapper from 'features/parameters/components/Parameters/SubP
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { setSDXLImg2ImgDenoisingStrength } from '../store/sdxlSlice'; import { setSDXLImg2ImgDenoisingStrength } from '../store/sdxlSlice';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -36,8 +36,8 @@ const ParamSDXLImg2ImgDenoisingStrength = () => {
}, [dispatch]); }, [dispatch]);
return ( return (
<SubParametersWrapper> <IAIInformationalPopover feature="paramDenoisingStrength">
<IAIInformationalPopover details="paramDenoisingStrength"> <SubParametersWrapper>
<IAISlider <IAISlider
label={t('sdxl.denoisingStrength')} label={t('sdxl.denoisingStrength')}
step={0.01} step={0.01}
@ -51,8 +51,8 @@ const ParamSDXLImg2ImgDenoisingStrength = () => {
withSliderMarks withSliderMarks
withReset withReset
/> />
</IAIInformationalPopover> </SubParametersWrapper>
</SubParametersWrapper> </IAIInformationalPopover>
); );
}; };

File diff suppressed because one or more lines are too long

View File

@ -152,6 +152,8 @@ export type SaveImageInvocation = s['SaveImageInvocation'];
export type ControlNetInvocation = s['ControlNetInvocation']; export type ControlNetInvocation = s['ControlNetInvocation'];
export type IPAdapterInvocation = s['IPAdapterInvocation']; export type IPAdapterInvocation = s['IPAdapterInvocation'];
export type CannyImageProcessorInvocation = s['CannyImageProcessorInvocation']; export type CannyImageProcessorInvocation = s['CannyImageProcessorInvocation'];
export type ColorMapImageProcessorInvocation =
s['ColorMapImageProcessorInvocation'];
export type ContentShuffleImageProcessorInvocation = export type ContentShuffleImageProcessorInvocation =
s['ContentShuffleImageProcessorInvocation']; s['ContentShuffleImageProcessorInvocation'];
export type HedImageProcessorInvocation = s['HedImageProcessorInvocation']; export type HedImageProcessorInvocation = s['HedImageProcessorInvocation'];

View File

@ -31,14 +31,14 @@ const invokeAIContent = defineStyle((props) => {
const informationalContent = defineStyle((props) => { const informationalContent = defineStyle((props) => {
return { return {
[$arrowBg.variable]: mode('colors.base.100', 'colors.base.600')(props), [$arrowBg.variable]: mode('colors.base.100', 'colors.base.700')(props),
[$popperBg.variable]: mode('colors.base.100', 'colors.base.600')(props), [$popperBg.variable]: mode('colors.base.100', 'colors.base.700')(props),
[$arrowShadowColor.variable]: mode( [$arrowShadowColor.variable]: mode(
'colors.base.400', 'colors.base.400',
'colors.base.400' 'colors.base.400'
)(props), )(props),
p: 4, p: 4,
bg: mode('base.100', 'base.600')(props), bg: mode('base.100', 'base.700')(props),
border: 'none', border: 'none',
shadow: 'dark-lg', shadow: 'dark-lg',
}; };
@ -46,6 +46,7 @@ const informationalContent = defineStyle((props) => {
const invokeAI = definePartsStyle((props) => ({ const invokeAI = definePartsStyle((props) => ({
content: invokeAIContent(props), content: invokeAIContent(props),
body: { padding: 0 },
})); }));
const informational = definePartsStyle((props) => ({ const informational = definePartsStyle((props) => ({

View File

@ -76,6 +76,7 @@ export const theme: ThemeOverride = {
direction: 'ltr', direction: 'ltr',
fonts: { fonts: {
body: `'Inter Variable', sans-serif`, body: `'Inter Variable', sans-serif`,
heading: `'Inter Variable', sans-serif`,
}, },
shadows: { shadows: {
light: { light: {