feat: updateable workflow nodes (#5102)

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [x] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [x] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [ ] Yes
- [x] No


## Description

[fix(nodes): bump version of nodes post-pydantic
v2](5cb3fdb64c)

This was not done, despite new metadata fields being added to many
nodes.

[feat(ui): add update node
functionality](3f6e8e9d6b)

A workflow's nodes may update itself, if its major version matches the
template's major version.

If the major versions do not match, the user will need to delete and
re-add the node (current behaviour).

The update functionality is not automatic (for now). The logic to update
the node is pretty simple, but I want to ensure it works well first
before doing it automatically when a workflow is loaded.

- New `Details` tab on Workflow Inspector, displays node title, type,
version, and notes
- Button to update the node is displayed on the `Details` tab
- Add hook to determine if a node needs an update, may be updated (i.e.
major versions match), and the callback to update the node in state
- Remove the notes modal from the little info icon
- Modularize the node building logic

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

Probably exist but not sure where.

## QA Instructions, Screenshots, Recordings

Load an old workflow with nodes that need to be updated. Click on each
node that needs updating and click the update button. Workflow should
work.

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->
This commit is contained in:
Millun Atluri 2023-11-16 12:57:01 +11:00 committed by GitHub
commit ead1b14ee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1673 additions and 344 deletions

View File

@ -96,7 +96,7 @@ class ControlOutput(BaseInvocationOutput):
control: ControlField = OutputField(description=FieldDescriptions.control) control: ControlField = OutputField(description=FieldDescriptions.control)
@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.0.0") @invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.0")
class ControlNetInvocation(BaseInvocation): class ControlNetInvocation(BaseInvocation):
"""Collects ControlNet info to pass to other nodes""" """Collects ControlNet info to pass to other nodes"""
@ -173,7 +173,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow):
title="Canny Processor", title="Canny Processor",
tags=["controlnet", "canny"], tags=["controlnet", "canny"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class CannyImageProcessorInvocation(ImageProcessorInvocation): class CannyImageProcessorInvocation(ImageProcessorInvocation):
"""Canny edge detection for ControlNet""" """Canny edge detection for ControlNet"""
@ -196,7 +196,7 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation):
title="HED (softedge) Processor", title="HED (softedge) Processor",
tags=["controlnet", "hed", "softedge"], tags=["controlnet", "hed", "softedge"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class HedImageProcessorInvocation(ImageProcessorInvocation): class HedImageProcessorInvocation(ImageProcessorInvocation):
"""Applies HED edge detection to image""" """Applies HED edge detection to image"""
@ -225,7 +225,7 @@ class HedImageProcessorInvocation(ImageProcessorInvocation):
title="Lineart Processor", title="Lineart Processor",
tags=["controlnet", "lineart"], tags=["controlnet", "lineart"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class LineartImageProcessorInvocation(ImageProcessorInvocation): class LineartImageProcessorInvocation(ImageProcessorInvocation):
"""Applies line art processing to image""" """Applies line art processing to image"""
@ -247,7 +247,7 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation):
title="Lineart Anime Processor", title="Lineart Anime Processor",
tags=["controlnet", "lineart", "anime"], tags=["controlnet", "lineart", "anime"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation):
"""Applies line art anime processing to image""" """Applies line art anime processing to image"""
@ -270,7 +270,7 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation):
title="Openpose Processor", title="Openpose Processor",
tags=["controlnet", "openpose", "pose"], tags=["controlnet", "openpose", "pose"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class OpenposeImageProcessorInvocation(ImageProcessorInvocation): class OpenposeImageProcessorInvocation(ImageProcessorInvocation):
"""Applies Openpose processing to image""" """Applies Openpose processing to image"""
@ -295,7 +295,7 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation):
title="Midas Depth Processor", title="Midas Depth Processor",
tags=["controlnet", "midas"], tags=["controlnet", "midas"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): class MidasDepthImageProcessorInvocation(ImageProcessorInvocation):
"""Applies Midas depth processing to image""" """Applies Midas depth processing to image"""
@ -322,7 +322,7 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation):
title="Normal BAE Processor", title="Normal BAE Processor",
tags=["controlnet"], tags=["controlnet"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): class NormalbaeImageProcessorInvocation(ImageProcessorInvocation):
"""Applies NormalBae processing to image""" """Applies NormalBae processing to image"""
@ -339,7 +339,7 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation):
@invocation( @invocation(
"mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.0.0" "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.1.0"
) )
class MlsdImageProcessorInvocation(ImageProcessorInvocation): class MlsdImageProcessorInvocation(ImageProcessorInvocation):
"""Applies MLSD processing to image""" """Applies MLSD processing to image"""
@ -362,7 +362,7 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation):
@invocation( @invocation(
"pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.0.0" "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.1.0"
) )
class PidiImageProcessorInvocation(ImageProcessorInvocation): class PidiImageProcessorInvocation(ImageProcessorInvocation):
"""Applies PIDI processing to image""" """Applies PIDI processing to image"""
@ -389,7 +389,7 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation):
title="Content Shuffle Processor", title="Content Shuffle Processor",
tags=["controlnet", "contentshuffle"], tags=["controlnet", "contentshuffle"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation):
"""Applies content shuffle processing to image""" """Applies content shuffle processing to image"""
@ -419,7 +419,7 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation):
title="Zoe (Depth) Processor", title="Zoe (Depth) Processor",
tags=["controlnet", "zoe", "depth"], tags=["controlnet", "zoe", "depth"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation):
"""Applies Zoe depth processing to image""" """Applies Zoe depth processing to image"""
@ -435,7 +435,7 @@ class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation):
title="Mediapipe Face Processor", title="Mediapipe Face Processor",
tags=["controlnet", "mediapipe", "face"], tags=["controlnet", "mediapipe", "face"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): class MediapipeFaceProcessorInvocation(ImageProcessorInvocation):
"""Applies mediapipe face processing to image""" """Applies mediapipe face processing to image"""
@ -458,7 +458,7 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation):
title="Leres (Depth) Processor", title="Leres (Depth) Processor",
tags=["controlnet", "leres", "depth"], tags=["controlnet", "leres", "depth"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class LeresImageProcessorInvocation(ImageProcessorInvocation): class LeresImageProcessorInvocation(ImageProcessorInvocation):
"""Applies leres processing to image""" """Applies leres processing to image"""
@ -487,7 +487,7 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation):
title="Tile Resample Processor", title="Tile Resample Processor",
tags=["controlnet", "tile"], tags=["controlnet", "tile"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class TileResamplerProcessorInvocation(ImageProcessorInvocation): class TileResamplerProcessorInvocation(ImageProcessorInvocation):
"""Tile resampler processor""" """Tile resampler processor"""
@ -527,7 +527,7 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation):
title="Segment Anything Processor", title="Segment Anything Processor",
tags=["controlnet", "segmentanything"], tags=["controlnet", "segmentanything"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class SegmentAnythingProcessorInvocation(ImageProcessorInvocation): class SegmentAnythingProcessorInvocation(ImageProcessorInvocation):
"""Applies segment anything processing to image""" """Applies segment anything processing to image"""
@ -569,7 +569,7 @@ class SamDetectorReproducibleColors(SamDetector):
title="Color Map Processor", title="Color Map Processor",
tags=["controlnet"], tags=["controlnet"],
category="controlnet", category="controlnet",
version="1.0.0", version="1.1.0",
) )
class ColorMapImageProcessorInvocation(ImageProcessorInvocation): class ColorMapImageProcessorInvocation(ImageProcessorInvocation):
"""Generates a color map from the provided image""" """Generates a color map from the provided image"""

View File

@ -11,7 +11,7 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego
from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation
@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.0.0") @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.1.0")
class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow): class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Simple inpaint using opencv.""" """Simple inpaint using opencv."""

View File

@ -438,7 +438,7 @@ def get_faces_list(
return all_faces return all_faces
@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.0.2") @invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.1.0")
class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata): class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Bound, extract, and mask a face from an image using MediaPipe detection""" """Bound, extract, and mask a face from an image using MediaPipe detection"""
@ -532,7 +532,7 @@ class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata):
return output return output
@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.0.2") @invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.1.0")
class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata): class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Face mask creation using mediapipe face detection""" """Face mask creation using mediapipe face detection"""
@ -650,7 +650,7 @@ class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation( @invocation(
"face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.0.2" "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.1.0"
) )
class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, WithMetadata): class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Outputs an image with detected face IDs printed on each face. For use with other FaceTools.""" """Outputs an image with detected face IDs printed on each face. For use with other FaceTools."""

View File

@ -36,7 +36,7 @@ class ShowImageInvocation(BaseInvocation):
) )
@invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.0.0") @invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.1.0")
class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Creates a blank image and forwards it to the pipeline""" """Creates a blank image and forwards it to the pipeline"""
@ -66,7 +66,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
) )
@invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.0.0") @invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.1.0")
class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Crops an image to a specified box. The box can be outside of the image.""" """Crops an image to a specified box. The box can be outside of the image."""
@ -100,7 +100,7 @@ class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.1") @invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.1.0")
class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Pastes an image into another image.""" """Pastes an image into another image."""
@ -154,7 +154,7 @@ class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.0.0") @invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.1.0")
class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Extracts the alpha channel of an image as a mask.""" """Extracts the alpha channel of an image as a mask."""
@ -186,7 +186,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.0.0") @invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.1.0")
class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Multiplies two images together using `PIL.ImageChops.multiply()`.""" """Multiplies two images together using `PIL.ImageChops.multiply()`."""
@ -220,7 +220,7 @@ class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata):
IMAGE_CHANNELS = Literal["A", "R", "G", "B"] IMAGE_CHANNELS = Literal["A", "R", "G", "B"]
@invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.0.0") @invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.1.0")
class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Gets a channel from an image.""" """Gets a channel from an image."""
@ -253,7 +253,7 @@ class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata):
IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"]
@invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.0.0") @invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.1.0")
class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Converts an image to a different mode.""" """Converts an image to a different mode."""
@ -283,7 +283,7 @@ class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.0.0") @invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.1.0")
class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Blurs an image""" """Blurs an image"""
@ -338,7 +338,7 @@ PIL_RESAMPLING_MAP = {
} }
@invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.0.0") @invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.1.0")
class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Resizes an image to specific dimensions""" """Resizes an image to specific dimensions"""
@ -375,7 +375,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow):
) )
@invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.0.0") @invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.1.0")
class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Scales an image by a factor""" """Scales an image by a factor"""
@ -417,7 +417,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow):
) )
@invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.0.0") @invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.1.0")
class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Linear interpolation of all pixels of an image""" """Linear interpolation of all pixels of an image"""
@ -451,7 +451,7 @@ class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.0.0") @invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.1.0")
class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Inverse linear interpolation of all pixels of an image""" """Inverse linear interpolation of all pixels of an image"""
@ -485,7 +485,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.0.0") @invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.1.0")
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Add blur to NSFW-flagged images""" """Add blur to NSFW-flagged images"""
@ -532,7 +532,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow):
title="Add Invisible Watermark", title="Add Invisible Watermark",
tags=["image", "watermark"], tags=["image", "watermark"],
category="image", category="image",
version="1.0.0", version="1.1.0",
) )
class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Add an invisible watermark to an image""" """Add an invisible watermark to an image"""
@ -561,7 +561,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow):
) )
@invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.0.0") @invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.1.0")
class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Applies an edge mask to an image""" """Applies an edge mask to an image"""
@ -612,7 +612,7 @@ class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata):
title="Combine Masks", title="Combine Masks",
tags=["image", "mask", "multiply"], tags=["image", "mask", "multiply"],
category="image", category="image",
version="1.0.0", version="1.1.0",
) )
class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`."""
@ -644,7 +644,7 @@ class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.0.0") @invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.1.0")
class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata):
""" """
Shifts the colors of a target image to match the reference image, optionally Shifts the colors of a target image to match the reference image, optionally
@ -755,7 +755,7 @@ class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.0.0") @invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.1.0")
class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Adjusts the Hue of an image.""" """Adjusts the Hue of an image."""
@ -858,7 +858,7 @@ CHANNEL_FORMATS = {
"value", "value",
], ],
category="image", category="image",
version="1.0.0", version="1.1.0",
) )
class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Add or subtract a value from a specific color channel of an image.""" """Add or subtract a value from a specific color channel of an image."""
@ -929,7 +929,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"value", "value",
], ],
category="image", category="image",
version="1.0.0", version="1.1.0",
) )
class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Scale a specific color channel of an image.""" """Scale a specific color channel of an image."""
@ -988,7 +988,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata)
title="Save Image", title="Save Image",
tags=["primitives", "image"], tags=["primitives", "image"],
category="primitives", category="primitives",
version="1.0.1", version="1.1.0",
use_cache=False, use_cache=False,
) )
class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata):

View File

@ -118,7 +118,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int]
return si return si
@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0")
class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Infills transparent areas of an image with a solid color""" """Infills transparent areas of an image with a solid color"""
@ -154,7 +154,7 @@ class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0")
class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Infills transparent areas of an image with tiles of the image""" """Infills transparent areas of an image with tiles of the image"""
@ -192,7 +192,7 @@ class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation( @invocation(
"infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0" "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0"
) )
class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Infills transparent areas of an image using the PatchMatch algorithm""" """Infills transparent areas of an image using the PatchMatch algorithm"""
@ -245,7 +245,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0")
class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Infills transparent areas of an image using the LaMa model""" """Infills transparent areas of an image using the LaMa model"""
@ -274,7 +274,7 @@ class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
) )
@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint") @invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0")
class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Infills transparent areas of an image using OpenCV Inpainting""" """Infills transparent areas of an image using OpenCV Inpainting"""

