Compare commits

..

1 Commits

Author SHA1 Message Date
22a1a721d7 {release} 3.6.4 2024-02-13 11:52:32 -07:00
76 changed files with 397 additions and 1447 deletions

View File

@ -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 ------------------------------------

View File

@ -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"

View File

@ -32,7 +32,6 @@ To use a community workflow, download the the `.json` node graph file and load i
+ [Image to Character Art Image Nodes](#image-to-character-art-image-nodes)
+ [Image Picker](#image-picker)
+ [Image Resize Plus](#image-resize-plus)
+ [Latent Upscale](#latent-upscale)
+ [Load Video Frame](#load-video-frame)
+ [Make 3D](#make-3d)
+ [Mask Operations](#mask-operations)
@ -291,13 +290,6 @@ View:
</br><img src="https://raw.githubusercontent.com/VeyDlin/image-resize-plus-node/master/.readme/node.png" width="500" />
--------------------------------
### Latent Upscale
**Description:** This node uses a small (~2.4mb) model to upscale the latents used in a Stable Diffusion 1.5 or Stable Diffusion XL image generation, rather than the typical interpolation method, avoiding the traditional downsides of the latent upscale technique.
**Node Link:** [https://github.com/gogurtenjoyer/latent-upscale](https://github.com/gogurtenjoyer/latent-upscale)
--------------------------------
### Load Video Frame

View File

@ -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]:

View File

@ -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:

View File

@ -287,14 +287,6 @@ class ModelCache(object):
if torch.device(source_device).type == torch.device(target_device).type:
return
if target_device.type == "cuda":
vram_device = (
target_device if target_device.index is not None else torch.device(str(target_device), index=0)
)
free_mem, _ = torch.cuda.mem_get_info(torch.device(vram_device))
if cache_entry.size > free_mem:
raise torch.cuda.OutOfMemoryError
start_model_to_time = time.time()
snapshot_before = self._capture_memory_snapshot()
cache_entry.model.to(target_device)
@ -364,10 +356,6 @@ class ModelCache(object):
self.cache.logger.debug(f"Locking {self.key} in {self.cache.execution_device}")
self.cache._print_cuda_stats()
except torch.cuda.OutOfMemoryError:
self.cache.logger.warning("Out of GPU memory encountered.")
self.cache_entry.unlock()
raise
except Exception:
self.cache_entry.unlock()
raise
@ -536,6 +524,7 @@ class ModelCache(object):
break
if not cache_entry.locked and cache_entry.loaded:
self._move_model_to_device(model_key, self.storage_device)
vram_in_use = torch.cuda.memory_allocated()
self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB")

View File

@ -30,8 +30,6 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe
# 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
for m_name, m in model.named_modules():
if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
continue
@ -42,7 +40,8 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe
block_num = int(block_num)
resnet_num = int(resnet_num)
if block_num >= len(model.down_blocks) - skipped_layers:
# Could be configurable to allow skipping arbitrary numbers of down blocks
if block_num >= len(model.down_blocks):
continue
# Skip the second resnet (could be configurable)

View File

@ -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",

View File

@ -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:

View File

@ -81,7 +81,7 @@
"outputs": "Ausgabe",
"data": "Daten",
"safetensors": "Safe-Tensors",
"outpaint": "Outpaint (Außen ausmalen)",
"outpaint": "Ausmalen",
"details": "Details",
"format": "Format",
"unknown": "Unbekannt",
@ -110,18 +110,17 @@
"nextPage": "Nächste Seite",
"unknownError": "Unbekannter Fehler",
"unsaved": "Nicht gespeichert",
"aboutDesc": "Verwenden Sie Invoke für die Arbeit? Siehe hier:",
"aboutDesc": "Verwenden Sie Invoke für die Arbeit? Dann siehe hier:",
"localSystem": "Lokales System",
"orderBy": "Ordnen nach",
"saveAs": "Speichern als",
"saveAs": "Speicher als",
"updated": "Aktualisiert",
"copy": "Kopieren",
"aboutHeading": "Nutzen Sie Ihre kreative Energie",
"toResolve": "Lösen"
"aboutHeading": "Nutzen Sie Ihre kreative Energie"
},
"gallery": {
"generations": "Erzeugungen",
"showGenerations": "Zeige Ergebnisse",
"showGenerations": "Zeige Erzeugnisse",
"uploads": "Uploads",
"showUploads": "Zeige Uploads",
"galleryImageSize": "Bildgröße",
@ -151,9 +150,9 @@
"problemDeletingImagesDesc": "Ein oder mehrere Bilder konnten nicht gelöscht werden",
"starImage": "Bild markieren",
"assets": "Ressourcen",
"unstarImage": "Markierung entfernen",
"unstarImage": "Markierung Entfernen",
"image": "Bild",
"deleteSelection": "Lösche Auswahl",
"deleteSelection": "Lösche markierte",
"dropToUpload": "$t(gallery.drop) zum hochladen",
"dropOrUpload": "$t(gallery.drop) oder hochladen",
"drop": "Ablegen",
@ -591,18 +590,10 @@
"general": "Allgemein",
"hiresStrength": "High Res Stärke",
"hidePreview": "Verstecke Vorschau",
"showPreview": "Zeige Vorschau",
"aspect": "Seitenverhältnis",
"aspectRatio": "Seitenverhältnis",
"scheduler": "Planer",
"aspectRatioFree": "Frei",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (kann zu groß sein)",
"lockAspectRatio": "Seitenverhältnis sperren",
"swapDimensions": "Seitenverhältnis umkehren",
"setToOptimalSize": "Optimiere Größe für Modell"
"showPreview": "Zeige Vorschau"
},
"settings": {
"displayInProgress": "Zwischenbilder anzeigen",
"displayInProgress": "Bilder in Bearbeitung anzeigen",
"saveSteps": "Speichern der Bilder alle n Schritte",
"confirmOnDelete": "Bestätigen beim Löschen",
"displayHelpIcons": "Hilfesymbole anzeigen",
@ -615,34 +606,7 @@
"useSlidersForAll": "Schieberegler für alle Optionen verwenden",
"showAdvancedOptions": "Erweiterte Optionen anzeigen",
"alternateCanvasLayout": "Alternatives Leinwand-Layout",
"clearIntermediatesDesc1": "Das Löschen der Zwischenbilder setzt Leinwand und ControlNet zurück.",
"favoriteSchedulers": "Lieblings-Planer",
"favoriteSchedulersPlaceholder": "Keine Planer favorisiert",
"generation": "Erzeugung",
"enableInformationalPopovers": "Info-Popouts anzeigen",
"shouldLogToConsole": "Konsole loggen",
"showProgressInViewer": "Zwischenbilder im Viewer anzeigen",
"clearIntermediatesDesc3": "Ihre Bilder werden nicht gelöscht.",
"clearIntermediatesWithCount_one": "Lösche {{count}} Zwischenbilder",
"clearIntermediatesWithCount_other": "Lösche {{count}} Zwischenbilder",
"reloadingIn": "Neuladen in",
"enableNodesEditor": "Nodes Editor aktivieren",
"autoChangeDimensions": "Breite/Höhe auf Modellstandard setzen",
"experimental": "Experimentell",
"intermediatesCleared_one": "{{count}} Zwischenbilder gelöscht",
"intermediatesCleared_other": "{{count}} Zwischenbilder gelöscht",
"enableInvisibleWatermark": "Unsichtbares Wasserzeichen aktivieren",
"general": "Allgemein",
"consoleLogLevel": "Protokollierungsstufe",
"clearIntermediatesDisabled": "Warteschlange muss leer sein, um Zwischenbilder zu löschen",
"developer": "Entwickler",
"antialiasProgressImages": "Zwischenbilder mit Anti-Alias",
"beta": "Beta",
"ui": "Benutzeroberfläche",
"clearIntermediatesDesc2": "Zwischenbilder sind Nebenprodukte der Erstellung. Sie zu löschen macht Festplattenspeicher frei.",
"clearIntermediates": "Zwischenbilder löschen",
"intermediatesClearedFailed": "Problem beim Löschen der Zwischenbilder",
"enableNSFWChecker": "Auf unangemessene Inhalte prüfen"
"clearIntermediatesDesc1": "Das Löschen der Zwischenprodukte setzt Leinwand und ControlNet zurück."
},
"toast": {
"tempFoldersEmptied": "Temp-Ordner geleert",
@ -687,9 +651,7 @@
"problemCopyingCanvas": "Problem beim Kopieren der Leinwand",
"problemCopyingCanvasDesc": "Kann Basis-Layer nicht exportieren",
"problemDownloadingCanvas": "Problem beim Herunterladen der Leinwand",
"setAsCanvasInitialImage": "Als Ausgangsbild gesetzt",
"addedToBoard": "Dem Board hinzugefügt",
"loadedWithWarnings": "Workflow mit Warnungen geladen"
"setAsCanvasInitialImage": "Als Ausgangsbild gesetzt"
},
"tooltip": {
"feature": {
@ -771,23 +733,23 @@
"accessibility": {
"modelSelect": "Modell-Auswahl",
"uploadImage": "Bild hochladen",
"previousImage": "Vorheriges Bild",
"previousImage": "Voriges Bild",
"useThisParameter": "Benutze diesen Parameter",
"copyMetadataJson": "Kopiere JSON-Metadaten",
"copyMetadataJson": "Kopiere Metadaten JSON",
"zoomIn": "Vergrößern",
"rotateClockwise": "Im Uhrzeigersinn drehen",
"flipHorizontally": "Horizontal drehen",
"flipVertically": "Vertikal drehen",
"modifyConfig": "Optionen einstellen",
"toggleAutoscroll": "Auroscroll ein/ausschalten",
"toggleLogViewer": "Log-Betrachter ein/ausschalten",
"toggleLogViewer": "Log Betrachter ein/ausschalten",
"showOptionsPanel": "Seitenpanel anzeigen",
"reset": "Zurücksetzten",
"nextImage": "Nächstes Bild",
"zoomOut": "Verkleinern",
"rotateCounterClockwise": "Gegen den Uhrzeigersinn drehen",
"showGalleryPanel": "Galerie-Panel anzeigen",
"exitViewer": "Betrachter beenden",
"showGalleryPanel": "Galeriefenster anzeigen",
"exitViewer": "Betrachten beenden",
"menu": "Menü",
"loadMore": "Mehr laden",
"invokeProgressBar": "Invoke Fortschrittsanzeige",
@ -797,7 +759,7 @@
"about": "Über"
},
"boards": {
"autoAddBoard": "Automatisches Hinzufügen zum Board",
"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",
@ -806,13 +768,13 @@
"noMatching": "Keine passenden Ordner",
"selectBoard": "Ordner aussuchen",
"cancel": "Abbrechen",
"addBoard": "Board hinzufügen",
"addBoard": "Ordner hinzufügen",
"uncategorized": "Ohne Kategorie",
"downloadBoard": "Ordner runterladen",
"changeBoard": "Ordner wechseln",
"loading": "Laden...",
"clearSearch": "Suche leeren",
"bottomMessage": "Löschen des Boards und seiner Bilder setzt alle Funktionen zurück, die sie gerade verwenden.",
"bottomMessage": "Durch das Löschen dieses Ordners und seiner Bilder werden alle Funktionen zurückgesetzt, die sie derzeit verwenden.",
"deleteBoardOnly": "Nur Ordner löschen",
"deleteBoard": "Löschen Ordner",
"deleteBoardAndImages": "Löschen Ordner und Bilder",
@ -903,15 +865,11 @@
"maxFaces": "Maximale Anzahl Gesichter",
"resizeSimple": "Größe ändern (einfach)",
"large": "Groß",
"modelSize": "Modellgröße",
"modelSize": "Modell Größe",
"small": "Klein",
"base": "Basis",
"depthAnything": "Depth Anything",
"depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth-Anything-Technik",
"face": "Gesicht",
"body": "Körper",
"hands": "Hände",
"dwOpenpose": "DW Openpose"
"depthAnything": "Depth Anything / \"Tiefe irgendwas\"",
"depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth Anything-Technik"
},
"queue": {
"status": "Status",
@ -946,7 +904,7 @@
"batchValues": "Stapel Werte",
"queueCountPrediction": "{{promptsCount}} Prompts × {{iterations}} Iterationen -> {{count}} Generationen",
"queuedCount": "{{pending}} wartenden Elemente",
"clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.",
"clearQueueAlertDialog": "Die Warteschlange leeren, stoppt den aktuellen Prozess und leert die Warteschlange komplett.",
"completedIn": "Fertig in",
"cancelBatchSucceeded": "Stapel abgebrochen",
"cancelBatch": "Stapel stoppen",
@ -955,20 +913,20 @@
"cancelBatchFailed": "Problem beim Abbruch vom Stapel",
"clearQueueAlertDialog2": "Warteschlange wirklich leeren?",
"pruneSucceeded": "{{item_count}} abgeschlossene Elemente aus der Warteschlange entfernt",
"pauseSucceeded": "Prozess angehalten",
"pauseSucceeded": "Prozessor angehalten",
"cancelFailed": "Problem beim Stornieren des Auftrags",
"pauseFailed": "Problem beim Anhalten des Prozesses",
"pauseFailed": "Problem beim Anhalten des Prozessors",
"front": "Vorne",
"pruneTooltip": "Bereinigen Sie {{item_count}} abgeschlossene Aufträge",
"resumeFailed": "Problem beim Fortsetzen des Prozesses",
"resumeFailed": "Problem beim wieder aufnehmen von Prozessor",
"pruneFailed": "Problem beim leeren der Warteschlange",
"pauseTooltip": "Prozess anhalten",
"pauseTooltip": "Pause von Prozessor",
"back": "Hinten",
"resumeSucceeded": "Prozess wird fortgesetzt",
"resumeTooltip": "Prozess wieder aufnehmen",
"resumeSucceeded": "Prozessor wieder aufgenommen",
"resumeTooltip": "Prozessor wieder aufnehmen",
"time": "Zeit",
"batchQueuedDesc_one": "{{count}} Eintrag an {{direction}} der Wartschlange hinzugefügt",
"batchQueuedDesc_other": "{{count}} Einträge an {{direction}} der Wartschlange hinzugefügt",
"batchQueuedDesc_one": "{{count}} Eintrag ans {{direction}} der Wartschlange hinzugefügt",
"batchQueuedDesc_other": "{{count}} Einträge ans {{direction}} der Wartschlange hinzugefügt",
"openQueue": "Warteschlange öffnen",
"batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung",
"batchFieldValues": "Stapelverarbeitungswerte",
@ -1007,7 +965,7 @@
},
"popovers": {
"noiseUseCPU": {
"heading": "Nutze CPU-Rauschen",
"heading": "Nutze Prozessor rauschen",
"paragraphs": [
"Entscheidet, ob auf der CPU oder GPU Rauschen erzeugt wird.",
"Mit aktiviertem CPU-Rauschen wird ein bestimmter Seedwert das gleiche Bild auf jeder Maschine erzeugen.",
@ -1017,7 +975,8 @@
"paramModel": {
"heading": "Modell",
"paragraphs": [
"Modell für die Entrauschungsschritte."
"Modell für die Entrauschungsschritte.",
"Verschiedene Modelle werden in der Regel so trainiert, dass sie sich auf die Erzeugung bestimmter Ästhetik und/oder Inhalte spezialisiert."
]
},
"paramIterations": {
@ -1125,18 +1084,12 @@
"Wie stark wird das ControlNet das generierte Bild beeinflussen wird."
],
"heading": "Einfluss"
},
"paramScheduler": {
"paragraphs": [
"\"Planer\" definiert, wie iterativ Rauschen zu einem Bild hinzugefügt wird, oder wie ein Sample bei der Ausgabe eines Modells aktualisiert wird."
],
"heading": "Planer"
}
},
"ui": {
"lockRatio": "Verhältnis sperren",
"hideProgressImages": "Fortschrittsbilder verbergen",
"showProgressImages": "Fortschrittsbilder anzeigen",
"hideProgressImages": "Verstecke Prozess Bild",
"showProgressImages": "Zeige Prozess Bild",
"swapSizes": "Tausche Größen"
},
"invocationCache": {
@ -1334,19 +1287,7 @@
"vaeFieldDescription": "VAE Submodell.",
"unknownInput": "Unbekannte Eingabe: {{name}}",
"unknownNodeType": "Unbekannter Knotentyp",
"float": "Kommazahlen",
"latentsPolymorphic": "Latents Polymorph",
"integerPolymorphicDescription": "Eine Sammlung von ganzen Zahlen.",
"integerPolymorphic": "Ganze Zahl Polymorph",
"ipAdapterPolymorphic": "IP-Adapter Polymorph",
"floatPolymorphic": "Fließkommazahl Polymorph",
"enumDescription": "Aufzählungen sind Werte, die eine von mehreren Optionen sein können.",
"floatCollection": "Fließkommazahl Sammlung",
"enum": "Aufzählung",
"floatPolymorphicDescription": "Eine Sammlung von Fließkommazahlen",
"fullyContainNodes": "Vollständig ausgewählte Nodes auswählen",
"editMode": "Im Workflow-Editor bearbeiten",
"floatCollectionDescription": "Eine Sammlung von Fließkommazahlen"
"float": "Kommazahlen"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",
@ -1395,12 +1336,12 @@
},
"control": {
"title": "Kontrolle",
"controlAdaptersTab": "Kontroll-Adapter",
"ipTab": "Bild-Prompts"
"controlAdaptersTab": "Kontroll Adapter",
"ipTab": "Bild Beschreibung"
},
"compositing": {
"coherenceTab": "Kohärenzpass",
"infillTab": "Füllung / Infill",
"infillTab": "Füllung",
"title": "Compositing"
}
},
@ -1438,15 +1379,5 @@
},
"app": {
"storeNotInitialized": "App-Store ist nicht initialisiert"
},
"sdxl": {
"concatPromptStyle": "Verknüpfen von Prompt & Stil",
"scheduler": "Planer",
"steps": "Schritte",
"useRefiner": "Refiner verwenden",
"selectAModel": "Modell auswählen"
},
"dynamicPrompts": {
"showDynamicPrompts": "Dynamische Prompts anzeigen"
}
}

View File

@ -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",
@ -1424,8 +1419,9 @@
"clipSkip": {
"heading": "CLIP Skip",
"paragraphs": [
"How many layers of the CLIP model to skip.",
"Certain models are better suited to be used with CLIP Skip."
"Choose how many layers of the CLIP model to skip.",
"Some models work better with certain CLIP Skip settings.",
"A higher value typically results in a less detailed image."
]
},
"paramNegativeConditioning": {
@ -1445,8 +1441,7 @@
"paramScheduler": {
"heading": "Scheduler",
"paragraphs": [
"Scheduler used during the generation process.",
"Each scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output."
"Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output."
]
},
"compositingBlur": {
@ -1463,52 +1458,47 @@
},
"compositingCoherenceMode": {
"heading": "Mode",
"paragraphs": ["Method used to create a coherent image with the newly generated masked area."]
"paragraphs": ["The mode of the Coherence Pass."]
},
"compositingCoherenceSteps": {
"heading": "Steps",
"paragraphs": ["Number of steps in the Coherence Pass.", "Similar to Generation Steps."]
"paragraphs": ["Number of denoising steps used in the Coherence Pass.", "Same as the main Steps parameter."]
},
"compositingStrength": {
"heading": "Strength",
"paragraphs": ["Amount of noise added for the Coherence Pass.", "Similar to Denoising Strength."]
"paragraphs": [
"Denoising strength for the Coherence Pass.",
"Same as the Image to Image Denoising Strength parameter."
]
},
"compositingMaskAdjustments": {
"heading": "Mask Adjustments",
"paragraphs": ["Adjust the mask."]
},
"controlNetBeginEnd": {
"heading": "Begin / End Step Percentage",
"paragraphs": [
"Which steps of the denoising process will have the ControlNet applied.",
"ControlNets applied at the beginning of the process guide composition, and ControlNets applied at the end guide details."
]
},
"controlNetControlMode": {
"heading": "Control Mode",
"paragraphs": ["Lends more weight to either the prompt or ControlNet."]
},
"controlNetResizeMode": {
"heading": "Resize Mode",
"paragraphs": ["How the ControlNet image will be fit to the image output size."]
},
"controlNet": {
"heading": "ControlNet",
"paragraphs": [
"ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected."
]
},
"controlNetBeginEnd": {
"heading": "Begin / End Step Percentage",
"paragraphs": [
"The part of the of the denoising process that will have the Control Adapter applied.",
"Generally, Control Adapters applied at the start of the process guide composition, and Control Adapters applied at the end guide details."
]
},
"controlNetControlMode": {
"heading": "Control Mode",
"paragraphs": ["Lend more weight to either the prompt or ControlNet."]
},
"controlNetProcessor": {
"heading": "Processor",
"paragraphs": [
"Method of processing the input image to guide the generation process. Different processors will providedifferent effects or styles in your generated images."
]
},
"controlNetResizeMode": {
"heading": "Resize Mode",
"paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."]
},
"controlNetWeight": {
"heading": "Weight",
"paragraphs": [
"Weight of the Control Adapter. Higher weight will lead to larger impacts on the final image."
]
"paragraphs": ["How strongly the ControlNet will impact the generated image."]
},
"dynamicPrompts": {
"heading": "Dynamic Prompts",
@ -1531,23 +1521,13 @@
"Per Image will use a unique seed for each image. This provides more variation."
]
},
"imageFit": {
"heading": "Fit Initial Image to Output Size",
"paragraphs": [
"Resizes the initial image to the width and height of the output image. Recommended to enable."
]
},
"infillMethod": {
"heading": "Infill Method",
"paragraphs": ["Method of infilling during the Outpainting or Inpainting process."]
"paragraphs": ["Method to infill the selected area."]
},
"lora": {
"heading": "LoRA",
"paragraphs": ["Lightweight models that are used in conjunction with base models."]
},
"loraWeight": {
"heading": "Weight",
"paragraphs": ["Weight of the LoRA. Higher weight will lead to larger impacts on the final image."]
"heading": "LoRA Weight",
"paragraphs": ["Higher LoRA weight will lead to larger impacts on the final image."]
},
"noiseUseCPU": {
"heading": "Use CPU Noise",
@ -1557,25 +1537,14 @@
"There is no performance impact to enabling CPU Noise."
]
},
"paramAspect": {
"heading": "Aspect",
"paragraphs": [
"Aspect ratio of the generated image. Changing the ratio will update the Width and Height accordingly.",
"“Optimize” will set the Width and Height to optimal dimensions for the chosen model."
]
},
"paramCFGScale": {
"heading": "CFG Scale",
"paragraphs": [
"Controls how much the prompt influences the generation process.",
"High CFG Scale values can result in over-saturation and distorted generation results. "
]
"paragraphs": ["Controls how much your prompt influences the generation process."]
},
"paramCFGRescaleMultiplier": {
"heading": "CFG Rescale Multiplier",
"paragraphs": [
"Rescale multiplier for CFG guidance, used for models trained using zero-terminal SNR (ztsnr).",
"Suggested value of 0.7 for these models."
"Rescale multiplier for CFG guidance, used for models trained using zero-terminal SNR (ztsnr). Suggested value 0.7."
]
},
"paramDenoisingStrength": {
@ -1585,16 +1554,6 @@
"0 will result in an identical image, while 1 will result in a completely new image."
]
},
"paramHeight": {
"heading": "Height",
"paragraphs": ["Height of the generated image. Must be a multiple of 8."]
},
"paramHrf": {
"heading": "Enable High Resolution Fix",
"paragraphs": [
"Generate high quality images at a larger resolution than optimal for the model. Generally used to prevent duplication in the generated image."
]
},
"paramIterations": {
"heading": "Iterations",
"paragraphs": [
@ -1605,7 +1564,8 @@
"paramModel": {
"heading": "Model",
"paragraphs": [
"Model used for generation. Different models are trained to specialize in producing different aesthetic results and content."
"Model used for the denoising steps.",
"Different models are typically trained to specialize in producing particular aesthetic results and content."
]
},
"paramRatio": {
@ -1619,7 +1579,7 @@
"heading": "Seed",
"paragraphs": [
"Controls the starting noise used for generation.",
"Disable the “Random” option to produce identical results with the same generation settings."
"Disable “Random Seed” to produce identical results with the same generation settings."
]
},
"paramSteps": {
@ -1629,10 +1589,6 @@
"Higher step counts will typically create better images but will require more generation time."
]
},
"paramUpscaleMethod": {
"heading": "Upscale Method",
"paragraphs": ["Method used to upscale the image for High Resolution Fix."]
},
"paramVAE": {
"heading": "VAE",
"paragraphs": ["Model used for translating AI output into the final image."]
@ -1640,82 +1596,14 @@
"paramVAEPrecision": {
"heading": "VAE Precision",
"paragraphs": [
"The precision used during VAE encoding and decoding.",
"Fp16/Half precision is more efficient, at the expense of minor image variations."
]
},
"paramWidth": {
"heading": "Width",
"paragraphs": ["Width of the generated image. Must be a multiple of 8."]
},
"patchmatchDownScaleSize": {
"heading": "Downscale",
"paragraphs": [
"How much downscaling occurs before infilling.",
"Higher downscaling will improve performance and reduce quality."
]
},
"refinerModel": {
"heading": "Refiner Model",
"paragraphs": [
"Model used during the refiner portion of the generation process.",
"Similar to the Generation Model."
]
},
"refinerPositiveAestheticScore": {
"heading": "Positive Aesthetic Score",
"paragraphs": [
"Weight generations to be more similar to images with a high aesthetic score, based on the training data."
]
},
"refinerNegativeAestheticScore": {
"heading": "Negative Aesthetic Score",
"paragraphs": [
"Weight generations to be more similar to images with a low aesthetic score, based on the training data."
]
},
"refinerScheduler": {
"heading": "Scheduler",
"paragraphs": [
"Scheduler used during the refiner portion of the generation process.",
"Similar to the Generation Scheduler."
]
},
"refinerStart": {
"heading": "Refiner Start",
"paragraphs": [
"Where in the generation process the refiner will start to be used.",
"0 means the refiner will be used for the entire generation process, 0.8 means the refiner will be used for the last 20% of the generation process."
]
},
"refinerSteps": {
"heading": "Steps",
"paragraphs": [
"Number of steps that will be performed during the refiner portion of the generation process.",
"Similar to the Generation Steps."
]
},
"refinerCfgScale": {
"heading": "CFG Scale",
"paragraphs": [
"Controls how much the prompt influences the generation process.",
"Similar to the Generation CFG Scale."
"The precision used during VAE encoding and decoding. FP16/half precision is more efficient, at the expense of minor image variations."
]
},
"scaleBeforeProcessing": {
"heading": "Scale Before Processing",
"paragraphs": [
"“Auto” scales the selected area to the size best suited for the model before the image generation process.",
"“Manual” allows you to choose the width and height the selected area will be scaled to before the image generation process."
"Scales the selected area to the size best suited for the model before the image generation process."
]
},
"seamlessTilingXAxis": {
"heading": "Seamless Tiling X Axis",
"paragraphs": ["Seamlessly tile an image along the horizontal axis."]
},
"seamlessTilingYAxis": {
"heading": "Seamless Tiling Y Axis",
"paragraphs": ["Seamlessly tile an image along the vertical axis."]
}
},
"ui": {

View File

@ -1138,11 +1138,7 @@
"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.",
"viewMode": "Utilizzare nella vista lineare",
"reorderLinearView": "Riordina la vista lineare",
"editMode": "Modifica nell'editor del flusso di lavoro",
"resetToDefaultValue": "Ripristina il valore predefinito"
"clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate."
},
"boards": {
"autoAddBoard": "Aggiungi automaticamente bacheca",
@ -1245,11 +1241,7 @@
"large": "Grande",
"small": "Piccolo",
"depthAnythingDescription": "Generazione di mappe di profondità utilizzando la tecnica Depth Anything",
"modelSize": "Dimensioni del modello",
"dwOpenposeDescription": "Stima della posa umana utilizzando DW Openpose",
"face": "Viso",
"body": "Corpo",
"hands": "Mani"
"modelSize": "Dimensioni del modello"
},
"queue": {
"queueFront": "Aggiungi all'inizio della coda",
@ -1379,8 +1371,7 @@
"popovers": {
"paramScheduler": {
"paragraphs": [
"Il campionatore utilizzato durante il processo di generazione.",
"Ciascun campionatore definisce come aggiungere in modo iterativo il rumore a un'immagine o come aggiornare un campione in base all'output di un modello."
"Il campionatore definisce come aggiungere in modo iterativo il rumore a un'immagine o come aggiornare un campione in base all'output di un modello."
],
"heading": "Campionatore"
},
@ -1393,8 +1384,8 @@
"compositingCoherenceSteps": {
"heading": "Passi",
"paragraphs": [
"Numero di passi utilizzati nel Passaggio di Coerenza.",
"Simile ai passi di generazione."
"Numero di passi di riduzione del rumore utilizzati nel Passaggio di Coerenza.",
"Uguale al parametro principale Passi."
]
},
"compositingBlur": {
@ -1406,13 +1397,14 @@
"compositingCoherenceMode": {
"heading": "Modalità",
"paragraphs": [
"Metodo utilizzato per creare un'immagine coerente con l'area mascherata appena generata."
"La modalità del Passaggio di Coerenza."
]
},
"clipSkip": {
"paragraphs": [
"Scegli quanti livelli del modello CLIP saltare.",
"Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip."
"Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip.",
"Un valore più alto in genere produce un'immagine meno dettagliata."
]
},
"compositingCoherencePass": {
@ -1424,8 +1416,8 @@
"compositingStrength": {
"heading": "Forza",
"paragraphs": [
"Quantità di rumore aggiunta per il Passaggio di Coerenza.",
"Simile alla forza di riduzione del rumore."
"Intensità di riduzione del rumore per il passaggio di coerenza.",
"Uguale al parametro intensità di riduzione del rumore da immagine a immagine."
]
},
"paramNegativeConditioning": {
@ -1451,8 +1443,8 @@
"controlNetBeginEnd": {
"heading": "Percentuale passi Inizio / Fine",
"paragraphs": [
"La parte del processo di rimozione del rumore in cui verrà applicato l'adattatore di controllo.",
"In genere, gli adattatori di controllo applicati all'inizio del processo guidano la composizione, mentre quelli applicati alla fine guidano i dettagli."
"A quali passi del processo di rimozione del rumore verrà applicato ControlNet.",
"I ControlNet applicati all'inizio del processo guidano la composizione, mentre i ControlNet applicati alla fine guidano i dettagli."
]
},
"noiseUseCPU": {
@ -1465,7 +1457,7 @@
},
"scaleBeforeProcessing": {
"paragraphs": [
"\"Auto\" ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine."
"Ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine."
],
"heading": "Scala prima dell'elaborazione"
},
@ -1500,21 +1492,20 @@
"paramVAEPrecision": {
"heading": "Precisione VAE",
"paragraphs": [
"La precisione utilizzata durante la codifica e decodifica VAE.",
"Fp16/Mezza precisione è più efficiente, a scapito di minori variazioni dell'immagine."
"La precisione utilizzata durante la codifica e decodifica VAE. FP16/mezza precisione è più efficiente, a scapito di minori variazioni dell'immagine."
]
},
"paramSeed": {
"paragraphs": [
"Controlla il rumore iniziale utilizzato per la generazione.",
"Disabilita l'opzione \"Casuale\" per produrre risultati identici con le stesse impostazioni di generazione."
"Disabilita seme \"Casuale\" per produrre risultati identici con le stesse impostazioni di generazione."
],
"heading": "Seme"
},
"controlNetResizeMode": {
"heading": "Modalità ridimensionamento",
"paragraphs": [
"Metodo per adattare le dimensioni dell'immagine in ingresso dell'adattatore di controllo alle dimensioni della generazione di output."
"Come l'immagine ControlNet verrà adattata alle dimensioni di output dell'immagine."
]
},
"dynamicPromptsSeedBehaviour": {
@ -1529,7 +1520,8 @@
"paramModel": {
"heading": "Modello",
"paragraphs": [
"Modello utilizzato per la generazione. Diversi modelli vengono addestrati per specializzarsi nella produzione di risultati e contenuti estetici diversi."
"Modello utilizzato per i passaggi di riduzione del rumore.",
"Diversi modelli sono generalmente addestrati per specializzarsi nella produzione di particolari risultati e contenuti estetici."
]
},
"paramDenoisingStrength": {
@ -1547,26 +1539,25 @@
},
"infillMethod": {
"paragraphs": [
"Metodo di riempimento durante il processo di Outpainting o Inpainting."
"Metodo per riempire l'area selezionata."
],
"heading": "Metodo di riempimento"
},
"controlNetWeight": {
"heading": "Peso",
"paragraphs": [
"Peso dell'adattatore di controllo. Un peso maggiore portea impatti maggiori sull'immagine finale."
"Quanto forte sal'impatto di ControlNet sull'immagine generata."
]
},
"paramCFGScale": {
"heading": "Scala CFG",
"paragraphs": [
"Controlla quanto il prompt influenza il processo di generazione.",
"Valori elevati della scala CFG possono provocare una saturazione eccessiva e distorsioni nei risultati della generazione. "
"Controlla quanto il tuo prompt influenza il processo di generazione."
]
},
"controlNetControlMode": {
"paragraphs": [
"Attribuisce più peso al prompt oppure a ControlNet."
"Attribuisce più peso al prompt o a ControlNet."
],
"heading": "Modalità di controllo"
},
@ -1578,9 +1569,9 @@
]
},
"lora": {
"heading": "LoRA",
"heading": "Peso LoRA",
"paragraphs": [
"Modelli leggeri utilizzati insieme ai modelli base."
"Un peso LoRA più elevato porterà a impatti maggiori sull'immagine finale."
]
},
"controlNet": {
@ -1592,65 +1583,8 @@
"paramCFGRescaleMultiplier": {
"heading": "Moltiplicatore di riscala CFG",
"paragraphs": [
"Moltiplicatore di riscala per la guida CFG, utilizzato per modelli addestrati utilizzando SNR a terminale zero (ztsnr).",
"Valore suggerito di 0.7 per questi modelli."
"Moltiplicatore di riscala per la guida CFG, utilizzato per modelli addestrati utilizzando SNR a terminale zero (ztsnr). Valore suggerito 0.7."
]
},
"controlNetProcessor": {
"heading": "Processore",
"paragraphs": [
"Metodo di elaborazione dell'immagine di input per guidare il processo di generazione. Processori diversi forniranno effetti o stili diversi nelle immagini generate."
]
},
"imageFit": {
"heading": "Adatta l'immagine iniziale alle dimensioni di output",
"paragraphs": [
"Ridimensiona l'immagine iniziale in base alla larghezza e all'altezza dell'immagine di output. Si consiglia di abilitarlo."
]
},
"loraWeight": {
"heading": "Peso",
"paragraphs": [
"Peso del LoRA. Un peso maggiore comporterà un impatto maggiore sull'immagine finale."
]
},
"paramAspect": {
"heading": "Aspetto",
"paragraphs": [
"Proporzioni dell'immagine generata. La modifica del rapporto aggiornerà di conseguenza la larghezza e l'altezza.",
"\"Ottimizza\" imposterà la larghezza e l'altezza alle dimensioni ottimali per il modello scelto."
]
},
"paramHeight": {
"heading": "Altezza",
"paragraphs": [
"Altezza dell'immagine generata. Deve essere un multiplo di 8."
]
},
"paramHrf": {
"heading": "Abilita correzione alta risoluzione",
"paragraphs": [
"Genera immagini di alta qualità con una risoluzione maggiore di quella ottimale per il modello. Generalmente utilizzato per impedire la duplicazione nell'immagine generata."
]
},
"paramUpscaleMethod": {
"heading": "Metodo di ampliamento",
"paragraphs": [
"Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione."
]
},
"patchmatchDownScaleSize": {
"heading": "Ridimensiona",
"paragraphs": [
"Quanto ridimensionamento avviene prima del riempimento.",
"Un ridimensionamento più elevato migliorerà le prestazioni e ridurrà la qualità."
]
},
"paramWidth": {
"paragraphs": [
"Larghezza dell'immagine generata. Deve essere un multiplo di 8."
],
"heading": "Larghezza"
}
},
"sdxl": {

View File

@ -1217,14 +1217,16 @@
"clipSkip": {
"paragraphs": [
"Kies hoeveel CLIP-modellagen je wilt overslaan.",
"Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen."
"Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen.",
"Een hogere waarde geeft meestal een minder gedetailleerde afbeelding."
],
"heading": "Overslaan CLIP"
},
"paramModel": {
"heading": "Model",
"paragraphs": [
"Model gebruikt voor de ontruisingsstappen."
"Model gebruikt voor de ontruisingsstappen.",
"Verschillende modellen zijn meestal getraind om zich te specialiseren in het maken van bepaalde esthetische resultaten en materiaal."
]
},
"compositingCoherencePass": {

View File

@ -1353,14 +1353,16 @@
"clipSkip": {
"paragraphs": [
"Выберите, сколько слоев модели CLIP нужно пропустить.",
"Некоторые модели работают лучше с определенными настройками пропуска CLIP."
"Некоторые модели работают лучше с определенными настройками пропуска CLIP.",
"Более высокое значение обычно приводит к менее детализированному изображению."
],
"heading": "CLIP пропуск"
},
"paramModel": {
"heading": "Модель",
"paragraphs": [
"Модель, используемая для шагов шумоподавления."
"Модель, используемая для шагов шумоподавления.",
"Различные модели обычно обучаются, чтобы специализироваться на достижении определенных эстетических результатов и содержания."
]
},
"compositingCoherencePass": {

View File

@ -1444,14 +1444,16 @@
"clipSkip": {
"paragraphs": [
"选择要跳过 CLIP 模型多少层。",
"部分模型跳过特定数值的层时效果会更好。"
"部分模型跳过特定数值的层时效果会更好。",
"较高的数值通常会导致图像细节更少。"
],
"heading": "CLIP 跳过层"
},
"paramModel": {
"heading": "模型",
"paragraphs": [
"用于去噪过程的模型。"
"用于去噪过程的模型。",
"不同的模型一般会通过接受训练来专门产生特定的美学内容和结果。"
]
},
"paramIterations": {

View File

@ -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();
});

View File

@ -13,46 +13,28 @@ export type Feature =
| 'compositingCoherenceSteps'
| 'compositingStrength'
| 'compositingMaskAdjustments'
| 'controlNet'
| 'controlNetBeginEnd'
| 'controlNetControlMode'
| 'controlNetProcessor'
| 'controlNetResizeMode'
| 'controlNet'
| 'controlNetWeight'
| 'dynamicPrompts'
| 'dynamicPromptsMaxPrompts'
| 'dynamicPromptsSeedBehaviour'
| 'imageFit'
| 'infillMethod'
| 'lora'
| 'loraWeight'
| 'noiseUseCPU'
| 'paramAspect'
| 'paramCFGScale'
| 'paramCFGRescaleMultiplier'
| 'paramDenoisingStrength'
| 'paramHeight'
| 'paramHrf'
| 'paramIterations'
| 'paramModel'
| 'paramRatio'
| 'paramSeed'
| 'paramSteps'
| 'paramUpscaleMethod'
| 'paramVAE'
| 'paramVAEPrecision'
| 'paramWidth'
| 'patchmatchDownScaleSize'
| 'refinerModel'
| 'refinerNegativeAestheticScore'
| 'refinerPositiveAestheticScore'
| 'refinerScheduler'
| 'refinerStart'
| 'refinerSteps'
| 'refinerCfgScale'
| 'scaleBeforeProcessing'
| 'seamlessTilingXAxis'
| 'seamlessTilingYAxis';
| 'scaleBeforeProcessing';
export type PopoverData = PopoverProps & {
image?: string;
@ -64,57 +46,21 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = {
paramNegativeConditioning: {
placement: 'right',
},
clipSkip: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
controlNet: {
href: 'https://support.invoke.ai/support/solutions/articles/151000105880',
},
controlNetBeginEnd: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178148',
},
controlNetWeight: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178148',
},
lora: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159072',
},
loraWeight: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159072-concepts-low-rank-adaptations-loras-',
},
compositingBlur: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings',
},
compositingBlurMethod: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings',
},
compositingCoherenceMode: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings',
},
compositingCoherenceSteps: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings',
},
compositingStrength: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings',
href: 'https://support.invoke.ai/support/solutions/articles/151000158838',
},
infillMethod: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158841-infill-and-scaling',
href: 'https://support.invoke.ai/support/solutions/articles/151000158841',
},
scaleBeforeProcessing: {
href: 'https://support.invoke.ai/support/solutions/articles/151000158841',
},
paramCFGScale: {
href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI',
},
paramCFGRescaleMultiplier: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
paramDenoisingStrength: {
href: 'https://support.invoke.ai/support/solutions/articles/151000094998-image-to-image',
},
paramHrf: {
href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-',
},
paramIterations: {
href: 'https://support.invoke.ai/support/solutions/articles/151000159073',
},
@ -124,10 +70,7 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = {
},
paramScheduler: {
placement: 'right',
href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI',
},
paramSeed: {
href: 'https://support.invoke.ai/support/solutions/articles/151000096684-what-is-a-seed-how-do-i-use-it-to-recreate-the-same-image-',
href: 'https://support.invoke.ai/support/solutions/articles/151000159073',
},
paramModel: {
placement: 'right',
@ -138,53 +81,15 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = {
},
controlNetControlMode: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000178148',
},
controlNetProcessor: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000105880-using-controlnet',
},
controlNetResizeMode: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000178148',
},
paramVAE: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
paramVAEPrecision: {
placement: 'right',
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
paramUpscaleMethod: {
href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-',
},
refinerModel: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerNegativeAestheticScore: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerPositiveAestheticScore: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerScheduler: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerStart: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerSteps: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
refinerCfgScale: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner',
},
seamlessTilingXAxis: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
seamlessTilingYAxis: {
href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings',
},
} as const;

