Merge branch 'main' into fix/post-model-sync

This commit is contained in:
blessedcoolant 2023-07-20 20:16:14 +12:00 committed by GitHub
commit 0795d8764f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1161 additions and 436 deletions

View File

@ -1,42 +1,38 @@
# How to Contribute
## Welcome to Invoke AI ## Welcome to Invoke AI
We're thrilled to have you here and we're excited for you to contribute.
Invoke AI originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use. Invoke AI originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use.
Here are some guidelines to help you get started:
### Technical Prerequisites ## Contributing to Invoke AI
Anyone who wishes to contribute to InvokeAI, whether features, bug fixes, code cleanup, testing, code reviews, documentation or translation is very much encouraged to do so.
Front-end: You'll need a working knowledge of React and TypeScript. To join, just raise your hand on the InvokeAI Discord server (#dev-chat) or the GitHub discussion board.
Back-end: Depending on the scope of your contribution, you may need to know SQLite, FastAPI, Python, and Socketio. Also, a good majority of the backend logic involved in processing images is built in a modular way using a concept called "Nodes", which are isolated functions that carry out individual, discrete operations. This design allows for easy contributions of novel pipelines and capabilities. ### Areas of contribution:
### How to Submit Contributions #### Development
If youd like to help with development, please see our [development guide](contribution_guides/development.md). If youre unfamiliar with contributing to open source projects, there is a tutorial contained within the development guide.
To start contributing, please follow these steps: #### Documentation
If youd like to help with documentation, please see our [documentation guide](contribution_guides/documenation.md).
1. Familiarize yourself with our roadmap and open projects to see where your skills and interests align. These documents can serve as a source of inspiration. #### Translation
2. Open a Pull Request (PR) with a clear description of the feature you're adding or the problem you're solving. Make sure your contribution aligns with the project's vision. If you'd like to help with translation, please see our [translation guide](docs/contributing/.contribution_guides/translation.md).
3. Adhere to general best practices. This includes assuming interoperability with other nodes, keeping the scope of your functions as small as possible, and organizing your code according to our architecture documents.
### Types of Contributions We're Looking For #### Tutorials
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.
We welcome all contributions that improve the project. Right now, we're especially looking for: We hope you enjoy using our software as much as we enjoy creating it, and we hope that some of those of you who are reading this will elect to become part of our contributor community.
1. Quality of life (QOL) enhancements on the front-end.
2. New backend capabilities added through nodes.
3. Incorporating additional optimizations from the broader open-source software community.
### Communication and Decision-making Process ### Contributors
Project maintainers and code owners review PRs to ensure they align with the project's goals. They may provide design or architectural guidance, suggestions on user experience, or provide more significant feedback on the contribution itself. Expect to receive feedback on your submissions, and don't hesitate to ask questions or propose changes. This project is a combined effort of dedicated people from across the world. [Check out the list of all these amazing people](https://invoke-ai.github.io/InvokeAI/other/CONTRIBUTORS/). We thank them for their time, hard work and effort.
For more robust discussions, or if you're planning to add capabilities not currently listed on our roadmap, please reach out to us on our Discord server. That way, we can ensure your proposed contribution aligns with the project's direction before you start writing code. ### Code of Conduct
### Code of Conduct and Contribution Expectations The InvokeAI community is a welcoming place, and we want your help in maintaining that. Please review our [Code of Conduct](https://github.com/invoke-ai/InvokeAI/blob/main/CODE_OF_CONDUCT.md) to learn more - it's essential to maintaining a respectful and inclusive environment.
We want everyone in our community to have a positive experience. To facilitate this, we've established a code of conduct and a statement of values that we expect all contributors to adhere to. Please take a moment to review these documents—they're essential to maintaining a respectful and inclusive environment.
By making a contribution to this project, you certify that: By making a contribution to this project, you certify that:
@ -49,6 +45,12 @@ This disclaimer is not a license and does not grant any rights or permissions. Y
This disclaimer is provided "as is" without warranty of any kind, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the contribution or the use or other dealings in the contribution. This disclaimer is provided "as is" without warranty of any kind, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the contribution or the use or other dealings in the contribution.
### Support
For support, please use this repository's [GitHub Issues](https://github.com/invoke-ai/InvokeAI/issues), or join the [Discord](https://discord.gg/ZmtBAhwWhy).
Original portions of the software are Copyright (c) 2023 by respective contributors.
--- ---
Remember, your contributions help make this project great. We're excited to see what you'll bring to our community! Remember, your contributions help make this project great. We're excited to see what you'll bring to our community!

View File

@ -0,0 +1,91 @@
# Development
## **What do I need to know to help?**
If you are looking to help to with a code contribution, InvokeAI uses several different technologies under the hood: Python (Pydantic, FastAPI, diffusers) and Typescript (React, Redux Toolkit, ChakraUI, Mantine, Konva). Familiarity with StableDiffusion and image generation concepts is helpful, but not essential.
For more information, please review our area specific documentation:
* #### [InvokeAI Architecure](../ARCHITECTURE.md)
* #### [Frontend Documentation](development_guides/contributingToFrontend.md)
* #### [Node Documentation](../INVOCATIONS.md)
* #### [Local Development](../LOCAL_DEVELOPMENT.md)
If you don't feel ready to make a code contribution yet, no problem! You can also help out in other ways, such as [documentation](documentation.md) or [translation](translation.md).
There are two paths to making a development contribution:
1. Choosing an open issue to address. Open issues can be found in the [Issues](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen) section of the InvokeAI repository. These are tagged by the issue type (bug, enhancement, etc.) along with the “good first issues” tag denoting if they are suitable for first time contributors.
1. Additional items can be found on our roadmap <******************************link to roadmap>******************************. The roadmap is organized in terms of priority, and contains features of varying size and complexity. If there is an inflight item youd like to help with, reach out to the contributor assigned to the item to see how you can help.
2. Opening a new issue or feature to add. **Please make sure you have searched through existing issues before creating new ones.**
*Regardless of what you choose, please post in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord before you start development in order to confirm that the issue or feature is aligned with the current direction of the project. We value our contributors time and effort and want to ensure that no ones time is being misspent.*
## Best Practices:
* Keep your pull requests small. Smaller pull requests are more likely to be accepted and merged
* Comments! Commenting your code helps reviwers easily understand your contribution
* Use Python and Typescripts typing systems, and consider using an editor with [LSP](https://microsoft.github.io/language-server-protocol/) support to streamline development
* Make all communications public. This ensure knowledge is shared with the whole community
## **How do I make a contribution?**
Never made an open source contribution before? Wondering how contributions work in our project? Here's a quick rundown!
Before starting these steps, ensure you have your local environment [configured for development](../LOCAL_DEVELOPMENT.md).
1. Find a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) that you are interested in addressing or a feature that you would like to add. Then, reach out to our team in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord to ensure you are setup for success.
2. Fork the [InvokeAI](https://github.com/invoke-ai/InvokeAI) repository to your GitHub profile. This means that you will have a copy of the repository under **your-GitHub-username/InvokeAI**.
3. Clone the repository to your local machine using:
```bash
git clone https://github.com/your-GitHub-username/InvokeAI.git
```
If you're unfamiliar with using Git through the commandline, [GitHub Desktop](https://desktop.github.com) is a easy-to-use alternative with a UI. You can do all the same steps listed here, but through the interface.
4. Create a new branch for your fix using:
```bash
git checkout -b branch-name-here
```
5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add.
6. Add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index:
```bash
git add insert-paths-of-changed-files-here
```
7. Store the contents of the index with a descriptive message.
```bash
git commit -m "Insert a short message of the changes made here"
```
8. Push the changes to the remote repository using
```markdown
git push origin branch-name-here
```
9. Submit a pull request to the **main** branch of the InvokeAI repository.
10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #1234".
11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it!
12. Wait for the pull request to be reviewed by other collaborators.
13. Make changes to the pull request if the reviewer(s) recommend them.
14. Celebrate your success after your pull request is merged!
If youd like to learn more about contributing to Open Source projects, here is a [Getting Started Guide](https://opensource.com/article/19/7/create-pull-request-github).
## **Where can I go for help?**
If you need help, you can ask questions in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord.
For frontend related work, **@pyschedelicious** is the best person to reach out to.
For backend related work, please reach out to **@blessedcoolant**, **@lstein**, **@StAlKeR7779** or **@pyschedelicious**.
## **What does the Code of Conduct mean for me?**
Our [Code of Conduct](CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code.

View File

@ -0,0 +1,75 @@
# Contributing to the Frontend
# InvokeAI Web UI
- [InvokeAI Web UI](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#invokeai-web-ui)
- [Stack](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#stack)
- [Contributing](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#contributing)
- [Dev Environment](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#dev-environment)
- [Production builds](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#production-builds)
The UI is a fairly straightforward Typescript React app, with the Unified Canvas being more complex.
Code is located in `invokeai/frontend/web/` for review.
## Stack
State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). We lean heavily on RTK:
- `createAsyncThunk` for HTTP requests
- `createEntityAdapter` for fetching images and models
- `createListenerMiddleware` for workflows
The API client and associated types are generated from the OpenAPI schema. See API_CLIENT.md.
Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a simple socket.io redux middleware to help).
[Chakra-UI](https://github.com/chakra-ui/chakra-ui) & [Mantine](https://github.com/mantinedev/mantine) for components and styling.
[Konva](https://github.com/konvajs/react-konva) for the canvas, but we are pushing the limits of what is feasible with it (and HTML canvas in general). We plan to rebuild it with [PixiJS](https://github.com/pixijs/pixijs) to take advantage of WebGL's improved raster handling.
[Vite](https://vitejs.dev/) for bundling.
Localisation is via [i18next](https://github.com/i18next/react-i18next), but translation happens on our [Weblate](https://hosted.weblate.org/engage/invokeai/) project. Only the English source strings should be changed on this repo.
## Contributing
Thanks for your interest in contributing to the InvokeAI Web UI!
We encourage you to ping @psychedelicious and @blessedcoolant on [Discord](https://discord.gg/ZmtBAhwWhy) if you want to contribute, just to touch base and ensure your work doesn't conflict with anything else going on. The project is very active.
### Dev Environment
**Setup**
1. Install [node](https://nodejs.org/en/download/). You can confirm node is installed with:
```bash
node --version
```
2. Install [yarn classic](https://classic.yarnpkg.com/lang/en/) and confirm it is installed by running this:
```bash
npm install --global yarn
yarn --version
```
From `invokeai/frontend/web/` run `yarn install` to get everything set up.
Start everything in dev mode:
1. Ensure your virtual environment is running
2. Start the dev server: `yarn dev`
3. Start the InvokeAI Nodes backend: `python scripts/invokeai-web.py # run from the repo root`
4. Point your browser to the dev server address e.g. [http://localhost:5173/](http://localhost:5173/)
### VSCode Remote Dev
We've noticed an intermittent issue with the VSCode Remote Dev port forwarding. If you use this feature of VSCode, you may intermittently click the Invoke button and then get nothing until the request times out. Suggest disabling the IDE's port forwarding feature and doing it manually via SSH:
`ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host`
### Production builds
For a number of technical and logistical reasons, we need to commit UI build artefacts to the repo.
If you submit a PR, there is a good chance we will ask you to include a separate commit with a build of the app.
To build for production, run `yarn build`.

View File

@ -0,0 +1,13 @@
# Documentation
Documentation is an important part of any open source project. It provides a clear and concise way to communicate how the software works, how to use it, and how to troubleshoot issues. Without proper documentation, it can be difficult for users to understand the purpose and functionality of the project.
## Contributing
All documentation is maintained in the InvokeAI GitHub repository. If you come across documentation that is out of date or incorrect, please submit a pull request with the necessary changes.
When updating or creating documentation, please keep in mind InvokeAI is a tool for everyone, not just those who have familiarity with generative art.
## Help & Questions
Please ping @imic1 or @hipsterusername in the [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.

View File

@ -0,0 +1,19 @@
# Translation
InvokeAI uses [Weblate](https://weblate.org/) for translation. Weblate is a FOSS project providing a scalable translation service. Weblate automates the tedious parts of managing translation of a growing project, and the service is generously provided at no cost to FOSS projects like InvokeAI.
## Contributing
If you'd like to contribute by adding or updating a translation, please visit our [Weblate project](https://hosted.weblate.org/engage/invokeai/). You'll need to sign in with your GitHub account (a number of other accounts are supported, including Google).
Once signed in, select a language and then the Web UI component. From here you can Browse and Translate strings from English to your chosen language. Zen mode offers a simpler translation experience.
Your changes will be attributed to you in the automated PR process; you don't need to do anything else.
## Help & Questions
Please check Weblate's [documentation](https://docs.weblate.org/en/latest/index.html) or ping @Harvestor on [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.
## Thanks
Thanks to the InvokeAI community for their efforts to translate the project!

View File

@ -0,0 +1,11 @@
# Tutorials
Tutorials help new & existing users expand their abilty to use InvokeAI to the full extent of our features and services.
Currently, we have a set of tutorials available on our [YouTube channel](https://www.youtube.com/@invokeai), but as InvokeAI continues to evolve with new updates, we want to ensure that we are giving our users the resources they need to succeed.
Tutorials can be in the form of videos or article walkthroughs on a subject of your choice. We recommend focusing tutorials on the key image generation methods, or on a specific component within one of the image generation methods.
## Contributing
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.

View File

@ -222,14 +222,10 @@ get solutions for common installation problems and other issues.
Anyone who wishes to contribute to this project, whether documentation, Anyone who wishes to contribute to this project, whether documentation,
features, bug fixes, code cleanup, testing, or code reviews, is very much features, bug fixes, code cleanup, testing, or code reviews, is very much
encouraged to do so. If you are unfamiliar with how to contribute to GitHub encouraged to do so.
projects, here is a
[Getting Started Guide](https://opensource.com/article/19/7/create-pull-request-github).
A full set of contribution guidelines, along with templates, are in progress, [Please take a look at our Contribution documentation to learn more about contributing to InvokeAI.
but for now the most important thing is to **make your pull request against the ](contributing/CONTRIBUTING.md)
"development" branch**, and not against "main". This will help keep public
breakage to a minimum and will allow you to propose more radical changes.
## :octicons-person-24: Contributors ## :octicons-person-24: Contributors

View File

@ -0,0 +1,28 @@
# Community Nodes
These are nodes that have been developed by the community for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md).
If you'd like to submit a node for the community, please refer to the [node creation overview](overview.md).
To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node.
To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor.
## List of Nodes
--------------------------------
### Super Cool Node Template
**Description:** This node allows you to do super cool things with InvokeAI.
**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
**Output Examples**
![Invoke AI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png)
## Help
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).

41
docs/nodes/overview.md Normal file
View File

@ -0,0 +1,41 @@
# Nodes
## What are Nodes?
An Node is simply a single operation that takes in some inputs and gives
out some outputs. We can then chain multiple nodes together to create more
complex functionality. All InvokeAI features are added through nodes.
This means nodes can be used to easily extend the image generation capabilities of InvokeAI, and allow you build workflows to suit your needs.
You can read more about nodes and the node editor [here](../features/NODES.md).
## Downloading Nodes
To download a new node, visit our list of [Community Nodes](communityNodes.md). These are codes that have been created by the community, for the community.
## Contributing Nodes
To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md).
Once youve created a node and confirmed that it behaves as expected locally, follow these steps:
- Make sure the node is contained in a new Python (.py) file
- Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list
- Make sure you are following the template below and have provided all relevant details about the node and what it does.
- A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project.
### Community Node Template
```markdown
--------------------------------
### Super Cool Node Template
**Description:** This node allows you to do super cool things with InvokeAI.
**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
**Output Examples**
![InvokeAI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png)
```

View File

@ -1,8 +1,7 @@
import io import io
from typing import Optional from typing import Optional
from fastapi import (Body, HTTPException, Path, Query, Request, Response, from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile
UploadFile)
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from PIL import Image from PIL import Image
@ -11,9 +10,11 @@ from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.item_storage import PaginatedResults from invokeai.app.services.item_storage import PaginatedResults
from invokeai.app.services.models.image_record import (ImageDTO, from invokeai.app.services.models.image_record import (
ImageRecordChanges, ImageDTO,
ImageUrlsDTO) ImageRecordChanges,
ImageUrlsDTO,
)
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
@ -84,15 +85,16 @@ async def delete_image(
# TODO: Does this need any exception handling at all? # TODO: Does this need any exception handling at all?
pass pass
@images_router.post("/clear-intermediates", operation_id="clear_intermediates") @images_router.post("/clear-intermediates", operation_id="clear_intermediates")
async def clear_intermediates() -> int: async def clear_intermediates() -> int:
"""Clears first 100 intermediates""" """Clears all intermediates"""
try: try:
count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True) count_deleted = ApiDependencies.invoker.services.images.delete_intermediates()
return count_deleted return count_deleted
except Exception as e: except Exception as e:
# TODO: Does this need any exception handling at all? raise HTTPException(status_code=500, detail="Failed to clear intermediates")
pass pass
@ -130,6 +132,7 @@ async def get_image_dto(
except Exception as e: except Exception as e:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@images_router.get( @images_router.get(
"/{image_name}/metadata", "/{image_name}/metadata",
operation_id="get_image_metadata", operation_id="get_image_metadata",
@ -254,7 +257,8 @@ async def list_image_dtos(
default=None, description="Whether to list intermediate images." default=None, description="Whether to list intermediate images."
), ),
board_id: Optional[str] = Query( board_id: Optional[str] = Query(
default=None, description="The board id to filter by. Use 'none' to find images without a board." default=None,
description="The board id to filter by. Use 'none' to find images without a board.",
), ),
offset: int = Query(default=0, description="The page offset"), offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"), limit: int = Query(default=10, description="The number of images per page"),

View File

@ -277,7 +277,7 @@ class InvokeAISettings(BaseSettings):
@classmethod @classmethod
def _excluded_from_yaml(self)->List[str]: def _excluded_from_yaml(self)->List[str]:
# combination of deprecated parameters and internal ones that shouldn't be exposed as invokeai.yaml options # combination of deprecated parameters and internal ones that shouldn't be exposed as invokeai.yaml options
return ['type','initconf', 'gpu_mem_reserved', 'max_loaded_models', 'version', 'from_file', 'model', 'restore'] return ['type','initconf', 'gpu_mem_reserved', 'max_loaded_models', 'version', 'from_file', 'model', 'restore', 'root']
class Config: class Config:
env_file_encoding = 'utf-8' env_file_encoding = 'utf-8'
@ -446,7 +446,7 @@ setting environment variables INVOKEAI_<setting>.
Path to the runtime root directory Path to the runtime root directory
''' '''
if self.root: if self.root:
return Path(self.root).expanduser() return Path(self.root).expanduser().absolute()
else: else:
return self.find_root() return self.find_root()

View File

@ -122,6 +122,11 @@ class ImageRecordStorageBase(ABC):
"""Deletes many image records.""" """Deletes many image records."""
pass pass
@abstractmethod
def delete_intermediates(self) -> list[str]:
"""Deletes all intermediate image records, returning a list of deleted image names."""
pass
@abstractmethod @abstractmethod
def save( def save(
self, self,
@ -461,6 +466,32 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
finally: finally:
self._lock.release() self._lock.release()
def delete_intermediates(self) -> list[str]:
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
SELECT image_name FROM images
WHERE is_intermediate = TRUE;
"""
)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
image_names = list(map(lambda r: r[0], result))
self._cursor.execute(
"""--sql
DELETE FROM images
WHERE is_intermediate = TRUE;
"""
)
self._conn.commit()
return image_names
except sqlite3.Error as e:
self._conn.rollback()
raise ImageRecordDeleteException from e
finally:
self._lock.release()
def save( def save(
self, self,
image_name: str, image_name: str,

View File

@ -6,21 +6,33 @@ from typing import TYPE_CHECKING, Optional
from PIL.Image import Image as PILImageType from PIL.Image import Image as PILImageType
from invokeai.app.invocations.metadata import ImageMetadata from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.models.image import (ImageCategory, from invokeai.app.models.image import (
InvalidImageCategoryException, ImageCategory,
InvalidOriginException, ResourceOrigin) InvalidImageCategoryException,
from invokeai.app.services.board_image_record_storage import \ InvalidOriginException,
BoardImageRecordStorageBase ResourceOrigin,
)
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
from invokeai.app.services.image_file_storage import ( from invokeai.app.services.image_file_storage import (
ImageFileDeleteException, ImageFileNotFoundException, ImageFileDeleteException,
ImageFileSaveException, ImageFileStorageBase) ImageFileNotFoundException,
ImageFileSaveException,
ImageFileStorageBase,
)
from invokeai.app.services.image_record_storage import ( from invokeai.app.services.image_record_storage import (
ImageRecordDeleteException, ImageRecordNotFoundException, ImageRecordDeleteException,
ImageRecordSaveException, ImageRecordStorageBase, OffsetPaginatedResults) ImageRecordNotFoundException,
ImageRecordSaveException,
ImageRecordStorageBase,
OffsetPaginatedResults,
)
from invokeai.app.services.item_storage import ItemStorageABC from invokeai.app.services.item_storage import ItemStorageABC
from invokeai.app.services.models.image_record import (ImageDTO, ImageRecord, from invokeai.app.services.models.image_record import (
ImageRecordChanges, ImageDTO,
image_record_to_dto) ImageRecord,
ImageRecordChanges,
image_record_to_dto,
)
from invokeai.app.services.resource_name import NameServiceBase from invokeai.app.services.resource_name import NameServiceBase
from invokeai.app.services.urls import UrlServiceBase from invokeai.app.services.urls import UrlServiceBase
from invokeai.app.util.metadata import get_metadata_graph_from_raw_session from invokeai.app.util.metadata import get_metadata_graph_from_raw_session
@ -109,12 +121,10 @@ class ImageServiceABC(ABC):
pass pass
@abstractmethod @abstractmethod
def delete_many(self, is_intermediate: bool) -> int: def delete_intermediates(self) -> int:
"""Deletes many images.""" """Deletes all intermediate images."""
pass pass
@abstractmethod @abstractmethod
def delete_images_on_board(self, board_id: str): def delete_images_on_board(self, board_id: str):
"""Deletes all images on a board.""" """Deletes all images on a board."""
@ -401,21 +411,13 @@ class ImageService(ImageServiceABC):
except Exception as e: except Exception as e:
self._services.logger.error("Problem deleting image records and files") self._services.logger.error("Problem deleting image records and files")
raise e raise e
def delete_many(self, is_intermediate: bool): def delete_intermediates(self) -> int:
try: try:
# only clears 100 at a time image_names = self._services.image_records.delete_intermediates()
images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,) count = len(image_names)
count = len(images.items) for image_name in image_names:
image_name_list = list(
map(
lambda r: r.image_name,
images.items,
)
)
for image_name in image_name_list:
self._services.image_files.delete(image_name) self._services.image_files.delete(image_name)
self._services.image_records.delete_many(image_name_list)
return count return count
except ImageRecordDeleteException: except ImageRecordDeleteException:
self._services.logger.error(f"Failed to delete image records") self._services.logger.error(f"Failed to delete image records")

View File

@ -552,7 +552,8 @@
"saveSteps": "Save images every n steps", "saveSteps": "Save images every n steps",
"confirmOnDelete": "Confirm On Delete", "confirmOnDelete": "Confirm On Delete",
"displayHelpIcons": "Display Help Icons", "displayHelpIcons": "Display Help Icons",
"useCanvasBeta": "Use Canvas Beta Layout", "alternateCanvasLayout": "Alternate Canvas Layout",
"enableNodesEditor": "Enable Nodes Editor",
"enableImageDebugging": "Enable Image Debugging", "enableImageDebugging": "Enable Image Debugging",
"useSlidersForAll": "Use Sliders For All Options", "useSlidersForAll": "Use Sliders For All Options",
"showProgressInViewer": "Show Progress Images in Viewer", "showProgressInViewer": "Show Progress Images in Viewer",
@ -569,7 +570,9 @@
"ui": "User Interface", "ui": "User Interface",
"favoriteSchedulers": "Favorite Schedulers", "favoriteSchedulers": "Favorite Schedulers",
"favoriteSchedulersPlaceholder": "No schedulers favorited", "favoriteSchedulersPlaceholder": "No schedulers favorited",
"showAdvancedOptions": "Show Advanced Options" "showAdvancedOptions": "Show Advanced Options",
"experimental": "Experimental",
"beta": "Beta"
}, },
"toast": { "toast": {
"serverError": "Server Error", "serverError": "Server Error",

View File

@ -6,11 +6,7 @@ import {
imageSelected, imageSelected,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { progressImageSet } from 'features/system/store/systemSlice'; import { progressImageSet } from 'features/system/store/systemSlice';
import { import { imagesAdapter, imagesApi } from 'services/api/endpoints/images';
SYSTEM_BOARDS,
imagesAdapter,
imagesApi,
} from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards'; import { isImageOutput } from 'services/api/guards';
import { sessionCanceled } from 'services/api/thunks/session'; import { sessionCanceled } from 'services/api/thunks/session';
import { import {
@ -32,8 +28,7 @@ export const addInvocationCompleteEventListener = () => {
); );
const session_id = action.payload.data.graph_execution_state_id; const session_id = action.payload.data.graph_execution_state_id;
const { cancelType, isCancelScheduled, boardIdToAddTo } = const { cancelType, isCancelScheduled } = getState().system;
getState().system;
// Handle scheduled cancelation // Handle scheduled cancelation
if (cancelType === 'scheduled' && isCancelScheduled) { if (cancelType === 'scheduled' && isCancelScheduled) {
@ -88,26 +83,28 @@ export const addInvocationCompleteEventListener = () => {
) )
); );
// add image to the board if we had one selected const { autoAddBoardId } = gallery;
if (boardIdToAddTo && !SYSTEM_BOARDS.includes(boardIdToAddTo)) {
// add image to the board if auto-add is enabled
if (autoAddBoardId) {
dispatch( dispatch(
imagesApi.endpoints.addImageToBoard.initiate({ imagesApi.endpoints.addImageToBoard.initiate({
board_id: boardIdToAddTo, board_id: autoAddBoardId,
imageDTO, imageDTO,
}) })
); );
} }
const { selectedBoardId } = gallery; const { selectedBoardId, shouldAutoSwitch } = gallery;
if (boardIdToAddTo && boardIdToAddTo !== selectedBoardId) {
dispatch(boardIdSelected(boardIdToAddTo));
} else if (!boardIdToAddTo) {
dispatch(boardIdSelected('all'));
}
// If auto-switch is enabled, select the new image // If auto-switch is enabled, select the new image
if (getState().gallery.shouldAutoSwitch) { if (shouldAutoSwitch) {
// if auto-add is enabled, switch the board as the image comes in
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
dispatch(boardIdSelected(autoAddBoardId));
} else if (!autoAddBoardId) {
dispatch(boardIdSelected('images'));
}
dispatch(imageSelected(imageDTO.image_name)); dispatch(imageSelected(imageDTO.image_name));
} }
} }

View File

@ -9,7 +9,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo } from 'react'; import { memo } from 'react';
interface Props extends SwitchProps { export interface IAISwitchProps extends SwitchProps {
label?: string; label?: string;
width?: string | number; width?: string | number;
formControlProps?: FormControlProps; formControlProps?: FormControlProps;
@ -20,7 +20,7 @@ interface Props extends SwitchProps {
/** /**
* Customized Chakra FormControl + Switch multi-part component. * Customized Chakra FormControl + Switch multi-part component.
*/ */
const IAISwitch = (props: Props) => { const IAISwitch = (props: IAISwitchProps) => {
const { const {
label, label,
isDisabled = false, isDisabled = false,

View File

@ -0,0 +1,80 @@
import { SelectItem } from '@mantine/core';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useRef } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
const selector = createSelector(
[stateSelector],
({ gallery }) => {
const { autoAddBoardId } = gallery;
return {
autoAddBoardId,
};
},
defaultSelectorOptions
);
const BoardAutoAddSelect = () => {
const dispatch = useAppDispatch();
const { autoAddBoardId } = useAppSelector(selector);
const inputRef = useRef<HTMLInputElement>(null);
const { boards, hasBoards } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
const boards: SelectItem[] = [
{
label: 'None',
value: 'none',
},
];
data?.forEach(({ board_id, board_name }) => {
boards.push({
label: board_name,
value: board_id,
});
});
return {
boards,
hasBoards: boards.length > 1,
};
},
});
const handleChange = useCallback(
(v: string | null) => {
if (!v) {
return;
}
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
},
[dispatch]
);
return (
<IAIMantineSearchableSelect
label="Auto-Add Board"
inputRef={inputRef}
autoFocus
placeholder={'Select a Board'}
value={autoAddBoardId}
data={boards}
nothingFound="No matching Boards"
itemComponent={IAIMantineSelectItemWithTooltip}
disabled={!hasBoards}
filter={(value, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
onChange={handleChange}
/>
);
};
export default BoardAutoAddSelect;

View File

@ -0,0 +1,60 @@
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { FaFolder } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
type Props = {
board?: BoardDTO;
board_id: string;
children: ContextMenuProps<HTMLDivElement>['children'];
setBoardToDelete?: (board?: BoardDTO) => void;
};
const BoardContextMenu = memo(
({ board, board_id, setBoardToDelete, children }: Props) => {
const dispatch = useAppDispatch();
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board?.board_id ?? board_id));
}, [board?.board_id, board_id, dispatch]);
return (
<Box sx={{ touchAction: 'none', height: 'full' }}>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
>
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
Select Board
</MenuItem>
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuList>
)}
>
{children}
</ContextMenu>
</Box>
);
}
);
BoardContextMenu.displayName = 'HoverableBoard';
export default BoardContextMenu;

View File

@ -1,5 +1,6 @@
import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useCreateBoardMutation } from 'services/api/endpoints/boards'; import { useCreateBoardMutation } from 'services/api/endpoints/boards';
const DEFAULT_BOARD_NAME = 'My Board'; const DEFAULT_BOARD_NAME = 'My Board';
@ -12,15 +13,14 @@ const AddBoardButton = () => {
}, [createBoard]); }, [createBoard]);
return ( return (
<IAIButton <IAIIconButton
icon={<FaPlus />}
isLoading={isLoading} isLoading={isLoading}
tooltip="Add Board"
aria-label="Add Board" aria-label="Add Board"
onClick={handleCreateBoard} onClick={handleCreateBoard}
size="sm" size="sm"
sx={{ px: 4 }} />
>
Add Board
</IAIButton>
); );
}; };

View File

@ -38,6 +38,7 @@ const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="assets"
onClick={handleClick} onClick={handleClick}
isSelected={isSelected} isSelected={isSelected}
icon={FaFileImage} icon={FaFileImage}

View File

@ -38,6 +38,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="images"
onClick={handleClick} onClick={handleClick}
isSelected={isSelected} isSelected={isSelected}
icon={FaImages} icon={FaImages}

View File

@ -29,6 +29,7 @@ const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="batch"
droppableData={droppableData} droppableData={droppableData}
onClick={handleBatchBoardClick} onClick={handleBatchBoardClick}
isSelected={isSelected} isSelected={isSelected}

View File

@ -1,31 +1,41 @@
import { import {
Badge, Badge,
Box, Box,
ChakraProps,
Editable, Editable,
EditableInput, EditableInput,
EditablePreview, EditablePreview,
Flex, Flex,
Image, Image,
MenuItem,
MenuList,
Text, Text,
useColorMode, useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/dist/query'; import { skipToken } from '@reduxjs/toolkit/dist/query';
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd'; import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
import { useAppDispatch } from 'app/store/storeHooks'; import { stateSelector } from 'app/store/store';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { FaTrash, FaUser } from 'react-icons/fa'; import { FaUser } from 'react-icons/fa';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards'; import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { BoardDTO } from 'services/api/types'; import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
import BoardContextMenu from '../BoardContextMenu';
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'accent.200',
color: 'blackAlpha.900',
};
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'base.500',
color: 'whiteAlpha.900',
};
interface GalleryBoardProps { interface GalleryBoardProps {
board: BoardDTO; board: BoardDTO;
isSelected: boolean; isSelected: boolean;
@ -35,6 +45,22 @@ interface GalleryBoardProps {
const GalleryBoard = memo( const GalleryBoard = memo(
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => { ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
return { isSelectedForAutoAdd };
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd } = useAppSelector(selector);
const { currentData: coverImage } = useGetImageDTOQuery( const { currentData: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken board.cover_image_name ?? skipToken
@ -53,10 +79,6 @@ const GalleryBoard = memo(
updateBoard({ board_id, changes: { board_name: newBoardName } }); updateBoard({ board_id, changes: { board_name: newBoardName } });
}; };
const handleDeleteBoard = useCallback(() => {
setBoardToDelete(board);
}, [board, setBoardToDelete]);
const droppableData: MoveBoardDropData = useMemo( const droppableData: MoveBoardDropData = useMemo(
() => ({ () => ({
id: board_id, id: board_id,
@ -68,37 +90,10 @@ const GalleryBoard = memo(
return ( return (
<Box sx={{ touchAction: 'none', height: 'full' }}> <Box sx={{ touchAction: 'none', height: 'full' }}>
<ContextMenu<HTMLDivElement> <BoardContextMenu
menuProps={{ size: 'sm', isLazy: true }} board={board}
menuButtonProps={{ board_id={board_id}
bg: 'transparent', setBoardToDelete={setBoardToDelete}
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
>
{board.image_count > 0 && (
<>
{/* <MenuItem
isDisabled={!board.image_count}
icon={<FaImages />}
onClickCapture={handleAddBoardToBatch}
>
Add Board to Batch
</MenuItem> */}
</>
)}
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDeleteBoard}
>
Delete Board
</MenuItem>
</MenuList>
)}
> >
{(ref) => ( {(ref) => (
<Flex <Flex
@ -154,7 +149,16 @@ const GalleryBoard = memo(
p: 1, p: 1,
}} }}
> >
<Badge variant="solid">{board.image_count}</Badge> <Badge
variant="solid"
sx={
isSelectedForAutoAdd
? AUTO_ADD_BADGE_STYLES
: BASE_BADGE_STYLES
}
>
{board.image_count}
</Badge>
</Flex> </Flex>
<IAIDroppable <IAIDroppable
data={droppableData} data={droppableData}
@ -172,7 +176,7 @@ const GalleryBoard = memo(
> >
<Editable <Editable
defaultValue={board_name} defaultValue={board_name}
submitOnBlur={false} submitOnBlur={true}
onSubmit={(nextValue) => { onSubmit={(nextValue) => {
handleUpdateBoardName(nextValue); handleUpdateBoardName(nextValue);
}} }}
@ -205,7 +209,7 @@ const GalleryBoard = memo(
</Flex> </Flex>
</Flex> </Flex>
)} )}
</ContextMenu> </BoardContextMenu>
</Box> </Box>
); );
} }

View File

@ -2,9 +2,12 @@ import { As, Badge, Flex } from '@chakra-ui/react';
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd'; import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { BoardId } from 'features/gallery/store/gallerySlice';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import BoardContextMenu from '../BoardContextMenu';
type GenericBoardProps = { type GenericBoardProps = {
board_id: BoardId;
droppableData?: TypesafeDroppableData; droppableData?: TypesafeDroppableData;
onClick: () => void; onClick: () => void;
isSelected: boolean; isSelected: boolean;
@ -22,6 +25,7 @@ const formatBadgeCount = (count: number) =>
const GenericBoard = (props: GenericBoardProps) => { const GenericBoard = (props: GenericBoardProps) => {
const { const {
board_id,
droppableData, droppableData,
onClick, onClick,
isSelected, isSelected,
@ -32,67 +36,72 @@ const GenericBoard = (props: GenericBoardProps) => {
} = props; } = props;
return ( return (
<Flex <BoardContextMenu board_id={board_id}>
sx={{ {(ref) => (
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}}
>
<Flex
onClick={onClick}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
shadow: isSelected ? 'selected.light' : undefined,
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
flexShrink: 0,
}}
>
<IAINoContentFallback
boxSize={8}
icon={icon}
sx={{
border: '2px solid var(--invokeai-colors-base-200)',
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
}}
/>
<Flex <Flex
ref={ref}
sx={{ sx={{
position: 'absolute', flexDir: 'column',
insetInlineEnd: 0, justifyContent: 'space-between',
top: 0, alignItems: 'center',
p: 1, cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}} }}
> >
{badgeCount !== undefined && ( <Flex
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge> onClick={onClick}
)} sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
shadow: isSelected ? 'selected.light' : undefined,
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
flexShrink: 0,
}}
>
<IAINoContentFallback
boxSize={8}
icon={icon}
sx={{
border: '2px solid var(--invokeai-colors-base-200)',
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
}}
/>
<Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
{badgeCount !== undefined && (
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
)}
</Flex>
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
</Flex>
<Flex
sx={{
h: 'full',
alignItems: 'center',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
color: isSelected ? 'base.900' : 'base.700',
_dark: { color: isSelected ? 'base.50' : 'base.200' },
}}
>
{label}
</Flex>
</Flex> </Flex>
<IAIDroppable data={droppableData} dropLabel={dropLabel} /> )}
</Flex> </BoardContextMenu>
<Flex
sx={{
h: 'full',
alignItems: 'center',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
color: isSelected ? 'base.900' : 'base.700',
_dark: { color: isSelected ? 'base.50' : 'base.200' },
}}
>
{label}
</Flex>
</Flex>
); );
}; };

View File

@ -39,6 +39,7 @@ const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="no_board"
droppableData={droppableData} droppableData={droppableData}
dropLabel={<Text fontSize="md">Move</Text>} dropLabel={<Text fontSize="md">Move</Text>}
onClick={handleClick} onClick={handleClick}

View File

@ -0,0 +1,79 @@
import { MenuItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types';
type Props = {
board: BoardDTO;
setBoardToDelete?: (board?: BoardDTO) => void;
};
const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
return { isSelectedForAutoAdd };
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd } = useAppSelector(selector);
const handleDelete = useCallback(() => {
if (!setBoardToDelete) {
return;
}
setBoardToDelete(board);
}, [board, setBoardToDelete]);
const handleToggleAutoAdd = useCallback(() => {
dispatch(
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
);
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
return (
<>
{board.image_count > 0 && (
<>
{/* <MenuItem
isDisabled={!board.image_count}
icon={<FaImages />}
onClickCapture={handleAddBoardToBatch}
>
Add Board to Batch
</MenuItem> */}
</>
)}
<MenuItem
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
onClickCapture={handleToggleAutoAdd}
>
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
</MenuItem>
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDelete}
>
Delete Board
</MenuItem>
</>
);
};
export default memo(GalleryBoardContextMenuItems);

View File

@ -0,0 +1,12 @@
import { BoardId } from 'features/gallery/store/gallerySlice';
import { memo } from 'react';
type Props = {
board_id: BoardId;
};
const SystemBoardContextMenuItems = ({ board_id }: Props) => {
return <></>;
};
export default memo(SystemBoardContextMenuItems);

View File

@ -1,20 +1,18 @@
import { ChevronUpIcon } from '@chakra-ui/icons'; import { ChevronUpIcon } from '@chakra-ui/icons';
import { Button, Flex, Text } from '@chakra-ui/react'; import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { memo } from 'react'; import { memo } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useBoardName } from 'services/api/hooks/useBoardName';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
(state) => { (state) => {
const { selectedBoardId } = state.gallery; const { selectedBoardId } = state.gallery;
return { return { selectedBoardId };
selectedBoardId,
};
}, },
defaultSelectorOptions defaultSelectorOptions
); );
@ -27,25 +25,7 @@ type Props = {
const GalleryBoardName = (props: Props) => { const GalleryBoardName = (props: Props) => {
const { isOpen, onToggle } = props; const { isOpen, onToggle } = props;
const { selectedBoardId } = useAppSelector(selector); const { selectedBoardId } = useAppSelector(selector);
const { selectedBoardName } = useListAllBoardsQuery(undefined, { const boardName = useBoardName(selectedBoardId);
selectFromResult: ({ data }) => {
let selectedBoardName = '';
if (selectedBoardId === 'images') {
selectedBoardName = 'All Images';
} else if (selectedBoardId === 'assets') {
selectedBoardName = 'All Assets';
} else if (selectedBoardId === 'no_board') {
selectedBoardName = 'No Board';
} else if (selectedBoardId === 'batch') {
selectedBoardName = 'Batch';
} else {
const selectedBoard = data?.find((b) => b.board_id === selectedBoardId);
selectedBoardName = selectedBoard?.board_name || 'Unknown Board';
}
return { selectedBoardName };
},
});
return ( return (
<Flex <Flex
@ -54,6 +34,8 @@ const GalleryBoardName = (props: Props) => {
size="sm" size="sm"
variant="ghost" variant="ghost"
sx={{ sx={{
position: 'relative',
gap: 2,
w: 'full', w: 'full',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@ -64,19 +46,22 @@ const GalleryBoardName = (props: Props) => {
}, },
}} }}
> >
<Text <Spacer />
noOfLines={1} <Box position="relative">
sx={{ <Text
w: 'full', noOfLines={1}
fontWeight: 600, sx={{
color: 'base.800', fontWeight: 600,
_dark: { color: 'base.800',
color: 'base.200', _dark: {
}, color: 'base.200',
}} },
> }}
{selectedBoardName} >
</Text> {boardName}
</Text>
</Box>
<Spacer />
<ChevronUpIcon <ChevronUpIcon
sx={{ sx={{
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)', transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',

View File

@ -1,19 +1,20 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover'; import IAIPopover from 'common/components/IAIPopover';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice'; import {
setGalleryImageMinimumWidth,
shouldAutoSwitchChanged,
} from 'features/gallery/store/gallerySlice';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa'; import { FaWrench } from 'react-icons/fa';
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -50,7 +51,7 @@ const GallerySettingsPopover = () => {
/> />
} }
> >
<Flex direction="column" gap={2}> <Flex direction="column" gap={4}>
<IAISlider <IAISlider
value={galleryImageMinimumWidth} value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth} onChange={handleChangeGalleryImageMinimumWidth}
@ -68,6 +69,7 @@ const GallerySettingsPopover = () => {
dispatch(shouldAutoSwitchChanged(e.target.checked)) dispatch(shouldAutoSwitchChanged(e.target.checked))
} }
/> />
<BoardAutoAddSelect />
</Flex> </Flex>
</IAIPopover> </IAIPopover>
); );

View File

@ -25,6 +25,7 @@ export type BoardId =
type GalleryState = { type GalleryState = {
selection: string[]; selection: string[];
shouldAutoSwitch: boolean; shouldAutoSwitch: boolean;
autoAddBoardId: string | null;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
selectedBoardId: BoardId; selectedBoardId: BoardId;
batchImageNames: string[]; batchImageNames: string[];
@ -34,6 +35,7 @@ type GalleryState = {
export const initialGalleryState: GalleryState = { export const initialGalleryState: GalleryState = {
selection: [], selection: [],
shouldAutoSwitch: true, shouldAutoSwitch: true,
autoAddBoardId: null,
galleryImageMinimumWidth: 96, galleryImageMinimumWidth: 96,
selectedBoardId: 'images', selectedBoardId: 'images',
batchImageNames: [], batchImageNames: [],
@ -123,14 +125,34 @@ export const gallerySlice = createSlice({
state.batchImageNames = []; state.batchImageNames = [];
state.selection = []; state.selection = [];
}, },
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
state.autoAddBoardId = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addMatcher( builder.addMatcher(
boardsApi.endpoints.deleteBoard.matchFulfilled, boardsApi.endpoints.deleteBoard.matchFulfilled,
(state, action) => { (state, action) => {
if (action.meta.arg.originalArgs === state.selectedBoardId) { const deletedBoardId = action.meta.arg.originalArgs;
if (deletedBoardId === state.selectedBoardId) {
state.selectedBoardId = 'images'; state.selectedBoardId = 'images';
} }
if (deletedBoardId === state.autoAddBoardId) {
state.autoAddBoardId = null;
}
}
);
builder.addMatcher(
boardsApi.endpoints.listAllBoards.matchFulfilled,
(state, action) => {
const boards = action.payload;
if (!state.autoAddBoardId) {
return;
}
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
state.autoAddBoardId = null;
}
} }
); );
}, },
@ -147,6 +169,7 @@ export const {
isBatchEnabledChanged, isBatchEnabledChanged,
imagesAddedToBatch, imagesAddedToBatch,
imagesRemovedFromBatch, imagesRemovedFromBatch,
autoAddBoardIdChanged,
} = gallerySlice.actions; } = gallerySlice.actions;
export default gallerySlice.reducer; export default gallerySlice.reducer;

View File

@ -1,6 +1,9 @@
import { Box, ChakraProps } from '@chakra-ui/react'; import { Box, ChakraProps, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import IAIIconButton, { import IAIIconButton, {
IAIIconButtonProps, IAIIconButtonProps,
@ -8,11 +11,13 @@ import IAIIconButton, {
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import { selectIsBusy } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa'; import { FaPlay } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName';
const IN_PROGRESS_STYLES: ChakraProps['sx'] = { const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
_disabled: { _disabled: {
@ -26,6 +31,20 @@ const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
}, },
}; };
const selector = createSelector(
[stateSelector, activeTabNameSelector, selectIsBusy],
({ gallery }, activeTabName, isBusy) => {
const { autoAddBoardId } = gallery;
return {
isBusy,
autoAddBoardId,
activeTabName,
};
},
defaultSelectorOptions
);
interface InvokeButton interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> { extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
iconButton?: boolean; iconButton?: boolean;
@ -35,8 +54,8 @@ export default function InvokeButton(props: InvokeButton) {
const { iconButton = false, ...rest } = props; const { iconButton = false, ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isReady = useIsReadyToInvoke(); const isReady = useIsReadyToInvoke();
const activeTabName = useAppSelector(activeTabNameSelector); const { isBusy, autoAddBoardId, activeTabName } = useAppSelector(selector);
const isProcessing = useAppSelector((state) => state.system.isProcessing); const autoAddBoardName = useBoardName(autoAddBoardId);
const handleInvoke = useCallback(() => { const handleInvoke = useCallback(() => {
dispatch(clampSymmetrySteps()); dispatch(clampSymmetrySteps());
@ -75,43 +94,52 @@ export default function InvokeButton(props: InvokeButton) {
<ProgressBar /> <ProgressBar />
</Box> </Box>
)} )}
{iconButton ? ( <Tooltip
<IAIIconButton placement="top"
aria-label={t('parameters.invoke')} hasArrow
type="submit" openDelay={500}
icon={<FaPlay />} label={
isDisabled={!isReady || isProcessing} autoAddBoardId ? `Auto-Adding to ${autoAddBoardName}` : undefined
onClick={handleInvoke} }
tooltip={t('parameters.invoke')} >
tooltipProps={{ placement: 'top' }} {iconButton ? (
colorScheme="accent" <IAIIconButton
id="invoke-button" aria-label={t('parameters.invoke')}
{...rest} type="submit"
sx={{ icon={<FaPlay />}
w: 'full', isDisabled={!isReady || isBusy}
flexGrow: 1, onClick={handleInvoke}
...(isProcessing ? IN_PROGRESS_STYLES : {}), tooltip={t('parameters.invoke')}
}} tooltipProps={{ placement: 'top' }}
/> colorScheme="accent"
) : ( id="invoke-button"
<IAIButton {...rest}
aria-label={t('parameters.invoke')} sx={{
type="submit" w: 'full',
isDisabled={!isReady || isProcessing} flexGrow: 1,
onClick={handleInvoke} ...(isBusy ? IN_PROGRESS_STYLES : {}),
colorScheme="accent" }}
id="invoke-button" />
{...rest} ) : (
sx={{ <IAIButton
w: 'full', aria-label={t('parameters.invoke')}
flexGrow: 1, type="submit"
fontWeight: 700, isDisabled={!isReady || isBusy}
...(isProcessing ? IN_PROGRESS_STYLES : {}), onClick={handleInvoke}
}} colorScheme="accent"
> id="invoke-button"
Invoke {...rest}
</IAIButton> sx={{
)} w: 'full',
flexGrow: 1,
fontWeight: 700,
...(isBusy ? IN_PROGRESS_STYLES : {}),
}}
>
Invoke
</IAIButton>
)}
</Tooltip>
</Box> </Box>
</Box> </Box>
); );

View File

@ -1,33 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import { setShouldLoopback } from 'features/parameters/store/postprocessingSlice';
import { useTranslation } from 'react-i18next';
import { FaRecycle } from 'react-icons/fa';
const loopbackSelector = createSelector(
postprocessingSelector,
({ shouldLoopback }) => shouldLoopback
);
const LoopbackButton = () => {
const dispatch = useAppDispatch();
const shouldLoopback = useAppSelector(loopbackSelector);
const { t } = useTranslation();
return (
<IAIIconButton
aria-label={t('parameters.toggleLoopback')}
tooltip={t('parameters.toggleLoopback')}
isChecked={shouldLoopback}
icon={<FaRecycle />}
onClick={() => {
dispatch(setShouldLoopback(!shouldLoopback));
}}
/>
);
};
export default LoopbackButton;

View File

@ -9,7 +9,6 @@ const ProcessButtons = () => {
return ( return (
<Flex gap={2}> <Flex gap={2}>
<InvokeButton /> <InvokeButton />
{/* {activeTabName === 'img2img' && <LoopbackButton />} */}
<CancelButton /> <CancelButton />
</Flex> </Flex>
); );

View File

@ -0,0 +1,57 @@
import { Badge, BadgeProps, Flex, Text, TextProps } from '@chakra-ui/react';
import IAISwitch, { IAISwitchProps } from 'common/components/IAISwitch';
import { useTranslation } from 'react-i18next';
type SettingSwitchProps = IAISwitchProps & {
label: string;
useBadge?: boolean;
badgeLabel?: string;
textProps?: TextProps;
badgeProps?: BadgeProps;
};
export default function SettingSwitch(props: SettingSwitchProps) {
const { t } = useTranslation();
const {
label,
textProps,
useBadge = false,
badgeLabel = t('settings.experimental'),
badgeProps,
...rest
} = props;
return (
<Flex justifyContent="space-between" py={1}>
<Flex gap={2} alignItems="center">
<Text
sx={{
fontSize: 14,
_dark: {
color: 'base.300',
},
}}
{...textProps}
>
{label}
</Text>
{useBadge && (
<Badge
size="xs"
sx={{
px: 2,
color: 'base.700',
bg: 'accent.200',
_dark: { bg: 'accent.500', color: 'base.200' },
}}
{...badgeProps}
>
{badgeLabel}
</Badge>
)}
</Flex>
<IAISwitch {...rest} />
</Flex>
);
}

View File

@ -1,60 +1,71 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallback, useEffect, useState } from 'react';
import { StyledFlex } from './SettingsModal';
import { Heading, Text } from '@chakra-ui/react'; import { Heading, Text } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback, useEffect } from 'react';
import IAIButton from '../../../../common/components/IAIButton'; import IAIButton from '../../../../common/components/IAIButton';
import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images'; import {
import { addToast } from '../../store/systemSlice'; useClearIntermediatesMutation,
useGetIntermediatesCountQuery,
} from '../../../../services/api/endpoints/images';
import { resetCanvas } from '../../../canvas/store/canvasSlice'; import { resetCanvas } from '../../../canvas/store/canvasSlice';
import { addToast } from '../../store/systemSlice';
import { StyledFlex } from './SettingsModal';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
export default function SettingsClearIntermediates() { export default function SettingsClearIntermediates() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isDisabled, setIsDisabled] = useState(false);
const { data: intermediatesCount, refetch: updateIntermediatesCount } =
useGetIntermediatesCountQuery();
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] = const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
useClearIntermediatesMutation(); useClearIntermediatesMutation();
const handleClickClearIntermediates = useCallback(() => { const handleClickClearIntermediates = useCallback(() => {
clearIntermediates({}) clearIntermediates()
.unwrap() .unwrap()
.then((response) => { .then((response) => {
dispatch(controlNetReset());
dispatch(resetCanvas()); dispatch(resetCanvas());
dispatch( dispatch(
addToast({ addToast({
title: title: `Cleared ${response} intermediates`,
response === 0
? `No intermediates to clear`
: `Successfully cleared ${response} intermediates`,
status: 'info', status: 'info',
}) })
); );
if (response < 100) {
setIsDisabled(true);
}
}); });
}, [clearIntermediates, dispatch]); }, [clearIntermediates, dispatch]);
useEffect(() => {
// update the count on mount
updateIntermediatesCount();
}, [updateIntermediatesCount]);
const buttonText = intermediatesCount
? `Clear ${intermediatesCount} Intermediate${
intermediatesCount > 1 ? 's' : ''
}`
: 'No Intermediates to Clear';
return ( return (
<StyledFlex> <StyledFlex>
<Heading size="sm">Clear Intermediates</Heading> <Heading size="sm">Clear Intermediates</Heading>
<IAIButton <IAIButton
colorScheme="error" colorScheme="warning"
onClick={handleClickClearIntermediates} onClick={handleClickClearIntermediates}
isLoading={isLoadingClearIntermediates} isLoading={isLoadingClearIntermediates}
isDisabled={isDisabled} isDisabled={!intermediatesCount}
> >
{isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'} {buttonText}
</IAIButton> </IAIButton>
<Text> <Text fontWeight="bold">
Will permanently delete first 100 intermediates found on disk and in Clearing intermediates will reset your Canvas and ControlNet state.
database
</Text> </Text>
<Text fontWeight="bold">This will also clear your canvas state.</Text> <Text variant="subtext">
<Text>
Intermediate images are byproducts of generation, different from the Intermediate images are byproducts of generation, different from the
result images in the gallery. Purging intermediates will free disk result images in the gallery. Clearing intermediates will free disk
space. Your gallery images will not be deleted. space.
</Text> </Text>
<Text variant="subtext">Your gallery images will not be deleted.</Text>
</StyledFlex> </StyledFlex>
); );
} }

View File

@ -11,13 +11,12 @@ import {
Text, Text,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector, current } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants'; import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import IAISwitch from 'common/components/IAISwitch';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { import {
SystemState, SystemState,
@ -25,7 +24,6 @@ import {
setEnableImageDebugging, setEnableImageDebugging,
setIsNodesEnabled, setIsNodesEnabled,
setShouldConfirmOnDelete, setShouldConfirmOnDelete,
setShouldDisplayGuides,
shouldAntialiasProgressImageChanged, shouldAntialiasProgressImageChanged,
shouldLogToConsoleChanged, shouldLogToConsoleChanged,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
@ -48,15 +46,15 @@ import {
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { LogLevelName } from 'roarr'; import { LogLevelName } from 'roarr';
import SettingsSchedulers from './SettingsSchedulers'; import SettingSwitch from './SettingSwitch';
import SettingsClearIntermediates from './SettingsClearIntermediates'; import SettingsClearIntermediates from './SettingsClearIntermediates';
import SettingsSchedulers from './SettingsSchedulers';
const selector = createSelector( const selector = createSelector(
[systemSelector, uiSelector], [systemSelector, uiSelector],
(system: SystemState, ui: UIState) => { (system: SystemState, ui: UIState) => {
const { const {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
consoleLogLevel, consoleLogLevel,
shouldLogToConsole, shouldLogToConsole,
@ -73,7 +71,6 @@ const selector = createSelector(
return { return {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
shouldUseCanvasBetaLayout, shouldUseCanvasBetaLayout,
shouldUseSliders, shouldUseSliders,
@ -139,7 +136,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
const { const {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
shouldUseCanvasBetaLayout, shouldUseCanvasBetaLayout,
shouldUseSliders, shouldUseSliders,
@ -195,7 +191,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Modal <Modal
isOpen={isSettingsModalOpen} isOpen={isSettingsModalOpen}
onClose={onSettingsModalClose} onClose={onSettingsModalClose}
size="xl" size="2xl"
isCentered isCentered
> >
<ModalOverlay /> <ModalOverlay />
@ -206,7 +202,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Flex sx={{ gap: 4, flexDirection: 'column' }}> <Flex sx={{ gap: 4, flexDirection: 'column' }}>
<StyledFlex> <StyledFlex>
<Heading size="sm">{t('settings.general')}</Heading> <Heading size="sm">{t('settings.general')}</Heading>
<IAISwitch <SettingSwitch
label={t('settings.confirmOnDelete')} label={t('settings.confirmOnDelete')}
isChecked={shouldConfirmOnDelete} isChecked={shouldConfirmOnDelete}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -214,7 +210,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
} }
/> />
{shouldShowAdvancedOptionsSettings && ( {shouldShowAdvancedOptionsSettings && (
<IAISwitch <SettingSwitch
label={t('settings.showAdvancedOptions')} label={t('settings.showAdvancedOptions')}
isChecked={shouldShowAdvancedOptions} isChecked={shouldShowAdvancedOptions}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -231,37 +227,21 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<StyledFlex> <StyledFlex>
<Heading size="sm">{t('settings.ui')}</Heading> <Heading size="sm">{t('settings.ui')}</Heading>
<IAISwitch <SettingSwitch
label={t('settings.displayHelpIcons')}
isChecked={shouldDisplayGuides}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldDisplayGuides(e.target.checked))
}
/>
{shouldShowBetaLayout && (
<IAISwitch
label={t('settings.useCanvasBeta')}
isChecked={shouldUseCanvasBetaLayout}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
}
/>
)}
<IAISwitch
label={t('settings.useSlidersForAll')} label={t('settings.useSlidersForAll')}
isChecked={shouldUseSliders} isChecked={shouldUseSliders}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseSliders(e.target.checked)) dispatch(setShouldUseSliders(e.target.checked))
} }
/> />
<IAISwitch <SettingSwitch
label={t('settings.showProgressInViewer')} label={t('settings.showProgressInViewer')}
isChecked={shouldShowProgressInViewer} isChecked={shouldShowProgressInViewer}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldShowProgressInViewer(e.target.checked)) dispatch(setShouldShowProgressInViewer(e.target.checked))
} }
/> />
<IAISwitch <SettingSwitch
label={t('settings.antialiasProgressImages')} label={t('settings.antialiasProgressImages')}
isChecked={shouldAntialiasProgressImage} isChecked={shouldAntialiasProgressImage}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -270,9 +250,21 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
) )
} }
/> />
{shouldShowBetaLayout && (
<SettingSwitch
label={t('settings.alternateCanvasLayout')}
useBadge
badgeLabel={t('settings.beta')}
isChecked={shouldUseCanvasBetaLayout}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
}
/>
)}
{shouldShowNodesToggle && ( {shouldShowNodesToggle && (
<IAISwitch <SettingSwitch
label="Enable Nodes Editor (Experimental)" label={t('settings.enableNodesEditor')}
useBadge
isChecked={isNodesEnabled} isChecked={isNodesEnabled}
onChange={handleToggleNodes} onChange={handleToggleNodes}
/> />
@ -282,7 +274,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
{shouldShowDeveloperSettings && ( {shouldShowDeveloperSettings && (
<StyledFlex> <StyledFlex>
<Heading size="sm">{t('settings.developer')}</Heading> <Heading size="sm">{t('settings.developer')}</Heading>
<IAISwitch <SettingSwitch
label={t('settings.shouldLogToConsole')} label={t('settings.shouldLogToConsole')}
isChecked={shouldLogToConsole} isChecked={shouldLogToConsole}
onChange={handleLogToConsoleChanged} onChange={handleLogToConsoleChanged}
@ -294,7 +286,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
value={consoleLogLevel} value={consoleLogLevel}
data={VALID_LOG_LEVELS.concat()} data={VALID_LOG_LEVELS.concat()}
/> />
<IAISwitch <SettingSwitch
label={t('settings.enableImageDebugging')} label={t('settings.enableImageDebugging')}
isChecked={enableImageDebugging} isChecked={enableImageDebugging}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -313,8 +305,12 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
</IAIButton> </IAIButton>
{shouldShowResetWebUiText && ( {shouldShowResetWebUiText && (
<> <>
<Text>{t('settings.resetWebUIDesc1')}</Text> <Text variant="subtext">
<Text>{t('settings.resetWebUIDesc2')}</Text> {t('settings.resetWebUIDesc1')}
</Text>
<Text variant="subtext">
{t('settings.resetWebUIDesc2')}
</Text>
</> </>
)} )}
</StyledFlex> </StyledFlex>

View File

@ -38,7 +38,6 @@ export interface SystemState {
currentIteration: number; currentIteration: number;
totalIterations: number; totalIterations: number;
currentStatusHasSteps: boolean; currentStatusHasSteps: boolean;
shouldDisplayGuides: boolean;
isCancelable: boolean; isCancelable: boolean;
enableImageDebugging: boolean; enableImageDebugging: boolean;
toastQueue: UseToastOptions[]; toastQueue: UseToastOptions[];
@ -84,14 +83,12 @@ export interface SystemState {
shouldAntialiasProgressImage: boolean; shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES; language: keyof typeof LANGUAGES;
isUploading: boolean; isUploading: boolean;
boardIdToAddTo?: string;
isNodesEnabled: boolean; isNodesEnabled: boolean;
} }
export const initialSystemState: SystemState = { export const initialSystemState: SystemState = {
isConnected: false, isConnected: false,
isProcessing: false, isProcessing: false,
shouldDisplayGuides: true,
isGFPGANAvailable: true, isGFPGANAvailable: true,
isESRGANAvailable: true, isESRGANAvailable: true,
shouldConfirmOnDelete: true, shouldConfirmOnDelete: true,
@ -134,9 +131,6 @@ export const systemSlice = createSlice({
setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => { setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => {
state.shouldConfirmOnDelete = action.payload; state.shouldConfirmOnDelete = action.payload;
}, },
setShouldDisplayGuides: (state, action: PayloadAction<boolean>) => {
state.shouldDisplayGuides = action.payload;
},
setIsCancelable: (state, action: PayloadAction<boolean>) => { setIsCancelable: (state, action: PayloadAction<boolean>) => {
state.isCancelable = action.payload; state.isCancelable = action.payload;
}, },
@ -204,7 +198,6 @@ export const systemSlice = createSlice({
*/ */
builder.addCase(appSocketSubscribed, (state, action) => { builder.addCase(appSocketSubscribed, (state, action) => {
state.sessionId = action.payload.sessionId; state.sessionId = action.payload.sessionId;
state.boardIdToAddTo = action.payload.boardId;
state.canceledSession = ''; state.canceledSession = '';
}); });
@ -213,7 +206,6 @@ export const systemSlice = createSlice({
*/ */
builder.addCase(appSocketUnsubscribed, (state) => { builder.addCase(appSocketUnsubscribed, (state) => {
state.sessionId = null; state.sessionId = null;
state.boardIdToAddTo = undefined;
}); });
/** /**
@ -390,7 +382,6 @@ export const {
setIsProcessing, setIsProcessing,
setShouldConfirmOnDelete, setShouldConfirmOnDelete,
setCurrentStatus, setCurrentStatus,
setShouldDisplayGuides,
setIsCancelable, setIsCancelable,
setEnableImageDebugging, setEnableImageDebugging,
addToast, addToast,

View File

@ -75,42 +75,49 @@ const ModelList = (props: ModelListProps) => {
labelPos="side" labelPos="side"
/> />
{['images', 'diffusers'].includes(modelFormatFilter) && <Flex
filteredDiffusersModels.length > 0 && ( flexDirection="column"
<StyledModelContainer> gap={4}
<Flex sx={{ gap: 2, flexDir: 'column' }}> maxHeight={window.innerHeight - 280}
<Text variant="subtext" fontSize="sm"> overflow="scroll"
Diffusers >
</Text> {['images', 'diffusers'].includes(modelFormatFilter) &&
{filteredDiffusersModels.map((model) => ( filteredDiffusersModels.length > 0 && (
<ModelListItem <StyledModelContainer>
key={model.id} <Flex sx={{ gap: 2, flexDir: 'column' }}>
model={model} <Text variant="subtext" fontSize="sm">
isSelected={selectedModelId === model.id} Diffusers
setSelectedModelId={setSelectedModelId} </Text>
/> {filteredDiffusersModels.map((model) => (
))} <ModelListItem
</Flex> key={model.id}
</StyledModelContainer> model={model}
)} isSelected={selectedModelId === model.id}
{['images', 'checkpoint'].includes(modelFormatFilter) && setSelectedModelId={setSelectedModelId}
filteredCheckpointModels.length > 0 && ( />
<StyledModelContainer> ))}
<Flex sx={{ gap: 2, flexDir: 'column' }}> </Flex>
<Text variant="subtext" fontSize="sm"> </StyledModelContainer>
Checkpoint )}
</Text> {['images', 'checkpoint'].includes(modelFormatFilter) &&
{filteredCheckpointModels.map((model) => ( filteredCheckpointModels.length > 0 && (
<ModelListItem <StyledModelContainer>
key={model.id} <Flex sx={{ gap: 2, flexDir: 'column' }}>
model={model} <Text variant="subtext" fontSize="sm">
isSelected={selectedModelId === model.id} Checkpoints
setSelectedModelId={setSelectedModelId} </Text>
/> {filteredCheckpointModels.map((model) => (
))} <ModelListItem
</Flex> key={model.id}
</StyledModelContainer> model={model}
)} isSelected={selectedModelId === model.id}
setSelectedModelId={setSelectedModelId}
/>
))}
</Flex>
</StyledModelContainer>
)}
</Flex>
</Flex> </Flex>
</Flex> </Flex>
); );
@ -146,8 +153,6 @@ const StyledModelContainer = (props: PropsWithChildren) => {
return ( return (
<Flex <Flex
flexDirection="column" flexDirection="column"
maxHeight={window.innerHeight - 280}
overflow="scroll"
gap={4} gap={4}
borderRadius={4} borderRadius={4}
p={4} p={4}

View File

@ -98,16 +98,7 @@ export default function ModelListItem(props: ModelListItemProps) {
onClick={handleSelectModel} onClick={handleSelectModel}
> >
<Flex gap={4} alignItems="center"> <Flex gap={4} alignItems="center">
<Badge <Badge minWidth={14} p={0.5} fontSize="sm" variant="solid">
minWidth={14}
p={1}
fontSize="sm"
sx={{
bg: 'base.350',
color: 'base.900',
_dark: { bg: 'base.500' },
}}
>
{ {
modelBaseTypeMap[ modelBaseTypeMap[
model.base_model as keyof typeof modelBaseTypeMap model.base_model as keyof typeof modelBaseTypeMap

View File

@ -127,6 +127,13 @@ export const imagesApi = api.injectEndpoints({
// 24 hours - reducing this to a few minutes would reduce memory usage. // 24 hours - reducing this to a few minutes would reduce memory usage.
keepUnusedDataFor: 86400, keepUnusedDataFor: 86400,
}), }),
getIntermediatesCount: build.query<number, void>({
query: () => ({ url: getListImagesUrl({ is_intermediate: true }) }),
providesTags: ['IntermediatesCount'],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total;
},
}),
getImageDTO: build.query<ImageDTO, string>({ getImageDTO: build.query<ImageDTO, string>({
query: (image_name) => ({ url: `images/${image_name}` }), query: (image_name) => ({ url: `images/${image_name}` }),
providesTags: (result, error, arg) => { providesTags: (result, error, arg) => {
@ -148,8 +155,9 @@ export const imagesApi = api.injectEndpoints({
}, },
keepUnusedDataFor: 86400, // 24 hours keepUnusedDataFor: 86400, // 24 hours
}), }),
clearIntermediates: build.mutation({ clearIntermediates: build.mutation<number, void>({
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }), query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
invalidatesTags: ['IntermediatesCount'],
}), }),
deleteImage: build.mutation<void, ImageDTO>({ deleteImage: build.mutation<void, ImageDTO>({
query: ({ image_name }) => ({ query: ({ image_name }) => ({
@ -617,6 +625,7 @@ export const imagesApi = api.injectEndpoints({
}); });
export const { export const {
useGetIntermediatesCountQuery,
useListImagesQuery, useListImagesQuery,
useLazyListImagesQuery, useLazyListImagesQuery,
useGetImageDTOQuery, useGetImageDTOQuery,

View File

@ -0,0 +1,26 @@
import { BoardId } from 'features/gallery/store/gallerySlice';
import { useListAllBoardsQuery } from '../endpoints/boards';
export const useBoardName = (board_id: BoardId | null | undefined) => {
const { boardName } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
let boardName = '';
if (board_id === 'images') {
boardName = 'All Images';
} else if (board_id === 'assets') {
boardName = 'All Assets';
} else if (board_id === 'no_board') {
boardName = 'No Board';
} else if (board_id === 'batch') {
boardName = 'Batch';
} else {
const selectedBoard = data?.find((b) => b.board_id === board_id);
boardName = selectedBoard?.board_name || 'Unknown Board';
}
return { boardName };
},
});
return boardName;
};

View File

@ -12,7 +12,7 @@ repo_url: 'https://github.com/invoke-ai/InvokeAI'
edit_uri: edit/main/docs/ edit_uri: edit/main/docs/
# Copyright # Copyright
copyright: Copyright &copy; 2022 InvokeAI Team copyright: Copyright &copy; 2023 InvokeAI Team
# Configuration # Configuration
theme: theme:
@ -35,8 +35,11 @@ theme:
features: features:
- navigation.instant - navigation.instant
- navigation.tabs - navigation.tabs
- navigation.tabs.sticky
- navigation.top - navigation.top
- navigation.tracking - navigation.tracking
- navigation.indexes
- navigation.path
- search.highlight - search.highlight
- search.suggest - search.suggest
- toc.integrate - toc.integrate
@ -95,3 +98,68 @@ plugins:
'installation/INSTALL_DOCKER.md': 'installation/040_INSTALL_DOCKER.md' 'installation/INSTALL_DOCKER.md': 'installation/040_INSTALL_DOCKER.md'
'installation/INSTALLING_MODELS.md': 'installation/050_INSTALLING_MODELS.md' 'installation/INSTALLING_MODELS.md': 'installation/050_INSTALLING_MODELS.md'
'installation/INSTALL_PATCHMATCH.md': 'installation/060_INSTALL_PATCHMATCH.md' 'installation/INSTALL_PATCHMATCH.md': 'installation/060_INSTALL_PATCHMATCH.md'
nav:
- Home: 'index.md'
- Installation:
- Overview: 'installation/index.md'
- Installing with the Automated Installer: 'installation/010_INSTALL_AUTOMATED.md'
- Installing manually: 'installation/020_INSTALL_MANUAL.md'
- NVIDIA Cuda / AMD ROCm: 'installation/030_INSTALL_CUDA_AND_ROCM.md'
- Installing with Docker: 'installation/040_INSTALL_DOCKER.md'
- Installing Models: 'installation/050_INSTALLING_MODELS.md'
- Installing PyPatchMatch: 'installation/060_INSTALL_PATCHMATCH.md'
- Installing xFormers: 'installation/070_INSTALL_XFORMERS.md'
- Developers Documentation: 'installation/Developers_documentation/BUILDING_BINARY_INSTALLERS.md'
- Deprecated Documentation:
- Binary Installer: 'installation/deprecated_documentation/INSTALL_BINARY.md'
- Runninng InvokeAI on Google Colab: 'installation/deprecated_documentation/INSTALL_JUPYTER.md'
- Manual Installation on Linux: 'installation/deprecated_documentation/INSTALL_LINUX.md'
- Manual Installation on macOS: 'installation/deprecated_documentation/INSTALL_MAC.md'
- Manual Installation on Windows: 'installation/deprecated_documentation/INSTALL_WINDOWS.md'
- Installing Invoke with pip: 'installation/deprecated_documentation/INSTALL_PCP.md'
- Source Installer: 'installation/deprecated_documentation/INSTALL_SOURCE.md'
- Community Nodes:
- Community Nodes: 'nodes/communityNodes.md'
- Overview: 'nodes/overview.md'
- Features:
- Overview: 'features/index.md'
- Concepts: 'features/CONCEPTS.md'
- Configuration: 'features/CONFIGURATION.md'
- ControlNet: 'features/CONTROLNET.md'
- Image-to-Image: 'features/IMG2IMG.md'
- Controlling Logging: 'features/LOGGING.md'
- Model Mergeing: 'features/MODEL_MERGING.md'
- Nodes Editor (Experimental): 'features/NODES.md'
- NSFW Checker: 'features/NSFW.md'
- Postprocessing: 'features/POSTPROCESS.md'
- Prompting Features: 'features/PROMPTS.md'
- Training: 'features/TRAINING.md'
- Unified Canvas: 'features/UNIFIED_CANVAS.md'
- Variations: 'features/VARIATIONS.md'
- InvokeAI Web Server: 'features/WEB.md'
- WebUI Hotkeys: "features/WEBUIHOTKEYS.md"
- Other: 'features/OTHER.md'
- Contributing:
- How to Contribute: 'contributing/CONTRIBUTING.md'
- Development:
- Overview: 'contributing/contribution_guides/development.md'
- InvokeAI Architecture: 'contributing/ARCHITECTURE.md'
- Frontend Documentation: 'contributing/contribution_guides/development_guides/contributingToFrontend.md'
- Local Development: 'contributing/LOCAL_DEVELOPMENT.md'
- Documentation: 'contributing/contribution_guides/documentation.md'
- Translation: 'contributing/contribution_guides/translation.md'
- Tutorials: 'contributing/contribution_guides/tutorials.md'
- Changelog: 'CHANGELOG.md'
- Deprecated:
- Command Line Interface: 'deprecated/CLI.md'
- Embiggen: 'deprecated/EMBIGGEN.md'
- Inpainting: 'deprecated/INPAINTING.md'
- Outpainting: 'deprecated/OUTPAINTING.md'
- Help:
- Sampler Convergence: 'help/SAMPLER_CONVERGENCE.md'
- Other:
- Contributors: 'other/CONTRIBUTORS.md'
- CompViz-README: 'other/README-CompViz.md'

View File

@ -5,6 +5,7 @@
- [ ] Bug Fix - [ ] Bug Fix
- [ ] Optimization - [ ] Optimization
- [ ] Documentation Update - [ ] Documentation Update
- [ ] Community Node Submission
## Have you discussed this change with the InvokeAI team? ## Have you discussed this change with the InvokeAI team?
@ -12,6 +13,11 @@
- [ ] No, because: - [ ] No, because:
## Have you updated relevant documentation?
- [ ] Yes
- [ ] No
## Description ## Description