View File

@ -790,7 +790,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
title="Latents to Image", title="Latents to Image",
tags=["latents", "image", "vae", "l2i"], tags=["latents", "image", "vae", "l2i"],
category="latents", category="latents",
version="1.0.0", version="1.1.0",
) )
class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Generates an image from latents.""" """Generates an image from latents."""

View File

@ -326,7 +326,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation):
title="ONNX Latents to Image", title="ONNX Latents to Image",
tags=["latents", "image", "vae", "onnx"], tags=["latents", "image", "vae", "onnx"],
category="image", category="image",
version="1.0.0", version="1.1.0",
) )
class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
"""Generates an image from latents.""" """Generates an image from latents."""

View File

@ -29,7 +29,7 @@ if choose_torch_device() == torch.device("mps"):
from torch import mps from torch import mps
@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.1.0") @invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.2.0")
class ESRGANInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ESRGANInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Upscales an image using RealESRGAN.""" """Upscales an image using RealESRGAN."""

View File

@ -920,7 +920,10 @@
"unknownTemplate": "Unknown Template", "unknownTemplate": "Unknown Template",
"unkownInvocation": "Unknown Invocation type", "unkownInvocation": "Unknown Invocation type",
"updateNode": "Update Node", "updateNode": "Update Node",
"updateAllNodes": "Update All Nodes",
"updateApp": "Update App", "updateApp": "Update App",
"unableToUpdateNodes_one": "Unable to update {{count}} node",
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
"vaeField": "Vae", "vaeField": "Vae",
"vaeFieldDescription": "Vae submodel.", "vaeFieldDescription": "Vae submodel.",
"vaeModelField": "VAE", "vaeModelField": "VAE",