View File

@ -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 };

View File

@ -1,6 +1,5 @@
import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct';
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
import {
@ -62,10 +61,12 @@ export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => {
}
return (
<FormControl isDisabled={!isEnabled} orientation="vertical">
<InformationalPopover feature="controlNetBeginEnd">
<FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel>
</InformationalPopover>
<FormControl
isDisabled={!isEnabled}
// feature="controlNetBeginEnd"
orientation="vertical"
>
<FormLabel>{t('controlnet.beginEndStepPercent')}</FormLabel>
<CompositeRangeSlider
aria-label={ariaLabel}
value={value}

View File

@ -2,7 +2,6 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
import { useControlAdapterProcessorNode } from 'features/controlAdapters/hooks/useControlAdapterProcessorNode';
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
@ -59,9 +58,7 @@ const ParamControlAdapterProcessorSelect = ({ id }: Props) => {
}
return (
<FormControl isDisabled={!isEnabled}>
<InformationalPopover feature="controlNetProcessor">
<FormLabel>{t('controlnet.processor')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('controlnet.processor')}</FormLabel>
<Combobox value={value} options={options} onChange={onChange} />
</FormControl>
);

View File

@ -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);

View File

@ -1,7 +1,6 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setHrfMethod } from 'features/hrf/store/hrfSlice';
import { isParameterHRFMethod } from 'features/parameters/types/parameterSchemas';
import { memo, useCallback, useMemo } from 'react';
@ -31,9 +30,7 @@ const ParamHrfMethodSelect = () => {
return (
<FormControl>
<InformationalPopover feature="paramUpscaleMethod">
<FormLabel>{t('hrf.upscaleMethod')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('hrf.upscaleMethod')}</FormLabel>
<Combobox value={value} options={options} onChange={onChange} />
</FormControl>
);

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setHrfStrength } from 'features/hrf/store/hrfSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -26,9 +25,7 @@ const ParamHrfStrength = () => {
return (
<FormControl>
<InformationalPopover feature="paramDenoisingStrength">
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.denoisingStrength')}</FormLabel>
<CompositeSlider
min={sliderMin}
max={sliderMax}

View File

@ -1,6 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setHrfEnabled } from 'features/hrf/store/hrfSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@ -19,9 +18,7 @@ const ParamHrfToggle = () => {
return (
<FormControl w="full">
<InformationalPopover feature="paramHrf">
<FormLabel flexGrow={1}>{t('hrf.enableHrf')}</FormLabel>
</InformationalPopover>
<FormLabel flexGrow={1}>{t('hrf.enableHrf')}</FormLabel>
<Switch isChecked={hrfEnabled} onChange={handleHrfEnabled} />
</FormControl>
);

