mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
commit
ead1b14ee7
@ -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"""
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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"""
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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);
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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);
|
@ -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]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
119
invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts
Normal file
119
invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts
Normal 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 };
|
||||||
|
};
|
@ -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'
|
||||||
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
};
|
1107
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
1107
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
File diff suppressed because one or more lines are too long
@ -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'];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user