View File

@ -72,6 +72,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
import { addTabChangedListener } from './listeners/tabChanged'; import { addTabChangedListener } from './listeners/tabChanged';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested'; import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
import { addWorkflowLoadedListener } from './listeners/workflowLoaded'; import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
export const listenerMiddleware = createListenerMiddleware(); export const listenerMiddleware = createListenerMiddleware();
@ -178,6 +179,7 @@ addReceivedOpenAPISchemaListener();
// Workflows // Workflows
addWorkflowLoadedListener(); addWorkflowLoadedListener();
addUpdateAllNodesRequestedListener();
// DND // DND
addImageDroppedListener(); addImageDroppedListener();

View File

@ -0,0 +1,52 @@
import {
getNeedsUpdate,
updateNode,
} from 'features/nodes/hooks/useNodeVersion';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
import { startAppListening } from '..';
import { logger } from 'app/logging/logger';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
export const addUpdateAllNodesRequestedListener = () => {
startAppListening({
actionCreator: updateAllNodesRequested,
effect: (action, { dispatch, getState }) => {
const log = logger('nodes');
const nodes = getState().nodes.nodes;
const templates = getState().nodes.nodeTemplates;
let unableToUpdateCount = 0;
nodes.forEach((node) => {
const template = templates[node.data.type];
const needsUpdate = getNeedsUpdate(node, template);
const updatedNode = updateNode(node, template);
if (!updatedNode) {
if (needsUpdate) {
unableToUpdateCount++;
}
return;
}
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
});
if (unableToUpdateCount) {
log.warn(
`Unable to update ${unableToUpdateCount} nodes. Please report this issue.`
);
dispatch(
addToast(
makeToast({
title: t('nodes.unableToUpdateNodes', {
count: unableToUpdateCount,
}),
})
)
);
}
},
});
};

View File

