mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-23 18:17:59 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bdd779b4 | ||
|
|
f92b4d4f35 | ||
|
|
3ca00e3948 | ||
|
|
61114ce854 | ||
|
|
f3554b4e1b | ||
|
|
9dcb025241 | ||
|
|
ecf646066a | ||
|
|
3fd10b68cd | ||
|
|
6e32c7993c | ||
|
|
8329533848 | ||
|
|
fc7157b029 | ||
|
|
a1897f7490 | ||
|
|
a89b3efd14 | ||
|
|
5259693ed1 | ||
|
|
d77c24206d | ||
|
|
c5069557f3 | ||
|
|
9b220f61bd | ||
|
|
7fc3af12cc | ||
|
|
e2721b46b6 | ||
|
|
17118a04bd | ||
|
|
24788e3c83 | ||
|
|
056387c981 | ||
|
|
8a43d90273 | ||
|
|
4f9b9760db | ||
|
|
fdaddafa56 | ||
|
|
23d59abbd7 | ||
|
|
cf7fa5bce8 | ||
|
|
39e41998bb | ||
|
|
c6eff71b74 | ||
|
|
6ea4c47757 | ||
|
|
91f91aa835 | ||
|
|
ea7868d076 | ||
|
|
7d86f00d82 | ||
|
|
7785061e7d | ||
|
|
3370052e54 | ||
|
|
325dacd29c | ||
|
|
f4981a6ba9 | ||
|
|
8c159942eb | ||
|
|
deb4dc64af | ||
|
|
1a11437b6f | ||
|
|
04572c94ad | ||
|
|
1e9e78089e | ||
|
|
e65f93663d | ||
|
|
2a796fe25e |
8
.github/workflows/build-container.yml
vendored
8
.github/workflows/build-container.yml
vendored
@@ -45,6 +45,9 @@ jobs:
|
||||
steps:
|
||||
- name: Free up more disk space on the runner
|
||||
# https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
|
||||
# the /mnt dir has 70GBs of free space
|
||||
# /dev/sda1 74G 28K 70G 1% /mnt
|
||||
# According to some online posts the /mnt is not always there, so checking before setting docker to use it
|
||||
run: |
|
||||
echo "----- Free space before cleanup"
|
||||
df -h
|
||||
@@ -52,6 +55,11 @@ jobs:
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo swapoff /mnt/swapfile
|
||||
sudo rm -rf /mnt/swapfile
|
||||
if [ -d /mnt ]; then
|
||||
sudo chmod -R 777 /mnt
|
||||
echo '{"data-root": "/mnt/docker-root"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
fi
|
||||
echo "----- Free space after cleanup"
|
||||
df -h
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ If the key is unrecognized, this call raises an
|
||||
|
||||
#### exists(key) -> AnyModelConfig
|
||||
|
||||
Returns True if a model with the given key exists in the databsae.
|
||||
Returns True if a model with the given key exists in the database.
|
||||
|
||||
#### search_by_path(path) -> AnyModelConfig
|
||||
|
||||
@@ -718,7 +718,7 @@ When downloading remote models is implemented, additional
|
||||
configuration information, such as list of trigger terms, will be
|
||||
retrieved from the HuggingFace and Civitai model repositories.
|
||||
|
||||
The probed values can be overriden by providing a dictionary in the
|
||||
The probed values can be overridden by providing a dictionary in the
|
||||
optional `config` argument passed to `import_model()`. You may provide
|
||||
overriding values for any of the model's configuration
|
||||
attributes. Here is an example of setting the
|
||||
@@ -841,7 +841,7 @@ variable.
|
||||
|
||||
#### installer.start(invoker)
|
||||
|
||||
The `start` method is called by the API intialization routines when
|
||||
The `start` method is called by the API initialization routines when
|
||||
the API starts up. Its effect is to call `sync_to_config()` to
|
||||
synchronize the model record store database with what's currently on
|
||||
disk.
|
||||
|
||||
@@ -16,7 +16,7 @@ We thank [all contributors](https://github.com/invoke-ai/InvokeAI/graphs/contrib
|
||||
- @psychedelicious (Spencer Mabrito) - Web Team Leader
|
||||
- @joshistoast (Josh Corbett) - Web Development
|
||||
- @cheerio (Mary Rogers) - Lead Engineer & Web App Development
|
||||
- @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
|
||||
- @ebr (Eugene Brodsky) - Cloud/DevOps/Software engineer; your friendly neighbourhood cluster-autoscaler
|
||||
- @sunija - Standalone version
|
||||
- @brandon (Brandon Rising) - Platform, Infrastructure, Backend Systems
|
||||
- @ryanjdick (Ryan Dick) - Machine Learning & Training
|
||||
|
||||
@@ -41,7 +41,7 @@ Nodes have a "Use Cache" option in their footer. This allows for performance imp
|
||||
|
||||
There are several node grouping concepts that can be examined with a narrow focus. These (and other) groupings can be pieced together to make up functional graph setups, and are important to understanding how groups of nodes work together as part of a whole. Note that the screenshots below aren't examples of complete functioning node graphs (see Examples).
|
||||
|
||||
### Noise
|
||||
### Create Latent Noise
|
||||
|
||||
An initial noise tensor is necessary for the latent diffusion process. As a result, the Denoising node requires a noise node input.
|
||||
|
||||
|
||||
@@ -36,9 +36,19 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
image: ImageField = InputField(description="The image to encode.")
|
||||
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
|
||||
|
||||
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoencoderKL) -> int:
|
||||
"""Estimate the working memory required by the invocation in bytes."""
|
||||
# Encode operations use approximately 50% of the memory required for decode operations
|
||||
h = image_tensor.shape[-2]
|
||||
w = image_tensor.shape[-1]
|
||||
element_size = next(vae.parameters()).element_size()
|
||||
scaling_constant = 1100 # 50% of decode scaling constant (2200)
|
||||
working_memory = h * w * element_size * scaling_constant
|
||||
return int(working_memory)
|
||||
|
||||
@staticmethod
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
|
||||
with vae_info as vae:
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
|
||||
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
|
||||
assert isinstance(vae, AutoencoderKL)
|
||||
|
||||
vae.disable_tiling()
|
||||
@@ -62,7 +72,12 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
|
||||
|
||||
vae_info = context.models.load(self.vae.vae)
|
||||
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
|
||||
assert isinstance(vae_info.model, AutoencoderKL)
|
||||
|
||||
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
|
||||
latents = self.vae_encode(
|
||||
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
|
||||
)
|
||||
|
||||
latents = latents.to("cpu")
|
||||
name = context.tensors.save(tensor=latents)
|
||||
|
||||
@@ -328,6 +328,21 @@ class FluxDenoiseInvocation(BaseInvocation):
|
||||
cfg_scale_end_step=self.cfg_scale_end_step,
|
||||
)
|
||||
|
||||
kontext_extension = None
|
||||
if self.kontext_conditioning:
|
||||
if not self.controlnet_vae:
|
||||
raise ValueError("A VAE (e.g., controlnet_vae) must be provided to use Kontext conditioning.")
|
||||
|
||||
kontext_extension = KontextExtension(
|
||||
context=context,
|
||||
kontext_conditioning=self.kontext_conditioning
|
||||
if isinstance(self.kontext_conditioning, list)
|
||||
else [self.kontext_conditioning],
|
||||
vae_field=self.controlnet_vae,
|
||||
device=TorchDevice.choose_torch_device(),
|
||||
dtype=inference_dtype,
|
||||
)
|
||||
|
||||
with ExitStack() as exit_stack:
|
||||
# Prepare ControlNet extensions.
|
||||
# Note: We do this before loading the transformer model to minimize peak memory (see implementation).
|
||||
@@ -385,21 +400,6 @@ class FluxDenoiseInvocation(BaseInvocation):
|
||||
dtype=inference_dtype,
|
||||
)
|
||||
|
||||
kontext_extension = None
|
||||
if self.kontext_conditioning:
|
||||
if not self.controlnet_vae:
|
||||
raise ValueError("A VAE (e.g., controlnet_vae) must be provided to use Kontext conditioning.")
|
||||
|
||||
kontext_extension = KontextExtension(
|
||||
context=context,
|
||||
kontext_conditioning=self.kontext_conditioning
|
||||
if isinstance(self.kontext_conditioning, list)
|
||||
else [self.kontext_conditioning],
|
||||
vae_field=self.controlnet_vae,
|
||||
device=TorchDevice.choose_torch_device(),
|
||||
dtype=inference_dtype,
|
||||
)
|
||||
|
||||
# Prepare Kontext conditioning if provided
|
||||
img_cond_seq = None
|
||||
img_cond_seq_ids = None
|
||||
|
||||
@@ -35,14 +35,24 @@ class FluxVaeEncodeInvocation(BaseInvocation):
|
||||
input=Input.Connection,
|
||||
)
|
||||
|
||||
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoEncoder) -> int:
|
||||
"""Estimate the working memory required by the invocation in bytes."""
|
||||
# Encode operations use approximately 50% of the memory required for decode operations
|
||||
h = image_tensor.shape[-2]
|
||||
w = image_tensor.shape[-1]
|
||||
element_size = next(vae.parameters()).element_size()
|
||||
scaling_constant = 1100 # 50% of decode scaling constant (2200)
|
||||
working_memory = h * w * element_size * scaling_constant
|
||||
return int(working_memory)
|
||||
|
||||
@staticmethod
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
|
||||
# TODO(ryand): Expose seed parameter at the invocation level.
|
||||
# TODO(ryand): Write a util function for generating random tensors that is consistent across devices / dtypes.
|
||||
# There's a starting point in get_noise(...), but it needs to be extracted and generalized. This function
|
||||
# should be used for VAE encode sampling.
|
||||
generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
|
||||
with vae_info as vae:
|
||||
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
|
||||
assert isinstance(vae, AutoEncoder)
|
||||
vae_dtype = next(iter(vae.parameters())).dtype
|
||||
image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
|
||||
@@ -60,7 +70,10 @@ class FluxVaeEncodeInvocation(BaseInvocation):
|
||||
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
|
||||
|
||||
context.util.signal_progress("Running VAE")
|
||||
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
|
||||
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
|
||||
latents = self.vae_encode(
|
||||
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
|
||||
)
|
||||
|
||||
latents = latents.to("cpu")
|
||||
name = context.tensors.save(tensor=latents)
|
||||
|
||||
@@ -52,11 +52,48 @@ class ImageToLatentsInvocation(BaseInvocation):
|
||||
tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
|
||||
fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
|
||||
|
||||
def _estimate_working_memory(
|
||||
self, image_tensor: torch.Tensor, use_tiling: bool, vae: AutoencoderKL | AutoencoderTiny
|
||||
) -> int:
|
||||
"""Estimate the working memory required by the invocation in bytes."""
|
||||
# Encode operations use approximately 50% of the memory required for decode operations
|
||||
element_size = 4 if self.fp32 else 2
|
||||
scaling_constant = 1100 # 50% of decode scaling constant (2200)
|
||||
|
||||
if use_tiling:
|
||||
tile_size = self.tile_size
|
||||
if tile_size == 0:
|
||||
tile_size = vae.tile_sample_min_size
|
||||
assert isinstance(tile_size, int)
|
||||
h = tile_size
|
||||
w = tile_size
|
||||
working_memory = h * w * element_size * scaling_constant
|
||||
|
||||
# We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap
|
||||
# and number of tiles. We could make this more precise in the future, but this should be good enough for
|
||||
# most use cases.
|
||||
working_memory = working_memory * 1.25
|
||||
else:
|
||||
h = image_tensor.shape[-2]
|
||||
w = image_tensor.shape[-1]
|
||||
working_memory = h * w * element_size * scaling_constant
|
||||
|
||||
if self.fp32:
|
||||
# If we are running in FP32, then we should account for the likely increase in model size (~250MB).
|
||||
working_memory += 250 * 2**20
|
||||
|
||||
return int(working_memory)
|
||||
|
||||
@staticmethod
|
||||
def vae_encode(
|
||||
vae_info: LoadedModel, upcast: bool, tiled: bool, image_tensor: torch.Tensor, tile_size: int = 0
|
||||
vae_info: LoadedModel,
|
||||
upcast: bool,
|
||||
tiled: bool,
|
||||
image_tensor: torch.Tensor,
|
||||
tile_size: int = 0,
|
||||
estimated_working_memory: int = 0,
|
||||
) -> torch.Tensor:
|
||||
with vae_info as vae:
|
||||
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
|
||||
assert isinstance(vae, (AutoencoderKL, AutoencoderTiny))
|
||||
orig_dtype = vae.dtype
|
||||
if upcast:
|
||||
@@ -113,14 +150,23 @@ class ImageToLatentsInvocation(BaseInvocation):
|
||||
image = context.images.get_pil(self.image.image_name)
|
||||
|
||||
vae_info = context.models.load(self.vae.vae)
|
||||
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny))
|
||||
|
||||
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
|
||||
if image_tensor.dim() == 3:
|
||||
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
|
||||
|
||||
use_tiling = self.tiled or context.config.get().force_tiled_decode
|
||||
estimated_working_memory = self._estimate_working_memory(image_tensor, use_tiling, vae_info.model)
|
||||
|
||||
context.util.signal_progress("Running VAE encoder")
|
||||
latents = self.vae_encode(
|
||||
vae_info=vae_info, upcast=self.fp32, tiled=self.tiled, image_tensor=image_tensor, tile_size=self.tile_size
|
||||
vae_info=vae_info,
|
||||
upcast=self.fp32,
|
||||
tiled=self.tiled,
|
||||
image_tensor=image_tensor,
|
||||
tile_size=self.tile_size,
|
||||
estimated_working_memory=estimated_working_memory,
|
||||
)
|
||||
|
||||
latents = latents.to("cpu")
|
||||
|
||||
@@ -32,9 +32,19 @@ class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
image: ImageField = InputField(description="The image to encode")
|
||||
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
|
||||
|
||||
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoencoderKL) -> int:
|
||||
"""Estimate the working memory required by the invocation in bytes."""
|
||||
# Encode operations use approximately 50% of the memory required for decode operations
|
||||
h = image_tensor.shape[-2]
|
||||
w = image_tensor.shape[-1]
|
||||
element_size = next(vae.parameters()).element_size()
|
||||
scaling_constant = 1100 # 50% of decode scaling constant (2200)
|
||||
working_memory = h * w * element_size * scaling_constant
|
||||
return int(working_memory)
|
||||
|
||||
@staticmethod
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
|
||||
with vae_info as vae:
|
||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
|
||||
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
|
||||
assert isinstance(vae, AutoencoderKL)
|
||||
|
||||
vae.disable_tiling()
|
||||
@@ -58,7 +68,12 @@ class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
|
||||
|
||||
vae_info = context.models.load(self.vae.vae)
|
||||
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
|
||||
assert isinstance(vae_info.model, AutoencoderKL)
|
||||
|
||||
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
|
||||
latents = self.vae_encode(
|
||||
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
|
||||
)
|
||||
|
||||
latents = latents.to("cpu")
|
||||
name = context.tensors.save(tensor=latents)
|
||||
|
||||
@@ -186,8 +186,9 @@ class ModelInstallService(ModelInstallServiceBase):
|
||||
info: AnyModelConfig = self._probe(Path(model_path), config) # type: ignore
|
||||
|
||||
if preferred_name := config.name:
|
||||
# Careful! Don't use pathlib.Path(...).with_suffix - it can will strip everything after the first dot.
|
||||
preferred_name = f"{preferred_name}{model_path.suffix}"
|
||||
if Path(model_path).is_file():
|
||||
# Careful! Don't use pathlib.Path(...).with_suffix - it can will strip everything after the first dot.
|
||||
preferred_name = f"{preferred_name}{model_path.suffix}"
|
||||
|
||||
dest_path = (
|
||||
self.app_config.models_path / info.base.value / info.type.value / (preferred_name or model_path.name)
|
||||
@@ -622,16 +623,13 @@ class ModelInstallService(ModelInstallServiceBase):
|
||||
if old_path == new_path:
|
||||
return old_path
|
||||
|
||||
if new_path.exists():
|
||||
raise FileExistsError(f"Cannot move {old_path} to {new_path}: destination already exists")
|
||||
|
||||
new_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# if path already exists then we jigger the name to make it unique
|
||||
counter: int = 1
|
||||
while new_path.exists():
|
||||
path = new_path.with_stem(new_path.stem + f"_{counter:02d}")
|
||||
if not path.exists():
|
||||
new_path = path
|
||||
counter += 1
|
||||
move(old_path, new_path)
|
||||
|
||||
return new_path
|
||||
|
||||
def _probe(self, model_path: Path, config: Optional[ModelRecordChanges] = None):
|
||||
|
||||
@@ -106,8 +106,8 @@ class KontextExtension:
|
||||
|
||||
# Track cumulative dimensions for spatial tiling
|
||||
# These track the running extent of the virtual canvas in latent space
|
||||
h = 0 # Running height extent
|
||||
w = 0 # Running width extent
|
||||
canvas_h = 0 # Running canvas height
|
||||
canvas_w = 0 # Running canvas width
|
||||
|
||||
vae_info = self._context.models.load(self._vae_field.vae)
|
||||
|
||||
@@ -131,12 +131,20 @@ class KontextExtension:
|
||||
|
||||
# Continue with VAE encoding
|
||||
# Don't sample from the distribution for reference images - use the mean (matching ComfyUI)
|
||||
with vae_info as vae:
|
||||
# Estimate working memory for encode operation (50% of decode memory requirements)
|
||||
img_h = image_tensor.shape[-2]
|
||||
img_w = image_tensor.shape[-1]
|
||||
element_size = next(vae_info.model.parameters()).element_size()
|
||||
scaling_constant = 1100 # 50% of decode scaling constant (2200)
|
||||
estimated_working_memory = int(img_h * img_w * element_size * scaling_constant)
|
||||
|
||||
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
|
||||
assert isinstance(vae, AutoEncoder)
|
||||
vae_dtype = next(iter(vae.parameters())).dtype
|
||||
image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
|
||||
# Use sample=False to get the distribution mean without noise
|
||||
kontext_latents_unpacked = vae.encode(image_tensor, sample=False)
|
||||
TorchDevice.empty_cache()
|
||||
|
||||
# Extract tensor dimensions
|
||||
batch_size, _, latent_height, latent_width = kontext_latents_unpacked.shape
|
||||
@@ -154,21 +162,33 @@ class KontextExtension:
|
||||
kontext_latents_packed = pack(kontext_latents_unpacked).to(self._device, self._dtype)
|
||||
|
||||
# Determine spatial offsets for this reference image
|
||||
# - Compare the potential new canvas dimensions if we add the image vertically vs horizontally
|
||||
# - Choose the placement that results in a more square-like canvas
|
||||
h_offset = 0
|
||||
w_offset = 0
|
||||
|
||||
if idx > 0: # First image starts at (0, 0)
|
||||
# Check which placement would result in better canvas dimensions
|
||||
# If adding to height would make the canvas taller than wide, tile horizontally
|
||||
# Otherwise, tile vertically
|
||||
if latent_height + h > latent_width + w:
|
||||
# Calculate potential canvas dimensions for each tiling option
|
||||
# Option 1: Tile vertically (below existing content)
|
||||
potential_h_vertical = canvas_h + latent_height
|
||||
|
||||
# Option 2: Tile horizontally (to the right of existing content)
|
||||
potential_w_horizontal = canvas_w + latent_width
|
||||
|
||||
# Choose arrangement that minimizes the maximum dimension
|
||||
# This keeps the canvas closer to square, optimizing attention computation
|
||||
if potential_h_vertical > potential_w_horizontal:
|
||||
# Tile horizontally (to the right of existing images)
|
||||
w_offset = w
|
||||
w_offset = canvas_w
|
||||
canvas_w = canvas_w + latent_width
|
||||
canvas_h = max(canvas_h, latent_height)
|
||||
else:
|
||||
# Tile vertically (below existing images)
|
||||
h_offset = h
|
||||
h_offset = canvas_h
|
||||
canvas_h = canvas_h + latent_height
|
||||
canvas_w = max(canvas_w, latent_width)
|
||||
else:
|
||||
# First image - just set canvas dimensions
|
||||
canvas_h = latent_height
|
||||
canvas_w = latent_width
|
||||
|
||||
# Generate IDs with both index offset and spatial offsets
|
||||
kontext_ids = generate_img_ids_with_offset(
|
||||
@@ -182,11 +202,6 @@ class KontextExtension:
|
||||
w_offset=w_offset,
|
||||
)
|
||||
|
||||
# Update cumulative dimensions
|
||||
# Track the maximum extent of the virtual canvas after placing this image
|
||||
h = max(h, latent_height + h_offset)
|
||||
w = max(w, latent_width + w_offset)
|
||||
|
||||
all_latents.append(kontext_latents_packed)
|
||||
all_ids.append(kontext_ids)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ def main():
|
||||
"/data/invokeai/models/.download_cache/https__huggingface.co_black-forest-labs_flux.1-schnell_resolve_main_flux1-schnell.safetensors/flux1-schnell.safetensors"
|
||||
)
|
||||
|
||||
with log_time("Intialize FLUX transformer on meta device"):
|
||||
with log_time("Initialize FLUX transformer on meta device"):
|
||||
# TODO(ryand): Determine if this is a schnell model or a dev model and load the appropriate config.
|
||||
p = params["flux-schnell"]
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def main():
|
||||
)
|
||||
|
||||
# inference_dtype = torch.bfloat16
|
||||
with log_time("Intialize FLUX transformer on meta device"):
|
||||
with log_time("Initialize FLUX transformer on meta device"):
|
||||
# TODO(ryand): Determine if this is a schnell model or a dev model and load the appropriate config.
|
||||
p = params["flux-schnell"]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def main():
|
||||
"""
|
||||
model_path = Path("/data/misc/text_encoder_2")
|
||||
|
||||
with log_time("Intialize T5 on meta device"):
|
||||
with log_time("Initialize T5 on meta device"):
|
||||
model_config = AutoConfig.from_pretrained(model_path)
|
||||
with accelerate.init_empty_weights():
|
||||
model = AutoModelForTextEncoding.from_config(model_config)
|
||||
|
||||
@@ -114,6 +114,9 @@
|
||||
"t2iAdapter": "T2I Adapter",
|
||||
"positivePrompt": "Positive Prompt",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"removeNegativePrompt": "Remove Negative Prompt",
|
||||
"addNegativePrompt": "Add Negative Prompt",
|
||||
"selectYourModel": "Select Your Model",
|
||||
"discordLabel": "Discord",
|
||||
"dontAskMeAgain": "Don't ask me again",
|
||||
"dontShowMeThese": "Don't show me these",
|
||||
@@ -767,6 +770,7 @@
|
||||
"allPrompts": "All Prompts",
|
||||
"cfgScale": "CFG scale",
|
||||
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
|
||||
"clipSkip": "$t(parameters.clipSkip)",
|
||||
"createdBy": "Created By",
|
||||
"generationMode": "Generation Mode",
|
||||
"guidance": "Guidance",
|
||||
@@ -1287,6 +1291,7 @@
|
||||
"remixImage": "Remix Image",
|
||||
"usePrompt": "Use Prompt",
|
||||
"useSeed": "Use Seed",
|
||||
"useClipSkip": "Use CLIP Skip",
|
||||
"width": "Width",
|
||||
"gaussianBlur": "Gaussian Blur",
|
||||
"boxBlur": "Box Blur",
|
||||
@@ -2180,7 +2185,8 @@
|
||||
"rgReferenceImagesNotSupported": "regional Reference Images not supported for selected base model",
|
||||
"rgAutoNegativeNotSupported": "Auto-Negative not supported for selected base model",
|
||||
"rgNoRegion": "no region drawn",
|
||||
"fluxFillIncompatibleWithControlLoRA": "Control LoRA is not compatible with FLUX Fill"
|
||||
"fluxFillIncompatibleWithControlLoRA": "Control LoRA is not compatible with FLUX Fill",
|
||||
"bboxHidden": "Bounding box is hidden (shift+o to toggle)"
|
||||
},
|
||||
"errors": {
|
||||
"unableToFindImage": "Unable to find image",
|
||||
@@ -2672,8 +2678,8 @@
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"items": [
|
||||
"Studio state is saved to the server, allowing you to continue your work on any device.",
|
||||
"Support for multiple reference images for FLUX Kontext (local model only)."
|
||||
"Misc QoL: Toggle Bbox visibility, highlight nodes with errors, prevent adding node fields to Builder form multiple times, CLIP Skip metadata recallable",
|
||||
"Reduced VRAM usage for multiple Kontext Ref images and VAE encoding"
|
||||
],
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchRecentReleaseVideos": "Watch Recent Release Videos",
|
||||
|
||||
@@ -128,7 +128,9 @@
|
||||
"search": "Cerca",
|
||||
"clear": "Cancella",
|
||||
"compactView": "Vista compatta",
|
||||
"fullView": "Vista completa"
|
||||
"fullView": "Vista completa",
|
||||
"removeNegativePrompt": "Rimuovi prompt negativo",
|
||||
"addNegativePrompt": "Aggiungi prompt negativo"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Dimensione dell'immagine",
|
||||
@@ -410,6 +412,10 @@
|
||||
"cancelSegmentAnything": {
|
||||
"title": "Annulla Segment Anything",
|
||||
"desc": "Annulla l'operazione Segment Anything corrente."
|
||||
},
|
||||
"fitBboxToLayers": {
|
||||
"title": "Adatta il riquadro di delimitazione ai livelli",
|
||||
"desc": "Regola automaticamente il riquadro di delimitazione della generazione per adattarlo ai livelli visibili"
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
|
||||
@@ -71,7 +71,7 @@ interface Props extends PropsWithChildren {
|
||||
* If provided, overrides in-app navigation to the model manager
|
||||
*/
|
||||
onClickGoToModelManager?: () => void;
|
||||
storagePersistThrottle?: number;
|
||||
storagePersistDebounce?: number;
|
||||
}
|
||||
|
||||
const InvokeAIUI = ({
|
||||
@@ -98,7 +98,7 @@ const InvokeAIUI = ({
|
||||
loggingOverrides,
|
||||
onClickGoToModelManager,
|
||||
whatsNew,
|
||||
storagePersistThrottle = 2000,
|
||||
storagePersistDebounce = 300,
|
||||
}: Props) => {
|
||||
const [store, setStore] = useState<ReturnType<typeof createStore> | undefined>(undefined);
|
||||
const [didRehydrate, setDidRehydrate] = useState(false);
|
||||
@@ -318,7 +318,7 @@ const InvokeAIUI = ({
|
||||
const onRehydrated = () => {
|
||||
setDidRehydrate(true);
|
||||
};
|
||||
const store = createStore({ persist: true, persistThrottle: storagePersistThrottle, onRehydrated });
|
||||
const store = createStore({ persist: true, persistDebounce: storagePersistDebounce, onRehydrated });
|
||||
setStore(store);
|
||||
$store.set(store);
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
@@ -333,7 +333,7 @@ const InvokeAIUI = ({
|
||||
window.$store = undefined;
|
||||
}
|
||||
};
|
||||
}, [storagePersistThrottle]);
|
||||
}, [storagePersistDebounce]);
|
||||
|
||||
if (!store || !didRehydrate) {
|
||||
return <Loading />;
|
||||
|
||||
@@ -184,7 +184,7 @@ const PERSISTED_KEYS = Object.values(SLICE_CONFIGS)
|
||||
.filter((sliceConfig) => !!sliceConfig.persistConfig)
|
||||
.map((sliceConfig) => sliceConfig.slice.reducerPath);
|
||||
|
||||
export const createStore = (options?: { persist?: boolean; persistThrottle?: number; onRehydrated?: () => void }) => {
|
||||
export const createStore = (options?: { persist?: boolean; persistDebounce?: number; onRehydrated?: () => void }) => {
|
||||
const store = configureStore({
|
||||
reducer: rememberedRootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
@@ -204,7 +204,7 @@ export const createStore = (options?: { persist?: boolean; persistThrottle?: num
|
||||
if (options?.persist) {
|
||||
return enhancers.prepend(
|
||||
rememberEnhancer(reduxRememberDriver, PERSISTED_KEYS, {
|
||||
persistThrottle: options?.persistThrottle ?? 2000,
|
||||
persistDebounce: options?.persistDebounce ?? 2000,
|
||||
serialize,
|
||||
unserialize,
|
||||
prefix: '',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasAlertsBboxVisibility = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isBboxHidden = useStore(canvasManager.tool.tools.bbox.$isBboxHidden);
|
||||
|
||||
if (!isBboxHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<AlertIcon />
|
||||
<AlertTitle>{t('controlLayers.warnings.bboxHidden')}</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasAlertsBboxVisibility.displayName = 'CanvasAlertsBboxVisibility';
|
||||
@@ -15,6 +15,7 @@ import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/u
|
||||
import { useCanvasFilterHotkey } from 'features/controlLayers/hooks/useCanvasFilterHotkey';
|
||||
import { useCanvasInvertMaskHotkey } from 'features/controlLayers/hooks/useCanvasInvertMaskHotkey';
|
||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||
import { useCanvasToggleBboxHotkey } from 'features/controlLayers/hooks/useCanvasToggleBboxHotkey';
|
||||
import { useCanvasToggleNonRasterLayersHotkey } from 'features/controlLayers/hooks/useCanvasToggleNonRasterLayersHotkey';
|
||||
import { useCanvasTransformHotkey } from 'features/controlLayers/hooks/useCanvasTransformHotkey';
|
||||
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
|
||||
@@ -31,6 +32,7 @@ export const CanvasToolbar = memo(() => {
|
||||
useCanvasFilterHotkey();
|
||||
useCanvasInvertMaskHotkey();
|
||||
useCanvasToggleNonRasterLayersHotkey();
|
||||
useCanvasToggleBboxHotkey();
|
||||
|
||||
return (
|
||||
<Flex w="full" gap={2} alignItems="center" px={2}>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCanvasToggleBboxHotkey = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
|
||||
const handleToggleBboxVisibility = useCallback(() => {
|
||||
canvasManager.tool.tools.bbox.toggleBboxVisibility();
|
||||
}, [canvasManager]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'toggleBbox',
|
||||
category: 'canvas',
|
||||
callback: handleToggleBboxVisibility,
|
||||
dependencies: [handleToggleBboxVisibility],
|
||||
});
|
||||
};
|
||||
@@ -66,6 +66,11 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
*/
|
||||
$aspectRatioBuffer = atom(1);
|
||||
|
||||
/**
|
||||
* Buffer to store the visibility of the bbox.
|
||||
*/
|
||||
$isBboxHidden = atom(false);
|
||||
|
||||
constructor(parent: CanvasToolModule) {
|
||||
super();
|
||||
this.id = getPrefixedId(this.type);
|
||||
@@ -191,6 +196,9 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
|
||||
// Update on busy state changes
|
||||
this.subscriptions.add(this.manager.$isBusy.listen(this.render));
|
||||
|
||||
// Listen for stage changes to update the bbox's visibility
|
||||
this.subscriptions.add(this.$isBboxHidden.listen(this.render));
|
||||
}
|
||||
|
||||
// This is a noop. The cursor is changed when the cursor enters or leaves the bbox.
|
||||
@@ -206,13 +214,15 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the bbox. The bbox is only visible when the tool is set to 'bbox'.
|
||||
* Renders the bbox.
|
||||
*/
|
||||
render = () => {
|
||||
const tool = this.manager.tool.$tool.get();
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
|
||||
|
||||
this.konva.group.visible(!this.$isBboxHidden.get());
|
||||
|
||||
// We need to reach up to the preview layer to enable/disable listening so that the bbox can be interacted with.
|
||||
// If the mangaer is busy, we disable listening so the bbox cannot be interacted with.
|
||||
this.konva.group.listening(tool === 'bbox' && !this.manager.$isBusy.get());
|
||||
@@ -478,4 +488,8 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
this.subscriptions.clear();
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
toggleBboxVisibility = () => {
|
||||
this.$isBboxHidden.set(!this.$isBboxHidden.get());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,14 +107,7 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp CLIP skip layer count to the bounds of the new model
|
||||
if (model.base === 'sdxl') {
|
||||
// We don't support user-defined CLIP skip for SDXL because it doesn't do anything useful
|
||||
state.clipSkip = 0;
|
||||
} else {
|
||||
const { maxClip } = CLIP_SKIP_MAP[model.base];
|
||||
state.clipSkip = clamp(state.clipSkip, 0, maxClip);
|
||||
}
|
||||
applyClipSkip(state, model, state.clipSkip);
|
||||
},
|
||||
vaeSelected: (state, action: PayloadAction<ParameterVAEModel | null>) => {
|
||||
// null is a valid VAE!
|
||||
@@ -170,7 +163,7 @@ const slice = createSlice({
|
||||
state.vaePrecision = action.payload;
|
||||
},
|
||||
setClipSkip: (state, action: PayloadAction<number>) => {
|
||||
state.clipSkip = action.payload;
|
||||
applyClipSkip(state, state.model, action.payload);
|
||||
},
|
||||
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldUseCpuNoise = action.payload;
|
||||
@@ -181,15 +174,6 @@ const slice = createSlice({
|
||||
negativePromptChanged: (state, action: PayloadAction<ParameterNegativePrompt>) => {
|
||||
state.negativePrompt = action.payload;
|
||||
},
|
||||
positivePrompt2Changed: (state, action: PayloadAction<string>) => {
|
||||
state.positivePrompt2 = action.payload;
|
||||
},
|
||||
negativePrompt2Changed: (state, action: PayloadAction<string>) => {
|
||||
state.negativePrompt2 = action.payload;
|
||||
},
|
||||
shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldConcatPrompts = action.payload;
|
||||
},
|
||||
refinerModelChanged: (state, action: PayloadAction<ParameterSDXLRefinerModel | null>) => {
|
||||
const result = zParamsState.shape.refinerModel.safeParse(action.payload);
|
||||
if (!result.success) {
|
||||
@@ -375,6 +359,33 @@ const slice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
const applyClipSkip = (state: { clipSkip: number }, model: ParameterModel | null, clipSkip: number) => {
|
||||
if (model === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxClip = getModelMaxClipSkip(model);
|
||||
|
||||
state.clipSkip = clamp(clipSkip, 0, maxClip);
|
||||
};
|
||||
|
||||
const hasModelClipSkip = (model: ParameterModel | null) => {
|
||||
if (model === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getModelMaxClipSkip(model) > 0;
|
||||
};
|
||||
|
||||
const getModelMaxClipSkip = (model: ParameterModel) => {
|
||||
if (model.base === 'sdxl') {
|
||||
// We don't support user-defined CLIP skip for SDXL because it doesn't do anything useful
|
||||
return 0;
|
||||
}
|
||||
|
||||
return CLIP_SKIP_MAP[model.base].maxClip;
|
||||
};
|
||||
|
||||
const resetState = (state: ParamsState): ParamsState => {
|
||||
// When a new session is requested, we need to keep the current model selections, plus dependent state
|
||||
// like VAE precision. Everything else gets reset to default.
|
||||
@@ -425,9 +436,6 @@ export const {
|
||||
shouldUseCpuNoiseChanged,
|
||||
positivePromptChanged,
|
||||
negativePromptChanged,
|
||||
positivePrompt2Changed,
|
||||
negativePrompt2Changed,
|
||||
shouldConcatPromptsChanged,
|
||||
refinerModelChanged,
|
||||
setRefinerSteps,
|
||||
setRefinerCFGScale,
|
||||
@@ -460,8 +468,7 @@ export const paramsSliceConfig: SliceConfig<typeof slice> = {
|
||||
};
|
||||
|
||||
export const selectParamsSlice = (state: RootState) => state.params;
|
||||
export const createParamsSelector = <T>(selector: Selector<ParamsState, T>) =>
|
||||
createSelector(selectParamsSlice, selector);
|
||||
const createParamsSelector = <T>(selector: Selector<ParamsState, T>) => createSelector(selectParamsSlice, selector);
|
||||
|
||||
export const selectBase = createParamsSelector((params) => params.model?.base);
|
||||
export const selectIsSDXL = createParamsSelector((params) => params.model?.base === 'sdxl');
|
||||
@@ -497,7 +504,8 @@ export const selectCFGScale = createParamsSelector((params) => params.cfgScale);
|
||||
export const selectGuidance = createParamsSelector((params) => params.guidance);
|
||||
export const selectSteps = createParamsSelector((params) => params.steps);
|
||||
export const selectCFGRescaleMultiplier = createParamsSelector((params) => params.cfgRescaleMultiplier);
|
||||
export const selectCLIPSKip = createParamsSelector((params) => params.clipSkip);
|
||||
export const selectCLIPSkip = createParamsSelector((params) => params.clipSkip);
|
||||
export const selectHasModelCLIPSkip = createParamsSelector((params) => hasModelClipSkip(params.model));
|
||||
export const selectCanvasCoherenceEdgeSize = createParamsSelector((params) => params.canvasCoherenceEdgeSize);
|
||||
export const selectCanvasCoherenceMinDenoise = createParamsSelector((params) => params.canvasCoherenceMinDenoise);
|
||||
export const selectCanvasCoherenceMode = createParamsSelector((params) => params.canvasCoherenceMode);
|
||||
@@ -518,9 +526,6 @@ export const selectModelSupportsNegativePrompt = createSelector(
|
||||
[selectIsFLUX, selectIsChatGPT4o, selectIsFluxKontext],
|
||||
(isFLUX, isChatGPT4o, isFluxKontext) => !isFLUX && !isChatGPT4o && !isFluxKontext
|
||||
);
|
||||
export const selectPositivePrompt2 = createParamsSelector((params) => params.positivePrompt2);
|
||||
export const selectNegativePrompt2 = createParamsSelector((params) => params.negativePrompt2);
|
||||
export const selectShouldConcatPrompts = createParamsSelector((params) => params.shouldConcatPrompts);
|
||||
export const selectScheduler = createParamsSelector((params) => params.scheduler);
|
||||
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
|
||||
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
|
||||
|
||||
@@ -14,9 +14,7 @@ import {
|
||||
zParameterMaskBlurMethod,
|
||||
zParameterModel,
|
||||
zParameterNegativePrompt,
|
||||
zParameterNegativeStylePromptSDXL,
|
||||
zParameterPositivePrompt,
|
||||
zParameterPositiveStylePromptSDXL,
|
||||
zParameterPrecision,
|
||||
zParameterScheduler,
|
||||
zParameterSDXLRefinerModel,
|
||||
@@ -534,9 +532,6 @@ export const zParamsState = z.object({
|
||||
shouldUseCpuNoise: z.boolean(),
|
||||
positivePrompt: zParameterPositivePrompt,
|
||||
negativePrompt: zParameterNegativePrompt,
|
||||
positivePrompt2: zParameterPositiveStylePromptSDXL,
|
||||
negativePrompt2: zParameterNegativeStylePromptSDXL,
|
||||
shouldConcatPrompts: z.boolean(),
|
||||
refinerModel: zParameterSDXLRefinerModel.nullable(),
|
||||
refinerSteps: z.number(),
|
||||
refinerCFGScale: z.number(),
|
||||
@@ -584,9 +579,6 @@ export const getInitialParamsState = (): ParamsState => ({
|
||||
shouldUseCpuNoise: true,
|
||||
positivePrompt: '',
|
||||
negativePrompt: null,
|
||||
positivePrompt2: '',
|
||||
negativePrompt2: '',
|
||||
shouldConcatPrompts: true,
|
||||
refinerModel: null,
|
||||
refinerSteps: 20,
|
||||
refinerCFGScale: 7.5,
|
||||
|
||||
@@ -40,7 +40,7 @@ export const GallerySettingsPopover = memo(() => {
|
||||
<PopoverBody>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Text fontWeight="semibold" color="base.300">
|
||||
Gallery Settings
|
||||
{t('gallery.gallerySettings')}
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallCLIPSkip } from 'features/gallery/hooks/useRecallCLIPSkip';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
export const ImageMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
|
||||
@@ -28,6 +29,7 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
const recallDimensions = useRecallDimensions(imageDTO);
|
||||
const recallCLIPSkip = useRecallCLIPSkip(imageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
@@ -55,10 +57,14 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
<MenuItem icon={<PiRulerBold />} onClick={recallDimensions.recall} isDisabled={!recallDimensions.isEnabled}>
|
||||
{t('parameters.useSize')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiRulerBold />} onClick={recallCLIPSkip.recall} isDisabled={!recallCLIPSkip.isEnabled}>
|
||||
{t('parameters.useClipSkip')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemMetadataRecallActions.displayName = 'ImageMenuItemMetadataRecallActions';
|
||||
ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.displayName =
|
||||
'ImageMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowBendUpLeftBold, PiPlantBold, PiQuotesBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActionsUpscaleTab = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label={t('parameters.recallMetadata')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem icon={<PiQuotesBold />} onClick={recallPrompts.recall} isDisabled={!recallPrompts.isEnabled}>
|
||||
{t('parameters.usePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlantBold />} onClick={recallSeed.recall} isDisabled={!recallSeed.isEnabled}>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemMetadataRecallActionsUpscaleTab.displayName = 'ImageMenuItemMetadataRecallActionsUpscaleTab';
|
||||
@@ -6,7 +6,7 @@ import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/
|
||||
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
|
||||
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
|
||||
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
|
||||
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
|
||||
import { ImageMenuItemMetadataRecallActionsCanvasGenerateTabs } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
import { ImageMenuItemNewCanvasFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImageSubMenu';
|
||||
import { ImageMenuItemNewLayerFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu';
|
||||
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
|
||||
@@ -21,6 +21,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { ImageMenuItemMetadataRecallActionsUpscaleTab } from './ImageMenuItemMetadataRecallActionsUpscaleTab';
|
||||
import { ImageMenuItemUseAsPromptTemplate } from './ImageMenuItemUseAsPromptTemplate';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
@@ -42,7 +43,8 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
|
||||
</IconMenuItemGroup>
|
||||
<MenuDivider />
|
||||
<ImageMenuItemLoadWorkflow />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemMetadataRecallActions />}
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemMetadataRecallActionsCanvasGenerateTabs />}
|
||||
{tab === 'upscaling' && <ImageMenuItemMetadataRecallActionsUpscaleTab />}
|
||||
<MenuDivider />
|
||||
<ImageMenuItemSendToUpscale />
|
||||
<ImageMenuItemUseForPromptGeneration />
|
||||
|
||||
@@ -33,8 +33,6 @@ const ImageMetadataActions = (props: Props) => {
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.GenerationMode} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.PositivePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.NegativePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.PositiveStylePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.NegativeStylePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.MainModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.VAEModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Width} />
|
||||
@@ -42,6 +40,7 @@ const ImageMetadataActions = (props: Props) => {
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Seed} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Steps} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Scheduler} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CLIPSkip} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGScale} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGRescaleMultiplier} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Guidance} />
|
||||
|
||||
@@ -31,6 +31,7 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
const { t } = useTranslation();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isCanvasOrGenerateTab = tab === 'canvas' || tab === 'generate';
|
||||
const isCanvasOrGenerateOrUpscalingTab = tab === 'canvas' || tab === 'generate' || tab === 'upscaling';
|
||||
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
|
||||
@@ -94,7 +95,7 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
onClick={recallRemix.recall}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
{isCanvasOrGenerateOrUpscalingTab && (
|
||||
<IconButton
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
@@ -105,7 +106,7 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
onClick={recallPrompts.recall}
|
||||
/>
|
||||
)}
|
||||
{isCanvasOrGenerateTab && (
|
||||
{isCanvasOrGenerateOrUpscalingTab && (
|
||||
<IconButton
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectHasModelCLIPSkip } from 'features/controlLayers/store/paramsSlice';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const ALLOWED_TABS: TabName[] = ['canvas', 'generate', 'upscaling'];
|
||||
|
||||
export const useRecallCLIPSkip = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const hasModelCLIPSkip = useAppSelector(selectHasModelCLIPSkip);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const [hasCLIPSkip, setHasCLIPSkip] = useState(false);
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageDTO.image_name);
|
||||
|
||||
useEffect(() => {
|
||||
const parse = async () => {
|
||||
try {
|
||||
await MetadataHandlers.CLIPSkip.parse(metadata, store);
|
||||
setHasCLIPSkip(true);
|
||||
} catch {
|
||||
setHasCLIPSkip(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasModelCLIPSkip) {
|
||||
setHasCLIPSkip(false);
|
||||
return;
|
||||
}
|
||||
|
||||
parse();
|
||||
}, [metadata, store, hasModelCLIPSkip]);
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ALLOWED_TABS.includes(tab)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasCLIPSkip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [hasCLIPSkip, isLoading, metadata, tab]);
|
||||
|
||||
const recall = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.CLIPSkip, store });
|
||||
}, [metadata, isEnabled, store]);
|
||||
|
||||
return {
|
||||
recall,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { useClearStylePresetWithToast } from './useClearStylePresetWithToast';
|
||||
|
||||
const ALLOWED_TABS: TabName[] = ['canvas', 'generate', 'upscaling'];
|
||||
|
||||
export const useRecallPrompts = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
@@ -19,12 +22,7 @@ export const useRecallPrompts = (imageDTO: ImageDTO) => {
|
||||
const parse = async () => {
|
||||
try {
|
||||
const result = await MetadataUtils.hasMetadataByHandlers({
|
||||
handlers: [
|
||||
MetadataHandlers.PositivePrompt,
|
||||
MetadataHandlers.NegativePrompt,
|
||||
MetadataHandlers.PositiveStylePrompt,
|
||||
MetadataHandlers.NegativeStylePrompt,
|
||||
],
|
||||
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
|
||||
metadata,
|
||||
store,
|
||||
require: 'some',
|
||||
@@ -43,7 +41,7 @@ export const useRecallPrompts = (imageDTO: ImageDTO) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
if (!ALLOWED_TABS.includes(tab)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const ALLOWED_TABS: TabName[] = ['canvas', 'generate', 'upscaling'];
|
||||
|
||||
export const useRecallSeed = (imageDTO: ImageDTO) => {
|
||||
const store = useAppStore();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
@@ -30,7 +33,7 @@ export const useRecallSeed = (imageDTO: ImageDTO) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tab !== 'canvas' && tab !== 'generate') {
|
||||
if (!ALLOWED_TABS.includes(tab)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,13 @@ import { bboxHeightChanged, bboxWidthChanged, canvasMetadataRecalled } from 'fea
|
||||
import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/lorasSlice';
|
||||
import {
|
||||
heightChanged,
|
||||
negativePrompt2Changed,
|
||||
negativePromptChanged,
|
||||
positivePrompt2Changed,
|
||||
positivePromptChanged,
|
||||
refinerModelChanged,
|
||||
selectBase,
|
||||
setCfgRescaleMultiplier,
|
||||
setCfgScale,
|
||||
setClipSkip,
|
||||
setGuidance,
|
||||
setImg2imgStrength,
|
||||
setRefinerCFGScale,
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
setSeamlessYAxis,
|
||||
setSeed,
|
||||
setSteps,
|
||||
shouldConcatPromptsChanged,
|
||||
vaeSelected,
|
||||
widthChanged,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
@@ -44,12 +42,12 @@ import { modelSelected } from 'features/parameters/store/actions';
|
||||
import type {
|
||||
ParameterCFGRescaleMultiplier,
|
||||
ParameterCFGScale,
|
||||
ParameterCLIPSkip,
|
||||
ParameterGuidance,
|
||||
ParameterHeight,
|
||||
ParameterModel,
|
||||
ParameterNegativePrompt,
|
||||
ParameterPositivePrompt,
|
||||
ParameterPositiveStylePromptSDXL,
|
||||
ParameterScheduler,
|
||||
ParameterSDXLRefinerModel,
|
||||
ParameterSDXLRefinerNegativeAestheticScore,
|
||||
@@ -67,12 +65,11 @@ import {
|
||||
zLoRAWeight,
|
||||
zParameterCFGRescaleMultiplier,
|
||||
zParameterCFGScale,
|
||||
zParameterCLIPSkip,
|
||||
zParameterGuidance,
|
||||
zParameterImageDimension,
|
||||
zParameterNegativePrompt,
|
||||
zParameterNegativeStylePromptSDXL,
|
||||
zParameterPositivePrompt,
|
||||
zParameterPositiveStylePromptSDXL,
|
||||
zParameterScheduler,
|
||||
zParameterSDXLRefinerNegativeAestheticScore,
|
||||
zParameterSDXLRefinerPositiveAestheticScore,
|
||||
@@ -289,46 +286,6 @@ const NegativePrompt: SingleMetadataHandler<ParameterNegativePrompt> = {
|
||||
};
|
||||
//#endregion Negative Prompt
|
||||
|
||||
//#region SDXL Positive Style Prompt
|
||||
const PositiveStylePrompt: SingleMetadataHandler<ParameterPositiveStylePromptSDXL> = {
|
||||
[SingleMetadataKey]: true,
|
||||
type: 'PositiveStylePrompt',
|
||||
parse: (metadata, _store) => {
|
||||
const raw = getProperty(metadata, 'positive_style_prompt');
|
||||
const parsed = zParameterPositiveStylePromptSDXL.parse(raw);
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
recall: (value, store) => {
|
||||
store.dispatch(positivePrompt2Changed(value));
|
||||
},
|
||||
i18nKey: 'sdxl.posStylePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterPositiveStylePromptSDXL>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
};
|
||||
//#endregion SDXL Positive Style Prompt
|
||||
|
||||
//#region SDXL Negative Style Prompt
|
||||
const NegativeStylePrompt: SingleMetadataHandler<ParameterPositiveStylePromptSDXL> = {
|
||||
[SingleMetadataKey]: true,
|
||||
type: 'NegativeStylePrompt',
|
||||
parse: (metadata, _store) => {
|
||||
const raw = getProperty(metadata, 'negative_style_prompt');
|
||||
const parsed = zParameterNegativeStylePromptSDXL.parse(raw);
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
recall: (value, store) => {
|
||||
store.dispatch(negativePrompt2Changed(value));
|
||||
},
|
||||
i18nKey: 'sdxl.negStylePrompt',
|
||||
LabelComponent: MetadataLabel,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterPositiveStylePromptSDXL>) => (
|
||||
<MetadataPrimitiveValue value={value} />
|
||||
),
|
||||
};
|
||||
//#endregion SDXL Negative Style Prompt
|
||||
|
||||
//#region CFG Scale
|
||||
const CFGScale: SingleMetadataHandler<ParameterCFGScale> = {
|
||||
[SingleMetadataKey]: true,
|
||||
@@ -367,6 +324,24 @@ const CFGRescaleMultiplier: SingleMetadataHandler<ParameterCFGRescaleMultiplier>
|
||||
};
|
||||
//#endregion CFG Rescale Multiplier
|
||||
|
||||
//#region CLIP Skip
|
||||
const CLIPSkip: SingleMetadataHandler<ParameterCLIPSkip> = {
|
||||
[SingleMetadataKey]: true,
|
||||
type: 'CLIPSkip',
|
||||
parse: (metadata, _store) => {
|
||||
const raw = getProperty(metadata, 'clip_skip');
|
||||
const parsed = zParameterCLIPSkip.parse(raw);
|
||||
return Promise.resolve(parsed);
|
||||
},
|
||||
recall: (value, store) => {
|
||||
store.dispatch(setClipSkip(value));
|
||||
},
|
||||
i18nKey: 'metadata.clipSkip',
|
||||
LabelComponent: MetadataLabel,
|
||||
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterCLIPSkip>) => <MetadataPrimitiveValue value={value} />,
|
||||
};
|
||||
//#endregion CLIP Skip
|
||||
|
||||
//#region Guidance
|
||||
const Guidance: SingleMetadataHandler<ParameterGuidance> = {
|
||||
[SingleMetadataKey]: true,
|
||||
@@ -927,10 +902,9 @@ export const MetadataHandlers = {
|
||||
GenerationMode,
|
||||
PositivePrompt,
|
||||
NegativePrompt,
|
||||
PositiveStylePrompt,
|
||||
NegativeStylePrompt,
|
||||
CFGScale,
|
||||
CFGRescaleMultiplier,
|
||||
CLIPSkip,
|
||||
Guidance,
|
||||
Scheduler,
|
||||
Width,
|
||||
@@ -1052,26 +1026,6 @@ const recallByHandlers = async (arg: {
|
||||
}
|
||||
}
|
||||
|
||||
// We may need to update the prompt concat flag based on the recalled prompts
|
||||
const positivePrompt = recalled.get(MetadataHandlers.PositivePrompt);
|
||||
const negativePrompt = recalled.get(MetadataHandlers.NegativePrompt);
|
||||
const positiveStylePrompt = recalled.get(MetadataHandlers.PositiveStylePrompt);
|
||||
const negativeStylePrompt = recalled.get(MetadataHandlers.NegativeStylePrompt);
|
||||
|
||||
// The values will be undefined if the handler was not recalled
|
||||
if (
|
||||
positivePrompt !== undefined ||
|
||||
negativePrompt !== undefined ||
|
||||
positiveStylePrompt !== undefined ||
|
||||
negativeStylePrompt !== undefined
|
||||
) {
|
||||
const concat =
|
||||
(Boolean(positiveStylePrompt) && positiveStylePrompt === positivePrompt) ||
|
||||
(Boolean(negativeStylePrompt) && negativeStylePrompt === negativePrompt);
|
||||
|
||||
store.dispatch(shouldConcatPromptsChanged(concat));
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
if (recalled.size > 0) {
|
||||
toast({
|
||||
@@ -1094,12 +1048,7 @@ const recallByHandlers = async (arg: {
|
||||
const recallPrompts = async (metadata: unknown, store: AppStore) => {
|
||||
const recalled = await recallByHandlers({
|
||||
metadata,
|
||||
handlers: [
|
||||
MetadataHandlers.PositivePrompt,
|
||||
MetadataHandlers.NegativePrompt,
|
||||
MetadataHandlers.PositiveStylePrompt,
|
||||
MetadataHandlers.NegativeStylePrompt,
|
||||
],
|
||||
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
|
||||
store,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import InvocationNodeTitle from 'features/nodes/components/flow/nodes/common/InvocationNodeTitle';
|
||||
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
|
||||
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
|
||||
import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon';
|
||||
import { useNodeHasErrors } from 'features/nodes/hooks/useNodeIsInvalid';
|
||||
import { memo } from 'react';
|
||||
@@ -35,7 +35,7 @@ const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
|
||||
<Flex sx={sx} data-is-open={isOpen} data-is-invalid={isInvalid}>
|
||||
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
|
||||
<InvocationNodeClassificationIcon nodeId={nodeId} />
|
||||
<NodeTitle nodeId={nodeId} />
|
||||
<InvocationNodeTitle nodeId={nodeId} />
|
||||
<Flex alignItems="center">
|
||||
<InvocationNodeStatusIndicator nodeId={nodeId} />
|
||||
<InvocationNodeInfoIcon nodeId={nodeId} />
|
||||
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
|
||||
export const InputFieldAddToFormRoot = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const addToRoot = useAddNodeFieldToRoot(nodeId, fieldName);
|
||||
const { isAddedToRoot, addNodeFieldToRoot } = useAddNodeFieldToRoot(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -21,7 +21,8 @@ export const InputFieldAddToFormRoot = memo(({ nodeId, fieldName }: Props) => {
|
||||
icon={<PiPlusBold />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
onClick={addToRoot}
|
||||
onClick={addNodeFieldToRoot}
|
||||
isDisabled={isAddedToRoot}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -30,12 +30,12 @@ const labelSx: SystemStyleObject = {
|
||||
_hover: {
|
||||
fontWeight: 'semibold !important',
|
||||
},
|
||||
'&[data-is-invalid="true"]': {
|
||||
color: 'error.300',
|
||||
},
|
||||
'&[data-is-added-to-form="true"]': {
|
||||
color: 'blue.300',
|
||||
},
|
||||
'&[data-is-invalid="true"]': {
|
||||
color: 'error.300',
|
||||
},
|
||||
'&[data-is-disabled="true"]': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
@@ -106,7 +106,7 @@ export const InputFieldTitle = memo((props: Props) => {
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
{editable.value}
|
||||
{isAddedToForm && <Icon as={PiLinkBold} color="blue.200" ml={1} />}
|
||||
{isAddedToForm && <Icon as={PiLinkBold} color={isInvalid ? 'error.300' : 'blue.200'} ml={1} />}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Input, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEditable } from 'common/hooks/useEditable';
|
||||
import { useBatchGroupColorToken } from 'features/nodes/hooks/useBatchGroupColorToken';
|
||||
import { useBatchGroupId } from 'features/nodes/hooks/useBatchGroupId';
|
||||
import { useNodeHasErrors } from 'features/nodes/hooks/useNodeIsInvalid';
|
||||
import { useNodeTemplateTitleSafe } from 'features/nodes/hooks/useNodeTemplateTitleSafe';
|
||||
import { useNodeUserTitleSafe } from 'features/nodes/hooks/useNodeUserTitleSafe';
|
||||
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
@@ -10,13 +12,21 @@ import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const labelSx: SystemStyleObject = {
|
||||
fontWeight: 'semibold',
|
||||
'&[data-is-invalid="true"]': {
|
||||
color: 'error.300',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
const InvocationNodeTitle = ({ nodeId, title }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isInvalid = useNodeHasErrors();
|
||||
const label = useNodeUserTitleSafe();
|
||||
const batchGroupId = useBatchGroupId(nodeId);
|
||||
const batchGroupColorToken = useBatchGroupColorToken(batchGroupId);
|
||||
@@ -53,10 +63,11 @@ const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
{!editable.isEditing && (
|
||||
<Text
|
||||
className={NO_FIT_ON_DOUBLE_CLICK_CLASS}
|
||||
fontWeight="semibold"
|
||||
color={batchGroupColorToken}
|
||||
onDoubleClick={editable.startEditing}
|
||||
sx={labelSx}
|
||||
noOfLines={1}
|
||||
color={batchGroupColorToken}
|
||||
data-is-invalid={isInvalid}
|
||||
onDoubleClick={editable.startEditing}
|
||||
>
|
||||
{titleWithBatchGroupId}
|
||||
</Text>
|
||||
@@ -73,4 +84,4 @@ const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeTitle);
|
||||
export default memo(InvocationNodeTitle);
|
||||
@@ -5,6 +5,7 @@ import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/I
|
||||
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
|
||||
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { useNodeHasErrors } from 'features/nodes/hooks/useNodeIsInvalid';
|
||||
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
|
||||
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME, NO_FIT_ON_DOUBLE_CLICK_CLASS, NODE_WIDTH } from 'features/nodes/types/constants';
|
||||
@@ -29,6 +30,8 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
const isLocked = useIsWorkflowEditorLocked();
|
||||
const isInvalid = useNodeHasErrors();
|
||||
const hasError = isMissingTemplate || isInvalid;
|
||||
|
||||
const executionState = useNodeExecutionState(nodeId);
|
||||
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
|
||||
@@ -74,7 +77,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
data-is-editor-locked={isLocked}
|
||||
data-is-selected={selected}
|
||||
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
|
||||
data-status={isMissingTemplate ? 'error' : needsUpdate ? 'warning' : undefined}
|
||||
data-status={hasError ? 'error' : needsUpdate ? 'warning' : undefined}
|
||||
>
|
||||
<Box sx={shadowsSx} />
|
||||
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
|
||||
|
||||
@@ -2,15 +2,20 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
|
||||
import { formElementAdded } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFormRootElementId } from 'features/nodes/store/selectors';
|
||||
import { buildSelectWorkflowFormNodeExists, selectFormRootElementId } from 'features/nodes/store/selectors';
|
||||
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const rootElementId = useAppSelector(selectFormRootElementId);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const field = useInputFieldInstance(fieldName);
|
||||
const selectWorkflowFormNodeExists = useMemo(
|
||||
() => buildSelectWorkflowFormNodeExists(nodeId, fieldName),
|
||||
[nodeId, fieldName]
|
||||
);
|
||||
const isAddedToRoot = useAppSelector(selectWorkflowFormNodeExists);
|
||||
|
||||
const addNodeFieldToRoot = useCallback(() => {
|
||||
const element = buildNodeFieldElement(nodeId, fieldName, fieldTemplate.type);
|
||||
@@ -23,5 +28,5 @@ export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
);
|
||||
}, [nodeId, fieldName, fieldTemplate.type, dispatch, rootElementId, field.value]);
|
||||
|
||||
return addNodeFieldToRoot;
|
||||
return { isAddedToRoot, addNodeFieldToRoot };
|
||||
};
|
||||
|
||||
@@ -103,3 +103,7 @@ export const selectWorkflowFormNodeFieldFieldIdentifiersDeduped = createSelector
|
||||
);
|
||||
|
||||
export const buildSelectElement = (id: string) => createNodesSelector((workflow) => workflow.form?.elements[id]);
|
||||
export const buildSelectWorkflowFormNodeExists = (nodeId: string, fieldName: string) =>
|
||||
createSelector(selectWorkflowFormNodeFieldFieldIdentifiersDeduped, (identifiers) =>
|
||||
identifiers.some((identifier) => identifier.nodeId === nodeId && identifier.fieldName === fieldName)
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
|
||||
type: 'sdxl_compel_prompt',
|
||||
id: getPrefixedId('neg_cond'),
|
||||
prompt: prompts.negative,
|
||||
style: prompts.negativeStyle,
|
||||
style: prompts.negative,
|
||||
});
|
||||
modelLoader = g.addNode({
|
||||
type: 'sdxl_model_loader',
|
||||
@@ -130,21 +130,14 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
|
||||
g.addEdge(modelLoader, 'unet', tiledMultidiffusion, 'unet');
|
||||
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'prompt');
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'style');
|
||||
|
||||
addSDXLLoRAs(state, g, tiledMultidiffusion, modelLoader, null, posCond, negCond);
|
||||
|
||||
g.upsertMetadata({
|
||||
negative_prompt: prompts.negative,
|
||||
negative_style_prompt: prompts.negativeStyle,
|
||||
});
|
||||
|
||||
if (prompts.useMainPromptsForStyle) {
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'style');
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_style_prompt');
|
||||
} else {
|
||||
posCond.style = prompts.positiveStyle;
|
||||
g.upsertMetadata({ positive_style_prompt: prompts.positiveStyle });
|
||||
}
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
|
||||
} else {
|
||||
const prompts = selectPresetModifiedPrompts(state);
|
||||
|
||||
@@ -179,6 +172,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
|
||||
g.upsertMetadata({
|
||||
negative_prompt: prompts.negative,
|
||||
});
|
||||
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
|
||||
}
|
||||
|
||||
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
|
||||
|
||||
@@ -156,17 +156,24 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise<GraphBuilder
|
||||
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0);
|
||||
|
||||
if (validFLUXKontextConfigs.length > 0) {
|
||||
const kontextConcatenator = g.addNode({
|
||||
id: getPrefixedId('flux_kontext_image_prep'),
|
||||
type: 'flux_kontext_image_prep',
|
||||
images: validFLUXKontextConfigs.map(({ config }) => zImageField.parse(config.image)),
|
||||
const fluxKontextCollect = g.addNode({
|
||||
type: 'collect',
|
||||
id: getPrefixedId('flux_kontext_collect'),
|
||||
});
|
||||
const kontextConditioning = g.addNode({
|
||||
type: 'flux_kontext',
|
||||
id: getPrefixedId('flux_kontext'),
|
||||
});
|
||||
g.addEdge(kontextConcatenator, 'image', kontextConditioning, 'image');
|
||||
g.addEdge(kontextConditioning, 'kontext_cond', denoise, 'kontext_conditioning');
|
||||
for (const { config } of validFLUXKontextConfigs) {
|
||||
const kontextImagePrep = g.addNode({
|
||||
id: getPrefixedId('flux_kontext_image_prep'),
|
||||
type: 'flux_kontext_image_prep',
|
||||
images: [zImageField.parse(config.image)],
|
||||
});
|
||||
const kontextConditioning = g.addNode({
|
||||
type: 'flux_kontext',
|
||||
id: getPrefixedId('flux_kontext'),
|
||||
});
|
||||
g.addEdge(kontextImagePrep, 'image', kontextConditioning, 'image');
|
||||
g.addEdge(kontextConditioning, 'kontext_cond', fluxKontextCollect, 'item');
|
||||
}
|
||||
g.addEdge(fluxKontextCollect, 'collection', denoise, 'kontext_conditioning');
|
||||
|
||||
g.upsertMetadata({ ref_images: [validFLUXKontextConfigs] }, 'merge');
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export const buildSDXLGraph = async (arg: GraphBuilderArg): Promise<GraphBuilder
|
||||
type: 'sdxl_compel_prompt',
|
||||
id: getPrefixedId('neg_cond'),
|
||||
prompt: prompts.negative,
|
||||
style: prompts.useMainPromptsForStyle ? prompts.negative : prompts.negativeStyle,
|
||||
style: prompts.negative,
|
||||
});
|
||||
const negCondCollect = g.addNode({
|
||||
type: 'collect',
|
||||
@@ -123,6 +123,8 @@ export const buildSDXLGraph = async (arg: GraphBuilderArg): Promise<GraphBuilder
|
||||
g.addEdge(modelLoader, 'clip2', negCond, 'clip2');
|
||||
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'prompt');
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'style');
|
||||
|
||||
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');
|
||||
g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning');
|
||||
|
||||
@@ -141,20 +143,11 @@ export const buildSDXLGraph = async (arg: GraphBuilderArg): Promise<GraphBuilder
|
||||
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
|
||||
scheduler,
|
||||
negative_prompt: prompts.negative,
|
||||
negative_style_prompt: prompts.useMainPromptsForStyle ? prompts.negative : prompts.negativeStyle,
|
||||
vae: vae ?? undefined,
|
||||
});
|
||||
g.addEdgeToMetadata(seed, 'value', 'seed');
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
|
||||
|
||||
if (prompts.useMainPromptsForStyle) {
|
||||
g.addEdge(positivePrompt, 'value', posCond, 'style');
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_style_prompt');
|
||||
} else {
|
||||
posCond.style = prompts.positiveStyle;
|
||||
g.upsertMetadata({ positive_style_prompt: prompts.positiveStyle });
|
||||
}
|
||||
|
||||
const seamless = addSeamless(state, g, denoise, modelLoader, vaeLoader);
|
||||
|
||||
addSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond);
|
||||
|
||||
@@ -85,7 +85,7 @@ export const selectPresetModifiedPrompts = createSelector(
|
||||
selectListStylePresetsRequestState,
|
||||
(params, stylePresetSlice, listStylePresetsRequestState) => {
|
||||
const negativePrompt = params.negativePrompt ?? '';
|
||||
const { positivePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = params;
|
||||
const { positivePrompt } = params;
|
||||
const { activeStylePresetId } = stylePresetSlice;
|
||||
|
||||
if (activeStylePresetId) {
|
||||
@@ -107,9 +107,6 @@ export const selectPresetModifiedPrompts = createSelector(
|
||||
return {
|
||||
positive: presetModifiedPositivePrompt,
|
||||
negative: presetModifiedNegativePrompt,
|
||||
positiveStyle: positivePrompt2,
|
||||
negativeStyle: negativePrompt2,
|
||||
useMainPromptsForStyle: shouldConcatPrompts,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -117,9 +114,6 @@ export const selectPresetModifiedPrompts = createSelector(
|
||||
return {
|
||||
positive: positivePrompt,
|
||||
negative: negativePrompt,
|
||||
positiveStyle: positivePrompt2,
|
||||
negativeStyle: negativePrompt2,
|
||||
useMainPromptsForStyle: shouldConcatPrompts,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectCLIPSKip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCLIPSkip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice';
|
||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||
import { selectCLIPSkipConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ParamClipSkip = () => {
|
||||
const clipSkip = useAppSelector(selectCLIPSKip);
|
||||
const clipSkip = useAppSelector(selectCLIPSkip);
|
||||
const config = useAppSelector(selectCLIPSkipConfig);
|
||||
const model = useAppSelector(selectModel);
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusMinusBold } from 'react-icons/pi';
|
||||
|
||||
export const NegativePromptToggleButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -18,8 +20,8 @@ export const NegativePromptToggleButton = memo(() => {
|
||||
}, [dispatch, hasNegativePrompt]);
|
||||
|
||||
const label = useMemo(
|
||||
() => (hasNegativePrompt ? 'Remove Negative Prompt' : 'Add Negative Prompt'),
|
||||
[hasNegativePrompt]
|
||||
() => (hasNegativePrompt ? t('common.removeNegativePrompt') : t('common.addNegativePrompt')),
|
||||
[hasNegativePrompt, t]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
|
||||
import {
|
||||
positivePromptChanged,
|
||||
selectBase,
|
||||
selectModelSupportsNegativePrompt,
|
||||
selectPositivePrompt,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
@@ -21,7 +20,6 @@ import { PromptExpansionOverlay } from 'features/prompt/PromptExpansion/PromptEx
|
||||
import { promptExpansionApi } from 'features/prompt/PromptExpansion/state';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
import { usePrompt } from 'features/prompt/usePrompt';
|
||||
import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton';
|
||||
import {
|
||||
selectStylePresetActivePresetId,
|
||||
selectStylePresetViewMode,
|
||||
@@ -42,7 +40,6 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
|
||||
export const ParamPositivePrompt = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prompt = useAppSelector(selectPositivePrompt);
|
||||
const baseModel = useAppSelector(selectBase);
|
||||
const viewMode = useAppSelector(selectStylePresetViewMode);
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
|
||||
@@ -118,7 +115,6 @@ export const ParamPositivePrompt = memo(() => {
|
||||
<PromptOverlayButtonWrapper>
|
||||
<Flex flexDir="column" gap={2} justifyContent="flex-start" alignItems="center">
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
{baseModel === 'sdxl' && <SDXLConcatButton />}
|
||||
<ShowDynamicPromptsPreviewButton />
|
||||
{modelSupportsNegativePrompt && <NegativePromptToggleButton />}
|
||||
</Flex>
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { RefImageList } from 'features/controlLayers/components/RefImage/RefImageList';
|
||||
import {
|
||||
createParamsSelector,
|
||||
selectHasNegativePrompt,
|
||||
selectModelSupportsNegativePrompt,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectHasNegativePrompt, selectModelSupportsNegativePrompt } from 'features/controlLayers/store/paramsSlice';
|
||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
||||
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
|
||||
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectWithStylePrompts = createParamsSelector((params) => {
|
||||
const isSDXL = params.model?.base === 'sdxl';
|
||||
const shouldConcatPrompts = params.shouldConcatPrompts;
|
||||
return isSDXL && !shouldConcatPrompts;
|
||||
});
|
||||
|
||||
export const Prompts = memo(() => {
|
||||
const withStylePrompts = useAppSelector(selectWithStylePrompts);
|
||||
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
|
||||
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
|
||||
return (
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<ParamPositivePrompt />
|
||||
{withStylePrompts && <ParamSDXLPositiveStylePrompt />}
|
||||
{modelSupportsNegativePrompt && hasNegativePrompt && <ParamNegativePrompt />}
|
||||
{withStylePrompts && <ParamSDXLNegativeStylePrompt />}
|
||||
<RefImageList />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,32 +1,17 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
createParamsSelector,
|
||||
selectHasNegativePrompt,
|
||||
selectModelSupportsNegativePrompt,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectHasNegativePrompt, selectModelSupportsNegativePrompt } from 'features/controlLayers/store/paramsSlice';
|
||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
||||
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
|
||||
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectWithStylePrompts = createParamsSelector((params) => {
|
||||
const isSDXL = params.model?.base === 'sdxl';
|
||||
const shouldConcatPrompts = params.shouldConcatPrompts;
|
||||
return isSDXL && !shouldConcatPrompts;
|
||||
});
|
||||
|
||||
export const UpscalePrompts = memo(() => {
|
||||
const withStylePrompts = useAppSelector(selectWithStylePrompts);
|
||||
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
|
||||
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
|
||||
return (
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<ParamPositivePrompt />
|
||||
{withStylePrompts && <ParamSDXLPositiveStylePrompt />}
|
||||
{modelSupportsNegativePrompt && hasNegativePrompt && <ParamNegativePrompt />}
|
||||
{withStylePrompts && <ParamSDXLNegativeStylePrompt />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,16 +33,6 @@ export const [zParameterNegativePrompt, isParameterNegativePrompt] = buildParame
|
||||
export type ParameterNegativePrompt = z.infer<typeof zParameterNegativePrompt>;
|
||||
// #endregion
|
||||
|
||||
// #region Positive style prompt (SDXL)
|
||||
export const [zParameterPositiveStylePromptSDXL, isParameterPositiveStylePromptSDXL] = buildParameter(z.string());
|
||||
export type ParameterPositiveStylePromptSDXL = z.infer<typeof zParameterPositiveStylePromptSDXL>;
|
||||
// #endregion
|
||||
|
||||
// #region Positive style prompt (SDXL)
|
||||
export const [zParameterNegativeStylePromptSDXL, isParameterNegativeStylePromptSDXL] = buildParameter(z.string());
|
||||
export type ParameterNegativeStylePromptSDXL = z.infer<typeof zParameterNegativeStylePromptSDXL>;
|
||||
// #endregion
|
||||
|
||||
// #region Steps
|
||||
export const [zParameterSteps, isParameterSteps] = buildParameter(z.number().int().min(1));
|
||||
export type ParameterSteps = z.infer<typeof zParameterSteps>;
|
||||
@@ -203,3 +193,8 @@ export type ParameterCanvasCoherenceMode = z.infer<typeof zParameterCanvasCohere
|
||||
export const [zLoRAWeight, isParameterLoRAWeight] = buildParameter(z.number());
|
||||
export type ParameterLoRAWeight = z.infer<typeof zLoRAWeight>;
|
||||
// #endregion
|
||||
|
||||
// #region CLIP skip
|
||||
export const [zParameterCLIPSkip, isParameterCLIPSkip] = buildParameter(z.number().int().min(0));
|
||||
export type ParameterCLIPSkip = z.infer<typeof zParameterCLIPSkip>;
|
||||
// #endregion
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
|
||||
import { negativePrompt2Changed, selectNegativePrompt2 } from 'features/controlLayers/store/paramsSlice';
|
||||
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
import { usePrompt } from 'features/prompt/usePrompt';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
|
||||
trackWidth: false,
|
||||
trackHeight: true,
|
||||
};
|
||||
|
||||
export const ParamSDXLNegativeStylePrompt = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prompt = useAppSelector(selectNegativePrompt2);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
usePersistedTextAreaSize('negative_style_prompt', textareaRef, persistOptions);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleChange = useCallback(
|
||||
(v: string) => {
|
||||
dispatch(negativePrompt2Changed(v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
|
||||
prompt,
|
||||
textareaRef: textareaRef,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||
<Box pos="relative">
|
||||
<Textarea
|
||||
className="negative-style-prompt-textarea"
|
||||
name="prompt"
|
||||
ref={textareaRef}
|
||||
value={prompt}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
fontSize="sm"
|
||||
variant="darkFilled"
|
||||
minH={24}
|
||||
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||
paddingInlineEnd={10}
|
||||
paddingInlineStart={3}
|
||||
paddingTop={0}
|
||||
paddingBottom={3}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
<PromptLabel label={t('sdxl.negStylePrompt')} />
|
||||
</Box>
|
||||
</PromptPopover>
|
||||
);
|
||||
});
|
||||
|
||||
ParamSDXLNegativeStylePrompt.displayName = 'ParamSDXLNegativeStylePrompt';
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
|
||||
import { positivePrompt2Changed, selectPositivePrompt2 } from 'features/controlLayers/store/paramsSlice';
|
||||
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
import { usePrompt } from 'features/prompt/usePrompt';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
|
||||
trackWidth: false,
|
||||
trackHeight: true,
|
||||
};
|
||||
|
||||
export const ParamSDXLPositiveStylePrompt = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prompt = useAppSelector(selectPositivePrompt2);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
usePersistedTextAreaSize('positive_style_prompt', textareaRef, persistOptions);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleChange = useCallback(
|
||||
(v: string) => {
|
||||
dispatch(positivePrompt2Changed(v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
|
||||
prompt,
|
||||
textareaRef: textareaRef,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||
<Box pos="relative">
|
||||
<Textarea
|
||||
className="positive-style-prompt-textarea"
|
||||
name="prompt"
|
||||
ref={textareaRef}
|
||||
value={prompt}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
fontSize="sm"
|
||||
variant="darkFilled"
|
||||
minH={24}
|
||||
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||
paddingInlineEnd={10}
|
||||
paddingInlineStart={3}
|
||||
paddingTop={0}
|
||||
paddingBottom={3}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
<PromptLabel label={t('sdxl.posStylePrompt')} />
|
||||
</Box>
|
||||
</PromptPopover>
|
||||
);
|
||||
});
|
||||
|
||||
ParamSDXLPositiveStylePrompt.displayName = 'ParamSDXLPositiveStylePrompt';
|
||||
@@ -1,37 +0,0 @@
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectShouldConcatPrompts, shouldConcatPromptsChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi';
|
||||
|
||||
export const SDXLConcatButton = memo(() => {
|
||||
const shouldConcatPrompts = useAppSelector(selectShouldConcatPrompts);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleShouldConcatPromptChange = useCallback(() => {
|
||||
dispatch(shouldConcatPromptsChanged(!shouldConcatPrompts));
|
||||
}, [dispatch, shouldConcatPrompts]);
|
||||
|
||||
const label = useMemo(
|
||||
() => (shouldConcatPrompts ? t('sdxl.concatPromptStyle') : t('sdxl.freePromptStyle')),
|
||||
[shouldConcatPrompts, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
onClick={handleShouldConcatPromptChange}
|
||||
icon={shouldConcatPrompts ? <PiLinkSimpleBold size={14} /> : <PiLinkSimpleBreakBold size={14} />}
|
||||
variant="promptOverlay"
|
||||
fontSize={12}
|
||||
px={0.5}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
SDXLConcatButton.displayName = 'SDXLConcatButton';
|
||||
@@ -126,6 +126,7 @@ export const useHotkeyData = (): HotkeysData => {
|
||||
addHotkey('canvas', 'cancelSegmentAnything', ['esc']);
|
||||
addHotkey('canvas', 'toggleNonRasterLayers', ['shift+h']);
|
||||
addHotkey('canvas', 'fitBboxToMasks', ['shift+b']);
|
||||
addHotkey('canvas', 'toggleBbox', ['shift+o']);
|
||||
|
||||
// Workflows
|
||||
addHotkey('workflows', 'addNode', ['shift+a', 'space']);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import type { IDockviewPanelProps, IGridviewPanelProps } from 'dockview';
|
||||
import { selectSystemShouldEnableHighlightFocusedRegions } from 'features/system/store/systemSlice';
|
||||
import type { PanelParameters } from 'features/ui/layouts/auto-layout-context';
|
||||
import type { DockviewPanelParameters, GridviewPanelParameters } from 'features/ui/layouts/auto-layout-context';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
@@ -30,8 +30,8 @@ const sx: SystemStyleObject = {
|
||||
export const AutoLayoutPanelContainer = memo(
|
||||
(
|
||||
props:
|
||||
| PropsWithChildren<IDockviewPanelProps<PanelParameters>>
|
||||
| PropsWithChildren<IGridviewPanelProps<PanelParameters>>
|
||||
| PropsWithChildren<IDockviewPanelProps<DockviewPanelParameters>>
|
||||
| PropsWithChildren<IGridviewPanelProps<GridviewPanelParameters>>
|
||||
) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const shouldHighlightFocusedRegions = useAppSelector(selectSystemShouldEnableHighlightFocusedRegions);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasAlertsBboxVisibility } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsBboxVisibility';
|
||||
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
import { CanvasAlertsSaveAllImagesToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery';
|
||||
@@ -92,6 +93,7 @@ export const CanvasWorkspacePanel = memo(() => {
|
||||
<CanvasAlertsSelectedEntityStatus />
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsInvocationProgress />
|
||||
<CanvasAlertsBboxVisibility />
|
||||
</Flex>
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||
<Menu>
|
||||
|
||||
@@ -3,10 +3,12 @@ import { setFocusedRegion } from 'common/hooks/focus';
|
||||
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
|
||||
import type { IDockviewPanelHeaderProps } from 'dockview';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PanelParameters } from './auto-layout-context';
|
||||
import type { DockviewPanelParameters } from './auto-layout-context';
|
||||
|
||||
export const DockviewTab = memo((props: IDockviewPanelHeaderProps<PanelParameters>) => {
|
||||
export const DockviewTab = memo((props: IDockviewPanelHeaderProps<DockviewPanelParameters>) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const setActive = useCallback(() => {
|
||||
if (!props.api.isActive) {
|
||||
@@ -23,7 +25,7 @@ export const DockviewTab = memo((props: IDockviewPanelHeaderProps<PanelParameter
|
||||
return (
|
||||
<Flex ref={ref} alignItems="center" h="full" onPointerDown={onPointerDown}>
|
||||
<Text userSelect="none" px={4}>
|
||||
{props.api.title ?? props.api.id}
|
||||
{t(props.params.i18nKey)}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,13 @@ import type { IDockviewPanelHeaderProps } from 'dockview';
|
||||
import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
|
||||
|
||||
import type { PanelParameters } from './auto-layout-context';
|
||||
import type { DockviewPanelParameters } from './auto-layout-context';
|
||||
|
||||
export const DockviewTabCanvasViewer = memo((props: IDockviewPanelHeaderProps<PanelParameters>) => {
|
||||
export const DockviewTabCanvasViewer = memo((props: IDockviewPanelHeaderProps<DockviewPanelParameters>) => {
|
||||
const { t } = useTranslation();
|
||||
const isGenerationInProgress = useIsGenerationInProgress();
|
||||
const currentQueueItemDestination = useCurrentQueueItemDestination();
|
||||
|
||||
@@ -29,7 +31,7 @@ export const DockviewTabCanvasViewer = memo((props: IDockviewPanelHeaderProps<Pa
|
||||
return (
|
||||
<Flex ref={ref} position="relative" alignItems="center" h="full" onPointerDown={onPointerDown}>
|
||||
<Text userSelect="none" px={4}>
|
||||
{props.api.title ?? props.api.id}
|
||||
{t(props.params.i18nKey)}
|
||||
</Text>
|
||||
{currentQueueItemDestination === 'canvas' && isGenerationInProgress && (
|
||||
<ProgressBar position="absolute" bottom={0} left={0} right={0} h={1} borderRadius="none" />
|
||||
|
||||
@@ -7,11 +7,13 @@ import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagin
|
||||
import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
|
||||
|
||||
import type { PanelParameters } from './auto-layout-context';
|
||||
import type { DockviewPanelParameters } from './auto-layout-context';
|
||||
|
||||
export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps<PanelParameters>) => {
|
||||
export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps<DockviewPanelParameters>) => {
|
||||
const { t } = useTranslation();
|
||||
const isGenerationInProgress = useIsGenerationInProgress();
|
||||
const canvasSessionId = useAppSelector(selectCanvasSessionId);
|
||||
const currentQueueItemDestination = useCurrentQueueItemDestination();
|
||||
@@ -32,7 +34,7 @@ export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps
|
||||
return (
|
||||
<Flex ref={ref} position="relative" alignItems="center" h="full" onPointerDown={onPointerDown}>
|
||||
<Text userSelect="none" px={4}>
|
||||
{props.api.title ?? props.api.id}
|
||||
{t(props.params.i18nKey)}
|
||||
</Text>
|
||||
{currentQueueItemDestination === canvasSessionId && isGenerationInProgress && (
|
||||
<ProgressBar position="absolute" bottom={0} left={0} right={0} h={1} borderRadius="none" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { IDockviewPanelHeaderProps } from 'dockview';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IconType } from 'react-icons';
|
||||
import {
|
||||
PiBoundingBoxBold,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
PiTextAaBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
import type { DockviewPanelParameters } from './auto-layout-context';
|
||||
|
||||
const TAB_ICONS: Record<TabName, IconType> = {
|
||||
generate: PiTextAaBold,
|
||||
canvas: PiBoundingBoxBold,
|
||||
@@ -25,7 +28,8 @@ const TAB_ICONS: Record<TabName, IconType> = {
|
||||
queue: PiQueueBold,
|
||||
};
|
||||
|
||||
export const DockviewTabLaunchpad = memo((props: IDockviewPanelHeaderProps) => {
|
||||
export const DockviewTabLaunchpad = memo((props: IDockviewPanelHeaderProps<DockviewPanelParameters>) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
|
||||
@@ -44,7 +48,7 @@ export const DockviewTabLaunchpad = memo((props: IDockviewPanelHeaderProps) => {
|
||||
return (
|
||||
<Flex ref={ref} alignItems="center" h="full" px={4} gap={3} onPointerDown={onPointerDown}>
|
||||
<Icon as={TAB_ICONS[activeTab]} color="invokeYellow.300" boxSize={5} />
|
||||
<Text userSelect="none">{props.api.title ?? props.api.id}</Text>
|
||||
<Text userSelect="none">{t(props.params.i18nKey)}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
|
||||
import type { IDockviewPanelHeaderProps } from 'dockview';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
|
||||
|
||||
import type { PanelParameters } from './auto-layout-context';
|
||||
import type { DockviewPanelParameters } from './auto-layout-context';
|
||||
|
||||
export const DockviewTabProgress = memo((props: IDockviewPanelHeaderProps<PanelParameters>) => {
|
||||
export const DockviewTabProgress = memo((props: IDockviewPanelHeaderProps<DockviewPanelParameters>) => {
|
||||
const { t } = useTranslation();
|
||||
const isGenerationInProgress = useIsGenerationInProgress();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -27,7 +29,7 @@ export const DockviewTabProgress = memo((props: IDockviewPanelHeaderProps<PanelP
|
||||
return (
|
||||
<Flex ref={ref} position="relative" alignItems="center" h="full" onPointerDown={onPointerDown}>
|
||||
<Text userSelect="none" px={4}>
|
||||
{props.api.title ?? props.api.id}
|
||||
{t(props.params.i18nKey)}
|
||||
</Text>
|
||||
{isGenerationInProgress && (
|
||||
<ProgressBar position="absolute" bottom={0} left={0} right={0} h={1} borderRadius="none" />
|
||||
|
||||
@@ -4,12 +4,14 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
|
||||
import { ModelPicker } from 'features/parameters/components/ModelPicker';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdMoneyOff } from 'react-icons/md';
|
||||
import { useMainModels } from 'services/api/hooks/modelsByType';
|
||||
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
|
||||
import { type AnyModelConfig, isCheckpointMainModelConfig } from 'services/api/types';
|
||||
|
||||
export const InitialStateMainModelPicker = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const [modelConfigs] = useMainModels();
|
||||
const selectedModelConfig = useSelectedModelConfig();
|
||||
@@ -31,7 +33,7 @@ export const InitialStateMainModelPicker = memo(() => {
|
||||
return (
|
||||
<FormControl orientation="vertical" alignItems="unset">
|
||||
<FormLabel display="flex" fontSize="md" gap={2}>
|
||||
Select your Model{' '}
|
||||
{t('common.selectYourModel')}{' '}
|
||||
{isFluxDevSelected && (
|
||||
<InformationalPopover feature="fluxDevLicense" hideDisable={true}>
|
||||
<Flex justifyContent="flex-start">
|
||||
|
||||
@@ -27,15 +27,30 @@ export const useAutoLayoutContext = () => {
|
||||
return value;
|
||||
};
|
||||
|
||||
export type PanelParameters = {
|
||||
export type DockviewPanelParameters = {
|
||||
tab: TabName;
|
||||
focusRegion: FocusRegionName;
|
||||
i18nKey: string;
|
||||
};
|
||||
|
||||
export type GridviewPanelParameters = {
|
||||
tab: TabName;
|
||||
focusRegion: FocusRegionName;
|
||||
};
|
||||
|
||||
export type AutoLayoutGridviewComponents = Record<string, FunctionComponent<IGridviewPanelProps<PanelParameters>>>;
|
||||
export type AutoLayoutDockviewComponents = Record<string, FunctionComponent<IDockviewPanelProps<PanelParameters>>>;
|
||||
export type RootLayoutGridviewComponents = Record<string, FunctionComponent<IGridviewPanelProps<PanelParameters>>>;
|
||||
type PanelProps = IDockviewPanelProps<PanelParameters> | IGridviewPanelProps<PanelParameters>;
|
||||
export type AutoLayoutGridviewComponents = Record<
|
||||
string,
|
||||
FunctionComponent<IGridviewPanelProps<GridviewPanelParameters>>
|
||||
>;
|
||||
export type AutoLayoutDockviewComponents = Record<
|
||||
string,
|
||||
FunctionComponent<IDockviewPanelProps<DockviewPanelParameters>>
|
||||
>;
|
||||
export type RootLayoutGridviewComponents = Record<
|
||||
string,
|
||||
FunctionComponent<IGridviewPanelProps<GridviewPanelParameters>>
|
||||
>;
|
||||
type PanelProps = IDockviewPanelProps<DockviewPanelParameters> | IGridviewPanelProps<GridviewPanelParameters>;
|
||||
|
||||
export const withPanelContainer = (Component: FunctionComponent) =>
|
||||
/* eslint-disable-next-line react/display-name */
|
||||
|
||||
@@ -9,7 +9,8 @@ import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightP
|
||||
import type {
|
||||
AutoLayoutDockviewComponents,
|
||||
AutoLayoutGridviewComponents,
|
||||
PanelParameters,
|
||||
DockviewPanelParameters,
|
||||
GridviewPanelParameters,
|
||||
RootLayoutGridviewComponents,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
|
||||
@@ -63,7 +64,7 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
|
||||
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
const launchpad = api.addPanel<DockviewPanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: t('ui.panels.launchpad'),
|
||||
@@ -71,10 +72,11 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
i18nKey: 'ui.panels.launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: t('ui.panels.canvas'),
|
||||
@@ -82,6 +84,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'canvas',
|
||||
i18nKey: 'ui.panels.canvas',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -89,7 +92,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: t('ui.panels.imageViewer'),
|
||||
@@ -97,6 +100,7 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
i18nKey: 'ui.panels.imageViewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -145,7 +149,7 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
const gallery = api.addPanel<GridviewPanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
@@ -156,7 +160,7 @@ const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<GridviewPanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -170,7 +174,7 @@ const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<GridviewPanelParameters>({
|
||||
id: LAYERS_PANEL_ID,
|
||||
component: LAYERS_PANEL_ID,
|
||||
minimumHeight: LAYERS_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -215,7 +219,7 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<GridviewPanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
|
||||
@@ -8,7 +8,8 @@ import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightP
|
||||
import type {
|
||||
AutoLayoutDockviewComponents,
|
||||
AutoLayoutGridviewComponents,
|
||||
PanelParameters,
|
||||
DockviewPanelParameters,
|
||||
GridviewPanelParameters,
|
||||
RootLayoutGridviewComponents,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
|
||||
@@ -57,7 +58,7 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
const launchpad = api.addPanel<DockviewPanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: t('ui.panels.launchpad'),
|
||||
@@ -65,10 +66,11 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
i18nKey: 'ui.panels.launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: t('ui.panels.imageViewer'),
|
||||
@@ -76,6 +78,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
i18nKey: 'ui.panels.imageViewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -123,7 +126,7 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
const gallery = api.addPanel<GridviewPanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
@@ -134,7 +137,7 @@ const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<GridviewPanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -179,7 +182,7 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<GridviewPanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
@@ -218,13 +221,13 @@ const rootPanelComponents: RootLayoutGridviewComponents = {
|
||||
|
||||
const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'root', api, () => {
|
||||
const main = api.addPanel<PanelParameters>({
|
||||
const main = api.addPanel<GridviewPanelParameters>({
|
||||
id: MAIN_PANEL_ID,
|
||||
component: MAIN_PANEL_ID,
|
||||
priority: LayoutPriority.High,
|
||||
});
|
||||
|
||||
const left = api.addPanel<PanelParameters>({
|
||||
const left = api.addPanel<GridviewPanelParameters>({
|
||||
id: LEFT_PANEL_ID,
|
||||
component: LEFT_PANEL_ID,
|
||||
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
|
||||
@@ -234,7 +237,7 @@ const initializeRootPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
const right = api.addPanel<PanelParameters>({
|
||||
const right = api.addPanel<GridviewPanelParameters>({
|
||||
id: RIGHT_PANEL_ID,
|
||||
component: RIGHT_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
|
||||
@@ -8,7 +8,8 @@ import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightP
|
||||
import type {
|
||||
AutoLayoutDockviewComponents,
|
||||
AutoLayoutGridviewComponents,
|
||||
PanelParameters,
|
||||
DockviewPanelParameters,
|
||||
GridviewPanelParameters,
|
||||
RootLayoutGridviewComponents,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
|
||||
@@ -57,7 +58,7 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
const launchpad = api.addPanel<DockviewPanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: t('ui.panels.launchpad'),
|
||||
@@ -65,10 +66,11 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
i18nKey: 'ui.panels.launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: t('ui.panels.imageViewer'),
|
||||
@@ -76,6 +78,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
i18nKey: 'ui.panels.imageViewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -121,7 +124,7 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
const gallery = api.addPanel<GridviewPanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
@@ -132,7 +135,7 @@ const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<GridviewPanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -177,7 +180,7 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<GridviewPanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
|
||||
@@ -10,7 +10,8 @@ import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightP
|
||||
import type {
|
||||
AutoLayoutDockviewComponents,
|
||||
AutoLayoutGridviewComponents,
|
||||
PanelParameters,
|
||||
DockviewPanelParameters,
|
||||
GridviewPanelParameters,
|
||||
RootLayoutGridviewComponents,
|
||||
} from 'features/ui/layouts/auto-layout-context';
|
||||
import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context';
|
||||
@@ -60,7 +61,7 @@ const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
|
||||
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'main', api, () => {
|
||||
const launchpad = api.addPanel<PanelParameters>({
|
||||
const launchpad = api.addPanel<DockviewPanelParameters>({
|
||||
id: LAUNCHPAD_PANEL_ID,
|
||||
component: LAUNCHPAD_PANEL_ID,
|
||||
title: t('ui.panels.launchpad'),
|
||||
@@ -68,10 +69,11 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'launchpad',
|
||||
i18nKey: 'ui.panels.launchpad',
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: WORKSPACE_PANEL_ID,
|
||||
component: WORKSPACE_PANEL_ID,
|
||||
title: t('ui.panels.workflowEditor'),
|
||||
@@ -79,6 +81,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'workflows',
|
||||
i18nKey: 'ui.panels.workflowEditor',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -86,7 +89,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<DockviewPanelParameters>({
|
||||
id: VIEWER_PANEL_ID,
|
||||
component: VIEWER_PANEL_ID,
|
||||
title: t('ui.panels.imageViewer'),
|
||||
@@ -94,6 +97,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
params: {
|
||||
tab,
|
||||
focusRegion: 'viewer',
|
||||
i18nKey: 'ui.panels.imageViewer',
|
||||
},
|
||||
position: {
|
||||
direction: 'within',
|
||||
@@ -141,7 +145,7 @@ const rightPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'right', api, () => {
|
||||
const gallery = api.addPanel<PanelParameters>({
|
||||
const gallery = api.addPanel<GridviewPanelParameters>({
|
||||
id: GALLERY_PANEL_ID,
|
||||
component: GALLERY_PANEL_ID,
|
||||
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
@@ -152,7 +156,7 @@ const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = api.addPanel<PanelParameters>({
|
||||
const boards = api.addPanel<GridviewPanelParameters>({
|
||||
id: BOARDS_PANEL_ID,
|
||||
component: BOARDS_PANEL_ID,
|
||||
minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX,
|
||||
@@ -197,7 +201,7 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
|
||||
|
||||
const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
navigationApi.registerContainer(tab, 'left', api, () => {
|
||||
api.addPanel<PanelParameters>({
|
||||
api.addPanel<GridviewPanelParameters>({
|
||||
id: SETTINGS_PANEL_ID,
|
||||
component: SETTINGS_PANEL_ID,
|
||||
params: {
|
||||
|
||||
@@ -105,6 +105,10 @@ export const uiSliceConfig: SliceConfig<typeof slice> = {
|
||||
state.activeTab = 'canvas';
|
||||
state._version = 3;
|
||||
}
|
||||
if (state._version === 3) {
|
||||
state.panels = {};
|
||||
state._version = 4;
|
||||
}
|
||||
return zUIState.parse(state);
|
||||
},
|
||||
persistDenylist: ['shouldShowImageDetails'],
|
||||
|
||||
@@ -13,7 +13,7 @@ const zSerializable = z.any().refine(isPlainObject);
|
||||
export type Serializable = z.infer<typeof zSerializable>;
|
||||
|
||||
export const zUIState = z.object({
|
||||
_version: z.literal(3),
|
||||
_version: z.literal(4),
|
||||
activeTab: zTabName,
|
||||
shouldShowImageDetails: z.boolean(),
|
||||
shouldShowProgressInViewer: z.boolean(),
|
||||
@@ -26,7 +26,7 @@ export const zUIState = z.object({
|
||||
});
|
||||
export type UIState = z.infer<typeof zUIState>;
|
||||
export const getInitialUIState = (): UIState => ({
|
||||
_version: 3 as const,
|
||||
_version: 4 as const,
|
||||
activeTab: 'generate' as const,
|
||||
shouldShowImageDetails: false,
|
||||
shouldShowProgressInViewer: true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
selectAutoSwitch,
|
||||
selectGalleryView,
|
||||
selectGetImageNamesQueryArgs,
|
||||
selectListBoardsQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
@@ -76,6 +77,14 @@ export const buildOnInvocationComplete = (
|
||||
}
|
||||
dispatch(boardsApi.util.upsertQueryEntries(entries));
|
||||
|
||||
dispatch(
|
||||
boardsApi.util.updateQueryData('listAllBoards', selectListBoardsQueryArgs(getState()), (draft) => {
|
||||
for (const board of draft) {
|
||||
board.image_count = board.image_count + (boardTotalAdditions[board.board_id] ?? 0);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Optimistic update and cache invalidation for image names queries that match this image's board and categories.
|
||||
* - Optimistic update for the cache that does not have a search term (we cannot derive the correct insertion
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "6.3.0rc2"
|
||||
__version__ = "6.4.0rc1"
|
||||
|
||||
@@ -46,7 +46,7 @@ def fetch_commits_between_tags(
|
||||
commit_info: list[CommitInfo] = []
|
||||
headers = {"Authorization": f"token {token}"} if token else None
|
||||
|
||||
# Get the total number of pages w/ an intial request - a bit hacky but it works...
|
||||
# Get the total number of pages w/ an initial request - a bit hacky but it works...
|
||||
response = requests.get(
|
||||
f"https://api.github.com/repos/{org_name}/{repo_name}/compare/{from_ref}...{to_ref}?page=1&per_page=100",
|
||||
headers=headers,
|
||||
|
||||
Reference in New Issue
Block a user