mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-24 04:58:11 -05:00
Compare commits
96 Commits
v5.10.0dev
...
v5.10.0a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e25a0c37 | ||
|
|
589b849e64 | ||
|
|
aedbc9f778 | ||
|
|
a0cf9e2e80 | ||
|
|
5c8f1c5666 | ||
|
|
fd37117221 | ||
|
|
5956f96e57 | ||
|
|
49622c37ed | ||
|
|
50387c8f64 | ||
|
|
e1538af219 | ||
|
|
e5a0010a72 | ||
|
|
b75d1b2473 | ||
|
|
b91bb9ba9f | ||
|
|
a7c818bcae | ||
|
|
a54b255718 | ||
|
|
3e04baa684 | ||
|
|
d23db705dd | ||
|
|
96a481530d | ||
|
|
a0b515979a | ||
|
|
2da8ac216b | ||
|
|
1558fe9a37 | ||
|
|
ded080ae04 | ||
|
|
982603e051 | ||
|
|
a23b5c3408 | ||
|
|
c9f93b3746 | ||
|
|
e381024cc0 | ||
|
|
bb65884040 | ||
|
|
920339dbeb | ||
|
|
0f618bdbcb | ||
|
|
8294e2cdea | ||
|
|
7da43be4b7 | ||
|
|
8561e9e540 | ||
|
|
b0d5e7e3d8 | ||
|
|
ab2d203d5e | ||
|
|
eae5c54091 | ||
|
|
ee2b486e8b | ||
|
|
a2c7050832 | ||
|
|
cd090eb76f | ||
|
|
3348755e6e | ||
|
|
d6dbdaacd1 | ||
|
|
1c6fa1ad18 | ||
|
|
39bed90eda | ||
|
|
c0e48193a7 | ||
|
|
41677394c0 | ||
|
|
405cfd46e7 | ||
|
|
9cc9a5c8b0 | ||
|
|
ddc0461882 | ||
|
|
0f09091a26 | ||
|
|
dedb77b6f2 | ||
|
|
89f8dbee6c | ||
|
|
8b0dc8ce84 | ||
|
|
018121e407 | ||
|
|
095025b637 | ||
|
|
ed8487659e | ||
|
|
3745d2be0c | ||
|
|
b5206e204f | ||
|
|
b237ccbdd8 | ||
|
|
224ebc72ae | ||
|
|
05c3d47be9 | ||
|
|
a4d709c169 | ||
|
|
5a8e95c700 | ||
|
|
e630f364df | ||
|
|
9c287038e4 | ||
|
|
8d32ede082 | ||
|
|
bab0b6d069 | ||
|
|
8e013ef3be | ||
|
|
8188484a40 | ||
|
|
5d8fe9fb56 | ||
|
|
8d3743c6f2 | ||
|
|
986b7426d2 | ||
|
|
8d8150b47e | ||
|
|
ae3944b4e0 | ||
|
|
6f0c5c9c05 | ||
|
|
89c999ca58 | ||
|
|
89cefc6a88 | ||
|
|
79e384e71c | ||
|
|
3ebe96765a | ||
|
|
97e158f13a | ||
|
|
2b1a36ef4a | ||
|
|
6824b4b036 | ||
|
|
e8a09a5ed8 | ||
|
|
c4df7d3cb9 | ||
|
|
b9e76afbf5 | ||
|
|
dfd8b8f220 | ||
|
|
a089e1bf5c | ||
|
|
875f3fe779 | ||
|
|
5fa2cf59e2 | ||
|
|
4d58c222f3 | ||
|
|
c27142bb02 | ||
|
|
e3c441fda4 | ||
|
|
6bb102f860 | ||
|
|
5c45ef1a8c | ||
|
|
7a218a8040 | ||
|
|
929d86768f | ||
|
|
3676160496 | ||
|
|
8e6ebb537b |
@@ -1,6 +1,6 @@
|
||||
# Builds and uploads the installer and python build artifacts.
|
||||
# Builds and uploads python build artifacts.
|
||||
|
||||
name: build installer
|
||||
name: build wheel
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -27,19 +27,12 @@ jobs:
|
||||
- name: setup frontend
|
||||
uses: ./.github/actions/install-frontend-deps
|
||||
|
||||
- name: create installer
|
||||
id: create_installer
|
||||
run: ./create_installer.sh
|
||||
working-directory: installer
|
||||
- name: build wheel
|
||||
id: build_wheel
|
||||
run: ./scripts/build_wheel.sh
|
||||
|
||||
- name: upload python distribution artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ steps.create_installer.outputs.DIST_PATH }}
|
||||
|
||||
- name: upload installer artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: installer
|
||||
path: ${{ steps.create_installer.outputs.INSTALLER_PATH }}
|
||||
path: ${{ steps.build_wheel.outputs.DIST_PATH }}
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
always_run: true
|
||||
|
||||
build:
|
||||
uses: ./.github/workflows/build-installer.yml
|
||||
uses: ./.github/workflows/build-wheel.yml
|
||||
|
||||
publish-testpypi:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
Makefile
10
Makefile
@@ -16,7 +16,7 @@ help:
|
||||
@echo "frontend-build Build the frontend in order to run on localhost:9090"
|
||||
@echo "frontend-dev Run the frontend in developer mode on localhost:5173"
|
||||
@echo "frontend-typegen Generate types for the frontend from the OpenAPI schema"
|
||||
@echo "installer-zip Build the installer .zip file for the current version"
|
||||
@echo "wheel Build the wheel for the current version"
|
||||
@echo "tag-release Tag the GitHub repository with the current version (use at release time only!)"
|
||||
@echo "openapi Generate the OpenAPI schema for the app, outputting to stdout"
|
||||
@echo "docs Serve the mkdocs site with live reload"
|
||||
@@ -64,13 +64,13 @@ frontend-dev:
|
||||
frontend-typegen:
|
||||
cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
|
||||
# Installer zip file
|
||||
installer-zip:
|
||||
cd installer && ./create_installer.sh
|
||||
# Tag the release
|
||||
wheel:
|
||||
cd scripts && ./build_wheel.sh
|
||||
|
||||
# Tag the release
|
||||
tag-release:
|
||||
cd installer && ./tag_release.sh
|
||||
cd scripts && ./tag_release.sh
|
||||
|
||||
# Generate the OpenAPI Schema for the app
|
||||
openapi:
|
||||
|
||||
@@ -99,4 +99,15 @@ CMD ["invokeai-web"]
|
||||
COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
|
||||
|
||||
# add sources last to minimize image changes on code changes
|
||||
COPY invokeai ${INVOKEAI_SRC}/invokeai
|
||||
COPY invokeai ${INVOKEAI_SRC}/invokeai
|
||||
|
||||
# this should not increase image size because we've already installed dependencies
|
||||
# in a previous layer
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then UV_INDEX="https://download.pytorch.org/whl/cpu"; \
|
||||
elif [ "$GPU_DRIVER" = "rocm" ]; then UV_INDEX="https://download.pytorch.org/whl/rocm6.2"; \
|
||||
fi && \
|
||||
uv pip install -e .
|
||||
|
||||
|
||||
@@ -60,16 +60,11 @@ Next, these jobs run and must pass. They are the same jobs that are run for ever
|
||||
- **`frontend-checks`**: runs `prettier` (format), `eslint` (lint), `dpdm` (circular refs), `tsc` (static type check) and `knip` (unused imports)
|
||||
- **`typegen-checks`**: ensures the frontend and backend types are synced
|
||||
|
||||
#### `build-installer` Job
|
||||
#### `build-wheel` Job
|
||||
|
||||
This sets up both python and frontend dependencies and builds the python package. Internally, this runs `installer/create_installer.sh` and uploads two artifacts:
|
||||
This sets up both python and frontend dependencies and builds the python package. Internally, this runs `./scripts/build_wheel.sh` and uploads `dist.zip`, which contains the wheel and unarchived build.
|
||||
|
||||
- **`dist`**: the python distribution, to be published on PyPI
|
||||
- **`InvokeAI-installer-${VERSION}.zip`**: the legacy install scripts
|
||||
|
||||
You don't need to download either of these files.
|
||||
|
||||
> The legacy install scripts are no longer used, but we haven't updated the workflow to skip building them.
|
||||
You don't need to download or test these artifacts.
|
||||
|
||||
#### Sanity Check & Smoke Test
|
||||
|
||||
@@ -79,7 +74,7 @@ It's possible to test the python package before it gets published to PyPI. We've
|
||||
|
||||
But, if you want to be extra-super careful, here's how to test it:
|
||||
|
||||
- Download the `dist.zip` build artifact from the `build-installer` job
|
||||
- Download the `dist.zip` build artifact from the `build-wheel` job
|
||||
- Unzip it and find the wheel file
|
||||
- Create a fresh Invoke install by following the [manual install guide](https://invoke-ai.github.io/InvokeAI/installation/manual/) - but instead of installing from PyPI, install from the wheel
|
||||
- Test the app
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Legacy Scripts
|
||||
|
||||
!!! warning "Legacy Scripts"
|
||||
|
||||
We recommend using the Invoke Launcher to install and update Invoke. It's a desktop application for Windows, macOS and Linux. It takes care of a lot of nitty gritty details for you.
|
||||
|
||||
Follow the [quick start guide](./quick_start.md) to get started.
|
||||
|
||||
!!! tip "Use the installer to update"
|
||||
|
||||
Using the installer for updates will not erase any of your data (images, models, boards, etc). It only updates the core libraries used to run Invoke.
|
||||
|
||||
Simply use the same path you installed to originally to update your existing installation.
|
||||
|
||||
Both release and pre-release versions can be installed using the installer. It also supports install through a wheel if needed.
|
||||
|
||||
Be sure to review the [installation requirements] and ensure your system has everything it needs to install Invoke.
|
||||
|
||||
## Getting the Latest Installer
|
||||
|
||||
Download the `InvokeAI-installer-vX.Y.Z.zip` file from the [latest release] page. It is at the bottom of the page, under **Assets**.
|
||||
|
||||
After unzipping the installer, you should have a `InvokeAI-Installer` folder with some files inside, including `install.bat` and `install.sh`.
|
||||
|
||||
## Running the Installer
|
||||
|
||||
!!! tip
|
||||
|
||||
Windows users should first double-click the `WinLongPathsEnabled.reg` file to prevent a failed installation due to long file paths.
|
||||
|
||||
Double-click the install script:
|
||||
|
||||
=== "Windows"
|
||||
|
||||
```sh
|
||||
install.bat
|
||||
```
|
||||
|
||||
=== "Linux/macOS"
|
||||
|
||||
```sh
|
||||
install.sh
|
||||
```
|
||||
|
||||
!!! info "Running the Installer from the commandline"
|
||||
|
||||
You can also run the install script from cmd/powershell (Windows) or terminal (Linux/macOS).
|
||||
|
||||
!!! warning "Untrusted Publisher (Windows)"
|
||||
|
||||
You may get a popup saying the file comes from an `Untrusted Publisher`. Click `More Info` and `Run Anyway` to get past this.
|
||||
|
||||
The installation process is simple, with a few prompts:
|
||||
|
||||
- Select the version to install. Unless you have a specific reason to install a specific version, select the default (the latest version).
|
||||
- Select location for the install. Be sure you have enough space in this folder for the base application, as described in the [installation requirements].
|
||||
- Select a GPU device.
|
||||
|
||||
!!! info "Slow Installation"
|
||||
|
||||
The installer needs to download several GB of data and install it all. It may appear to get stuck at 99.9% when installing `pytorch` or during a step labeled "Installing collected packages".
|
||||
|
||||
If it is stuck for over 10 minutes, something has probably gone wrong and you should close the window and restart.
|
||||
|
||||
## Running the Application
|
||||
|
||||
Find the install location you selected earlier. Double-click the launcher script to run the app:
|
||||
|
||||
=== "Windows"
|
||||
|
||||
```sh
|
||||
invoke.bat
|
||||
```
|
||||
|
||||
=== "Linux/macOS"
|
||||
|
||||
```sh
|
||||
invoke.sh
|
||||
```
|
||||
|
||||
Choose the first option to run the UI. After a series of startup messages, you'll see something like this:
|
||||
|
||||
```sh
|
||||
Uvicorn running on http://127.0.0.1:9090 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
Copy the URL into your browser and you should see the UI.
|
||||
|
||||
## Improved Outpainting with PatchMatch
|
||||
|
||||
PatchMatch is an extra add-on that can improve outpainting. Windows users are in luck - it works out of the box.
|
||||
|
||||
On macOS and Linux, a few extra steps are needed to set it up. See the [PatchMatch installation guide](./patchmatch.md).
|
||||
|
||||
## First-time Setup
|
||||
|
||||
You will need to [install some models] before you can generate.
|
||||
|
||||
Check the [configuration docs] for details on configuring the application.
|
||||
|
||||
## Updating
|
||||
|
||||
Updating is exactly the same as installing - download the latest installer, choose the latest version, enter your existing installation path, and the app will update. None of your data (images, models, boards, etc) will be erased.
|
||||
|
||||
!!! info "Dependency Resolution Issues"
|
||||
|
||||
We've found that pip's dependency resolution can cause issues when upgrading packages. One very common problem was pip "downgrading" torch from CUDA to CPU, but things broke in other novel ways.
|
||||
|
||||
The installer doesn't have this kind of problem, so we use it for updating as well.
|
||||
|
||||
## Installation Issues
|
||||
|
||||
If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord].
|
||||
|
||||
[installation requirements]: ./requirements.md
|
||||
[FAQ]: ../faq.md
|
||||
[install some models]: ./models.md
|
||||
[configuration docs]: ../configuration.md
|
||||
[latest release]: https://github.com/invoke-ai/InvokeAI/releases/latest
|
||||
[create an issue]: https://github.com/invoke-ai/InvokeAI/issues
|
||||
[discord]: https://discord.gg/ZmtBAhwWhy
|
||||
@@ -49,9 +49,9 @@ If you have an existing Invoke installation, you can select it and let the launc
|
||||
|
||||
!!! warning "Problem running the launcher on macOS"
|
||||
|
||||
macOS may not allow you to run the launcher. We are working to resolve this by signing the launcher executable. Until that is done, you can either use the [legacy scripts](./legacy_scripts.md) to install, or manually flag the launcher as safe:
|
||||
macOS may not allow you to run the launcher. We are working to resolve this by signing the launcher executable. Until that is done, you can manually flag the launcher as safe:
|
||||
|
||||
- Open the **Invoke-Installer-mac-arm64.dmg** file.
|
||||
- Open the **Invoke Community Edition.dmg** file.
|
||||
- Drag the launcher to **Applications**.
|
||||
- Open a terminal.
|
||||
- Run `xattr -d 'com.apple.quarantine' /Applications/Invoke\ Community\ Edition.app`.
|
||||
@@ -117,7 +117,6 @@ If you still have problems, ask for help on the Invoke [discord](https://discord
|
||||
|
||||
- You can install the Invoke application as a python package. See our [manual install](./manual.md) docs.
|
||||
- You can run Invoke with docker. See our [docker install](./docker.md) docs.
|
||||
- You can still use our legacy scripts to install and run Invoke. See the [legacy scripts](./legacy_scripts.md) docs.
|
||||
|
||||
## Need Help?
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,128 +0,0 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
@rem This script requires the user to install Python 3.10 or higher. All other
|
||||
@rem requirements are downloaded as needed.
|
||||
|
||||
@rem change to the script's directory
|
||||
PUSHD "%~dp0"
|
||||
|
||||
set "no_cache_dir=--no-cache-dir"
|
||||
if "%1" == "use-cache" (
|
||||
set "no_cache_dir="
|
||||
)
|
||||
|
||||
@rem Config
|
||||
@rem The version in the next line is replaced by an up to date release number
|
||||
@rem when create_installer.sh is run. Change the release number there.
|
||||
set INSTRUCTIONS=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/
|
||||
set TROUBLESHOOTING=https://invoke-ai.github.io/InvokeAI/help/FAQ/
|
||||
set PYTHON_URL=https://www.python.org/downloads/windows/
|
||||
set MINIMUM_PYTHON_VERSION=3.10.0
|
||||
set PYTHON_URL=https://www.python.org/downloads/release/python-3109/
|
||||
|
||||
set err_msg=An error has occurred and the script could not continue.
|
||||
|
||||
@rem --------------------------- Intro -------------------------------
|
||||
echo This script will install InvokeAI and its dependencies.
|
||||
echo.
|
||||
echo BEFORE YOU START PLEASE MAKE SURE TO DO THE FOLLOWING
|
||||
echo 1. Install python 3.10 or 3.11. Python version 3.9 is no longer supported.
|
||||
echo 2. Double-click on the file WinLongPathsEnabled.reg in order to
|
||||
echo enable long path support on your system.
|
||||
echo 3. Install the Visual C++ core libraries.
|
||||
echo Please download and install the libraries from:
|
||||
echo https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170
|
||||
echo.
|
||||
echo See %INSTRUCTIONS% for more details.
|
||||
echo.
|
||||
echo FOR THE BEST USER EXPERIENCE WE SUGGEST MAXIMIZING THIS WINDOW NOW.
|
||||
pause
|
||||
|
||||
@rem ---------------------------- check Python version ---------------
|
||||
echo ***** Checking and Updating Python *****
|
||||
|
||||
call python --version >.tmp1 2>.tmp2
|
||||
if %errorlevel% == 1 (
|
||||
set err_msg=Please install Python 3.10-11. See %INSTRUCTIONS% for details.
|
||||
goto err_exit
|
||||
)
|
||||
|
||||
for /f "tokens=2" %%i in (.tmp1) do set python_version=%%i
|
||||
if "%python_version%" == "" (
|
||||
set err_msg=No python was detected on your system. Please install Python version %MINIMUM_PYTHON_VERSION% or higher. We recommend Python 3.10.12 from %PYTHON_URL%
|
||||
goto err_exit
|
||||
)
|
||||
|
||||
call :compareVersions %MINIMUM_PYTHON_VERSION% %python_version%
|
||||
if %errorlevel% == 1 (
|
||||
set err_msg=Your version of Python is too low. You need at least %MINIMUM_PYTHON_VERSION% but you have %python_version%. We recommend Python 3.10.12 from %PYTHON_URL%
|
||||
goto err_exit
|
||||
)
|
||||
|
||||
@rem Cleanup
|
||||
del /q .tmp1 .tmp2
|
||||
|
||||
@rem -------------- Install and Configure ---------------
|
||||
|
||||
call python .\lib\main.py
|
||||
pause
|
||||
exit /b
|
||||
|
||||
@rem ------------------------ Subroutines ---------------
|
||||
@rem routine to do comparison of semantic version numbers
|
||||
@rem found at https://stackoverflow.com/questions/15807762/compare-version-numbers-in-batch-file
|
||||
:compareVersions
|
||||
::
|
||||
:: Compares two version numbers and returns the result in the ERRORLEVEL
|
||||
::
|
||||
:: Returns 1 if version1 > version2
|
||||
:: 0 if version1 = version2
|
||||
:: -1 if version1 < version2
|
||||
::
|
||||
:: The nodes must be delimited by . or , or -
|
||||
::
|
||||
:: Nodes are normally strictly numeric, without a 0 prefix. A letter suffix
|
||||
:: is treated as a separate node
|
||||
::
|
||||
setlocal enableDelayedExpansion
|
||||
set "v1=%~1"
|
||||
set "v2=%~2"
|
||||
call :divideLetters v1
|
||||
call :divideLetters v2
|
||||
:loop
|
||||
call :parseNode "%v1%" n1 v1
|
||||
call :parseNode "%v2%" n2 v2
|
||||
if %n1% gtr %n2% exit /b 1
|
||||
if %n1% lss %n2% exit /b -1
|
||||
if not defined v1 if not defined v2 exit /b 0
|
||||
if not defined v1 exit /b -1
|
||||
if not defined v2 exit /b 1
|
||||
goto :loop
|
||||
|
||||
|
||||
:parseNode version nodeVar remainderVar
|
||||
for /f "tokens=1* delims=.,-" %%A in ("%~1") do (
|
||||
set "%~2=%%A"
|
||||
set "%~3=%%B"
|
||||
)
|
||||
exit /b
|
||||
|
||||
|
||||
:divideLetters versionVar
|
||||
for %%C in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do set "%~1=!%~1:%%C=.%%C!"
|
||||
exit /b
|
||||
|
||||
:err_exit
|
||||
echo %err_msg%
|
||||
echo The installer will exit now.
|
||||
pause
|
||||
exit /b
|
||||
|
||||
pause
|
||||
|
||||
:Trim
|
||||
SetLocal EnableDelayedExpansion
|
||||
set Params=%*
|
||||
for /f "tokens=1*" %%a in ("!Params!") do EndLocal & set %1=%%b
|
||||
exit /b
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# make sure we are not already in a venv
|
||||
# (don't need to check status)
|
||||
deactivate >/dev/null 2>&1
|
||||
scriptdir=$(dirname "$0")
|
||||
cd $scriptdir
|
||||
|
||||
function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
|
||||
|
||||
MINIMUM_PYTHON_VERSION=3.10.0
|
||||
MAXIMUM_PYTHON_VERSION=3.11.100
|
||||
PYTHON=""
|
||||
for candidate in python3.11 python3.10 python3 python ; do
|
||||
if ppath=`which $candidate 2>/dev/null`; then
|
||||
# when using `pyenv`, the executable for an inactive Python version will exist but will not be operational
|
||||
# we check that this found executable can actually run
|
||||
if [ $($candidate --version &>/dev/null; echo ${PIPESTATUS}) -gt 0 ]; then continue; fi
|
||||
|
||||
python_version=$($ppath -V | awk '{ print $2 }')
|
||||
if [ $(version $python_version) -ge $(version "$MINIMUM_PYTHON_VERSION") ]; then
|
||||
if [ $(version $python_version) -le $(version "$MAXIMUM_PYTHON_VERSION") ]; then
|
||||
PYTHON=$ppath
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
echo "A suitable Python interpreter could not be found"
|
||||
echo "Please install Python $MINIMUM_PYTHON_VERSION or higher (maximum $MAXIMUM_PYTHON_VERSION) before running this script. See instructions at $INSTRUCTIONS for help."
|
||||
read -p "Press any key to exit"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
echo "For the best user experience we suggest enlarging or maximizing this window now."
|
||||
|
||||
exec $PYTHON ./lib/main.py ${@}
|
||||
read -p "Press any key to exit"
|
||||
@@ -1,438 +0,0 @@
|
||||
# Copyright (c) 2023 Eugene Brodsky (https://github.com/ebr)
|
||||
"""
|
||||
InvokeAI installer script
|
||||
"""
|
||||
|
||||
import locale
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import venv
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Optional, Tuple
|
||||
|
||||
SUPPORTED_PYTHON = ">=3.10.0,<=3.11.100"
|
||||
INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"]
|
||||
BOOTSTRAP_VENV_PREFIX = "invokeai-installer-tmp"
|
||||
DOCS_URL = "https://invoke-ai.github.io/InvokeAI/"
|
||||
DISCORD_URL = "https://discord.gg/ZmtBAhwWhy"
|
||||
|
||||
OS = platform.uname().system
|
||||
ARCH = platform.uname().machine
|
||||
VERSION = "latest"
|
||||
|
||||
|
||||
def get_version_from_wheel_filename(wheel_filename: str) -> str:
|
||||
match = re.search(r"-(\d+\.\d+\.\d+)", wheel_filename)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return version
|
||||
else:
|
||||
raise ValueError(f"Could not extract version from wheel filename: {wheel_filename}")
|
||||
|
||||
|
||||
class Installer:
|
||||
"""
|
||||
Deploys an InvokeAI installation into a given path
|
||||
"""
|
||||
|
||||
reqs: list[str] = INSTALLER_REQS
|
||||
|
||||
def __init__(self) -> None:
|
||||
if os.getenv("VIRTUAL_ENV") is not None:
|
||||
print("A virtual environment is already activated. Please 'deactivate' before installation.")
|
||||
sys.exit(-1)
|
||||
self.bootstrap()
|
||||
self.available_releases = get_github_releases()
|
||||
|
||||
def mktemp_venv(self) -> TemporaryDirectory[str]:
|
||||
"""
|
||||
Creates a temporary virtual environment for the installer itself
|
||||
|
||||
:return: path to the created virtual environment directory
|
||||
:rtype: TemporaryDirectory
|
||||
"""
|
||||
|
||||
# Cleaning up temporary directories on Windows results in a race condition
|
||||
# and a stack trace.
|
||||
# `ignore_cleanup_errors` was only added in Python 3.10
|
||||
if OS == "Windows" and int(platform.python_version_tuple()[1]) >= 10:
|
||||
venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX, ignore_cleanup_errors=True)
|
||||
else:
|
||||
venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX)
|
||||
|
||||
venv.create(venv_dir.name, with_pip=True)
|
||||
self.venv_dir = venv_dir
|
||||
set_sys_path(Path(venv_dir.name))
|
||||
|
||||
return venv_dir
|
||||
|
||||
def bootstrap(self, verbose: bool = False) -> TemporaryDirectory[str] | None:
|
||||
"""
|
||||
Bootstrap the installer venv with packages required at install time
|
||||
"""
|
||||
|
||||
print("Initializing the installer. This may take a minute - please wait...")
|
||||
|
||||
venv_dir = self.mktemp_venv()
|
||||
pip = get_pip_from_venv(Path(venv_dir.name))
|
||||
|
||||
cmd = [pip, "install", "--require-virtualenv", "--use-pep517"]
|
||||
cmd.extend(self.reqs)
|
||||
|
||||
try:
|
||||
# upgrade pip to the latest version to avoid a confusing message
|
||||
res = upgrade_pip(Path(venv_dir.name))
|
||||
if verbose:
|
||||
print(res)
|
||||
|
||||
# run the install prerequisites installation
|
||||
res = subprocess.check_output(cmd).decode()
|
||||
|
||||
if verbose:
|
||||
print(res)
|
||||
|
||||
return venv_dir
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
|
||||
def app_venv(self, venv_parent: Path) -> Path:
|
||||
"""
|
||||
Create a virtualenv for the InvokeAI installation
|
||||
"""
|
||||
|
||||
venv_dir = venv_parent / ".venv"
|
||||
|
||||
# Prefer to copy python executables
|
||||
# so that updates to system python don't break InvokeAI
|
||||
try:
|
||||
venv.create(venv_dir, with_pip=True)
|
||||
# If installing over an existing environment previously created with symlinks,
|
||||
# the executables will fail to copy. Keep symlinks in that case
|
||||
except shutil.SameFileError:
|
||||
venv.create(venv_dir, with_pip=True, symlinks=True)
|
||||
|
||||
return venv_dir
|
||||
|
||||
def install(
|
||||
self,
|
||||
root: str = "~/invokeai",
|
||||
yes_to_all: bool = False,
|
||||
find_links: Optional[str] = None,
|
||||
wheel: Optional[Path] = None,
|
||||
) -> None:
|
||||
"""Install the InvokeAI application into the given runtime path
|
||||
|
||||
Args:
|
||||
root: Destination path for the installation
|
||||
yes_to_all: Accept defaults to all questions
|
||||
find_links: A local directory to search for requirement wheels before going to remote indexes
|
||||
wheel: A wheel file to install
|
||||
"""
|
||||
|
||||
import messages
|
||||
|
||||
if wheel:
|
||||
messages.installing_from_wheel(wheel.name)
|
||||
version = get_version_from_wheel_filename(wheel.name)
|
||||
else:
|
||||
messages.welcome(self.available_releases)
|
||||
version = messages.choose_version(self.available_releases)
|
||||
|
||||
auto_dest = Path(os.environ.get("INVOKEAI_ROOT", root)).expanduser().resolve()
|
||||
destination = auto_dest if yes_to_all else messages.dest_path(root)
|
||||
if destination is None:
|
||||
print("Could not find or create the destination directory. Installation cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# create the venv for the app
|
||||
self.venv = self.app_venv(venv_parent=destination)
|
||||
|
||||
self.instance = InvokeAiInstance(runtime=destination, venv=self.venv, version=version)
|
||||
|
||||
# install dependencies and the InvokeAI application
|
||||
(extra_index_url, optional_modules) = get_torch_source() if not yes_to_all else (None, None)
|
||||
self.instance.install(extra_index_url, optional_modules, find_links, wheel)
|
||||
|
||||
# install the launch/update scripts into the runtime directory
|
||||
self.instance.install_user_scripts()
|
||||
|
||||
message = f"""
|
||||
*** Installation Successful ***
|
||||
|
||||
To start the application, run:
|
||||
{destination}/invoke.{"bat" if sys.platform == "win32" else "sh"}
|
||||
|
||||
For more information, troubleshooting and support, visit our docs at:
|
||||
{DOCS_URL}
|
||||
|
||||
Join the community on Discord:
|
||||
{DISCORD_URL}
|
||||
"""
|
||||
print(message)
|
||||
|
||||
|
||||
class InvokeAiInstance:
|
||||
"""
|
||||
Manages an installed instance of InvokeAI, comprising a virtual environment and a runtime directory.
|
||||
The virtual environment *may* reside within the runtime directory.
|
||||
A single runtime directory *may* be shared by multiple virtual environments, though this isn't currently tested or supported.
|
||||
"""
|
||||
|
||||
def __init__(self, runtime: Path, venv: Path, version: str = "stable") -> None:
|
||||
self.runtime = runtime
|
||||
self.venv = venv
|
||||
self.pip = get_pip_from_venv(venv)
|
||||
self.version = version
|
||||
|
||||
set_sys_path(venv)
|
||||
os.environ["INVOKEAI_ROOT"] = str(self.runtime.expanduser().resolve())
|
||||
os.environ["VIRTUAL_ENV"] = str(self.venv.expanduser().resolve())
|
||||
upgrade_pip(venv)
|
||||
|
||||
def get(self) -> tuple[Path, Path]:
|
||||
"""
|
||||
Get the location of the virtualenv directory for this installation
|
||||
|
||||
:return: Paths of the runtime and the venv directory
|
||||
:rtype: tuple[Path, Path]
|
||||
"""
|
||||
|
||||
return (self.runtime, self.venv)
|
||||
|
||||
def install(
|
||||
self,
|
||||
extra_index_url: Optional[str] = None,
|
||||
optional_modules: Optional[str] = None,
|
||||
find_links: Optional[str] = None,
|
||||
wheel: Optional[Path] = None,
|
||||
):
|
||||
"""Install the package from PyPi or a wheel, if provided.
|
||||
|
||||
Args:
|
||||
extra_index_url: the "--extra-index-url ..." line for pip to look in extra indexes.
|
||||
optional_modules: optional modules to install using "[module1,module2]" format.
|
||||
find_links: path to a directory containing wheels to be searched prior to going to the internet
|
||||
wheel: a wheel file to install
|
||||
"""
|
||||
|
||||
import messages
|
||||
|
||||
# not currently used, but may be useful for "install most recent version" option
|
||||
if self.version == "prerelease":
|
||||
version = None
|
||||
pre_flag = "--pre"
|
||||
elif self.version == "stable":
|
||||
version = None
|
||||
pre_flag = None
|
||||
else:
|
||||
version = self.version
|
||||
pre_flag = None
|
||||
|
||||
src = "invokeai"
|
||||
if optional_modules:
|
||||
src += optional_modules
|
||||
if version:
|
||||
src += f"=={version}"
|
||||
|
||||
messages.simple_banner("Installing the InvokeAI Application :art:")
|
||||
|
||||
from plumbum import FG, ProcessExecutionError, local
|
||||
|
||||
pip = local[self.pip]
|
||||
|
||||
# Uninstall xformers if it is present; the correct version of it will be reinstalled if needed
|
||||
_ = pip["uninstall", "-yqq", "xformers"] & FG
|
||||
|
||||
pipeline = pip[
|
||||
"install",
|
||||
"--require-virtualenv",
|
||||
"--force-reinstall",
|
||||
"--use-pep517",
|
||||
str(src) if not wheel else str(wheel),
|
||||
"--find-links" if find_links is not None else None,
|
||||
find_links,
|
||||
"--extra-index-url" if extra_index_url is not None else None,
|
||||
extra_index_url,
|
||||
pre_flag if not wheel else None, # Ignore the flag if we are installing a wheel
|
||||
]
|
||||
|
||||
try:
|
||||
_ = pipeline & FG
|
||||
except ProcessExecutionError as e:
|
||||
print(f"Error: {e}")
|
||||
print(
|
||||
"Could not install InvokeAI. Please try downloading the latest version of the installer and install again."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def install_user_scripts(self):
|
||||
"""
|
||||
Copy the launch and update scripts to the runtime dir
|
||||
"""
|
||||
|
||||
ext = "bat" if OS == "Windows" else "sh"
|
||||
|
||||
scripts = ["invoke"]
|
||||
|
||||
for script in scripts:
|
||||
src = Path(__file__).parent / ".." / "templates" / f"{script}.{ext}.in"
|
||||
dest = self.runtime / f"{script}.{ext}"
|
||||
shutil.copy(src, dest)
|
||||
os.chmod(dest, 0o0755)
|
||||
|
||||
|
||||
### Utility functions ###
|
||||
|
||||
|
||||
def get_pip_from_venv(venv_path: Path) -> str:
|
||||
"""
|
||||
Given a path to a virtual environment, get the absolute path to the `pip` executable
|
||||
in a cross-platform fashion. Does not validate that the pip executable
|
||||
actually exists in the virtualenv.
|
||||
|
||||
:param venv_path: Path to the virtual environment
|
||||
:type venv_path: Path
|
||||
:return: Absolute path to the pip executable
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
pip = "Scripts\\pip.exe" if OS == "Windows" else "bin/pip"
|
||||
return str(venv_path.expanduser().resolve() / pip)
|
||||
|
||||
|
||||
def upgrade_pip(venv_path: Path) -> str | None:
|
||||
"""
|
||||
Upgrade the pip executable in the given virtual environment
|
||||
"""
|
||||
|
||||
python = "Scripts\\python.exe" if OS == "Windows" else "bin/python"
|
||||
python = str(venv_path.expanduser().resolve() / python)
|
||||
|
||||
try:
|
||||
result = subprocess.check_output([python, "-m", "pip", "install", "--upgrade", "pip"]).decode(
|
||||
encoding=locale.getpreferredencoding()
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
result = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def set_sys_path(venv_path: Path) -> None:
|
||||
"""
|
||||
Given a path to a virtual environment, set the sys.path, in a cross-platform fashion,
|
||||
such that packages from the given venv may be imported in the current process.
|
||||
Ensure that the packages from system environment are not visible (emulate
|
||||
the virtual env 'activate' script) - this doesn't work on Windows yet.
|
||||
|
||||
:param venv_path: Path to the virtual environment
|
||||
:type venv_path: Path
|
||||
"""
|
||||
|
||||
# filter out any paths in sys.path that may be system- or user-wide
|
||||
# but leave the temporary bootstrap virtualenv as it contains packages we
|
||||
# temporarily need at install time
|
||||
sys.path = list(filter(lambda p: not p.endswith("-packages") or p.find(BOOTSTRAP_VENV_PREFIX) != -1, sys.path))
|
||||
|
||||
# determine site-packages/lib directory location for the venv
|
||||
lib = "Lib" if OS == "Windows" else f"lib/python{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
# add the site-packages location to the venv
|
||||
sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve()))
|
||||
|
||||
|
||||
def get_github_releases() -> tuple[list[str], list[str]] | None:
|
||||
"""
|
||||
Query Github for published (pre-)release versions.
|
||||
Return a tuple where the first element is a list of stable releases and the second element is a list of pre-releases.
|
||||
Return None if the query fails for any reason.
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
## get latest releases using github api
|
||||
url = "https://api.github.com/repos/invoke-ai/InvokeAI/releases"
|
||||
releases: list[str] = []
|
||||
pre_releases: list[str] = []
|
||||
try:
|
||||
res = requests.get(url)
|
||||
res.raise_for_status()
|
||||
tag_info = res.json()
|
||||
for tag in tag_info:
|
||||
if not tag["prerelease"]:
|
||||
releases.append(tag["tag_name"].lstrip("v"))
|
||||
else:
|
||||
pre_releases.append(tag["tag_name"].lstrip("v"))
|
||||
except requests.HTTPError as e:
|
||||
print(f"Error: {e}")
|
||||
print("Could not fetch version information from GitHub. Please check your network connection and try again.")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("An unexpected error occurred while trying to fetch version information from GitHub. Please try again.")
|
||||
return
|
||||
|
||||
releases.sort(reverse=True)
|
||||
pre_releases.sort(reverse=True)
|
||||
|
||||
return releases, pre_releases
|
||||
|
||||
|
||||
def get_torch_source() -> Tuple[str | None, str | None]:
|
||||
"""
|
||||
Determine the extra index URL for pip to use for torch installation.
|
||||
This depends on the OS and the graphics accelerator in use.
|
||||
This is only applicable to Windows and Linux, since PyTorch does not
|
||||
offer accelerated builds for macOS.
|
||||
|
||||
Prefer CUDA-enabled wheels if the user wasn't sure of their GPU, as it will fallback to CPU if possible.
|
||||
|
||||
A NoneType return means just go to PyPi.
|
||||
|
||||
:return: tuple consisting of (extra index url or None, optional modules to load or None)
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
from messages import GpuType, select_gpu
|
||||
|
||||
# device can be one of: "cuda", "rocm", "cpu", "cuda_and_dml, autodetect"
|
||||
device = select_gpu()
|
||||
|
||||
# The correct extra index URLs for torch are inconsistent, see https://pytorch.org/get-started/locally/#start-locally
|
||||
|
||||
url = None
|
||||
optional_modules: str | None = None
|
||||
if OS == "Linux":
|
||||
if device == GpuType.ROCM:
|
||||
url = "https://download.pytorch.org/whl/rocm6.1"
|
||||
elif device == GpuType.CPU:
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
elif device == GpuType.CUDA:
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
optional_modules = "[onnx-cuda]"
|
||||
elif device == GpuType.CUDA_WITH_XFORMERS:
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
optional_modules = "[xformers,onnx-cuda]"
|
||||
elif OS == "Windows":
|
||||
if device == GpuType.CUDA:
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
optional_modules = "[onnx-cuda]"
|
||||
elif device == GpuType.CUDA_WITH_XFORMERS:
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
optional_modules = "[xformers,onnx-cuda]"
|
||||
elif device.value == "cpu":
|
||||
# CPU uses the default PyPi index, no optional modules
|
||||
pass
|
||||
elif OS == "Darwin":
|
||||
# macOS uses the default PyPi index, no optional modules
|
||||
pass
|
||||
|
||||
# Fall back to defaults
|
||||
|
||||
return (url, optional_modules)
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
InvokeAI Installer
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from installer import Installer
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--root",
|
||||
dest="root",
|
||||
type=str,
|
||||
help="Destination path for installation",
|
||||
default=os.environ.get("INVOKEAI_ROOT") or "~/invokeai",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
"--yes-to-all",
|
||||
dest="yes_to_all",
|
||||
action="store_true",
|
||||
help="Assume default answers to all questions",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--find-links",
|
||||
dest="find_links",
|
||||
help="Specifies a directory of local wheel files to be searched prior to searching the online repositories.",
|
||||
type=Path,
|
||||
default=None,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--wheel",
|
||||
dest="wheel",
|
||||
help="Specifies a wheel for the InvokeAI package. Used for troubleshooting or testing prereleases.",
|
||||
type=Path,
|
||||
default=None,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
inst = Installer()
|
||||
|
||||
try:
|
||||
inst.install(**args.__dict__)
|
||||
except KeyboardInterrupt:
|
||||
print("\n")
|
||||
print("Ctrl-C pressed. Aborting.")
|
||||
print("Come back soon!")
|
||||
@@ -1,342 +0,0 @@
|
||||
# Copyright (c) 2023 Eugene Brodsky (https://github.com/ebr)
|
||||
"""
|
||||
Installer user interaction
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit.completion import FuzzyWordCompleter, PathCompleter
|
||||
from prompt_toolkit.validation import Validator
|
||||
from rich import box, print
|
||||
from rich.console import Console, Group, group
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
from rich.style import Style
|
||||
from rich.syntax import Syntax
|
||||
from rich.text import Text
|
||||
|
||||
OS = platform.uname().system
|
||||
ARCH = platform.uname().machine
|
||||
|
||||
if OS == "Windows":
|
||||
# Windows terminals look better without a background colour
|
||||
console = Console(style=Style(color="grey74"))
|
||||
else:
|
||||
console = Console(style=Style(color="grey74", bgcolor="grey19"))
|
||||
|
||||
|
||||
def welcome(available_releases: tuple[list[str], list[str]] | None = None) -> None:
|
||||
@group()
|
||||
def text():
|
||||
if (platform_specific := _platform_specific_help()) is not None:
|
||||
yield platform_specific
|
||||
yield ""
|
||||
yield Text.from_markup(
|
||||
"Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with [i]Control-C[/] and retry.",
|
||||
justify="center",
|
||||
)
|
||||
if available_releases is not None:
|
||||
latest_stable = available_releases[0][0]
|
||||
last_pre = available_releases[1][0]
|
||||
yield ""
|
||||
yield Text.from_markup(
|
||||
f"[red3]🠶[/] Latest stable release (recommended): [b bright_white]{latest_stable}", justify="center"
|
||||
)
|
||||
yield Text.from_markup(
|
||||
f"[red3]🠶[/] Last published pre-release version: [b bright_white]{last_pre}", justify="center"
|
||||
)
|
||||
|
||||
console.rule()
|
||||
print(
|
||||
Panel(
|
||||
title="[bold wheat1]Welcome to the InvokeAI Installer",
|
||||
renderable=text(),
|
||||
box=box.DOUBLE,
|
||||
expand=True,
|
||||
padding=(1, 2),
|
||||
style=Style(bgcolor="grey23", color="orange1"),
|
||||
subtitle=f"[bold grey39]{OS}-{ARCH}",
|
||||
)
|
||||
)
|
||||
console.line()
|
||||
|
||||
|
||||
def installing_from_wheel(wheel_filename: str) -> None:
|
||||
"""Display a message about installing from a wheel"""
|
||||
|
||||
@group()
|
||||
def text():
|
||||
yield Text.from_markup(f"You are installing from a wheel file: [bold]{wheel_filename}\n")
|
||||
yield Text.from_markup(
|
||||
"[bold orange3]If you are not sure why you are doing this, you should cancel and install InvokeAI normally."
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
title="Installing from Wheel",
|
||||
renderable=text(),
|
||||
box=box.DOUBLE,
|
||||
expand=True,
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
|
||||
should_proceed = Confirm.ask("Do you want to proceed?")
|
||||
|
||||
if not should_proceed:
|
||||
console.print("Installation cancelled.")
|
||||
exit()
|
||||
|
||||
|
||||
def choose_version(available_releases: tuple[list[str], list[str]] | None = None) -> str:
|
||||
"""
|
||||
Prompt the user to choose an Invoke version to install
|
||||
"""
|
||||
|
||||
# short circuit if we couldn't get a version list
|
||||
# still try to install the latest stable version
|
||||
if available_releases is None:
|
||||
return "stable"
|
||||
|
||||
console.print(":grey_question: [orange3]Please choose an Invoke version to install.")
|
||||
|
||||
choices = available_releases[0] + available_releases[1]
|
||||
|
||||
response = prompt(
|
||||
message=f" <Enter> to install the recommended release ({choices[0]}). <Tab> or type to pick a version: ",
|
||||
complete_while_typing=True,
|
||||
completer=FuzzyWordCompleter(choices),
|
||||
)
|
||||
console.print(f" Version {choices[0] if response == '' else response} will be installed.")
|
||||
|
||||
console.line()
|
||||
|
||||
return "stable" if response == "" else response
|
||||
|
||||
|
||||
def confirm_install(dest: Path) -> bool:
|
||||
if dest.exists():
|
||||
print(f":stop_sign: Directory {dest} already exists!")
|
||||
print(" Is this location correct?")
|
||||
default = False
|
||||
else:
|
||||
print(f":file_folder: InvokeAI will be installed in {dest}")
|
||||
default = True
|
||||
|
||||
dest_confirmed = Confirm.ask(" Please confirm:", default=default)
|
||||
|
||||
console.line()
|
||||
|
||||
return dest_confirmed
|
||||
|
||||
|
||||
def dest_path(dest: Optional[str | Path] = None) -> Path | None:
|
||||
"""
|
||||
Prompt the user for the destination path and create the path
|
||||
|
||||
:param dest: a filesystem path, defaults to None
|
||||
:type dest: str, optional
|
||||
:return: absolute path to the created installation directory
|
||||
:rtype: Path
|
||||
"""
|
||||
|
||||
if dest is not None:
|
||||
dest = Path(dest).expanduser().resolve()
|
||||
else:
|
||||
dest = Path.cwd().expanduser().resolve()
|
||||
prev_dest = init_path = dest
|
||||
dest_confirmed = False
|
||||
|
||||
while not dest_confirmed:
|
||||
browse_start = (dest or Path.cwd()).expanduser().resolve()
|
||||
|
||||
path_completer = PathCompleter(
|
||||
only_directories=True,
|
||||
expanduser=True,
|
||||
get_paths=lambda: [str(browse_start)], # noqa: B023
|
||||
# get_paths=lambda: [".."].extend(list(browse_start.iterdir()))
|
||||
)
|
||||
|
||||
console.line()
|
||||
|
||||
console.print(f":grey_question: [orange3]Please select the install destination:[/] \\[{browse_start}]: ")
|
||||
selected = prompt(
|
||||
">>> ",
|
||||
complete_in_thread=True,
|
||||
completer=path_completer,
|
||||
default=str(browse_start) + os.sep,
|
||||
vi_mode=True,
|
||||
complete_while_typing=True,
|
||||
# Test that this is not needed on Windows
|
||||
# complete_style=CompleteStyle.READLINE_LIKE,
|
||||
)
|
||||
prev_dest = dest
|
||||
dest = Path(selected)
|
||||
|
||||
console.line()
|
||||
|
||||
dest_confirmed = confirm_install(dest.expanduser().resolve())
|
||||
|
||||
if not dest_confirmed:
|
||||
dest = prev_dest
|
||||
|
||||
dest = dest.expanduser().resolve()
|
||||
|
||||
try:
|
||||
dest.mkdir(exist_ok=True, parents=True)
|
||||
return dest
|
||||
except PermissionError:
|
||||
console.print(
|
||||
f"Failed to create directory {dest} due to insufficient permissions",
|
||||
style=Style(color="red"),
|
||||
highlight=True,
|
||||
)
|
||||
except OSError:
|
||||
console.print_exception()
|
||||
|
||||
if Confirm.ask("Would you like to try again?"):
|
||||
dest_path(init_path)
|
||||
else:
|
||||
console.rule("Goodbye!")
|
||||
|
||||
|
||||
class GpuType(Enum):
|
||||
CUDA_WITH_XFORMERS = "xformers"
|
||||
CUDA = "cuda"
|
||||
ROCM = "rocm"
|
||||
CPU = "cpu"
|
||||
|
||||
|
||||
def select_gpu() -> GpuType:
|
||||
"""
|
||||
Prompt the user to select the GPU driver
|
||||
"""
|
||||
|
||||
if ARCH == "arm64" and OS != "Darwin":
|
||||
print(f"Only CPU acceleration is available on {ARCH} architecture. Proceeding with that.")
|
||||
return GpuType.CPU
|
||||
|
||||
nvidia = (
|
||||
"an [gold1 b]NVIDIA[/] RTX 3060 or newer GPU using CUDA",
|
||||
GpuType.CUDA,
|
||||
)
|
||||
vintage_nvidia = (
|
||||
"an [gold1 b]NVIDIA[/] RTX 20xx or older GPU using CUDA+xFormers",
|
||||
GpuType.CUDA_WITH_XFORMERS,
|
||||
)
|
||||
amd = (
|
||||
"an [gold1 b]AMD[/] GPU using ROCm",
|
||||
GpuType.ROCM,
|
||||
)
|
||||
cpu = (
|
||||
"Do not install any GPU support, use CPU for generation (slow)",
|
||||
GpuType.CPU,
|
||||
)
|
||||
|
||||
options = []
|
||||
if OS == "Windows":
|
||||
options = [nvidia, vintage_nvidia, cpu]
|
||||
if OS == "Linux":
|
||||
options = [nvidia, vintage_nvidia, amd, cpu]
|
||||
elif OS == "Darwin":
|
||||
options = [cpu]
|
||||
|
||||
if len(options) == 1:
|
||||
return options[0][1]
|
||||
|
||||
options = {str(i): opt for i, opt in enumerate(options, 1)}
|
||||
|
||||
console.rule(":space_invader: GPU (Graphics Card) selection :space_invader:")
|
||||
console.print(
|
||||
Panel(
|
||||
Group(
|
||||
"\n".join(
|
||||
[
|
||||
f"Detected the [gold1]{OS}-{ARCH}[/] platform",
|
||||
"",
|
||||
"See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/installation/requirements/[/] to ensure your system meets the minimum requirements.",
|
||||
"",
|
||||
"[red3]🠶[/] [b]Your GPU drivers must be correctly installed before using InvokeAI![/] [red3]🠴[/]",
|
||||
]
|
||||
),
|
||||
"",
|
||||
"Please select the type of GPU installed in your computer.",
|
||||
Panel(
|
||||
"\n".join([f"[dark_goldenrod b i]{i}[/] [dark_red]🢒[/]{opt[0]}" for (i, opt) in options.items()]),
|
||||
box=box.MINIMAL,
|
||||
),
|
||||
),
|
||||
box=box.MINIMAL,
|
||||
padding=(1, 1),
|
||||
)
|
||||
)
|
||||
choice = prompt(
|
||||
"Please make your selection: ",
|
||||
validator=Validator.from_callable(
|
||||
lambda n: n in options.keys(), error_message="Please select one the above options"
|
||||
),
|
||||
)
|
||||
|
||||
return options[choice][1]
|
||||
|
||||
|
||||
def simple_banner(message: str) -> None:
|
||||
"""
|
||||
A simple banner with a message, defined here for styling consistency
|
||||
|
||||
:param message: The message to display
|
||||
:type message: str
|
||||
"""
|
||||
|
||||
console.rule(message)
|
||||
|
||||
|
||||
# TODO this does not yet work correctly
|
||||
def windows_long_paths_registry() -> None:
|
||||
"""
|
||||
Display a message about applying the Windows long paths registry fix
|
||||
"""
|
||||
|
||||
with open(str(Path(__file__).parent / "WinLongPathsEnabled.reg"), "r", encoding="utf-16le") as code:
|
||||
syntax = Syntax(code.read(), line_numbers=True, lexer="regedit")
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
Group(
|
||||
"\n".join(
|
||||
[
|
||||
"We will now apply a registry fix to enable long paths on Windows. InvokeAI needs this to function correctly. We are asking your permission to modify the Windows Registry on your behalf.",
|
||||
"",
|
||||
"This is the change that will be applied:",
|
||||
str(syntax),
|
||||
]
|
||||
)
|
||||
),
|
||||
title="Windows Long Paths registry fix",
|
||||
box=box.HORIZONTALS,
|
||||
padding=(1, 1),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _platform_specific_help() -> Text | None:
|
||||
if OS == "Darwin":
|
||||
text = Text.from_markup(
|
||||
"""[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/]."""
|
||||
)
|
||||
elif OS == "Windows":
|
||||
text = Text.from_markup(
|
||||
"""[b wheat1]Windows Users![/]\n\nBefore you start, please do the following:
|
||||
1. Double-click on the file [b wheat1]WinLongPathsEnabled.reg[/] in order to
|
||||
enable long path support on your system.
|
||||
2. Make sure you have the [b wheat1]Visual C++ core libraries[/] installed. If not, install from
|
||||
[deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]"""
|
||||
)
|
||||
else:
|
||||
return
|
||||
return text
|
||||
@@ -1,52 +0,0 @@
|
||||
InvokeAI
|
||||
|
||||
Project homepage: https://github.com/invoke-ai/InvokeAI
|
||||
|
||||
Preparations:
|
||||
|
||||
You will need to install Python 3.10 or higher for this installer
|
||||
to work. Instructions are given here:
|
||||
https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/
|
||||
|
||||
Before you start the installer, please open up your system's command
|
||||
line window (Terminal or Command) and type the commands:
|
||||
|
||||
python --version
|
||||
|
||||
If all is well, it will print "Python 3.X.X", where the version number
|
||||
is at least 3.10.*, and not higher than 3.11.*.
|
||||
|
||||
If this works, check the version of the Python package manager, pip:
|
||||
|
||||
pip --version
|
||||
|
||||
You should get a message that indicates that the pip package
|
||||
installer was derived from Python 3.10 or 3.11. For example:
|
||||
"pip 22.0.1 from /usr/bin/pip (python 3.10)"
|
||||
|
||||
Long Paths on Windows:
|
||||
|
||||
If you are on Windows, you will need to enable Windows Long Paths to
|
||||
run InvokeAI successfully. If you're not sure what this is, you
|
||||
almost certainly need to do this.
|
||||
|
||||
Simply double-click the "WinLongPathsEnabled.reg" file located in
|
||||
this directory, and approve the Windows warnings. Note that you will
|
||||
need to have admin privileges in order to do this.
|
||||
|
||||
Launching the installer:
|
||||
|
||||
Windows: double-click the 'install.bat' file (while keeping it inside
|
||||
the InvokeAI-Installer folder).
|
||||
|
||||
Linux and Mac: Please open the terminal application and run
|
||||
'./install.sh' (while keeping it inside the InvokeAI-Installer
|
||||
folder).
|
||||
|
||||
The installer will create a directory of your choice and install the
|
||||
InvokeAI application within it. This directory contains everything you need to run
|
||||
invokeai. Once InvokeAI is up and running, you may delete the
|
||||
InvokeAI-Installer folder at your convenience.
|
||||
|
||||
For more information, please see
|
||||
https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/
|
||||
@@ -1,54 +0,0 @@
|
||||
@echo off
|
||||
|
||||
PUSHD "%~dp0"
|
||||
setlocal
|
||||
|
||||
call .venv\Scripts\activate.bat
|
||||
set INVOKEAI_ROOT=.
|
||||
|
||||
:start
|
||||
echo Desired action:
|
||||
echo 1. Generate images with the browser-based interface
|
||||
echo 2. Open the developer console
|
||||
echo 3. Command-line help
|
||||
echo Q - Quit
|
||||
echo.
|
||||
echo To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest
|
||||
echo.
|
||||
set /P choice="Please enter 1-4, Q: [1] "
|
||||
if not defined choice set choice=1
|
||||
IF /I "%choice%" == "1" (
|
||||
echo Starting the InvokeAI browser-based UI..
|
||||
python .venv\Scripts\invokeai-web.exe %*
|
||||
) ELSE IF /I "%choice%" == "2" (
|
||||
echo Developer Console
|
||||
echo Python command is:
|
||||
where python
|
||||
echo Python version is:
|
||||
python --version
|
||||
echo *************************
|
||||
echo You are now in the system shell, with the local InvokeAI Python virtual environment activated,
|
||||
echo so that you can troubleshoot this InvokeAI installation as necessary.
|
||||
echo *************************
|
||||
echo *** Type `exit` to quit this shell and deactivate the Python virtual environment ***
|
||||
call cmd /k
|
||||
) ELSE IF /I "%choice%" == "3" (
|
||||
echo Displaying command line help...
|
||||
python .venv\Scripts\invokeai-web.exe --help %*
|
||||
pause
|
||||
exit /b
|
||||
) ELSE IF /I "%choice%" == "q" (
|
||||
echo Goodbye!
|
||||
goto ending
|
||||
) ELSE (
|
||||
echo Invalid selection
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
goto start
|
||||
|
||||
endlocal
|
||||
pause
|
||||
|
||||
:ending
|
||||
exit /b
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MIT License
|
||||
|
||||
# Coauthored by Lincoln Stein, Eugene Brodsky and Joshua Kimsey
|
||||
# Copyright 2023, The InvokeAI Development Team
|
||||
|
||||
####
|
||||
# This launch script assumes that:
|
||||
# 1. it is located in the runtime directory,
|
||||
# 2. the .venv is also located in the runtime directory and is named exactly that
|
||||
#
|
||||
# If both of the above are not true, this script will likely not work as intended.
|
||||
# Activate the virtual environment and run `invoke.py` directly.
|
||||
####
|
||||
|
||||
set -eu
|
||||
|
||||
# Ensure we're in the correct folder in case user's CWD is somewhere else
|
||||
scriptdir=$(dirname $(readlink -f "$0"))
|
||||
cd "$scriptdir"
|
||||
|
||||
. .venv/bin/activate
|
||||
|
||||
export INVOKEAI_ROOT="$scriptdir"
|
||||
|
||||
# Stash the CLI args - when we prompt for user input, `$@` is overwritten
|
||||
PARAMS=$@
|
||||
|
||||
# This setting allows torch to fall back to CPU for operations that are not supported by MPS on macOS.
|
||||
if [ "$(uname -s)" == "Darwin" ]; then
|
||||
export PYTORCH_ENABLE_MPS_FALLBACK=1
|
||||
fi
|
||||
|
||||
# Primary function for the case statement to determine user input
|
||||
do_choice() {
|
||||
case $1 in
|
||||
1)
|
||||
clear
|
||||
printf "Generate images with a browser-based interface\n"
|
||||
invokeai-web $PARAMS
|
||||
;;
|
||||
2)
|
||||
clear
|
||||
printf "Open the developer console\n"
|
||||
file_name=$(basename "${BASH_SOURCE[0]}")
|
||||
bash --init-file "$file_name"
|
||||
;;
|
||||
3)
|
||||
clear
|
||||
printf "Command-line help\n"
|
||||
invokeai-web --help
|
||||
;;
|
||||
*)
|
||||
clear
|
||||
printf "Exiting...\n"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
clear
|
||||
}
|
||||
|
||||
# Command-line interface for launching Invoke functions
|
||||
do_line_input() {
|
||||
clear
|
||||
printf "What would you like to do?\n"
|
||||
printf "1: Generate images using the browser-based interface\n"
|
||||
printf "2: Open the developer console\n"
|
||||
printf "3: Command-line help\n"
|
||||
printf "Q: Quit\n\n"
|
||||
printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest\n\n"
|
||||
read -p "Please enter 1-4, Q: [1] " yn
|
||||
choice=${yn:='1'}
|
||||
do_choice $choice
|
||||
clear
|
||||
}
|
||||
|
||||
# Main IF statement for launching Invoke, and for checking if the user is in the developer console
|
||||
if [ "$0" != "bash" ]; then
|
||||
while true; do
|
||||
do_line_input
|
||||
done
|
||||
else # in developer console
|
||||
python --version
|
||||
printf "Press ^D to exit\n"
|
||||
export PS1="(InvokeAI) \u@\h \w> "
|
||||
fi
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
import math
|
||||
from typing import Literal, Optional
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
@@ -39,12 +40,15 @@ class FluxReduxOutput(BaseInvocationOutput):
|
||||
)
|
||||
|
||||
|
||||
DOWNSAMPLING_FUNCTIONS = Literal["nearest", "bilinear", "bicubic", "area", "nearest-exact"]
|
||||
|
||||
|
||||
@invocation(
|
||||
"flux_redux",
|
||||
title="FLUX Redux",
|
||||
tags=["ip_adapter", "control"],
|
||||
category="ip_adapter",
|
||||
version="2.0.0",
|
||||
version="2.1.0",
|
||||
classification=Classification.Beta,
|
||||
)
|
||||
class FluxReduxInvocation(BaseInvocation):
|
||||
@@ -61,18 +65,53 @@ class FluxReduxInvocation(BaseInvocation):
|
||||
title="FLUX Redux Model",
|
||||
ui_type=UIType.FluxReduxModel,
|
||||
)
|
||||
downsampling_factor: int = InputField(
|
||||
ge=1,
|
||||
le=9,
|
||||
default=1,
|
||||
description="Redux Downsampling Factor (1-9)",
|
||||
)
|
||||
downsampling_function: DOWNSAMPLING_FUNCTIONS = InputField(
|
||||
default="area",
|
||||
description="Redux Downsampling Function",
|
||||
)
|
||||
weight: float = InputField(
|
||||
ge=0,
|
||||
le=1,
|
||||
default=1.0,
|
||||
description="Redux weight (0.0-1.0)",
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxReduxOutput:
|
||||
image = context.images.get_pil(self.image.image_name, "RGB")
|
||||
|
||||
encoded_x = self._siglip_encode(context, image)
|
||||
redux_conditioning = self._flux_redux_encode(context, encoded_x)
|
||||
if self.downsampling_factor > 1 or self.weight != 1.0:
|
||||
redux_conditioning = self._downsample_weight(context, redux_conditioning)
|
||||
|
||||
tensor_name = context.tensors.save(redux_conditioning)
|
||||
return FluxReduxOutput(
|
||||
redux_cond=FluxReduxConditioningField(conditioning=TensorField(tensor_name=tensor_name), mask=self.mask)
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def _downsample_weight(self, context: InvocationContext, redux_conditioning: torch.Tensor) -> torch.Tensor:
|
||||
# Downsampling derived from https://github.com/kaibioinfo/ComfyUI_AdvancedRefluxControl
|
||||
(b, t, h) = redux_conditioning.shape
|
||||
m = int(math.sqrt(t))
|
||||
if self.downsampling_factor > 1:
|
||||
redux_conditioning = redux_conditioning.view(b, m, m, h)
|
||||
redux_conditioning = torch.nn.functional.interpolate(
|
||||
redux_conditioning.transpose(1, -1),
|
||||
size=(m // self.downsampling_factor, m // self.downsampling_factor),
|
||||
mode=self.downsampling_function,
|
||||
)
|
||||
redux_conditioning = redux_conditioning.transpose(1, -1).reshape(b, -1, h)
|
||||
if self.weight != 1.0:
|
||||
redux_conditioning = redux_conditioning * self.weight * self.weight
|
||||
return redux_conditioning
|
||||
|
||||
@torch.no_grad()
|
||||
def _siglip_encode(self, context: InvocationContext, image: Image.Image) -> torch.Tensor:
|
||||
siglip_model_config = self._get_siglip_model(context)
|
||||
|
||||
@@ -31,6 +31,12 @@ def run_app() -> None:
|
||||
if app_config.pytorch_cuda_alloc_conf:
|
||||
configure_torch_cuda_allocator(app_config.pytorch_cuda_alloc_conf, logger)
|
||||
|
||||
# This import must happen after configure_torch_cuda_allocator() is called, because the module imports torch.
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
|
||||
torch_device_name = TorchDevice.get_torch_device_name()
|
||||
logger.info(f"Using torch device: {torch_device_name}")
|
||||
|
||||
# Import from startup_utils here to avoid importing torch before configure_torch_cuda_allocator() is called.
|
||||
from invokeai.app.util.startup_utils import (
|
||||
apply_monkeypatches,
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import onnxruntime as ort
|
||||
import torch
|
||||
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
||||
from diffusers.schedulers.scheduling_utils import SchedulerMixin
|
||||
@@ -55,6 +56,16 @@ def calc_model_size_by_data(logger: logging.Logger, model: AnyModel) -> int:
|
||||
),
|
||||
):
|
||||
return model.calc_size()
|
||||
elif isinstance(model, ort.InferenceSession):
|
||||
if model._model_bytes is not None:
|
||||
# If the model is already loaded, return the size of the model bytes
|
||||
return len(model._model_bytes)
|
||||
elif model._model_path is not None:
|
||||
# If the model is not loaded, return the size of the model path
|
||||
return calc_model_size_by_fs(Path(model._model_path))
|
||||
else:
|
||||
# If neither is available, return 0
|
||||
return 0
|
||||
elif isinstance(
|
||||
model,
|
||||
(
|
||||
|
||||
@@ -1306,7 +1306,10 @@
|
||||
"unableToCopy": "Unable to Copy",
|
||||
"unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ",
|
||||
"unableToCopyDesc_theseSteps": "these steps",
|
||||
"fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks."
|
||||
"fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks.",
|
||||
"problemUnpublishingWorkflow": "Problem Unpublishing Workflow",
|
||||
"problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.",
|
||||
"workflowUnpublished": "Workflow Unpublished"
|
||||
},
|
||||
"popovers": {
|
||||
"clipSkip": {
|
||||
@@ -1786,8 +1789,8 @@
|
||||
"minimum": "Minimum",
|
||||
"maximum": "Maximum",
|
||||
"publish": "Publish",
|
||||
"published": "Published",
|
||||
"unpublish": "Unpublish",
|
||||
"published": "Published",
|
||||
"workflowLocked": "Workflow Locked",
|
||||
"workflowLockedPublished": "Published workflows are locked for editing.\nYou can unpublish the workflow to edit it, or make a copy of it.",
|
||||
"workflowLockedDuringPublishing": "Workflow is locked while configuring for publishing.",
|
||||
@@ -2017,6 +2020,14 @@
|
||||
"composition": "Composition Only",
|
||||
"compositionDesc": "Replicates layout & structure while ignoring the reference's style."
|
||||
},
|
||||
"fluxReduxImageInfluence": {
|
||||
"imageInfluence": "Image Influence",
|
||||
"lowest": "Lowest",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High",
|
||||
"highest": "Highest"
|
||||
},
|
||||
"fill": {
|
||||
"fillColor": "Fill Color",
|
||||
"fillStyle": "Fill Style",
|
||||
|
||||
@@ -1790,7 +1790,37 @@
|
||||
"maximum": "Massimo",
|
||||
"dropdown": "Elenco a discesa",
|
||||
"addOption": "Aggiungi opzione",
|
||||
"resetOptions": "Reimposta opzioni"
|
||||
"resetOptions": "Reimposta opzioni",
|
||||
"publish": "Pubblica",
|
||||
"workflowLocked": "Flusso di lavoro bloccato",
|
||||
"workflowLockedDuringPublishing": "Il flusso di lavoro è bloccato durante la configurazione per la pubblicazione.",
|
||||
"selectOutputNode": "Seleziona nodo di uscita",
|
||||
"changeOutputNode": "Cambia nodo di uscita",
|
||||
"publishedWorkflowOutputs": "Uscite",
|
||||
"noPublishableInputs": "Nessun ingresso pubblicabile",
|
||||
"published": "Pubblicato",
|
||||
"cannotPublish": "Impossibile pubblicare il flusso di lavoro",
|
||||
"noOutputNodeSelected": "Nessun nodo di uscita selezionato",
|
||||
"unpublish": "Annulla pubblicazione",
|
||||
"workflowLockedPublished": "I flussi di lavoro pubblicati sono bloccati per la modifica.\nPuoi annullare la pubblicazione del flusso di lavoro per modificarlo o crearne una copia.",
|
||||
"publishedWorkflowInputs": "Ingressi",
|
||||
"unpublishableInputs": "Questi input non pubblicabili verranno omessi",
|
||||
"publishWarnings": "Avvertenze",
|
||||
"errorWorkflowHasUnsavedChanges": "Il flusso di lavoro presenta modifiche non salvate",
|
||||
"errorWorkflowHasBatchOrGeneratorNodes": "Il flusso di lavoro ha nodi lotto e/o generatori",
|
||||
"errorWorkflowHasInvalidGraph": "Grafico del flusso di lavoro non valido (passare il mouse sul pulsante Invoke per i dettagli)",
|
||||
"errorWorkflowHasNoOutputNode": "Nessun nodo di uscita selezionato",
|
||||
"warningWorkflowHasUnpublishableInputFields": "Il flusso di lavoro presenta alcuni ingressi non pubblicabili: questi verranno omessi dal flusso di lavoro pubblicato",
|
||||
"publishFailed": "Pubblicazione non riuscita",
|
||||
"publishFailedDesc": "Si è verificato un problema durante la pubblicazione del flusso di lavoro. Riprova.",
|
||||
"publishSuccess": "Il tuo flusso di lavoro è in fase di pubblicazione",
|
||||
"publishSuccessDesc": "Controlla il <LinkComponent>pannello di controllo del progetto</LinkComponent> per verificarne l'avanzamento.",
|
||||
"publishedWorkflowIsLocked": "Il flusso di lavoro pubblicato è bloccato",
|
||||
"publishingValidationRun": "Esecuzione della convalida della pubblicazione",
|
||||
"publishingValidationRunInProgress": "È in corso la convalida della pubblicazione.",
|
||||
"publishedWorkflowsLocked": "I flussi di lavoro pubblicati sono bloccati e non possono essere modificati o eseguiti. Annulla la pubblicazione del flusso di lavoro o salva una copia per modificare o eseguire questo flusso di lavoro.",
|
||||
"warningWorkflowHasNoPublishableInputFields": "Nessun campo di ingresso pubblicabile selezionato: il flusso di lavoro pubblicato verrà eseguito solo con i valori predefiniti",
|
||||
"publishInProgress": "Pubblicazione in corso"
|
||||
},
|
||||
"loadMore": "Carica altro",
|
||||
"searchPlaceholder": "Cerca per nome, descrizione o etichetta",
|
||||
@@ -1807,7 +1837,8 @@
|
||||
"noRecentWorkflows": "Nessun flusso di lavoro recente",
|
||||
"view": "Visualizza",
|
||||
"recommended": "Consigliato per te",
|
||||
"emptyStringPlaceholder": "<stringa vuota>"
|
||||
"emptyStringPlaceholder": "<stringa vuota>",
|
||||
"published": "Pubblicato"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
|
||||
@@ -2229,7 +2229,7 @@
|
||||
"workflows": {
|
||||
"delete": "Xoá",
|
||||
"descending": "Giảm Dần",
|
||||
"created": "Ngày Tạo",
|
||||
"created": "Đã Tạo",
|
||||
"edit": "Chỉnh Sửa",
|
||||
"download": "Tải Xuống",
|
||||
"copyShareLink": "Sao Chép Liên Kết Chia Sẻ",
|
||||
@@ -2255,7 +2255,7 @@
|
||||
"saveWorkflow": "Lưu Workflow",
|
||||
"problemSavingWorkflow": "Có Vấn Đề Khi Lưu Workflow",
|
||||
"noDescription": "Không có mô tả",
|
||||
"updated": "Ngày Cập Nhật",
|
||||
"updated": "Đã Cập Nhật",
|
||||
"uploadWorkflow": "Tải Từ Tệp",
|
||||
"autoLayout": "Bố Trí Tự Động",
|
||||
"loadWorkflow": "$t(common.load) Workflow",
|
||||
@@ -2267,7 +2267,7 @@
|
||||
"saveWorkflowToProject": "Lưu Workflow Vào Dự Án",
|
||||
"workflowName": "Tên Workflow",
|
||||
"workflowLibrary": "Thư Viện Workflow",
|
||||
"opened": "Ngày Mở",
|
||||
"opened": "Đã Mở",
|
||||
"deleteWorkflow": "Xoá Workflow",
|
||||
"workflowEditorMenu": "Menu Biên Tập Workflow",
|
||||
"openLibrary": "Mở Thư Viện",
|
||||
@@ -2306,7 +2306,39 @@
|
||||
"containerColumnLayout": "Hộp Chứa (bố cục cột)",
|
||||
"resetOptions": "Tải Lại Lựa Chọn",
|
||||
"addOption": "Thêm Lựa Chọn",
|
||||
"dropdown": "Danh Sách Thả Xuống"
|
||||
"dropdown": "Danh Sách Thả Xuống",
|
||||
"publish": "Đăng Tải",
|
||||
"published": "Đã Đăng",
|
||||
"workflowLocked": "Workflow Bị Khóa",
|
||||
"workflowLockedDuringPublishing": "Workflow bị khóa khi đang điều chỉnh để đăng tải.",
|
||||
"selectOutputNode": "Chọn Node Đầu Ra",
|
||||
"changeOutputNode": "Đổi Node Đầu Ra",
|
||||
"publishedWorkflowOutputs": "Đầu Ra",
|
||||
"unpublishableInputs": "Những đầu vào không đăng tải được sẽ bị bỏ sót",
|
||||
"noPublishableInputs": "Không có đầu vào không đăng tải được",
|
||||
"noOutputNodeSelected": "Không có node đầu ra được chọn",
|
||||
"publishWarnings": "Cảnh Báo",
|
||||
"errorWorkflowHasUnsavedChanges": "Workflow có các thay đổi chưa lưu",
|
||||
"cannotPublish": "Không thể đăng workflow",
|
||||
"publishedWorkflowInputs": "Đầu Vào",
|
||||
"unpublish": "Chưa Đăng",
|
||||
"workflowLockedPublished": "Workflow được đăng tải sẽ bị khóa không thể biên tập.\nBạn có thể ngừng đăng để chỉnh sửa, hoặc tạo một bản sao của nó.",
|
||||
"errorWorkflowHasBatchOrGeneratorNodes": "Workflow có lô node và/hoặc node sản sinh",
|
||||
"errorWorkflowHasInvalidGraph": "Đồ thị workflow không hợp lệ (di chuột đến nút Khởi Động để xem chi tiết)",
|
||||
"errorWorkflowHasNoOutputNode": "Không có node đầu ra được chọn",
|
||||
"warningWorkflowHasUnpublishableInputFields": "Workflow có một số đầu ra không đăng được - chúng sẽ bị bỏ sót khỏi workflow",
|
||||
"publishFailed": "Đăng Tải Thất Bại",
|
||||
"publishFailedDesc": "Có vấn đề khi đăng tải workflow. Xin vui lòng thử lại.",
|
||||
"publishSuccessDesc": "Kiểm tra <LinkComponent>Bảng Dự Án</LinkComponent> để xem tiến độ.",
|
||||
"publishingValidationRun": "Kiểm Tra Tính Hợp Lệ",
|
||||
"publishedWorkflowsLocked": "Workflow đã đăng sẽ bị khóa và không thể biên tập hoặc chạy nữa. Hoặc là ngừng đăng, hoặc là lưu một bản sao của chính nó để biên tập hay chạy workflow này.",
|
||||
"publishInProgress": "Quá trình đăng tải đang diễn ra",
|
||||
"warningWorkflowHasNoPublishableInputFields": "Không có vùng đầu vào đăng tải được được chọn - workflow sẽ chạy với các giá trị mặc định",
|
||||
"publishSuccess": "Workflow của bạn đã được đăng",
|
||||
"publishedWorkflowIsLocked": "Workflow đã đăng đang bị khóa",
|
||||
"publishingValidationRunInProgress": "Quá trình kiểm tra tính hợp lệ đang diễn ra.",
|
||||
"selectingOutputNodeDesc": "Bấm vào node để biến nó thành node đầu ra của workflow.",
|
||||
"selectingOutputNode": "Chọn node đầu ra"
|
||||
},
|
||||
"yourWorkflows": "Workflow Của Bạn",
|
||||
"browseWorkflows": "Khám Phá Workflow",
|
||||
@@ -2323,7 +2355,8 @@
|
||||
"deselectAll": "Huỷ Chọn Tất Cả",
|
||||
"noRecentWorkflows": "Không Có Workflows Gần Đây",
|
||||
"recommended": "Có Thể Bạn Sẽ Cần",
|
||||
"emptyStringPlaceholder": "<xâu ký tự trống>"
|
||||
"emptyStringPlaceholder": "<xâu ký tự trống>",
|
||||
"published": "Đã Đăng"
|
||||
},
|
||||
"upscaling": {
|
||||
"missingUpscaleInitialImage": "Thiếu ảnh dùng để upscale",
|
||||
|
||||
@@ -1,54 +1,15 @@
|
||||
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
|
||||
import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator';
|
||||
import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { $didStudioInit, useStudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import { useSyncLoggingConfig } from 'app/logging/useSyncLoggingConfig';
|
||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import {
|
||||
NewCanvasSessionDialog,
|
||||
NewGallerySessionDialog,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||
import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import { AppContent } from 'features/ui/components/AppContent';
|
||||
import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog';
|
||||
import { LoadWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog';
|
||||
import i18n from 'i18n';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
import { useSocketIO } from 'services/events/useSocketIO';
|
||||
|
||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||
const DEFAULT_CONFIG = {};
|
||||
@@ -74,83 +35,10 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
<AppContent />
|
||||
{!didStudioInit && <Loading />}
|
||||
</Box>
|
||||
<HookIsolator config={config} studioInitAction={studioInitAction} />
|
||||
<ModalIsolator />
|
||||
<GlobalHookIsolator config={config} studioInitAction={studioInitAction} />
|
||||
<GlobalModalIsolator />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(App);
|
||||
|
||||
// Running these hooks in a separate component ensures we do not inadvertently rerender the entire app when they change.
|
||||
const HookIsolator = memo(
|
||||
({ config, studioInitAction }: { config: PartialAppConfig; studioInitAction?: StudioInitAction }) => {
|
||||
const language = useAppSelector(selectLanguage);
|
||||
const logger = useLogger('system');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// singleton!
|
||||
useReadinessWatcher();
|
||||
useSocketIO();
|
||||
useGlobalModifiersInit();
|
||||
useGlobalHotkeys();
|
||||
useGetOpenAPISchemaQuery();
|
||||
useSyncLoggingConfig();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (size(config)) {
|
||||
logger.info({ config }, 'Received config');
|
||||
dispatch(configChanged(config));
|
||||
}
|
||||
}, [dispatch, config, logger]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(appStarted());
|
||||
}, [dispatch]);
|
||||
|
||||
useStudioInitAction(studioInitAction);
|
||||
useStarterModelsToast();
|
||||
useSyncQueueStatus();
|
||||
useFocusRegionWatcher();
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
HookIsolator.displayName = 'HookIsolator';
|
||||
|
||||
const ModalIsolator = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<DeleteImageModal />
|
||||
<ChangeBoardModal />
|
||||
<DynamicPromptsModal />
|
||||
<StylePresetModal />
|
||||
<WorkflowLibraryModal />
|
||||
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<NewWorkflowConfirmationAlertDialog />
|
||||
<LoadWorkflowConfirmationAlertDialog />
|
||||
<DeleteStylePresetDialog />
|
||||
<DeleteWorkflowDialog />
|
||||
<ShareWorkflowModal />
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<NewGallerySessionDialog />
|
||||
<NewCanvasSessionDialog />
|
||||
<ImageContextMenu />
|
||||
<FullscreenDropzone />
|
||||
<VideosModal />
|
||||
<SaveWorkflowAsDialog />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasPasteModal />
|
||||
</CanvasManagerProviderGate>
|
||||
<LoadWorkflowFromGraphModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
ModalIsolator.displayName = 'ModalIsolator';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useStudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import { useSyncLoggingConfig } from 'app/logging/useSyncLoggingConfig';
|
||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import i18n from 'i18n';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
import { useSocketIO } from 'services/events/useSocketIO';
|
||||
|
||||
/**
|
||||
* GlobalHookIsolator is a logical component that runs global hooks in an isolated component, so that they do not
|
||||
* cause needless re-renders of any other components.
|
||||
*/
|
||||
export const GlobalHookIsolator = memo(
|
||||
({ config, studioInitAction }: { config: PartialAppConfig; studioInitAction?: StudioInitAction }) => {
|
||||
const language = useAppSelector(selectLanguage);
|
||||
const logger = useLogger('system');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// singleton!
|
||||
useReadinessWatcher();
|
||||
useSocketIO();
|
||||
useGlobalModifiersInit();
|
||||
useGlobalHotkeys();
|
||||
useGetOpenAPISchemaQuery();
|
||||
useSyncLoggingConfig();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (size(config)) {
|
||||
logger.info({ config }, 'Received config');
|
||||
dispatch(configChanged(config));
|
||||
}
|
||||
}, [dispatch, config, logger]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(appStarted());
|
||||
}, [dispatch]);
|
||||
|
||||
useStudioInitAction(studioInitAction);
|
||||
useStarterModelsToast();
|
||||
useSyncQueueStatus();
|
||||
useFocusRegionWatcher();
|
||||
useWorkflowBuilderWatcher();
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
GlobalHookIsolator.displayName = 'GlobalHookIsolator';
|
||||
@@ -0,0 +1,64 @@
|
||||
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import {
|
||||
NewCanvasSessionDialog,
|
||||
NewGallerySessionDialog,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||
import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
|
||||
import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog';
|
||||
import { LoadWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog';
|
||||
import { memo } from 'react';
|
||||
|
||||
/**
|
||||
* GlobalModalIsolator is a logical component that isolates global modal components, so that they do not cause needless
|
||||
* re-renders of any other components.
|
||||
*/
|
||||
export const GlobalModalIsolator = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<DeleteImageModal />
|
||||
<ChangeBoardModal />
|
||||
<DynamicPromptsModal />
|
||||
<StylePresetModal />
|
||||
<WorkflowLibraryModal />
|
||||
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<NewWorkflowConfirmationAlertDialog />
|
||||
<LoadWorkflowConfirmationAlertDialog />
|
||||
<DeleteStylePresetDialog />
|
||||
<DeleteWorkflowDialog />
|
||||
<ShareWorkflowModal />
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<NewGallerySessionDialog />
|
||||
<NewCanvasSessionDialog />
|
||||
<ImageContextMenu />
|
||||
<FullscreenDropzone />
|
||||
<VideosModal />
|
||||
<SaveWorkflowAsDialog />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasPasteModal />
|
||||
</CanvasManagerProviderGate>
|
||||
<LoadWorkflowFromGraphModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
GlobalModalIsolator.displayName = 'GlobalModalIsolator';
|
||||
@@ -1,20 +1,7 @@
|
||||
import type { UnknownAction } from '@reduxjs/toolkit';
|
||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
import type { Graph } from 'services/api/types';
|
||||
|
||||
export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
if (isAnyGraphBuilt(action)) {
|
||||
if (action.payload.nodes) {
|
||||
const sanitizedNodes: Graph['nodes'] = {};
|
||||
|
||||
return {
|
||||
...action,
|
||||
payload: { ...action.payload, nodes: sanitizedNodes },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (appInfoApi.endpoints.getOpenAPISchema.matchFulfilled(action)) {
|
||||
return {
|
||||
...action,
|
||||
|
||||
@@ -25,7 +25,6 @@ import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware
|
||||
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
|
||||
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
|
||||
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
|
||||
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
|
||||
import { addArchivedOrDeletedBoardListener } from './listeners/addArchivedOrDeletedBoardListener';
|
||||
@@ -85,9 +84,6 @@ addArchivedOrDeletedBoardListener(startAppListening);
|
||||
// Node schemas
|
||||
addGetOpenAPISchemaListener(startAppListening);
|
||||
|
||||
// Workflows
|
||||
addUpdateAllNodesRequestedListener(startAppListening);
|
||||
|
||||
// Models
|
||||
addModelSelectedListener(startAppListening);
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||
import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: updateAllNodesRequested,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const nodes = selectNodes(getState());
|
||||
const templates = $templates.get();
|
||||
|
||||
let unableToUpdateCount = 0;
|
||||
|
||||
nodes.filter(isInvocationNode).forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
unableToUpdateCount++;
|
||||
return;
|
||||
}
|
||||
if (!getNeedsUpdate(node.data, template)) {
|
||||
// No need to increment the count here, since we're not actually updating
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatedNode = updateNode(node, template);
|
||||
dispatch(
|
||||
nodesChanged([
|
||||
{ type: 'remove', id: updatedNode.id },
|
||||
{ type: 'add', item: updatedNode },
|
||||
])
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof NodeUpdateError) {
|
||||
unableToUpdateCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (unableToUpdateCount) {
|
||||
log.warn(
|
||||
t('nodes.unableToUpdateNodes', {
|
||||
count: unableToUpdateCount,
|
||||
})
|
||||
);
|
||||
toast({
|
||||
id: 'UNABLE_TO_UPDATE_NODES',
|
||||
title: t('nodes.unableToUpdateNodes', {
|
||||
count: unableToUpdateCount,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
id: 'ALL_NODES_UPDATED',
|
||||
title: t('nodes.allNodesUpdated'),
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
|
||||
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
|
||||
import { getDebugLoggerMiddleware } from 'app/store/middleware/debugLoggerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
|
||||
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
@@ -22,7 +21,6 @@ import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/model
|
||||
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
|
||||
import { workflowLibraryPersistConfig, workflowLibrarySlice } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
|
||||
@@ -60,7 +58,6 @@ const allReducers = {
|
||||
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
|
||||
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
|
||||
[queueSlice.name]: queueSlice.reducer,
|
||||
[workflowSlice.name]: workflowSlice.reducer,
|
||||
[hrfSlice.name]: hrfSlice.reducer,
|
||||
[canvasSlice.name]: undoable(canvasSlice.reducer, canvasUndoableConfig),
|
||||
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
|
||||
@@ -103,7 +100,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
||||
[galleryPersistConfig.name]: galleryPersistConfig,
|
||||
[nodesPersistConfig.name]: nodesPersistConfig,
|
||||
[systemPersistConfig.name]: systemPersistConfig,
|
||||
[workflowPersistConfig.name]: workflowPersistConfig,
|
||||
[uiPersistConfig.name]: uiPersistConfig,
|
||||
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
|
||||
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
||||
@@ -176,7 +172,6 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
.concat(api.middleware)
|
||||
.concat(dynamicMiddlewares)
|
||||
.concat(authToastMiddleware)
|
||||
.concat(getDebugLoggerMiddleware())
|
||||
.prepend(listenerMiddleware.middleware),
|
||||
enhancers: (getDefaultEnhancers) => {
|
||||
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import type { FLUXReduxImageInfluence as FLUXReduxImageInfluenceType } from 'features/controlLayers/store/types';
|
||||
import { isFLUXReduxImageInfluence } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type Props = {
|
||||
imageInfluence: FLUXReduxImageInfluenceType;
|
||||
onChange: (imageInfluence: FLUXReduxImageInfluenceType) => void;
|
||||
};
|
||||
|
||||
export const FLUXReduxImageInfluence = memo(({ imageInfluence, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
label: t('controlLayers.fluxReduxImageInfluence.lowest'),
|
||||
value: 'lowest',
|
||||
},
|
||||
{
|
||||
label: t('controlLayers.fluxReduxImageInfluence.low'),
|
||||
value: 'low',
|
||||
},
|
||||
{
|
||||
label: t('controlLayers.fluxReduxImageInfluence.medium'),
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
label: t('controlLayers.fluxReduxImageInfluence.high'),
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
label: t('controlLayers.fluxReduxImageInfluence.highest'),
|
||||
value: 'highest',
|
||||
},
|
||||
] satisfies { label: string; value: FLUXReduxImageInfluenceType }[],
|
||||
[t]
|
||||
);
|
||||
const _onChange = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
assert(isFLUXReduxImageInfluence(v?.value));
|
||||
onChange(v.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const value = useMemo(() => options.find((o) => o.value === imageInfluence), [options, imageInfluence]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel m={0}>{t('controlLayers.fluxReduxImageInfluence.imageInfluence')}</FormLabel>
|
||||
<Combobox value={value} options={options} onChange={_onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
FLUXReduxImageInfluence.displayName = 'FLUXReduxImageInfluence';
|
||||
@@ -61,7 +61,7 @@ export const IPAdapterImagePreview = memo(
|
||||
)}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
<DndImage imageDTO={imageDTO} borderWidth={1} borderStyle="solid" />
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
|
||||
return (
|
||||
<FormControl>
|
||||
<InformationalPopover feature="ipAdapterMethod">
|
||||
<FormLabel>{t('controlLayers.ipAdapterMethod.ipAdapterMethod')}</FormLabel>
|
||||
<FormLabel m={0}>{t('controlLayers.ipAdapterMethod.ipAdapterMethod')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox value={value} options={options} onChange={_onChange} />
|
||||
</FormControl>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginE
|
||||
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
|
||||
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
||||
import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
@@ -13,6 +14,7 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
referenceImageIPAdapterBeginEndStepPctChanged,
|
||||
referenceImageIPAdapterCLIPVisionModelChanged,
|
||||
referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
referenceImageIPAdapterMethodChanged,
|
||||
referenceImageIPAdapterModelChanged,
|
||||
@@ -20,7 +22,12 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntity, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
CLIPVisionModelV2,
|
||||
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||
IPMethodV2,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -65,6 +72,13 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeFLUXReduxImageInfluence = useCallback(
|
||||
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
||||
dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, imageInfluence }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
|
||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier, modelConfig }));
|
||||
@@ -116,7 +130,7 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
icon={<PiBoundingBoxBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={2} w="full" alignItems="center">
|
||||
<Flex gap={2} w="full">
|
||||
{ipAdapter.type === 'ip_adapter' && (
|
||||
<Flex flexDir="column" gap={2} w="full">
|
||||
{!isFLUX && <IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />}
|
||||
@@ -124,6 +138,14 @@ const IPAdapterSettingsContent = memo(() => {
|
||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||
</Flex>
|
||||
)}
|
||||
{ipAdapter.type === 'flux_redux' && (
|
||||
<Flex flexDir="column" gap={2} w="full" alignItems="flex-start">
|
||||
<FLUXReduxImageInfluence
|
||||
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
|
||||
onChange={onChangeFLUXReduxImageInfluence}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
|
||||
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
|
||||
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
||||
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
|
||||
@@ -15,13 +16,19 @@ import {
|
||||
rgIPAdapterBeginEndStepPctChanged,
|
||||
rgIPAdapterCLIPVisionModelChanged,
|
||||
rgIPAdapterDeleted,
|
||||
rgIPAdapterFLUXReduxImageInfluenceChanged,
|
||||
rgIPAdapterImageChanged,
|
||||
rgIPAdapterMethodChanged,
|
||||
rgIPAdapterModelChanged,
|
||||
rgIPAdapterWeightChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
CLIPVisionModelV2,
|
||||
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||
IPMethodV2,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -73,6 +80,13 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeFLUXReduxImageInfluence = useCallback(
|
||||
(imageInfluence: FLUXReduxImageInfluenceType) => {
|
||||
dispatch(rgIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
|
||||
},
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
|
||||
dispatch(rgIPAdapterModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
|
||||
@@ -151,6 +165,14 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||
</Flex>
|
||||
)}
|
||||
{ipAdapter.type === 'flux_redux' && (
|
||||
<Flex flexDir="column" gap={2} w="full">
|
||||
<FLUXReduxImageInfluence
|
||||
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
|
||||
onChange={onChangeFLUXReduxImageInfluence}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image}
|
||||
|
||||
@@ -407,8 +407,7 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
|
||||
|
||||
onStagePointerUp = (_e: KonvaEventObject<PointerEvent>) => {
|
||||
const color = this.$colorUnderCursor.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
this.manager.stateApi.setColor({ ...settings.color, ...color });
|
||||
this.manager.stateApi.setColor({ ...color, a: color.a / 255 });
|
||||
};
|
||||
|
||||
onStagePointerMove = (_e: KonvaEventObject<PointerEvent>) => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
ControlLoRAConfig,
|
||||
EntityMovedByPayload,
|
||||
FillStyle,
|
||||
FLUXReduxImageInfluence,
|
||||
RegionalGuidanceReferenceImageState,
|
||||
RgbColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -626,6 +627,20 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.method = method;
|
||||
},
|
||||
referenceImageIPAdapterFLUXReduxImageInfluenceChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ imageInfluence: FLUXReduxImageInfluence }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, imageInfluence } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
if (entity.ipAdapter.type !== 'flux_redux') {
|
||||
return;
|
||||
}
|
||||
entity.ipAdapter.imageInfluence = imageInfluence;
|
||||
},
|
||||
referenceImageIPAdapterModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
@@ -926,6 +941,26 @@ export const canvasSlice = createSlice({
|
||||
|
||||
referenceImage.ipAdapter.method = method;
|
||||
},
|
||||
rgIPAdapterFLUXReduxImageInfluenceChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<
|
||||
{ referenceImageId: string; imageInfluence: FLUXReduxImageInfluence },
|
||||
'regional_guidance'
|
||||
>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, referenceImageId, imageInfluence } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (referenceImage.ipAdapter.type !== 'flux_redux') {
|
||||
return;
|
||||
}
|
||||
|
||||
referenceImage.ipAdapter.imageInfluence = imageInfluence;
|
||||
},
|
||||
rgIPAdapterModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
@@ -1731,6 +1766,7 @@ export const {
|
||||
referenceImageIPAdapterCLIPVisionModelChanged,
|
||||
referenceImageIPAdapterWeightChanged,
|
||||
referenceImageIPAdapterBeginEndStepPctChanged,
|
||||
referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
|
||||
// Regions
|
||||
rgAdded,
|
||||
// rgRecalled,
|
||||
@@ -1746,6 +1782,7 @@ export const {
|
||||
rgIPAdapterMethodChanged,
|
||||
rgIPAdapterModelChanged,
|
||||
rgIPAdapterCLIPVisionModelChanged,
|
||||
rgIPAdapterFLUXReduxImageInfluenceChanged,
|
||||
// Inpaint mask
|
||||
inpaintMaskAdded,
|
||||
inpaintMaskConvertedToRegionalGuidance,
|
||||
|
||||
@@ -233,10 +233,15 @@ const zIPAdapterConfig = z.object({
|
||||
});
|
||||
export type IPAdapterConfig = z.infer<typeof zIPAdapterConfig>;
|
||||
|
||||
const zFLUXReduxImageInfluence = z.enum(['lowest', 'low', 'medium', 'high', 'highest']);
|
||||
export const isFLUXReduxImageInfluence = (v: unknown): v is FLUXReduxImageInfluence =>
|
||||
zFLUXReduxImageInfluence.safeParse(v).success;
|
||||
export type FLUXReduxImageInfluence = z.infer<typeof zFLUXReduxImageInfluence>;
|
||||
const zFLUXReduxConfig = z.object({
|
||||
type: z.literal('flux_redux'),
|
||||
image: zImageWithDims.nullable(),
|
||||
model: zServerValidatedModelIdentifierField.nullable(),
|
||||
imageInfluence: zFLUXReduxImageInfluence.default('highest'),
|
||||
});
|
||||
export type FLUXReduxConfig = z.infer<typeof zFLUXReduxConfig>;
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const initialFLUXRedux: FLUXReduxConfig = {
|
||||
type: 'flux_redux',
|
||||
image: null,
|
||||
model: null,
|
||||
imageInfluence: 'highest',
|
||||
};
|
||||
export const initialT2IAdapter: T2IAdapterConfig = {
|
||||
type: 't2i_adapter',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectWorkflowIsTouched } from 'features/nodes/store/workflowSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,7 +11,7 @@ const ClearFlowButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const isTouched = useAppSelector(selectWorkflowIsTouched);
|
||||
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
|
||||
|
||||
const handleNewWorkflow = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
@@ -26,12 +26,12 @@ const ClearFlowButton = () => {
|
||||
}, [dispatch, onClose, t]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!isTouched) {
|
||||
if (doesWorkflowHaveUnsavedChanges) {
|
||||
handleNewWorkflow();
|
||||
return;
|
||||
}
|
||||
onOpen();
|
||||
}, [handleNewWorkflow, isTouched, onOpen]);
|
||||
}, [doesWorkflowHaveUnsavedChanges, handleNewWorkflow, onOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowIsTouched } from 'features/nodes/store/workflowSlice';
|
||||
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,7 +7,7 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
|
||||
const SaveWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const isTouched = useAppSelector(selectWorkflowIsTouched);
|
||||
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
|
||||
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
|
||||
|
||||
return (
|
||||
@@ -16,7 +15,7 @@ const SaveWorkflowButton = () => {
|
||||
tooltip={t('workflows.saveWorkflow')}
|
||||
aria-label={t('workflows.saveWorkflow')}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
isDisabled={!isTouched}
|
||||
isDisabled={!doesWorkflowHaveUnsavedChanges}
|
||||
onClick={saveOrSaveAsWorkflow}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName';
|
||||
import { selectWorkflowName } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowName } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const TopCenterPanel = memo(() => {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
||||
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
|
||||
import {
|
||||
$isInPublishFlow,
|
||||
$isSelectingOutputNode,
|
||||
useIsValidationRunInProgress,
|
||||
useIsWorkflowPublished,
|
||||
} from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
|
||||
import { selectWorkflowIsPublished } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const TopLeftPanel = memo(() => {
|
||||
const isLocked = useIsWorkflowEditorLocked();
|
||||
const isInPublishFlow = useStore($isInPublishFlow);
|
||||
const isPublished = useAppSelector(selectWorkflowIsPublished);
|
||||
const isPublished = useIsWorkflowPublished();
|
||||
const isValidationRunInProgress = useIsValidationRunInProgress();
|
||||
const isSelectingOutputNode = useStore($isSelectingOutputNode);
|
||||
|
||||
|
||||
@@ -1,18 +1,82 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||
import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWarningBold } from 'react-icons/pi';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
const useUpdateNodes = () => {
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateNodes = useCallback(() => {
|
||||
const nodes = selectNodes(store.getState());
|
||||
const templates = $templates.get();
|
||||
|
||||
let unableToUpdateCount = 0;
|
||||
|
||||
nodes.filter(isInvocationNode).forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
unableToUpdateCount++;
|
||||
return;
|
||||
}
|
||||
if (!getNeedsUpdate(node.data, template)) {
|
||||
// No need to increment the count here, since we're not actually updating
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatedNode = updateNode(node, template);
|
||||
store.dispatch(
|
||||
nodesChanged([
|
||||
{ type: 'remove', id: updatedNode.id },
|
||||
{ type: 'add', item: updatedNode },
|
||||
])
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof NodeUpdateError) {
|
||||
unableToUpdateCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (unableToUpdateCount) {
|
||||
log.warn(
|
||||
t('nodes.unableToUpdateNodes', {
|
||||
count: unableToUpdateCount,
|
||||
})
|
||||
);
|
||||
toast({
|
||||
id: 'UNABLE_TO_UPDATE_NODES',
|
||||
title: t('nodes.unableToUpdateNodes', {
|
||||
count: unableToUpdateCount,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
id: 'ALL_NODES_UPDATED',
|
||||
title: t('nodes.allNodesUpdated'),
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
}, [store, t]);
|
||||
|
||||
return updateNodes;
|
||||
};
|
||||
|
||||
const UpdateNodesButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const nodesNeedUpdate = useGetNodesNeedUpdate();
|
||||
const handleClickUpdateNodes = useCallback(() => {
|
||||
dispatch(updateAllNodesRequested());
|
||||
}, [dispatch]);
|
||||
const updateNodes = useUpdateNodes();
|
||||
|
||||
if (!nodesNeedUpdate) {
|
||||
return null;
|
||||
@@ -23,7 +87,7 @@ const UpdateNodesButton = () => {
|
||||
tooltip={t('nodes.updateAllNodes')}
|
||||
aria-label={t('nodes.updateAllNodes')}
|
||||
icon={<PiWarningBold />}
|
||||
onClick={handleClickUpdateNodes}
|
||||
onClick={updateNodes}
|
||||
pointerEvents="auto"
|
||||
colorScheme="warning"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
import { Button, Flex, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowId } from 'features/nodes/store/selectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold, PiLockOpenBold } from 'react-icons/pi';
|
||||
import { useUnpublishWorkflowMutation } from 'services/api/endpoints/workflows';
|
||||
|
||||
export const PublishedWorkflowPanelContent = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const saveAs = useSaveOrSaveAsWorkflow();
|
||||
const [unpublishWorkflow] = useUnpublishWorkflowMutation();
|
||||
const workflowId = useAppSelector(selectWorkflowId);
|
||||
|
||||
const handleUnpublish = useCallback(async () => {
|
||||
if (workflowId) {
|
||||
try {
|
||||
await unpublishWorkflow(workflowId).unwrap();
|
||||
toast({
|
||||
title: t('toast.workflowUnpublished'),
|
||||
status: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t('toast.problemUnpublishingWorkflow'),
|
||||
description: t('toast.problemUnpublishingWorkflowDescription'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [unpublishWorkflow, workflowId, t]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" gap={2} alignItems="center">
|
||||
<Heading size="md" pt={32}>
|
||||
@@ -16,7 +41,7 @@ export const PublishedWorkflowPanelContent = memo(() => {
|
||||
<Button size="md" onClick={saveAs} variant="ghost" leftIcon={<PiCopyBold />}>
|
||||
{t('common.saveAs')}
|
||||
</Button>
|
||||
<Button size="md" onClick={undefined} variant="ghost" leftIcon={<PiLockOpenBold />}>
|
||||
<Button size="md" onClick={handleUnpublish} variant="ghost" leftIcon={<PiLockOpenBold />}>
|
||||
{t('workflows.builder.unpublish')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { linkifyOptions, linkifySx } from 'common/components/linkify';
|
||||
import { selectWorkflowDescription } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowDescription } from 'features/nodes/store/selectors';
|
||||
import Linkify from 'linkify-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsWorkflowPublished } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { WorkflowListMenuTrigger } from 'features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger';
|
||||
import { WorkflowViewEditToggleButton } from 'features/nodes/components/sidePanel/WorkflowViewEditToggleButton';
|
||||
import { selectWorkflowIsPublished, selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { WorkflowLibraryMenu } from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -10,7 +11,7 @@ import SaveWorkflowButton from './SaveWorkflowButton';
|
||||
|
||||
export const ActiveWorkflowNameAndActions = memo(() => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const isPublished = useAppSelector(selectWorkflowIsPublished);
|
||||
const isPublished = useIsWorkflowPublished();
|
||||
|
||||
return (
|
||||
<Flex w="full" alignItems="center" gap={1} minW={0}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowName } from 'features/nodes/store/selectors';
|
||||
import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal';
|
||||
import { selectWorkflowName } from 'features/nodes/store/workflowSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFolderOpenFill } from 'react-icons/pi';
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowIsTouched, selectWorkflowMode, selectWorkflowName } from 'features/nodes/store/workflowSlice';
|
||||
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { selectWorkflowName } from 'features/nodes/store/selectors';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDotOutlineFill } from 'react-icons/pi';
|
||||
|
||||
@@ -10,7 +12,7 @@ import { WorkflowWarning } from './viewMode/WorkflowWarning';
|
||||
export const WorkflowName = () => {
|
||||
const { t } = useTranslation();
|
||||
const name = useAppSelector(selectWorkflowName);
|
||||
const isTouched = useAppSelector(selectWorkflowIsTouched);
|
||||
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
|
||||
return (
|
||||
@@ -27,10 +29,10 @@ export const WorkflowName = () => {
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isTouched && mode === 'edit' && (
|
||||
{doesWorkflowHaveUnsavedChanges && mode === 'edit' && (
|
||||
<Tooltip label={t('nodes.newWorkflowDesc2')}>
|
||||
<Flex>
|
||||
<Icon as={PiDotOutlineFill} boxSize="20px" sx={{ color: 'invokeYellow.500' }} />
|
||||
<Icon as={PiDotOutlineFill} boxSize="20px" color="invokeYellow.500" />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -3,18 +3,18 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { EditModeLeftPanelContent } from 'features/nodes/components/sidePanel/EditModeLeftPanelContent';
|
||||
import { PublishedWorkflowPanelContent } from 'features/nodes/components/sidePanel/PublishedWorkflowPanelContent';
|
||||
import { $isInPublishFlow } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { $isInPublishFlow, useIsWorkflowPublished } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { PublishWorkflowPanelContent } from 'features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent';
|
||||
import { ActiveWorkflowDescription } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription';
|
||||
import { ActiveWorkflowNameAndActions } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowNameAndActions';
|
||||
import { selectWorkflowIsPublished, selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
|
||||
|
||||
const WorkflowsTabLeftPanel = () => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const isPublished = useAppSelector(selectWorkflowIsPublished);
|
||||
const isPublished = useIsWorkflowPublished();
|
||||
const isInPublishFlow = useStore($isInPublishFlow);
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,9 @@ import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/b
|
||||
import { HeadingElement } from 'features/nodes/components/sidePanel/builder/HeadingElement';
|
||||
import { NodeFieldElement } from 'features/nodes/components/sidePanel/builder/NodeFieldElement';
|
||||
import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement';
|
||||
import { selectFormRootElement, selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
import { selectFormRootElement } from 'features/nodes/store/selectors';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { ContainerElement } from 'features/nodes/types/workflow';
|
||||
import {
|
||||
CONTAINER_CLASS_NAME,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Portal,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { formElementContainerDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementContainerDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ContainerElement } from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { DividerElementEditMode } from 'features/nodes/components/sidePanel/builder/DividerElementEditMode';
|
||||
import { DividerElementViewMode } from 'features/nodes/components/sidePanel/builder/DividerElementViewMode';
|
||||
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { isDividerElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useDepthContext } from 'features/nodes/components/sidePanel/builder/con
|
||||
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
|
||||
import { useMouseOverFormField } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
|
||||
import { formElementRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementRemoved } from 'features/nodes/store/nodesSlice';
|
||||
import type { FormElement, NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { camelCase } from 'lodash-es';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { HeadingElementEditMode } from 'features/nodes/components/sidePanel/builder/HeadingElementEditMode';
|
||||
import { HeadingElementViewMode } from 'features/nodes/components/sidePanel/builder/HeadingElementViewMode';
|
||||
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { isHeadingElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEditable } from 'common/hooks/useEditable';
|
||||
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
|
||||
import { HeadingElementContent } from 'features/nodes/components/sidePanel/builder/HeadingElementContent';
|
||||
import { formElementHeadingDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementHeadingDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { HeadingElement } from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
|
||||
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
|
||||
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { isNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { fieldFloatValueChanged, formElementNodeFieldDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { type NodeFieldFloatSettings, zNumberComponent } from 'features/nodes/types/workflow';
|
||||
import { constrainNumber } from 'features/nodes/util/constrainNumber';
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { fieldIntegerValueChanged, formElementNodeFieldDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
|
||||
import { zNumberComponent } from 'features/nodes/types/workflow';
|
||||
|
||||
@@ -16,7 +16,7 @@ import { NodeFieldElementFloatSettings } from 'features/nodes/components/sidePan
|
||||
import { NodeFieldElementIntegerSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
|
||||
import { NodeFieldElementStringSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringSettings';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
isFloatFieldInputTemplate,
|
||||
isIntegerFieldInputTemplate,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, ButtonGroup, Divider, Flex, Grid, GridItem, IconButton, Input, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
|
||||
import { getDefaultStringOption, type NodeFieldStringDropdownSettings } from 'features/nodes/types/workflow';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { getDefaultStringOption, type NodeFieldStringSettings, zStringComponent } from 'features/nodes/types/workflow';
|
||||
import { omit } from 'lodash-es';
|
||||
import type { ChangeEvent } from 'react';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { TextElementEditMode } from 'features/nodes/components/sidePanel/builder/TextElementEditMode';
|
||||
import { TextElementViewMode } from 'features/nodes/components/sidePanel/builder/TextElementViewMode';
|
||||
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { isTextElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEditable } from 'common/hooks/useEditable';
|
||||
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
|
||||
import { TextElementContent } from 'features/nodes/components/sidePanel/builder/TextElementContent';
|
||||
import { formElementTextDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementTextDataChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { TextElement } from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { RootContainerElementEditMode } from 'features/nodes/components/sidePane
|
||||
import { buildFormElementDndData, useBuilderDndMonitor } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
|
||||
import { WorkflowBuilderEditMenu } from 'features/nodes/components/sidePanel/builder/WorkflowBuilderMenu';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
|
||||
import { selectIsFormEmpty } from 'features/nodes/store/selectors';
|
||||
import type { FormElement } from 'features/nodes/types/workflow';
|
||||
import { buildContainer, buildDivider, buildHeading, buildText } from 'features/nodes/types/workflow';
|
||||
import type { PropsWithChildren, RefObject } from 'react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useResetAllNodeFields } from 'features/nodes/components/sidePanel/builder/use-reset-all-node-fields';
|
||||
import { formReset } from 'features/nodes/store/workflowSlice';
|
||||
import { formReset } from 'features/nodes/store/nodesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiDotsThreeBold, PiTrashBold } from 'react-icons/pi';
|
||||
|
||||
@@ -27,14 +27,12 @@ import {
|
||||
getElement,
|
||||
getInitialValue,
|
||||
} from 'features/nodes/components/sidePanel/builder/form-manipulation';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import {
|
||||
formElementAdded,
|
||||
formElementContainerDataChanged,
|
||||
formElementReparented,
|
||||
selectFormRootElementId,
|
||||
selectWorkflowSlice,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { selectFormRootElementId, selectNodesSlice, selectWorkflowForm } from 'features/nodes/store/selectors';
|
||||
import type { FieldInputTemplate, StatefulFieldValue } from 'features/nodes/types/field';
|
||||
import type { ElementId, FormElement } from 'features/nodes/types/workflow';
|
||||
import { buildNodeFieldElement, isContainerElement } from 'features/nodes/types/workflow';
|
||||
@@ -100,7 +98,7 @@ const useGetElement = () => {
|
||||
const store = useAppStore();
|
||||
const _getElement = useCallback(
|
||||
<T extends FormElement>(elementId: ElementId, guard?: FormElementTypeGuard<T>): T => {
|
||||
const { form } = selectWorkflowSlice(store.getState());
|
||||
const form = selectWorkflowForm(store.getState());
|
||||
return getElement(form, elementId, guard);
|
||||
},
|
||||
[store]
|
||||
@@ -116,7 +114,7 @@ const useElementExists = () => {
|
||||
const store = useAppStore();
|
||||
const _elementExists = useCallback(
|
||||
(id: ElementId): boolean => {
|
||||
const { form } = selectWorkflowSlice(store.getState());
|
||||
const form = selectWorkflowForm(store.getState());
|
||||
return elementExists(form, id);
|
||||
},
|
||||
[store]
|
||||
@@ -132,7 +130,7 @@ const useGetAllowedDropRegions = () => {
|
||||
const store = useAppStore();
|
||||
const _getAllowedDropRegions = useCallback(
|
||||
(element: FormElement): CenterOrEdge[] => {
|
||||
const { form } = selectWorkflowSlice(store.getState());
|
||||
const form = selectWorkflowForm(store.getState());
|
||||
return getAllowedDropRegions(form, element);
|
||||
},
|
||||
[store]
|
||||
|
||||
@@ -22,6 +22,18 @@ import { assert, AssertionError } from 'tsafe';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('workflow builder form manipulation', () => {
|
||||
describe('getDefaultForm', () => {
|
||||
it('should return a form with a root element', () => {
|
||||
const form = getDefaultForm();
|
||||
expect(form).toHaveProperty('rootElementId');
|
||||
expect(form.elements[form.rootElementId]).toBeDefined();
|
||||
});
|
||||
it('should give the id "root" to the root element', () => {
|
||||
const form = getDefaultForm();
|
||||
expect(form.rootElementId).toBe('root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('elementExists', () => {
|
||||
const form = getDefaultForm();
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
|
||||
import { formElementAdded, selectFormRootElementId } from 'features/nodes/store/workflowSlice';
|
||||
import { formElementAdded } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFormRootElementId } from 'features/nodes/store/selectors';
|
||||
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildSelectElement } from 'features/nodes/store/selectors';
|
||||
import type { FormElement } from 'features/nodes/types/workflow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useElement = (id: string): FormElement | undefined => {
|
||||
const selector = useMemo(() => buildSelectElement(id), [id]);
|
||||
const element = useAppSelector(selector);
|
||||
return element;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFormInitialValues, selectNodeFieldElements } from 'features/nodes/store/workflowSlice';
|
||||
import { selectFormInitialValues, selectNodeFieldElements } from 'features/nodes/store/selectors';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useResetAllNodeFields = () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Button, Flex, Image, Link, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useIsWorkflowUntouched } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal';
|
||||
import { selectCleanEditor, workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import InvokeLogoSVG from 'public/assets/images/invoke-symbol-wht-lrg.svg';
|
||||
import { useCallback } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
export const EmptyState = () => {
|
||||
const isCleanEditor = useAppSelector(selectCleanEditor);
|
||||
const isWorkflowUntouched = useIsWorkflowUntouched();
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" userSelect="none" justifyContent="center">
|
||||
@@ -31,7 +32,7 @@ export const EmptyState = () => {
|
||||
minH={16}
|
||||
userSelect="none"
|
||||
/>
|
||||
{isCleanEditor ? <CleanEditorContent /> : <DirtyEditorContent />}
|
||||
{isWorkflowUntouched ? <CleanEditorContent /> : <DirtyEditorContent />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon
|
||||
import { RootContainerElementViewMode } from 'features/nodes/components/sidePanel/builder/ContainerElement';
|
||||
import { EmptyState } from 'features/nodes/components/sidePanel/viewMode/EmptyState';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
|
||||
import { selectIsFormEmpty } from 'features/nodes/store/selectors';
|
||||
import { t } from 'i18next';
|
||||
import { memo } from 'react';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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 { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
return {
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
notes: workflow.notes,
|
||||
author: workflow.author,
|
||||
tags: workflow.tags,
|
||||
name: nodes.name,
|
||||
description: nodes.description,
|
||||
notes: nodes.notes,
|
||||
author: nodes.author,
|
||||
tags: nodes.tags,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { EMPTY_OBJECT } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { getInitialWorkflow } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice, selectWorkflowId } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { buildWorkflowFast } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
|
||||
import stableHash from 'stable-hash';
|
||||
|
||||
const $maybePreviewWorkflow = atom<WorkflowV3 | null>(null);
|
||||
export const $previewWorkflow = computed(
|
||||
$maybePreviewWorkflow,
|
||||
(maybePreviewWorkflow) => maybePreviewWorkflow ?? EMPTY_OBJECT
|
||||
);
|
||||
const $previewWorkflowHash = computed($maybePreviewWorkflow, (maybePreviewWorkflow) => {
|
||||
if (maybePreviewWorkflow) {
|
||||
return stableHash(maybePreviewWorkflow);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const debouncedBuildPreviewWorkflow = debounce((nodesState: NodesState) => {
|
||||
$maybePreviewWorkflow.set(buildWorkflowFast(nodesState));
|
||||
}, 300);
|
||||
|
||||
export const useWorkflowBuilderWatcher = () => {
|
||||
useAssertSingleton('useWorkflowBuilderWatcher');
|
||||
const nodesState = useAppSelector(selectNodesSlice);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedBuildPreviewWorkflow(nodesState);
|
||||
}, [nodesState]);
|
||||
};
|
||||
|
||||
const queryOptions = {
|
||||
selectFromResult: ({ currentData }) => {
|
||||
if (!currentData) {
|
||||
return { serverWorkflowHash: null };
|
||||
}
|
||||
const { is_published: _is_published, ...serverWorkflow } = currentData.workflow;
|
||||
return {
|
||||
serverWorkflowHash: stableHash(serverWorkflow),
|
||||
};
|
||||
},
|
||||
} satisfies Parameters<typeof useGetWorkflowQuery>[1];
|
||||
|
||||
export const useDoesWorkflowHaveUnsavedChanges = () => {
|
||||
const workflowId = useAppSelector(selectWorkflowId);
|
||||
const previewWorkflowHash = useStore($previewWorkflowHash);
|
||||
const { serverWorkflowHash } = useGetWorkflowQuery(workflowId ?? skipToken, queryOptions);
|
||||
|
||||
const doesWorkflowHaveUnsavedChanges = useMemo(() => {
|
||||
if (serverWorkflowHash === null) {
|
||||
// If the hash is null, it means the workflow doesn't exist in the database
|
||||
return true;
|
||||
}
|
||||
return previewWorkflowHash !== serverWorkflowHash;
|
||||
}, [previewWorkflowHash, serverWorkflowHash]);
|
||||
|
||||
return doesWorkflowHaveUnsavedChanges;
|
||||
};
|
||||
|
||||
const initialWorkflowHash = stableHash({ ...getInitialWorkflow(), nodes: [], edges: [] });
|
||||
|
||||
export const useIsWorkflowUntouched = () => {
|
||||
const previewWorkflowHash = useStore($previewWorkflowHash);
|
||||
|
||||
const isWorkflowUntouched = useMemo(() => {
|
||||
return previewWorkflowHash === initialWorkflowHash;
|
||||
}, [previewWorkflowHash]);
|
||||
|
||||
return isWorkflowUntouched;
|
||||
};
|
||||
@@ -19,12 +19,13 @@ import { withResultAsync } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { ExternalLink } from 'features/gallery/components/ImageViewer/NoContentForViewer';
|
||||
import { NodeFieldElementOverlay } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
|
||||
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import {
|
||||
$isInPublishFlow,
|
||||
$isReadyToDoValidationRun,
|
||||
$isSelectingOutputNode,
|
||||
$outputNodeId,
|
||||
$validationRunBatchId,
|
||||
$validationRunData,
|
||||
usePublishInputs,
|
||||
} from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { useInputFieldTemplateTitleOrThrow } from 'features/nodes/hooks/useInputFieldTemplateTitleOrThrow';
|
||||
@@ -36,7 +37,6 @@ import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
|
||||
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
|
||||
import { selectHasBatchOrGeneratorNodes } from 'features/nodes/store/selectors';
|
||||
import { selectIsWorkflowSaved } from 'features/nodes/store/workflowSlice';
|
||||
import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows';
|
||||
import { $isReadyToEnqueue } from 'features/queue/store/readiness';
|
||||
import { selectAllowPublishWorkflows } from 'features/system/store/configSlice';
|
||||
@@ -200,7 +200,7 @@ const PublishWorkflowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isReadyToDoValidationRun = useStore($isReadyToDoValidationRun);
|
||||
const isReadyToEnqueue = useStore($isReadyToEnqueue);
|
||||
const isWorkflowSaved = useAppSelector(selectIsWorkflowSaved);
|
||||
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
|
||||
const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes);
|
||||
const outputNodeId = useStore($outputNodeId);
|
||||
const isSelectingOutputNode = useStore($isSelectingOutputNode);
|
||||
@@ -237,14 +237,18 @@ const PublishWorkflowButton = memo(() => {
|
||||
duration: null,
|
||||
});
|
||||
assert(result.value.enqueueResult.batch.batch_id);
|
||||
$validationRunBatchId.set(result.value.enqueueResult.batch.batch_id);
|
||||
assert(result.value.batchConfig.validation_run_data);
|
||||
$validationRunData.set({
|
||||
batchId: result.value.enqueueResult.batch.batch_id,
|
||||
workflowId: result.value.batchConfig.validation_run_data.workflow_id,
|
||||
});
|
||||
log.debug(parseify(result.value), 'Enqueued batch');
|
||||
}
|
||||
}, [enqueue, projectUrl, t]);
|
||||
|
||||
return (
|
||||
<PublishTooltip
|
||||
isWorkflowSaved={isWorkflowSaved}
|
||||
isWorkflowSaved={!doesWorkflowHaveUnsavedChanges}
|
||||
hasBatchOrGeneratorNodes={hasBatchOrGeneratorNodes}
|
||||
isReadyToEnqueue={isReadyToEnqueue}
|
||||
hasOutputNode={outputNodeId !== null && !isSelectingOutputNode}
|
||||
@@ -256,7 +260,7 @@ const PublishWorkflowButton = memo(() => {
|
||||
isDisabled={
|
||||
!allowPublishWorkflows ||
|
||||
!isReadyToEnqueue ||
|
||||
!isWorkflowSaved ||
|
||||
doesWorkflowHaveUnsavedChanges ||
|
||||
hasBatchOrGeneratorNodes ||
|
||||
!isReadyToDoValidationRun ||
|
||||
!(outputNodeId !== null && !isSelectingOutputNode)
|
||||
@@ -325,7 +329,7 @@ export const StartPublishFlowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const allowPublishWorkflows = useAppSelector(selectAllowPublishWorkflows);
|
||||
const isReadyToEnqueue = useStore($isReadyToEnqueue);
|
||||
const isWorkflowSaved = useAppSelector(selectIsWorkflowSaved);
|
||||
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
|
||||
const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes);
|
||||
const inputs = usePublishInputs();
|
||||
|
||||
@@ -335,7 +339,7 @@ export const StartPublishFlowButton = memo(() => {
|
||||
|
||||
return (
|
||||
<PublishTooltip
|
||||
isWorkflowSaved={isWorkflowSaved}
|
||||
isWorkflowSaved={!doesWorkflowHaveUnsavedChanges}
|
||||
hasBatchOrGeneratorNodes={hasBatchOrGeneratorNodes}
|
||||
isReadyToEnqueue={isReadyToEnqueue}
|
||||
hasOutputNode={true}
|
||||
@@ -347,7 +351,9 @@ export const StartPublishFlowButton = memo(() => {
|
||||
leftIcon={<PiLightningFill />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={!allowPublishWorkflows || !isReadyToEnqueue || !isWorkflowSaved || hasBatchOrGeneratorNodes}
|
||||
isDisabled={
|
||||
!allowPublishWorkflows || !isReadyToEnqueue || doesWorkflowHaveUnsavedChanges || hasBatchOrGeneratorNodes
|
||||
}
|
||||
>
|
||||
{t('workflows.builder.publish')}
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { FormControlProps } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, FormControl, FormControlGroup, FormLabel, Image, Input, Textarea } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import {
|
||||
selectWorkflowSlice,
|
||||
workflowAuthorChanged,
|
||||
workflowContactChanged,
|
||||
workflowDescriptionChanged,
|
||||
@@ -13,7 +11,17 @@ import {
|
||||
workflowNotesChanged,
|
||||
workflowTagsChanged,
|
||||
workflowVersionChanged,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
selectWorkflowAuthor,
|
||||
selectWorkflowContact,
|
||||
selectWorkflowDescription,
|
||||
selectWorkflowId,
|
||||
selectWorkflowName,
|
||||
selectWorkflowNotes,
|
||||
selectWorkflowTags,
|
||||
selectWorkflowVersion,
|
||||
} from 'features/nodes/store/selectors';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -21,23 +29,16 @@ import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
|
||||
|
||||
import { WorkflowThumbnailEditor } from './WorkflowThumbnail/WorkflowThumbnailEditor';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
const { id, author, name, description, tags, version, contact, notes } = workflow;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
tags,
|
||||
version,
|
||||
contact,
|
||||
notes,
|
||||
};
|
||||
});
|
||||
|
||||
const WorkflowGeneralTab = () => {
|
||||
const { id, author, name, description, tags, version, contact, notes } = useAppSelector(selector);
|
||||
const id = useAppSelector(selectWorkflowId);
|
||||
const name = useAppSelector(selectWorkflowName);
|
||||
const description = useAppSelector(selectWorkflowDescription);
|
||||
const notes = useAppSelector(selectWorkflowNotes);
|
||||
const author = useAppSelector(selectWorkflowAuthor);
|
||||
const contact = useAppSelector(selectWorkflowContact);
|
||||
const tags = useAppSelector(selectWorkflowTags);
|
||||
const version = useAppSelector(selectWorkflowVersion);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeName = useCallback(
|
||||
|
||||
@@ -1,43 +1,10 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { EMPTY_OBJECT } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { buildWorkflowFast } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { $previewWorkflow } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const $maybePreviewWorkflow = atom<WorkflowV3 | null>(null);
|
||||
const $previewWorkflow = computed(
|
||||
$maybePreviewWorkflow,
|
||||
(maybePreviewWorkflow) => maybePreviewWorkflow ?? EMPTY_OBJECT
|
||||
);
|
||||
|
||||
const debouncedBuildPreviewWorkflow = debounce(
|
||||
(nodes: NodesState['nodes'], edges: NodesState['edges'], workflow: WorkflowsState) => {
|
||||
$maybePreviewWorkflow.set(buildWorkflowFast({ nodes, edges, workflow }));
|
||||
},
|
||||
300
|
||||
);
|
||||
|
||||
const IsolatedWorkflowBuilderWatcher = memo(() => {
|
||||
const { nodes, edges } = useAppSelector(selectNodesSlice);
|
||||
const workflow = useAppSelector(selectWorkflowSlice);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedBuildPreviewWorkflow(nodes, edges, workflow);
|
||||
}, [edges, nodes, workflow]);
|
||||
|
||||
return null;
|
||||
});
|
||||
IsolatedWorkflowBuilderWatcher.displayName = 'IsolatedWorkflowBuilderWatcher';
|
||||
|
||||
const WorkflowJSONTab = () => {
|
||||
const previewWorkflow = useStore($previewWorkflow);
|
||||
const { t } = useTranslation();
|
||||
@@ -45,7 +12,6 @@ const WorkflowJSONTab = () => {
|
||||
return (
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2} h="full">
|
||||
<DataViewer data={previewWorkflow} label={t('nodes.workflow')} bg="base.850" color="base.200" />
|
||||
<IsolatedWorkflowBuilderWatcher />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Badge, Flex, Icon, Image, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { LockedWorkflowIcon } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/LockedWorkflowIcon';
|
||||
import { ShareWorkflowButton } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ShareWorkflow';
|
||||
import { selectWorkflowId, workflowModeChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowId } from 'features/nodes/store/selectors';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import InvokeLogo from 'public/assets/images/invoke-symbol-wht-lrg.svg';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -3,15 +3,19 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import {
|
||||
selectNodesSlice,
|
||||
selectWorkflowFormNodeFieldFieldIdentifiersDeduped,
|
||||
selectWorkflowId,
|
||||
} from 'features/nodes/store/selectors';
|
||||
import type { Templates } from 'features/nodes/store/types';
|
||||
import { selectWorkflowFormNodeFieldFieldIdentifiersDeduped } from 'features/nodes/store/workflowSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { isBoardFieldType } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { useGetBatchStatusQuery } from 'services/api/endpoints/queue';
|
||||
import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const $isInPublishFlow = atom(false);
|
||||
@@ -23,12 +27,12 @@ export const $isReadyToDoValidationRun = computed(
|
||||
return isInPublishFlow && outputNodeId !== null && !isSelectingOutputNode;
|
||||
}
|
||||
);
|
||||
export const $validationRunBatchId = atom<string | null>(null);
|
||||
export const $validationRunData = atom<{ batchId: string; workflowId: string } | null>(null);
|
||||
|
||||
export const useIsValidationRunInProgress = () => {
|
||||
const validationRunBatchId = useStore($validationRunBatchId);
|
||||
const validationRunData = useStore($validationRunData);
|
||||
const { isValidationRunInProgress } = useGetBatchStatusQuery(
|
||||
validationRunBatchId ? { batch_id: validationRunBatchId } : skipToken,
|
||||
validationRunData?.batchId ? { batch_id: validationRunData.batchId } : skipToken,
|
||||
{
|
||||
selectFromResult: ({ currentData }) => {
|
||||
if (!currentData) {
|
||||
@@ -41,7 +45,7 @@ export const useIsValidationRunInProgress = () => {
|
||||
},
|
||||
}
|
||||
);
|
||||
return validationRunBatchId !== null || isValidationRunInProgress;
|
||||
return validationRunData !== null || isValidationRunInProgress;
|
||||
};
|
||||
|
||||
export const selectFieldIdentifiersWithInvocationTypes = createSelector(
|
||||
@@ -88,3 +92,19 @@ export const usePublishInputs = () => {
|
||||
|
||||
return fieldIdentifiers;
|
||||
};
|
||||
|
||||
const queryOptions = {
|
||||
selectFromResult: ({ currentData }) => {
|
||||
if (!currentData) {
|
||||
return { isPublished: false };
|
||||
}
|
||||
return { isPublished: currentData.is_published };
|
||||
},
|
||||
} satisfies Parameters<typeof useGetWorkflowQuery>[1];
|
||||
|
||||
export const useIsWorkflowPublished = () => {
|
||||
const workflowId = useAppSelector(selectWorkflowId);
|
||||
const { isPublished } = useGetWorkflowQuery(workflowId ?? skipToken, queryOptions);
|
||||
|
||||
return isPublished;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -13,11 +12,11 @@ export const useInputFieldInitialFormValue = (elementId: string, nodeId: string,
|
||||
const dispatch = useAppDispatch();
|
||||
const selectInitialValue = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
if (!(elementId in workflow.formFieldInitialValues)) {
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
if (!(elementId in nodes.formFieldInitialValues)) {
|
||||
return uniqueNonexistentValue;
|
||||
}
|
||||
return workflow.formFieldInitialValues[elementId];
|
||||
return nodes.formFieldInitialValues[elementId];
|
||||
}),
|
||||
[elementId]
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $isInPublishFlow, useIsValidationRunInProgress } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { selectWorkflowIsPublished } from 'features/nodes/store/workflowSlice';
|
||||
import {
|
||||
$isInPublishFlow,
|
||||
useIsValidationRunInProgress,
|
||||
useIsWorkflowPublished,
|
||||
} from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
|
||||
export const useIsWorkflowEditorLocked = () => {
|
||||
const isInPublishFlow = useStore($isInPublishFlow);
|
||||
const isPublished = useAppSelector(selectWorkflowIsPublished);
|
||||
const isPublished = useIsWorkflowPublished();
|
||||
const isValidationRunInProgress = useIsValidationRunInProgress();
|
||||
|
||||
const isLocked = isInPublishFlow || isPublished || isValidationRunInProgress;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createAction, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import type { Graph } from 'services/api/types';
|
||||
|
||||
const textToImageGraphBuilt = createAction<Graph>('nodes/textToImageGraphBuilt');
|
||||
const imageToImageGraphBuilt = createAction<Graph>('nodes/imageToImageGraphBuilt');
|
||||
const canvasGraphBuilt = createAction<Graph>('nodes/canvasGraphBuilt');
|
||||
const nodesGraphBuilt = createAction<Graph>('nodes/nodesGraphBuilt');
|
||||
|
||||
export const isAnyGraphBuilt = isAnyOf(
|
||||
textToImageGraphBuilt,
|
||||
imageToImageGraphBuilt,
|
||||
canvasGraphBuilt,
|
||||
nodesGraphBuilt
|
||||
);
|
||||
|
||||
export const updateAllNodesRequested = createAction('nodes/updateAllNodesRequested');
|
||||
|
||||
export const workflowLoaded = createAction<WorkflowV3>('workflow/workflowLoaded');
|
||||
@@ -1,9 +1,24 @@
|
||||
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { EdgeChange, NodeChange, Viewport, XYPosition } from '@xyflow/react';
|
||||
import type {
|
||||
EdgeChange,
|
||||
EdgeSelectionChange,
|
||||
NodeChange,
|
||||
NodeDimensionChange,
|
||||
NodePositionChange,
|
||||
NodeSelectionChange,
|
||||
Viewport,
|
||||
XYPosition,
|
||||
} from '@xyflow/react';
|
||||
import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from '@xyflow/react';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import {
|
||||
addElement,
|
||||
removeElement,
|
||||
reparentElement,
|
||||
} from 'features/nodes/components/sidePanel/builder/form-manipulation';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import type {
|
||||
BoardFieldValue,
|
||||
@@ -83,17 +98,55 @@ import {
|
||||
} from 'features/nodes/types/field';
|
||||
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
|
||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||
import type {
|
||||
BuilderForm,
|
||||
ContainerElement,
|
||||
ElementId,
|
||||
FormElement,
|
||||
HeadingElement,
|
||||
NodeFieldElement,
|
||||
TextElement,
|
||||
WorkflowCategory,
|
||||
WorkflowV3,
|
||||
} from 'features/nodes/types/workflow';
|
||||
import {
|
||||
getDefaultForm,
|
||||
isContainerElement,
|
||||
isHeadingElement,
|
||||
isNodeFieldElement,
|
||||
isTextElement,
|
||||
} from 'features/nodes/types/workflow';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { UndoableOptions } from 'redux-undo';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { NodesState, PendingConnection, Templates } from './types';
|
||||
import type { PendingConnection, Templates } from './types';
|
||||
|
||||
const initialNodesState: NodesState = {
|
||||
export const getInitialWorkflow = (): Omit<NodesState, 'mode' | 'formFieldInitialValues' | '_version'> => {
|
||||
return {
|
||||
name: '',
|
||||
author: '',
|
||||
description: '',
|
||||
version: '',
|
||||
contact: '',
|
||||
tags: '',
|
||||
notes: '',
|
||||
exposedFields: [],
|
||||
meta: { version: '3.0.0', category: 'user' },
|
||||
form: getDefaultForm(),
|
||||
nodes: [],
|
||||
edges: [],
|
||||
// Even though this value is `undefined`, the keys _must_ be present for the presistence layer to rehydrate
|
||||
// them correctly. It uses a merge strategy that relies on the keys being present.
|
||||
id: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const initialState: NodesState = {
|
||||
_version: 1,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
formFieldInitialValues: {},
|
||||
...getInitialWorkflow(),
|
||||
};
|
||||
|
||||
type FieldValueAction<T extends FieldValue> = PayloadAction<{
|
||||
@@ -102,6 +155,24 @@ type FieldValueAction<T extends FieldValue> = PayloadAction<{
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
type FormElementDataChangedAction<T extends FormElement> = PayloadAction<{
|
||||
id: string;
|
||||
changes: Partial<T['data']>;
|
||||
}>;
|
||||
|
||||
const formElementDataChangedReducer = <T extends FormElement>(
|
||||
state: NodesState,
|
||||
action: FormElementDataChangedAction<T>,
|
||||
guard: (element: FormElement) => element is T
|
||||
) => {
|
||||
const { id, changes } = action.payload;
|
||||
const element = state.form?.elements[id];
|
||||
if (!element || !guard(element)) {
|
||||
return;
|
||||
}
|
||||
element.data = { ...element.data, ...changes } as T['data'];
|
||||
};
|
||||
|
||||
const getField = (nodeId: string, fieldName: string, state: NodesState) => {
|
||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||
const node = state.nodes?.[nodeIndex];
|
||||
@@ -131,7 +202,7 @@ const fieldValueReducer = <T extends FieldValue>(
|
||||
|
||||
export const nodesSlice = createSlice({
|
||||
name: 'nodes',
|
||||
initialState: initialNodesState,
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
nodesChanged: (state, action: PayloadAction<NodeChange<AnyNode>[]>) => {
|
||||
state.nodes = applyNodeChanges<AnyNode>(action.payload, state.nodes);
|
||||
@@ -145,6 +216,23 @@ export const nodesSlice = createSlice({
|
||||
}
|
||||
});
|
||||
state.edges = applyEdgeChanges<AnyEdge>(edgeChanges, state.edges);
|
||||
|
||||
// If a node was removed, we should remove any form fields that were associated with it. However, node changes
|
||||
// may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with
|
||||
// updated nodes. In this case, we should not remove the form fields. To handle this, we find the last remove
|
||||
// and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed
|
||||
// field.
|
||||
for (const el of Object.values(state.form.elements)) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const { nodeId } = el.data.fieldIdentifier;
|
||||
const removeIndex = action.payload.findLastIndex((change) => change.type === 'remove' && change.id === nodeId);
|
||||
const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId);
|
||||
if (removeIndex > addIndex) {
|
||||
removeElement({ form: state.form, id: el.id });
|
||||
}
|
||||
}
|
||||
},
|
||||
edgesChanged: (state, action: PayloadAction<EdgeChange<AnyEdge>[]>) => {
|
||||
const changes: EdgeChange<AnyEdge>[] = [];
|
||||
@@ -459,21 +547,101 @@ export const nodesSlice = createSlice({
|
||||
}
|
||||
node.data.notes = value;
|
||||
},
|
||||
nodeEditorReset: (state) => {
|
||||
state.nodes = [];
|
||||
state.edges = [];
|
||||
nodeEditorReset: () => deepClone(initialState),
|
||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||
state.name = action.payload;
|
||||
},
|
||||
workflowCategoryChanged: (state, action: PayloadAction<WorkflowCategory | undefined>) => {
|
||||
if (action.payload) {
|
||||
state.meta.category = action.payload;
|
||||
}
|
||||
},
|
||||
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.description = action.payload;
|
||||
},
|
||||
workflowTagsChanged: (state, action: PayloadAction<string>) => {
|
||||
state.tags = action.payload;
|
||||
},
|
||||
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
|
||||
state.author = action.payload;
|
||||
},
|
||||
workflowNotesChanged: (state, action: PayloadAction<string>) => {
|
||||
state.notes = action.payload;
|
||||
},
|
||||
workflowVersionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.version = action.payload;
|
||||
},
|
||||
workflowContactChanged: (state, action: PayloadAction<string>) => {
|
||||
state.contact = action.payload;
|
||||
},
|
||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||
state.id = action.payload;
|
||||
},
|
||||
formReset: (state) => {
|
||||
state.form = getDefaultForm();
|
||||
},
|
||||
formElementAdded: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
element: FormElement;
|
||||
parentId: ElementId;
|
||||
index?: number;
|
||||
initialValue?: StatefulFieldValue;
|
||||
}>
|
||||
) => {
|
||||
const { form } = state;
|
||||
const { element, parentId, index, initialValue } = action.payload;
|
||||
addElement({ form, element, parentId, index });
|
||||
if (isNodeFieldElement(element)) {
|
||||
state.formFieldInitialValues[element.id] = initialValue;
|
||||
}
|
||||
},
|
||||
formElementRemoved: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { form } = state;
|
||||
const { id } = action.payload;
|
||||
removeElement({ form, id });
|
||||
delete state.formFieldInitialValues[id];
|
||||
},
|
||||
formElementReparented: (state, action: PayloadAction<{ id: string; newParentId: string; index: number }>) => {
|
||||
const { form } = state;
|
||||
const { id, newParentId, index } = action.payload;
|
||||
reparentElement({ form, id, newParentId, index });
|
||||
},
|
||||
formElementHeadingDataChanged: (state, action: FormElementDataChangedAction<HeadingElement>) => {
|
||||
formElementDataChangedReducer(state, action, isHeadingElement);
|
||||
},
|
||||
formElementTextDataChanged: (state, action: FormElementDataChangedAction<TextElement>) => {
|
||||
formElementDataChangedReducer(state, action, isTextElement);
|
||||
},
|
||||
formElementNodeFieldDataChanged: (state, action: FormElementDataChangedAction<NodeFieldElement>) => {
|
||||
formElementDataChangedReducer(state, action, isNodeFieldElement);
|
||||
},
|
||||
formElementContainerDataChanged: (state, action: FormElementDataChangedAction<ContainerElement>) => {
|
||||
formElementDataChangedReducer(state, action, isContainerElement);
|
||||
},
|
||||
formFieldInitialValuesChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ formFieldInitialValues: NodesState['formFieldInitialValues'] }>
|
||||
) => {
|
||||
const { formFieldInitialValues } = action.payload;
|
||||
state.formFieldInitialValues = formFieldInitialValues;
|
||||
},
|
||||
workflowLoaded: (state, action: PayloadAction<WorkflowV3>) => {
|
||||
const { nodes, edges, is_published: _is_published, ...workflowExtra } = action.payload;
|
||||
|
||||
const formFieldInitialValues = getFormFieldInitialValues(workflowExtra.form, nodes);
|
||||
|
||||
return {
|
||||
...deepClone(initialState),
|
||||
...deepClone(workflowExtra),
|
||||
formFieldInitialValues,
|
||||
nodes: nodes.map((node) => ({ ...SHARED_NODE_PROPERTIES, ...node })),
|
||||
edges,
|
||||
};
|
||||
},
|
||||
undo: (state) => state,
|
||||
redo: (state) => state,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(workflowLoaded, (state, action) => {
|
||||
const { nodes, edges } = action.payload;
|
||||
|
||||
state.nodes = nodes.map((node) => ({ ...SHARED_NODE_PROPERTIES, ...node }));
|
||||
state.edges = edges;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
@@ -524,6 +692,25 @@ export const {
|
||||
nodesChanged,
|
||||
nodeUseCacheChanged,
|
||||
notesNodeValueChanged,
|
||||
workflowNameChanged,
|
||||
workflowCategoryChanged,
|
||||
workflowDescriptionChanged,
|
||||
workflowTagsChanged,
|
||||
workflowAuthorChanged,
|
||||
workflowNotesChanged,
|
||||
workflowVersionChanged,
|
||||
workflowContactChanged,
|
||||
workflowIDChanged,
|
||||
formReset,
|
||||
formElementAdded,
|
||||
formElementRemoved,
|
||||
formElementReparented,
|
||||
formElementHeadingDataChanged,
|
||||
formElementTextDataChanged,
|
||||
formElementNodeFieldDataChanged,
|
||||
formElementContainerDataChanged,
|
||||
formFieldInitialValuesChanged,
|
||||
workflowLoaded,
|
||||
undo,
|
||||
redo,
|
||||
} = nodesSlice.actions;
|
||||
@@ -553,38 +740,144 @@ const migrateNodesState = (state: any): any => {
|
||||
|
||||
export const nodesPersistConfig: PersistConfig<NodesState> = {
|
||||
name: nodesSlice.name,
|
||||
initialState: initialNodesState,
|
||||
initialState: initialState,
|
||||
migrate: migrateNodesState,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
const isSelectionAction = (action: UnknownAction) => {
|
||||
if (nodesChanged.match(action)) {
|
||||
if (action.payload.every((change) => change.type === 'select')) {
|
||||
return true;
|
||||
}
|
||||
type NodeSelectionAction = {
|
||||
type: ReturnType<typeof nodesChanged>['type'];
|
||||
payload: NodeSelectionChange[];
|
||||
};
|
||||
|
||||
type EdgeSelectionAction = {
|
||||
type: ReturnType<typeof edgesChanged>['type'];
|
||||
payload: EdgeSelectionChange[];
|
||||
};
|
||||
|
||||
const isNodeSelectionAction = (action: UnknownAction): action is NodeSelectionAction => {
|
||||
if (!nodesChanged.match(action)) {
|
||||
return false;
|
||||
}
|
||||
if (edgesChanged.match(action)) {
|
||||
if (action.payload.every((change) => change.type === 'select')) {
|
||||
return true;
|
||||
}
|
||||
if (action.payload.every((change) => change.type === 'select')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const individualGroupByMatcher = isAnyOf(nodesChanged);
|
||||
const isEdgeSelectionAction = (action: UnknownAction): action is EdgeSelectionAction => {
|
||||
if (!edgesChanged.match(action)) {
|
||||
return false;
|
||||
}
|
||||
if (action.payload.every((change) => change.type === 'select')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
type NodeDimensionChangeAction = {
|
||||
type: ReturnType<typeof nodesChanged>['type'];
|
||||
payload: NodeDimensionChange[];
|
||||
};
|
||||
|
||||
const isDimensionsChangeAction = (action: UnknownAction): action is NodeDimensionChangeAction => {
|
||||
if (!nodesChanged.match(action)) {
|
||||
return false;
|
||||
}
|
||||
if (action.payload.every((change) => change.type === 'dimensions')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
type NodePositionChangeAction = {
|
||||
type: ReturnType<typeof nodesChanged>['type'];
|
||||
payload: (NodeDimensionChange | NodePositionChange)[];
|
||||
};
|
||||
|
||||
const isPositionChangeAction = (action: UnknownAction): action is NodePositionChangeAction => {
|
||||
if (!nodesChanged.match(action)) {
|
||||
return false;
|
||||
}
|
||||
if (action.payload.every((change) => change.type === 'position')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Match field mutations that are high frequency and should be grouped together - for example, when a user is
|
||||
// typing in a text field, we don't want to create a new undo group for every keystroke.
|
||||
const isHighFrequencyFieldChangeAction = isAnyOf(
|
||||
fieldLabelChanged,
|
||||
fieldIntegerValueChanged,
|
||||
fieldFloatValueChanged,
|
||||
fieldFloatCollectionValueChanged,
|
||||
fieldIntegerCollectionValueChanged,
|
||||
fieldStringValueChanged,
|
||||
fieldStringCollectionValueChanged,
|
||||
fieldFloatGeneratorValueChanged,
|
||||
fieldIntegerGeneratorValueChanged,
|
||||
fieldStringGeneratorValueChanged,
|
||||
fieldImageGeneratorValueChanged,
|
||||
fieldDescriptionChanged
|
||||
);
|
||||
|
||||
// Match form changes that are high frequency and should be grouped together - for example, when a user is
|
||||
// typing in a text field, we don't want to create a new undo group for every keystroke.
|
||||
const isHighFrequencyFormChangeAction = isAnyOf(
|
||||
formElementHeadingDataChanged,
|
||||
formElementTextDataChanged,
|
||||
formElementNodeFieldDataChanged,
|
||||
formElementContainerDataChanged
|
||||
);
|
||||
|
||||
// Match workflow changes that are high frequency and should be grouped together - for example, when a user is
|
||||
// updating the workflow description, we don't want to create a new undo group for every keystroke.
|
||||
const isHighFrequencyWorkflowDetailsAction = isAnyOf(
|
||||
workflowNameChanged,
|
||||
workflowDescriptionChanged,
|
||||
workflowTagsChanged,
|
||||
workflowAuthorChanged,
|
||||
workflowNotesChanged,
|
||||
workflowVersionChanged,
|
||||
workflowContactChanged
|
||||
);
|
||||
|
||||
// Match node-scoped actions that are high frequency and should be grouped together - for example, when a user is
|
||||
// updating the node label, we don't want to create a new undo group for every keystroke. Or when a user is writing
|
||||
// a note in a notes node, we don't want to create a new undo group for every keystroke.
|
||||
const isHighFrequencyNodeScopedAction = isAnyOf(nodeLabelChanged, nodeNotesChanged, notesNodeValueChanged);
|
||||
|
||||
export const nodesUndoableConfig: UndoableOptions<NodesState, UnknownAction> = {
|
||||
limit: 64,
|
||||
undoType: nodesSlice.actions.undo.type,
|
||||
redoType: nodesSlice.actions.redo.type,
|
||||
groupBy: (action, state, history) => {
|
||||
if (isSelectionAction(action)) {
|
||||
// Changes to selection should never be recorded on their own
|
||||
return history.group;
|
||||
groupBy: (action, _state, _history) => {
|
||||
if (isHighFrequencyFieldChangeAction(action)) {
|
||||
// Group by type, node id and field name
|
||||
const { type, payload } = action;
|
||||
const { nodeId, fieldName } = payload;
|
||||
return `${type}-${nodeId}-${fieldName}`;
|
||||
}
|
||||
if (individualGroupByMatcher(action)) {
|
||||
return action.type;
|
||||
if (isPositionChangeAction(action)) {
|
||||
const ids = action.payload.map((change) => change.id).join(',');
|
||||
// Group by type and node ids
|
||||
return `dimensions-or-position-${ids}`;
|
||||
}
|
||||
if (isHighFrequencyFormChangeAction(action)) {
|
||||
// Group by type and form element id
|
||||
const { type, payload } = action;
|
||||
const { id } = payload;
|
||||
return `${type}-${id}`;
|
||||
}
|
||||
if (isHighFrequencyWorkflowDetailsAction(action)) {
|
||||
return 'workflow-details';
|
||||
}
|
||||
if (isHighFrequencyNodeScopedAction(action)) {
|
||||
const { type, payload } = action;
|
||||
const { nodeId } = payload;
|
||||
// Group by type and node id
|
||||
return `${type}-${nodeId}`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -593,51 +886,42 @@ export const nodesUndoableConfig: UndoableOptions<NodesState, UnknownAction> = {
|
||||
if (!action.type.startsWith(nodesSlice.name)) {
|
||||
return false;
|
||||
}
|
||||
if (nodesChanged.match(action)) {
|
||||
if (action.payload.every((change) => change.type === 'dimensions')) {
|
||||
return false;
|
||||
}
|
||||
// Ignore actions that only select or deselect nodes and edges
|
||||
if (isNodeSelectionAction(action) || isEdgeSelectionAction(action)) {
|
||||
return false;
|
||||
}
|
||||
if (isDimensionsChangeAction(action)) {
|
||||
// Ignore actions that only change the dimensions of nodes - these are internal to reactflow
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
// This is used for tracking `state.workflow.isTouched`
|
||||
export const isAnyNodeOrEdgeMutation = isAnyOf(
|
||||
edgesChanged,
|
||||
fieldBoardValueChanged,
|
||||
fieldBooleanValueChanged,
|
||||
fieldColorValueChanged,
|
||||
fieldControlNetModelValueChanged,
|
||||
fieldEnumModelValueChanged,
|
||||
fieldImageValueChanged,
|
||||
fieldImageCollectionValueChanged,
|
||||
fieldIPAdapterModelValueChanged,
|
||||
fieldT2IAdapterModelValueChanged,
|
||||
fieldLabelChanged,
|
||||
fieldLoRAModelValueChanged,
|
||||
fieldLLaVAModelValueChanged,
|
||||
fieldMainModelValueChanged,
|
||||
fieldIntegerValueChanged,
|
||||
fieldIntegerCollectionValueChanged,
|
||||
fieldFloatValueChanged,
|
||||
fieldFloatCollectionValueChanged,
|
||||
fieldRefinerModelValueChanged,
|
||||
fieldSchedulerValueChanged,
|
||||
fieldStringValueChanged,
|
||||
fieldStringCollectionValueChanged,
|
||||
fieldVaeModelValueChanged,
|
||||
fieldT5EncoderValueChanged,
|
||||
fieldCLIPEmbedValueChanged,
|
||||
fieldCLIPLEmbedValueChanged,
|
||||
fieldCLIPGEmbedValueChanged,
|
||||
fieldFluxVAEModelValueChanged,
|
||||
// The `nodesChanged` has extra logic and is handled in its own extra reducer
|
||||
// nodesChanged,
|
||||
nodeIsIntermediateChanged,
|
||||
nodeIsOpenChanged,
|
||||
nodeLabelChanged,
|
||||
nodeNotesChanged,
|
||||
nodeUseCacheChanged,
|
||||
notesNodeValueChanged
|
||||
);
|
||||
// The form builder's initial values are based on the current values of the node fields in the workflow.
|
||||
export const getFormFieldInitialValues = (form: BuilderForm, nodes: NodesState['nodes']) => {
|
||||
const formFieldInitialValues: Record<string, StatefulFieldValue> = {};
|
||||
|
||||
for (const el of Object.values(form.elements)) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const { nodeId, fieldName } = el.data.fieldIdentifier;
|
||||
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field = node.data.inputs[fieldName];
|
||||
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
formFieldInitialValues[el.id] = field.value;
|
||||
}
|
||||
|
||||
return formFieldInitialValues;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { getElement } from 'features/nodes/components/sidePanel/builder/form-manipulation';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import type { AnyNode, InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { isBatchNode, isGeneratorNode, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const selectNode = (nodesSlice: NodesState, nodeId: string): AnyNode => {
|
||||
@@ -85,3 +88,41 @@ export const selectMayRedo = createSelector(
|
||||
export const selectHasBatchOrGeneratorNodes = createSelector(selectNodes, (nodes) =>
|
||||
nodes.filter(isInvocationNode).some((node) => isBatchNode(node) || isGeneratorNode(node))
|
||||
);
|
||||
|
||||
export const selectWorkflowName = createNodesSelector((nodes) => nodes.name);
|
||||
|
||||
export const selectWorkflowId = createNodesSelector((workflow) => workflow.id);
|
||||
export const selectWorkflowDescription = createNodesSelector((workflow) => workflow.description);
|
||||
export const selectWorkflowNotes = createNodesSelector((workflow) => workflow.notes);
|
||||
export const selectWorkflowAuthor = createNodesSelector((workflow) => workflow.author);
|
||||
export const selectWorkflowContact = createNodesSelector((workflow) => workflow.contact);
|
||||
export const selectWorkflowTags = createNodesSelector((workflow) => workflow.tags);
|
||||
export const selectWorkflowVersion = createNodesSelector((workflow) => workflow.version);
|
||||
export const selectWorkflowForm = createNodesSelector((workflow) => workflow.form);
|
||||
|
||||
export const selectFormRootElementId = createNodesSelector((workflow) => {
|
||||
return workflow.form.rootElementId;
|
||||
});
|
||||
export const selectFormRootElement = createNodesSelector((workflow) => {
|
||||
return getElement(workflow.form, workflow.form.rootElementId, isContainerElement);
|
||||
});
|
||||
export const selectIsFormEmpty = createNodesSelector((workflow) => {
|
||||
const rootElement = workflow.form.elements[workflow.form.rootElementId];
|
||||
if (!rootElement || !isContainerElement(rootElement)) {
|
||||
return true;
|
||||
}
|
||||
return rootElement.data.children.length === 0;
|
||||
});
|
||||
export const selectFormInitialValues = createNodesSelector((workflow) => workflow.formFieldInitialValues);
|
||||
export const selectNodeFieldElements = createNodesSelector((workflow) =>
|
||||
Object.values(workflow.form.elements).filter(isNodeFieldElement)
|
||||
);
|
||||
export const selectWorkflowFormNodeFieldFieldIdentifiersDeduped = createSelector(
|
||||
selectNodeFieldElements,
|
||||
(nodeFieldElements) =>
|
||||
uniqBy(nodeFieldElements, (el) => `${el.data.fieldIdentifier.nodeId}-${el.data.fieldIdentifier.fieldName}`).map(
|
||||
(el) => el.data.fieldIdentifier
|
||||
)
|
||||
);
|
||||
|
||||
export const buildSelectElement = (id: string) => createNodesSelector((workflow) => workflow.form?.elements[id]);
|
||||
|
||||
@@ -13,17 +13,11 @@ export type PendingConnection = {
|
||||
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
|
||||
};
|
||||
|
||||
export type WorkflowMode = 'edit' | 'view';
|
||||
|
||||
export type NodesState = {
|
||||
_version: 1;
|
||||
nodes: AnyNode[];
|
||||
edges: AnyEdge[];
|
||||
};
|
||||
|
||||
export type WorkflowMode = 'edit' | 'view';
|
||||
|
||||
export type WorkflowsState = Omit<WorkflowV3, 'nodes' | 'edges'> & {
|
||||
_version: 1;
|
||||
isTouched: boolean;
|
||||
mode: WorkflowMode;
|
||||
formFieldInitialValues: Record<string, StatefulFieldValue>;
|
||||
};
|
||||
} & Omit<WorkflowV3, 'nodes' | 'edges' | 'is_published'>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { WorkflowMode } from 'features/nodes/store/types';
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
|
||||
@@ -8,6 +9,7 @@ import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types'
|
||||
export type WorkflowLibraryView = 'recent' | 'yours' | 'private' | 'shared' | 'defaults' | 'published';
|
||||
|
||||
type WorkflowLibraryState = {
|
||||
mode: WorkflowMode;
|
||||
view: WorkflowLibraryView;
|
||||
orderBy: WorkflowRecordOrderBy;
|
||||
direction: SQLiteDirection;
|
||||
@@ -16,6 +18,7 @@ type WorkflowLibraryState = {
|
||||
};
|
||||
|
||||
const initialWorkflowLibraryState: WorkflowLibraryState = {
|
||||
mode: 'view',
|
||||
searchTerm: '',
|
||||
orderBy: 'opened_at',
|
||||
direction: 'DESC',
|
||||
@@ -27,6 +30,9 @@ export const workflowLibrarySlice = createSlice({
|
||||
name: 'workflowLibrary',
|
||||
initialState: initialWorkflowLibraryState,
|
||||
reducers: {
|
||||
workflowModeChanged: (state, action: PayloadAction<WorkflowMode>) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
workflowLibrarySearchTermChanged: (state, action: PayloadAction<string>) => {
|
||||
state.searchTerm = action.payload;
|
||||
},
|
||||
@@ -60,6 +66,7 @@ export const workflowLibrarySlice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
workflowModeChanged,
|
||||
workflowLibrarySearchTermChanged,
|
||||
workflowLibraryOrderByChanged,
|
||||
workflowLibraryDirectionChanged,
|
||||
@@ -82,6 +89,7 @@ const selectWorkflowLibrarySlice = (state: RootState) => state.workflowLibrary;
|
||||
const createWorkflowLibrarySelector = <T>(selector: Selector<WorkflowLibraryState, T>) =>
|
||||
createSelector(selectWorkflowLibrarySlice, selector);
|
||||
|
||||
export const selectWorkflowMode = createWorkflowLibrarySelector((workflow) => workflow.mode);
|
||||
export const selectWorkflowLibrarySearchTerm = createWorkflowLibrarySelector(({ searchTerm }) => searchTerm);
|
||||
export const selectWorkflowLibraryHasSearchTerm = createWorkflowLibrarySelector(({ searchTerm }) => !!searchTerm);
|
||||
export const selectWorkflowLibraryOrderBy = createWorkflowLibrarySelector(({ orderBy }) => orderBy);
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import {
|
||||
addElement,
|
||||
getElement,
|
||||
removeElement,
|
||||
reparentElement,
|
||||
} from 'features/nodes/components/sidePanel/builder/form-manipulation';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { NodesState, WorkflowMode, WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
||||
import type { FieldIdentifier, StatefulFieldValue } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import type {
|
||||
BuilderForm,
|
||||
ContainerElement,
|
||||
ElementId,
|
||||
FormElement,
|
||||
HeadingElement,
|
||||
NodeFieldElement,
|
||||
TextElement,
|
||||
WorkflowCategory,
|
||||
WorkflowV3,
|
||||
} from 'features/nodes/types/workflow';
|
||||
import {
|
||||
buildContainer,
|
||||
getDefaultForm,
|
||||
isContainerElement,
|
||||
isHeadingElement,
|
||||
isNodeFieldElement,
|
||||
isTextElement,
|
||||
} from 'features/nodes/types/workflow';
|
||||
import { isEqual, uniqBy } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { selectNodesSlice } from './selectors';
|
||||
|
||||
type FormElementDataChangedAction<T extends FormElement> = PayloadAction<{
|
||||
id: string;
|
||||
changes: Partial<T['data']>;
|
||||
}>;
|
||||
|
||||
const formElementDataChangedReducer = <T extends FormElement>(
|
||||
state: WorkflowState,
|
||||
action: FormElementDataChangedAction<T>,
|
||||
guard: (element: FormElement) => element is T
|
||||
) => {
|
||||
const { id, changes } = action.payload;
|
||||
const element = state.form?.elements[id];
|
||||
if (!element || !guard(element)) {
|
||||
return;
|
||||
}
|
||||
element.data = { ...element.data, ...changes } as T['data'];
|
||||
};
|
||||
|
||||
const getBlankWorkflow = (): Omit<WorkflowV3, 'nodes' | 'edges'> => {
|
||||
return {
|
||||
name: '',
|
||||
author: '',
|
||||
description: '',
|
||||
version: '',
|
||||
contact: '',
|
||||
tags: '',
|
||||
notes: '',
|
||||
exposedFields: [],
|
||||
meta: { version: '3.0.0', category: 'user' },
|
||||
form: getDefaultForm(),
|
||||
// Even though these values are `undefined`, the keys _must_ be present for the presistence layer to rehydrate
|
||||
// them correctly. It uses a merge strategy that relies on the keys being present.
|
||||
id: undefined,
|
||||
is_published: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const initialWorkflowState: WorkflowState = {
|
||||
_version: 1,
|
||||
isTouched: false,
|
||||
mode: 'view',
|
||||
formFieldInitialValues: {},
|
||||
...getBlankWorkflow(),
|
||||
};
|
||||
|
||||
export const workflowSlice = createSlice({
|
||||
name: 'workflow',
|
||||
initialState: initialWorkflowState,
|
||||
reducers: {
|
||||
workflowModeChanged: (state, action: PayloadAction<WorkflowMode>) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||
state.name = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowCategoryChanged: (state, action: PayloadAction<WorkflowCategory | undefined>) => {
|
||||
if (action.payload) {
|
||||
state.meta.category = action.payload;
|
||||
}
|
||||
},
|
||||
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.description = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowTagsChanged: (state, action: PayloadAction<string>) => {
|
||||
state.tags = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
|
||||
state.author = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowNotesChanged: (state, action: PayloadAction<string>) => {
|
||||
state.notes = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowVersionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.version = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowContactChanged: (state, action: PayloadAction<string>) => {
|
||||
state.contact = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||
state.id = action.payload;
|
||||
},
|
||||
workflowIsPublishedChanged(state, action: PayloadAction<boolean>) {
|
||||
state.is_published = action.payload;
|
||||
},
|
||||
workflowSaved: (state) => {
|
||||
state.isTouched = false;
|
||||
},
|
||||
formReset: (state) => {
|
||||
const rootElement = buildContainer('column', []);
|
||||
state.form = {
|
||||
elements: { [rootElement.id]: rootElement },
|
||||
rootElementId: rootElement.id,
|
||||
};
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementAdded: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
element: FormElement;
|
||||
parentId: ElementId;
|
||||
index?: number;
|
||||
initialValue?: StatefulFieldValue;
|
||||
}>
|
||||
) => {
|
||||
const { form } = state;
|
||||
const { element, parentId, index, initialValue } = action.payload;
|
||||
addElement({ form, element, parentId, index });
|
||||
if (isNodeFieldElement(element)) {
|
||||
state.formFieldInitialValues[element.id] = initialValue;
|
||||
}
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementRemoved: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { form } = state;
|
||||
const { id } = action.payload;
|
||||
removeElement({ form, id });
|
||||
delete state.formFieldInitialValues[id];
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementReparented: (state, action: PayloadAction<{ id: string; newParentId: string; index: number }>) => {
|
||||
const { form } = state;
|
||||
const { id, newParentId, index } = action.payload;
|
||||
reparentElement({ form, id, newParentId, index });
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementHeadingDataChanged: (state, action: FormElementDataChangedAction<HeadingElement>) => {
|
||||
formElementDataChangedReducer(state, action, isHeadingElement);
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementTextDataChanged: (state, action: FormElementDataChangedAction<TextElement>) => {
|
||||
formElementDataChangedReducer(state, action, isTextElement);
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementNodeFieldDataChanged: (state, action: FormElementDataChangedAction<NodeFieldElement>) => {
|
||||
formElementDataChangedReducer(state, action, isNodeFieldElement);
|
||||
state.isTouched = true;
|
||||
},
|
||||
formElementContainerDataChanged: (state, action: FormElementDataChangedAction<ContainerElement>) => {
|
||||
formElementDataChangedReducer(state, action, isContainerElement);
|
||||
state.isTouched = true;
|
||||
},
|
||||
formFieldInitialValuesChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ formFieldInitialValues: WorkflowState['formFieldInitialValues'] }>
|
||||
) => {
|
||||
const { formFieldInitialValues } = action.payload;
|
||||
state.formFieldInitialValues = formFieldInitialValues;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(workflowLoaded, (state, action): WorkflowState => {
|
||||
const { nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
|
||||
const formFieldInitialValues = getFormFieldInitialValues(workflowExtra.form, nodes);
|
||||
|
||||
return {
|
||||
...deepClone(initialWorkflowState),
|
||||
...deepClone(workflowExtra),
|
||||
formFieldInitialValues,
|
||||
mode: state.mode,
|
||||
};
|
||||
});
|
||||
|
||||
builder.addCase(nodeEditorReset, (state) => {
|
||||
const mode = state.mode;
|
||||
const newState = deepClone(initialWorkflowState);
|
||||
newState.mode = mode;
|
||||
return newState;
|
||||
});
|
||||
|
||||
builder.addCase(nodesChanged, (state, action) => {
|
||||
// If a node was removed, we should remove any exposed fields that were associated with it. However, node changes
|
||||
// may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with
|
||||
// updated nodes. In this case, we should not remove the exposed fields. To handle this, we find the last remove
|
||||
// and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed
|
||||
// field.
|
||||
const fieldsToRemove: FieldIdentifier[] = [];
|
||||
|
||||
state.exposedFields.forEach((field) => {
|
||||
const removeIndex = action.payload.findLastIndex(
|
||||
(change) => change.type === 'remove' && change.id === field.nodeId
|
||||
);
|
||||
const addIndex = action.payload.findLastIndex(
|
||||
(change) => change.type === 'add' && change.item.id === field.nodeId
|
||||
);
|
||||
if (removeIndex > addIndex) {
|
||||
fieldsToRemove.push({ nodeId: field.nodeId, fieldName: field.fieldName });
|
||||
}
|
||||
});
|
||||
state.exposedFields = state.exposedFields.filter((field) => !fieldsToRemove.some((f) => isEqual(f, field)));
|
||||
|
||||
if (state.form) {
|
||||
for (const el of Object.values(state.form?.elements || {})) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const { nodeId } = el.data.fieldIdentifier;
|
||||
const removeIndex = action.payload.findLastIndex(
|
||||
(change) => change.type === 'remove' && change.id === nodeId
|
||||
);
|
||||
const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId);
|
||||
if (removeIndex > addIndex) {
|
||||
removeElement({ form: state.form, id: el.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 || fieldsToRemove.length > 0) {
|
||||
state.isTouched = true;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
|
||||
state.isTouched = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
workflowModeChanged,
|
||||
workflowNameChanged,
|
||||
workflowCategoryChanged,
|
||||
workflowDescriptionChanged,
|
||||
workflowTagsChanged,
|
||||
workflowAuthorChanged,
|
||||
workflowNotesChanged,
|
||||
workflowVersionChanged,
|
||||
workflowContactChanged,
|
||||
workflowIDChanged,
|
||||
workflowIsPublishedChanged,
|
||||
workflowSaved,
|
||||
formReset,
|
||||
formElementAdded,
|
||||
formElementRemoved,
|
||||
formElementReparented,
|
||||
formElementHeadingDataChanged,
|
||||
formElementTextDataChanged,
|
||||
formElementNodeFieldDataChanged,
|
||||
formElementContainerDataChanged,
|
||||
formFieldInitialValuesChanged,
|
||||
} = workflowSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrateWorkflowState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
state._version = 1;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const workflowPersistConfig: PersistConfig<WorkflowState> = {
|
||||
name: workflowSlice.name,
|
||||
initialState: initialWorkflowState,
|
||||
migrate: migrateWorkflowState,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectWorkflowSlice = (state: RootState) => state.workflow;
|
||||
const createWorkflowSelector = <T>(selector: Selector<WorkflowState, T>) =>
|
||||
createSelector(selectWorkflowSlice, selector);
|
||||
|
||||
// The form builder's initial values are based on the current values of the node fields in the workflow.
|
||||
export const getFormFieldInitialValues = (form: BuilderForm, nodes: NodesState['nodes']) => {
|
||||
const formFieldInitialValues: Record<string, StatefulFieldValue> = {};
|
||||
|
||||
for (const el of Object.values(form.elements)) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const { nodeId, fieldName } = el.data.fieldIdentifier;
|
||||
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field = node.data.inputs[fieldName];
|
||||
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
formFieldInitialValues[el.id] = field.value;
|
||||
}
|
||||
|
||||
return formFieldInitialValues;
|
||||
};
|
||||
|
||||
export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.name);
|
||||
export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id);
|
||||
export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode);
|
||||
export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched);
|
||||
export const selectWorkflowDescription = createWorkflowSelector((workflow) => workflow.description);
|
||||
export const selectWorkflowForm = createWorkflowSelector((workflow) => workflow.form);
|
||||
export const selectWorkflowIsPublished = createWorkflowSelector((workflow) => workflow.is_published);
|
||||
export const selectIsWorkflowSaved = createSelector(selectWorkflowId, selectWorkflowIsTouched, (id, isTouched) => {
|
||||
return id !== undefined && !isTouched;
|
||||
});
|
||||
|
||||
export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflowSlice], (nodes, workflow) => {
|
||||
const noNodes = !nodes.nodes.length;
|
||||
const isTouched = workflow.isTouched;
|
||||
const savedWorkflow = !!workflow.id;
|
||||
return noNodes && !isTouched && !savedWorkflow;
|
||||
});
|
||||
|
||||
export const selectFormRootElementId = createWorkflowSelector((workflow) => {
|
||||
return workflow.form.rootElementId;
|
||||
});
|
||||
export const selectFormRootElement = createWorkflowSelector((workflow) => {
|
||||
return getElement(workflow.form, workflow.form.rootElementId, isContainerElement);
|
||||
});
|
||||
export const selectIsFormEmpty = createWorkflowSelector((workflow) => {
|
||||
const rootElement = workflow.form.elements[workflow.form.rootElementId];
|
||||
if (!rootElement || !isContainerElement(rootElement)) {
|
||||
return true;
|
||||
}
|
||||
return rootElement.data.children.length === 0;
|
||||
});
|
||||
export const selectFormInitialValues = createWorkflowSelector((workflow) => workflow.formFieldInitialValues);
|
||||
export const selectNodeFieldElements = createWorkflowSelector((workflow) =>
|
||||
Object.values(workflow.form.elements).filter(isNodeFieldElement)
|
||||
);
|
||||
export const selectWorkflowFormNodeFieldFieldIdentifiersDeduped = createSelector(
|
||||
selectNodeFieldElements,
|
||||
(nodeFieldElements) =>
|
||||
uniqBy(nodeFieldElements, (el) => `${el.data.fieldIdentifier.nodeId}-${el.data.fieldIdentifier.fieldName}`).map(
|
||||
(el) => el.data.fieldIdentifier
|
||||
)
|
||||
);
|
||||
|
||||
const buildSelectElement = (id: string) => createWorkflowSelector((workflow) => workflow.form?.elements[id]);
|
||||
export const useElement = (id: string): FormElement | undefined => {
|
||||
const selector = useMemo(() => buildSelectElement(id), [id]);
|
||||
const element = useAppSelector(selector);
|
||||
return element;
|
||||
};
|
||||
@@ -258,8 +258,10 @@ const zFormElement = z.union([zContainerElement, zNodeFieldElement, zHeadingElem
|
||||
|
||||
export type FormElement = z.infer<typeof zFormElement>;
|
||||
|
||||
const ROOT_ELEMENT_ID = 'root';
|
||||
export const getDefaultForm = (): BuilderForm => {
|
||||
const rootElement = buildContainer('column', []);
|
||||
rootElement.id = ROOT_ELEMENT_ID;
|
||||
return {
|
||||
elements: {
|
||||
[rootElement.id]: rootElement,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { CanvasReferenceImageState, FLUXReduxConfig } from 'features/controlLayers/store/types';
|
||||
import type {
|
||||
CanvasReferenceImageState,
|
||||
FLUXReduxConfig,
|
||||
FLUXReduxImageInfluence,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isFLUXReduxConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
@@ -36,6 +40,45 @@ export const addFLUXReduxes = ({ entities, g, collector, model }: AddFLUXReduxAr
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* To fine-tune the image influence, edit this object.
|
||||
* - downsampling_factor: 1 to 9, where 1 is the most image influence and 9 is the least. 1 is FLUX redux in its original form.
|
||||
* - downsampling_function: the function used to downsample the image. Defaults to 'area'. Dunno about how it affects the image.
|
||||
* - weight: 0 to 1. the conditioning is multiplied by the square of this value. 1 means no change.
|
||||
*
|
||||
* See invokeai/app/invocations/flux_redux.py for more details.
|
||||
*/
|
||||
export const IMAGE_INFLUENCE_TO_SETTINGS: Record<
|
||||
FLUXReduxImageInfluence,
|
||||
Pick<Invocation<'flux_redux'>, 'downsampling_factor' | 'downsampling_function' | 'weight'>
|
||||
> = {
|
||||
lowest: {
|
||||
downsampling_factor: 5,
|
||||
// downsampling_function: 'area',
|
||||
weight: 1,
|
||||
},
|
||||
low: {
|
||||
downsampling_factor: 4,
|
||||
// downsampling_function: 'area',
|
||||
weight: 1,
|
||||
},
|
||||
medium: {
|
||||
downsampling_factor: 3,
|
||||
// downsampling_function: 'area',
|
||||
weight: 1,
|
||||
},
|
||||
high: {
|
||||
downsampling_factor: 2,
|
||||
// downsampling_function: 'area',
|
||||
weight: 1,
|
||||
},
|
||||
highest: {
|
||||
downsampling_factor: 1,
|
||||
// downsampling_function: 'area',
|
||||
weight: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const addFLUXRedux = (id: string, ipAdapter: FLUXReduxConfig, g: Graph, collector: Invocation<'collect'>) => {
|
||||
const { model: fluxReduxModel, image } = ipAdapter;
|
||||
assert(image, 'FLUX Redux image is required');
|
||||
@@ -48,6 +91,7 @@ const addFLUXRedux = (id: string, ipAdapter: FLUXReduxConfig, g: Graph, collecto
|
||||
image: {
|
||||
image_name: image.image_name,
|
||||
},
|
||||
...IMAGE_INFLUENCE_TO_SETTINGS[ipAdapter.imageInfluence ?? 'highest'],
|
||||
});
|
||||
|
||||
g.addEdge(node, 'redux_cond', collector, 'item');
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types';
|
||||
import { getRegionalGuidanceWarnings } from 'features/controlLayers/store/validators';
|
||||
import { IMAGE_INFLUENCE_TO_SETTINGS } from 'features/nodes/util/graph/generation/addFLUXRedux';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import type { Invocation, MainModelConfig } from 'services/api/types';
|
||||
@@ -313,6 +314,7 @@ export const addRegions = async ({
|
||||
image: {
|
||||
image_name: image.image_name,
|
||||
},
|
||||
...IMAGE_INFLUENCE_TO_SETTINGS[ipAdapter.imageInfluence ?? 'highest'],
|
||||
});
|
||||
|
||||
// Connect the mask to the conditioning
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
||||
@@ -15,12 +14,6 @@ import { fromZodError } from 'zod-validation-error';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
type BuildWorkflowArg = {
|
||||
nodes: NodesState['nodes'];
|
||||
edges: NodesState['edges'];
|
||||
workflow: WorkflowsState;
|
||||
};
|
||||
|
||||
const workflowKeys = [
|
||||
'name',
|
||||
'author',
|
||||
@@ -35,10 +28,9 @@ const workflowKeys = [
|
||||
'form',
|
||||
] satisfies (keyof WorkflowV3)[];
|
||||
|
||||
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
|
||||
|
||||
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
|
||||
const clonedWorkflow = pick(workflow, workflowKeys);
|
||||
export const buildWorkflowFast = (nodesState: NodesState): WorkflowV3 => {
|
||||
const { nodes, edges, ...rest } = nodesState;
|
||||
const clonedWorkflow = pick(rest, workflowKeys);
|
||||
|
||||
const newWorkflow: WorkflowV3 = {
|
||||
...clonedWorkflow,
|
||||
@@ -69,9 +61,9 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
|
||||
return deepClone(newWorkflow);
|
||||
};
|
||||
|
||||
export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 | null => {
|
||||
export const buildWorkflowWithValidation = (nodesState: NodesState): WorkflowV3 | null => {
|
||||
// builds what really, really should be a valid workflow
|
||||
const workflowToValidate = buildWorkflowFast({ nodes, edges, workflow });
|
||||
const workflowToValidate = buildWorkflowFast(nodesState);
|
||||
|
||||
// but bc we are storing this in the DB, let's be extra sure
|
||||
const result = zWorkflowV3.safeParse(workflowToValidate);
|
||||
@@ -91,10 +83,8 @@ export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWor
|
||||
export const useBuildWorkflowFast = (): (() => WorkflowV3) => {
|
||||
const store = useAppStore();
|
||||
const buildWorkflow = useCallback(() => {
|
||||
const state = store.getState();
|
||||
const { nodes, edges } = selectNodesSlice(state);
|
||||
const workflow = selectWorkflowSlice(state);
|
||||
return buildWorkflowFast({ nodes, edges, workflow });
|
||||
const nodesState = selectNodesSlice(store.getState());
|
||||
return buildWorkflowFast(nodesState);
|
||||
}, [store]);
|
||||
|
||||
return buildWorkflow;
|
||||
|
||||
@@ -6,10 +6,9 @@ import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsS
|
||||
import { selectIterations } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { $isInPublishFlow } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { $isInPublishFlow, useIsWorkflowPublished } from 'features/nodes/components/sidePanel/workflow/publish';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { selectWorkflowIsPublished } from 'features/nodes/store/workflowSlice';
|
||||
import type { BatchSizeResult } from 'features/nodes/util/node/resolveBatchValue';
|
||||
import { getBatchSize } from 'features/nodes/util/node/resolveBatchValue';
|
||||
import type { Reason } from 'features/queue/store/readiness';
|
||||
@@ -178,7 +177,7 @@ const IsReadyText = memo(({ isReady, prepend }: { isReady: boolean; prepend: boo
|
||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||
const [_, enqueueMutation] = useEnqueueBatchMutation(enqueueMutationFixedCacheKeyOptions);
|
||||
const isInPublishFlow = useStore($isInPublishFlow);
|
||||
const isPublished = useAppSelector(selectWorkflowIsPublished);
|
||||
const isPublished = useIsWorkflowPublished();
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (enqueueMutation.isLoading) {
|
||||
|
||||
@@ -120,11 +120,11 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
)}
|
||||
</Flex>
|
||||
<Flex alignItems="center" w={COLUMN_WIDTHS.validationRun} flexShrink={0}>
|
||||
{!isValidationRun && <Badge>{t('workflows.builder.publishingValidationRun')}</Badge>}
|
||||
{isValidationRun && <Badge>{t('workflows.builder.publishingValidationRun')}</Badge>}
|
||||
</Flex>
|
||||
<Flex alignItems="center" w={COLUMN_WIDTHS.actions} pe={3}>
|
||||
<ButtonGroup size="xs" variant="ghost">
|
||||
{(!isFailed || !isRetryEnabled) && (
|
||||
{(!isFailed || !isRetryEnabled || isValidationRun) && (
|
||||
<IconButton
|
||||
onClick={handleCancelQueueItem}
|
||||
isDisabled={isCanceled}
|
||||
@@ -133,7 +133,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
)}
|
||||
{isFailed && isRetryEnabled && (
|
||||
{isFailed && isRetryEnabled && !isValidationRun && (
|
||||
<IconButton
|
||||
onClick={handleRetryQueueItem}
|
||||
isLoading={isLoadingRetryQueueItem}
|
||||
|
||||
@@ -26,14 +26,9 @@ export const useEnqueueWorkflows = () => {
|
||||
dispatch(enqueueRequestedWorkflows());
|
||||
const state = getState();
|
||||
const nodesState = selectNodesSlice(state);
|
||||
const workflow = state.workflow;
|
||||
const templates = $templates.get();
|
||||
const graph = buildNodesGraph(state, templates);
|
||||
const builtWorkflow = buildWorkflowWithValidation({
|
||||
nodes: nodesState.nodes,
|
||||
edges: nodesState.edges,
|
||||
workflow,
|
||||
});
|
||||
const builtWorkflow = buildWorkflowWithValidation(nodesState);
|
||||
|
||||
if (builtWorkflow) {
|
||||
// embedded workflows don't have an id
|
||||
@@ -134,10 +129,10 @@ export const useEnqueueWorkflows = () => {
|
||||
} as const;
|
||||
});
|
||||
|
||||
assert(workflow.id, 'Workflow without ID cannot be used for API validation run');
|
||||
assert(nodesState.id, 'Workflow without ID cannot be used for API validation run');
|
||||
|
||||
batchConfig.validation_run_data = {
|
||||
workflow_id: workflow.id,
|
||||
workflow_id: nodesState.id,
|
||||
input_fields: api_input_fields,
|
||||
output_fields: api_output_fields,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ReactFlowProvider } from '@xyflow/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const WorkflowsMainPanel = memo(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
import type { CanvasRightPanelTabName, TabName, UIState } from './uiTypes';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user