View File

@ -10,7 +10,6 @@ import {
Text,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { LoRA } from 'features/lora/store/loraSlice';
import { loraIsEnabledChanged, loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice';
import { memo, useCallback } from 'react';
@ -58,31 +57,29 @@ export const LoRACard = memo((props: LoRACardProps) => {
</Flex>
</Flex>
</CardHeader>
<InformationalPopover feature="loraWeight">
<CardBody>
<CompositeSlider
value={lora.weight}
onChange={handleChange}
min={-1}
max={2}
step={0.01}
marks={marks}
defaultValue={0.75}
isDisabled={!lora.isEnabled}
/>
<CompositeNumberInput
value={lora.weight}
onChange={handleChange}
min={-5}
max={5}
step={0.01}
w={20}
flexShrink={0}
defaultValue={0.75}
isDisabled={!lora.isEnabled}
/>
</CardBody>
</InformationalPopover>
<CardBody>
<CompositeSlider
value={lora.weight}
onChange={handleChange}
min={-1}
max={2}
step={0.01}
marks={marks}
defaultValue={0.75}
isDisabled={!lora.isEnabled}
/>
<CompositeNumberInput
value={lora.weight}
onChange={handleChange}
min={-5}
max={5}
step={0.01}
w={20}
flexShrink={0}
defaultValue={0.75}
isDisabled={!lora.isEnabled}
/>
</CardBody>
</Card>
);
});

View File

@ -2,7 +2,6 @@ import type { ChakraProps } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice';
import { memo, useCallback, useMemo } from 'react';
@ -58,9 +57,7 @@ const LoRASelect = () => {
return (
<FormControl isDisabled={!options.length}>
<InformationalPopover feature="lora">
<FormLabel>{t('models.lora')} </FormLabel>
</InformationalPopover>
<FormLabel>{t('models.lora')} </FormLabel>
<Combobox
placeholder={placeholder}
value={null}

View File

@ -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 }));