@ -3,7 +3,7 @@ import { memo } from 'react';
import NodeCollapseButton from '../common/NodeCollapseButton'; import NodeCollapseButton from '../common/NodeCollapseButton';
import NodeTitle from '../common/NodeTitle'; import NodeTitle from '../common/NodeTitle';
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles'; import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
import InvocationNodeNotes from './InvocationNodeNotes'; import InvocationNodeInfoIcon from './InvocationNodeInfoIcon';
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator'; import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
type Props = { type Props = {
@ -34,7 +34,7 @@ const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
<NodeTitle nodeId={nodeId} /> <NodeTitle nodeId={nodeId} />
<Flex alignItems="center"> <Flex alignItems="center">
<InvocationNodeStatusIndicator nodeId={nodeId} /> <InvocationNodeStatusIndicator nodeId={nodeId} />
<InvocationNodeNotes nodeId={nodeId} /> <InvocationNodeInfoIcon nodeId={nodeId} />
</Flex> </Flex>
{!isOpen && <InvocationNodeCollapsedHandles nodeId={nodeId} />} {!isOpen && <InvocationNodeCollapsedHandles nodeId={nodeId} />}
</Flex> </Flex>

View File

@ -1,85 +1,39 @@
import { import { Flex, Icon, Text, Tooltip } from '@chakra-ui/react';
Flex,
Icon,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
Tooltip,
useDisclosure,
} from '@chakra-ui/react';
import { compare } from 'compare-versions'; import { compare } from 'compare-versions';
import { useNodeData } from 'features/nodes/hooks/useNodeData'; import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle'; import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
import { isInvocationNodeData } from 'features/nodes/types/types'; import { isInvocationNodeData } from 'features/nodes/types/types';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { FaInfoCircle } from 'react-icons/fa';
import NotesTextarea from './NotesTextarea';
import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaInfoCircle } from 'react-icons/fa';
interface Props { interface Props {
nodeId: string; nodeId: string;
} }
const InvocationNodeNotes = ({ nodeId }: Props) => { const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { needsUpdate } = useNodeVersion(nodeId);
const label = useNodeLabel(nodeId);
const title = useNodeTemplateTitle(nodeId);
const doVersionsMatch = useDoNodeVersionsMatch(nodeId);
const { t } = useTranslation();
return ( return (
<> <Tooltip
<Tooltip label={<TooltipContent nodeId={nodeId} />}
label={<TooltipContent nodeId={nodeId} />} placement="top"
placement="top" shouldWrapChildren
shouldWrapChildren >
> <Icon
<Flex as={FaInfoCircle}
className="nodrag" sx={{
onClick={onOpen} boxSize: 4,
sx={{ w: 8,
alignItems: 'center', color: needsUpdate ? 'error.400' : 'base.400',
justifyContent: 'center', }}
w: 8, />
h: 8, </Tooltip>
cursor: 'pointer',
}}
>
<Icon
as={FaInfoCircle}
sx={{
boxSize: 4,
w: 8,
color: doVersionsMatch ? 'base.400' : 'error.400',
}}
/>
</Flex>
</Tooltip>
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>{label || title || t('nodes.unknownNode')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<NotesTextarea nodeId={nodeId} />
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
</>
); );
}; };
export default memo(InvocationNodeNotes); export default memo(InvocationNodeInfoIcon);
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
const data = useNodeData(nodeId); const data = useNodeData(nodeId);

View File

@ -3,15 +3,22 @@ import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice'; import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa'; import { FaPlus, FaSync } from 'react-icons/fa';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IAIButton from 'common/components/IAIButton';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
const TopLeftPanel = () => { const TopLeftPanel = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const nodesNeedUpdate = useGetNodesNeedUpdate();
const handleOpenAddNodePopover = useCallback(() => { const handleOpenAddNodePopover = useCallback(() => {
dispatch(addNodePopoverOpened()); dispatch(addNodePopoverOpened());
}, [dispatch]); }, [dispatch]);
const handleClickUpdateNodes = useCallback(() => {
dispatch(updateAllNodesRequested());
}, [dispatch]);
return ( return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}> <Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
@ -21,6 +28,11 @@ const TopLeftPanel = () => {
icon={<FaPlus />} icon={<FaPlus />}
onClick={handleOpenAddNodePopover} onClick={handleOpenAddNodePopover}
/> />
{nodesNeedUpdate && (
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
{t('nodes.updateAllNodes')}
</IAIButton>
)}
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,125 @@
import {
Box,
Flex,
FormControl,
FormLabel,
HStack,
Text,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
import {
InvocationNodeData,
InvocationTemplate,
isInvocationNode,
} from 'features/nodes/types/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa';
import { Node } from 'reactflow';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
import ScrollableContent from '../ScrollableContent';
import EditableNodeTitle from './details/EditableNodeTitle';
const selector = createSelector(
stateSelector,
({ nodes }) => {
const lastSelectedNodeId =
nodes.selectedNodes[nodes.selectedNodes.length - 1];
const lastSelectedNode = nodes.nodes.find(
(node) => node.id === lastSelectedNodeId
);
const lastSelectedNodeTemplate = lastSelectedNode
? nodes.nodeTemplates[lastSelectedNode.data.type]
: undefined;
return {
node: lastSelectedNode,
template: lastSelectedNodeTemplate,
};
},
defaultSelectorOptions
);
const InspectorDetailsTab = () => {
const { node, template } = useAppSelector(selector);
const { t } = useTranslation();
if (!template || !isInvocationNode(node)) {
return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
}
return <Content node={node} template={template} />;
};
export default memo(InspectorDetailsTab);
const Content = (props: {
node: Node<InvocationNodeData>;
template: InvocationTemplate;
}) => {
const { t } = useTranslation();
const { needsUpdate, updateNode } = useNodeVersion(props.node.id);
return (
<Box
sx={{
position: 'relative',
w: 'full',
h: 'full',
}}
>
<ScrollableContent>
<Flex
sx={{
flexDir: 'column',
position: 'relative',
p: 1,
gap: 2,
w: 'full',
}}
>
<EditableNodeTitle nodeId={props.node.data.id} />
<HStack>
<FormControl>
<FormLabel>Node Type</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.template.title}
</Text>
</FormControl>
<Flex
flexDir="row"
alignItems="center"
justifyContent="space-between"
w="full"
>
<FormControl isInvalid={needsUpdate}>
<FormLabel>Node Version</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.node.data.version}
</Text>
</FormControl>
{needsUpdate && (
<IAIIconButton
aria-label={t('nodes.updateNode')}
tooltip={t('nodes.updateNode')}
icon={<FaSync />}
onClick={updateNode}
/>
)}
</Flex>
</HStack>
<NotesTextarea nodeId={props.node.data.id} />
</Flex>
</ScrollableContent>
</Box>
);
};

View File

@ -10,7 +10,7 @@ import { memo } from 'react';
import InspectorDataTab from './InspectorDataTab'; import InspectorDataTab from './InspectorDataTab';
import InspectorOutputsTab from './InspectorOutputsTab'; import InspectorOutputsTab from './InspectorOutputsTab';
import InspectorTemplateTab from './InspectorTemplateTab'; import InspectorTemplateTab from './InspectorTemplateTab';
// import InspectorDetailsTab from './InspectorDetailsTab'; import InspectorDetailsTab from './InspectorDetailsTab';
const InspectorPanel = () => { const InspectorPanel = () => {
return ( return (
@ -30,16 +30,16 @@ const InspectorPanel = () => {
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }} sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
> >
<TabList> <TabList>
{/* <Tab>Details</Tab> */} <Tab>Details</Tab>
<Tab>Outputs</Tab> <Tab>Outputs</Tab>
<Tab>Data</Tab> <Tab>Data</Tab>
<Tab>Template</Tab> <Tab>Template</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
{/* <TabPanel> <TabPanel>
<InspectorDetailsTab /> <InspectorDetailsTab />
</TabPanel> */} </TabPanel>
<TabPanel> <TabPanel>
<InspectorOutputsTab /> <InspectorOutputsTab />
</TabPanel> </TabPanel>

View File

@ -0,0 +1,74 @@
import {
Editable,
EditableInput,
EditablePreview,
Flex,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
title?: string;
};
const EditableNodeTitle = ({ nodeId, title }: Props) => {
const dispatch = useAppDispatch();
const label = useNodeLabel(nodeId);
const templateTitle = useNodeTemplateTitle(nodeId);
const { t } = useTranslation();
const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback(
async (newTitle: string) => {
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')
);
},
[dispatch, nodeId, title, templateTitle, label, t]
);
const handleChange = useCallback((newTitle: string) => {
setLocalTitle(newTitle);
}, []);
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(
label || title || templateTitle || t('nodes.problemSettingTitle')
);
}, [label, templateTitle, title, t]);
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Editable
as={Flex}
value={localTitle}
onChange={handleChange}
onSubmit={handleSubmit}
w="full"
fontWeight={600}
>
<EditablePreview noOfLines={1} />
<EditableInput
className="nodrag"
_focusVisible={{ boxShadow: 'none' }}
/>
</Editable>
</Flex>
);
};
export default memo(EditableNodeTitle);

View File

@ -1,19 +1,10 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { reduce } from 'lodash-es';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Node, useReactFlow } from 'reactflow'; import { Node, useReactFlow } from 'reactflow';
import { AnyInvocationType } from 'services/events/types'; import { AnyInvocationType } from 'services/events/types';
import { v4 as uuidv4 } from 'uuid'; import { buildNodeData } from '../store/util/buildNodeData';
import {
CurrentImageNodeData,
InputFieldValue,
InvocationNodeData,
NotesNodeData,
OutputFieldValue,
} from '../types/types';
import { buildInputFieldValue } from '../util/fieldValueBuilders';
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants'; import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants';
const templatesSelector = createSelector( const templatesSelector = createSelector(
@ -26,14 +17,12 @@ export const SHARED_NODE_PROPERTIES: Partial<Node> = {
}; };
export const useBuildNodeData = () => { export const useBuildNodeData = () => {
const invocationTemplates = useAppSelector(templatesSelector); const nodeTemplates = useAppSelector(templatesSelector);
const flow = useReactFlow(); const flow = useReactFlow();
return useCallback( return useCallback(
(type: AnyInvocationType | 'current_image' | 'notes') => { (type: AnyInvocationType | 'current_image' | 'notes') => {
const nodeId = uuidv4();
let _x = window.innerWidth / 2; let _x = window.innerWidth / 2;
let _y = window.innerHeight / 2; let _y = window.innerHeight / 2;
@ -47,111 +36,15 @@ export const useBuildNodeData = () => {
_y = rect.height / 2 - NODE_WIDTH / 2; _y = rect.height / 2 - NODE_WIDTH / 2;
} }
const { x, y } = flow.project({ const position = flow.project({
x: _x, x: _x,
y: _y, y: _y,
}); });
if (type === 'current_image') { const template = nodeTemplates[type];
const node: Node<CurrentImageNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'current_image',
position: { x: x, y: y },
data: {
id: nodeId,
type: 'current_image',
isOpen: true,
label: 'Current Image',
},
};
return node; return buildNodeData(type, position, template);
}
if (type === 'notes') {
const node: Node<NotesNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'notes',
position: { x: x, y: y },
data: {
id: nodeId,
isOpen: true,
label: 'Notes',
notes: '',
type: 'notes',
},
};
return node;
}
const template = invocationTemplates[type];
if (template === undefined) {
console.error(`Unable to find template ${type}.`);
return;
}
const inputs = reduce(
template.inputs,
(inputsAccumulator, inputTemplate, inputName) => {
const fieldId = uuidv4();
const inputFieldValue: InputFieldValue = buildInputFieldValue(
fieldId,
inputTemplate
);
inputsAccumulator[inputName] = inputFieldValue;
return inputsAccumulator;
},
{} as Record<string, InputFieldValue>
);
const outputs = reduce(
template.outputs,
(outputsAccumulator, outputTemplate, outputName) => {
const fieldId = uuidv4();
const outputFieldValue: OutputFieldValue = {
id: fieldId,
name: outputName,
type: outputTemplate.type,
fieldKind: 'output',
};
outputsAccumulator[outputName] = outputFieldValue;
return outputsAccumulator;
},
{} as Record<string, OutputFieldValue>
);
const invocation: Node<InvocationNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'invocation',
position: { x: x, y: y },
data: {
id: nodeId,
type,
version: template.version,
label: '',
notes: '',
isOpen: true,
embedWorkflow: false,
isIntermediate: type === 'save_image' ? false : true,
inputs,
outputs,
useCache: template.useCache,
},
};
return invocation;
}, },
[invocationTemplates, flow] [nodeTemplates, flow]
); );
}; };

