mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-17 08:57:57 -05:00
Compare commits
1 Commits
3.7.0
...
maryhipp/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea64649135 |
@@ -18,8 +18,8 @@ ENV INVOKEAI_SRC=/opt/invokeai
|
||||
ENV VIRTUAL_ENV=/opt/venv/invokeai
|
||||
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ARG TORCH_VERSION=2.1.2
|
||||
ARG TORCHVISION_VERSION=0.16.2
|
||||
ARG TORCH_VERSION=2.1.0
|
||||
ARG TORCHVISION_VERSION=0.16
|
||||
ARG GPU_DRIVER=cuda
|
||||
ARG TARGETPLATFORM="linux/amd64"
|
||||
# unused but available
|
||||
@@ -35,7 +35,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then \
|
||||
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cpu"; \
|
||||
elif [ "$GPU_DRIVER" = "rocm" ]; then \
|
||||
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/rocm5.6"; \
|
||||
extra_index_url_arg="--index-url https://download.pytorch.org/whl/rocm5.6"; \
|
||||
else \
|
||||
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu121"; \
|
||||
fi &&\
|
||||
@@ -54,7 +54,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
if [ "$GPU_DRIVER" = "cuda" ] && [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
pip install -e ".[xformers]"; \
|
||||
else \
|
||||
pip install $extra_index_url_arg -e "."; \
|
||||
pip install -e "."; \
|
||||
fi
|
||||
|
||||
# #### Build the Web UI ------------------------------------
|
||||
|
||||
@@ -21,7 +21,7 @@ run() {
|
||||
printf "%s\n" "$build_args"
|
||||
fi
|
||||
|
||||
docker compose build $build_args $service_name
|
||||
docker compose build $build_args
|
||||
unset build_args
|
||||
|
||||
printf "%s\n" "starting service $service_name"
|
||||
|
||||
@@ -230,13 +230,13 @@ manager, please follow these steps:
|
||||
=== "local Webserver"
|
||||
|
||||
```bash
|
||||
invokeai-web
|
||||
invokeai --web
|
||||
```
|
||||
|
||||
=== "Public Webserver"
|
||||
|
||||
```bash
|
||||
invokeai-web --host 0.0.0.0
|
||||
invokeai --web --host 0.0.0.0
|
||||
```
|
||||
|
||||
=== "CLI"
|
||||
@@ -402,4 +402,4 @@ environment variable INVOKEAI_ROOT to point to the installation directory.
|
||||
Note that if you run into problems with the Conda installation, the InvokeAI
|
||||
staff will **not** be able to help you out. Caveat Emptor!
|
||||
|
||||
[dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
|
||||
[dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
|
||||
@@ -154,7 +154,7 @@ class ImageService(ImageServiceABC):
|
||||
self.__invoker.services.logger.error("Image record not found")
|
||||
raise
|
||||
except Exception as e:
|
||||
self.__invoker.services.logger.error("Problem getting image metadata")
|
||||
self.__invoker.services.logger.error("Problem getting image DTO")
|
||||
raise e
|
||||
|
||||
def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]:
|
||||
|
||||
@@ -540,7 +540,7 @@ class Graph(BaseModel):
|
||||
except NodeNotFoundError:
|
||||
return False
|
||||
|
||||
def get_node(self, node_path: str) -> BaseInvocation:
|
||||
def get_node(self, node_path: str) -> InvocationsUnion:
|
||||
"""Gets a node from the graph using a node path."""
|
||||
# Materialized graphs may have nodes at the top level
|
||||
graph, node_id = self._get_graph_and_node(node_path)
|
||||
@@ -891,7 +891,7 @@ class GraphExecutionState(BaseModel):
|
||||
# If next is still none, there's no next node, return None
|
||||
return next_node
|
||||
|
||||
def complete(self, node_id: str, output: BaseInvocationOutput) -> None:
|
||||
def complete(self, node_id: str, output: InvocationOutputsUnion):
|
||||
"""Marks a node as complete"""
|
||||
|
||||
if node_id not in self.execution_graph.nodes:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Callable, List, Union
|
||||
from typing import List, Union
|
||||
|
||||
import torch.nn as nn
|
||||
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
|
||||
from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
|
||||
from diffusers.models import AutoencoderKL, UNet2DConditionModel
|
||||
|
||||
|
||||
def _conv_forward_asymmetric(self, input, weight, bias):
|
||||
@@ -27,51 +26,70 @@ def _conv_forward_asymmetric(self, input, weight, bias):
|
||||
|
||||
@contextmanager
|
||||
def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]):
|
||||
# Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor
|
||||
to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = []
|
||||
try:
|
||||
# Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence
|
||||
skipped_layers = 1
|
||||
to_restore = []
|
||||
|
||||
for m_name, m in model.named_modules():
|
||||
if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
||||
continue
|
||||
|
||||
if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name:
|
||||
# down_blocks.1.resnets.1.conv1
|
||||
_, block_num, _, resnet_num, submodule_name = m_name.split(".")
|
||||
block_num = int(block_num)
|
||||
resnet_num = int(resnet_num)
|
||||
|
||||
if block_num >= len(model.down_blocks) - skipped_layers:
|
||||
if isinstance(model, UNet2DConditionModel):
|
||||
if ".attentions." in m_name:
|
||||
continue
|
||||
|
||||
# Skip the second resnet (could be configurable)
|
||||
if resnet_num > 0:
|
||||
if ".resnets." in m_name:
|
||||
if ".conv2" in m_name:
|
||||
continue
|
||||
if ".conv_shortcut" in m_name:
|
||||
continue
|
||||
|
||||
"""
|
||||
if isinstance(model, UNet2DConditionModel):
|
||||
if False and ".upsamplers." in m_name:
|
||||
continue
|
||||
|
||||
# Skip Conv2d layers (could be configurable)
|
||||
if submodule_name == "conv2":
|
||||
if False and ".downsamplers." in m_name:
|
||||
continue
|
||||
|
||||
m.asymmetric_padding_mode = {}
|
||||
m.asymmetric_padding = {}
|
||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["x"] = (
|
||||
m._reversed_padding_repeated_twice[0],
|
||||
m._reversed_padding_repeated_twice[1],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["y"] = (
|
||||
0,
|
||||
0,
|
||||
m._reversed_padding_repeated_twice[2],
|
||||
m._reversed_padding_repeated_twice[3],
|
||||
)
|
||||
if True and ".resnets." in m_name:
|
||||
if True and ".conv1" in m_name:
|
||||
if False and "down_blocks" in m_name:
|
||||
continue
|
||||
if False and "mid_block" in m_name:
|
||||
continue
|
||||
if False and "up_blocks" in m_name:
|
||||
continue
|
||||
|
||||
to_restore.append((m, m._conv_forward))
|
||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
||||
if True and ".conv2" in m_name:
|
||||
continue
|
||||
|
||||
if True and ".conv_shortcut" in m_name:
|
||||
continue
|
||||
|
||||
if True and ".attentions." in m_name:
|
||||
continue
|
||||
|
||||
if False and m_name in ["conv_in", "conv_out"]:
|
||||
continue
|
||||
"""
|
||||
|
||||
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
||||
m.asymmetric_padding_mode = {}
|
||||
m.asymmetric_padding = {}
|
||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["x"] = (
|
||||
m._reversed_padding_repeated_twice[0],
|
||||
m._reversed_padding_repeated_twice[1],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["y"] = (
|
||||
0,
|
||||
0,
|
||||
m._reversed_padding_repeated_twice[2],
|
||||
m._reversed_padding_repeated_twice[3],
|
||||
)
|
||||
|
||||
to_restore.append((m, m._conv_forward))
|
||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"@chakra-ui/react-use-size": "^2.1.0",
|
||||
"@dagrejs/graphlib": "^2.1.13",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/inter": "^5.0.16",
|
||||
"@invoke-ai/ui-library": "^0.0.18",
|
||||
|
||||
15
invokeai/frontend/web/pnpm-lock.yaml
generated
15
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -22,9 +22,6 @@ dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@18.2.0)
|
||||
@@ -2887,18 +2884,6 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.1.0
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
|
||||
react: 18.2.0
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@dnd-kit/utilities@3.2.2(react@18.2.0):
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"random": "Zufall",
|
||||
"batch": "Stapel-Manager",
|
||||
"advanced": "Erweitert",
|
||||
"unifiedCanvas": "Leinwand",
|
||||
"unifiedCanvas": "Einheitliche Leinwand",
|
||||
"openInNewTab": "In einem neuem Tab öffnen",
|
||||
"statusProcessing": "wird bearbeitet",
|
||||
"linear": "Linear",
|
||||
@@ -127,7 +127,7 @@
|
||||
"galleryImageResetSize": "Größe zurücksetzen",
|
||||
"gallerySettings": "Galerie-Einstellungen",
|
||||
"maintainAspectRatio": "Seitenverhältnis beibehalten",
|
||||
"autoSwitchNewImages": "Auto-Wechsel zu neuen Bildern",
|
||||
"autoSwitchNewImages": "Automatisch zu neuen Bildern wechseln",
|
||||
"singleColumnLayout": "Einspaltiges Layout",
|
||||
"allImagesLoaded": "Alle Bilder geladen",
|
||||
"loadMore": "Mehr laden",
|
||||
@@ -226,7 +226,7 @@
|
||||
},
|
||||
"sendToImageToImage": {
|
||||
"title": "An Bild zu Bild senden",
|
||||
"desc": "Aktuelles Bild an Bild-zu-Bild senden"
|
||||
"desc": "Aktuelles Bild an Bild zu Bild senden"
|
||||
},
|
||||
"deleteImage": {
|
||||
"title": "Bild löschen",
|
||||
@@ -258,7 +258,7 @@
|
||||
},
|
||||
"selectEraser": {
|
||||
"title": "Radiergummi auswählen",
|
||||
"desc": "Wählt den Radiergummi aus"
|
||||
"desc": "Wählt den Radiergummi für die Leinwand aus"
|
||||
},
|
||||
"decreaseBrushSize": {
|
||||
"title": "Pinselgröße verkleinern",
|
||||
@@ -330,7 +330,7 @@
|
||||
},
|
||||
"downloadImage": {
|
||||
"title": "Bild herunterladen",
|
||||
"desc": "Aktuelles Bild herunterladen"
|
||||
"desc": "Aktuelle Leinwand herunterladen"
|
||||
},
|
||||
"undoStroke": {
|
||||
"title": "Pinselstrich rückgängig machen",
|
||||
@@ -564,8 +564,8 @@
|
||||
"img2imgStrength": "Bild-zu-Bild-Stärke",
|
||||
"toggleLoopback": "Loopback umschalten",
|
||||
"sendTo": "Senden an",
|
||||
"sendToImg2Img": "Senden an Bild-zu-Bild",
|
||||
"sendToUnifiedCanvas": "Senden an Leinwand",
|
||||
"sendToImg2Img": "Senden an Bild zu Bild",
|
||||
"sendToUnifiedCanvas": "Senden an Unified Canvas",
|
||||
"copyImageToLink": "Bild-Link kopieren",
|
||||
"downloadImage": "Bild herunterladen",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
@@ -604,9 +604,7 @@
|
||||
"resetComplete": "Die Web-Oberfläche wurde zurückgesetzt.",
|
||||
"models": "Modelle",
|
||||
"useSlidersForAll": "Schieberegler für alle Optionen verwenden",
|
||||
"showAdvancedOptions": "Erweiterte Optionen anzeigen",
|
||||
"alternateCanvasLayout": "Alternatives Leinwand-Layout",
|
||||
"clearIntermediatesDesc1": "Das Löschen der Zwischenprodukte setzt Leinwand und ControlNet zurück."
|
||||
"showAdvancedOptions": "Erweiterte Optionen anzeigen"
|
||||
},
|
||||
"toast": {
|
||||
"tempFoldersEmptied": "Temp-Ordner geleert",
|
||||
@@ -620,7 +618,7 @@
|
||||
"imageSavedToGallery": "Bild in die Galerie gespeichert",
|
||||
"canvasMerged": "Leinwand zusammengeführt",
|
||||
"sentToImageToImage": "Gesendet an Bild zu Bild",
|
||||
"sentToUnifiedCanvas": "Gesendet an Leinwand",
|
||||
"sentToUnifiedCanvas": "Gesendet an Unified Canvas",
|
||||
"parametersSet": "Parameter festlegen",
|
||||
"parametersNotSet": "Parameter nicht festgelegt",
|
||||
"parametersNotSetDesc": "Keine Metadaten für dieses Bild gefunden.",
|
||||
@@ -637,21 +635,7 @@
|
||||
"metadataLoadFailed": "Metadaten konnten nicht geladen werden",
|
||||
"initialImageSet": "Ausgangsbild festgelegt",
|
||||
"initialImageNotSet": "Ausgangsbild nicht festgelegt",
|
||||
"initialImageNotSetDesc": "Ausgangsbild konnte nicht geladen werden",
|
||||
"setCanvasInitialImage": "Ausgangsbild setzen",
|
||||
"problemMergingCanvas": "Problem bei Verschmelzung der Leinwand",
|
||||
"canvasCopiedClipboard": "Leinwand in Zwischenablage kopiert",
|
||||
"canvasSentControlnetAssets": "Leinwand an ControlNet & Sammlung geschickt",
|
||||
"problemDownloadingCanvasDesc": "Kann Basis-Layer nicht exportieren",
|
||||
"canvasDownloaded": "Leinwand heruntergeladen",
|
||||
"problemSavingCanvasDesc": "Kann Basis-Layer nicht exportieren",
|
||||
"canvasSavedGallery": "Leinwand in Galerie gespeichert",
|
||||
"problemMergingCanvasDesc": "Kann Basis-Layer nicht exportieren",
|
||||
"problemSavingCanvas": "Problem beim Speichern der Leinwand",
|
||||
"problemCopyingCanvas": "Problem beim Kopieren der Leinwand",
|
||||
"problemCopyingCanvasDesc": "Kann Basis-Layer nicht exportieren",
|
||||
"problemDownloadingCanvas": "Problem beim Herunterladen der Leinwand",
|
||||
"setAsCanvasInitialImage": "Als Ausgangsbild gesetzt"
|
||||
"initialImageNotSetDesc": "Ausgangsbild konnte nicht geladen werden"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
@@ -664,7 +648,7 @@
|
||||
"faceCorrection": "Gesichtskorrektur mit GFPGAN oder Codeformer: Der Algorithmus erkennt Gesichter im Bild und korrigiert alle Fehler. Ein hoher Wert verändert das Bild stärker, was zu attraktiveren Gesichtern führt. Codeformer mit einer höheren Genauigkeit bewahrt das Originalbild auf Kosten einer stärkeren Gesichtskorrektur.",
|
||||
"imageToImage": "Bild zu Bild lädt ein beliebiges Bild als Ausgangsbild, aus dem dann zusammen mit dem Prompt ein neues Bild erzeugt wird. Je höher der Wert ist, desto stärker wird das Ergebnisbild verändert. Werte von 0,0 bis 1,0 sind möglich, der empfohlene Bereich ist .25-.75",
|
||||
"boundingBox": "Der Begrenzungsrahmen ist derselbe wie die Einstellungen für Breite und Höhe bei Text-zu-Bild oder Bild-zu-Bild. Es wird nur der Bereich innerhalb des Rahmens verarbeitet.",
|
||||
"seamCorrection": "Behandlung von sichtbaren Übergängen, die zwischen den erzeugten Bildern auftreten.",
|
||||
"seamCorrection": "Steuert die Behandlung von sichtbaren Übergängen, die zwischen den erzeugten Bildern auf der Leinwand auftreten.",
|
||||
"infillAndScaling": "Verwalten Sie Infill-Methoden (für maskierte oder gelöschte Bereiche der Leinwand) und Skalierung (nützlich für kleine Begrenzungsrahmengrößen)."
|
||||
}
|
||||
},
|
||||
@@ -675,17 +659,17 @@
|
||||
"maskingOptions": "Maskierungsoptionen",
|
||||
"enableMask": "Maske aktivieren",
|
||||
"preserveMaskedArea": "Maskierten Bereich bewahren",
|
||||
"clearMask": "Maske löschen (Shift+C)",
|
||||
"clearMask": "Maske löschen",
|
||||
"brush": "Pinsel",
|
||||
"eraser": "Radierer",
|
||||
"fillBoundingBox": "Begrenzungsrahmen füllen",
|
||||
"eraseBoundingBox": "Begrenzungsrahmen löschen",
|
||||
"colorPicker": "Pipette",
|
||||
"colorPicker": "Farbpipette",
|
||||
"brushOptions": "Pinseloptionen",
|
||||
"brushSize": "Größe",
|
||||
"move": "Bewegen",
|
||||
"resetView": "Ansicht zurücksetzen",
|
||||
"mergeVisible": "Sichtbare zusammenführen",
|
||||
"mergeVisible": "Sichtbare Zusammenführen",
|
||||
"saveToGallery": "In Galerie speichern",
|
||||
"copyToClipboard": "In Zwischenablage kopieren",
|
||||
"downloadAsImage": "Als Bild herunterladen",
|
||||
@@ -699,15 +683,15 @@
|
||||
"darkenOutsideSelection": "Außerhalb der Auswahl verdunkeln",
|
||||
"autoSaveToGallery": "Automatisch in Galerie speichern",
|
||||
"saveBoxRegionOnly": "Nur Auswahlbox speichern",
|
||||
"limitStrokesToBox": "Striche auf Auswahl beschränken",
|
||||
"showCanvasDebugInfo": "Zusätzliche Informationen anzeigen",
|
||||
"limitStrokesToBox": "Striche auf Box beschränken",
|
||||
"showCanvasDebugInfo": "Zusätzliche Informationen zur Leinwand anzeigen",
|
||||
"clearCanvasHistory": "Leinwand-Verlauf löschen",
|
||||
"clearHistory": "Verlauf löschen",
|
||||
"clearCanvasHistoryMessage": "Wenn Sie den Verlauf löschen, bleibt die aktuelle Leinwand intakt, aber der Verlauf der Rückgängig- und Wiederherstellung wird unwiderruflich gelöscht.",
|
||||
"clearCanvasHistoryConfirm": "Sind Sie sicher, dass Sie den Verlauf löschen möchten?",
|
||||
"clearCanvasHistoryMessage": "Wenn Sie den Verlauf der Leinwand löschen, bleibt die aktuelle Leinwand intakt, aber der Verlauf der Rückgängig- und Wiederherstellung wird unwiderruflich gelöscht.",
|
||||
"clearCanvasHistoryConfirm": "Sind Sie sicher, dass Sie den Verlauf der Leinwand löschen möchten?",
|
||||
"emptyTempImageFolder": "Temp-Image Ordner leeren",
|
||||
"emptyFolder": "Leerer Ordner",
|
||||
"emptyTempImagesFolderMessage": "Wenn Sie den Ordner für temporäre Bilder leeren, wird die Leinwand zurückgesetzt. Dies umfasst den gesamten Verlauf der Rückgängig-/Wiederherstellungsvorgänge, die Bilder im Bereitstellungsbereich und die Leinwand-Basisebene.",
|
||||
"emptyTempImagesFolderMessage": "Wenn Sie den Ordner für temporäre Bilder leeren, wird auch der Unified Canvas vollständig zurückgesetzt. Dies umfasst den gesamten Verlauf der Rückgängig-/Wiederherstellungsvorgänge, die Bilder im Bereitstellungsbereich und die Leinwand-Basisebene.",
|
||||
"emptyTempImagesFolderConfirm": "Sind Sie sicher, dass Sie den temporären Ordner leeren wollen?",
|
||||
"activeLayer": "Aktive Ebene",
|
||||
"canvasScale": "Leinwand Maßstab",
|
||||
@@ -724,7 +708,7 @@
|
||||
"discardAll": "Alles verwerfen",
|
||||
"betaClear": "Löschen",
|
||||
"betaDarkenOutside": "Außen abdunkeln",
|
||||
"betaLimitToBox": "Auf Auswahl begrenzen",
|
||||
"betaLimitToBox": "Begrenzung auf das Feld",
|
||||
"betaPreserveMasked": "Maskiertes bewahren",
|
||||
"antialiasing": "Kantenglättung",
|
||||
"showResultsOn": "Zeige Ergebnisse (An)",
|
||||
@@ -762,7 +746,7 @@
|
||||
"autoAddBoard": "Automatisches Hinzufügen zum Ordner",
|
||||
"topMessage": "Dieser Ordner enthält Bilder die in den folgenden Funktionen verwendet werden:",
|
||||
"move": "Bewegen",
|
||||
"menuItemAutoAdd": "Auto-Hinzufügen zu diesem Ordner",
|
||||
"menuItemAutoAdd": "Automatisches Hinzufügen zu diesem Ordner",
|
||||
"myBoard": "Meine Ordner",
|
||||
"searchBoard": "Ordner durchsuchen...",
|
||||
"noMatching": "Keine passenden Ordner",
|
||||
@@ -842,6 +826,7 @@
|
||||
"pidi": "PIDI",
|
||||
"normalBae": "Normales BAE",
|
||||
"mlsdDescription": "Minimalistischer Liniensegmentdetektor",
|
||||
"openPoseDescription": "Schätzung der menschlichen Pose mit Openpose",
|
||||
"control": "Kontrolle",
|
||||
"coarse": "Grob",
|
||||
"crop": "Zuschneiden",
|
||||
@@ -854,9 +839,10 @@
|
||||
"lineartAnimeDescription": "Lineart-Verarbeitung im Anime-Stil",
|
||||
"minConfidence": "Minimales Vertrauen",
|
||||
"megaControl": "Mega-Kontrolle",
|
||||
"autoConfigure": "Prozessor Auto-konfig",
|
||||
"autoConfigure": "Prozessor automatisch konfigurieren",
|
||||
"normalBaeDescription": "Normale BAE-Verarbeitung",
|
||||
"noneDescription": "Es wurde keine Verarbeitung angewendet",
|
||||
"openPose": "Openpose / \"Pose nutzen\"",
|
||||
"lineartAnime": "Lineart Anime / \"Strichzeichnung Anime\"",
|
||||
"mediapipeFaceDescription": "Gesichtserkennung mit Mediapipe",
|
||||
"canny": "\"Canny\"",
|
||||
@@ -958,7 +944,7 @@
|
||||
"initImage": "Erstes Bild",
|
||||
"variations": "Seed-Gewichtungs-Paare",
|
||||
"vae": "VAE",
|
||||
"workflow": "Workflow",
|
||||
"workflow": "Arbeitsablauf",
|
||||
"scheduler": "Planer",
|
||||
"noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden",
|
||||
"recallParameters": "Parameter wiederherstellen"
|
||||
@@ -1070,20 +1056,6 @@
|
||||
"\"Per Bild\" wird einen einzigartigen Seed-Wert für jedes Bild verwenden. Dies bietet mehr Variationen."
|
||||
],
|
||||
"heading": "Seed-Verhalten"
|
||||
},
|
||||
"dynamicPrompts": {
|
||||
"paragraphs": [
|
||||
"\"Dynamische Prompts\" übersetzt einen Prompt in mehrere.",
|
||||
"Die Ausgangs-Syntax ist \"ein {roter|grüner|blauer} ball\". Das generiert 3 Prompts: \"ein roter ball\", \"ein grüner ball\" und \"ein blauer ball\".",
|
||||
"Sie können die Syntax so oft verwenden, wie Sie in einem einzigen Prompt möchten, aber stellen Sie sicher, dass die Anzahl der Prompts zur Einstellung von \"Max Prompts\" passt."
|
||||
],
|
||||
"heading": "Dynamische Prompts"
|
||||
},
|
||||
"controlNetWeight": {
|
||||
"paragraphs": [
|
||||
"Wie stark wird das ControlNet das generierte Bild beeinflussen wird."
|
||||
],
|
||||
"heading": "Einfluss"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
@@ -1188,10 +1160,10 @@
|
||||
"outputFieldInInput": "Ausgabefeld im Eingang",
|
||||
"problemReadingWorkflow": "Problem beim Lesen des Arbeitsablaufs vom Bild",
|
||||
"reloadNodeTemplates": "Knoten-Vorlagen neu laden",
|
||||
"newWorkflow": "Neuer Arbeitsablauf / Workflow",
|
||||
"newWorkflow": "Neuer Arbeitsablauf",
|
||||
"newWorkflowDesc": "Einen neuen Arbeitsablauf erstellen?",
|
||||
"noFieldsLinearview": "Keine Felder zur linearen Ansicht hinzugefügt",
|
||||
"clearWorkflow": "Workflow löschen",
|
||||
"clearWorkflow": "Arbeitsablauf löschen",
|
||||
"clearWorkflowDesc": "Diesen Arbeitsablauf löschen und neu starten?",
|
||||
"noConnectionInProgress": "Es besteht keine Verbindung",
|
||||
"notes": "Anmerkungen",
|
||||
@@ -1248,8 +1220,8 @@
|
||||
"stringDescription": "Zeichenfolgen (Strings) sind Text.",
|
||||
"fieldTypesMustMatch": "Feldtypen müssen übereinstimmen",
|
||||
"fitViewportNodes": "An Ansichtsgröße anpassen",
|
||||
"missingCanvaInitMaskImages": "Fehlende Startbilder und Masken auf der Leinwand",
|
||||
"missingCanvaInitImage": "Fehlendes Startbild auf der Leinwand",
|
||||
"missingCanvaInitMaskImages": "Fehlende Startbilder und Masken auf der Arbeitsfläche",
|
||||
"missingCanvaInitImage": "Fehlendes Startbild auf der Arbeitsfläche",
|
||||
"ipAdapterModelDescription": "IP-Adapter-Modellfeld",
|
||||
"latentsPolymorphicDescription": "Zwischen Nodes können Latents weitergegeben werden.",
|
||||
"loadingNodes": "Lade Nodes...",
|
||||
@@ -1349,7 +1321,7 @@
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"noSystemWorkflows": "Keine System-Arbeitsabläufe",
|
||||
"workflowName": "Arbeitsablauf-Name",
|
||||
"workflowIsOpen": "Arbeitsablauf ist geöffnet",
|
||||
"workflowIsOpen": "Arbeitsablauf ist offen",
|
||||
"saveWorkflowAs": "Arbeitsablauf speichern als",
|
||||
"searchWorkflows": "Suche Arbeitsabläufe",
|
||||
"newWorkflowCreated": "Neuer Arbeitsablauf erstellt",
|
||||
|
||||
@@ -175,7 +175,6 @@
|
||||
"statusUpscaling": "Upscaling",
|
||||
"statusUpscalingESRGAN": "Upscaling (ESRGAN)",
|
||||
"template": "Template",
|
||||
"toResolve": "To resolve",
|
||||
"training": "Training",
|
||||
"trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.",
|
||||
"trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.",
|
||||
@@ -901,7 +900,6 @@
|
||||
"doesNotExist": "does not exist",
|
||||
"downloadWorkflow": "Download Workflow JSON",
|
||||
"edge": "Edge",
|
||||
"editMode": "Edit in Workflow Editor",
|
||||
"enum": "Enum",
|
||||
"enumDescription": "Enums are values that may be one of a number of options.",
|
||||
"executionStateCompleted": "Completed",
|
||||
@@ -997,10 +995,8 @@
|
||||
"problemReadingMetadata": "Problem reading metadata from image",
|
||||
"problemReadingWorkflow": "Problem reading workflow from image",
|
||||
"problemSettingTitle": "Problem Setting Title",
|
||||
"resetToDefaultValue": "Reset to default value",
|
||||
"reloadNodeTemplates": "Reload Node Templates",
|
||||
"removeLinearView": "Remove from Linear View",
|
||||
"reorderLinearView": "Reorder Linear View",
|
||||
"newWorkflow": "New Workflow",
|
||||
"newWorkflowDesc": "Create a new workflow?",
|
||||
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
|
||||
@@ -1071,7 +1067,6 @@
|
||||
"vaeModelFieldDescription": "TODO",
|
||||
"validateConnections": "Validate Connections and Graph",
|
||||
"validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked",
|
||||
"viewMode": "Use in Linear View",
|
||||
"unableToGetWorkflowVersion": "Unable to get workflow schema version",
|
||||
"unrecognizedWorkflowVersion": "Unrecognized workflow schema version {{version}}",
|
||||
"version": "Version",
|
||||
|
||||
@@ -795,8 +795,7 @@
|
||||
"workflowDeleted": "Flusso di lavoro eliminato",
|
||||
"problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro",
|
||||
"resetInitialImage": "Reimposta l'immagine iniziale",
|
||||
"uploadInitialImage": "Carica l'immagine iniziale",
|
||||
"problemDownloadingImage": "Impossibile scaricare l'immagine"
|
||||
"uploadInitialImage": "Carica l'immagine iniziale"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
@@ -1135,10 +1134,7 @@
|
||||
"newWorkflow": "Nuovo flusso di lavoro",
|
||||
"newWorkflowDesc": "Creare un nuovo flusso di lavoro?",
|
||||
"newWorkflowDesc2": "Il flusso di lavoro attuale presenta modifiche non salvate.",
|
||||
"unsupportedAnyOfLength": "unione di troppi elementi ({{count}})",
|
||||
"clearWorkflowDesc": "Cancellare questo flusso di lavoro e avviarne uno nuovo?",
|
||||
"clearWorkflow": "Cancella il flusso di lavoro",
|
||||
"clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate."
|
||||
"unsupportedAnyOfLength": "unione di troppi elementi ({{count}})"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
||||
@@ -1195,6 +1191,7 @@
|
||||
"f": "F",
|
||||
"h": "A",
|
||||
"prompt": "Prompt",
|
||||
"openPoseDescription": "Stima della posa umana utilizzando Openpose",
|
||||
"resizeMode": "Ridimensionamento",
|
||||
"weight": "Peso",
|
||||
"selectModel": "Seleziona un modello",
|
||||
@@ -1675,9 +1672,7 @@
|
||||
"downloadWorkflow": "Salva su file",
|
||||
"uploadWorkflow": "Carica da file",
|
||||
"projectWorkflows": "Flussi di lavoro del progetto",
|
||||
"noWorkflows": "Nessun flusso di lavoro",
|
||||
"workflowCleared": "Flusso di lavoro cancellato",
|
||||
"saveWorkflowToProject": "Salva flusso di lavoro nel progetto"
|
||||
"noWorkflows": "Nessun flusso di lavoro"
|
||||
},
|
||||
"app": {
|
||||
"storeNotInitialized": "Il negozio non è inizializzato"
|
||||
|
||||
@@ -555,6 +555,7 @@
|
||||
"balanced": "バランス",
|
||||
"prompt": "プロンプト",
|
||||
"depthMidasDescription": "Midasを使用して深度マップを生成",
|
||||
"openPoseDescription": "Openposeを使用してポーズを推定",
|
||||
"control": "コントロール",
|
||||
"resizeMode": "リサイズモード",
|
||||
"weight": "重み",
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
"h": "H",
|
||||
"prompt": "프롬프트",
|
||||
"depthMidasDescription": "Midas를 사용하여 Depth map 생성하기",
|
||||
"openPoseDescription": "Openpose를 이용한 사람 포즈 추정",
|
||||
"control": "Control",
|
||||
"resizeMode": "크기 조정 모드",
|
||||
"t2iEnabledControlNetDisabled": "$t(common.t2iAdapter) 사용 가능,$t(common.controlNet) 사용 불가능",
|
||||
@@ -369,6 +370,7 @@
|
||||
"normalBaeDescription": "Normal BAE 처리",
|
||||
"noneDescription": "처리되지 않음",
|
||||
"saveControlImage": "Control Image 저장",
|
||||
"openPose": "Openpose",
|
||||
"toggleControlNet": "해당 ControlNet으로 전환",
|
||||
"delete": "삭제",
|
||||
"controlAdapter_other": "Control Adapter(s)",
|
||||
|
||||
@@ -1033,6 +1033,7 @@
|
||||
"prompt": "Prompt",
|
||||
"depthMidasDescription": "Genereer diepteblad via Midas",
|
||||
"controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))",
|
||||
"openPoseDescription": "Menselijke pose-benadering via Openpose",
|
||||
"control": "Controle",
|
||||
"resizeMode": "Modus schaling",
|
||||
"t2iEnabledControlNetDisabled": "$t(common.t2iAdapter) ingeschakeld, $t(common.controlNet)s uitgeschakeld",
|
||||
@@ -1071,6 +1072,7 @@
|
||||
"normalBaeDescription": "Normale BAE-verwerking",
|
||||
"noneDescription": "Geen verwerking toegepast",
|
||||
"saveControlImage": "Bewaar controle-afbeelding",
|
||||
"openPose": "Openpose",
|
||||
"toggleControlNet": "Zet deze ControlNet aan/uit",
|
||||
"delete": "Verwijder",
|
||||
"controlAdapter_one": "Control-adapter",
|
||||
|
||||
@@ -1155,6 +1155,7 @@
|
||||
"resetControlImage": "Сбросить контрольное изображение",
|
||||
"prompt": "Запрос",
|
||||
"controlnet": "$t(controlnet.controlAdapter_one) №{{number}} $t(common.controlNet)",
|
||||
"openPoseDescription": "Оценка позы человека с помощью Openpose",
|
||||
"resizeMode": "Режим изменения размера",
|
||||
"t2iEnabledControlNetDisabled": "$t(common.t2iAdapter) включен, $t(common.controlNet)s отключен",
|
||||
"weight": "Вес",
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
"mediapipeFace": "Mediapipe Yüz",
|
||||
"megaControl": "Aşırı Yönetim",
|
||||
"mlsd": "M-LSD",
|
||||
"openPoseDescription": "Openpose kullanarak poz belirleme",
|
||||
"setControlImageDimensions": "Yönetim Görseli Boyutlarını En/Boydan Al",
|
||||
"pidi": "PIDI",
|
||||
"scribble": "çiziktirme",
|
||||
@@ -272,6 +273,7 @@
|
||||
"mlsdDescription": "Minimalist Line Segment Detector (Kolay Çizgi Parçası Algılama)",
|
||||
"normalBae": "Normal BAE",
|
||||
"normalBaeDescription": "Normal BAE işleme",
|
||||
"openPose": "Openpose",
|
||||
"resetControlImage": "Yönetim Görselini Kaldır",
|
||||
"enableIPAdapter": "IP Aracını Etkinleştir",
|
||||
"lineart": "Çizim",
|
||||
|
||||
@@ -1143,6 +1143,7 @@
|
||||
"balanced": "平衡",
|
||||
"prompt": "Prompt (提示词控制)",
|
||||
"depthMidasDescription": "使用 Midas 生成深度图",
|
||||
"openPoseDescription": "使用 Openpose 进行人体姿态估计",
|
||||
"resizeMode": "缩放模式",
|
||||
"weight": "权重",
|
||||
"selectModel": "选择一个模型",
|
||||
@@ -1206,6 +1207,7 @@
|
||||
"megaControl": "Mega Control (超级控制)",
|
||||
"depthZoe": "Depth (Zoe)",
|
||||
"colorMap": "Color",
|
||||
"openPose": "Openpose",
|
||||
"controlAdapter_other": "Control Adapters",
|
||||
"lineartAnime": "Lineart Anime",
|
||||
"canny": "Canny",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { UnknownAction } from '@reduxjs/toolkit';
|
||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import type { Graph } from 'services/api/types';
|
||||
import { socketGeneratorProgress } from 'services/events/actions';
|
||||
|
||||
@@ -18,7 +18,7 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
}
|
||||
}
|
||||
|
||||
if (appInfoApi.endpoints.getOpenAPISchema.matchFulfilled(action)) {
|
||||
if (receivedOpenAPISchema.fulfilled.match(action)) {
|
||||
return {
|
||||
...action,
|
||||
payload: '<OpenAPI schema omitted>',
|
||||
|
||||
@@ -23,7 +23,6 @@ import { addControlNetImageProcessedListener } from './listeners/controlNetImage
|
||||
import { addEnqueueRequestedCanvasListener } from './listeners/enqueueRequestedCanvas';
|
||||
import { addEnqueueRequestedLinear } from './listeners/enqueueRequestedLinear';
|
||||
import { addEnqueueRequestedNodes } from './listeners/enqueueRequestedNodes';
|
||||
import { addGetOpenAPISchemaListener } from './listeners/getOpenAPISchema';
|
||||
import {
|
||||
addImageAddedToBoardFulfilledListener,
|
||||
addImageAddedToBoardRejectedListener,
|
||||
@@ -48,6 +47,7 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte
|
||||
import { addModelSelectedListener } from './listeners/modelSelected';
|
||||
import { addModelsLoadedListener } from './listeners/modelsLoaded';
|
||||
import { addDynamicPromptsListener } from './listeners/promptChanged';
|
||||
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
|
||||
import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected';
|
||||
import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected';
|
||||
import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress';
|
||||
@@ -150,7 +150,7 @@ addImageRemovedFromBoardRejectedListener();
|
||||
addBoardIdSelectedListener();
|
||||
|
||||
// Node schemas
|
||||
addGetOpenAPISchemaListener();
|
||||
addReceivedOpenAPISchemaListener();
|
||||
|
||||
// Workflows
|
||||
addWorkflowLoadRequestedListener();
|
||||
|
||||
@@ -3,18 +3,18 @@ import { parseify } from 'common/util/serialize';
|
||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
|
||||
import { size } from 'lodash-es';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
|
||||
import { startAppListening } from '..';
|
||||
|
||||
export const addGetOpenAPISchemaListener = () => {
|
||||
export const addReceivedOpenAPISchemaListener = () => {
|
||||
startAppListening({
|
||||
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
|
||||
actionCreator: receivedOpenAPISchema.fulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const log = logger('system');
|
||||
const schemaJSON = action.payload;
|
||||
|
||||
log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema');
|
||||
log.debug({ schemaJSON }, 'Received OpenAPI schema');
|
||||
const { nodesAllowlist, nodesDenylist } = getState().config;
|
||||
|
||||
const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist);
|
||||
@@ -26,14 +26,10 @@ export const addGetOpenAPISchemaListener = () => {
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: appInfoApi.endpoints.getOpenAPISchema.matchRejected,
|
||||
actionCreator: receivedOpenAPISchema.rejected,
|
||||
effect: (action) => {
|
||||
// If action.meta.condition === true, the request was canceled/skipped because another request was in flight or
|
||||
// the value was already in the cache. We don't want to log these errors.
|
||||
if (!action.meta.condition) {
|
||||
const log = logger('system');
|
||||
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
|
||||
}
|
||||
const log = logger('system');
|
||||
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { $baseUrl } from 'app/store/nanostores/baseUrl';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { isEqual, size } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import { api } from 'services/api';
|
||||
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
|
||||
import { startAppListening } from '../..';
|
||||
@@ -76,4 +77,17 @@ export const addSocketConnectedEventListener = () => {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketConnected,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { nodeTemplates, config } = getState();
|
||||
// We only want to re-fetch the schema if we don't have any node templates
|
||||
if (!size(nodeTemplates.templates) && !config.disabledTabs.includes('nodes')) {
|
||||
// This request is a createAsyncThunk - resetting API state as in the above listener
|
||||
// will not trigger this request, so we need to manually do it.
|
||||
dispatch(receivedOpenAPISchema());
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/typ
|
||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { t } from 'i18next';
|
||||
import { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
@@ -52,6 +53,7 @@ export const addWorkflowLoadRequestedListener = () => {
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setActiveTab('nodes'));
|
||||
requestAnimationFrame(() => {
|
||||
$flow.get()?.fitView();
|
||||
});
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imageDownloaded } from 'features/gallery/store/actions';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useImageUrlToBlob } from './useImageUrlToBlob';
|
||||
|
||||
export const useDownloadImage = () => {
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const imageUrlToBlob = useImageUrlToBlob();
|
||||
const dispatch = useAppDispatch();
|
||||
const authToken = useStore($authToken);
|
||||
|
||||
const downloadImage = useCallback(
|
||||
async (image_url: string, image_name: string) => {
|
||||
try {
|
||||
const requestOpts = authToken
|
||||
? {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
const blob = await fetch(image_url, requestOpts).then((resp) => resp.blob());
|
||||
const blob = await imageUrlToBlob(image_url);
|
||||
|
||||
if (!blob) {
|
||||
throw new Error('Unable to create Blob');
|
||||
}
|
||||
@@ -46,7 +40,7 @@ export const useDownloadImage = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, toaster, dispatch, authToken]
|
||||
[t, toaster, imageUrlToBlob, dispatch]
|
||||
);
|
||||
|
||||
return { downloadImage };
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DndContextTypesafe } from './DndContextTypesafe';
|
||||
|
||||
type Props = PropsWithChildren & {
|
||||
items: string[];
|
||||
onDragEnd(event: DragEndEvent): void;
|
||||
};
|
||||
|
||||
const DndSortable = (props: Props) => {
|
||||
return (
|
||||
<DndContextTypesafe onDragEnd={props.onDragEnd}>
|
||||
<SortableContext items={props.items} strategy={verticalListSortingStrategy}>
|
||||
{props.children}
|
||||
</SortableContext>
|
||||
</DndContextTypesafe>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DndSortable);
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||
@@ -10,7 +11,6 @@ import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdDeviceHub } from 'react-icons/md';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||
import { Flow } from './flow/Flow';
|
||||
@@ -40,7 +40,7 @@ const exit: AnimationProps['exit'] = {
|
||||
};
|
||||
|
||||
const NodeEditor = () => {
|
||||
const { data, isLoading } = useGetOpenAPISchemaQuery();
|
||||
const isReady = useAppSelector((s) => s.nodes.isReady);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex
|
||||
@@ -53,7 +53,7 @@ const NodeEditor = () => {
|
||||
justifyContent="center"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{data && (
|
||||
{isReady && (
|
||||
<motion.div initial={initial} animate={animate} exit={exit} style={isReadyMotionStyles}>
|
||||
<Flow />
|
||||
<AddNodePopover />
|
||||
@@ -65,7 +65,7 @@ const NodeEditor = () => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{isLoading && (
|
||||
{!isReady && (
|
||||
<motion.div initial={initial} animate={animate} exit={exit} style={notIsReadyMotionStyles}>
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import {
|
||||
selectWorkflowSlice,
|
||||
workflowExposedFieldAdded,
|
||||
@@ -19,7 +18,7 @@ type Props = {
|
||||
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
|
||||
const selectIsExposed = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
@@ -31,8 +30,8 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
|
||||
const isExposed = useAppSelector(selectIsExposed);
|
||||
|
||||
const handleExposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
|
||||
}, [dispatch, fieldName, nodeId, value]);
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const handleUnexposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
|
||||
@@ -60,9 +60,10 @@ import VAEModelFieldInputComponent from './inputs/VAEModelFieldInputComponent';
|
||||
type InputFieldProps = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
saveToGraph?: boolean;
|
||||
};
|
||||
|
||||
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
|
||||
const InputFieldRenderer = ({ nodeId, fieldName, saveToGraph = true }: InputFieldProps) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldInstance = useFieldInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
|
||||
@@ -76,69 +77,181 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) {
|
||||
return <StringFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<StringFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isBooleanFieldInputInstance(fieldInstance) && isBooleanFieldInputTemplate(fieldTemplate)) {
|
||||
return <BooleanFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<BooleanFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(isIntegerFieldInputInstance(fieldInstance) && isIntegerFieldInputTemplate(fieldTemplate)) ||
|
||||
(isFloatFieldInputInstance(fieldInstance) && isFloatFieldInputTemplate(fieldTemplate))
|
||||
) {
|
||||
return <NumberFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<NumberFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEnumFieldInputInstance(fieldInstance) && isEnumFieldInputTemplate(fieldTemplate)) {
|
||||
return <EnumFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<EnumFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isImageFieldInputInstance(fieldInstance) && isImageFieldInputTemplate(fieldTemplate)) {
|
||||
return <ImageFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<ImageFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isBoardFieldInputInstance(fieldInstance) && isBoardFieldInputTemplate(fieldTemplate)) {
|
||||
return <BoardFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<BoardFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMainModelFieldInputInstance(fieldInstance) && isMainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <MainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<MainModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <RefinerModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<RefinerModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isVAEModelFieldInputInstance(fieldInstance) && isVAEModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <VAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<VAEModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <LoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<LoRAModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isControlNetModelFieldInputInstance(fieldInstance) && isControlNetModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <ControlNetModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<ControlNetModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isIPAdapterModelFieldInputInstance(fieldInstance) && isIPAdapterModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <IPAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<IPAdapterModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isT2IAdapterModelFieldInputInstance(fieldInstance) && isT2IAdapterModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <T2IAdapterModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<T2IAdapterModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isColorFieldInputInstance(fieldInstance) && isColorFieldInputTemplate(fieldTemplate)) {
|
||||
return <ColorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<ColorFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSDXLMainModelFieldInputInstance(fieldInstance) && isSDXLMainModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <SDXLMainModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<SDXLMainModelFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSchedulerFieldInputInstance(fieldInstance) && isSchedulerFieldInputTemplate(fieldTemplate)) {
|
||||
return <SchedulerFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
return (
|
||||
<SchedulerFieldInputComponent
|
||||
nodeId={nodeId}
|
||||
field={fieldInstance}
|
||||
fieldTemplate={fieldTemplate}
|
||||
saveToGraph={saveToGraph}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldInstance && fieldTemplate) {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiDotsSixVerticalBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
@@ -22,79 +19,46 @@ type Props = {
|
||||
|
||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
layerStyle="second"
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
borderRadius="base"
|
||||
w="full"
|
||||
p={4}
|
||||
paddingLeft={0}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
flexDir="column"
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('nodes.reorderLinearView')}
|
||||
variant="ghost"
|
||||
icon={<PiDotsSixVerticalBold />}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
mx={2}
|
||||
height="full"
|
||||
/>
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex alignItems="center">
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
||||
<Spacer />
|
||||
{isValueChanged && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
<Flex h="full" alignItems="center">
|
||||
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
aria-label={t('nodes.removeLinearView')}
|
||||
tooltip={t('nodes.removeLinearView')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveField}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
|
||||
<Flex>
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
<Flex h="full" alignItems="center">
|
||||
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
aria-label={t('nodes.removeLinearView')}
|
||||
tooltip={t('nodes.removeLinearView')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveField}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} saveToGraph={false} />
|
||||
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { memo, useCallback } from 'react';
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const { nodeId, field, fieldTemplate, saveToGraph } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleValueChanged = useCallback(
|
||||
@@ -18,10 +18,11 @@ const StringFieldInputComponent = (props: FieldComponentProps<StringFieldInputIn
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value: e.target.value,
|
||||
saveToGraph,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
[dispatch, field.name, nodeId, saveToGraph]
|
||||
);
|
||||
|
||||
if (fieldTemplate.ui_component === 'textarea') {
|
||||
|
||||
@@ -4,4 +4,5 @@ export type FieldComponentProps<V extends FieldInputInstance, T extends FieldInp
|
||||
nodeId: string;
|
||||
field: V;
|
||||
fieldTemplate: T;
|
||||
saveToGraph: boolean;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
||||
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
||||
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
|
||||
import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName';
|
||||
import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const name = useAppSelector((s) => s.workflow.name);
|
||||
return (
|
||||
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
|
||||
<Flex gap="2">
|
||||
<AddNodeButton />
|
||||
<Flex flexDir="column" gap="2">
|
||||
<Flex gap="2">
|
||||
<AddNodeButton />
|
||||
<WorkflowLibraryButton />
|
||||
</Flex>
|
||||
<UpdateNodesButton />
|
||||
</Flex>
|
||||
<Spacer />
|
||||
{!!name.length && <WorkflowName />}
|
||||
<WorkflowName />
|
||||
<Spacer />
|
||||
<ClearFlowButton />
|
||||
<SaveWorkflowButton />
|
||||
|
||||
@@ -25,7 +25,6 @@ const UpdateNodesButton = () => {
|
||||
icon={<PiWarningBold />}
|
||||
onClick={handleClickUpdateNodes}
|
||||
pointerEvents="auto"
|
||||
colorScheme="warning"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const name = useAppSelector((s) => s.workflow.name);
|
||||
|
||||
return (
|
||||
<Text m={2} fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold" opacity={0.8}>
|
||||
{name}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopCenterPanel);
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsClockwiseBold } from 'react-icons/pi';
|
||||
import { useLazyGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
|
||||
const ReloadNodeTemplatesButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const [_getOpenAPISchema] = useLazyGetOpenAPISchemaQuery();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleReloadSchema = useCallback(() => {
|
||||
_getOpenAPISchema();
|
||||
}, [_getOpenAPISchema]);
|
||||
dispatch(receivedOpenAPISchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TopRightPanel = () => {
|
||||
return (
|
||||
<Flex gap={2} position="absolute" top={2} insetInlineEnd={2}>
|
||||
<WorkflowLibraryButton />
|
||||
<WorkflowLibraryMenu />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopRightPanel);
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
|
||||
export const ModeToggle = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector((s) => s.workflow.mode);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickEdit = useCallback(() => {
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
}, [dispatch]);
|
||||
|
||||
const onClickView = useCallback(() => {
|
||||
dispatch(workflowModeChanged('view'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="flex-end">
|
||||
{mode === 'view' && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.editMode')}
|
||||
tooltip={t('nodes.editMode')}
|
||||
onClick={onClickEdit}
|
||||
icon={<PiPencilBold />}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
)}
|
||||
{mode === 'edit' && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.viewMode')}
|
||||
tooltip={t('nodes.viewMode')}
|
||||
onClick={onClickView}
|
||||
icon={<PiEyeBold />}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,22 @@
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import InspectorPanel from './inspector/InspectorPanel';
|
||||
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
|
||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||
import { WorkflowMenu } from './WorkflowMenu';
|
||||
import { WorkflowName } from './WorkflowName';
|
||||
|
||||
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
mode: workflow.mode,
|
||||
};
|
||||
});
|
||||
|
||||
const NodeEditorPanelGroup = () => {
|
||||
const { mode } = useAppSelector(selector);
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const panelStorage = usePanelStorage();
|
||||
|
||||
const handleDoubleClickHandle = useCallback(() => {
|
||||
if (!panelGroupRef.current) {
|
||||
return;
|
||||
@@ -42,33 +27,22 @@ const NodeEditorPanelGroup = () => {
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<QueueControls />
|
||||
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
|
||||
<Flex justifyContent="space-between" alignItems="center" gap="4">
|
||||
<WorkflowLibraryButton />
|
||||
<WorkflowName />
|
||||
</Flex>
|
||||
<WorkflowMenu />
|
||||
</Flex>
|
||||
|
||||
{mode === 'view' && <WorkflowViewMode />}
|
||||
{mode === 'edit' && (
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
storage={panelStorage}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
</Panel>
|
||||
<ResizeHandle orientation="horizontal" onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
)}
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
storage={panelStorage}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
</Panel>
|
||||
<ResizeHandle orientation="horizontal" onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
|
||||
|
||||
import { ModeToggle } from './ModeToggle';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
mode: workflow.mode,
|
||||
};
|
||||
});
|
||||
|
||||
export const WorkflowMenu = () => {
|
||||
const { mode } = useAppSelector(selector);
|
||||
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
{mode === 'edit' && <SaveWorkflowButton />}
|
||||
<NewWorkflowButton />
|
||||
<ModeToggle />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDotOutlineFill } from 'react-icons/pi';
|
||||
|
||||
import WorkflowInfoTooltipContent from './viewMode/WorkflowInfoTooltipContent';
|
||||
import { WorkflowWarning } from './viewMode/WorkflowWarning';
|
||||
|
||||
export const WorkflowName = () => {
|
||||
const { name, isTouched, mode } = useAppSelector((s) => s.workflow);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex gap="1" alignItems="center">
|
||||
{name.length ? (
|
||||
<Tooltip label={<WorkflowInfoTooltipContent />} placement="top">
|
||||
<Text fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold">
|
||||
{name}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text fontSize="lg" fontStyle="italic" fontWeight="semibold">
|
||||
{t('workflows.unnamedWorkflow')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isTouched && mode === 'edit' && (
|
||||
<Tooltip label="Workflow has unsaved changes">
|
||||
<Flex>
|
||||
<Icon as={PiDotOutlineFill} boxSize="20px" sx={{ color: 'invokeYellow.500' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
<WorkflowWarning />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
|
||||
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { t } from 'i18next';
|
||||
import { memo } from 'react';
|
||||
import { PiArrowCounterClockwiseBold, PiInfoBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const WorkflowField = ({ nodeId, fieldName }: Props) => {
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'input');
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<Flex layerStyle="second" position="relative" borderRadius="base" w="full" p={4} gap="2" flexDir="column">
|
||||
<Flex alignItems="center">
|
||||
<FormLabel fontSize="sm">{label || fieldTemplateTitle}</FormLabel>
|
||||
|
||||
<Spacer />
|
||||
{isValueChanged && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
<Flex h="24px" alignItems="center">
|
||||
<Icon fontSize="md" color="base.300" as={PiInfoBold} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowField);
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Box, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
notes: workflow.notes,
|
||||
author: workflow.author,
|
||||
tags: workflow.tags,
|
||||
};
|
||||
});
|
||||
|
||||
const WorkflowInfoTooltipContent = () => {
|
||||
const { name, description, notes, author, tags } = useAppSelector(selector);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap="2">
|
||||
{!!name.length && (
|
||||
<Box>
|
||||
<Text fontWeight="semibold">{t('nodes.workflowName')}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{name}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!!author.length && (
|
||||
<Box>
|
||||
<Text fontWeight="semibold">{t('nodes.workflowAuthor')}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{author}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!!tags.length && (
|
||||
<Box>
|
||||
<Text fontWeight="semibold">{t('nodes.workflowTags')}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{tags}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!!description.length && (
|
||||
<Box>
|
||||
<Text fontWeight="semibold">{t('nodes.workflowDescription')}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!!notes.length && (
|
||||
<Box>
|
||||
<Text fontWeight="semibold">{t('nodes.workflowNotes')}</Text>
|
||||
<Text opacity={0.7} fontStyle="oblique 5deg">
|
||||
{notes}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowInfoTooltipContent);
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { t } from 'i18next';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import WorkflowField from './WorkflowField';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
fields: workflow.exposedFields,
|
||||
name: workflow.name,
|
||||
};
|
||||
});
|
||||
|
||||
export const WorkflowViewMode = () => {
|
||||
const { isLoading } = useGetOpenAPISchemaQuery();
|
||||
const { fields } = useAppSelector(selector);
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
||||
{isLoading ? (
|
||||
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
|
||||
) : fields.length ? (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<WorkflowField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
|
||||
))
|
||||
) : (
|
||||
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Flex, Icon, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||
import { PiWarningBold } from 'react-icons/pi';
|
||||
|
||||
import { WorkflowWarningTooltip } from './WorkflowWarningTooltip';
|
||||
|
||||
export const WorkflowWarning = () => {
|
||||
const nodesNeedUpdate = useGetNodesNeedUpdate();
|
||||
|
||||
if (!nodesNeedUpdate) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={<WorkflowWarningTooltip />}>
|
||||
<Flex h="full" alignItems="center" gap="2">
|
||||
<Icon color="warning.400" as={PiWarningBold} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const WorkflowWarningTooltip = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap="2">
|
||||
<Flex flexDir="column" gap="2">
|
||||
<Text fontWeight="semibold">{t('toast.loadedWithWarnings')}</Text>
|
||||
<Flex flexDir="column">
|
||||
<Text>{t('common.toResolve')}:</Text>
|
||||
<Text>
|
||||
{t('nodes.editMode')} >> {t('nodes.updateAllNodes')} >> {t('common.save')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +1,31 @@
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import DndSortable from 'features/dnd/components/DndSortable';
|
||||
import type { DragEndEvent } from 'features/dnd/types';
|
||||
import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
|
||||
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => workflow.exposedFields);
|
||||
|
||||
const WorkflowLinearTab = () => {
|
||||
const fields = useAppSelector(selector);
|
||||
const { isLoading } = useGetOpenAPISchemaQuery();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`);
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = fieldsStrings.indexOf(active.id as string);
|
||||
const newIndex = fieldsStrings.indexOf(over.id as string);
|
||||
|
||||
const newFields = arrayMove(fieldsStrings, oldIndex, newIndex)
|
||||
.map((field) => fields.find((obj) => `${obj.nodeId}.${obj.fieldName}` === field))
|
||||
.filter((field) => field) as FieldIdentifier[];
|
||||
|
||||
dispatch(workflowExposedFieldsReordered(newFields));
|
||||
}
|
||||
},
|
||||
[dispatch, fields]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<DndSortable onDragEnd={handleDragEnd} items={fields.map((field) => `${field.nodeId}.${field.fieldName}`)}>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
||||
{isLoading ? (
|
||||
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
|
||||
) : fields.length ? (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<LinearViewField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
|
||||
))
|
||||
) : (
|
||||
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
|
||||
)}
|
||||
</Flex>
|
||||
</DndSortable>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
||||
{fields.length ? (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<LinearViewField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
|
||||
))
|
||||
) : (
|
||||
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -12,17 +12,17 @@ const WorkflowPanel = () => {
|
||||
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
|
||||
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
|
||||
<TabList>
|
||||
<Tab>{t('common.details')}</Tab>
|
||||
<Tab>{t('common.linear')}</Tab>
|
||||
<Tab>{t('common.details')}</Tab>
|
||||
<Tab>JSON</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<WorkflowGeneralTab />
|
||||
<WorkflowLinearTab />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<WorkflowLinearTab />
|
||||
<WorkflowGeneralTab />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<WorkflowJSONTab />
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useFieldOriginalValue = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectOriginalExposedFieldValues = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
selectWorkflowSlice,
|
||||
(workflow) =>
|
||||
workflow.originalExposedFieldValues.find((v) => v.nodeId === nodeId && v.fieldName === fieldName)?.value
|
||||
),
|
||||
[nodeId, fieldName]
|
||||
);
|
||||
const originalValue = useAppSelector(selectOriginalExposedFieldValues);
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const isValueChanged = useMemo(() => !isEqual(value, originalValue), [value, originalValue]);
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: originalValue }));
|
||||
}, [dispatch, fieldName, nodeId, originalValue]);
|
||||
|
||||
return { originalValue, isValueChanged, onReset };
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useFieldValue = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return node?.data.inputs[fieldName]?.value;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const value = useAppSelector(selector);
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice';
|
||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import type {
|
||||
BoardFieldValue,
|
||||
@@ -18,7 +19,6 @@ import type {
|
||||
MainModelFieldValue,
|
||||
SchedulerFieldValue,
|
||||
SDXLRefinerModelFieldValue,
|
||||
StatefulFieldValue,
|
||||
StringFieldValue,
|
||||
T2IAdapterModelFieldValue,
|
||||
VAEModelFieldValue,
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
zMainModelFieldValue,
|
||||
zSchedulerFieldValue,
|
||||
zSDXLRefinerModelFieldValue,
|
||||
zStatefulFieldValue,
|
||||
zStringFieldValue,
|
||||
zT2IAdapterModelFieldValue,
|
||||
zVAEModelFieldValue,
|
||||
@@ -66,6 +65,7 @@ import {
|
||||
SelectionMode,
|
||||
updateEdge,
|
||||
} from 'reactflow';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import {
|
||||
socketGeneratorProgress,
|
||||
socketInvocationComplete,
|
||||
@@ -92,6 +92,7 @@ export const initialNodesState: NodesState = {
|
||||
_version: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
isReady: false,
|
||||
connectionStartParams: null,
|
||||
connectionStartFieldType: null,
|
||||
connectionMade: false,
|
||||
@@ -117,6 +118,7 @@ type FieldValueAction<T extends FieldValue> = PayloadAction<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
value: T;
|
||||
saveToGraph: boolean;
|
||||
}>;
|
||||
|
||||
const fieldValueReducer = <T extends FieldValue>(
|
||||
@@ -480,53 +482,52 @@ export const nodesSlice = createSlice({
|
||||
selectedEdgesChanged: (state, action: PayloadAction<string[]>) => {
|
||||
state.selectedEdges = action.payload;
|
||||
},
|
||||
fieldValueReset: (state, action: FieldValueAction<StatefulFieldValue>) => {
|
||||
fieldValueReducer(state, action, zStatefulFieldValue);
|
||||
},
|
||||
fieldStringValueChanged: (state, action: FieldValueAction<StringFieldValue>) => {
|
||||
fieldValueReducer(state, action, zStringFieldValue);
|
||||
if (action.payload.saveToGraph) {
|
||||
fieldValueReducer(state, action, zStringFieldValue);
|
||||
}
|
||||
},
|
||||
fieldNumberValueChanged: (state, action: FieldValueAction<IntegerFieldValue | FloatFieldValue>) => {
|
||||
fieldValueReducer(state, action, zIntegerFieldValue.or(zFloatFieldValue));
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zIntegerFieldValue.or(zFloatFieldValue)) };
|
||||
},
|
||||
fieldBooleanValueChanged: (state, action: FieldValueAction<BooleanFieldValue>) => {
|
||||
fieldValueReducer(state, action, zBooleanFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zBooleanFieldValue) }
|
||||
},
|
||||
fieldBoardValueChanged: (state, action: FieldValueAction<BoardFieldValue>) => {
|
||||
fieldValueReducer(state, action, zBoardFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zBoardFieldValue) }
|
||||
},
|
||||
fieldImageValueChanged: (state, action: FieldValueAction<ImageFieldValue>) => {
|
||||
fieldValueReducer(state, action, zImageFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zImageFieldValue) }
|
||||
},
|
||||
fieldColorValueChanged: (state, action: FieldValueAction<ColorFieldValue>) => {
|
||||
fieldValueReducer(state, action, zColorFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zColorFieldValue) }
|
||||
},
|
||||
fieldMainModelValueChanged: (state, action: FieldValueAction<MainModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zMainModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zMainModelFieldValue) }
|
||||
},
|
||||
fieldRefinerModelValueChanged: (state, action: FieldValueAction<SDXLRefinerModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zSDXLRefinerModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zSDXLRefinerModelFieldValue) }
|
||||
},
|
||||
fieldVaeModelValueChanged: (state, action: FieldValueAction<VAEModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zVAEModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zVAEModelFieldValue) }
|
||||
},
|
||||
fieldLoRAModelValueChanged: (state, action: FieldValueAction<LoRAModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zLoRAModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zLoRAModelFieldValue) }
|
||||
},
|
||||
fieldControlNetModelValueChanged: (state, action: FieldValueAction<ControlNetModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zControlNetModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zControlNetModelFieldValue) }
|
||||
},
|
||||
fieldIPAdapterModelValueChanged: (state, action: FieldValueAction<IPAdapterModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zIPAdapterModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zIPAdapterModelFieldValue) }
|
||||
},
|
||||
fieldT2IAdapterModelValueChanged: (state, action: FieldValueAction<T2IAdapterModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zT2IAdapterModelFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zT2IAdapterModelFieldValue) }
|
||||
},
|
||||
fieldEnumModelValueChanged: (state, action: FieldValueAction<EnumFieldValue>) => {
|
||||
fieldValueReducer(state, action, zEnumFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zEnumFieldValue) }
|
||||
},
|
||||
fieldSchedulerValueChanged: (state, action: FieldValueAction<SchedulerFieldValue>) => {
|
||||
fieldValueReducer(state, action, zSchedulerFieldValue);
|
||||
if (action.payload.saveToGraph) { fieldValueReducer(state, action, zSchedulerFieldValue) }
|
||||
},
|
||||
notesNodeValueChanged: (state, action: PayloadAction<{ nodeId: string; value: string }>) => {
|
||||
const { nodeId, value } = action.payload;
|
||||
@@ -679,6 +680,10 @@ export const nodesSlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||
state.isReady = false;
|
||||
});
|
||||
|
||||
builder.addCase(workflowLoaded, (state, action) => {
|
||||
const { nodes, edges } = action.payload;
|
||||
state.nodes = applyNodeChanges(
|
||||
@@ -750,6 +755,9 @@ export const nodesSlice = createSlice({
|
||||
});
|
||||
}
|
||||
});
|
||||
builder.addCase(nodeTemplatesBuilt, (state) => {
|
||||
state.isReady = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -765,7 +773,6 @@ export const {
|
||||
edgesChanged,
|
||||
edgesDeleted,
|
||||
edgeUpdated,
|
||||
fieldValueReset,
|
||||
fieldBoardValueChanged,
|
||||
fieldBooleanValueChanged,
|
||||
fieldColorValueChanged,
|
||||
@@ -840,6 +847,7 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
|
||||
nodeIsOpenChanged,
|
||||
nodeLabelChanged,
|
||||
nodeNotesChanged,
|
||||
nodesChanged,
|
||||
nodesDeleted,
|
||||
nodeUseCacheChanged,
|
||||
notesNodeValueChanged,
|
||||
@@ -866,6 +874,7 @@ export const nodesPersistConfig: PersistConfig<NodesState> = {
|
||||
'connectionStartFieldType',
|
||||
'selectedNodes',
|
||||
'selectedEdges',
|
||||
'isReady',
|
||||
'nodesToCopy',
|
||||
'edgesToCopy',
|
||||
'connectionMade',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/nodes/types/field';
|
||||
import type { FieldType } from 'features/nodes/types/field';
|
||||
import type {
|
||||
AnyNode,
|
||||
InvocationNodeEdge,
|
||||
@@ -26,6 +26,7 @@ export type NodesState = {
|
||||
selectedEdges: string[];
|
||||
nodeExecutionStates: Record<string, NodeExecutionState>;
|
||||
viewport: Viewport;
|
||||
isReady: boolean;
|
||||
nodesToCopy: AnyNode[];
|
||||
edgesToCopy: InvocationNodeEdge[];
|
||||
isAddNodePopoverOpen: boolean;
|
||||
@@ -33,16 +34,9 @@ export type NodesState = {
|
||||
selectionMode: SelectionMode;
|
||||
};
|
||||
|
||||
export type WorkflowMode = 'edit' | 'view';
|
||||
export type FieldIdentifierWithValue = FieldIdentifier & {
|
||||
value: StatefulFieldValue;
|
||||
};
|
||||
|
||||
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'> & {
|
||||
_version: 1;
|
||||
isTouched: boolean;
|
||||
mode: WorkflowMode;
|
||||
originalExposedFieldValues: FieldIdentifierWithValue[];
|
||||
};
|
||||
|
||||
export type NodeTemplatesState = {
|
||||
|
||||
@@ -2,16 +2,12 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||
import type {
|
||||
FieldIdentifierWithValue,
|
||||
WorkflowMode,
|
||||
WorkflowsState as WorkflowState,
|
||||
} from 'features/nodes/store/types';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { fieldStringValueChanged, isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||
import type { NodesState, WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
||||
import { zStringFieldValue, type FieldIdentifier, type FieldValue } from 'features/nodes/types/field';
|
||||
import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
|
||||
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
||||
name: '',
|
||||
@@ -28,41 +24,49 @@ export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
||||
|
||||
export const initialWorkflowState: WorkflowState = {
|
||||
_version: 1,
|
||||
isTouched: false,
|
||||
mode: 'view',
|
||||
originalExposedFieldValues: [],
|
||||
isTouched: true,
|
||||
...blankWorkflow,
|
||||
};
|
||||
|
||||
type FieldValueAction<T extends FieldValue> = PayloadAction<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
value: T;
|
||||
saveToGraph: boolean;
|
||||
}>;
|
||||
|
||||
const exposedFieldValueReducer = <T extends FieldValue>(
|
||||
state: WorkflowState,
|
||||
action: FieldValueAction<T>,
|
||||
schema: z.ZodTypeAny
|
||||
) => {
|
||||
const { nodeId, fieldName, value } = action.payload;
|
||||
const exposedField = state.exposedFields.find(field => field.nodeId === nodeId)
|
||||
|
||||
const result = schema.safeParse(value);
|
||||
if (!result || !exposedField || !result.success) {
|
||||
return;
|
||||
}
|
||||
exposedField.value = schema.safeParse(value);
|
||||
|
||||
};
|
||||
|
||||
export const workflowSlice = createSlice({
|
||||
name: 'workflow',
|
||||
initialState: initialWorkflowState,
|
||||
reducers: {
|
||||
workflowModeChanged: (state, action: PayloadAction<WorkflowMode>) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
workflowExposedFieldAdded: (state, action: PayloadAction<FieldIdentifierWithValue>) => {
|
||||
workflowExposedFieldAdded: (state, action: PayloadAction<FieldIdentifier>) => {
|
||||
state.exposedFields = uniqBy(
|
||||
state.exposedFields.concat(omit(action.payload, 'value')),
|
||||
(field) => `${field.nodeId}-${field.fieldName}`
|
||||
);
|
||||
state.originalExposedFieldValues = uniqBy(
|
||||
state.originalExposedFieldValues.concat(action.payload),
|
||||
state.exposedFields.concat(action.payload),
|
||||
(field) => `${field.nodeId}-${field.fieldName}`
|
||||
);
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowExposedFieldRemoved: (state, action: PayloadAction<FieldIdentifier>) => {
|
||||
state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload));
|
||||
state.originalExposedFieldValues = state.originalExposedFieldValues.filter(
|
||||
(field) => !isEqual(omit(field, 'value'), action.payload)
|
||||
);
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowExposedFieldsReordered: (state, action: PayloadAction<FieldIdentifier[]>) => {
|
||||
state.exposedFields = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
|
||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||
state.name = action.payload;
|
||||
state.isTouched = true;
|
||||
@@ -99,43 +103,15 @@ export const workflowSlice = createSlice({
|
||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||
state.id = action.payload;
|
||||
},
|
||||
workflowReset: () => cloneDeep(initialWorkflowState),
|
||||
workflowSaved: (state) => {
|
||||
state.isTouched = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(workflowLoaded, (state, action) => {
|
||||
const { nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
|
||||
const originalExposedFieldValues: FieldIdentifierWithValue[] = [];
|
||||
|
||||
workflowExtra.exposedFields.forEach((field) => {
|
||||
const node = nodes.find((n) => n.id === field.nodeId);
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = node.data.inputs[field.fieldName];
|
||||
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalExposedFieldValue = {
|
||||
nodeId: field.nodeId,
|
||||
fieldName: field.fieldName,
|
||||
value: input.value,
|
||||
};
|
||||
originalExposedFieldValues.push(originalExposedFieldValue);
|
||||
});
|
||||
|
||||
return {
|
||||
...cloneDeep(initialWorkflowState),
|
||||
...cloneDeep(workflowExtra),
|
||||
originalExposedFieldValues,
|
||||
mode: state.mode,
|
||||
};
|
||||
const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
return { ...initialWorkflowState, ...cloneDeep(workflowExtra) };
|
||||
});
|
||||
|
||||
builder.addCase(nodesDeleted, (state, action) => {
|
||||
@@ -146,40 +122,23 @@ export const workflowSlice = createSlice({
|
||||
|
||||
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
|
||||
|
||||
builder.addCase(nodesChanged, (state, action) => {
|
||||
// Not all changes to nodes should result in the workflow being marked touched
|
||||
const filteredChanges = action.payload.filter((change) => {
|
||||
// We always want to mark the workflow as touched if a node is added, removed, or reset
|
||||
if (['add', 'remove', 'reset'].includes(change.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Position changes can change the position and the dragging status of the node - ignore if the change doesn't
|
||||
// affect the position
|
||||
if (change.type === 'position' && (change.position || change.positionAbsolute)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This change isn't relevant
|
||||
return false;
|
||||
});
|
||||
|
||||
if (filteredChanges.length > 0) {
|
||||
state.isTouched = true;
|
||||
builder.addCase(fieldStringValueChanged, (state, action) => {
|
||||
if (!action.payload.saveToGraph) {
|
||||
exposedFieldValueReducer(state, action, zStringFieldValue);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
|
||||
state.isTouched = true;
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
workflowModeChanged,
|
||||
workflowExposedFieldAdded,
|
||||
workflowExposedFieldRemoved,
|
||||
workflowExposedFieldsReordered,
|
||||
workflowNameChanged,
|
||||
workflowCategoryChanged,
|
||||
workflowDescriptionChanged,
|
||||
@@ -189,6 +148,7 @@ export const {
|
||||
workflowVersionChanged,
|
||||
workflowContactChanged,
|
||||
workflowIDChanged,
|
||||
workflowReset,
|
||||
workflowSaved,
|
||||
} = workflowSlice.actions;
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ export const zFieldTypeBase = z.object({
|
||||
export const zFieldIdentifier = z.object({
|
||||
nodeId: z.string().trim().min(1),
|
||||
fieldName: z.string().trim().min(1),
|
||||
value: z.any().optional()
|
||||
});
|
||||
export type FieldIdentifier = z.infer<typeof zFieldIdentifier>;
|
||||
export const isFieldIdentifier = (val: unknown): val is FieldIdentifier => zFieldIdentifier.safeParse(val).success;
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
NOISE,
|
||||
NOISE_HRF,
|
||||
RESIZE_HRF,
|
||||
SEAMLESS,
|
||||
VAE_LOADER,
|
||||
} from './constants';
|
||||
import { setMetadataReceivingNode, upsertMetadata } from './metadata';
|
||||
@@ -31,6 +30,7 @@ import { setMetadataReceivingNode, upsertMetadata } from './metadata';
|
||||
// Copy certain connections from previous DENOISE_LATENTS to new DENOISE_LATENTS_HRF.
|
||||
function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void {
|
||||
const destinationFields = [
|
||||
'vae',
|
||||
'control',
|
||||
'ip_adapter',
|
||||
'metadata',
|
||||
@@ -107,10 +107,9 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
|
||||
}
|
||||
const log = logger('txt2img');
|
||||
|
||||
const { vae, seamlessXAxis, seamlessYAxis } = state.generation;
|
||||
const { vae } = state.generation;
|
||||
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
|
||||
const isAutoVae = !vae;
|
||||
const isSeamlessEnabled = seamlessXAxis || seamlessYAxis;
|
||||
const width = state.generation.width;
|
||||
const height = state.generation.height;
|
||||
const optimalDimension = selectOptimalDimension(state);
|
||||
@@ -159,7 +158,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
|
||||
},
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -260,7 +259,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
|
||||
graph.edges.push(
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -323,7 +322,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
|
||||
graph.edges.push(
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
SDXL_IMAGE_TO_IMAGE_GRAPH,
|
||||
SDXL_TEXT_TO_IMAGE_GRAPH,
|
||||
SEAMLESS,
|
||||
VAE_LOADER,
|
||||
} from './constants';
|
||||
import { upsertMetadata } from './metadata';
|
||||
|
||||
@@ -24,8 +23,7 @@ export const addSeamlessToLinearGraph = (
|
||||
modelLoaderNodeId: string
|
||||
): void => {
|
||||
// Remove Existing UNet Connections
|
||||
const { seamlessXAxis, seamlessYAxis, vae } = state.generation;
|
||||
const isAutoVae = !vae;
|
||||
const { seamlessXAxis, seamlessYAxis } = state.generation;
|
||||
|
||||
graph.nodes[SEAMLESS] = {
|
||||
id: SEAMLESS,
|
||||
@@ -34,15 +32,6 @@ export const addSeamlessToLinearGraph = (
|
||||
seamless_y: seamlessYAxis,
|
||||
} as SeamlessModeInvocation;
|
||||
|
||||
if (!isAutoVae) {
|
||||
graph.nodes[VAE_LOADER] = {
|
||||
type: 'vae_loader',
|
||||
id: VAE_LOADER,
|
||||
is_intermediate: true,
|
||||
vae_model: vae,
|
||||
};
|
||||
}
|
||||
|
||||
if (seamlessXAxis) {
|
||||
upsertMetadata(graph, {
|
||||
seamless_x: seamlessXAxis,
|
||||
@@ -86,7 +75,7 @@ export const addSeamlessToLinearGraph = (
|
||||
},
|
||||
{
|
||||
source: {
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: modelLoaderNodeId,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
SDXL_IMAGE_TO_IMAGE_GRAPH,
|
||||
SDXL_REFINER_INPAINT_CREATE_MASK,
|
||||
SDXL_TEXT_TO_IMAGE_GRAPH,
|
||||
SEAMLESS,
|
||||
TEXT_TO_IMAGE_GRAPH,
|
||||
VAE_LOADER,
|
||||
} from './constants';
|
||||
@@ -32,16 +31,15 @@ export const addVAEToGraph = (
|
||||
graph: NonNullableGraph,
|
||||
modelLoaderNodeId: string = MAIN_MODEL_LOADER
|
||||
): void => {
|
||||
const { vae, canvasCoherenceMode, seamlessXAxis, seamlessYAxis } = state.generation;
|
||||
const { vae, canvasCoherenceMode } = state.generation;
|
||||
const { boundingBoxScaleMethod } = state.canvas;
|
||||
const { refinerModel } = state.sdxl;
|
||||
|
||||
const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod);
|
||||
|
||||
const isAutoVae = !vae;
|
||||
const isSeamlessEnabled = seamlessXAxis || seamlessYAxis;
|
||||
|
||||
if (!isAutoVae && !isSeamlessEnabled) {
|
||||
if (!isAutoVae) {
|
||||
graph.nodes[VAE_LOADER] = {
|
||||
type: 'vae_loader',
|
||||
id: VAE_LOADER,
|
||||
@@ -58,7 +56,7 @@ export const addVAEToGraph = (
|
||||
) {
|
||||
graph.edges.push({
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -76,7 +74,7 @@ export const addVAEToGraph = (
|
||||
) {
|
||||
graph.edges.push({
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -94,7 +92,7 @@ export const addVAEToGraph = (
|
||||
) {
|
||||
graph.edges.push({
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -113,7 +111,7 @@ export const addVAEToGraph = (
|
||||
graph.edges.push(
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -123,7 +121,7 @@ export const addVAEToGraph = (
|
||||
},
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -133,7 +131,7 @@ export const addVAEToGraph = (
|
||||
},
|
||||
{
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -147,7 +145,7 @@ export const addVAEToGraph = (
|
||||
if (canvasCoherenceMode !== 'unmasked') {
|
||||
graph.edges.push({
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
@@ -162,7 +160,7 @@ export const addVAEToGraph = (
|
||||
if (graph.id === SDXL_CANVAS_INPAINT_GRAPH || graph.id === SDXL_CANVAS_OUTPAINT_GRAPH) {
|
||||
graph.edges.push({
|
||||
source: {
|
||||
node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER,
|
||||
field: 'vae',
|
||||
},
|
||||
destination: {
|
||||
|
||||
@@ -101,11 +101,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
const clearStorage = useClearStorage();
|
||||
|
||||
const handleOpenSettingsModel = useCallback(() => {
|
||||
if (shouldShowClearIntermediates) {
|
||||
refetchIntermediatesCount();
|
||||
}
|
||||
refetchIntermediatesCount();
|
||||
_onSettingsModalOpen();
|
||||
}, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]);
|
||||
}, [_onSettingsModalOpen, refetchIntermediatesCount]);
|
||||
|
||||
const handleClickResetWebUI = useCallback(() => {
|
||||
clearStorage();
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||
import { memo } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
const NodesTab = () => {
|
||||
const mode = useAppSelector((s) => s.workflow.mode);
|
||||
|
||||
if (mode === 'edit') {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<Flex w="full" h="full">
|
||||
<CurrentImageDisplay />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodesTab);
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFilePlusBold } from 'react-icons/pi';
|
||||
|
||||
export const NewWorkflowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderButton = useCallback(
|
||||
(onClick: () => void) => (
|
||||
<IconButton
|
||||
aria-label={t('nodes.newWorkflow')}
|
||||
tooltip={t('nodes.newWorkflow')}
|
||||
icon={<PiFilePlusBold />}
|
||||
onClick={onClick}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
),
|
||||
[t]
|
||||
);
|
||||
|
||||
return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
|
||||
});
|
||||
|
||||
NewWorkflowButton.displayName = 'NewWorkflowButton';
|
||||
@@ -1,63 +0,0 @@
|
||||
import { ConfirmationAlertDialog, Flex, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
renderButton: (onClick: () => void) => JSX.Element;
|
||||
};
|
||||
|
||||
export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const isTouched = useAppSelector((s) => s.workflow.isTouched);
|
||||
|
||||
const handleNewWorkflow = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
|
||||
dispatch(
|
||||
addToast(
|
||||
makeToast({
|
||||
title: t('workflows.newWorkflowCreated'),
|
||||
status: 'success',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [dispatch, onClose, t]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!isTouched) {
|
||||
handleNewWorkflow();
|
||||
return;
|
||||
}
|
||||
onOpen();
|
||||
}, [handleNewWorkflow, isTouched, onOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.renderButton(onClick)}
|
||||
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('nodes.newWorkflow')}
|
||||
acceptCallback={handleNewWorkflow}
|
||||
>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text>{t('nodes.newWorkflowDesc')}</Text>
|
||||
<Text variant="subtext">{t('nodes.newWorkflowDesc2')}</Text>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
NewWorkflowConfirmationAlertDialog.displayName = 'NewWorkflowConfirmationAlertDialog';
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext';
|
||||
import { useLoadWorkflowFromFile } from 'features/workflowLibrary/hooks/useLoadWorkflowFromFile';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiUploadSimpleBold } from 'react-icons/pi';
|
||||
|
||||
const UploadWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const resetRef = useRef<() => void>(null);
|
||||
const { onClose } = useWorkflowLibraryModalContext();
|
||||
const loadWorkflowFromFile = useLoadWorkflowFromFile({ resetRef, onSuccess: onClose });
|
||||
|
||||
const onDropAccepted = useCallback(
|
||||
(files: File[]) => {
|
||||
if (!files[0]) {
|
||||
return;
|
||||
}
|
||||
loadWorkflowFromFile(files[0]);
|
||||
},
|
||||
[loadWorkflowFromFile]
|
||||
);
|
||||
|
||||
const { getInputProps, getRootProps } = useDropzone({
|
||||
accept: { 'application/json': ['.json'] },
|
||||
onDropAccepted,
|
||||
noDrag: true,
|
||||
multiple: false,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('workflows.uploadWorkflow')}
|
||||
tooltip={t('workflows.uploadWorkflow')}
|
||||
leftIcon={<PiUploadSimpleBold />}
|
||||
{...getRootProps()}
|
||||
pointerEvents="auto"
|
||||
>
|
||||
{t('workflows.uploadWorkflow')}
|
||||
</Button>
|
||||
<input {...getInputProps()} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UploadWorkflowButton);
|
||||
@@ -2,7 +2,7 @@ import { IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFolderOpenBold } from 'react-icons/pi';
|
||||
import { PiBooksBold } from 'react-icons/pi';
|
||||
|
||||
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
|
||||
@@ -15,7 +15,7 @@ const WorkflowLibraryButton = () => {
|
||||
<IconButton
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
tooltip={t('workflows.workflowLibrary')}
|
||||
icon={<PiFolderOpenBold />}
|
||||
icon={<PiBooksBold />}
|
||||
onClick={disclosure.onOpen}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Combobox,
|
||||
@@ -30,8 +29,6 @@ import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types'
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { z } from 'zod';
|
||||
|
||||
import UploadWorkflowButton from './UploadWorkflowButton';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
const zOrderBy = z.enum(['opened_at', 'created_at', 'updated_at', 'name']);
|
||||
@@ -224,16 +221,11 @@ const WorkflowLibraryList = () => {
|
||||
<IAINoContentFallback label={t('workflows.noWorkflows')} />
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<Flex w="full">
|
||||
<Box flex="1">
|
||||
<UploadWorkflowButton />
|
||||
</Box>
|
||||
<Box flex="1" textAlign="center">
|
||||
{data && <WorkflowLibraryPagination data={data} page={page} setPage={setPage} />}
|
||||
</Box>
|
||||
<Box flex="1"></Box>
|
||||
</Flex>
|
||||
{data && (
|
||||
<Flex w="full" justifyContent="space-around">
|
||||
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,60 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
|
||||
import { ConfirmationAlertDialog, Flex, MenuItem, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFilePlusBold } from 'react-icons/pi';
|
||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
|
||||
export const NewWorkflowMenuItem = memo(() => {
|
||||
const NewWorkflowMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const isTouched = useAppSelector((s) => s.workflow.isTouched);
|
||||
|
||||
const renderButton = useCallback(
|
||||
(onClick: () => void) => (
|
||||
<MenuItem as="button" icon={<PiFilePlusBold />} onClick={onClick}>
|
||||
const handleNewWorkflow = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
|
||||
dispatch(
|
||||
addToast(
|
||||
makeToast({
|
||||
title: t('workflows.newWorkflowCreated'),
|
||||
status: 'success',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [dispatch, onClose, t]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!isTouched) {
|
||||
handleNewWorkflow();
|
||||
return;
|
||||
}
|
||||
onOpen();
|
||||
}, [handleNewWorkflow, isTouched, onOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem as="button" icon={<PiFlowArrowBold />} onClick={onClick}>
|
||||
{t('nodes.newWorkflow')}
|
||||
</MenuItem>
|
||||
),
|
||||
[t]
|
||||
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('nodes.newWorkflow')}
|
||||
acceptCallback={handleNewWorkflow}
|
||||
>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text>{t('nodes.newWorkflowDesc')}</Text>
|
||||
<Text variant="subtext">{t('nodes.newWorkflowDesc2')}</Text>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
|
||||
});
|
||||
|
||||
NewWorkflowMenuItem.displayName = 'NewWorkflowMenuItem';
|
||||
export default memo(NewWorkflowMenuItem);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useGlobalMenuClose,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
||||
import { NewWorkflowMenuItem } from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
|
||||
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
|
||||
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
||||
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
|
||||
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
|
||||
|
||||
@@ -10,12 +10,11 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
type useLoadWorkflowFromFileOptions = {
|
||||
resetRef: RefObject<() => void>;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
type UseLoadWorkflowFromFile = (options: useLoadWorkflowFromFileOptions) => (file: File | null) => void;
|
||||
|
||||
export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef, onSuccess }) => {
|
||||
export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const logger = useLogger('nodes');
|
||||
const { t } = useTranslation();
|
||||
@@ -32,7 +31,6 @@ export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef, onS
|
||||
const parsedJSON = JSON.parse(String(rawJSON));
|
||||
dispatch(workflowLoadRequested({ workflow: parsedJSON, asCopy: true }));
|
||||
dispatch(workflowLoadedFromFile());
|
||||
onSuccess && onSuccess();
|
||||
} catch (e) {
|
||||
// There was a problem reading the file
|
||||
logger.error(t('nodes.unableToLoadWorkflow'));
|
||||
@@ -53,7 +51,7 @@ export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef, onS
|
||||
// Reset the file picker internal state so that the same file can be loaded again
|
||||
resetRef.current?.();
|
||||
},
|
||||
[dispatch, logger, resetRef, t, onSuccess]
|
||||
[dispatch, logger, resetRef, t]
|
||||
);
|
||||
|
||||
return loadWorkflowFromFile;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import type { paths } from 'services/api/schema';
|
||||
import type { AppConfig, AppDependencyVersions, AppVersion } from 'services/api/types';
|
||||
|
||||
@@ -59,14 +57,6 @@ export const appInfoApi = api.injectEndpoints({
|
||||
}),
|
||||
invalidatesTags: ['InvocationCacheStatus'],
|
||||
}),
|
||||
getOpenAPISchema: build.query<OpenAPIV3_1.Document, void>({
|
||||
query: () => {
|
||||
const openAPISchemaUrl = $openAPISchemaUrl.get();
|
||||
const url = openAPISchemaUrl ? openAPISchemaUrl : `${window.location.href.replace(/\/$/, '')}/openapi.json`;
|
||||
return url;
|
||||
},
|
||||
providesTags: ['Schema'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -78,6 +68,4 @@ export const {
|
||||
useDisableInvocationCacheMutation,
|
||||
useEnableInvocationCacheMutation,
|
||||
useGetInvocationCacheStatusQuery,
|
||||
useGetOpenAPISchemaQuery,
|
||||
useLazyGetOpenAPISchemaQuery,
|
||||
} = appInfoApi;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FetchBaseQueryArgs } from '@reduxjs/toolkit/dist/query/fetchBaseQuery';
|
||||
import type { BaseQueryFn, FetchArgs, FetchBaseQueryError, TagDescription } from '@reduxjs/toolkit/query/react';
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
@@ -36,7 +35,6 @@ export const tagTypes = [
|
||||
'SDXLRefinerModel',
|
||||
'Workflow',
|
||||
'WorkflowsRecent',
|
||||
'Schema',
|
||||
// This is invalidated on reconnect. It should be used for queries that have changing data,
|
||||
// especially related to the queue and generation.
|
||||
'FetchOnReconnect',
|
||||
@@ -53,7 +51,7 @@ const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryE
|
||||
const authToken = $authToken.get();
|
||||
const projectId = $projectId.get();
|
||||
|
||||
const fetchBaseQueryArgs: FetchBaseQueryArgs = {
|
||||
const rawBaseQuery = fetchBaseQuery({
|
||||
baseUrl: baseUrl ? `${baseUrl}/api/v1` : `${window.location.href.replace(/\/$/, '')}/api/v1`,
|
||||
prepareHeaders: (headers) => {
|
||||
if (authToken) {
|
||||
@@ -65,17 +63,7 @@ const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryE
|
||||
|
||||
return headers;
|
||||
},
|
||||
};
|
||||
|
||||
// When fetching the openapi.json, we need to remove circular references from the JSON.
|
||||
if (
|
||||
(args instanceof Object && args.url.includes('openapi.json')) ||
|
||||
(typeof args === 'string' && args.includes('openapi.json'))
|
||||
) {
|
||||
fetchBaseQueryArgs.jsonReplacer = getCircularReplacer();
|
||||
}
|
||||
|
||||
const rawBaseQuery = fetchBaseQuery(fetchBaseQueryArgs);
|
||||
});
|
||||
|
||||
return rawBaseQuery(args, api, extraOptions);
|
||||
};
|
||||
@@ -86,25 +74,3 @@ export const api = createApi({
|
||||
tagTypes,
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
||||
function getCircularReplacer() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ancestors: Record<string, any>[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (key: string, value: any) {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value;
|
||||
}
|
||||
// `this` is the object that value is contained in, i.e., its direct parent.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore don't think it's possible to not have TS complain about this...
|
||||
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
||||
ancestors.pop();
|
||||
}
|
||||
if (ancestors.includes(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
ancestors.push(value);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
40
invokeai/frontend/web/src/services/api/thunks/schema.ts
Normal file
40
invokeai/frontend/web/src/services/api/thunks/schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
|
||||
|
||||
function getCircularReplacer() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ancestors: Record<string, any>[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (key: string, value: any) {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value;
|
||||
}
|
||||
// `this` is the object that value is contained in, i.e., its direct parent.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore don't think it's possible to not have TS complain about this...
|
||||
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
||||
ancestors.pop();
|
||||
}
|
||||
if (ancestors.includes(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
ancestors.push(value);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
export const receivedOpenAPISchema = createAsyncThunk('nodes/receivedOpenAPISchema', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const openAPISchemaUrl = $openAPISchemaUrl.get();
|
||||
|
||||
const url = openAPISchemaUrl ? openAPISchemaUrl : `${window.location.href.replace(/\/$/, '')}/openapi.json`;
|
||||
const response = await fetch(url);
|
||||
const openAPISchema = await response.json();
|
||||
|
||||
const schemaJSON = JSON.parse(JSON.stringify(openAPISchema, getCircularReplacer()));
|
||||
|
||||
return schemaJSON;
|
||||
} catch (error) {
|
||||
return rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.7.0"
|
||||
__version__ = "3.6.3"
|
||||
|
||||
@@ -33,11 +33,11 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
# Core generation dependencies, pinned for reproducible builds.
|
||||
"accelerate==0.27.2",
|
||||
"accelerate==0.26.1",
|
||||
"clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip",
|
||||
"compel==2.0.2",
|
||||
"controlnet-aux==0.0.7",
|
||||
"diffusers[torch]==0.26.3",
|
||||
"diffusers[torch]==0.26.2",
|
||||
"invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids
|
||||
"mediapipe==0.10.7", # needed for "mediapipeface" controlnet model
|
||||
"numpy==1.26.4", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal()
|
||||
|
||||
Reference in New Issue
Block a user