View File

@ -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} />
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
</Flex>
);
};

View File

@ -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 />

View File

@ -25,7 +25,6 @@ const UpdateNodesButton = () => {
icon={<PiWarningBold />}
onClick={handleClickUpdateNodes}
pointerEvents="auto"
colorScheme="warning"
/>
);
};

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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')} &gt;&gt; {t('nodes.updateAllNodes')} &gt;&gt; {t('common.save')}
</Text>
</Flex>
</Flex>
</Flex>
);
};

View File

@ -1,15 +1,11 @@
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';
@ -19,43 +15,21 @@ 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">
{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>
</ScrollableContent>
</Box>
);

View File

@ -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 />

View File

@ -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 };
};

View File

@ -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;
};

View File

@ -18,7 +18,6 @@ import type {
MainModelFieldValue,
SchedulerFieldValue,
SDXLRefinerModelFieldValue,
StatefulFieldValue,
StringFieldValue,
T2IAdapterModelFieldValue,
VAEModelFieldValue,
@ -37,7 +36,6 @@ import {
zMainModelFieldValue,
zSchedulerFieldValue,
zSDXLRefinerModelFieldValue,
zStatefulFieldValue,
zStringFieldValue,
zT2IAdapterModelFieldValue,
zVAEModelFieldValue,
@ -480,9 +478,6 @@ 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);
},
@ -765,7 +760,6 @@ export const {
edgesChanged,
edgesDeleted,
edgeUpdated,
fieldValueReset,
fieldBoardValueChanged,
fieldBooleanValueChanged,
fieldColorValueChanged,
@ -840,6 +834,7 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
nodeIsOpenChanged,
nodeLabelChanged,
nodeNotesChanged,
nodesChanged,
nodesDeleted,
nodeUseCacheChanged,
notesNodeValueChanged,

View File

@ -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,
@ -33,16 +33,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 = {

View File

@ -2,16 +2,11 @@ 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 { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
name: '',
@ -28,9 +23,7 @@ export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
export const initialWorkflowState: WorkflowState = {
_version: 1,
isTouched: false,
mode: 'view',
originalExposedFieldValues: [],
isTouched: true,
...blankWorkflow,
};
@ -38,29 +31,15 @@ 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>) => {
@ -99,43 +78,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,29 +97,6 @@ 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.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
state.isTouched = true;
});
@ -176,10 +104,8 @@ export const workflowSlice = createSlice({
});
export const {
workflowModeChanged,
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
workflowExposedFieldsReordered,
workflowNameChanged,
workflowCategoryChanged,
workflowDescriptionChanged,
@ -189,6 +115,7 @@ export const {
workflowVersionChanged,
workflowContactChanged,
workflowIDChanged,
workflowReset,
workflowSaved,
} = workflowSlice.actions;

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setInfillPatchmatchDownscaleSize } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -28,9 +27,7 @@ const ParamInfillPatchmatchDownscaleSize = () => {
return (
<FormControl isDisabled={infillMethod !== 'patchmatch'}>
<InformationalPopover feature="patchmatchDownScaleSize">
<FormLabel>{t('parameters.patchmatchDownScaleSize')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.patchmatchDownScaleSize')}</FormLabel>
<CompositeSlider
min={sliderMin}
max={sliderMax}

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { memo, useCallback, useMemo } from 'react';
@ -28,9 +27,7 @@ export const ParamHeight = memo(() => {
return (
<FormControl>
<InformationalPopover feature="paramHeight">
<FormLabel>{t('parameters.height')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.height')}</FormLabel>
<CompositeSlider
value={ctx.height}
defaultValue={optimalDimension}

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { memo, useCallback, useMemo } from 'react';
@ -28,9 +27,7 @@ export const ParamWidth = memo(() => {
return (
<FormControl>
<InformationalPopover feature="paramWidth">
<FormLabel>{t('parameters.width')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.width')}</FormLabel>
<CompositeSlider
value={ctx.width}
onChange={onChange}

View File

@ -1,7 +1,6 @@
import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { SingleValue } from 'chakra-react-select';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { isAspectRatioID } from 'features/parameters/components/ImageSize/types';
@ -29,9 +28,7 @@ export const AspectRatioSelect = memo(() => {
return (
<FormControl>
<InformationalPopover feature="paramAspect">
<FormLabel>{t('parameters.aspect')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.aspect')}</FormLabel>
<Combobox value={value} onChange={onChange} options={ASPECT_RATIO_OPTIONS} sx={selectStyles} />
</FormControl>
);

View File

@ -1,7 +1,6 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setShouldFitToWidthHeight } from 'features/parameters/store/generationSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@ -23,9 +22,7 @@ const ImageToImageFit = () => {
return (
<FormControl w="full">
<InformationalPopover feature="imageFit">
<FormLabel flexGrow={1}>{t('parameters.imageFit')}</FormLabel>
</InformationalPopover>
<FormLabel flexGrow={1}>{t('parameters.imageFit')}</FormLabel>
<Switch isChecked={shouldFitToWidthHeight} onChange={handleChangeFit} />
</FormControl>
);

View File

@ -1,7 +1,6 @@
import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { modelSelected } from 'features/parameters/store/actions';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
@ -42,22 +41,18 @@ const ParamMainModelSelect = () => {
});
return (
<FormControl isDisabled={!options.length} isInvalid={!options.length}>
<InformationalPopover feature="paramModel">
<Tooltip label={tooltipLabel}>
<FormControl isDisabled={!options.length} isInvalid={!options.length}>
<FormLabel>{t('modelManager.model')}</FormLabel>
</InformationalPopover>
<Tooltip label={tooltipLabel}>
<Box w="full">
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</Box>
</Tooltip>
</FormControl>
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
);
};

View File

@ -1,6 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setSeamlessXAxis } from 'features/parameters/store/generationSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@ -21,9 +20,7 @@ const ParamSeamlessXAxis = () => {
return (
<FormControl>
<InformationalPopover feature="seamlessTilingXAxis">
<FormLabel>{t('parameters.seamlessXAxis')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.seamlessXAxis')}</FormLabel>
<Switch isChecked={seamlessXAxis} onChange={handleChange} />
</FormControl>
);

View File

@ -1,6 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setSeamlessYAxis } from 'features/parameters/store/generationSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@ -19,9 +18,7 @@ const ParamSeamlessYAxis = () => {
return (
<FormControl>
<InformationalPopover feature="seamlessTilingYAxis">
<FormLabel>{t('parameters.seamlessYAxis')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('parameters.seamlessYAxis')}</FormLabel>
<Switch isChecked={seamlessYAxis} onChange={handleChange} />
</FormControl>
);

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setRefinerCFGScale } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -22,9 +21,7 @@ const ParamSDXLRefinerCFGScale = () => {
return (
<FormControl>
<InformationalPopover feature="refinerCfgScale">
<FormLabel>{t('sdxl.cfgScale')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.cfgScale')}</FormLabel>
<CompositeSlider
value={refinerCFGScale}
defaultValue={initial}

View File

@ -1,7 +1,6 @@
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import { refinerModelChanged, selectSdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback } from 'react';
@ -44,9 +43,7 @@ const ParamSDXLRefinerModelSelect = () => {
});
return (
<FormControl isDisabled={!options.length} isInvalid={!options.length}>
<InformationalPopover feature="refinerModel">
<FormLabel>{t('sdxl.refinermodel')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.refinermodel')}</FormLabel>
<Combobox
value={value}
placeholder={placeholder}

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setRefinerNegativeAestheticScore } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -15,9 +14,7 @@ const ParamSDXLRefinerNegativeAestheticScore = () => {
return (
<FormControl>
<InformationalPopover feature="refinerNegativeAestheticScore">
<FormLabel>{t('sdxl.negAestheticScore')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.negAestheticScore')}</FormLabel>
<CompositeSlider
min={1}
max={10}

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setRefinerPositiveAestheticScore } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -14,9 +13,7 @@ const ParamSDXLRefinerPositiveAestheticScore = () => {
return (
<FormControl>
<InformationalPopover feature="refinerPositiveAestheticScore">
<FormLabel>{t('sdxl.posAestheticScore')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.posAestheticScore')}</FormLabel>
<CompositeSlider
step={0.5}
min={1}

View File

@ -1,7 +1,6 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants';
import { isParameterScheduler } from 'features/parameters/types/parameterSchemas';
import { setRefinerScheduler } from 'features/sdxl/store/sdxlSlice';
@ -27,9 +26,7 @@ const ParamSDXLRefinerScheduler = () => {
return (
<FormControl>
<InformationalPopover feature="refinerScheduler">
<FormLabel>{t('sdxl.scheduler')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.scheduler')}</FormLabel>
<Combobox value={value} options={SCHEDULER_OPTIONS} onChange={onChange} />
</FormControl>
);

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setRefinerStart } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -13,9 +12,7 @@ const ParamSDXLRefinerStart = () => {
return (
<FormControl>
<InformationalPopover feature="refinerStart">
<FormLabel>{t('sdxl.refinerStart')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.refinerStart')}</FormLabel>
<CompositeSlider
step={0.01}
min={0}

View File

@ -1,6 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { setRefinerSteps } from 'features/sdxl/store/sdxlSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -28,9 +27,7 @@ const ParamSDXLRefinerSteps = () => {
return (
<FormControl>
<InformationalPopover feature="refinerSteps">
<FormLabel>{t('sdxl.steps')}</FormLabel>
</InformationalPopover>
<FormLabel>{t('sdxl.steps')}</FormLabel>
<CompositeSlider
value={refinerSteps}
defaultValue={initial}

View File

@ -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();

View File

@ -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);

View File

@ -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';

View File

@ -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';

View File

@ -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"
/>

View File

@ -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);

View File

@ -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';

View File

@ -52,22 +52,10 @@ const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryE
const baseUrl = $baseUrl.get();
const authToken = $authToken.get();
const projectId = $projectId.get();
const isOpenAPIRequest =
(args instanceof Object && args.url.includes('openapi.json')) ||
(typeof args === 'string' && args.includes('openapi.json'));
const fetchBaseQueryArgs: FetchBaseQueryArgs = {
baseUrl: baseUrl ? `${baseUrl}/api/v1` : `${window.location.href.replace(/\/$/, '')}/api/v1`,
};
// When fetching the openapi.json, we need to remove circular references from the JSON.
if (isOpenAPIRequest) {
fetchBaseQueryArgs.jsonReplacer = getCircularReplacer();
}
// openapi.json isn't protected by authorization, but all other requests need to include the auth token and project id.
if (!isOpenAPIRequest) {
fetchBaseQueryArgs.prepareHeaders = (headers) => {
prepareHeaders: (headers) => {
if (authToken) {
headers.set('Authorization', `Bearer ${authToken}`);
}
@ -76,7 +64,15 @@ 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);

View File

@ -1 +1 @@
__version__ = "3.7.0"
__version__ = "3.6.4"

View File

@ -33,7 +33,7 @@ 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",