View File

@ -0,0 +1,25 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { getNeedsUpdate } from './useNodeVersion';
const selector = createSelector(
stateSelector,
(state) => {
const nodes = state.nodes.nodes;
const templates = state.nodes.nodeTemplates;
const needsUpdate = nodes.some((node) => {
const template = templates[node.data.type];
return getNeedsUpdate(node, template);
});
return needsUpdate;
},
defaultSelectorOptions
);
export const useGetNodesNeedUpdate = () => {
const getNeedsUpdate = useAppSelector(selector);
return getNeedsUpdate;
};

View File

@ -0,0 +1,27 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useMemo } from 'react';
import { AnyInvocationType } from 'services/events/types';
export const useNodeTemplateByType = (
type: AnyInvocationType | 'current_image' | 'notes'
) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const nodeTemplate = nodes.nodeTemplates[type];
return nodeTemplate;
},
defaultSelectorOptions
),
[type]
);
const nodeTemplate = useAppSelector(selector);
return nodeTemplate;
};

View File

@ -0,0 +1,119 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { satisfies } from 'compare-versions';
import { cloneDeep, defaultsDeep } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { Node } from 'reactflow';
import { AnyInvocationType } from 'services/events/types';
import { nodeReplaced } from '../store/nodesSlice';
import { buildNodeData } from '../store/util/buildNodeData';
import {
InvocationNodeData,
InvocationTemplate,
NodeData,
isInvocationNode,
zParsedSemver,
} from '../types/types';
import { useAppToaster } from 'app/components/Toaster';
import { useTranslation } from 'react-i18next';
export const getNeedsUpdate = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
if (!isInvocationNode(node) || !template) {
return false;
}
return node.data.version !== template.version;
};
export const getMayUpdateNode = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
const needsUpdate = getNeedsUpdate(node, template);
if (
!needsUpdate ||
!isInvocationNode(node) ||
!template ||
!node.data.version
) {
return false;
}
const templateMajor = zParsedSemver.parse(template.version).major;
return satisfies(node.data.version, `^${templateMajor}`);
};
export const updateNode = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
const mayUpdate = getMayUpdateNode(node, template);
if (
!mayUpdate ||
!isInvocationNode(node) ||
!template ||
!node.data.version
) {
return;
}
const defaults = buildNodeData(
node.data.type as AnyInvocationType,
node.position,
template
) as Node<InvocationNodeData>;
const clone = cloneDeep(node);
clone.data.version = template.version;
defaultsDeep(clone, defaults);
return clone;
};
export const useNodeVersion = (nodeId: string) => {
const dispatch = useAppDispatch();
const toast = useAppToaster();
const { t } = useTranslation();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
return { node, nodeTemplate };
},
defaultSelectorOptions
),
[nodeId]
);
const { node, nodeTemplate } = useAppSelector(selector);
const needsUpdate = useMemo(
() => getNeedsUpdate(node, nodeTemplate),
[node, nodeTemplate]
);
const mayUpdate = useMemo(
() => getMayUpdateNode(node, nodeTemplate),
[node, nodeTemplate]
);
const _updateNode = useCallback(() => {
const needsUpdate = getNeedsUpdate(node, nodeTemplate);
const updatedNode = updateNode(node, nodeTemplate);
if (!updatedNode) {
if (needsUpdate) {
toast({ title: t('nodes.unableToUpdateNodes', { count: 1 }) });
}
return;
}
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
}, [dispatch, node, nodeTemplate, t, toast]);
return { needsUpdate, mayUpdate, updateNode: _updateNode };
};

