diff --git a/invokeai/app/invocations/flux_fill.py b/invokeai/app/invocations/flux_fill.py index f3715cc06a..2602c7cf07 100644 --- a/invokeai/app/invocations/flux_fill.py +++ b/invokeai/app/invocations/flux_fill.py @@ -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", diff --git a/invokeai/app/invocations/flux_lora_loader.py b/invokeai/app/invocations/flux_lora_loader.py index 58d1a92ea6..fe1e399676 100644 --- a/invokeai/app/invocations/flux_lora_loader.py +++ b/invokeai/app/invocations/flux_lora_loader.py @@ -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): diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index ad11492559..2324b97985 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -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 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", diff --git a/invokeai/app/invocations/mask.py b/invokeai/app/invocations/mask.py index 8edcbb1f94..c3456cdf18 100644 --- a/invokeai/app/invocations/mask.py +++ b/invokeai/app/invocations/mask.py @@ -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) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index f6d3194cc3..8889074897 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -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.""" diff --git a/invokeai/backend/model_manager/legacy_probe.py b/invokeai/backend/model_manager/legacy_probe.py index 74d4b4906a..b37136dfd7 100644 --- a/invokeai/backend/model_manager/legacy_probe.py +++ b/invokeai/backend/model_manager/legacy_probe.py @@ -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: diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 976abe2d64..c232ac641a 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -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", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0a8a16d347..3c6e881042 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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": "", "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", diff --git a/invokeai/frontend/web/public/locales/fr.json b/invokeai/frontend/web/public/locales/fr.json index b644f2bed3..1c95e7219b 100644 --- a/invokeai/frontend/web/public/locales/fr.json +++ b/invokeai/frontend/web/public/locales/fr.json @@ -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", diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index fe15b64dbe..c907db61bd 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -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": { diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index bf1f8cee03..99664c519c 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -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}}", diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index a1c434ad68..5df7dcf13b 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -879,7 +879,6 @@ "unableToExtractSchemaNameFromRef": "невозможно извлечь имя схемы из ссылки", "executionStateError": "Ошибка", "prototypeDesc": "Этот вызов является прототипом. Он может претерпевать изменения при обновлении приложения и может быть удален в любой момент.", - "unknownOutput": "Неизвестный вывод: {{name}}", "executionStateCompleted": "Выполнено", "node": "Узел", "workflowAuthor": "Автор", diff --git a/invokeai/frontend/web/public/locales/vi.json b/invokeai/frontend/web/public/locales/vi.json index d76ab4d4f2..4977cae43a 100644 --- a/invokeai/frontend/web/public/locales/vi.json +++ b/invokeai/frontend/web/public/locales/vi.json @@ -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ử Bắt Đầu Làm Quen để 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 Kích Hoạt để 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 Thư Viện Ảnh hoặc chỉnh sửa chúng ở Canvas.", "noModelsInstalled": "Dường như bạn chưa tải model nào cả! Bạn có thể tải xuống các model khởi đầu hoặc nhập vào thêm model.", - "lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo hướng dẫn VRAM Thấp của chúng tôi." + "lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo hướng dẫn VRAM Thấp 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 Kích Hoạt nhằm tạo sinh ảnh. Muốn khám phá thêm workflow? Nhấp vào icon thư mục 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": { diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index 51a6cb2e08..bfcb97ce35 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -908,7 +908,6 @@ "unableToGetWorkflowVersion": "无法获取工作流架构版本", "nodePack": "节点包", "unableToExtractSchemaNameFromRef": "无法从参考中提取架构名", - "unknownOutput": "未知输出:{{name}}", "unknownErrorValidatingWorkflow": "验证工作流时出现未知错误", "collectionFieldType": "{{name}}(合集)", "unknownNodeType": "未知节点类型", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx index d940d00d61..ea61e9e30a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx @@ -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 so we can safely assume that the entity exists. // Should never throw. const entity = selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings'); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts index 6ec86e000c..425dab23ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts @@ -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[] = []; diff --git a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts index 4bd2436c0b..05feddcb77 100644 --- a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts @@ -42,8 +42,7 @@ export class InvalidModelConfigError extends Error { export const fetchModelConfig = async (key: string): Promise => { 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 => const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: ModelType): Promise => { 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}`); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx index c4f1ba380f..3338b008ed 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx @@ -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'), }), diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx index c336456613..cf832ef610 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx @@ -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) => { dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value })); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx index ccc2a2fb85..34b1373853 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx @@ -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); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx index 5f43fc2977..1adee688cb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx @@ -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 ; + // fallback may be null, indicating we should render nothing at all - must check for undefined explicitly + if (fallback !== undefined) { + return fallback; + } + return ( + + ); } 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 ( + + + + {label} + + + + ); + } +); + +Fallback.displayName = 'Fallback'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx index 6fdfe7a144..d25664070e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 1255f3465b..6761130e9b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -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 ; } else { @@ -162,8 +163,10 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) return ; } else if (settings.component === 'textarea') { return ; + } else if (settings.component === 'dropdown') { + return ; } else { - assert>(false, 'Unexpected settings.component'); + assert>(false, 'Unexpected settings'); } } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx index 4c73b3aac7..f1da09d43b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx @@ -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(null); - const label = useInputFieldLabel(nodeId, fieldName); + const label = useInputFieldLabelSafe(nodeId, fieldName); const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName); const { t } = useTranslation(); const isConnected = useInputFieldIsConnected(nodeId, fieldName); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx index f83454ffd9..e456f771aa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx @@ -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); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx deleted file mode 100644 index 615034ea32..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx +++ /dev/null @@ -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 ( - - - - {t('nodes.unknownInput', { name })} - - - - ); -}); - -InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx index 2b50e35706..85b01fdae9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx @@ -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 ; + return ; } return children; }); OutputFieldGate.displayName = 'OutputFieldGate'; + +const Fallback = memo(({ nodeId, fieldName }: Props) => { + const { t } = useTranslation(); + const name = useOutputFieldName(nodeId, fieldName); + + return ( + + + + {t('nodes.unexpectedField_withName', { name })} + + + + ); +}); + +Fallback.displayName = 'Fallback'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx deleted file mode 100644 index 9de792b9e5..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx +++ /dev/null @@ -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 ( - - - - {t('nodes.unknownOutput', { name })} - - - - ); -}); - -OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown.tsx new file mode 100644 index 0000000000..3d4242e7ec --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown.tsx @@ -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 } + > + ) => { + const { value, onChange } = useStringField(props); + + return ( + + ); + } +); + +StringFieldDropdown.displayName = 'StringFieldDropdown'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx index d547428984..0cc17c4bf4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput.tsx @@ -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) => { const { value, onChange } = useStringField(props); + const { t } = useTranslation(); - return ; + return ( + + ); } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx index 1985796470..91abe05202 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea.tsx @@ -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) => { + const { t } = useTranslation(); const { value, onChange } = useStringField(props); return (