mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 04:58:04 -05:00
Merge branch 'main' into llava
This commit is contained in:
@@ -27,7 +27,7 @@ class FluxFillOutput(BaseInvocationOutput):
|
||||
|
||||
@invocation(
|
||||
"flux_fill",
|
||||
title="FLUX Fill",
|
||||
title="FLUX Fill Conditioning",
|
||||
tags=["inpaint"],
|
||||
category="inpaint",
|
||||
version="1.0.0",
|
||||
|
||||
@@ -28,10 +28,10 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
|
||||
|
||||
@invocation(
|
||||
"flux_lora_loader",
|
||||
title="FLUX LoRA",
|
||||
title="Apply LoRA - FLUX",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.2.0",
|
||||
version="1.2.1",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
@@ -107,10 +107,10 @@ class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
|
||||
@invocation(
|
||||
"flux_lora_collection_loader",
|
||||
title="FLUX LoRA Collection Loader",
|
||||
title="Apply LoRA Collection - FLUX",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.3.0",
|
||||
version="1.3.1",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
|
||||
@@ -1051,7 +1051,7 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
tags=["image", "mask", "id"],
|
||||
category="image",
|
||||
version="1.0.0",
|
||||
classification=Classification.Internal,
|
||||
classification=Classification.Deprecated,
|
||||
)
|
||||
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
"""Handles Canvas V2 image output masking and cropping"""
|
||||
@@ -1089,6 +1089,112 @@ class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
return ImageOutput.build(image_dto)
|
||||
|
||||
|
||||
@invocation(
|
||||
"expand_mask_with_fade", title="Expand Mask with Fade", tags=["image", "mask"], category="image", version="1.0.0"
|
||||
)
|
||||
class ExpandMaskWithFadeInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
"""Expands a mask with a fade effect. The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
|
||||
The mask is thresholded to create a binary mask, and then a distance transform is applied to create a fade effect.
|
||||
The fade size is specified in pixels, and the mask is expanded by that amount. The result is a mask with a smooth transition from black to white.
|
||||
"""
|
||||
|
||||
mask: ImageField = InputField(description="The mask to expand")
|
||||
threshold: int = InputField(default=0, ge=0, le=255, description="The threshold for the binary mask (0-255)")
|
||||
fade_size_px: int = InputField(default=32, ge=0, description="The size of the fade in pixels")
|
||||
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
pil_mask = context.images.get_pil(self.mask.image_name, mode="L")
|
||||
|
||||
np_mask = numpy.array(pil_mask)
|
||||
|
||||
# Threshold the mask to create a binary mask - 0 for black, 255 for white
|
||||
# If we don't threshold we can get some weird artifacts
|
||||
np_mask = numpy.where(np_mask > self.threshold, 255, 0).astype(numpy.uint8)
|
||||
|
||||
# Create a mask for the black region (1 where black, 0 otherwise)
|
||||
black_mask = (np_mask == 0).astype(numpy.uint8)
|
||||
|
||||
# Invert the black region
|
||||
bg_mask = 1 - black_mask
|
||||
|
||||
# Create a distance transform of the inverted mask
|
||||
dist = cv2.distanceTransform(bg_mask, cv2.DIST_L2, 5)
|
||||
|
||||
# Normalize distances so that pixels <fade_size_px become a linear gradient (0 to 1)
|
||||
d_norm = numpy.clip(dist / self.fade_size_px, 0, 1)
|
||||
|
||||
# Control points: x values (normalized distance) and corresponding fade pct y values.
|
||||
|
||||
# There are some magic numbers here that are used to create a smooth transition:
|
||||
# - The first point is at 0% of fade size from edge of mask (meaning the edge of the mask), and is 0% fade (black)
|
||||
# - The second point is 1px from the edge of the mask and also has 0% fade, effectively expanding the mask
|
||||
# by 1px. This fixes an issue where artifacts can occur at the edge of the mask
|
||||
# - The third point is at 20% of the fade size from the edge of the mask and has 20% fade
|
||||
# - The fourth point is at 80% of the fade size from the edge of the mask and has 90% fade
|
||||
# - The last point is at 100% of the fade size from the edge of the mask and has 100% fade (white)
|
||||
|
||||
# x values: 0 = mask edge, 1 = fade_size_px from edge
|
||||
x_control = numpy.array([0.0, 1.0 / self.fade_size_px, 0.2, 0.8, 1.0])
|
||||
# y values: 0 = black, 1 = white
|
||||
y_control = numpy.array([0.0, 0.0, 0.2, 0.9, 1.0])
|
||||
|
||||
# Fit a cubic polynomial that smoothly passes through the control points
|
||||
coeffs = numpy.polyfit(x_control, y_control, 3)
|
||||
poly = numpy.poly1d(coeffs)
|
||||
|
||||
# Evaluate and clip the smooth mapping
|
||||
feather = numpy.clip(poly(d_norm), 0, 1)
|
||||
|
||||
# Build final image.
|
||||
np_result = numpy.where(black_mask == 1, 0, (feather * 255).astype(numpy.uint8))
|
||||
|
||||
# Convert back to PIL, grayscale
|
||||
pil_result = Image.fromarray(np_result.astype(numpy.uint8), mode="L")
|
||||
|
||||
image_dto = context.images.save(image=pil_result, image_category=ImageCategory.MASK)
|
||||
|
||||
return ImageOutput.build(image_dto)
|
||||
|
||||
|
||||
@invocation(
|
||||
"apply_mask_to_image",
|
||||
title="Apply Mask to Image",
|
||||
tags=["image", "mask", "blend"],
|
||||
category="image",
|
||||
version="1.0.0",
|
||||
)
|
||||
class ApplyMaskToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
"""
|
||||
Extracts a region from a generated image using a mask and blends it seamlessly onto a source image.
|
||||
The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
|
||||
"""
|
||||
|
||||
image: ImageField = InputField(description="The image from which to extract the masked region")
|
||||
mask: ImageField = InputField(description="The mask defining the region (black=keep, white=discard)")
|
||||
invert_mask: bool = InputField(
|
||||
default=False,
|
||||
description="Whether to invert the mask before applying it",
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
# Load images
|
||||
image = context.images.get_pil(self.image.image_name, mode="RGBA")
|
||||
mask = context.images.get_pil(self.mask.image_name, mode="L")
|
||||
|
||||
if self.invert_mask:
|
||||
# Invert the mask if requested
|
||||
mask = ImageOps.invert(mask.copy())
|
||||
|
||||
# Combine the mask as the alpha channel of the image
|
||||
r, g, b, _ = image.split() # Split the image into RGB and alpha channels
|
||||
result_image = Image.merge("RGBA", (r, g, b, mask)) # Use the mask as the new alpha channel
|
||||
|
||||
# Save the resulting image
|
||||
image_dto = context.images.save(image=result_image)
|
||||
|
||||
return ImageOutput.build(image_dto)
|
||||
|
||||
|
||||
@invocation(
|
||||
"img_noise",
|
||||
title="Add Image Noise",
|
||||
|
||||
@@ -67,7 +67,7 @@ class AlphaMaskToTensorInvocation(BaseInvocation):
|
||||
invert: bool = InputField(default=False, description="Whether to invert the mask.")
|
||||
|
||||
def invoke(self, context: InvocationContext) -> MaskOutput:
|
||||
image = context.images.get_pil(self.image.image_name)
|
||||
image = context.images.get_pil(self.image.image_name, mode="RGBA")
|
||||
mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
|
||||
if self.invert:
|
||||
mask[0] = torch.tensor(np.array(image)[:, :, 3] == 0, dtype=torch.bool)
|
||||
|
||||
@@ -181,7 +181,7 @@ class LoRALoaderOutput(BaseInvocationOutput):
|
||||
clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
|
||||
|
||||
|
||||
@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.3")
|
||||
@invocation("lora_loader", title="Apply LoRA - SD1.5", tags=["model"], category="model", version="1.0.4")
|
||||
class LoRALoaderInvocation(BaseInvocation):
|
||||
"""Apply selected lora to unet and text_encoder."""
|
||||
|
||||
@@ -244,7 +244,7 @@ class LoRASelectorOutput(BaseInvocationOutput):
|
||||
lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA")
|
||||
|
||||
|
||||
@invocation("lora_selector", title="LoRA Model - SD1.5", tags=["model"], category="model", version="1.0.2")
|
||||
@invocation("lora_selector", title="Select LoRA", tags=["model"], category="model", version="1.0.3")
|
||||
class LoRASelectorInvocation(BaseInvocation):
|
||||
"""Selects a LoRA model and weight."""
|
||||
|
||||
@@ -258,7 +258,7 @@ class LoRASelectorInvocation(BaseInvocation):
|
||||
|
||||
|
||||
@invocation(
|
||||
"lora_collection_loader", title="LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.1"
|
||||
"lora_collection_loader", title="Apply LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.2"
|
||||
)
|
||||
class LoRACollectionLoader(BaseInvocation):
|
||||
"""Applies a collection of LoRAs to the provided UNet and CLIP models."""
|
||||
@@ -322,10 +322,10 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput):
|
||||
|
||||
@invocation(
|
||||
"sdxl_lora_loader",
|
||||
title="LoRA Model - SDXL",
|
||||
title="Apply LoRA - SDXL",
|
||||
tags=["lora", "model"],
|
||||
category="model",
|
||||
version="1.0.4",
|
||||
version="1.0.5",
|
||||
)
|
||||
class SDXLLoRALoaderInvocation(BaseInvocation):
|
||||
"""Apply selected lora to unet and text_encoder."""
|
||||
@@ -402,10 +402,10 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
|
||||
|
||||
@invocation(
|
||||
"sdxl_lora_collection_loader",
|
||||
title="LoRA Collection - SDXL",
|
||||
title="Apply LoRA Collection - SDXL",
|
||||
tags=["model"],
|
||||
category="model",
|
||||
version="1.1.1",
|
||||
version="1.1.2",
|
||||
)
|
||||
class SDXLLoRACollectionLoader(BaseInvocation):
|
||||
"""Applies a collection of SDXL LoRAs to the provided UNet and CLIP models."""
|
||||
|
||||
@@ -563,14 +563,20 @@ class CheckpointProbeBase(ProbeBase):
|
||||
|
||||
if base_type == BaseModelType.Flux:
|
||||
in_channels = state_dict["img_in.weight"].shape[1]
|
||||
if in_channels == 64:
|
||||
return ModelVariantType.Normal
|
||||
elif in_channels == 384:
|
||||
|
||||
# FLUX Model variant types are distinguished by input channels:
|
||||
# - Unquantized Dev and Schnell have in_channels=64
|
||||
# - BNB-NF4 Dev and Schnell have in_channels=1
|
||||
# - FLUX Fill has in_channels=384
|
||||
# - Unsure of quantized FLUX Fill models
|
||||
# - Unsure of GGUF-quantized models
|
||||
if in_channels == 384:
|
||||
# This is a FLUX Fill model. FLUX Fill needs special handling throughout the application. The variant
|
||||
# type is used to determine whether to use the fill model or the base model.
|
||||
return ModelVariantType.Inpaint
|
||||
else:
|
||||
raise InvalidModelConfigException(
|
||||
f"Unexpected in_channels (in_channels={in_channels}) for FLUX model at {self.model_path}."
|
||||
)
|
||||
# Fall back on "normal" variant type for all other FLUX models.
|
||||
return ModelVariantType.Normal
|
||||
|
||||
in_channels = state_dict["model.diffusion_model.input_blocks.0.0.weight"].shape[1]
|
||||
if in_channels == 9:
|
||||
|
||||
@@ -114,7 +114,9 @@
|
||||
"layout": "Layout",
|
||||
"board": "Ordner",
|
||||
"combinatorial": "Kombinatorisch",
|
||||
"saveChanges": "Änderungen speichern"
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"error_withCount_one": "{{count}} Fehler",
|
||||
"error_withCount_other": "{{count}} Fehler"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Bildgröße",
|
||||
@@ -764,10 +766,10 @@
|
||||
"layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert",
|
||||
"sentToCanvas": "An Leinwand gesendet",
|
||||
"problemDeletingWorkflow": "Problem beim Löschen des Arbeitsablaufs",
|
||||
"uploadFailedInvalidUploadDesc_withCount_one": "Es darf maximal 1 PNG- oder JPEG-Bild sein.",
|
||||
"uploadFailedInvalidUploadDesc_withCount_other": "Es dürfen maximal {{count}} PNG- oder JPEG-Bilder sein.",
|
||||
"uploadFailedInvalidUploadDesc_withCount_one": "Darf maximal 1 PNG-, JPEG- oder WEBP-Bild sein.",
|
||||
"uploadFailedInvalidUploadDesc_withCount_other": "Dürfen maximal {{count}} PNG-, JPEG- oder WEBP-Bild sein.",
|
||||
"problemRetrievingWorkflow": "Problem beim Abrufen des Arbeitsablaufs",
|
||||
"uploadFailedInvalidUploadDesc": "Müssen PNG- oder JPEG-Bilder sein.",
|
||||
"uploadFailedInvalidUploadDesc": "Müssen PNG-, JPEG- oder WEBP-Bilder sein.",
|
||||
"pasteSuccess": "Eingefügt in {{destination}}",
|
||||
"pasteFailed": "Einfügen fehlgeschlagen",
|
||||
"unableToCopy": "Kopieren nicht möglich",
|
||||
@@ -1259,7 +1261,6 @@
|
||||
"nodePack": "Knoten-Pack",
|
||||
"loadWorkflow": "Lade Workflow",
|
||||
"snapToGrid": "Am Gitternetz einrasten",
|
||||
"unknownOutput": "Unbekannte Ausgabe: {{name}}",
|
||||
"updateNode": "Knoten updaten",
|
||||
"edge": "Rand / Kante",
|
||||
"sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht",
|
||||
@@ -1325,7 +1326,9 @@
|
||||
"description": "Beschreibung",
|
||||
"loadWorkflowDesc": "Arbeitsablauf laden?",
|
||||
"loadWorkflowDesc2": "Ihr aktueller Arbeitsablauf enthält nicht gespeicherte Änderungen.",
|
||||
"loadingTemplates": "Lade {{name}}"
|
||||
"loadingTemplates": "Lade {{name}}",
|
||||
"missingSourceOrTargetHandle": "Fehlender Quell- oder Zielgriff",
|
||||
"missingSourceOrTargetNode": "Fehlender Quell- oder Zielknoten"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Korrektur für hohe Auflösungen",
|
||||
|
||||
@@ -194,7 +194,9 @@
|
||||
"combinatorial": "Combinatorial",
|
||||
"layout": "Layout",
|
||||
"row": "Row",
|
||||
"column": "Column"
|
||||
"column": "Column",
|
||||
"value": "Value",
|
||||
"label": "Label"
|
||||
},
|
||||
"hrf": {
|
||||
"hrf": "High Resolution Fix",
|
||||
@@ -1014,7 +1016,10 @@
|
||||
"unknownNodeType": "Unknown node type",
|
||||
"unknownTemplate": "Unknown Template",
|
||||
"unknownInput": "Unknown input: {{name}}",
|
||||
"unknownOutput": "Unknown output: {{name}}",
|
||||
"missingField_withName": "Missing field \"{{name}}\"",
|
||||
"unexpectedField_withName": "Unexpected field \"{{name}}\"",
|
||||
"unknownField_withName": "Unknown field \"{{name}}\"",
|
||||
"unknownFieldEditWorkflowToFix_withName": "Workflow contains an unknown field \"{{name}}\".\nEdit the workflow to fix the issue.",
|
||||
"updateNode": "Update Node",
|
||||
"updateApp": "Update App",
|
||||
"loadingTemplates": "Loading {{name}}",
|
||||
@@ -1299,7 +1304,8 @@
|
||||
"problemDeletingWorkflow": "Problem Deleting Workflow",
|
||||
"unableToCopy": "Unable to Copy",
|
||||
"unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ",
|
||||
"unableToCopyDesc_theseSteps": "these steps"
|
||||
"unableToCopyDesc_theseSteps": "these steps",
|
||||
"fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks."
|
||||
},
|
||||
"popovers": {
|
||||
"clipSkip": {
|
||||
@@ -1740,6 +1746,7 @@
|
||||
"openLibrary": "Open Library",
|
||||
"workflowThumbnail": "Workflow Thumbnail",
|
||||
"saveChanges": "Save Changes",
|
||||
"emptyStringPlaceholder": "<empty string>",
|
||||
"builder": {
|
||||
"deleteAllElements": "Delete All Form Elements",
|
||||
"resetAllNodeFields": "Reset All Node Fields",
|
||||
@@ -1764,6 +1771,9 @@
|
||||
"singleLine": "Single Line",
|
||||
"multiLine": "Multi Line",
|
||||
"slider": "Slider",
|
||||
"dropdown": "Dropdown",
|
||||
"addOption": "Add Option",
|
||||
"resetOptions": "Reset Options",
|
||||
"both": "Both",
|
||||
"emptyRootPlaceholderViewMode": "Click Edit to start building a form for this workflow.",
|
||||
"emptyRootPlaceholderEditMode": "Drag a form element or node field here to get started.",
|
||||
@@ -1950,7 +1960,8 @@
|
||||
"rgNegativePromptNotSupported": "Negative Prompt not supported for selected base model",
|
||||
"rgReferenceImagesNotSupported": "regional Reference Images not supported for selected base model",
|
||||
"rgAutoNegativeNotSupported": "Auto-Negative not supported for selected base model",
|
||||
"rgNoRegion": "no region drawn"
|
||||
"rgNoRegion": "no region drawn",
|
||||
"fluxFillIncompatibleWithControlLoRA": "Control LoRA is not compatible with FLUX Fill"
|
||||
},
|
||||
"errors": {
|
||||
"unableToFindImage": "Unable to find image",
|
||||
@@ -2333,7 +2344,7 @@
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"items": [
|
||||
"Workflows: New and improved Workflow Library.",
|
||||
"FLUX: Support for FLUX Redux in Workflows and Canvas."
|
||||
"FLUX: Support for FLUX Redux & FLUX Fill in Workflows and Canvas."
|
||||
],
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchRecentReleaseVideos": "Watch Recent Release Videos",
|
||||
|
||||
@@ -1653,7 +1653,6 @@
|
||||
"collectionFieldType": "{{name}} (Collection)",
|
||||
"newWorkflow": "Nouveau Workflow",
|
||||
"reorderLinearView": "Réorganiser la vue linéaire",
|
||||
"unknownOutput": "Sortie inconnue : {{name}}",
|
||||
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
|
||||
"unsupportedMismatchedUnion": "type CollectionOrScalar non concordant avec les types de base {{firstType}} et {{secondType}}",
|
||||
"unableToParseFieldType": "impossible d'analyser le type de champ",
|
||||
|
||||
@@ -779,7 +779,8 @@
|
||||
"enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa",
|
||||
"modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate",
|
||||
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni.",
|
||||
"showDetailedInvocationProgress": "Mostra dettagli avanzamento"
|
||||
"showDetailedInvocationProgress": "Mostra dettagli avanzamento",
|
||||
"enableHighlightFocusedRegions": "Evidenzia le regioni interessate"
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Caricamento fallito",
|
||||
@@ -968,7 +969,6 @@
|
||||
"unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro",
|
||||
"nodePack": "Pacchetto di nodi",
|
||||
"unableToExtractSchemaNameFromRef": "Impossibile estrarre il nome dello schema dal riferimento",
|
||||
"unknownOutput": "Output sconosciuto: {{name}}",
|
||||
"unknownNodeType": "Tipo di nodo sconosciuto",
|
||||
"targetNodeDoesNotExist": "Connessione non valida: il nodo di destinazione/input {{node}} non esiste",
|
||||
"unknownFieldType": "$t(nodes.unknownField) tipo: {{type}}",
|
||||
@@ -1776,7 +1776,9 @@
|
||||
"text": "Testo",
|
||||
"numberInput": "Ingresso numerico",
|
||||
"containerRowLayout": "Contenitore (disposizione riga)",
|
||||
"containerColumnLayout": "Contenitore (disposizione colonna)"
|
||||
"containerColumnLayout": "Contenitore (disposizione colonna)",
|
||||
"minimum": "Minimo",
|
||||
"maximum": "Massimo"
|
||||
},
|
||||
"loadMore": "Carica altro",
|
||||
"searchPlaceholder": "Cerca per nome, descrizione o etichetta",
|
||||
@@ -1791,7 +1793,8 @@
|
||||
"private": "Privato",
|
||||
"deselectAll": "Deseleziona tutto",
|
||||
"noRecentWorkflows": "Nessun flusso di lavoro recente",
|
||||
"view": "Visualizza"
|
||||
"view": "Visualizza",
|
||||
"recommended": "Consigliato per te"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
@@ -2350,8 +2353,8 @@
|
||||
"watchRecentReleaseVideos": "Guarda i video su questa versione",
|
||||
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
|
||||
"items": [
|
||||
"Gestione della memoria: nuova impostazione per gli utenti con GPU Nvidia per ridurre l'utilizzo della VRAM.",
|
||||
"Prestazioni: continui miglioramenti alle prestazioni e alla reattività complessive dell'applicazione."
|
||||
"Flussi di lavoro: nuova e migliorata libreria dei flussi di lavoro.",
|
||||
"FLUX: supporto per FLUX Redux in Flussi di lavoro e Tela."
|
||||
]
|
||||
},
|
||||
"system": {
|
||||
|
||||
@@ -425,7 +425,6 @@
|
||||
"newWorkflow": "Nieuwe werkstroom",
|
||||
"unknownErrorValidatingWorkflow": "Onbekende fout bij valideren werkstroom",
|
||||
"unsupportedAnyOfLength": "te veel union-leden ({{count}})",
|
||||
"unknownOutput": "Onbekende uitvoer: {{name}}",
|
||||
"viewMode": "Gebruik in lineaire weergave",
|
||||
"unableToExtractSchemaNameFromRef": "fout bij het extraheren van de schemanaam via de ref",
|
||||
"unsupportedMismatchedUnion": "niet-overeenkomende soort CollectionOrScalar met basissoorten {{firstType}} en {{secondType}}",
|
||||
|
||||
@@ -879,7 +879,6 @@
|
||||
"unableToExtractSchemaNameFromRef": "невозможно извлечь имя схемы из ссылки",
|
||||
"executionStateError": "Ошибка",
|
||||
"prototypeDesc": "Этот вызов является прототипом. Он может претерпевать изменения при обновлении приложения и может быть удален в любой момент.",
|
||||
"unknownOutput": "Неизвестный вывод: {{name}}",
|
||||
"executionStateCompleted": "Выполнено",
|
||||
"node": "Узел",
|
||||
"workflowAuthor": "Автор",
|
||||
|
||||
@@ -236,7 +236,8 @@
|
||||
"layout": "Bố Cục",
|
||||
"row": "Hàng",
|
||||
"board": "Bảng",
|
||||
"saveChanges": "Lưu Thay Đổi"
|
||||
"saveChanges": "Lưu Thay Đổi",
|
||||
"error_withCount_other": "{{count}} lỗi"
|
||||
},
|
||||
"prompt": {
|
||||
"addPromptTrigger": "Thêm Prompt Trigger",
|
||||
@@ -769,7 +770,8 @@
|
||||
"urlForbiddenErrorMessage": "Bạn có thể cần yêu cầu quyền truy cập từ trang web đang cung cấp model.",
|
||||
"urlUnauthorizedErrorMessage": "Bạn có thể cần thiếp lập một token API để dùng được model này.",
|
||||
"fluxRedux": "FLUX Redux",
|
||||
"sigLip": "SigLIP"
|
||||
"sigLip": "SigLIP",
|
||||
"llavaOnevision": "LLaVA OneVision"
|
||||
},
|
||||
"metadata": {
|
||||
"guidance": "Hướng Dẫn",
|
||||
@@ -892,7 +894,6 @@
|
||||
"targetNodeFieldDoesNotExist": "Kết nối không phù hợp: đích đến/đầu vào của vùng {{node}}.{{field}} không tồn tại",
|
||||
"missingTemplate": "Node không hợp lệ: node {{node}} thuộc loại {{type}} bị thiếu mẫu trình bày (chưa tải?)",
|
||||
"unsupportedMismatchedUnion": "Dạng số lượng dữ liệu không khớp với {{firstType}} và {{secondType}}",
|
||||
"unknownOutput": "Đầu Ra Không Rõ: {{name}}",
|
||||
"betaDesc": "Trình kích hoạt này vẫn trong giai đoạn beta. Cho đến khi ổn định, nó có thể phá hỏng thay đổi trong khi cập nhật ứng dụng. Chúng tôi dự định hỗ trợ trình kích hoạt này về lâu dài.",
|
||||
"cannotConnectInputToInput": "Không thế kết nối đầu vào với đầu vào",
|
||||
"showEdgeLabelsHelp": "Hiển thị tên trên kết nối, chỉ ra những node được kết nối",
|
||||
@@ -1618,7 +1619,8 @@
|
||||
"displayInProgress": "Hiển Thị Hình Ảnh Đang Xử Lý",
|
||||
"intermediatesClearedFailed": "Có Vấn Đề Khi Dọn Sạch Sản Phẩm Trung Gian",
|
||||
"enableInvisibleWatermark": "Bật Chế Độ Ẩn Watermark",
|
||||
"showDetailedInvocationProgress": "Hiện Dữ Liệu Xử Lý"
|
||||
"showDetailedInvocationProgress": "Hiện Dữ Liệu Xử Lý",
|
||||
"enableHighlightFocusedRegions": "Nhấn Mạnh Khu Vực Chỉ Định"
|
||||
},
|
||||
"sdxl": {
|
||||
"loading": "Đang Tải...",
|
||||
@@ -2288,7 +2290,11 @@
|
||||
"container": "Hộp Chứa",
|
||||
"heading": "Đầu Dòng",
|
||||
"text": "Văn Bản",
|
||||
"divider": "Gạch Chia"
|
||||
"divider": "Gạch Chia",
|
||||
"minimum": "Tối Thiểu",
|
||||
"maximum": "Tối Đa",
|
||||
"containerRowLayout": "Hộp Chứa (bố cục hàng)",
|
||||
"containerColumnLayout": "Hộp Chứa (bố cục cột)"
|
||||
},
|
||||
"yourWorkflows": "Workflow Của Bạn",
|
||||
"browseWorkflows": "Khám Phá Workflow",
|
||||
@@ -2300,7 +2306,11 @@
|
||||
"filterByTags": "Lọc Theo Nhãn",
|
||||
"recentlyOpened": "Mở Gần Đây",
|
||||
"private": "Cá Nhân",
|
||||
"loadMore": "Tải Thêm"
|
||||
"loadMore": "Tải Thêm",
|
||||
"view": "Xem",
|
||||
"deselectAll": "Huỷ Chọn Tất Cả",
|
||||
"noRecentWorkflows": "Không Có Workflows Gần Đây",
|
||||
"recommended": "Có Thể Bạn Sẽ Cần"
|
||||
},
|
||||
"upscaling": {
|
||||
"missingUpscaleInitialImage": "Thiếu ảnh dùng để upscale",
|
||||
@@ -2327,7 +2337,8 @@
|
||||
"gettingStartedSeries": "Cần thêm hướng dẫn? Xem thử <LinkComponent>Bắt Đầu Làm Quen</LinkComponent> để biết thêm mẹo khai thác toàn bộ tiềm năng của Invoke Studio.",
|
||||
"toGetStarted": "Để bắt đầu, hãy nhập lệnh vào hộp và nhấp chuột vào <StrongComponent>Kích Hoạt</StrongComponent> để tạo ra bức ảnh đầu tiên. Chọn một mẫu trình bày cho lệnh để cải thiện kết quả. Bạn có thể chọn để lưu ảnh trực tiếp vào <StrongComponent>Thư Viện Ảnh</StrongComponent> hoặc chỉnh sửa chúng ở <StrongComponent>Canvas</StrongComponent>.",
|
||||
"noModelsInstalled": "Dường như bạn chưa tải model nào cả! Bạn có thể <DownloadStarterModelsButton>tải xuống các model khởi đầu</DownloadStarterModelsButton> hoặc <ImportModelsButton>nhập vào thêm model</ImportModelsButton>.",
|
||||
"lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi."
|
||||
"lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo <LinkComponent>hướng dẫn VRAM Thấp</LinkComponent> của chúng tôi.",
|
||||
"toGetStartedWorkflow": "Để bắt đầu, hãy điền vào khu vực bên trái và bấm <StrongComponent>Kích Hoạt</StrongComponent> nhằm tạo sinh ảnh. Muốn khám phá thêm workflow? Nhấp vào <StrongComponent>icon thư mục</StrongComponent> nằm cạnh tiêu đề workflow để xem một dãy các mẫu trình bày khác."
|
||||
},
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "Có Gì Mới Ở Invoke",
|
||||
@@ -2335,8 +2346,8 @@
|
||||
"watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất",
|
||||
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
|
||||
"items": [
|
||||
"Trình Quản Lý Bộ Nhớ: Thiết lập mới cho người dùng với GPU Nvidia để giảm lượng VRAM sử dụng.",
|
||||
"Hiệu suất: Các cải thiện tiếp theo nhằm gói gọn hiệu suất và khả năng phản hồi của ứng dụng."
|
||||
"Workflow: Thư Viện Workflow mới và đã được cải tiến.",
|
||||
"FLUX: Hỗ trợ FLUX Redux trong Workflow và Canvas."
|
||||
]
|
||||
},
|
||||
"upsell": {
|
||||
|
||||
@@ -908,7 +908,6 @@
|
||||
"unableToGetWorkflowVersion": "无法获取工作流架构版本",
|
||||
"nodePack": "节点包",
|
||||
"unableToExtractSchemaNameFromRef": "无法从参考中提取架构名",
|
||||
"unknownOutput": "未知输出:{{name}}",
|
||||
"unknownErrorValidatingWorkflow": "验证工作流时出现未知错误",
|
||||
"collectionFieldType": "{{name}}(合集)",
|
||||
"unknownNodeType": "未知节点类型",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
||||
import { selectModel } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
@@ -19,11 +18,12 @@ import { upperFirst } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWarningBold } from 'react-icons/pi';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const buildSelectWarnings = (entityIdentifier: CanvasEntityIdentifier, t: TFunction) => {
|
||||
return createSelector(selectCanvasSlice, selectModel, (canvas, model) => {
|
||||
return createSelector(selectCanvasSlice, selectMainModelConfig, (canvas, model) => {
|
||||
// This component is used within a <CanvasEntityStateGate /> so we can safely assume that the entity exists.
|
||||
// Should never throw.
|
||||
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings');
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import type { MainModelConfig } from 'services/api/types';
|
||||
|
||||
const WARNINGS = {
|
||||
UNSUPPORTED_MODEL: 'controlLayers.warnings.unsupportedModel',
|
||||
@@ -20,13 +20,14 @@ const WARNINGS = {
|
||||
CONTROL_ADAPTER_NO_MODEL_SELECTED: 'controlLayers.warnings.controlAdapterNoModelSelected',
|
||||
CONTROL_ADAPTER_INCOMPATIBLE_BASE_MODEL: 'controlLayers.warnings.controlAdapterIncompatibleBaseModel',
|
||||
CONTROL_ADAPTER_NO_CONTROL: 'controlLayers.warnings.controlAdapterNoControl',
|
||||
FLUX_FILL_NO_WORKY_WITH_CONTROL_LORA: 'controlLayers.warnings.fluxFillIncompatibleWithControlLoRA',
|
||||
} as const;
|
||||
|
||||
type WarningTKey = (typeof WARNINGS)[keyof typeof WARNINGS];
|
||||
|
||||
export const getRegionalGuidanceWarnings = (
|
||||
entity: CanvasRegionalGuidanceState,
|
||||
model: ParameterModel | null
|
||||
model: MainModelConfig | null | undefined
|
||||
): WarningTKey[] => {
|
||||
const warnings: WarningTKey[] = [];
|
||||
|
||||
@@ -78,7 +79,7 @@ export const getRegionalGuidanceWarnings = (
|
||||
|
||||
export const getGlobalReferenceImageWarnings = (
|
||||
entity: CanvasReferenceImageState,
|
||||
model: ParameterModel | null
|
||||
model: MainModelConfig | null | undefined
|
||||
): WarningTKey[] => {
|
||||
const warnings: WarningTKey[] = [];
|
||||
|
||||
@@ -110,7 +111,7 @@ export const getGlobalReferenceImageWarnings = (
|
||||
|
||||
export const getControlLayerWarnings = (
|
||||
entity: CanvasControlLayerState,
|
||||
model: ParameterModel | null
|
||||
model: MainModelConfig | null | undefined
|
||||
): WarningTKey[] => {
|
||||
const warnings: WarningTKey[] = [];
|
||||
|
||||
@@ -129,6 +130,13 @@ export const getControlLayerWarnings = (
|
||||
} else if (entity.controlAdapter.model.base !== model.base) {
|
||||
// Supported model architecture but doesn't match
|
||||
warnings.push(WARNINGS.CONTROL_ADAPTER_INCOMPATIBLE_BASE_MODEL);
|
||||
} else if (
|
||||
model.base === 'flux' &&
|
||||
model.variant === 'inpaint' &&
|
||||
entity.controlAdapter.model.type === 'control_lora'
|
||||
) {
|
||||
// FLUX inpaint variants are FLUX Fill models - not compatible w/ Control LoRA
|
||||
warnings.push(WARNINGS.FLUX_FILL_NO_WORKY_WITH_CONTROL_LORA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +145,7 @@ export const getControlLayerWarnings = (
|
||||
|
||||
export const getRasterLayerWarnings = (
|
||||
_entity: CanvasRasterLayerState,
|
||||
_model: ParameterModel | null
|
||||
_model: MainModelConfig | null | undefined
|
||||
): WarningTKey[] => {
|
||||
const warnings: WarningTKey[] = [];
|
||||
|
||||
@@ -148,7 +156,7 @@ export const getRasterLayerWarnings = (
|
||||
|
||||
export const getInpaintMaskWarnings = (
|
||||
_entity: CanvasInpaintMaskState,
|
||||
_model: ParameterModel | null
|
||||
_model: MainModelConfig | null | undefined
|
||||
): WarningTKey[] => {
|
||||
const warnings: WarningTKey[] = [];
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ export class InvalidModelConfigError extends Error {
|
||||
export const fetchModelConfig = async (key: string): Promise<AnyModelConfig> => {
|
||||
const { dispatch } = getStore();
|
||||
try {
|
||||
const req = dispatch(modelsApi.endpoints.getModelConfig.initiate(key));
|
||||
req.unsubscribe();
|
||||
const req = dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false }));
|
||||
return await req.unwrap();
|
||||
} catch {
|
||||
throw new ModelConfigNotFoundError(`Unable to retrieve model config for key ${key}`);
|
||||
@@ -62,8 +61,9 @@ export const fetchModelConfig = async (key: string): Promise<AnyModelConfig> =>
|
||||
const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: ModelType): Promise<AnyModelConfig> => {
|
||||
const { dispatch } = getStore();
|
||||
try {
|
||||
const req = dispatch(modelsApi.endpoints.getModelConfigByAttrs.initiate({ name, base, type }));
|
||||
req.unsubscribe();
|
||||
const req = dispatch(
|
||||
modelsApi.endpoints.getModelConfigByAttrs.initiate({ name, base, type }, { subscribe: false })
|
||||
);
|
||||
return await req.unwrap();
|
||||
} catch {
|
||||
throw new ModelConfigNotFoundError(`Unable to retrieve model config for name/base/type ${name}/${base}/${type}`);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ModelTypeFilter = memo(() => {
|
||||
clip_vision: 'CLIP Vision',
|
||||
spandrel_image_to_image: t('modelManager.spandrelImageToImage'),
|
||||
control_lora: t('modelManager.controlLora'),
|
||||
siglip: t('modelManager.siglip'),
|
||||
siglip: t('modelManager.sigLip'),
|
||||
flux_redux: t('modelManager.fluxRedux'),
|
||||
llava_onevision: t('modelManager.llavaOnevision'),
|
||||
}),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Textarea,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
|
||||
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
|
||||
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import type { ChangeEvent } from 'react';
|
||||
@@ -48,7 +48,7 @@ InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover';
|
||||
const Content = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const description = useInputFieldDescription(nodeId, fieldName);
|
||||
const description = useInputFieldDescriptionSafe(nodeId, fieldName);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value }));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/componen
|
||||
import { useNodeFieldDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
|
||||
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useRef } from 'react';
|
||||
@@ -22,7 +22,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
|
||||
|
||||
@@ -1,23 +1,83 @@
|
||||
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
|
||||
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
|
||||
import { useInputFieldNameSafe } from 'features/nodes/hooks/useInputFieldNameSafe';
|
||||
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fallback?: ReactNode;
|
||||
formatLabel?: (name: string) => string;
|
||||
}>;
|
||||
|
||||
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
|
||||
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
|
||||
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
|
||||
if (fallback !== undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return (
|
||||
<Fallback
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
formatLabel={formatLabel}
|
||||
hasInstance={hasInstance}
|
||||
hasTemplate={hasTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
InputFieldGate.displayName = 'InputFieldGate';
|
||||
|
||||
const Fallback = memo(
|
||||
({
|
||||
nodeId,
|
||||
fieldName,
|
||||
formatLabel,
|
||||
hasTemplate,
|
||||
hasInstance,
|
||||
}: {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
formatLabel?: (name: string) => string;
|
||||
hasTemplate: boolean;
|
||||
hasInstance: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useInputFieldNameSafe(nodeId, fieldName);
|
||||
const label = useMemo(() => {
|
||||
if (formatLabel) {
|
||||
return formatLabel(name);
|
||||
}
|
||||
if (hasTemplate && !hasInstance) {
|
||||
return t('nodes.missingField_withName', { name });
|
||||
}
|
||||
if (!hasTemplate && hasInstance) {
|
||||
return t('nodes.unexpectedField_withName', { name });
|
||||
}
|
||||
return t('nodes.unknownField_withName', { name });
|
||||
}, [formatLabel, hasInstance, hasTemplate, name, t]);
|
||||
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<Flex w="full" px={1} py={1} justifyContent="center">
|
||||
<Text fontWeight="semibold" color="error.300" whiteSpace="pre" textAlign="center">
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Fallback.displayName = 'Fallback';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useIsConnectionInProgress,
|
||||
useIsConnectionStartField,
|
||||
} from 'features/nodes/hooks/useFieldConnectionState';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
@@ -62,7 +62,7 @@ const handleStyles = {
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export const InputFieldHandle = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
|
||||
const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]);
|
||||
|
||||
@@ -13,10 +13,11 @@ import { StringGeneratorFieldInputComponent } from 'features/nodes/components/fl
|
||||
import { IntegerFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput';
|
||||
import { IntegerFieldInputAndSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider';
|
||||
import { IntegerFieldSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider';
|
||||
import { StringFieldDropdown } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown';
|
||||
import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput';
|
||||
import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import {
|
||||
isBoardFieldInputInstance,
|
||||
isBoardFieldInputTemplate,
|
||||
@@ -135,7 +136,7 @@ type Props = {
|
||||
|
||||
export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => {
|
||||
const field = useInputFieldInstance(nodeId, fieldName);
|
||||
const template = useInputFieldTemplate(nodeId, fieldName);
|
||||
const template = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
|
||||
// When deciding which component to render, first we check the type of the template, which is more efficient than the
|
||||
// instance type check. The instance type check uses zod and is slower.
|
||||
@@ -151,7 +152,7 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
|
||||
if (!isStringFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
if (settings?.type !== 'string-field-config') {
|
||||
if (!settings || settings.type !== 'string-field-config') {
|
||||
if (template.ui_component === 'textarea') {
|
||||
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else {
|
||||
@@ -162,8 +163,10 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
|
||||
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'textarea') {
|
||||
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'dropdown') {
|
||||
return <StringFieldDropdown nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
|
||||
} else {
|
||||
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
|
||||
assert<Equals<never, typeof settings>>(false, 'Unexpected settings');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useIsConnectionStartField,
|
||||
} from 'features/nodes/hooks/useFieldConnectionState';
|
||||
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
|
||||
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY, NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
|
||||
@@ -43,7 +43,7 @@ interface Props {
|
||||
export const InputFieldTitle = memo((props: Props) => {
|
||||
const { nodeId, fieldName, isInvalid, isDragging } = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const label = useInputFieldLabel(nodeId, fieldName);
|
||||
const label = useInputFieldLabelSafe(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldErrors } from 'features/nodes/hooks/useInputFieldErrors';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
@@ -16,7 +16,7 @@ export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldErrors = useInputFieldErrors(nodeId, fieldName);
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
|
||||
import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useInputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{t('nodes.unknownInput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
|
||||
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
|
||||
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
|
||||
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
@@ -12,10 +15,27 @@ export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) =>
|
||||
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate) {
|
||||
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
return <Fallback nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
OutputFieldGate.displayName = 'OutputFieldGate';
|
||||
|
||||
const Fallback = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useOutputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unexpectedField_withName', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
Fallback.displayName = 'Fallback';
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
|
||||
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useOutputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unknownOutput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Select } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/useStringField';
|
||||
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldStringSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StringFieldDropdown = memo(
|
||||
(
|
||||
props: FieldComponentProps<
|
||||
StringFieldInputInstance,
|
||||
StringFieldInputTemplate,
|
||||
{ settings: Extract<NodeFieldStringSettings, { component: 'dropdown' }> }
|
||||
>
|
||||
) => {
|
||||
const { value, onChange } = useStringField(props);
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={onChange}
|
||||
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
|
||||
isDisabled={props.settings.options.length === 0}
|
||||
value={value}
|
||||
>
|
||||
{props.settings.options.map((choice, i) => (
|
||||
<option key={`${i}_${choice.value}`} value={choice.value}>
|
||||
{choice.label || choice.value || `Option ${i + 1}`}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StringFieldDropdown.displayName = 'StringFieldDropdown';
|
||||
@@ -4,12 +4,21 @@ import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/
|
||||
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const StringFieldInput = memo(
|
||||
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { value, onChange } = useStringField(props);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Input className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`} value={value} onChange={onChange} />;
|
||||
return (
|
||||
<Input
|
||||
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
|
||||
placeholder={t('workflows.emptyStringPlaceholder')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ import { useStringField } from 'features/nodes/components/flow/nodes/Invocation/
|
||||
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import type { StringFieldInputInstance, StringFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const StringFieldTextarea = memo(
|
||||
(props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { t } = useTranslation();
|
||||
const { value, onChange } = useStringField(props);
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
className={`${NO_DRAG_CLASS} ${NO_PAN_CLASS} ${NO_WHEEL_CLASS}`}
|
||||
placeholder={t('workflows.emptyStringPlaceholder')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
h="full"
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useStringField = (props: FieldComponentProps<StringFieldInputInstan
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
dispatch(
|
||||
fieldStringValueChanged({
|
||||
nodeId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, Divider, Flex, FormLabel, Grid, GridItem, IconButton, Input } from '@invoke-ai/ui-library';
|
||||
import { Button, Divider, Flex, Grid, GridItem, IconButton, Input, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
|
||||
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
|
||||
@@ -107,42 +107,6 @@ export const StringFieldCollectionInputComponent = memo(
|
||||
|
||||
StringFieldCollectionInputComponent.displayName = 'StringFieldCollectionInputComponent';
|
||||
|
||||
type StringListItemContentProps = {
|
||||
value: string;
|
||||
index: number;
|
||||
onRemoveString: (index: number) => void;
|
||||
onChangeString: (index: number, value: string) => void;
|
||||
};
|
||||
|
||||
const StringListItemContent = memo(({ value, index, onRemoveString, onChangeString }: StringListItemContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRemove = useCallback(() => {
|
||||
onRemoveString(index);
|
||||
}, [index, onRemoveString]);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeString(index, e.target.value);
|
||||
},
|
||||
[index, onChangeString]
|
||||
);
|
||||
return (
|
||||
<Flex alignItems="center" gap={1}>
|
||||
<Input size="xs" resize="none" value={value} onChange={onChange} />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={onClickRemove}
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('common.remove')}
|
||||
tooltip={t('common.remove')}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
StringListItemContent.displayName = 'StringListItemContent';
|
||||
|
||||
type ListItemContentProps = {
|
||||
value: string;
|
||||
index: number;
|
||||
@@ -166,19 +130,26 @@ const ListItemContent = memo(({ value, index, onRemoveString, onChangeString }:
|
||||
return (
|
||||
<>
|
||||
<GridItem>
|
||||
<FormLabel ps={1} m={0}>
|
||||
<Text variant="subtext" textAlign="center" minW={8}>
|
||||
{index + 1}.
|
||||
</FormLabel>
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Input size="sm" resize="none" value={value} onChange={onChange} />
|
||||
<Input
|
||||
placeholder={t('workflows.emptyStringPlaceholder')}
|
||||
size="sm"
|
||||
resize="none"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
minW={8}
|
||||
minH={8}
|
||||
onClick={onClickRemove}
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('common.delete')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
|
||||
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
|
||||
@@ -47,8 +48,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest
|
||||
<Label element={element} />
|
||||
<Spacer />
|
||||
{isContainerElement(element) && <ContainerElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
|
||||
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && (
|
||||
<InputFieldGate
|
||||
nodeId={element.data.fieldIdentifier.nodeId}
|
||||
fieldName={element.data.fieldIdentifier.fieldName}
|
||||
fallback={null} // Do not render these buttons if the field is not found
|
||||
>
|
||||
<ZoomToNodeButton element={element} />
|
||||
<NodeFieldElementSettings element={element} />
|
||||
</InputFieldGate>
|
||||
)}
|
||||
<RemoveElementButton element={element} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
|
||||
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
|
||||
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
@@ -15,19 +14,11 @@ export const NodeFieldElement = memo(({ id }: { id: string }) => {
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return (
|
||||
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
|
||||
<NodeFieldElementViewMode el={el} />
|
||||
</InputFieldGate>
|
||||
);
|
||||
return <NodeFieldElementViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return (
|
||||
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
|
||||
<NodeFieldElementEditMode el={el} />
|
||||
</InputFieldGate>
|
||||
);
|
||||
return <NodeFieldElementEditMode el={el} />;
|
||||
});
|
||||
|
||||
NodeFieldElement.displayName = 'NodeFieldElement';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { FormHelperText, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { linkifyOptions, linkifySx } from 'common/components/linkify';
|
||||
import { useEditable } from 'common/hooks/useEditable';
|
||||
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import Linkify from 'linkify-react';
|
||||
@@ -13,8 +13,8 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import { Box, Divider, Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
|
||||
@@ -8,9 +9,11 @@ import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/
|
||||
import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
|
||||
import { NodeFieldElementDescriptionEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable';
|
||||
import { NodeFieldElementLabelEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable';
|
||||
import { NodeFieldElementStringDropdownSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringDropdownSettings';
|
||||
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
@@ -31,25 +34,11 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
|
||||
const containerCtx = useContainerContext();
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
|
||||
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
|
||||
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
</FormControl>
|
||||
</FormElementEditModeContent>
|
||||
<NodeFieldElementEditModeContent dragHandleRef={dragHandleRef} el={el} isDragging={isDragging} />
|
||||
<NodeFieldElementOverlay element={el} />
|
||||
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
|
||||
</Flex>
|
||||
@@ -58,6 +47,48 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
|
||||
|
||||
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
|
||||
|
||||
const NodeFieldElementEditModeContent = memo(
|
||||
({
|
||||
el,
|
||||
dragHandleRef,
|
||||
isDragging,
|
||||
}: {
|
||||
el: NodeFieldElement;
|
||||
dragHandleRef: RefObject<HTMLDivElement>;
|
||||
isDragging: boolean;
|
||||
}) => {
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
return (
|
||||
<>
|
||||
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
|
||||
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
{data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && (
|
||||
<>
|
||||
<Divider />
|
||||
<NodeFieldElementStringDropdownSettings id={id} settings={data.settings} />
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
</InputFieldGate>
|
||||
</FormElementEditModeContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
NodeFieldElementEditModeContent.displayName = 'NodeFieldElementEditModeContent';
|
||||
|
||||
const nodeFieldOverlaySx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@@ -14,42 +14,48 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
config: NodeFieldFloatSettings;
|
||||
settings: NodeFieldFloatSettings;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fieldTemplate: FloatFieldInputTemplate;
|
||||
};
|
||||
|
||||
export const NodeFieldElementFloatSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingComponent
|
||||
id={id}
|
||||
settings={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
<SettingMin id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
|
||||
|
||||
const SettingComponent = memo(({ id, config }: Props) => {
|
||||
const SettingComponent = memo(({ id, settings }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
component: zNumberComponent.parse(e.target.value),
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
[settings, dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<Select value={settings.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
<option value="slider">{t('workflows.builder.slider')}</option>
|
||||
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
|
||||
@@ -59,36 +65,36 @@ const SettingComponent = memo(({ id, config }: Props) => {
|
||||
});
|
||||
SettingComponent.displayName = 'SettingComponent';
|
||||
|
||||
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
min: config.min !== undefined ? undefined : floatField.min,
|
||||
const onToggleOverride = useCallback(() => {
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
min: settings.min !== undefined ? undefined : floatField.min,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField, id]);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [settings, dispatch, floatField, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(min: number) => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
min,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
|
||||
// We may need to update the value if it is outside the new min/max range
|
||||
const constrained = constrainNumber(field.value, floatField, newConfig);
|
||||
const constrained = constrainNumber(field.value, floatField, newSettings);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
[settings, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
const constraintMin = useMemo(
|
||||
@@ -97,20 +103,20 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
);
|
||||
|
||||
const constraintMax = useMemo(
|
||||
() => (config.max ?? floatField.max) - floatField.step,
|
||||
[config.max, floatField.max, floatField.step]
|
||||
() => (settings.max ?? floatField.max) - floatField.step,
|
||||
[settings.max, floatField.max, floatField.step]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
|
||||
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
<Switch isChecked={settings.min !== undefined} onChange={onToggleOverride} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.min === undefined}
|
||||
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
|
||||
isDisabled={settings.min === undefined}
|
||||
value={settings.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : settings.min}
|
||||
onChange={onChange}
|
||||
min={constraintMin}
|
||||
max={constraintMax}
|
||||
@@ -121,41 +127,41 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
});
|
||||
SettingMin.displayName = 'SettingMin';
|
||||
|
||||
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
max: config.max !== undefined ? undefined : floatField.max,
|
||||
const onToggleOverride = useCallback(() => {
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
max: settings.max !== undefined ? undefined : floatField.max,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField, id]);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [settings, dispatch, floatField, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(max: number) => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
max,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
|
||||
// We may need to update the value if it is outside the new min/max range
|
||||
const constrained = constrainNumber(field.value, floatField, newConfig);
|
||||
const constrained = constrainNumber(field.value, floatField, newSettings);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
[settings, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
const constraintMin = useMemo(
|
||||
() => (config.min ?? floatField.min) + floatField.step,
|
||||
[config.min, floatField.min, floatField.step]
|
||||
() => (settings.min ?? floatField.min) + floatField.step,
|
||||
[settings.min, floatField.min, floatField.step]
|
||||
);
|
||||
|
||||
const constraintMax = useMemo(
|
||||
@@ -167,12 +173,12 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
|
||||
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
<Switch isChecked={settings.max !== undefined} onChange={onToggleOverride} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.max === undefined}
|
||||
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
|
||||
isDisabled={settings.max === undefined}
|
||||
value={settings.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : settings.max}
|
||||
onChange={onChange}
|
||||
min={constraintMin}
|
||||
max={constraintMax}
|
||||
|
||||
@@ -15,42 +15,48 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
config: NodeFieldIntegerSettings;
|
||||
settings: NodeFieldIntegerSettings;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fieldTemplate: IntegerFieldInputTemplate;
|
||||
};
|
||||
|
||||
export const NodeFieldElementIntegerSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingComponent
|
||||
id={id}
|
||||
settings={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
<SettingMin id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
NodeFieldElementIntegerSettings.displayName = 'NodeFieldElementIntegerSettings';
|
||||
|
||||
const SettingComponent = memo(({ id, config }: Props) => {
|
||||
const SettingComponent = memo(({ id, settings }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
component: zNumberComponent.parse(e.target.value),
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
[settings, dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<Select value={settings.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
<option value="slider">{t('workflows.builder.slider')}</option>
|
||||
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
|
||||
@@ -60,37 +66,37 @@ const SettingComponent = memo(({ id, config }: Props) => {
|
||||
});
|
||||
SettingComponent.displayName = 'SettingComponent';
|
||||
|
||||
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
min: config.min !== undefined ? undefined : integerField.min,
|
||||
const onToggleOverride = useCallback(() => {
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
min: settings.min !== undefined ? undefined : integerField.min,
|
||||
};
|
||||
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, integerField.min, id]);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [settings, dispatch, integerField.min, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(min: number) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
min,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
|
||||
// We may need to update the value if it is outside the new min/max range
|
||||
const constrained = constrainNumber(field.value, integerField, newConfig);
|
||||
const constrained = constrainNumber(field.value, integerField, newSettings);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, id, field, integerField, nodeId, fieldName]
|
||||
[settings, dispatch, id, field, integerField, nodeId, fieldName]
|
||||
);
|
||||
|
||||
const constraintMin = useMemo(
|
||||
@@ -99,20 +105,20 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
);
|
||||
|
||||
const constraintMax = useMemo(
|
||||
() => (config.max ?? integerField.max) - integerField.step,
|
||||
[config.max, integerField.max, integerField.step]
|
||||
() => (settings.max ?? integerField.max) - integerField.step,
|
||||
[settings.max, integerField.max, integerField.step]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
|
||||
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
<Switch isChecked={settings.min !== undefined} onChange={onToggleOverride} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.min === undefined}
|
||||
value={config.min ?? (`${integerField.min} (inherited)` as unknown as number)}
|
||||
isDisabled={settings.min === undefined}
|
||||
value={settings.min ?? (`${integerField.min} (inherited)` as unknown as number)}
|
||||
onChange={onChange}
|
||||
min={constraintMin}
|
||||
max={constraintMax}
|
||||
@@ -123,42 +129,42 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
});
|
||||
SettingMin.displayName = 'SettingMin';
|
||||
|
||||
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
max: config.max !== undefined ? undefined : integerField.max,
|
||||
const onToggleOverride = useCallback(() => {
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
max: settings.max !== undefined ? undefined : integerField.max,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, integerField.max, id]);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [settings, dispatch, integerField.max, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(max: number) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
max,
|
||||
};
|
||||
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
|
||||
// We may need to update the value if it is outside the new min/max range
|
||||
const constrained = constrainNumber(field.value, integerField, newConfig);
|
||||
const constrained = constrainNumber(field.value, integerField, newSettings);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, integerField, id, nodeId]
|
||||
[settings, dispatch, field.value, fieldName, integerField, id, nodeId]
|
||||
);
|
||||
|
||||
const constraintMin = useMemo(
|
||||
() => (config.min ?? integerField.min) + integerField.step,
|
||||
[config.min, integerField.min, integerField.step]
|
||||
() => (settings.min ?? integerField.min) + integerField.step,
|
||||
[settings.min, integerField.min, integerField.step]
|
||||
);
|
||||
|
||||
const constraintMax = useMemo(
|
||||
@@ -170,12 +176,12 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
|
||||
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
<Switch isChecked={settings.max !== undefined} onChange={onToggleOverride} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.max === undefined}
|
||||
value={config.max ?? (`${integerField.max} (inherited)` as unknown as number)}
|
||||
isDisabled={settings.max === undefined}
|
||||
value={settings.max ?? (`${integerField.max} (inherited)` as unknown as number)}
|
||||
onChange={onChange}
|
||||
min={constraintMin}
|
||||
max={constraintMax}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Flex, FormLabel, Spacer } from '@invoke-ai/ui-library';
|
||||
import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
export const NodeFieldElementLabel = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const label = useInputFieldLabelSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
|
||||
const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Flex, FormLabel, Input, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEditable } from 'common/hooks/useEditable';
|
||||
import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
@@ -12,8 +12,8 @@ export const NodeFieldElementLabelEditable = memo(({ el }: { el: NodeFieldElemen
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const label = useInputFieldLabelSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { NodeFieldElementFloatSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings';
|
||||
import { NodeFieldElementIntegerSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
|
||||
import { NodeFieldElementStringSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringSettings';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import {
|
||||
isFloatFieldInputTemplate,
|
||||
@@ -36,7 +36,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
|
||||
const { id, data } = element;
|
||||
const { showDescription, fieldIdentifier } = data;
|
||||
const { nodeId, fieldName } = fieldIdentifier;
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -79,7 +79,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
|
||||
{settings?.type === 'integer-field-config' && isIntegerFieldInputTemplate(fieldTemplate) && (
|
||||
<NodeFieldElementIntegerSettings
|
||||
id={id}
|
||||
config={settings}
|
||||
settings={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
@@ -88,13 +88,15 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
|
||||
{settings?.type === 'float-field-config' && isFloatFieldInputTemplate(fieldTemplate) && (
|
||||
<NodeFieldElementFloatSettings
|
||||
id={id}
|
||||
config={settings}
|
||||
settings={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
)}
|
||||
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
|
||||
{settings?.type === 'string-field-config' && (
|
||||
<NodeFieldElementStringSettings id={id} settings={settings} />
|
||||
)}
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Button, ButtonGroup, Divider, Flex, Grid, GridItem, IconButton, Input, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import { getDefaultStringOption, type NodeFieldStringDropdownSettings } from 'features/nodes/types/workflow';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiPlusBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const overlayscrollbarsOptions = getOverlayScrollbarsParams({}).options;
|
||||
|
||||
export const NodeFieldElementStringDropdownSettings = memo(
|
||||
({ id, settings }: { id: string; settings: NodeFieldStringDropdownSettings }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onRemoveOption = useCallback(
|
||||
(index: number) => {
|
||||
const options = [...settings.options];
|
||||
options.splice(index, 1);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
|
||||
},
|
||||
[settings, dispatch, id]
|
||||
);
|
||||
|
||||
const onChangeOptionValue = useCallback(
|
||||
(index: number, value: string) => {
|
||||
if (!settings.options[index]) {
|
||||
return;
|
||||
}
|
||||
const option = { ...settings.options[index], value };
|
||||
const options = [...settings.options];
|
||||
options[index] = option;
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
|
||||
},
|
||||
[dispatch, id, settings]
|
||||
);
|
||||
|
||||
const onChangeOptionLabel = useCallback(
|
||||
(index: number, label: string) => {
|
||||
if (!settings.options[index]) {
|
||||
return;
|
||||
}
|
||||
const option = { ...settings.options[index], label };
|
||||
const options = [...settings.options];
|
||||
options[index] = option;
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
|
||||
},
|
||||
[dispatch, id, settings]
|
||||
);
|
||||
|
||||
const onAddOption = useCallback(() => {
|
||||
const options = [...settings.options, getDefaultStringOption()];
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
|
||||
}, [dispatch, id, settings]);
|
||||
|
||||
const onResetOptions = useCallback(() => {
|
||||
const options = [getDefaultStringOption()];
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: { ...settings, options } } }));
|
||||
}, [dispatch, id, settings]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className={NO_DRAG_CLASS}
|
||||
position="relative"
|
||||
w="full"
|
||||
h="auto"
|
||||
maxH={64}
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
borderRadius="base"
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
>
|
||||
<ButtonGroup isAttached={false} w="full">
|
||||
<Button onClick={onAddOption} variant="ghost" flex={1} leftIcon={<PiPlusBold />}>
|
||||
{t('workflows.builder.addOption')}
|
||||
</Button>
|
||||
<Button onClick={onResetOptions} variant="ghost" flex={1} leftIcon={<PiArrowCounterClockwiseBold />}>
|
||||
{t('workflows.builder.resetOptions')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{settings.options.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<OverlayScrollbarsComponent
|
||||
className={NO_WHEEL_CLASS}
|
||||
defer
|
||||
style={overlayScrollbarsStyles}
|
||||
options={overlayscrollbarsOptions}
|
||||
>
|
||||
<Grid gap={1} gridTemplateColumns="auto 1fr 1fr auto" gridTemplateRows="auto 1fr" alignItems="center">
|
||||
<GridItem minW={8}>
|
||||
<Text textAlign="center" variant="subtext">
|
||||
#
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text textAlign="center" variant="subtext">
|
||||
{t('common.label')}
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text textAlign="center" variant="subtext">
|
||||
{t('common.value')}
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem />
|
||||
{settings.options.map(({ value, label }, index) => (
|
||||
<ListItemContent
|
||||
key={index}
|
||||
value={value}
|
||||
label={label}
|
||||
index={index}
|
||||
onRemoveOption={onRemoveOption}
|
||||
onChangeOptionValue={onChangeOptionValue}
|
||||
onChangeOptionLabel={onChangeOptionLabel}
|
||||
isRemoveDisabled={settings.options.length <= 1}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NodeFieldElementStringDropdownSettings.displayName = 'NodeFieldElementStringDropdownSettings';
|
||||
|
||||
type ListItemContentProps = {
|
||||
value: string;
|
||||
label: string;
|
||||
index: number;
|
||||
onRemoveOption: (index: number) => void;
|
||||
onChangeOptionValue: (index: number, value: string) => void;
|
||||
onChangeOptionLabel: (index: number, label: string) => void;
|
||||
isRemoveDisabled: boolean;
|
||||
};
|
||||
|
||||
const ListItemContent = memo(
|
||||
({
|
||||
value,
|
||||
label,
|
||||
index,
|
||||
onRemoveOption,
|
||||
onChangeOptionValue,
|
||||
onChangeOptionLabel,
|
||||
isRemoveDisabled,
|
||||
}: ListItemContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRemove = useCallback(() => {
|
||||
onRemoveOption(index);
|
||||
}, [index, onRemoveOption]);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeOptionValue(index, e.target.value);
|
||||
},
|
||||
[index, onChangeOptionValue]
|
||||
);
|
||||
|
||||
const onChangeLabel = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeOptionLabel(index, e.target.value);
|
||||
},
|
||||
[index, onChangeOptionLabel]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridItem>
|
||||
<Text variant="subtext" textAlign="center">
|
||||
{index + 1}.
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Input size="sm" resize="none" placeholder="label" value={label} onChange={onChangeLabel} />
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Input size="sm" resize="none" placeholder="value" value={value} onChange={onChangeValue} />
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
size="sm"
|
||||
variant="link"
|
||||
minW={8}
|
||||
minH={8}
|
||||
onClick={onClickRemove}
|
||||
isDisabled={isRemoveDisabled}
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('common.delete')}
|
||||
/>
|
||||
</GridItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
ListItemContent.displayName = 'ListItemContent';
|
||||
@@ -1,36 +1,55 @@
|
||||
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { type NodeFieldStringSettings, zStringComponent } from 'features/nodes/types/workflow';
|
||||
import { getDefaultStringOption, type NodeFieldStringSettings, zStringComponent } from 'features/nodes/types/workflow';
|
||||
import { omit } from 'lodash-es';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NodeFieldElementStringSettings = memo(
|
||||
({ id, config }: { id: string; config: NodeFieldStringSettings }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
type Props = {
|
||||
id: string;
|
||||
settings: NodeFieldStringSettings;
|
||||
};
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldStringSettings = {
|
||||
...config,
|
||||
component: zStringComponent.parse(e.target.value),
|
||||
export const NodeFieldElementStringSettings = memo(({ id, settings }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const component = zStringComponent.parse(e.target.value);
|
||||
if (component === settings.component) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
if (component === 'dropdown') {
|
||||
// if the component is changing to dropdown, we need to set the options
|
||||
const newSettings: NodeFieldStringSettings = {
|
||||
...settings,
|
||||
component,
|
||||
options: [getDefaultStringOption()],
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
);
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
return;
|
||||
} else {
|
||||
// if the component is changing from dropdown, we need to remove the options
|
||||
const newSettings: NodeFieldStringSettings = omit({ ...settings, component }, 'options');
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}
|
||||
},
|
||||
[settings, dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="input">{t('workflows.builder.singleLine')}</option>
|
||||
<option value="textarea">{t('workflows.builder.multiLine')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={settings.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="input">{t('workflows.builder.singleLine')}</option>
|
||||
<option value="textarea">{t('workflows.builder.multiLine')}</option>
|
||||
<option value="dropdown">{t('workflows.builder.dropdown')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
NodeFieldElementStringSettings.displayName = 'NodeFieldElementStringSettings';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
|
||||
import { linkifyOptions, linkifySx } from 'common/components/linkify';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
|
||||
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
|
||||
import { useInputFieldTemplateOrThrow, useInputFieldTemplateSafe } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
|
||||
import Linkify from 'linkify-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
'&[data-parent-layout="column"]': {
|
||||
@@ -25,16 +27,23 @@ const sx: SystemStyleObject = {
|
||||
},
|
||||
};
|
||||
|
||||
const useFormatFallbackLabel = () => {
|
||||
const { t } = useTranslation();
|
||||
const formatLabel = useCallback((name: string) => t('nodes.unknownFieldEditWorkflowToFix_withName', { name }), [t]);
|
||||
return formatLabel;
|
||||
};
|
||||
|
||||
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const containerCtx = useContainerContext();
|
||||
const formatFallbackLabel = useFormatFallbackLabel();
|
||||
|
||||
const _description = useMemo(
|
||||
() => description || fieldTemplate.description,
|
||||
[description, fieldTemplate.description]
|
||||
() => description || fieldTemplate?.description || '',
|
||||
[description, fieldTemplate?.description]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -45,22 +54,45 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
|
||||
data-parent-layout={containerCtx.layout}
|
||||
data-with-description={showDescription && !!_description}
|
||||
>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabel el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && _description && (
|
||||
<FormHelperText sx={linkifySx}>
|
||||
<Linkify options={linkifyOptions}>{_description}</Linkify>
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
<InputFieldGate
|
||||
nodeId={el.data.fieldIdentifier.nodeId}
|
||||
fieldName={el.data.fieldIdentifier.fieldName}
|
||||
formatLabel={formatFallbackLabel}
|
||||
>
|
||||
<NodeFieldElementViewModeContent el={el} />
|
||||
</InputFieldGate>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode';
|
||||
|
||||
const NodeFieldElementViewModeContent = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
|
||||
const _description = useMemo(
|
||||
() => description || fieldTemplate.description,
|
||||
[description, fieldTemplate.description]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabel el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && _description && (
|
||||
<FormHelperText sx={linkifySx}>
|
||||
<Linkify options={linkifyOptions}>{_description}</Linkify>
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
NodeFieldElementViewModeContent.displayName = 'NodeFieldElementViewModeContent';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { formElementAdded, selectFormRootElementId } from 'features/nodes/store/workflowSlice';
|
||||
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { useCallback } from 'react';
|
||||
@@ -8,7 +8,7 @@ import { useCallback } from 'react';
|
||||
export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const rootElementId = useAppSelector(selectFormRootElementId);
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const field = useInputFieldInstance(nodeId, fieldName);
|
||||
|
||||
const addNodeFieldToRoot = useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
@@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
|
||||
export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const selectIsChanged = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldDescription = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return '';
|
||||
}
|
||||
return node?.data.inputs[fieldName]?.description ?? '';
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const notes = useAppSelector(selector);
|
||||
return notes;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Gets the user-defined description of an input field for a given node.
|
||||
*
|
||||
* If the node doesn't exist or is not an invocation node, an empty string is returned.
|
||||
*
|
||||
* @param nodeId The ID of the node
|
||||
* @param fieldName The name of the field
|
||||
*/
|
||||
export const useInputFieldDescriptionSafe = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
selectNodesSlice,
|
||||
(nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.description ?? ''
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const description = useAppSelector(selector);
|
||||
return description;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldLabel = (nodeId: string, fieldName: string): string => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? '';
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const label = useAppSelector(selector);
|
||||
|
||||
return label;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Gets the user-defined label of an input field for a given node.
|
||||
*
|
||||
* If the node doesn't exist or is not an invocation node, an empty string is returned.
|
||||
*
|
||||
* @param nodeId The ID of the node
|
||||
* @param fieldName The name of the field
|
||||
*/
|
||||
export const useInputFieldLabelSafe = (nodeId: string, fieldName: string): string => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? ''),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const label = useAppSelector(selector);
|
||||
|
||||
return label;
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldName = (nodeId: string, fieldName: string) => {
|
||||
export const useInputFieldNameSafe = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selector = useMemo(
|
||||
@@ -3,7 +3,16 @@ import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
|
||||
/**
|
||||
* Returns the template for a specific input field of a node.
|
||||
*
|
||||
* **Note:** This hook will throw an error if the template for the input field is not found.
|
||||
*
|
||||
* @param nodeId - The ID of the node.
|
||||
* @param fieldName - The name of the input field.
|
||||
* @throws Will throw an error if the template for the input field is not found.
|
||||
*/
|
||||
export const useInputFieldTemplateOrThrow = (nodeId: string, fieldName: string): FieldInputTemplate => {
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const fieldTemplate = useMemo(() => {
|
||||
const _fieldTemplate = template.inputs[fieldName];
|
||||
@@ -12,3 +21,17 @@ export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldI
|
||||
}, [fieldName, template.inputs]);
|
||||
return fieldTemplate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the template for a specific input field of a node.
|
||||
*
|
||||
* **Note:** This function is a safe version of `useInputFieldTemplate` and will not throw an error if the template is not found.
|
||||
*
|
||||
* @param nodeId - The ID of the node.
|
||||
* @param fieldName - The name of the input field.
|
||||
*/
|
||||
export const useInputFieldTemplateSafe = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
|
||||
return fieldTemplate;
|
||||
};
|
||||
|
||||
@@ -91,14 +91,37 @@ const zNodeFieldIntegerSettings = z.object({
|
||||
export type NodeFieldIntegerSettings = z.infer<typeof zNodeFieldIntegerSettings>;
|
||||
export const getIntegerFieldSettingsDefaults = (): NodeFieldIntegerSettings => zNodeFieldIntegerSettings.parse({});
|
||||
|
||||
export const zStringComponent = z.enum(['input', 'textarea']);
|
||||
const zStringOption = z
|
||||
.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
.default({ label: '', value: '' });
|
||||
type StringOption = z.infer<typeof zStringOption>;
|
||||
export const getDefaultStringOption = (): StringOption => ({ label: '', value: '' });
|
||||
export const zStringComponent = z.enum(['input', 'textarea', 'dropdown']);
|
||||
const STRING_FIELD_CONFIG_TYPE = 'string-field-config';
|
||||
const zNodeFieldStringSettings = z.object({
|
||||
const zNodeFieldStringInputSettings = z.object({
|
||||
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
|
||||
component: zStringComponent.default('input'),
|
||||
component: z.literal('input').default('input'),
|
||||
});
|
||||
const zNodeFieldStringTextareaSettings = z.object({
|
||||
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
|
||||
component: z.literal('textarea').default('textarea'),
|
||||
});
|
||||
const zNodeFieldStringDropdownSettings = z.object({
|
||||
type: z.literal(STRING_FIELD_CONFIG_TYPE).default(STRING_FIELD_CONFIG_TYPE),
|
||||
component: z.literal('dropdown').default('dropdown'),
|
||||
options: z.array(zStringOption),
|
||||
});
|
||||
export type NodeFieldStringDropdownSettings = z.infer<typeof zNodeFieldStringDropdownSettings>;
|
||||
const zNodeFieldStringSettings = z.union([
|
||||
zNodeFieldStringInputSettings,
|
||||
zNodeFieldStringTextareaSettings,
|
||||
zNodeFieldStringDropdownSettings,
|
||||
]);
|
||||
export type NodeFieldStringSettings = z.infer<typeof zNodeFieldStringSettings>;
|
||||
export const getStringFieldSettingsDefaults = (): NodeFieldStringSettings => zNodeFieldStringSettings.parse({});
|
||||
export const getStringFieldSettingsDefaults = (): NodeFieldStringSettings => zNodeFieldStringInputSettings.parse({});
|
||||
|
||||
const zNodeFieldData = z.object({
|
||||
fieldIdentifier: zFieldIdentifier,
|
||||
|
||||
@@ -4,9 +4,8 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { CanvasControlLayerState, Rect } from 'features/controlLayers/store/types';
|
||||
import { getControlLayerWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import type { ImageDTO, Invocation } from 'services/api/types';
|
||||
import type { ImageDTO, Invocation, MainModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('system');
|
||||
@@ -17,7 +16,7 @@ type AddControlNetsArg = {
|
||||
g: Graph;
|
||||
rect: Rect;
|
||||
collector: Invocation<'collect'>;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
};
|
||||
|
||||
type AddControlNetsResult = {
|
||||
@@ -66,7 +65,7 @@ type AddT2IAdaptersArg = {
|
||||
g: Graph;
|
||||
rect: Rect;
|
||||
collector: Invocation<'collect'>;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
};
|
||||
|
||||
type AddT2IAdaptersResult = {
|
||||
@@ -114,7 +113,7 @@ type AddControlLoRAArg = {
|
||||
entities: CanvasControlLayerState[];
|
||||
g: Graph;
|
||||
rect: Rect;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
denoise: Invocation<'flux_denoise'>;
|
||||
};
|
||||
|
||||
@@ -129,9 +128,9 @@ export const addControlLoRA = async ({ manager, entities, g, rect, model, denois
|
||||
// No valid control LoRA found
|
||||
return;
|
||||
}
|
||||
if (validControlLayers.length > 1) {
|
||||
throw new Error('Cannot add more than one FLUX control LoRA.');
|
||||
}
|
||||
|
||||
assert(model.variant !== 'inpaint', 'FLUX Control LoRA is not compatible with FLUX Fill.');
|
||||
assert(validControlLayers.length <= 1, 'Cannot add more than one FLUX control LoRA.');
|
||||
|
||||
const getImageDTOResult = await withResultAsync(() => {
|
||||
const adapter = manager.adapters.controlLayers.get(validControlLayer.id);
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { Dimensions } from 'features/controlLayers/store/types';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
|
||||
type AddFLUXFillArg = {
|
||||
state: RootState;
|
||||
g: Graph;
|
||||
manager: CanvasManager;
|
||||
l2i: Invocation<'flux_vae_decode'>;
|
||||
denoise: Invocation<'flux_denoise'>;
|
||||
originalSize: Dimensions;
|
||||
scaledSize: Dimensions;
|
||||
};
|
||||
|
||||
export const addFLUXFill = async ({
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
denoise,
|
||||
originalSize,
|
||||
scaledSize,
|
||||
}: AddFLUXFillArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
|
||||
// FLUX Fill always fully denoises
|
||||
denoise.denoising_start = 0;
|
||||
denoise.denoising_end = 1;
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
const canvasSettings = selectCanvasSettingsSlice(state);
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
|
||||
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const fluxFill = g.addNode({ type: 'flux_fill', id: getPrefixedId('flux_fill') });
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
// Scale before processing requires some resizing
|
||||
|
||||
// Combine the inpaint mask and the initial image's alpha channel into a single mask
|
||||
const maskAlphaToMask = g.addNode({
|
||||
id: getPrefixedId('alpha_to_mask'),
|
||||
type: 'tomask',
|
||||
image: { image_name: maskImage.image_name },
|
||||
invert: !canvasSettings.preserveMask,
|
||||
});
|
||||
const initialImageAlphaToMask = g.addNode({
|
||||
id: getPrefixedId('image_alpha_to_mask'),
|
||||
type: 'tomask',
|
||||
image: { image_name: initialImage.image_name },
|
||||
});
|
||||
const maskCombine = g.addNode({
|
||||
id: getPrefixedId('mask_combine'),
|
||||
type: 'mask_combine',
|
||||
});
|
||||
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
|
||||
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
|
||||
|
||||
// Resize the combined and initial image to the scaled size
|
||||
const resizeInputMaskToScaledSize = g.addNode({
|
||||
id: getPrefixedId('resize_mask_to_scaled_size'),
|
||||
type: 'img_resize',
|
||||
...scaledSize,
|
||||
});
|
||||
g.addEdge(maskCombine, 'image', resizeInputMaskToScaledSize, 'image');
|
||||
|
||||
const alphaMaskToTensorMask = g.addNode({
|
||||
type: 'image_mask_to_tensor',
|
||||
id: getPrefixedId('image_mask_to_tensor'),
|
||||
});
|
||||
g.addEdge(resizeInputMaskToScaledSize, 'image', alphaMaskToTensorMask, 'image');
|
||||
g.addEdge(alphaMaskToTensorMask, 'mask', fluxFill, 'mask');
|
||||
|
||||
// Resize the initial image to the scaled size and add to the FLUX Fill node
|
||||
const resizeInputImageToScaledSize = g.addNode({
|
||||
id: getPrefixedId('resize_image_to_scaled_size'),
|
||||
type: 'img_resize',
|
||||
image: { image_name: initialImage.image_name },
|
||||
...scaledSize,
|
||||
});
|
||||
g.addEdge(resizeInputImageToScaledSize, 'image', fluxFill, 'image');
|
||||
|
||||
// Provide the FLUX Fill conditioning w/ image and mask to the denoise node
|
||||
g.addEdge(fluxFill, 'fill_cond', denoise, 'fill_conditioning');
|
||||
|
||||
// Resize the output image back to the original size
|
||||
const resizeOutputImageToOriginalSize = g.addNode({
|
||||
id: getPrefixedId('resize_image_to_original_size'),
|
||||
type: 'img_resize',
|
||||
...originalSize,
|
||||
});
|
||||
// After denoising, resize the image and mask back to original size
|
||||
g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image');
|
||||
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
g.addEdge(maskCombine, 'image', expandMask, 'mask');
|
||||
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
} else {
|
||||
// Combine the inpaint mask and the initial image's alpha channel into a single mask
|
||||
const maskAlphaToMask = g.addNode({
|
||||
id: getPrefixedId('alpha_to_mask'),
|
||||
type: 'tomask',
|
||||
image: { image_name: maskImage.image_name },
|
||||
invert: !canvasSettings.preserveMask,
|
||||
});
|
||||
const initialImageAlphaToMask = g.addNode({
|
||||
id: getPrefixedId('image_alpha_to_mask'),
|
||||
type: 'tomask',
|
||||
image: { image_name: initialImage.image_name },
|
||||
});
|
||||
const maskCombine = g.addNode({
|
||||
id: getPrefixedId('mask_combine'),
|
||||
type: 'mask_combine',
|
||||
});
|
||||
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
|
||||
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
|
||||
|
||||
const alphaMaskToTensorMask = g.addNode({
|
||||
type: 'image_mask_to_tensor',
|
||||
id: getPrefixedId('image_mask_to_tensor'),
|
||||
});
|
||||
g.addEdge(maskCombine, 'image', alphaMaskToTensorMask, 'image');
|
||||
g.addEdge(alphaMaskToTensorMask, 'mask', fluxFill, 'mask');
|
||||
|
||||
fluxFill.image = { image_name: initialImage.image_name };
|
||||
g.addEdge(fluxFill, 'fill_cond', denoise, 'fill_conditioning');
|
||||
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
g.addEdge(maskCombine, 'image', expandMask, 'mask');
|
||||
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2,8 +2,7 @@ import type { CanvasReferenceImageState, FLUXReduxConfig } from 'features/contro
|
||||
import { isFLUXReduxConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import type { Invocation, MainModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type AddFLUXReduxResult = {
|
||||
@@ -14,7 +13,7 @@ type AddFLUXReduxArg = {
|
||||
entities: CanvasReferenceImageState[];
|
||||
g: Graph;
|
||||
collector: Invocation<'collect'>;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
};
|
||||
|
||||
export const addFLUXReduxes = ({ entities, g, collector, model }: AddFLUXReduxArg): AddFLUXReduxResult => {
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import type { Invocation, MainModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type AddIPAdaptersResult = {
|
||||
@@ -17,7 +16,7 @@ type AddIPAdaptersArg = {
|
||||
entities: CanvasReferenceImageState[];
|
||||
g: Graph;
|
||||
collector: Invocation<'collect'>;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
};
|
||||
|
||||
export const addIPAdapters = ({ entities, g, collector, model }: AddIPAdaptersArg): AddIPAdaptersResult => {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const addInpaint = async ({
|
||||
scaledSize,
|
||||
denoising_start,
|
||||
fp32,
|
||||
}: AddInpaintArg): Promise<Invocation<'canvas_v2_mask_and_crop' | 'img_resize'>> => {
|
||||
}: AddInpaintArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
@@ -104,10 +104,10 @@ export const addInpaint = async ({
|
||||
edge_radius: params.canvasCoherenceEdgeSize,
|
||||
fp32,
|
||||
});
|
||||
const canvasPasteBack = g.addNode({
|
||||
id: getPrefixedId('canvas_v2_mask_and_crop'),
|
||||
type: 'canvas_v2_mask_and_crop',
|
||||
mask_blur: params.maskBlur,
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
|
||||
// Resize initial image and mask to scaled size, feed into to gradient mask
|
||||
@@ -128,18 +128,31 @@ export const addInpaint = async ({
|
||||
// After denoising, resize the image and mask back to original size
|
||||
g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
|
||||
|
||||
// Finally, paste the generated masked image back onto the original image
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
// After denoising, resize the image and mask back to original size
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
} else {
|
||||
// No scale before processing, much simpler
|
||||
const i2l = g.addNode({
|
||||
@@ -164,11 +177,6 @@ export const addInpaint = async ({
|
||||
fp32,
|
||||
image: { image_name: initialImage.image_name },
|
||||
});
|
||||
const canvasPasteBack = g.addNode({
|
||||
id: getPrefixedId('canvas_v2_mask_and_crop'),
|
||||
type: 'canvas_v2_mask_and_crop',
|
||||
mask_blur: params.maskBlur,
|
||||
});
|
||||
|
||||
g.addEdge(alphaToMask, 'image', createGradientMask, 'mask');
|
||||
g.addEdge(i2l, 'latents', denoise, 'latents');
|
||||
@@ -178,16 +186,35 @@ export const addInpaint = async ({
|
||||
g.addEdge(modelLoader, 'unet', createGradientMask, 'unet');
|
||||
}
|
||||
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
|
||||
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types';
|
||||
export const addNSFWChecker = (
|
||||
g: Graph,
|
||||
imageOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
>
|
||||
): Invocation<'img_nsfw'> => {
|
||||
const nsfw = g.addNode({
|
||||
|
||||
@@ -40,7 +40,7 @@ export const addOutpaint = async ({
|
||||
scaledSize,
|
||||
denoising_start,
|
||||
fp32,
|
||||
}: AddOutpaintArg): Promise<Invocation<'canvas_v2_mask_and_crop' | 'img_resize'>> => {
|
||||
}: AddOutpaintArg): Promise<Invocation<'invokeai_img_blend' | 'apply_mask_to_image'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
@@ -142,29 +142,39 @@ export const addOutpaint = async ({
|
||||
type: 'img_resize',
|
||||
...originalSize,
|
||||
});
|
||||
const canvasPasteBack = g.addNode({
|
||||
id: getPrefixedId('canvas_v2_mask_and_crop'),
|
||||
type: 'canvas_v2_mask_and_crop',
|
||||
mask_blur: params.maskBlur,
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
|
||||
// Resize initial image and mask to scaled size, feed into to gradient mask
|
||||
|
||||
// After denoising, resize the image and mask back to original size
|
||||
g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image');
|
||||
|
||||
// Finally, paste the generated masked image back onto the original image
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
|
||||
g.addEdge(expandMask, 'image', resizeOutputMaskToOriginalSize, 'image');
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
} else {
|
||||
infill.image = { image_name: initialImage.image_name };
|
||||
// No scale before processing, much simpler
|
||||
@@ -197,11 +207,6 @@ export const addOutpaint = async ({
|
||||
fp32,
|
||||
image: { image_name: initialImage.image_name },
|
||||
});
|
||||
const canvasPasteBack = g.addNode({
|
||||
id: getPrefixedId('canvas_v2_mask_and_crop'),
|
||||
type: 'canvas_v2_mask_and_crop',
|
||||
mask_blur: params.maskBlur,
|
||||
});
|
||||
g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1');
|
||||
g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2');
|
||||
g.addEdge(maskCombine, 'image', createGradientMask, 'mask');
|
||||
@@ -214,15 +219,35 @@ export const addOutpaint = async ({
|
||||
}
|
||||
|
||||
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
const expandMask = g.addNode({
|
||||
type: 'expand_mask_with_fade',
|
||||
id: getPrefixedId('expand_mask_with_fade'),
|
||||
fade_size_px: params.maskBlur,
|
||||
});
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask');
|
||||
|
||||
// Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending
|
||||
// to canvas but not outputting only masked regions
|
||||
if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
const imageLayerBlend = g.addNode({
|
||||
type: 'invokeai_img_blend',
|
||||
id: getPrefixedId('image_layer_blend'),
|
||||
layer_base: { image_name: initialImage.image_name },
|
||||
});
|
||||
g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper');
|
||||
g.addEdge(expandMask, 'image', imageLayerBlend, 'mask');
|
||||
return imageLayerBlend;
|
||||
} else {
|
||||
// Otherwise, just apply the mask
|
||||
const applyMaskToImage = g.addNode({
|
||||
type: 'apply_mask_to_image',
|
||||
id: getPrefixedId('apply_mask_to_image'),
|
||||
invert_mask: true,
|
||||
});
|
||||
g.addEdge(expandMask, 'image', applyMaskToImage, 'mask');
|
||||
g.addEdge(l2i, 'image', applyMaskToImage, 'image');
|
||||
return applyMaskToImage;
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,9 +6,8 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types';
|
||||
import { getRegionalGuidanceWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import type { Invocation, MainModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('system');
|
||||
@@ -26,7 +25,7 @@ type AddRegionsArg = {
|
||||
regions: CanvasRegionalGuidanceState[];
|
||||
g: Graph;
|
||||
bbox: Rect;
|
||||
model: ParameterModel;
|
||||
model: MainModelConfig;
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'>;
|
||||
negCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'> | null;
|
||||
posCondCollect: Invocation<'collect'>;
|
||||
|
||||
@@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types';
|
||||
export const addWatermarker = (
|
||||
g: Graph,
|
||||
imageOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
>
|
||||
): Invocation<'img_watermark'> => {
|
||||
const watermark = g.addNode({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import { addFLUXFill } from 'features/nodes/util/graph/generation/addFLUXFill';
|
||||
import { addFLUXLoRAs } from 'features/nodes/util/graph/generation/addFLUXLoRAs';
|
||||
import { addFLUXReduxes } from 'features/nodes/util/graph/generation/addFLUXRedux';
|
||||
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
getPresetModifiedPrompts,
|
||||
getSizes,
|
||||
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { t } from 'i18next';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -47,9 +48,10 @@ export const buildFLUXGraph = async (
|
||||
|
||||
const { originalSize, scaledSize } = getSizes(bbox);
|
||||
|
||||
const model = selectMainModelConfig(state);
|
||||
|
||||
const {
|
||||
model,
|
||||
guidance,
|
||||
guidance: baseGuidance,
|
||||
seed,
|
||||
steps,
|
||||
fluxVAE,
|
||||
@@ -60,10 +62,34 @@ export const buildFLUXGraph = async (
|
||||
} = params;
|
||||
|
||||
assert(model, 'No model found in state');
|
||||
assert(model.base === 'flux', 'Model is not a FLUX model');
|
||||
assert(t5EncoderModel, 'No T5 Encoder model found in state');
|
||||
assert(clipEmbedModel, 'No CLIP Embed model found in state');
|
||||
assert(fluxVAE, 'No FLUX VAE model found in state');
|
||||
|
||||
const isFLUXFill = model.variant === 'inpaint';
|
||||
let guidance = baseGuidance;
|
||||
if (isFLUXFill) {
|
||||
// FLUX Fill doesn't work with Text to Image or Image to Image generation modes. Well, technically, it does, but
|
||||
// the outputs are garbagio.
|
||||
//
|
||||
// Unfortunately, we do not know the generation mode until the user clicks Invoke, so this is the first place we
|
||||
// can check for this incompatibility.
|
||||
//
|
||||
// We are opting to fail loudly instead of produce garbage images, hence this being an assert.
|
||||
//
|
||||
// The message in this assert will be shown in a toast to the user, so we are using the translation system for it.
|
||||
//
|
||||
// The other asserts above are just for sanity & type check and should never be hit, so they do not have
|
||||
// translations.
|
||||
assert(generationMode === 'inpaint' || generationMode === 'outpaint', t('toast.fluxFillIncompatibleWithT2IAndI2I'));
|
||||
|
||||
// FLUX Fill wants much higher guidance values than normal FLUX - silently "fix" the value for the user.
|
||||
// TODO(psyche): Figure out a way to alert the user that this is happening - maybe return warnings from the graph
|
||||
// builder and toast them?
|
||||
guidance = 30;
|
||||
}
|
||||
|
||||
const { positivePrompt } = getPresetModifiedPrompts(state);
|
||||
|
||||
const g = new Graph(getPrefixedId('flux_graph'));
|
||||
@@ -115,16 +141,13 @@ export const buildFLUXGraph = async (
|
||||
|
||||
addFLUXLoRAs(state, g, denoise, modelLoader, posCond);
|
||||
|
||||
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
|
||||
assert(modelConfig.base === 'flux');
|
||||
|
||||
g.upsertMetadata({
|
||||
generation_mode: 'flux_txt2img',
|
||||
guidance,
|
||||
width: originalSize.width,
|
||||
height: originalSize.height,
|
||||
positive_prompt: positivePrompt,
|
||||
model: Graph.getModelMetadataField(modelConfig),
|
||||
model: Graph.getModelMetadataField(model),
|
||||
seed,
|
||||
steps,
|
||||
vae: fluxVAE,
|
||||
@@ -143,10 +166,27 @@ export const buildFLUXGraph = async (
|
||||
}
|
||||
|
||||
let canvasOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
> = l2i;
|
||||
|
||||
if (generationMode === 'txt2img') {
|
||||
if (isFLUXFill) {
|
||||
canvasOutput = await addFLUXFill({
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
denoise,
|
||||
originalSize,
|
||||
scaledSize,
|
||||
});
|
||||
} else if (generationMode === 'txt2img') {
|
||||
canvasOutput = addTextToImage({ g, l2i, originalSize, scaledSize });
|
||||
} else if (generationMode === 'img2img') {
|
||||
canvasOutput = await addImageToImage({
|
||||
@@ -206,7 +246,7 @@ export const buildFLUXGraph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
collector: controlNetCollector,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
if (controlNetResult.addedControlNets > 0) {
|
||||
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
|
||||
@@ -220,7 +260,7 @@ export const buildFLUXGraph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
denoise,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
|
||||
const ipAdapterCollect = g.addNode({
|
||||
@@ -231,7 +271,7 @@ export const buildFLUXGraph = async (
|
||||
entities: canvas.referenceImages.entities,
|
||||
g,
|
||||
collector: ipAdapterCollect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
|
||||
const fluxReduxCollect = g.addNode({
|
||||
@@ -242,7 +282,7 @@ export const buildFLUXGraph = async (
|
||||
entities: canvas.referenceImages.entities,
|
||||
g,
|
||||
collector: fluxReduxCollect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
|
||||
const regionsResult = await addRegions({
|
||||
@@ -250,7 +290,7 @@ export const buildFLUXGraph = async (
|
||||
regions: canvas.regionalGuidance.entities,
|
||||
g,
|
||||
bbox: canvas.bbox.rect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
posCond,
|
||||
negCond: null,
|
||||
posCondCollect,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
|
||||
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
|
||||
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
|
||||
@@ -24,8 +23,8 @@ import {
|
||||
getPresetModifiedPrompts,
|
||||
getSizes,
|
||||
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -45,9 +44,9 @@ export const buildSD1Graph = async (
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
const model = selectMainModelConfig(state);
|
||||
|
||||
const {
|
||||
model,
|
||||
cfgScale: cfg_scale,
|
||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||
scheduler,
|
||||
@@ -137,8 +136,7 @@ export const buildSD1Graph = async (
|
||||
g.addEdge(noise, 'noise', denoise, 'noise');
|
||||
g.addEdge(denoise, 'latents', l2i, 'latents');
|
||||
|
||||
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
|
||||
assert(modelConfig.base === 'sd-1' || modelConfig.base === 'sd-2');
|
||||
assert(model.base === 'sd-1' || model.base === 'sd-2');
|
||||
|
||||
g.upsertMetadata({
|
||||
generation_mode: 'txt2img',
|
||||
@@ -148,7 +146,7 @@ export const buildSD1Graph = async (
|
||||
height: originalSize.height,
|
||||
positive_prompt: positivePrompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: Graph.getModelMetadataField(modelConfig),
|
||||
model: Graph.getModelMetadataField(model),
|
||||
seed,
|
||||
steps,
|
||||
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
|
||||
@@ -170,7 +168,14 @@ export const buildSD1Graph = async (
|
||||
const denoising_start = 1 - params.img2imgStrength;
|
||||
|
||||
let canvasOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
> = l2i;
|
||||
|
||||
if (generationMode === 'txt2img') {
|
||||
@@ -233,7 +238,7 @@ export const buildSD1Graph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
collector: controlNetCollector,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
if (controlNetResult.addedControlNets > 0) {
|
||||
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
|
||||
@@ -251,7 +256,7 @@ export const buildSD1Graph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
collector: t2iAdapterCollector,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
if (t2iAdapterResult.addedT2IAdapters > 0) {
|
||||
g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
|
||||
@@ -267,7 +272,7 @@ export const buildSD1Graph = async (
|
||||
entities: canvas.referenceImages.entities,
|
||||
g,
|
||||
collector: ipAdapterCollect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
|
||||
const regionsResult = await addRegions({
|
||||
@@ -275,7 +280,7 @@ export const buildSD1Graph = async (
|
||||
regions: canvas.regionalGuidance.entities,
|
||||
g,
|
||||
bbox: canvas.bbox.rect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
posCond,
|
||||
negCond,
|
||||
posCondCollect,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
|
||||
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
|
||||
import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker';
|
||||
@@ -19,8 +18,8 @@ import {
|
||||
getPresetModifiedPrompts,
|
||||
getSizes,
|
||||
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -33,6 +32,10 @@ export const buildSD3Graph = async (
|
||||
const generationMode = await manager.compositor.getGenerationMode();
|
||||
log.debug({ generationMode }, 'Building SD3 graph');
|
||||
|
||||
const model = selectMainModelConfig(state);
|
||||
assert(model, 'No model found in state');
|
||||
assert(model.base === 'sd-3');
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
const canvasSettings = selectCanvasSettingsSlice(state);
|
||||
const canvas = selectCanvasSlice(state);
|
||||
@@ -40,7 +43,6 @@ export const buildSD3Graph = async (
|
||||
const { bbox } = canvas;
|
||||
|
||||
const {
|
||||
model,
|
||||
cfgScale: cfg_scale,
|
||||
seed,
|
||||
steps,
|
||||
@@ -52,8 +54,6 @@ export const buildSD3Graph = async (
|
||||
img2imgStrength,
|
||||
} = params;
|
||||
|
||||
assert(model, 'No model found in state');
|
||||
|
||||
const { originalSize, scaledSize } = getSizes(bbox);
|
||||
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||
|
||||
@@ -107,9 +107,6 @@ export const buildSD3Graph = async (
|
||||
|
||||
g.addEdge(denoise, 'latents', l2i, 'latents');
|
||||
|
||||
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
|
||||
assert(modelConfig.base === 'sd-3');
|
||||
|
||||
g.upsertMetadata({
|
||||
generation_mode: 'sd3_txt2img',
|
||||
cfg_scale,
|
||||
@@ -117,7 +114,7 @@ export const buildSD3Graph = async (
|
||||
height: originalSize.height,
|
||||
positive_prompt: positivePrompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: Graph.getModelMetadataField(modelConfig),
|
||||
model: Graph.getModelMetadataField(model),
|
||||
seed,
|
||||
steps,
|
||||
vae: vae ?? undefined,
|
||||
@@ -135,7 +132,14 @@ export const buildSD3Graph = async (
|
||||
}
|
||||
|
||||
let canvasOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
> = l2i;
|
||||
|
||||
if (generationMode === 'txt2img') {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters';
|
||||
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
|
||||
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
|
||||
@@ -24,8 +23,8 @@ import {
|
||||
getPresetModifiedPrompts,
|
||||
getSizes,
|
||||
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -40,6 +39,10 @@ export const buildSDXLGraph = async (
|
||||
const generationMode = await manager.compositor.getGenerationMode();
|
||||
log.debug({ generationMode }, 'Building SDXL graph');
|
||||
|
||||
const model = selectMainModelConfig(state);
|
||||
assert(model, 'No model found in state');
|
||||
assert(model.base === 'sdxl');
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
const canvasSettings = selectCanvasSettingsSlice(state);
|
||||
const canvas = selectCanvasSlice(state);
|
||||
@@ -47,7 +50,6 @@ export const buildSDXLGraph = async (
|
||||
const { bbox } = canvas;
|
||||
|
||||
const {
|
||||
model,
|
||||
cfgScale: cfg_scale,
|
||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||
scheduler,
|
||||
@@ -136,9 +138,6 @@ export const buildSDXLGraph = async (
|
||||
g.addEdge(noise, 'noise', denoise, 'noise');
|
||||
g.addEdge(denoise, 'latents', l2i, 'latents');
|
||||
|
||||
const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
|
||||
assert(modelConfig.base === 'sdxl');
|
||||
|
||||
g.upsertMetadata({
|
||||
generation_mode: 'sdxl_txt2img',
|
||||
cfg_scale,
|
||||
@@ -147,7 +146,7 @@ export const buildSDXLGraph = async (
|
||||
height: originalSize.height,
|
||||
positive_prompt: positivePrompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: Graph.getModelMetadataField(modelConfig),
|
||||
model: Graph.getModelMetadataField(model),
|
||||
seed,
|
||||
steps,
|
||||
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
|
||||
@@ -175,7 +174,14 @@ export const buildSDXLGraph = async (
|
||||
: 1 - params.img2imgStrength;
|
||||
|
||||
let canvasOutput: Invocation<
|
||||
'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i'
|
||||
| 'l2i'
|
||||
| 'img_nsfw'
|
||||
| 'img_watermark'
|
||||
| 'img_resize'
|
||||
| 'invokeai_img_blend'
|
||||
| 'apply_mask_to_image'
|
||||
| 'flux_vae_decode'
|
||||
| 'sd3_l2i'
|
||||
> = l2i;
|
||||
|
||||
if (generationMode === 'txt2img') {
|
||||
@@ -238,7 +244,7 @@ export const buildSDXLGraph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
collector: controlNetCollector,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
if (controlNetResult.addedControlNets > 0) {
|
||||
g.addEdge(controlNetCollector, 'collection', denoise, 'control');
|
||||
@@ -256,7 +262,7 @@ export const buildSDXLGraph = async (
|
||||
g,
|
||||
rect: canvas.bbox.rect,
|
||||
collector: t2iAdapterCollector,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
if (t2iAdapterResult.addedT2IAdapters > 0) {
|
||||
g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter');
|
||||
@@ -272,7 +278,7 @@ export const buildSDXLGraph = async (
|
||||
entities: canvas.referenceImages.entities,
|
||||
g,
|
||||
collector: ipAdapterCollect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
});
|
||||
|
||||
const regionsResult = await addRegions({
|
||||
@@ -280,7 +286,7 @@ export const buildSDXLGraph = async (
|
||||
regions: canvas.regionalGuidance.entities,
|
||||
g,
|
||||
bbox: canvas.bbox.rect,
|
||||
model: modelConfig,
|
||||
model,
|
||||
posCond,
|
||||
negCond,
|
||||
posCondCollect,
|
||||
|
||||
@@ -39,6 +39,8 @@ import i18n from 'i18next';
|
||||
import { debounce, groupBy, upperFirst } from 'lodash-es';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { useEffect } from 'react';
|
||||
import { selectMainModelConfig } from 'services/api/endpoints/models';
|
||||
import type { MainModelConfig } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
/**
|
||||
@@ -85,8 +87,10 @@ const debouncedUpdateReasons = debounce(
|
||||
store: AppStore
|
||||
) => {
|
||||
if (tab === 'canvas') {
|
||||
const model = selectMainModelConfig(store.getState());
|
||||
const reasons = await getReasonsWhyCannotEnqueueCanvasTab({
|
||||
isConnected,
|
||||
model,
|
||||
canvas,
|
||||
params,
|
||||
dynamicPrompts,
|
||||
@@ -314,6 +318,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: {
|
||||
|
||||
const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
|
||||
isConnected: boolean;
|
||||
model: MainModelConfig | null | undefined;
|
||||
canvas: CanvasState;
|
||||
params: ParamsState;
|
||||
dynamicPrompts: DynamicPromptsState;
|
||||
@@ -325,6 +330,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
|
||||
}) => {
|
||||
const {
|
||||
isConnected,
|
||||
model,
|
||||
canvas,
|
||||
params,
|
||||
dynamicPrompts,
|
||||
@@ -334,7 +340,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
|
||||
canvasIsCompositing,
|
||||
canvasIsSelectingObject,
|
||||
} = arg;
|
||||
const { model, positivePrompt } = params;
|
||||
const { positivePrompt } = params;
|
||||
const reasons: Reason[] = [];
|
||||
|
||||
if (!isConnected) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
|
||||
import { isFluxFillMainModelModelConfig } from 'services/api/types';
|
||||
|
||||
const formLabelProps: FormLabelProps = {
|
||||
minW: '4rem',
|
||||
@@ -83,7 +84,7 @@ export const GenerationSettingsAccordion = memo(() => {
|
||||
{!isFLUX && !isSD3 && !isUpscaling && <ParamScheduler />}
|
||||
{isUpscaling && <ParamUpscaleScheduler />}
|
||||
<ParamSteps />
|
||||
{isFLUX && <ParamGuidance />}
|
||||
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && <ParamGuidance />}
|
||||
{isUpscaling && <ParamUpscaleCFGScale />}
|
||||
{!isFLUX && !isUpscaling && <ParamCFGScale />}
|
||||
</FormControlGroup>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type { EntityState } from '@reduxjs/toolkit';
|
||||
import { createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { createEntityAdapter, createSelector } from '@reduxjs/toolkit';
|
||||
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import queryString from 'query-string';
|
||||
import type { operations, paths } from 'services/api/schema';
|
||||
import type { AnyModelConfig, GetHFTokenStatusResponse, SetHFTokenArg, SetHFTokenResponse } from 'services/api/types';
|
||||
import {
|
||||
type AnyModelConfig,
|
||||
type GetHFTokenStatusResponse,
|
||||
isNonRefinerMainModelConfig,
|
||||
type SetHFTokenArg,
|
||||
type SetHFTokenResponse,
|
||||
} from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
import type { ApiTagDescription } from '..';
|
||||
@@ -313,3 +320,23 @@ export const {
|
||||
} = modelsApi;
|
||||
|
||||
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
|
||||
export const selectMainModelConfig = createSelector(
|
||||
selectModelConfigsQuery,
|
||||
selectParamsSlice,
|
||||
(modelConfigs, { model }) => {
|
||||
if (!modelConfigs.data) {
|
||||
return null;
|
||||
}
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
|
||||
if (!modelConfig) {
|
||||
return null;
|
||||
}
|
||||
if (!isNonRefinerMainModelConfig(modelConfig)) {
|
||||
return null;
|
||||
}
|
||||
return modelConfig;
|
||||
}
|
||||
);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -251,6 +251,10 @@ export const isFluxMainModelModelConfig = (config: AnyModelConfig): config is Ma
|
||||
return config.type === 'main' && config.base === 'flux';
|
||||
};
|
||||
|
||||
export const isFluxFillMainModelModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
|
||||
return config.type === 'main' && config.base === 'flux' && config.variant === 'inpaint';
|
||||
};
|
||||
|
||||
export const isNonSDXLMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
|
||||
return config.type === 'main' && (config.base === 'sd-1' || config.base === 'sd-2');
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.8.1"
|
||||
__version__ = "5.9.0rc1"
|
||||
|
||||
Reference in New Issue
Block a user