View File

@ -21,3 +21,7 @@ export const isAnyGraphBuilt = isAnyOf(
export const workflowLoadRequested = createAction<Workflow>( export const workflowLoadRequested = createAction<Workflow>(
'nodes/workflowLoadRequested' 'nodes/workflowLoadRequested'
); );
export const updateAllNodesRequested = createAction(
'nodes/updateAllNodesRequested'
);

View File

@ -149,6 +149,18 @@ const nodesSlice = createSlice({
nodesChanged: (state, action: PayloadAction<NodeChange[]>) => { nodesChanged: (state, action: PayloadAction<NodeChange[]>) => {
state.nodes = applyNodeChanges(action.payload, state.nodes); state.nodes = applyNodeChanges(action.payload, state.nodes);
}, },
nodeReplaced: (
state,
action: PayloadAction<{ nodeId: string; node: Node }>
) => {
const nodeIndex = state.nodes.findIndex(
(n) => n.id === action.payload.nodeId
);
if (nodeIndex < 0) {
return;
}
state.nodes[nodeIndex] = action.payload.node;
},
nodeAdded: ( nodeAdded: (
state, state,
action: PayloadAction< action: PayloadAction<
@ -1029,6 +1041,7 @@ export const {
mouseOverFieldChanged, mouseOverFieldChanged,
mouseOverNodeChanged, mouseOverNodeChanged,
nodeAdded, nodeAdded,
nodeReplaced,
nodeEditorReset, nodeEditorReset,
nodeEmbedWorkflowChanged, nodeEmbedWorkflowChanged,
nodeExclusivelySelected, nodeExclusivelySelected,

View File

@ -0,0 +1,127 @@
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import {
CurrentImageNodeData,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
NotesNodeData,
OutputFieldValue,
} from 'features/nodes/types/types';
import { buildInputFieldValue } from 'features/nodes/util/fieldValueBuilders';
import { reduce } from 'lodash-es';
import { Node, XYPosition } from 'reactflow';
import { AnyInvocationType } from 'services/events/types';
import { v4 as uuidv4 } from 'uuid';
export const SHARED_NODE_PROPERTIES: Partial<Node> = {
dragHandle: `.${DRAG_HANDLE_CLASSNAME}`,
};
export const buildNodeData = (
type: AnyInvocationType | 'current_image' | 'notes',
position: XYPosition,
template?: InvocationTemplate
):
| Node<CurrentImageNodeData>
| Node<NotesNodeData>
| Node<InvocationNodeData>
| undefined => {
const nodeId = uuidv4();
if (type === 'current_image') {
const node: Node<CurrentImageNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'current_image',
position,
data: {
id: nodeId,
type: 'current_image',
isOpen: true,
label: 'Current Image',
},
};
return node;
}
if (type === 'notes') {
const node: Node<NotesNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'notes',
position,
data: {
id: nodeId,
isOpen: true,
label: 'Notes',
notes: '',
type: 'notes',
},
};
return node;
}
if (template === undefined) {
console.error(`Unable to find template ${type}.`);
return;
}
const inputs = reduce(
template.inputs,
(inputsAccumulator, inputTemplate, inputName) => {
const fieldId = uuidv4();
const inputFieldValue: InputFieldValue = buildInputFieldValue(
fieldId,
inputTemplate
);
inputsAccumulator[inputName] = inputFieldValue;
return inputsAccumulator;
},
{} as Record<string, InputFieldValue>
);
const outputs = reduce(
template.outputs,
(outputsAccumulator, outputTemplate, outputName) => {
const fieldId = uuidv4();
const outputFieldValue: OutputFieldValue = {
id: fieldId,
name: outputName,
type: outputTemplate.type,
fieldKind: 'output',
};
outputsAccumulator[outputName] = outputFieldValue;
return outputsAccumulator;
},
{} as Record<string, OutputFieldValue>
);
const invocation: Node<InvocationNodeData> = {
...SHARED_NODE_PROPERTIES,
id: nodeId,
type: 'invocation',
position,
data: {
id: nodeId,
type,
version: template.version,
label: '',
notes: '',
isOpen: true,
embedWorkflow: false,
isIntermediate: type === 'save_image' ? false : true,
inputs,
outputs,
useCache: template.useCache,
},
};
return invocation;
};

File diff suppressed because one or more lines are too long

View File

@ -48,9 +48,11 @@ export type OffsetPaginatedResults_ImageDTO_ =
s['OffsetPaginatedResults_ImageDTO_']; s['OffsetPaginatedResults_ImageDTO_'];
// Models // Models
export type ModelType = s['ModelType']; export type ModelType =
s['invokeai__backend__model_management__models__base__ModelType'];
export type SubModelType = s['SubModelType']; export type SubModelType = s['SubModelType'];
export type BaseModelType = s['BaseModelType']; export type BaseModelType =
s['invokeai__backend__model_management__models__base__BaseModelType'];
export type MainModelField = s['MainModelField']; export type MainModelField = s['MainModelField'];
export type OnnxModelField = s['OnnxModelField']; export type OnnxModelField = s['OnnxModelField'];
export type VAEModelField = s['VAEModelField']; export type VAEModelField = s['VAEModelField'];
@ -58,7 +60,7 @@ export type LoRAModelField = s['LoRAModelField'];
export type ControlNetModelField = s['ControlNetModelField']; export type ControlNetModelField = s['ControlNetModelField'];
export type IPAdapterModelField = s['IPAdapterModelField']; export type IPAdapterModelField = s['IPAdapterModelField'];
export type T2IAdapterModelField = s['T2IAdapterModelField']; export type T2IAdapterModelField = s['T2IAdapterModelField'];
export type ModelsList = s['ModelsList']; export type ModelsList = s['invokeai__app__api__routers__models__ModelsList'];
export type ControlField = s['ControlField']; export type ControlField = s['ControlField'];
export type IPAdapterField = s['IPAdapterField']; export type IPAdapterField = s['IPAdapterField'];