Merge branch 'main' into refactor/model-manager-3

This commit is contained in:
Lincoln Stein 2023-11-26 13:33:41 -05:00 committed by GitHub
commit 60eae7443a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 662 additions and 585 deletions

View File

@ -20,10 +20,16 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'eslint-plugin-react-hooks'],
plugins: [
'react',
'@typescript-eslint',
'eslint-plugin-react-hooks',
'i18next',
],
root: true,
rules: {
curly: 'error',
'i18next/no-literal-string': 2,
'react/jsx-no-bind': ['error', { allowBind: true }],
'react/jsx-curly-brace-presence': [
'error',

View File

@ -1,87 +0,0 @@
# Generated axios API client
- [Generated axios API client](#generated-axios-api-client)
- [Generation](#generation)
- [Generate the API client from the nodes web server](#generate-the-api-client-from-the-nodes-web-server)
- [Generate the API client from JSON](#generate-the-api-client-from-json)
- [Getting the JSON from the nodes web server](#getting-the-json-from-the-nodes-web-server)
- [Getting the JSON with a python script](#getting-the-json-with-a-python-script)
- [Generate the API client](#generate-the-api-client)
- [The generated client](#the-generated-client)
- [API client customisation](#api-client-customisation)
This API client is generated by an [openapi code generator](https://github.com/ferdikoomen/openapi-typescript-codegen).
All files in `invokeai/frontend/web/src/services/api/` are made by the generator.
## Generation
The axios client may be generated by from the OpenAPI schema from the nodes web server, or from JSON.
### Generate the API client from the nodes web server
We need to start the nodes web server, which serves the OpenAPI schema to the generator.
1. Start the nodes web server.
```bash
# from the repo root
python scripts/invokeai-web.py
```
2. Generate the API client.
```bash
# from invokeai/frontend/web/
yarn api:web
```
### Generate the API client from JSON
The JSON can be acquired from the nodes web server, or with a python script.
#### Getting the JSON from the nodes web server
Start the nodes web server as described above, then download the file.
```bash
# from invokeai/frontend/web/
curl http://localhost:9090/openapi.json -o openapi.json
```
#### Getting the JSON with a python script
Run this python script from the repo root, so it can access the nodes server modules.
The script will output `openapi.json` in the repo root. Then we need to move it to `invokeai/frontend/web/`.
```bash
# from the repo root
python invokeai/app/util/generate_openapi_json.py
mv invokeai/app/util/openapi.json invokeai/frontend/web/services/fixtures/
```
#### Generate the API client
Now we can generate the API client from the JSON.
```bash
# from invokeai/frontend/web/
yarn api:file
```
## The generated client
The client will be written to `invokeai/frontend/web/services/api/`:
- `axios` client
- TS types
- An easily parseable schema, which we can use to generate UI
## API client customisation
The generator has a default `request.ts` file that implements a base `axios` client. The generated client uses this base client.
One shortcoming of this is base client is it does not provide response headers unless the response body is empty. To fix this, we provide our own lightly-patched `request.ts`.
To access the headers, call `getHeaders(response)` on any response from the generated api client. This function is exported from `invokeai/frontend/web/src/services/util/getHeaders.ts`.

View File

@ -1,21 +0,0 @@
# Events
Events via `socket.io`
## `actions.ts`
Redux actions for all socket events. Payloads all include a timestamp, and optionally some other data.
Any reducer (or middleware) can respond to the actions.
## `middleware.ts`
Redux middleware for events.
Handles dispatching the event actions. Only put logic here if it can't really go anywhere else.
For example, on connect we want to load images to the gallery if it's not populated. This requires dispatching a thunk, so we need to directly dispatch this in the middleware.
## `types.ts`
Hand-written types for the socket events. Cannot generate these from the server, but fortunately they are few and simple.

View File

@ -1,17 +0,0 @@
# Node Editor Design
WIP
nodes
everything in `src/features/nodes/`
have a look at `state.nodes.invocation`
- on socket connect, if no schema saved, fetch `localhost:9090/openapi.json`, save JSON to `state.nodes.schema`
- on fulfilled schema fetch, `parseSchema()` the schema. this outputs a `Record<string, Invocation>` which is saved to `state.nodes.invocations` - `Invocation` is like a template for the node
- when you add a node, the the `Invocation` template is passed to `InvocationComponent.tsx` to build the UI component for that node
- inputs/outputs have field types - and each field type gets an `FieldComponent` which includes a dispatcher to write state changes to redux `nodesSlice`
- `reactflow` sends changes to nodes/edges to redux
- to invoke, `buildNodesGraph()` state, then send this
- changed onClick Invoke button actions to build the schema, then when schema builds it dispatches the actual network request to create the session - see `session.ts`

View File

@ -1,17 +0,0 @@
# Package Scripts
WIP walkthrough of `package.json` scripts.
## `theme` & `theme:watch`
These run the Chakra CLI to generate types for the theme, or watch for code change and re-generate the types.
The CLI essentially monkeypatches Chakra's files in `node_modules`.
## `postinstall`
The `postinstall` script patches a few packages and runs the Chakra CLI to generate types for the theme.
### Patch `@chakra-ui/cli`
See: <https://github.com/chakra-ui/chakra-ui/issues/7394>

View File

@ -1,33 +1,98 @@
# InvokeAI Web UI
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [InvokeAI Web UI](#invokeai-web-ui)
- [Stack](#stack)
- [Core Libraries](#core-libraries)
- [Redux Toolkit](#redux-toolkit)
- [Socket\.IO](#socketio)
- [Chakra UI](#chakra-ui)
- [KonvaJS](#konvajs)
- [Vite](#vite)
- [i18next & Weblate](#i18next--weblate)
- [openapi-typescript](#openapi-typescript)
- [Client Types Generation](#client-types-generation)
- [Package Scripts](#package-scripts)
- [Contributing](#contributing)
- [Dev Environment](#dev-environment)
- [VSCode Remote Dev](#vscode-remote-dev)
- [Production builds](#production-builds)
The UI is a fairly straightforward Typescript React app. The only really fancy stuff is the Unified Canvas.
<!-- /code_chunk_output -->
Code in `invokeai/frontend/web/` if you want to have a look.
The UI is a fairly straightforward Typescript React app.
## Stack
## Core Libraries
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 app makes heavy use of a handful of libraries.
The API client and associated types are generated from the OpenAPI schema. See API_CLIENT.md.
### Redux Toolkit
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).
[Redux Toolkit](https://github.com/reduxjs/redux-toolkit) is used for state management and fetching/caching:
[Chakra-UI](https://github.com/chakra-ui/chakra-ui) for components and styling.
- `RTK-Query` for data fetching and caching
- `createAsyncThunk` for a couple other HTTP requests
- `createEntityAdapter` to normalize things like images and models
- `createListenerMiddleware` for async workflows
[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.
We use [redux-remember](https://github.com/zewish/redux-remember) for persistence.
[Vite](https://vitejs.dev/) for bundling.
### Socket\.IO
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.
[Socket\.IO](https://github.com/socketio/socket.io) is used for server-to-client events, like generation process and queue state changes.
### Chakra UI
[Chakra UI](https://github.com/chakra-ui/chakra-ui) is our primary UI library, but we also use a few components from [Mantine v6](https://v6.mantine.dev/).
### KonvaJS
[KonvaJS](https://github.com/konvajs/react-konva) powers the canvas. In the future, we'd like to explore [PixiJS](https://github.com/pixijs/pixijs) or WebGPU.
### Vite
[Vite](https://github.com/vitejs/vite) is our bundler.
### i18next & Weblate
We use [i18next](https://github.com/i18next/react-i18next) for localisation, but translation to languages other than English happens on our [Weblate](https://hosted.weblate.org/engage/invokeai/) project. **Only the English source strings should be changed on this repo.**
### openapi-typescript
[openapi-typescript](https://github.com/drwpow/openapi-typescript) is used to generate types from the server's OpenAPI schema. See TYPES_CODEGEN.md.
## Client Types Generation
We use [`openapi-typescript`](https://github.com/drwpow/openapi-typescript) to generate types from the app's OpenAPI schema.
The generated types are written to `invokeai/frontend/web/src/services/api/schema.d.ts`. This file is committed to the repo.
The server must be started and available at <http://127.0.0.1:9090>.
```sh
# from the repo root, start the server
python scripts/invokeai-web.py
# from invokeai/frontend/web/, run the script
yarn typegen
```
## Package Scripts
See `package.json` for all scripts.
Run with `yarn <script name>`.
- `dev`: run the frontend in dev mode, enabling hot reloading
- `build`: run all checks (madge, eslint, prettier, tsc) and then build the frontend
- `typegen`: generate types from the OpenAPI schema (see [Client Types Generation](#client-types-generation))
- `lint:madge`: check frontend for circular dependencies
- `lint:eslint`: check frontend for code quality
- `lint:prettier`: check frontend for code formatting
- `lint:tsc`: check frontend for type issues
- `lint`: run all checks concurrently
- `fix`: run `eslint` and `prettier`, fixing fixable issues
## Contributing

View File

@ -19,7 +19,6 @@
"dist"
],
"scripts": {
"prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky",
"dev": "concurrently \"vite dev\" \"yarn run theme:watch\"",
"dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"",
"build": "yarn run lint && vite build",
@ -30,7 +29,7 @@
"lint:prettier": "prettier --check .",
"lint:tsc": "tsc --noEmit",
"lint": "concurrently -g -n eslint,prettier,tsc,madge -c cyan,green,magenta,yellow \"yarn run lint:eslint\" \"yarn run lint:prettier\" \"yarn run lint:tsc\" \"yarn run lint:madge\"",
"fix": "eslint --fix . && prettier --loglevel warn --write . && tsc --noEmit",
"fix": "eslint --fix . && prettier --loglevel warn --write .",
"lint-staged": "lint-staged",
"postinstall": "patch-package && yarn run theme",
"theme": "chakra-cli tokens src/theme/theme.ts",
@ -80,7 +79,6 @@
"lodash-es": "^4.17.21",
"nanostores": "^0.9.4",
"new-github-issue-url": "^1.0.0",
"openapi-fetch": "^0.8.1",
"overlayscrollbars": "^2.4.4",
"overlayscrollbars-react": "^0.5.3",
"patch-package": "^8.0.0",
@ -133,6 +131,7 @@
"concurrently": "^8.2.2",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-i18next": "^6.0.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",

View File

@ -90,7 +90,16 @@
"openInNewTab": "In einem neuem Tab öffnen",
"statusProcessing": "wird bearbeitet",
"linear": "Linear",
"imagePrompt": "Bild Prompt"
"imagePrompt": "Bild Prompt",
"checkpoint": "Checkpoint",
"inpaint": "inpaint",
"simple": "Einfach",
"template": "Vorlage",
"outputs": "Ausgabe",
"data": "Daten",
"safetensors": "Safetensors",
"outpaint": "outpaint",
"details": "Details"
},
"gallery": {
"generations": "Erzeugungen",
@ -110,7 +119,6 @@
"preparingDownload": "bereite Download vor",
"preparingDownloadFailed": "Problem beim Download vorbereiten",
"deleteImage": "Lösche Bild",
"images": "Bilder",
"copy": "Kopieren",
"download": "Runterladen",
"setCurrentImage": "Setze aktuelle Bild",
@ -120,7 +128,8 @@
"downloadSelection": "Auswahl herunterladen",
"currentlyInUse": "Dieses Bild wird derzeit in den folgenden Funktionen verwendet:",
"deleteImagePermanent": "Gelöschte Bilder können nicht wiederhergestellt werden.",
"autoAssignBoardOnClick": "Board per Klick automatisch zuweisen"
"autoAssignBoardOnClick": "Board per Klick automatisch zuweisen",
"noImageSelected": "Kein Bild ausgewählt"
},
"hotkeys": {
"keyboardShortcuts": "Tastenkürzel",
@ -454,7 +463,7 @@
"quickAdd": "Schnell hinzufügen",
"simpleModelDesc": "Geben Sie einen Pfad zu einem lokalen Diffusers-Modell, einem lokalen Checkpoint-/Safetensors-Modell, einer HuggingFace-Repo-ID oder einer Checkpoint-/Diffusers-Modell-URL an.",
"modelDeleted": "Modell gelöscht",
"inpainting": "v1 Ausmalen",
"inpainting": "v1 Inpainting",
"modelUpdateFailed": "Modellaktualisierung fehlgeschlagen",
"useCustomConfig": "Benutzerdefinierte Konfiguration verwenden",
"settings": "Einstellungen",
@ -473,7 +482,10 @@
"variant": "Variante",
"loraModels": "LoRAs",
"modelDeleteFailed": "Modell konnte nicht gelöscht werden",
"mergedModelName": "Zusammengeführter Modellname"
"mergedModelName": "Zusammengeführter Modellname",
"checkpointOrSafetensors": "$t(common.checkpoint) / $t(common.safetensors)",
"formMessageDiffusersModelLocation": "Diffusers Modell Speicherort",
"noModelSelected": "Kein Modell ausgewählt"
},
"parameters": {
"images": "Bilder",
@ -683,7 +695,8 @@
"exitViewer": "Betrachten beenden",
"menu": "Menü",
"loadMore": "Mehr laden",
"invokeProgressBar": "Invoke Fortschrittsanzeige"
"invokeProgressBar": "Invoke Fortschrittsanzeige",
"mode": "Modus"
},
"boards": {
"autoAddBoard": "Automatisches Hinzufügen zum Ordner",
@ -701,7 +714,11 @@
"changeBoard": "Ordner wechseln",
"loading": "Laden...",
"clearSearch": "Suche leeren",
"bottomMessage": "Durch das Löschen dieses Ordners und seiner Bilder werden alle Funktionen zurückgesetzt, die sie derzeit verwenden."
"bottomMessage": "Durch das Löschen dieses Ordners und seiner Bilder werden alle Funktionen zurückgesetzt, die sie derzeit verwenden.",
"deleteBoardOnly": "Nur Ordner löschen",
"deleteBoard": "Löschen Ordner",
"deleteBoardAndImages": "Löschen Ordner und Bilder",
"deletedBoardsCannotbeRestored": "Gelöschte Ordner könnte nicht wiederhergestellt werden"
},
"controlnet": {
"showAdvanced": "Zeige Erweitert",
@ -786,7 +803,8 @@
"canny": "Canny",
"hedDescription": "Ganzheitlich verschachtelte Kantenerkennung",
"scribble": "Scribble",
"maxFaces": "Maximal Anzahl Gesichter"
"maxFaces": "Maximal Anzahl Gesichter",
"unstarImage": "Markierung aufheben"
},
"queue": {
"status": "Status",
@ -840,7 +858,8 @@
"pauseTooltip": "Pause von Prozessor",
"back": "Hinten",
"resumeSucceeded": "Prozessor wieder aufgenommen",
"resumeTooltip": "Prozessor wieder aufnehmen"
"resumeTooltip": "Prozessor wieder aufnehmen",
"time": "Zeit"
},
"metadata": {
"negativePrompt": "Negativ Beschreibung",
@ -868,7 +887,8 @@
"vae": "VAE",
"workflow": "Arbeitsablauf",
"scheduler": "Scheduler",
"noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden"
"noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden",
"recallParameters": "Recall Parameters"
},
"popovers": {
"noiseUseCPU": {
@ -944,7 +964,9 @@
"booleanCollection": "Boolesche Werte Sammlung",
"cannotConnectToSelf": "Es kann keine Verbindung zu sich selbst hergestellt werden",
"colorCodeEdges": "Farbkodierte Kanten",
"addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)"
"addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)",
"boardField": "Ordner",
"boardFieldDescription": "Ein Galerie Ordner"
},
"hrf": {
"enableHrf": "Aktivieren Sie die Korrektur für hohe Auflösungen",
@ -968,6 +990,8 @@
"selectModel": "Wählen ein Modell aus",
"noRefinerModelsInstalled": "Keine SDXL Refiner-Modelle installiert",
"noLoRAsInstalled": "Keine LoRAs installiert",
"selectLoRA": "Wählen ein LoRA aus"
"selectLoRA": "Wählen ein LoRA aus",
"esrganModel": "ESRGAN Modell",
"addLora": "LoRA hinzufügen"
}
}

View File

@ -1,6 +1,7 @@
{
"accessibility": {
"copyMetadataJson": "Copy metadata JSON",
"createIssue":"Create Issue",
"exitViewer": "Exit Viewer",
"flipHorizontally": "Flip Horizontally",
"flipVertically": "Flip Vertically",
@ -12,6 +13,7 @@
"nextImage": "Next Image",
"previousImage": "Previous Image",
"reset": "Reset",
"resetUI":"$t(accessibility.reset) UI",
"rotateClockwise": "Rotate Clockwise",
"rotateCounterClockwise": "Rotate Counter-Clockwise",
"showGalleryPanel": "Show Gallery Panel",
@ -38,6 +40,8 @@
"loading": "Loading...",
"menuItemAutoAdd": "Auto-add to this Board",
"move": "Move",
"movingImagesToBoard_one": "Moving {{count}} image to board:",
"movingImagesToBoard_other": "Moving {{count}} images to board:",
"myBoard": "My Board",
"noMatching": "No matching Boards",
"searchBoard": "Search Boards...",
@ -49,11 +53,13 @@
"common": {
"accept": "Accept",
"advanced": "Advanced",
"ai": "ai",
"areYouSure": "Are you sure?",
"auto": "Auto",
"back": "Back",
"batch": "Batch Manager",
"cancel": "Cancel",
"copyError":"$t(gallery.copy) Error",
"close": "Close",
"on": "On",
"checkpoint": "Checkpoint",
@ -67,6 +73,10 @@
"darkMode": "Dark Mode",
"discordLabel": "Discord",
"dontAskMeAgain": "Don't ask me again",
"error": "Error",
"file": "File",
"folder": "Folder",
"format":"format",
"generate": "Generate",
"githubLabel": "Github",
"hotkeysLabel": "Hotkeys",
@ -74,6 +84,8 @@
"imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
"inpaint": "inpaint",
"input": "Input",
"installed": "Installed",
"langArabic": "العربية",
"langBrPortuguese": "Português do Brasil",
"langDutch": "Nederlands",
@ -101,6 +113,7 @@
"nodeEditor": "Node Editor",
"nodes": "Workflow Editor",
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"outpaint": "outpaint",
"outputs": "Outputs",
@ -114,6 +127,7 @@
"safetensors": "Safetensors",
"settingsLabel": "Settings",
"simple": "Simple",
"somethingWentWrong": "Something went wrong",
"statusConnected": "Connected",
"statusConvertingModel": "Converting Model",
"statusDisconnected": "Disconnected",
@ -252,6 +266,7 @@
"embedding": {
"addEmbedding": "Add Embedding",
"incompatibleModel": "Incompatible base model:",
"noEmbeddingsLoaded": "No Embeddings Loaded",
"noMatchingEmbedding": "No matching Embeddings"
},
"queue": {
@ -330,7 +345,8 @@
"enableFailed": "Problem Enabling Invocation Cache",
"disable": "Disable",
"disableSucceeded": "Invocation Cache Disabled",
"disableFailed": "Problem Disabling Invocation Cache"
"disableFailed": "Problem Disabling Invocation Cache",
"useCache": "Use Cache"
},
"gallery": {
"allImagesLoaded": "All Images Loaded",
@ -339,6 +355,9 @@
"autoSwitchNewImages": "Auto-Switch to New Images",
"copy": "Copy",
"currentlyInUse": "This image is currently in use in the following features:",
"drop":"Drop",
"dropOrUpload":"$t(gallery.drop) or Upload",
"dropToUpload":"$t(gallery.drop) to Upload",
"deleteImage": "Delete Image",
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
"deleteImagePermanent": "Deleted images cannot be restored.",
@ -348,7 +367,7 @@
"galleryImageSize": "Image Size",
"gallerySettings": "Gallery Settings",
"generations": "Generations",
"images": "Images",
"image": "image",
"loading": "Loading",
"loadMore": "Load More",
"maintainAspectRatio": "Maintain Aspect Ratio",
@ -360,6 +379,7 @@
"singleColumnLayout": "Single Column Layout",
"unableToLoad": "Unable to load Gallery",
"uploads": "Uploads",
"deleteSelection": "Delete Selection",
"downloadSelection": "Download Selection",
"preparingDownload": "Preparing Download",
"preparingDownloadFailed": "Problem Preparing Download"
@ -629,6 +649,7 @@
"closeAdvanced": "Close Advanced",
"config": "Config",
"configValidationMsg": "Path to the config file of your model.",
"conversionNotSupported": "Conversion Not Supported",
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
"convertToDiffusers": "Convert To Diffusers",
@ -754,6 +775,7 @@
"esrganModel": "ESRGAN Model",
"loading": "loading",
"noLoRAsAvailable": "No LoRAs available",
"noLoRAsLoaded":"No LoRAs Loaded",
"noMatchingLoRAs": "No matching LoRAs",
"noMatchingModels": "No matching Models",
"noModelsAvailable": "No models available",
@ -765,6 +787,7 @@
"nodes": {
"addNode": "Add Node",
"addNodeToolTip": "Add Node (Shift+A, Space)",
"addLinearView":"Add to Linear View",
"animatedEdges": "Animated Edges",
"animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes",
"boardField": "Board",
@ -885,6 +908,7 @@
"noMatchingNodes": "No matching nodes",
"noNodeSelected": "No node selected",
"nodeOpacity": "Node Opacity",
"nodeVersion": "Node Version",
"noOutputRecorded": "No outputs recorded",
"noOutputSchemaName": "No output schema name found in ref object",
"notes": "Notes",
@ -892,6 +916,7 @@
"oNNXModelField": "ONNX Model",
"oNNXModelFieldDescription": "ONNX model field.",
"outputField": "Output Field",
"outputFieldInInput": "Output field in input",
"outputFields": "Output Fields",
"outputNode": "Output node",
"outputSchemaNotFound": "Output schema not found",
@ -937,10 +962,14 @@
"uNetFieldDescription": "UNet submodel.",
"unhandledInputProperty": "Unhandled input property",
"unhandledOutputProperty": "Unhandled output property",
"unknownField": "Unknown Field",
"unknownField": "Unknown field",
"unknownFieldType": "$(nodes.unknownField) type",
"unknownNode": "Unknown Node",
"unknownNodeType":"$t(nodes.unknownNode) type",
"unknownTemplate": "Unknown Template",
"unknownInput": "Unknown input",
"unkownInvocation": "Unknown Invocation type",
"unknownOutput": "Unknown output",
"updateNode": "Update Node",
"updateAllNodes": "Update All Nodes",
"updateApp": "Update App",
@ -1082,6 +1111,7 @@
"upscaling": "Upscaling",
"unmasked": "Unmasked",
"useAll": "Use All",
"useSize": "Use Size",
"useCpuNoise": "Use CPU Noise",
"cpuNoise": "CPU Noise",
"gpuNoise": "GPU Noise",
@ -1171,7 +1201,8 @@
"clearIntermediatesWithCount_other": "Clear {{count}} Intermediates",
"intermediatesCleared_one": "Cleared {{count}} Intermediate",
"intermediatesCleared_other": "Cleared {{count}} Intermediates",
"intermediatesClearedFailed": "Problem Clearing Intermediates"
"intermediatesClearedFailed": "Problem Clearing Intermediates",
"reloadingIn": "Reloading in"
},
"toast": {
"addedToBoard": "Added to board",
@ -1199,6 +1230,7 @@
"initialImageNotSet": "Initial Image Not Set",
"initialImageNotSetDesc": "Could not load initial image",
"initialImageSet": "Initial Image Set",
"invalidUpload": "Invalid Upload",
"loadedWithWarnings": "Workflow Loaded with Warnings",
"maskSavedAssets": "Mask Saved to Assets",
"maskSentControlnetAssets": "Mask Sent to ControlNet & Assets",
@ -1517,7 +1549,7 @@
"clearCanvasHistoryConfirm": "Are you sure you want to clear the canvas history?",
"clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.",
"clearHistory": "Clear History",
"clearMask": "Clear Mask",
"clearMask": "Clear Mask (Shift+C)",
"colorPicker": "Color Picker",
"copyToClipboard": "Copy to Clipboard",
"cursorPosition": "Cursor Position",
@ -1544,6 +1576,7 @@
"redo": "Redo",
"resetView": "Reset View",
"saveBoxRegionOnly": "Save Box Region Only",
"saveMask":"Save $t(unifiedCanvas.mask)",
"saveToGallery": "Save To Gallery",
"scaledBoundingBox": "Scaled Bounding Box",
"showCanvasDebugInfo": "Show Additional Canvas Info",

View File

@ -98,7 +98,6 @@
"deleteImage": "Eliminar Imagen",
"deleteImageBin": "Las imágenes eliminadas se enviarán a la papelera de tu sistema operativo.",
"deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.",
"images": "Imágenes",
"assets": "Activos",
"autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic"
},

View File

@ -89,7 +89,9 @@
"t2iAdapter": "Adattatore T2I",
"controlAdapter": "Adattatore di Controllo",
"controlNet": "ControlNet",
"auto": "Automatico"
"auto": "Automatico",
"simple": "Semplice",
"details": "Dettagli"
},
"gallery": {
"generations": "Generazioni",
@ -108,7 +110,6 @@
"deleteImage": "Elimina l'immagine",
"deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.",
"deleteImageBin": "Le immagini eliminate verranno spostate nel Cestino del tuo sistema operativo.",
"images": "Immagini",
"assets": "Risorse",
"autoAssignBoardOnClick": "Assegna automaticamente la bacheca al clic",
"featuresWillReset": "Se elimini questa immagine, quelle funzionalità verranno immediatamente ripristinate.",
@ -120,7 +121,8 @@
"setCurrentImage": "Imposta come immagine corrente",
"preparingDownload": "Preparazione del download",
"preparingDownloadFailed": "Problema durante la preparazione del download",
"downloadSelection": "Scarica gli elementi selezionati"
"downloadSelection": "Scarica gli elementi selezionati",
"noImageSelected": "Nessuna immagine selezionata"
},
"hotkeys": {
"keyboardShortcuts": "Tasti rapidi",
@ -474,7 +476,8 @@
"closeAdvanced": "Chiudi Avanzate",
"modelType": "Tipo di modello",
"customConfigFileLocation": "Posizione del file di configurazione personalizzato",
"vaePrecision": "Precisione VAE"
"vaePrecision": "Precisione VAE",
"noModelSelected": "Nessun modello selezionato"
},
"parameters": {
"images": "Immagini",
@ -601,7 +604,9 @@
"seamlessX": "Senza cuciture X",
"seamlessY": "Senza cuciture Y",
"imageActions": "Azioni Immagine",
"aspectRatioFree": "Libere"
"aspectRatioFree": "Libere",
"maskEdge": "Maschera i bordi",
"unmasked": "No maschera"
},
"settings": {
"models": "Modelli",
@ -642,7 +647,10 @@
"clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia",
"clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie",
"clearIntermediatesWithCount_other": "Cancella {{count}} immagini intermedie",
"clearIntermediatesDisabled": "La coda deve essere vuota per cancellare le immagini intermedie"
"clearIntermediatesDisabled": "La coda deve essere vuota per cancellare le immagini intermedie",
"enableNSFWChecker": "Abilita controllo NSFW",
"enableInvisibleWatermark": "Abilita filigrana invisibile",
"enableInformationalPopovers": "Abilita testo informativo a comparsa"
},
"toast": {
"tempFoldersEmptied": "Cartella temporanea svuotata",
@ -727,7 +735,8 @@
"setCanvasInitialImage": "Imposta come immagine iniziale della tela",
"workflowLoaded": "Flusso di lavoro caricato",
"setIPAdapterImage": "Imposta come immagine per l'Adattatore IP",
"problemSavingMaskDesc": "Impossibile salvare la maschera"
"problemSavingMaskDesc": "Impossibile salvare la maschera",
"setAsCanvasInitialImage": "Imposta come immagine iniziale della tela"
},
"tooltip": {
"feature": {
@ -828,7 +837,8 @@
"modifyConfig": "Modifica configurazione",
"menu": "Menu",
"showGalleryPanel": "Mostra il pannello Galleria",
"loadMore": "Carica altro"
"loadMore": "Carica altro",
"mode": "Modalità"
},
"ui": {
"hideProgressImages": "Nascondi avanzamento immagini",
@ -1048,7 +1058,11 @@
"noMatching": "Nessuna bacheca corrispondente",
"selectBoard": "Seleziona una Bacheca",
"uncategorized": "Non categorizzato",
"downloadBoard": "Scarica la bacheca"
"downloadBoard": "Scarica la bacheca",
"deleteBoardOnly": "Elimina solo la Bacheca",
"deleteBoard": "Elimina Bacheca",
"deleteBoardAndImages": "Elimina Bacheca e Immagini",
"deletedBoardsCannotbeRestored": "Le bacheche eliminate non possono essere ripristinate"
},
"controlnet": {
"contentShuffleDescription": "Rimescola il contenuto di un'immagine",
@ -1089,7 +1103,7 @@
"none": "Nessuno",
"incompatibleBaseModel": "Modello base incompatibile:",
"pidiDescription": "Elaborazione immagini PIDI",
"fill": "Riempire",
"fill": "Riempie",
"colorMapDescription": "Genera una mappa dei colori dall'immagine",
"lineartAnimeDescription": "Elaborazione lineart in stile anime",
"imageResolution": "Risoluzione dell'immagine",
@ -1183,7 +1197,9 @@
"clearQueueAlertDialog2": "Sei sicuro di voler cancellare la coda?",
"item": "Elemento",
"graphFailedToQueue": "Impossibile mettere in coda il grafico",
"queueMaxExceeded": "È stato superato il limite massimo di {{max_queue_size}} e {{skip}} elementi verrebbero saltati"
"queueMaxExceeded": "È stato superato il limite massimo di {{max_queue_size}} e {{skip}} elementi verrebbero saltati",
"batchFieldValues": "Valori Campi Lotto",
"time": "Tempo"
},
"embedding": {
"noMatchingEmbedding": "Nessun Incorporamento corrispondente",
@ -1199,7 +1215,9 @@
"selectModel": "Seleziona un modello",
"selectLoRA": "Seleziona un LoRA",
"noRefinerModelsInstalled": "Nessun modello SDXL Refiner installato",
"noLoRAsInstalled": "Nessun LoRA installato"
"noLoRAsInstalled": "Nessun LoRA installato",
"esrganModel": "Modello ESRGAN",
"addLora": "Aggiungi LoRA"
},
"invocationCache": {
"disable": "Disabilita",
@ -1231,7 +1249,8 @@
"promptsWithCount_one": "{{count}} Prompt",
"promptsWithCount_many": "{{count}} Prompt",
"promptsWithCount_other": "{{count}} Prompt",
"dynamicPrompts": "Prompt dinamici"
"dynamicPrompts": "Prompt dinamici",
"promptsPreview": "Anteprima dei prompt"
},
"popovers": {
"paramScheduler": {

View File

@ -660,7 +660,7 @@
"queueTotal": "合計 {{total}}",
"resumeSucceeded": "処理が再開されました",
"resumeTooltip": "処理を再開",
"resume": "再",
"resume": "再",
"status": "ステータス",
"pruneSucceeded": "キューから完了アイテム{{item_count}}件を削除しました",
"cancelTooltip": "現在のアイテムをキャンセル",

View File

@ -110,7 +110,6 @@
"deleteImageBin": "Verwijderde afbeeldingen worden naar de prullenbak van je besturingssysteem gestuurd.",
"deleteImagePermanent": "Verwijderde afbeeldingen kunnen niet worden hersteld.",
"assets": "Eigen onderdelen",
"images": "Afbeeldingen",
"autoAssignBoardOnClick": "Ken automatisch bord toe bij klikken",
"featuresWillReset": "Als je deze afbeelding verwijdert, dan worden deze functies onmiddellijk teruggezet.",
"loading": "Bezig met laden",

View File

@ -101,7 +101,6 @@
"deleteImagePermanent": "Удаленные изображения невозможно восстановить.",
"deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.",
"deleteImage": "Удалить изображение",
"images": "Изображения",
"assets": "Ресурсы",
"autoAssignBoardOnClick": "Авто-назначение доски по клику"
},

View File

@ -90,7 +90,16 @@
"controlAdapter": "Control Adapter",
"controlNet": "ControlNet",
"on": "开",
"auto": "自动"
"auto": "自动",
"checkpoint": "Checkpoint",
"inpaint": "内补重绘",
"simple": "简单",
"template": "模板",
"outputs": "输出",
"data": "数据",
"safetensors": "Safetensors",
"outpaint": "外扩绘制",
"details": "详情"
},
"gallery": {
"generations": "生成的图像",
@ -109,7 +118,6 @@
"deleteImage": "删除图片",
"deleteImageBin": "被删除的图片会发送到你操作系统的回收站。",
"deleteImagePermanent": "删除的图片无法被恢复。",
"images": "图片",
"assets": "素材",
"autoAssignBoardOnClick": "点击后自动分配面板",
"featuresWillReset": "如果您删除该图像,这些功能会立即被重置。",
@ -121,7 +129,8 @@
"setCurrentImage": "设为当前图像",
"preparingDownload": "准备下载",
"preparingDownloadFailed": "准备下载时出现问题",
"downloadSelection": "下载所选内容"
"downloadSelection": "下载所选内容",
"noImageSelected": "无选中的图像"
},
"hotkeys": {
"keyboardShortcuts": "键盘快捷键",
@ -475,7 +484,9 @@
"oliveModels": "Olive",
"loraModels": "LoRA",
"alpha": "Alpha",
"vaePrecision": "VAE 精度"
"vaePrecision": "VAE 精度",
"checkpointOrSafetensors": "$t(common.checkpoint) / $t(common.safetensors)",
"noModelSelected": "无选中的模型"
},
"parameters": {
"images": "图像",
@ -602,7 +613,9 @@
"seamlessX&Y": "无缝 X & Y",
"aspectRatioFree": "自由",
"seamlessX": "无缝 X",
"seamlessY": "无缝 Y"
"seamlessY": "无缝 Y",
"maskEdge": "遮罩边缘",
"unmasked": "取消遮罩"
},
"settings": {
"models": "模型",
@ -639,7 +652,10 @@
"clearIntermediatesDesc1": "清除中间产物会重置您的画布和 ControlNet 状态。",
"intermediatesClearedFailed": "清除中间产物时出现问题",
"clearIntermediatesWithCount_other": "清除 {{count}} 个中间产物",
"clearIntermediatesDisabled": "队列为空才能清理中间产物"
"clearIntermediatesDisabled": "队列为空才能清理中间产物",
"enableNSFWChecker": "启用成人内容检测器",
"enableInvisibleWatermark": "启用不可见水印",
"enableInformationalPopovers": "启用信息弹窗"
},
"toast": {
"tempFoldersEmptied": "临时文件夹已清空",
@ -705,7 +721,7 @@
"modelAddFailed": "模型添加失败",
"problemDownloadingCanvas": "下载画布时出现问题",
"problemMergingCanvas": "合并画布时出现问题",
"setCanvasInitialImage": "设画布初始图像",
"setCanvasInitialImage": "设画布初始图像",
"imageUploaded": "图像已上传",
"addedToBoard": "已添加到面板",
"workflowLoaded": "工作流已加载",
@ -722,7 +738,8 @@
"canvasSavedGallery": "画布已保存到图库",
"imageUploadFailed": "图像上传失败",
"problemImportingMask": "导入遮罩时出现问题",
"baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型"
"baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型",
"setAsCanvasInitialImage": "设为画布初始图像"
},
"unifiedCanvas": {
"layer": "图层",
@ -808,7 +825,8 @@
"toggleAutoscroll": "切换自动缩放",
"menu": "菜单",
"showGalleryPanel": "显示图库浮窗",
"loadMore": "加载更多"
"loadMore": "加载更多",
"mode": "模式"
},
"ui": {
"showProgressImages": "显示处理中的图片",
@ -1031,7 +1049,9 @@
"integerPolymorphic": "整数多态",
"latentsPolymorphic": "Latents 多态",
"conditioningField": "条件",
"latentsField": "Latents"
"latentsField": "Latents",
"updateAllNodes": "更新所有节点",
"unableToUpdateNodes_other": "{{count}} 个节点无法完成更新"
},
"controlnet": {
"resize": "直接缩放",
@ -1117,7 +1137,8 @@
"openPose": "Openpose",
"controlAdapter_other": "Control Adapters",
"lineartAnime": "Lineart Anime",
"canny": "Canny"
"canny": "Canny",
"unstarImage": "取消收藏图像"
},
"queue": {
"status": "状态",
@ -1176,7 +1197,9 @@
"queueTotal": "总计 {{total}}",
"enqueueing": "队列中的批次",
"queueMaxExceeded": "超出最大值 {{max_queue_size}},将跳过 {{skip}}",
"graphFailedToQueue": "节点图加入队列失败"
"graphFailedToQueue": "节点图加入队列失败",
"batchFieldValues": "批处理值",
"time": "时间"
},
"sdxl": {
"refinerStart": "Refiner 开始作用时机",
@ -1234,7 +1257,9 @@
"selectModel": "选择一个模型",
"selectLoRA": "选择一个 LoRA",
"noRefinerModelsInstalled": "无已安装的 SDXL Refiner 模型",
"noLoRAsInstalled": "无已安装的 LoRA"
"noLoRAsInstalled": "无已安装的 LoRA",
"esrganModel": "ESRGAN 模型",
"addLora": "添加 LoRA"
},
"boards": {
"autoAddBoard": "自动添加面板",
@ -1252,7 +1277,11 @@
"changeBoard": "更改面板",
"loading": "加载中...",
"clearSearch": "清除检索",
"downloadBoard": "下载面板"
"downloadBoard": "下载面板",
"deleteBoardOnly": "仅删除面板",
"deleteBoard": "删除面板",
"deleteBoardAndImages": "删除面板和图像",
"deletedBoardsCannotbeRestored": "已删除的面板无法被恢复"
},
"embedding": {
"noMatchingEmbedding": "不匹配的 Embedding",
@ -1271,7 +1300,8 @@
"combinatorial": "组合生成",
"maxPrompts": "最大提示词数",
"dynamicPrompts": "动态提示词",
"promptsWithCount_other": "{{count}} 个提示词"
"promptsWithCount_other": "{{count}} 个提示词",
"promptsPreview": "提示词预览"
},
"popovers": {
"compositingMaskAdjustments": {

View File

@ -1,61 +1,19 @@
import fs from 'node:fs';
import openapiTS from 'openapi-typescript';
import { COLORS } from './colors.js';
const OPENAPI_URL = 'http://127.0.0.1:9090/openapi.json';
const OUTPUT_FILE = 'src/services/api/schema.d.ts';
async function main() {
process.stdout.write(
`Generating types "${OPENAPI_URL}" --> "${OUTPUT_FILE}"...\n\n`
`Generating types "${OPENAPI_URL}" --> "${OUTPUT_FILE}"...`
);
const types = await openapiTS(OPENAPI_URL, {
exportType: true,
transform: (schemaObject, metadata) => {
transform: (schemaObject) => {
if ('format' in schemaObject && schemaObject.format === 'binary') {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}
/**
* Because invocations may have required fields that accept connection input, the generated
* types may be incorrect.
*
* For example, the ImageResizeInvocation has a required `image` field, but because it accepts
* connection input, it should be optional on instantiation of the field.
*
* To handle this, the schema exposes an `input` property that can be used to determine if the
* field accepts connection input. If it does, we can make the field optional.
*/
if ('class' in schemaObject && schemaObject.class === 'invocation') {
// We only want to make fields optional if they are required
if (!Array.isArray(schemaObject?.required)) {
schemaObject.required = [];
}
schemaObject.required.forEach((prop) => {
const acceptsConnection = ['any', 'connection'].includes(
schemaObject.properties?.[prop]?.['input']
);
if (acceptsConnection) {
// remove this prop from the required array
const invocationName = metadata.path.split('/').pop();
console.log(
`Making connectable field optional: ${COLORS.fg.green}${invocationName}.${COLORS.fg.cyan}${prop}${COLORS.reset}`
);
schemaObject.required = schemaObject.required.filter(
(r) => r !== prop
);
}
});
return;
}
// Check if we are generating types for an invocation output
if ('class' in schemaObject && schemaObject.class === 'output') {
// modify output types
}
},
});
fs.writeFileSync(OUTPUT_FILE, types);

View File

@ -20,6 +20,7 @@ import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
import GlobalHotkeys from './GlobalHotkeys';
import PreselectedImage from './PreselectedImage';
import Toaster from './Toaster';
import { useSocketIO } from '../hooks/useSocketIO';
const DEFAULT_CONFIG = {};
@ -33,10 +34,12 @@ interface Props {
const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
const language = useAppSelector(languageSelector);
const logger = useLogger('system');
const dispatch = useAppDispatch();
// singleton!
useSocketIO();
const handleReset = useCallback(() => {
localStorage.clear();
location.reload();

View File

@ -2,6 +2,7 @@ import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
import newGithubIssueUrl from 'new-github-issue-url';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa';
import { FaArrowRotateLeft } from 'react-icons/fa6';
import { serializeError } from 'serialize-error';
@ -13,6 +14,7 @@ type Props = {
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
const toast = useToast();
const { t } = useTranslation();
const handleCopy = useCallback(() => {
const text = JSON.stringify(serializeError(error), null, 2);
@ -53,7 +55,7 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
p: 16,
}}
>
<Heading>Something went wrong</Heading>
<Heading>{t('common.somethingWentWrong')}</Heading>
<Flex
layerStyle="second"
sx={{
@ -80,13 +82,15 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
leftIcon={<FaArrowRotateLeft />}
onClick={resetErrorBoundary}
>
Reset UI
{t('accessibility.resetUI')}
</IAIButton>
<IAIButton leftIcon={<FaCopy />} onClick={handleCopy}>
Copy Error
{t('common.copyError')}
</IAIButton>
<Link href={url} isExternal>
<IAIButton leftIcon={<FaExternalLinkAlt />}>Create Issue</IAIButton>
<IAIButton leftIcon={<FaExternalLinkAlt />}>
{t('accessibility.createIssue')}
</IAIButton>
</Link>
</Flex>
</Flex>

View File

@ -1,26 +1,27 @@
import { Middleware } from '@reduxjs/toolkit';
import { $socketOptions } from 'app/hooks/useSocketIO';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $customStarUI, CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { $projectId } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { store } from 'app/store/store';
import { PartialAppConfig } from 'app/types/invokeai';
import React, {
lazy,
memo,
PropsWithChildren,
ReactNode,
lazy,
memo,
useEffect,
} from 'react';
import { Provider } from 'react-redux';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { $authToken, $baseUrl, $projectId } from 'services/api/client';
import { socketMiddleware } from 'services/events/middleware';
import { ManagerOptions, SocketOptions } from 'socket.io-client';
import Loading from '../../common/components/Loading/Loading';
import '../../i18n';
import AppDndContext from '../../features/dnd/components/AppDndContext';
import { $customStarUI, CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
import {
$queueId,
DEFAULT_QUEUE_ID,
} from 'features/queue/store/queueNanoStore';
import '../../i18n';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -38,6 +39,8 @@ interface Props extends PropsWithChildren {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;
}
const InvokeAIUI = ({
@ -50,6 +53,8 @@ const InvokeAIUI = ({
queueId,
selectedImage,
customStarUi,
socketOptions,
isDebugging = false,
}: Props) => {
useEffect(() => {
// configure API client token
@ -82,9 +87,7 @@ const InvokeAIUI = ({
// rebuild socket middleware with token and apiUrl
if (middleware && middleware.length > 0) {
addMiddleware(socketMiddleware(), ...middleware);
} else {
addMiddleware(socketMiddleware());
addMiddleware(...middleware);
}
return () => {
@ -116,6 +119,24 @@ const InvokeAIUI = ({
};
}, [headerComponent]);
useEffect(() => {
if (socketOptions) {
$socketOptions.set(socketOptions);
}
return () => {
$socketOptions.set({});
};
}, [socketOptions]);
useEffect(() => {
if (isDebugging) {
$isDebugging.set(isDebugging);
}
return () => {
$isDebugging.set(false);
};
}, [isDebugging]);
return (
<React.StrictMode>
<Provider store={store}>

View File

@ -0,0 +1,109 @@
import { useStore } from '@nanostores/react';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { useAppDispatch } from 'app/store/storeHooks';
import { MapStore, WritableAtom, atom, map } from 'nanostores';
import { useEffect } from 'react';
import {
ClientToServerEvents,
ServerToClientEvents,
} from 'services/events/types';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { ManagerOptions, Socket, SocketOptions, io } from 'socket.io-client';
// Inject socket options and url into window for debugging
declare global {
interface Window {
$socketOptions?: MapStore<Partial<ManagerOptions & SocketOptions>>;
$socketUrl?: WritableAtom<string>;
}
}
const makeSocketOptions = (): Partial<ManagerOptions & SocketOptions> => {
const socketOptions: Parameters<typeof io>[0] = {
timeout: 60000,
path: '/ws/socket.io',
autoConnect: false, // achtung! removing this breaks the dynamic middleware
forceNew: true,
};
// if building in package mode, replace socket url with open api base url minus the http protocol
if (['nodes', 'package'].includes(import.meta.env.MODE)) {
const authToken = $authToken.get();
if (authToken) {
// TODO: handle providing jwt to socket.io
socketOptions.auth = { token: authToken };
}
socketOptions.transports = ['websocket', 'polling'];
}
return socketOptions;
};
const makeSocketUrl = (): string => {
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
let socketUrl = `${wsProtocol}://${window.location.host}`;
if (['nodes', 'package'].includes(import.meta.env.MODE)) {
const baseUrl = $baseUrl.get();
if (baseUrl) {
//eslint-disable-next-line
socketUrl = baseUrl.replace(/^https?\:\/\//i, '');
}
}
return socketUrl;
};
const makeSocket = (): Socket<ServerToClientEvents, ClientToServerEvents> => {
const socketOptions = makeSocketOptions();
const socketUrl = $socketUrl.get();
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
socketUrl,
{ ...socketOptions, ...$socketOptions.get() }
);
return socket;
};
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
export const $socketUrl = atom<string>(makeSocketUrl());
export const $isSocketInitialized = atom<boolean>(false);
/**
* Initializes the socket.io connection and sets up event listeners.
*/
export const useSocketIO = () => {
const dispatch = useAppDispatch();
const socketOptions = useStore($socketOptions);
const socketUrl = useStore($socketUrl);
const baseUrl = useStore($baseUrl);
const authToken = useStore($authToken);
useEffect(() => {
if ($isSocketInitialized.get()) {
// Singleton!
return;
}
const socket = makeSocket();
setEventListeners({ dispatch, socket });
socket.connect();
if ($isDebugging.get()) {
window.$socketOptions = $socketOptions;
window.$socketUrl = $socketUrl;
console.log('Socket initialized', socket);
}
$isSocketInitialized.set(true);
return () => {
if ($isDebugging.get()) {
window.$socketOptions = undefined;
window.$socketUrl = undefined;
console.log('Socket teardown', socket);
}
socket.disconnect();
$isSocketInitialized.set(false);
};
}, [dispatch, socketOptions, socketUrl, baseUrl, authToken]);
};

View File

@ -0,0 +1,6 @@
import { atom } from 'nanostores';
/**
* The user's auth token.
*/
export const $authToken = atom<string | undefined>();

View File

@ -0,0 +1,6 @@
import { atom } from 'nanostores';
/**
* The OpenAPI base url.
*/
export const $baseUrl = atom<string | undefined>();

View File

@ -0,0 +1,3 @@
import { atom } from 'nanostores';
export const $isDebugging = atom<boolean>(false);

View File

@ -0,0 +1,6 @@
import { atom } from 'nanostores';
/**
* The optional project-id header.
*/
export const $projectId = atom<string | undefined>();

View File

@ -12,14 +12,14 @@ import deleteImageModalReducer from 'features/deleteImageModal/store/slice';
import dynamicPromptsReducer from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
import loraReducer from 'features/lora/store/loraSlice';
import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice';
import nodesReducer from 'features/nodes/store/nodesSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import queueReducer from 'features/queue/store/queueSlice';
import sdxlReducer from 'features/sdxl/store/sdxlSlice';
import configReducer from 'features/system/store/configSlice';
import systemReducer from 'features/system/store/systemSlice';
import queueReducer from 'features/queue/store/queueSlice';
import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice';
import dynamicMiddlewares from 'redux-dynamic-middlewares';

View File

@ -1,6 +1,7 @@
import { Box, Flex, Heading } from '@chakra-ui/react';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
type ImageUploadOverlayProps = {
isDragAccept: boolean;
@ -9,6 +10,7 @@ type ImageUploadOverlayProps = {
};
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const { t } = useTranslation();
const {
isDragAccept,
isDragReject: _isDragAccept,
@ -76,11 +78,13 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
}}
>
{isDragAccept ? (
<Heading size="lg">Drop to Upload</Heading>
<Heading size="lg">{t('gallery.dropToUpload')}</Heading>
) : (
<>
<Heading size="lg">Invalid Upload</Heading>
<Heading size="md">Must be single JPEG or PNG image</Heading>
<Heading size="lg">{t('toast.invalidUpload')}</Heading>
<Heading size="md">
{t('toast.uploadFailedInvalidUploadDesc')}
</Heading>
</>
)}
</Flex>

View File

@ -1,5 +1,5 @@
import { $authToken } from 'app/store/nanostores/authToken';
import { useCallback } from 'react';
import { $authToken } from 'services/api/client';
/**
* Converts an image URL to a Blob by creating an <img /> element, drawing it to canvas

View File

@ -1,7 +1,7 @@
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { $authToken } from 'app/store/nanostores/authToken';
import { memo } from 'react';
import { Image } from 'react-konva';
import { $authToken } from 'services/api/client';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import useImage from 'use-image';
import { CanvasImage } from '../store/canvasTypes';

View File

@ -155,10 +155,10 @@ const IAICanvasMaskOptions = () => {
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
</Box>
<IAIButton size="sm" leftIcon={<FaSave />} onClick={handleSaveMask}>
Save Mask
{t('unifiedCanvas.saveMask')}
</IAIButton>
<IAIButton size="sm" leftIcon={<FaTrash />} onClick={handleClearMask}>
{t('unifiedCanvas.clearMask')} (Shift+C)
{t('unifiedCanvas.clearMask')}
</IAIButton>
</Flex>
</IAIPopover>

View File

@ -110,8 +110,10 @@ const ChangeBoardModal = () => {
<AlertDialogBody>
<Flex sx={{ flexDir: 'column', gap: 4 }}>
<Text>
Moving {`${imagesToChange.length}`} image
{`${imagesToChange.length > 1 ? 's' : ''}`} to board:
{t('boards.movingImagesToBoard', {
count: imagesToChange.length,
})}
:
</Text>
<IAIMantineSearchableSelect
placeholder={

View File

@ -124,10 +124,10 @@ const DeleteImageModal = () => {
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={handleClose}>
Cancel
{t('boards.cancel')}
</IAIButton>
<IAIButton colorScheme="error" onClick={handleDelete} ml={3}>
Delete
{t('controlnet.delete')}
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>

View File

@ -1,6 +1,7 @@
import { Box, ChakraProps, Flex, Heading, Image, Text } from '@chakra-ui/react';
import { memo } from 'react';
import { TypesafeDraggableData } from '../types';
import { useTranslation } from 'react-i18next';
type OverlayDragImageProps = {
dragData: TypesafeDraggableData | null;
@ -26,6 +27,7 @@ const STYLES: ChakraProps['sx'] = {
};
const DragPreview = (props: OverlayDragImageProps) => {
const { t } = useTranslation();
if (!props.dragData) {
return null;
}
@ -89,7 +91,7 @@ const DragPreview = (props: OverlayDragImageProps) => {
}}
>
<Heading>{props.dragData.payload.imageDTOs.length}</Heading>
<Heading size="sm">Images</Heading>
<Heading size="sm">{t('parameters.images')}</Heading>
</Flex>
);
}

View File

@ -121,7 +121,7 @@ const ParamEmbeddingPopover = (props: Props) => {
_dark: { color: 'base.700' },
}}
>
<Text>No Embeddings Loaded</Text>
<Text>{t('embedding.noEmbeddingsLoaded')}</Text>
</Flex>
) : (
<IAIMantineSearchableSelect

View File

@ -32,6 +32,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { BoardDTO } from 'services/api/types';
import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu';
import { useTranslation } from 'react-i18next';
interface GalleryBoardProps {
board: BoardDTO;
@ -143,7 +144,7 @@ const GalleryBoard = ({
const handleChange = useCallback((newBoardName: string) => {
setLocalBoardName(newBoardName);
}, []);
const { t } = useTranslation();
return (
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
<Flex
@ -292,7 +293,9 @@ const GalleryBoard = ({
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
dropLabel={
<Text fontSize="md">{t('unifiedCanvas.move')}</Text>
}
/>
</Flex>
</Tooltip>

View File

@ -19,6 +19,7 @@ import {
useGetBoardAssetsTotalQuery,
useGetBoardImagesTotalQuery,
} from 'services/api/endpoints/boards';
import { useTranslation } from 'react-i18next';
interface Props {
isSelected: boolean;
@ -71,7 +72,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
}),
[]
);
const { t } = useTranslation();
return (
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
<Flex
@ -161,7 +162,9 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
/>
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
dropLabel={
<Text fontSize="md">{t('unifiedCanvas.move')}</Text>
}
/>
</Flex>
</Tooltip>

View File

@ -35,6 +35,7 @@ import {
FaCode,
FaHourglassHalf,
FaQuoteRight,
FaRulerVertical,
FaSeedling,
} from 'react-icons/fa';
import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6';
@ -95,8 +96,12 @@ const CurrentImageButtons = () => {
const toaster = useAppToaster();
const { t } = useTranslation();
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
const {
recallBothPrompts,
recallSeed,
recallWidthAndHeight,
recallAllParameters,
} = useRecallParameters();
const { currentData: imageDTO } = useGetImageDTOQuery(
lastSelectedImage?.image_name ?? skipToken
@ -117,6 +122,8 @@ const CurrentImageButtons = () => {
dispatch(workflowLoadRequested(workflow));
}, [dispatch, workflow]);
useHotkeys('w', handleLoadWorkflow, [workflow]);
const handleClickUseAllParameters = useCallback(() => {
recallAllParameters(metadata);
}, [metadata, recallAllParameters]);
@ -127,7 +134,7 @@ const CurrentImageButtons = () => {
recallSeed(metadata?.seed);
}, [metadata?.seed, recallSeed]);
useHotkeys('s', handleUseSeed, [imageDTO]);
useHotkeys('s', handleUseSeed, [metadata]);
const handleUsePrompt = useCallback(() => {
recallBothPrompts(
@ -144,9 +151,13 @@ const CurrentImageButtons = () => {
recallBothPrompts,
]);
useHotkeys('p', handleUsePrompt, [imageDTO]);
useHotkeys('p', handleUsePrompt, [metadata]);
useHotkeys('w', handleLoadWorkflow, [workflow]);
const handleUseSize = useCallback(() => {
recallWidthAndHeight(metadata?.width, metadata?.height);
}, [metadata?.width, metadata?.height, recallWidthAndHeight]);
useHotkeys('d', handleUseSize, [metadata]);
const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img());
@ -267,6 +278,19 @@ const CurrentImageButtons = () => {
isDisabled={metadata?.seed === null || metadata?.seed === undefined}
onClick={handleUseSeed}
/>
<IAIIconButton
isLoading={isLoadingMetadata}
icon={<FaRulerVertical />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
isDisabled={
metadata?.height === null ||
metadata?.height === undefined ||
metadata?.width === null ||
metadata?.width === undefined
}
onClick={handleUseSize}
/>
<IAIIconButton
isLoading={isLoadingMetadata}
icon={<FaAsterisk />}

View File

@ -111,7 +111,7 @@ const MultipleSelectionMenuItems = () => {
icon={<FaTrash />}
onClickCapture={handleDeleteSelection}
>
Delete Selection
{t('gallery.deleteSelection')}
</MenuItem>
</>
);

View File

@ -113,7 +113,7 @@ const ImageGalleryContent = () => {
leftIcon={<FaImages />}
data-testid="images-tab"
>
{t('gallery.images')}
{t('parameters.images')}
</Tab>
<Tab
as={IAIButton}

View File

@ -49,7 +49,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
}}
>
<Flex gap={2}>
<Text fontWeight="semibold">File:</Text>
<Text fontWeight="semibold">{t('common.file')}:</Text>
<Link href={image.image_url} isExternal maxW="calc(100% - 3rem)">
{image.image_name}
<ExternalLinkIcon mx="2px" />

View File

@ -165,7 +165,7 @@ export default function FoundModelsList() {
},
}}
>
Installed
{t('common.installed')}
</Text>
)}
</Flex>
@ -215,7 +215,7 @@ export default function FoundModelsList() {
/>
<Flex p={2} gap={2}>
<Text sx={{ fontWeight: 600 }}>
Models Found: {foundModels.length}
{t('modelManager.modelsFound')}: {foundModels.length}
</Text>
<Text
sx={{
@ -226,7 +226,7 @@ export default function FoundModelsList() {
},
}}
>
Not Installed: {filteredModels.length}
{t('common.notInstalled')}: {filteredModels.length}
</Text>
</Flex>

View File

@ -76,7 +76,7 @@ function SearchFolderForm() {
_dark: { color: 'base.300' },
}}
>
Folder
{t('common.folder')}
</Text>
{!searchFolder ? (
<IAIInput

View File

@ -114,7 +114,7 @@ export default function CheckpointModelEdit(props: CheckpointModelEditProps) {
{model.model_name}
</Text>
<Text fontSize="sm" color="base.400">
{MODEL_TYPE_MAP[model.base_model]} Model
{MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')}
</Text>
</Flex>
{![''].includes(model.base_model) ? (
@ -128,7 +128,7 @@ export default function CheckpointModelEdit(props: CheckpointModelEditProps) {
_dark: { bg: 'error.400' },
}}
>
Conversion Not Supported
{t('modelManager.conversionNotSupported')}
</Badge>
)}
</Flex>

View File

@ -95,7 +95,7 @@ export default function DiffusersModelEdit(props: DiffusersModelEditProps) {
{model.model_name}
</Text>
<Text fontSize="sm" color="base.400">
{MODEL_TYPE_MAP[model.base_model]} Model
{MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')}
</Text>
</Flex>
<Divider />

View File

@ -95,8 +95,8 @@ export default function LoRAModelEdit(props: LoRAModelEditProps) {
{model.model_name}
</Text>
<Text fontSize="sm" color="base.400">
{MODEL_TYPE_MAP[model.base_model]} Model {' '}
{LORA_MODEL_FORMAT_MAP[model.model_format]} format
{MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')} {' '}
{LORA_MODEL_FORMAT_MAP[model.model_format]} {t('common.format')}
</Text>
</Flex>
<Divider />

View File

@ -10,6 +10,7 @@ import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { stateSelector } from 'app/store/store';
import { useTranslation } from 'react-i18next';
const selector = createSelector(stateSelector, ({ system, gallery }) => {
const imageDTO = gallery.selection[gallery.selection.length - 1];
@ -66,7 +67,7 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
const handleMouseLeave = useCallback(() => {
setIsHovering(false);
}, []);
const { t } = useTranslation();
return (
<NodeWrapper
nodeId={props.nodeProps.id}
@ -99,7 +100,7 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
_dark: { color: 'base.200' },
}}
>
Current Image
{t('nodes.currentImage')}
</Text>
</Flex>
<Flex

View File

@ -4,8 +4,10 @@ import { useEmbedWorkflow } from 'features/nodes/hooks/useEmbedWorkflow';
import { useWithWorkflow } from 'features/nodes/hooks/useWithWorkflow';
import { nodeEmbedWorkflowChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const withWorkflow = useWithWorkflow(nodeId);
const embedWorkflow = useEmbedWorkflow(nodeId);
@ -27,7 +29,9 @@ const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => {
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Workflow</FormLabel>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
{t('metadata.workflow')}
</FormLabel>
<Checkbox
className="nopan"
size="sm"

View File

@ -75,7 +75,7 @@ const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
const { status, progress, progressImage } = nodeExecutionState;
const { t } = useTranslation();
if (status === NodeStatus.PENDING) {
return <Text>Pending</Text>;
return <Text>{t('queue.pending')}</Text>;
}
if (status === NodeStatus.IN_PROGRESS) {
if (progressImage) {

View File

@ -3,6 +3,7 @@ import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { memo } from 'react';
import NodeCollapseButton from '../common/NodeCollapseButton';
import NodeWrapper from '../common/NodeWrapper';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
@ -19,6 +20,7 @@ const InvocationNodeUnknownFallback = ({
type,
selected,
}: Props) => {
const { t } = useTranslation();
return (
<NodeWrapper nodeId={nodeId} selected={selected}>
<Flex
@ -61,7 +63,7 @@ const InvocationNodeUnknownFallback = ({
}}
>
<Box>
<Text as="span">Unknown node type: </Text>
<Text as="span">{t('nodes.unknownNodeType')}: </Text>
<Text as="span" fontWeight={600}>
{type}
</Text>

View File

@ -4,8 +4,10 @@ import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const hasImageOutput = useHasImageOutput(nodeId);
const isIntermediate = useIsIntermediate(nodeId);
@ -27,7 +29,9 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save to Gallery</FormLabel>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
{t('hotkeys.saveToGallery.title')}
</FormLabel>
<Checkbox
className="nopan"
size="sm"

View File

@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { useUseCache } from 'features/nodes/hooks/useUseCache';
import { nodeUseCacheChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch();
@ -18,10 +19,12 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
},
[dispatch, nodeId]
);
const { t } = useTranslation();
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Use Cache</FormLabel>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
{t('invocationCache.useCache')}
</FormLabel>
<Checkbox
className="nopan"
size="sm"

View File

@ -79,7 +79,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
icon={<FaPlus />}
onClick={handleExposeField}
>
Add to Linear View
{t('nodes.addLinearView')}
</MenuItem>
);
}
@ -90,7 +90,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
icon={<FaMinus />}
onClick={handleUnexposeField}
>
Remove from Linear View
{t('nodes.removeLinearView')}
</MenuItem>
);
}
@ -102,6 +102,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
isExposed,
mayExpose,
nodeId,
t,
]);
const renderMenuFunc = useCallback(

View File

@ -49,8 +49,16 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
{fieldTemplate.description}
</Text>
)}
{fieldTemplate && <Text>Type: {FIELDS[fieldTemplate.type].title}</Text>}
{isInputTemplate && <Text>Input: {startCase(fieldTemplate.input)}</Text>}
{fieldTemplate && (
<Text>
{t('parameters.type')}: {FIELDS[fieldTemplate.type].title}
</Text>
)}
{isInputTemplate && (
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
)}
</Flex>
);
};

View File

@ -7,6 +7,7 @@ import EditableFieldTitle from './EditableFieldTitle';
import FieldContextMenu from './FieldContextMenu';
import FieldHandle from './FieldHandle';
import InputFieldRenderer from './InputFieldRenderer';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
@ -14,6 +15,7 @@ interface Props {
}
const InputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
@ -49,7 +51,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
<FormControl
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
>
Unknown input: {fieldName}
{t('nodes.unknownInput')}: {fieldName}
</FormControl>
</InputFieldWrapper>
);

View File

@ -18,6 +18,7 @@ import VaeModelInputField from './inputs/VaeModelInputField';
import IPAdapterModelInputField from './inputs/IPAdapterModelInputField';
import T2IAdapterModelInputField from './inputs/T2IAdapterModelInputField';
import BoardInputField from './inputs/BoardInputField';
import { useTranslation } from 'react-i18next';
type InputFieldProps = {
nodeId: string;
@ -25,11 +26,16 @@ type InputFieldProps = {
};
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
const { t } = useTranslation();
const field = useFieldData(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
if (fieldTemplate?.fieldKind === 'output') {
return <Box p={2}>Output field in input: {field?.type}</Box>;
return (
<Box p={2}>
{t('nodes.outputFieldInInput')}: {field?.type}
</Box>
);
}
if (
@ -249,7 +255,7 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
_dark: { color: 'error.300' },
}}
>
Unknown field type: {field?.type}
{t('nodes.unknownFieldType')}: {field?.type}
</Text>
</Box>
);

View File

@ -5,6 +5,7 @@ import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { PropsWithChildren, memo } from 'react';
import FieldHandle from './FieldHandle';
import FieldTooltipContent from './FieldTooltipContent';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
@ -12,6 +13,7 @@ interface Props {
}
const OutputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output');
const {
@ -28,7 +30,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
<FormControl
sx={{ color: 'error.400', textAlign: 'right', fontSize: 'sm' }}
>
Unknown output: {fieldName}
{t('nodes.unknownOutput')}: {fieldName}
</FormControl>
</OutputFieldWrapper>
);

View File

@ -16,6 +16,7 @@ import {
ImagePolymorphicInputFieldValue,
} from 'features/nodes/types/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUndo } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/types';
@ -103,18 +104,24 @@ const ImageInputFieldComponent = (
export default memo(ImageInputFieldComponent);
const UploadElement = memo(() => (
<Text fontSize={16} fontWeight={600}>
Drop or Upload
</Text>
));
const UploadElement = memo(() => {
const { t } = useTranslation();
return (
<Text fontSize={16} fontWeight={600}>
{t('gallery.dropOrUpload')}
</Text>
);
});
UploadElement.displayName = 'UploadElement';
const DropLabel = memo(() => (
<Text fontSize={16} fontWeight={600}>
Drop
</Text>
));
const DropLabel = memo(() => {
const { t } = useTranslation();
return (
<Text fontSize={16} fontWeight={600}>
{t('gallery.drop')}
</Text>
);
});
DropLabel.displayName = 'DropLabel';

View File

@ -91,7 +91,7 @@ const LoRAModelInputFieldComponent = (
return (
<Flex sx={{ justifyContent: 'center', p: 2 }}>
<Text sx={{ fontSize: 'sm', color: 'base.500', _dark: 'base.700' }}>
No LoRAs Loaded
{t('models.noLoRAsLoaded')}
</Text>
</Flex>
);

View File

@ -90,7 +90,7 @@ const Content = (props: {
<EditableNodeTitle nodeId={props.node.data.id} />
<HStack>
<FormControl>
<FormLabel>Node Type</FormLabel>
<FormLabel>{t('nodes.nodeType')}</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.template.title}
</Text>
@ -102,7 +102,7 @@ const Content = (props: {
w="full"
>
<FormControl isInvalid={needsUpdate}>
<FormLabel>Node Version</FormLabel>
<FormLabel>{t('nodes.nodeVersion')}</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{props.node.data.version}
</Text>

View File

@ -5,11 +5,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
import { FaUndo, FaUpload } from 'react-icons/fa';
import InitialImage from './InitialImage';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaRulerVertical, FaUndo, FaUpload } from 'react-icons/fa';
import { PostUploadAction } from 'services/api/types';
import InitialImage from './InitialImage';
const selector = createSelector(
[stateSelector],
@ -17,6 +20,7 @@ const selector = createSelector(
const { initialImage } = state.generation;
return {
isResetButtonDisabled: !initialImage,
initialImage,
};
},
defaultSelectorOptions
@ -27,7 +31,9 @@ const postUploadAction: PostUploadAction = {
};
const InitialImageDisplay = () => {
const { isResetButtonDisabled } = useAppSelector(selector);
const { recallWidthAndHeight } = useRecallParameters();
const { t } = useTranslation();
const { isResetButtonDisabled, initialImage } = useAppSelector(selector);
const dispatch = useAppDispatch();
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
@ -38,6 +44,14 @@ const InitialImageDisplay = () => {
dispatch(clearInitialImage());
}, [dispatch]);
const handleUseSizeInitialImage = useCallback(() => {
if (initialImage) {
recallWidthAndHeight(initialImage.width, initialImage.height);
}
}, [initialImage, recallWidthAndHeight]);
useHotkeys('shift+d', handleUseSizeInitialImage, [initialImage]);
return (
<Flex
layerStyle="first"
@ -73,7 +87,7 @@ const InitialImageDisplay = () => {
},
}}
>
Initial Image
{t('metadata.initImage')}
</Text>
<Spacer />
<IAIIconButton
@ -82,6 +96,13 @@ const InitialImageDisplay = () => {
icon={<FaUpload />}
{...getUploadButtonProps()}
/>
<IAIIconButton
tooltip={`${t('parameters.useSize')} (Shift+D)`}
aria-label={`${t('parameters.useSize')} (Shift+D)`}
icon={<FaRulerVertical />}
onClick={handleUseSizeInitialImage}
isDisabled={isResetButtonDisabled}
/>
<IAIIconButton
tooltip="Reset Initial Image"
aria-label="Reset Initial Image"

View File

@ -373,6 +373,26 @@ export const useRecallParameters = () => {
[dispatch, parameterSetToast, parameterNotSetToast]
);
/**
* Recall width and height with toast
*/
const recallWidthAndHeight = useCallback(
(width: unknown, height: unknown) => {
if (!isValidWidth(width)) {
allParameterNotSetToast();
return;
}
if (!isValidHeight(height)) {
allParameterNotSetToast();
return;
}
dispatch(setHeight(height));
dispatch(setWidth(width));
allParameterSetToast();
},
[dispatch, allParameterSetToast, allParameterNotSetToast]
);
/**
* Recall strength with toast
*/
@ -966,6 +986,7 @@ export const useRecallParameters = () => {
recallSteps,
recallWidth,
recallHeight,
recallWidthAndHeight,
recallStrength,
recallHrfEnabled,
recallHrfStrength,

View File

@ -106,7 +106,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
flexDir="column"
>
<Heading size="sm" color="error.500" _dark={{ color: 'error.400' }}>
Error
{t('common.error')}
</Heading>
<pre>{queueItem.error}</pre>
</Flex>

View File

@ -1,3 +1,5 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Image, Text } from '@chakra-ui/react';
import InvokeAILogoImage from 'assets/images/logo.png';
import { AnimatePresence, motion } from 'framer-motion';

View File

@ -423,7 +423,8 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Flex justifyContent="center">
<Text fontSize="lg">
<Text>
{t('settings.resetComplete')} Reloading in {countdown}...
{t('settings.resetComplete')} {t('settings.reloadingIn')}{' '}
{countdown}...
</Text>
</Text>
</Flex>

View File

@ -1,43 +0,0 @@
import { atom, computed } from 'nanostores';
import createClient from 'openapi-fetch';
import { paths } from 'services/api/schema';
/**
* We use nanostores to store the token and base url for very simple reactivity
*/
/**
* The user's auth token.
*/
export const $authToken = atom<string | undefined>();
/**
* The OpenAPI base url.
*/
export const $baseUrl = atom<string | undefined>();
/**
* The optional project-id header.
*/
export const $projectId = atom<string | undefined>();
/**
* Autogenerated, type-safe fetch client for the API. Used when RTK Query is not an option.
* Dynamically updates when the token or base url changes.
* Use `$client.get()` to get the client.
*
* @example
* const { get, post, del } = $client.get();
*/
export const $client = computed(
[$authToken, $baseUrl, $projectId],
(authToken, baseUrl, projectId) =>
createClient<paths>({
headers: {
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
...(projectId ? { 'project-id': projectId } : {}),
},
// do not include `api/v1` in the base url for this client
baseUrl: `${baseUrl ?? ''}`,
})
);

View File

@ -4,7 +4,7 @@ import {
ThunkDispatch,
createEntityAdapter,
} from '@reduxjs/toolkit';
import { $queueId } from 'features/queue/store/queueNanoStore';
import { $queueId } from 'app/store/nanostores/queueId';
import { listParamsReset } from 'features/queue/store/queueSlice';
import queryString from 'query-string';
import { ApiTagDescription, api } from '..';

View File

@ -6,7 +6,9 @@ import {
createApi,
fetchBaseQuery,
} from '@reduxjs/toolkit/query/react';
import { $authToken, $baseUrl, $projectId } from 'services/api/client';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $projectId } from 'app/store/nanostores/projectId';
export const tagTypes = [
'AppVersion',

File diff suppressed because one or more lines are too long

View File

@ -1,63 +0,0 @@
import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
import { AppThunkDispatch, RootState } from 'app/store/store';
import { $authToken, $baseUrl } from 'services/api/client';
import {
ClientToServerEvents,
ServerToClientEvents,
} from 'services/events/types';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { Socket, io } from 'socket.io-client';
export const socketMiddleware = () => {
let areListenersSet = false;
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
let socketUrl = `${wsProtocol}://${window.location.host}`;
const socketOptions: Parameters<typeof io>[0] = {
timeout: 60000,
path: '/ws/socket.io',
autoConnect: false, // achtung! removing this breaks the dynamic middleware
};
// if building in package mode, replace socket url with open api base url minus the http protocol
if (['nodes', 'package'].includes(import.meta.env.MODE)) {
const baseUrl = $baseUrl.get();
if (baseUrl) {
//eslint-disable-next-line
socketUrl = baseUrl.replace(/^https?\:\/\//i, '');
}
const authToken = $authToken.get();
if (authToken) {
// TODO: handle providing jwt to socket.io
socketOptions.auth = { token: authToken };
}
socketOptions.transports = ['websocket', 'polling'];
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
socketUrl,
socketOptions
);
const middleware: Middleware =
(storeApi: MiddlewareAPI<AppThunkDispatch, RootState>) =>
(next) =>
(action) => {
// Set listeners for `connect` and `disconnect` events once
// Must happen in middleware to get access to `dispatch`
if (!areListenersSet) {
setEventListeners({ storeApi, socket });
areListenersSet = true;
socket.connect();
}
next(action);
};
return middleware;
};

View File

@ -1,7 +1,5 @@
import { MiddlewareAPI } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { AppDispatch, RootState } from 'app/store/store';
import { $queueId } from 'features/queue/store/queueNanoStore';
import { $queueId } from 'app/store/nanostores/queueId';
import { AppDispatch } from 'app/store/store';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { Socket } from 'socket.io-client';
@ -23,20 +21,16 @@ import { ClientToServerEvents, ServerToClientEvents } from '../types';
type SetEventListenersArg = {
socket: Socket<ServerToClientEvents, ClientToServerEvents>;
storeApi: MiddlewareAPI<AppDispatch, RootState>;
dispatch: AppDispatch;
};
export const setEventListeners = (arg: SetEventListenersArg) => {
const { socket, storeApi } = arg;
const { dispatch } = storeApi;
const { socket, dispatch } = arg;
/**
* Connect
*/
socket.on('connect', () => {
const log = logger('socketio');
log.debug('Connected');
dispatch(socketConnected());
const queue_id = $queueId.get();
socket.emit('subscribe_queue', { queue_id });

View File

@ -3517,6 +3517,14 @@ eslint-config-prettier@^9.0.0:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
eslint-plugin-i18next@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-i18next/-/eslint-plugin-i18next-6.0.3.tgz#a388f9982deb040102c1c4498e4dc38d9bd6ffd9"
integrity sha512-RtQXYfg6PZCjejIQ/YG+dUj/x15jPhufJ9hUDGH0kCpJ6CkVMAWOQ9exU1CrbPmzeykxLjrXkjAaOZF/V7+DOA==
dependencies:
lodash "^4.17.21"
requireindex "~1.1.0"
eslint-plugin-react-hooks@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
@ -5028,23 +5036,11 @@ open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
openapi-fetch@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.8.1.tgz#a2bda1f72a8311e92cc789d1c8fec7b2d8ca28b6"
integrity sha512-xmzMaBCydPTMd0TKy4P2DYx/JOe9yjXtPIky1n1GV7nJJdZ3IZgSHvAWVbe06WsPD8EreR7E97IAiskPr6sa2g==
dependencies:
openapi-typescript-helpers "^0.0.4"
openapi-types@^12.1.3:
version "12.1.3"
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
openapi-typescript-helpers@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.4.tgz#ffe7c4868f094fcc8502dbdcddc6c32ce8011aee"
integrity sha512-Q0MTapapFAG993+dx8lNw33X6P/6EbFr31yNymJHq56fNc6dODyRm8tWyRnGxuC74lyl1iCRMV6nQCGQsfVNKg==
openapi-typescript@^6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-6.7.0.tgz#6d1a4dfc0db60b61573a3ea3c52984a79c638c67"
@ -5629,6 +5625,11 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
requireindex@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162"
integrity sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==
requirejs-config-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc"