diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index c7e7f4bc87..6d43d447f4 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -34,12 +34,9 @@ body: id: whatisexpected attributes: label: What should this feature add? - description: Please try to explain the functionality this feature should add + description: Explain the functionality this feature should add. Feature requests should be for single features. Please create multiple requests if you want to request multiple features. placeholder: | - Instead of one huge text field, it would be nice to have forms for bug-reports, feature-requests, ... - Great benefits with automatic labeling, assigning and other functionalitys not available in that form - via old-fashioned markdown-templates. I would also love to see the use of a moderator bot 🤖 like - https://github.com/marketplace/actions/issue-moderator-with-commands to auto close old issues and other things + I'd like a button that creates an image of banana sushi every time I press it. Each image should be different. There should be a toggle next to the button that enables strawberry mode, in which the images are of strawberry sushi instead. validations: required: true diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index fda7561679..11e5fc90a0 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -335,8 +335,8 @@ class ImageResizeInvocation(BaseInvocation): """Resizes an image to specific dimensions""" image: ImageField = InputField(description="The image to resize") - width: int = InputField(default=512, ge=64, multiple_of=8, description="The width to resize to (px)") - height: int = InputField(default=512, ge=64, multiple_of=8, description="The height to resize to (px)") + width: int = InputField(default=512, gt=0, description="The width to resize to (px)") + height: int = InputField(default=512, gt=0, description="The height to resize to (px)") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") metadata: Optional[CoreMetadata] = InputField( default=None, description=FieldDescriptions.core_metadata, ui_hidden=True diff --git a/invokeai/backend/util/hotfixes.py b/invokeai/backend/util/hotfixes.py index 983d0b7601..852d640161 100644 --- a/invokeai/backend/util/hotfixes.py +++ b/invokeai/backend/util/hotfixes.py @@ -772,11 +772,13 @@ diffusers.models.controlnet.ControlNetModel = ControlNetModel # NOTE: with this patch, torch.compile crashes on 2.0 torch(already fixed in nightly) # https://github.com/huggingface/diffusers/pull/4315 # https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/lora.py#L96C18-L96C18 -def new_LoRACompatibleConv_forward(self, x): +def new_LoRACompatibleConv_forward(self, hidden_states, scale: float = 1.0): if self.lora_layer is None: - return super(diffusers.models.lora.LoRACompatibleConv, self).forward(x) + return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states) else: - return super(diffusers.models.lora.LoRACompatibleConv, self).forward(x) + self.lora_layer(x) + return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states) + ( + scale * self.lora_layer(hidden_states) + ) diffusers.models.lora.LoRACompatibleConv.forward = new_LoRACompatibleConv_forward diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c1983c6a53..d846f3ca47 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -619,56 +619,174 @@ "addNodeToolTip": "Add Node (Shift+A, Space)", "animatedEdges": "Animated Edges", "animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes", + "boolean": "Booleans", + "booleanCollection": "Boolean Collection", + "booleanCollectionDescription": "A collection of booleans.", + "booleanDescription": "Booleans are true or false.", + "booleanPolymorphic": "Boolean Polymorphic", + "booleanPolymorphicDescription": "A collection of booleans.", "cannotConnectInputToInput": "Cannot connect input to input", "cannotConnectOutputToOutput": "Cannot connect output to output", "cannotConnectToSelf": "Cannot connect to self", + "clipField": "Clip", + "clipFieldDescription": "Tokenizer and text_encoder submodels.", + "collection": "Collection", + "collectionDescription": "TODO", + "collectionItem": "Collection Item", + "collectionItemDescription": "TODO", "colorCodeEdges": "Color-Code Edges", "colorCodeEdgesHelp": "Color-code edges according to their connected fields", + "colorCollectionDescription": "A collection of colors.", + "colorField": "Color", + "colorFieldDescription": "A RGBA color.", + "colorPolymorphic": "Color Polymorphic", + "colorPolymorphicDescription": "A collection of colors.", + "conditioningCollection": "Conditioning Collection", + "conditioningCollectionDescription": "Conditioning may be passed between nodes.", + "conditioningField": "Conditioning", + "conditioningFieldDescription": "Conditioning may be passed between nodes.", + "conditioningPolymorphic": "Conditioning Polymorphic", + "conditioningPolymorphicDescription": "Conditioning may be passed between nodes.", "connectionWouldCreateCycle": "Connection would create a cycle", + "controlCollection": "Control Collection", + "controlCollectionDescription": "Control info passed between nodes.", + "controlField": "Control", + "controlFieldDescription": "Control info passed between nodes.", "currentImage": "Current Image", "currentImageDescription": "Displays the current image in the Node Editor", + "denoiseMaskField": "Denoise Mask", + "denoiseMaskFieldDescription": "Denoise Mask may be passed between nodes", + "doesNotExist": "does not exist", "downloadWorkflow": "Download Workflow JSON", + "edge": "Edge", + "enum": "Enum", + "enumDescription": "Enums are values that may be one of a number of options.", + "executionStateCompleted": "Completed", + "executionStateError": "Error", + "executionStateInProgress": "In Progress", "fieldTypesMustMatch": "Field types must match", "fitViewportNodes": "Fit View", + "float": "Float", + "floatCollection": "Float Collection", + "floatCollectionDescription": "A collection of floats.", + "floatDescription": "Floats are numbers with a decimal point.", + "floatPolymorphic": "Float Polymorphic", + "floatPolymorphicDescription": "A collection of floats.", "fullyContainNodes": "Fully Contain Nodes to Select", "fullyContainNodesHelp": "Nodes must be fully inside the selection box to be selected", "hideGraphNodes": "Hide Graph Overlay", "hideLegendNodes": "Hide Field Type Legend", "hideMinimapnodes": "Hide MiniMap", + "imageCollection": "Image Collection", + "imageCollectionDescription": "A collection of images.", + "imageField": "Image", + "imageFieldDescription": "Images may be passed between nodes.", + "imagePolymorphic": "Image Polymorphic", + "imagePolymorphicDescription": "A collection of images.", + "inputFields": "Input Feilds", "inputMayOnlyHaveOneConnection": "Input may only have one connection", + "inputNode": "Input Node", + "integer": "Integer", + "integerCollection": "Integer Collection", + "integerCollectionDescription": "A collection of integers.", + "integerDescription": "Integers are whole numbers, without a decimal point.", + "integerPolymorphic": "Integer Polymorphic", + "integerPolymorphicDescription": "A collection of integers.", + "invalidOutputSchema": "Invalid output schema", + "latentsCollection": "Latents Collection", + "latentsCollectionDescription": "Latents may be passed between nodes.", + "latentsField": "Latents", + "latentsFieldDescription": "Latents may be passed between nodes.", + "latentsPolymorphic": "Latents Polymorphic", + "latentsPolymorphicDescription": "Latents may be passed between nodes.", "loadingNodes": "Loading Nodes...", "loadWorkflow": "Load Workflow", + "loRAModelField": "LoRA", + "loRAModelFieldDescription": "TODO", + "mainModelField": "Model", + "mainModelFieldDescription": "TODO", + "maybeIncompatible": "May be Incompatible With Installed", + "mismatchedVersion": "Has Mismatched Version", + "missingCanvaInitImage": "Missing canvas init image", + "missingCanvaInitMaskImages": "Missing canvas init and mask images", + "missingTemplate": "Missing Template", "noConnectionData": "No connection data", "noConnectionInProgress": "No connection in progress", + "node": "Node", "nodeOutputs": "Node Outputs", "nodeSearch": "Search for nodes", "nodeTemplate": "Node Template", + "nodeType": "Node Type", "noFieldsLinearview": "No fields added to Linear View", "noFieldType": "No field type", + "noImageFoundState": "No initial image found in state", "noMatchingNodes": "No matching nodes", "noNodeSelected": "No node selected", "noOpacity": "Node Opacity", "noOutputRecorded": "No outputs recorded", + "noOutputSchemaName": "No output schema name found in ref object", "notes": "Notes", "notesDescription": "Add notes about your workflow", + "oNNXModelField": "ONNX Model", + "oNNXModelFieldDescription": "ONNX model field.", + "outputFields": "Output Feilds", + "outputNode": "Output node", + "outputSchemaNotFound": "Output schema not found", "pickOne": "Pick One", + "problemReadingMetadata": "Problem reading metadata from image", + "problemReadingWorkflow": "Problem reading workflow from image", "problemSettingTitle": "Problem Setting Title", "reloadNodeTemplates": "Reload Node Templates", "removeLinearView": "Remove from Linear View", "resetWorkflow": "Reset Workflow", "resetWorkflowDesc": "Are you sure you want to reset this workflow?", "resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.", + "scheduler": "Scheduler", + "schedulerDescription": "TODO", + "sDXLMainModelField": "SDXL Model", + "sDXLMainModelFieldDescription": "SDXL model field.", + "sDXLRefinerModelField": "Refiner Model", + "sDXLRefinerModelFieldDescription": "TODO", "showGraphNodes": "Show Graph Overlay", "showLegendNodes": "Show Field Type Legend", "showMinimapnodes": "Show MiniMap", + "skipped": "Skipped", + "skippedReservedInput": "Skipped reserved input field", + "skippedReservedOutput": "Skipped reserved output field", + "skippingInputNoTemplate": "Skipping input field with no template", + "skippingReservedFieldType": "Skipping reserved field type", + "skippingUnknownInputType": "Skipping unknown input field type", + "skippingUnknownOutputType": "Skipping unknown output field type", "snapToGrid": "Snap to Grid", "snapToGridHelp": "Snap nodes to grid when moved", + "sourceNode": "Source node", + "string": "String", + "stringCollection": "String Collection", + "stringCollectionDescription": "A collection of strings.", + "stringDescription": "Strings are text.", + "stringPolymorphic": "String Polymorphic", + "stringPolymorphicDescription": "A collection of strings.", "unableToLoadWorkflow": "Unable to Validate Workflow", + "unableToParseEdge": "Unable to parse edge", + "unableToParseNode": "Unable to parse node", "unableToValidateWorkflow": "Unable to Validate Workflow", + "uNetField": "UNet", + "uNetFieldDescription": "UNet submodel.", + "unhandledInputProperty": "Unhandled input property", + "unhandledOutputProperty": "Unhandled output property", "unknownField": "Unknown Field", + "unknownNode": "Unknown Node", + "unknownTemplate": "Unknown Template", "unkownInvocation": "Unknown Invocation type", + "updateApp": "Update App", + "vaeField": "Vae", + "vaeFieldDescription": "Vae submodel.", + "vaeModelField": "VAE", + "vaeModelFieldDescription": "TODO", "validateConnections": "Validate Connections and Graph", "validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked", + "version": "Version", + "versionUnknown": " Version Unknown", "workflow": "Workflow", "workflowAuthor": "Author", "workflowContact": "Contact", @@ -680,15 +798,7 @@ "workflowValidation": "Workflow Validation Error", "workflowVersion": "Version", "zoomInNodes": "Zoom In", - "zoomOutNodes": "Zoom Out", - "executionStateError": "Error", - "executionStateCompleted": "Completed", - "executionStateInProgress": "In Progress", - "versionUnknown": " Version Unknown", - "unknownNode": "Unknown Node", - "version": "Version", - "updateApp": "Update App", - "unknownTemplate": "Unknown Template" + "zoomOutNodes": "Zoom Out" }, "parameters": { "aspectRatio": "Ratio", @@ -853,8 +963,14 @@ "useSlidersForAll": "Use Sliders For All Options" }, "toast": { + "addedToBoard": "Added to board", + "baseModelChangedCleared": "Base model changed, cleared", "canceled": "Processing Canceled", + "canvasCopiedClipboard": "Canvas Copied to Clipboard", + "canvasDownloaded": "Canvas Downloaded", "canvasMerged": "Canvas Merged", + "canvasSavedGallery": "Canvas Saved to Gallery", + "canvasSentControlnetAssets": "Canvas Sent to ControlNet & Assets", "connected": "Connected to Server", "disconnected": "Disconnected from Server", "downloadImageStarted": "Image Download Started", @@ -863,10 +979,18 @@ "imageLinkCopied": "Image Link Copied", "imageNotLoaded": "No Image Loaded", "imageNotLoadedDesc": "Could not find image", + "imageSaved": "Image Saved", "imageSavedToGallery": "Image Saved to Gallery", + "imageSavingFailed": "Image Saving Failed", + "imageUploaded": "Image Uploaded", + "imageUploadFailed": "Image Upload Failed", + "incompatibleSubmodel": "incompatible submodel", "initialImageNotSet": "Initial Image Not Set", "initialImageNotSetDesc": "Could not load initial image", "initialImageSet": "Initial Image Set", + "loadedWithWarnings": "Workflow Loaded with Warnings", + "maskSavedAssets": "Mask Saved to Assets", + "maskSentControlnetAssets": "Mask Sent to ControlNet & Assets", "metadataLoadFailed": "Failed to load metadata", "modelAdded": "Model Added: {{modelName}}", "modelAddedSimple": "Model Added", @@ -887,8 +1011,20 @@ "parametersNotSet": "Parameters Not Set", "parametersNotSetDesc": "No metadata found for this image.", "parametersSet": "Parameters Set", + "problemCopyingCanvas": "Problem Copying Canvas", + "problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingImage": "Unable to Copy Image", "problemCopyingImageLink": "Unable to Copy Image Link", + "problemDownloadingCanvas": "Problem Downloading Canvas", + "problemDownloadingCanvasDesc": "Unable to export base layer", + "problemImportingMask": "Problem Importing Mask", + "problemImportingMaskDesc": "Unable to export mask", + "problemMergingCanvas": "Problem Merging Canvas", + "problemMergingCanvasDesc": "Unable to export base layer", + "problemSavingCanvas": "Problem Saving Canvas", + "problemSavingCanvasDesc": "Unable to export base layer", + "problemSavingMask": "Problem Saving Mask", + "problemSavingMaskDesc": "Unable to export mask", "promptNotSet": "Prompt Not Set", "promptNotSetDesc": "Could not find prompt for this image.", "promptSet": "Prompt Set", @@ -898,11 +1034,16 @@ "sentToImageToImage": "Sent To Image To Image", "sentToUnifiedCanvas": "Sent to Unified Canvas", "serverError": "Server Error", + "setCanvasInitialImage": "Set as canvas initial image", + "setControlImage": "Set as control image", + "setInitialImage": "Set as initial image", + "setNodeField": "Set as node field", "tempFoldersEmptied": "Temp Folder Emptied", "uploadFailed": "Upload failed", "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", "uploadFailedUnableToLoadDesc": "Unable to load file", - "upscalingFailed": "Upscaling Failed" + "upscalingFailed": "Upscaling Failed", + "workflowLoaded": "Workflow Loaded" }, "tooltip": { "feature": { diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index aa1919abfb..8c033440e3 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -12,33 +12,26 @@ import { languageSelector } from 'features/system/store/systemSelectors'; import InvokeTabs from 'features/ui/components/InvokeTabs'; import i18n from 'i18n'; import { size } from 'lodash-es'; -import { ReactNode, memo, useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { usePreselectedImage } from '../../features/parameters/hooks/usePreselectedImage'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; -import { CustomStarUi } from '../../features/ui/store/uiTypes'; -import { setCustomStarUi } from '../../features/ui/store/uiSlice'; +import { useStore } from '@nanostores/react'; +import { $headerComponent } from 'app/store/nanostores/headerComponent'; const DEFAULT_CONFIG = {}; interface Props { config?: PartialAppConfig; - headerComponent?: ReactNode; selectedImage?: { imageName: string; action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; }; - customStarUi?: CustomStarUi; } -const App = ({ - config = DEFAULT_CONFIG, - headerComponent, - selectedImage, - customStarUi, -}: Props) => { +const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { const language = useAppSelector(languageSelector); const logger = useLogger('system'); @@ -61,12 +54,6 @@ const App = ({ } }, [dispatch, config, logger]); - useEffect(() => { - if (customStarUi) { - dispatch(setCustomStarUi(customStarUi)); - } - }, [customStarUi, dispatch]); - useEffect(() => { dispatch(appStarted()); }, [dispatch]); @@ -75,6 +62,8 @@ const App = ({ handlePreselectedImage(selectedImage); }, [handlePreselectedImage, selectedImage]); + const headerComponent = useStore($headerComponent); + return ( import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); @@ -83,18 +84,33 @@ const InvokeAIUI = ({ }; }, [apiUrl, token, middleware, projectId]); + useEffect(() => { + if (customStarUi) { + $customStarUI.set(customStarUi); + } + + return () => { + $customStarUI.set(undefined); + }; + }, [customStarUi]); + + useEffect(() => { + if (headerComponent) { + $headerComponent.set(headerComponent); + } + + return () => { + $headerComponent.set(undefined); + }; + }, [headerComponent]); + return ( }> - + diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts index 9e66d1bdb8..c328aceedf 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts @@ -4,6 +4,7 @@ import { $logger } from 'app/logging/logger'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard'; +import { t } from 'i18next'; export const addCanvasCopiedToClipboardListener = () => { startAppListening({ @@ -20,8 +21,8 @@ export const addCanvasCopiedToClipboardListener = () => { moduleLog.error('Problem getting base layer blob'); dispatch( addToast({ - title: 'Problem Copying Canvas', - description: 'Unable to export base layer', + title: t('toast.problemCopyingCanvas'), + description: t('toast.problemCopyingCanvasDesc'), status: 'error', }) ); @@ -32,7 +33,7 @@ export const addCanvasCopiedToClipboardListener = () => { dispatch( addToast({ - title: 'Canvas Copied to Clipboard', + title: t('toast.canvasCopiedClipboard'), status: 'success', }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts index b101d00541..23faf4a356 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts @@ -4,6 +4,7 @@ import { $logger } from 'app/logging/logger'; import { downloadBlob } from 'features/canvas/util/downloadBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; export const addCanvasDownloadedAsImageListener = () => { startAppListening({ @@ -20,8 +21,8 @@ export const addCanvasDownloadedAsImageListener = () => { moduleLog.error('Problem getting base layer blob'); dispatch( addToast({ - title: 'Problem Downloading Canvas', - description: 'Unable to export base layer', + title: t('toast.problemDownloadingCanvas'), + description: t('toast.problemDownloadingCanvasDesc'), status: 'error', }) ); @@ -29,7 +30,9 @@ export const addCanvasDownloadedAsImageListener = () => { } downloadBlob(blob, 'canvas.png'); - dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' })); + dispatch( + addToast({ title: t('toast.canvasDownloaded'), status: 'success' }) + ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts index fb411a6e25..5181df134f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts @@ -5,6 +5,7 @@ import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlic import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addCanvasImageToControlNetListener = () => { startAppListening({ @@ -19,8 +20,8 @@ export const addCanvasImageToControlNetListener = () => { log.error('Problem getting base layer blob'); dispatch( addToast({ - title: 'Problem Saving Canvas', - description: 'Unable to export base layer', + title: t('toast.problemSavingCanvas'), + description: t('toast.problemSavingCanvasDesc'), status: 'error', }) ); @@ -40,7 +41,7 @@ export const addCanvasImageToControlNetListener = () => { crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: 'Canvas Sent to ControlNet & Assets' }, + toastOptions: { title: t('toast.canvasSentControlnetAssets') }, }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts index e701b93352..f814d94f3a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts @@ -4,6 +4,7 @@ import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addCanvasMaskSavedToGalleryListener = () => { startAppListening({ @@ -30,8 +31,8 @@ export const addCanvasMaskSavedToGalleryListener = () => { log.error('Problem getting mask layer blob'); dispatch( addToast({ - title: 'Problem Saving Mask', - description: 'Unable to export mask', + title: t('toast.problemSavingMask'), + description: t('toast.problemSavingMaskDesc'), status: 'error', }) ); @@ -51,7 +52,7 @@ export const addCanvasMaskSavedToGalleryListener = () => { crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: 'Mask Saved to Assets' }, + toastOptions: { title: t('toast.maskSavedAssets') }, }, }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts index 6c97259f02..671c7f63e4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts @@ -5,6 +5,7 @@ import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlic import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addCanvasMaskToControlNetListener = () => { startAppListening({ @@ -31,8 +32,8 @@ export const addCanvasMaskToControlNetListener = () => { log.error('Problem getting mask layer blob'); dispatch( addToast({ - title: 'Problem Importing Mask', - description: 'Unable to export mask', + title: t('toast.problemImportingMask'), + description: t('toast.problemImportingMaskDesc'), status: 'error', }) ); @@ -52,7 +53,7 @@ export const addCanvasMaskToControlNetListener = () => { crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: 'Mask Sent to ControlNet & Assets' }, + toastOptions: { title: t('toast.maskSentControlnetAssets') }, }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index 21c506242d..62f7b60036 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -6,6 +6,7 @@ import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addCanvasMergedListener = () => { startAppListening({ @@ -20,8 +21,8 @@ export const addCanvasMergedListener = () => { moduleLog.error('Problem getting base layer blob'); dispatch( addToast({ - title: 'Problem Merging Canvas', - description: 'Unable to export base layer', + title: t('toast.problemMergingCanvas'), + description: t('toast.problemMergingCanvasDesc'), status: 'error', }) ); @@ -34,8 +35,8 @@ export const addCanvasMergedListener = () => { moduleLog.error('Problem getting canvas base layer'); dispatch( addToast({ - title: 'Problem Merging Canvas', - description: 'Unable to export base layer', + title: t('toast.problemMergingCanvas'), + description: t('toast.problemMergingCanvasDesc'), status: 'error', }) ); @@ -55,7 +56,7 @@ export const addCanvasMergedListener = () => { is_intermediate: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: 'Canvas Merged' }, + toastOptions: { title: t('toast.canvasMerged') }, }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index dbadb72a52..0bb8ad8550 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -4,6 +4,7 @@ import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addCanvasSavedToGalleryListener = () => { startAppListening({ @@ -18,8 +19,8 @@ export const addCanvasSavedToGalleryListener = () => { log.error('Problem getting base layer blob'); dispatch( addToast({ - title: 'Problem Saving Canvas', - description: 'Unable to export base layer', + title: t('toast.problemSavingCanvas'), + description: t('toast.problemSavingCanvasDesc'), status: 'error', }) ); @@ -39,7 +40,7 @@ export const addCanvasSavedToGalleryListener = () => { crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: 'Canvas Saved to Gallery' }, + toastOptions: { title: t('toast.canvasSavedGallery') }, }, }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 0c55908748..2cc406469b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -9,9 +9,10 @@ import { omit } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { startAppListening } from '..'; import { imagesApi } from '../../../../../services/api/endpoints/images'; +import { t } from 'i18next'; const DEFAULT_UPLOADED_TOAST: UseToastOptions = { - title: 'Image Uploaded', + title: t('toast.imageUploaded'), status: 'success', }; @@ -57,8 +58,8 @@ export const addImageUploadedFulfilledListener = () => { // Fall back to just the board id if we can't find the board for some reason const board = data?.find((b) => b.board_id === autoAddBoardId); const description = board - ? `Added to board ${board.board_name}` - : `Added to board ${autoAddBoardId}`; + ? `${t('toast.addedToBoard')} ${board.board_name}` + : `${t('toast.addedToBoard')} ${autoAddBoardId}`; dispatch( addToast({ @@ -75,7 +76,7 @@ export const addImageUploadedFulfilledListener = () => { dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: 'Set as canvas initial image', + description: t('toast.setCanvasInitialImage'), }) ); return; @@ -92,7 +93,7 @@ export const addImageUploadedFulfilledListener = () => { dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: 'Set as control image', + description: t('toast.setControlImage'), }) ); return; @@ -103,7 +104,7 @@ export const addImageUploadedFulfilledListener = () => { dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: 'Set as initial image', + description: t('toast.setInitialImage'), }) ); return; @@ -117,7 +118,7 @@ export const addImageUploadedFulfilledListener = () => { dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: `Set as node field ${fieldName}`, + description: `${t('toast.setNodeField')} ${fieldName}`, }) ); return; @@ -140,7 +141,7 @@ export const addImageUploadedRejectedListener = () => { log.error({ ...sanitizedData }, 'Image upload failed'); dispatch( addToast({ - title: 'Image Upload Failed', + title: t('toast.imageUploadFailed'), description: action.error.message, status: 'error', }) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 240890f043..eb25b22293 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -14,6 +14,7 @@ import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { forEach } from 'lodash-es'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addModelSelectedListener = () => { startAppListening({ @@ -67,7 +68,9 @@ export const addModelSelectedListener = () => { dispatch( addToast( makeToast({ - title: `Base model changed, cleared ${modelsCleared} incompatible submodel${ + title: `${t( + 'toast.baseModelChangedCleared' + )} ${modelsCleared} ${t('toast.incompatibleSubmodel')}${ modelsCleared === 1 ? '' : 's' }`, status: 'warning', diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts index b14e18ea63..c00cf78beb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts @@ -2,6 +2,7 @@ import { stagingAreaImageSaved } from 'features/canvas/store/actions'; import { addToast } from 'features/system/store/systemSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addStagingAreaImageSavedListener = () => { startAppListening({ @@ -28,11 +29,11 @@ export const addStagingAreaImageSavedListener = () => { }) ); } - dispatch(addToast({ title: 'Image Saved', status: 'success' })); + dispatch(addToast({ title: t('toast.imageSaved'), status: 'success' })); } catch (error) { dispatch( addToast({ - title: 'Image Saving Failed', + title: t('toast.imageSavingFailed'), description: (error as Error)?.message, status: 'error', }) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts index c447720941..de697a70e5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts @@ -7,6 +7,7 @@ import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { startAppListening } from '..'; +import { t } from 'i18next'; export const addWorkflowLoadedListener = () => { startAppListening({ @@ -27,7 +28,7 @@ export const addWorkflowLoadedListener = () => { dispatch( addToast( makeToast({ - title: 'Workflow Loaded', + title: t('toast.workflowLoaded'), status: 'success', }) ) @@ -36,7 +37,7 @@ export const addWorkflowLoadedListener = () => { dispatch( addToast( makeToast({ - title: 'Workflow Loaded with Warnings', + title: t('toast.loadedWithWarnings'), status: 'warning', }) ) diff --git a/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts b/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts new file mode 100644 index 0000000000..0459c2f31f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts @@ -0,0 +1,14 @@ +import { MenuItemProps } from '@chakra-ui/react'; +import { atom } from 'nanostores'; + +export type CustomStarUi = { + on: { + icon: MenuItemProps['icon']; + text: string; + }; + off: { + icon: MenuItemProps['icon']; + text: string; + }; +}; +export const $customStarUI = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/headerComponent.ts b/invokeai/frontend/web/src/app/store/nanostores/headerComponent.ts new file mode 100644 index 0000000000..90a4775ff9 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/headerComponent.ts @@ -0,0 +1,4 @@ +import { atom } from 'nanostores'; +import { ReactNode } from 'react'; + +export const $headerComponent = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/index.ts b/invokeai/frontend/web/src/app/store/nanostores/index.ts new file mode 100644 index 0000000000..ae43ed3035 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/index.ts @@ -0,0 +1,3 @@ +/** + * For non-serializable data that needs to be available throughout the app, or when redux is not appropriate, use nanostores. + */ diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index ce2a21c6e7..f84f3dd9c7 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -86,10 +86,7 @@ export const store = configureStore({ .concat(autoBatchEnhancer()); }, middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - immutableCheck: false, - serializableCheck: false, - }) + getDefaultMiddleware({ immutableCheck: false }) .concat(api.middleware) .concat(dynamicMiddlewares) .prepend(listenerMiddleware.middleware), diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx index 4f9e47282d..4a67898942 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx @@ -154,6 +154,8 @@ const IAICanvas = () => { resizeObserver.observe(containerRef.current); + dispatch(canvasResized(containerRef.current.getBoundingClientRect())); + return () => { resizeObserver.disconnect(); }; diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts b/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts index 954c36869c..255cb2850b 100644 --- a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts +++ b/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts @@ -8,7 +8,7 @@ const calculateScale = ( const scaleX = (containerWidth * padding) / contentWidth; const scaleY = (containerHeight * padding) / contentHeight; const scaleFit = Math.min(1, Math.min(scaleX, scaleY)); - return scaleFit; + return scaleFit ? scaleFit : 1; }; export default calculateScale; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx index e2a9013aac..29b45761ee 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx @@ -1,4 +1,6 @@ import { MenuItem } from '@chakra-ui/react'; +import { useStore } from '@nanostores/react'; +import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imagesToChangeSelected, @@ -12,12 +14,11 @@ import { useStarImagesMutation, useUnstarImagesMutation, } from '../../../../services/api/endpoints/images'; -import { uiSelector } from '../../../ui/store/uiSelectors'; const MultipleSelectionMenuItems = () => { const dispatch = useAppDispatch(); const selection = useAppSelector((state) => state.gallery.selection); - const { customStarUi } = useAppSelector(uiSelector); + const customStarUi = useStore($customStarUI); const [starImages] = useStarImagesMutation(); const [unstarImages] = useUnstarImagesMutation(); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 68c5c2e1ae..c50b0c13dd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,5 +1,7 @@ import { Flex, MenuItem, Spinner } from '@chakra-ui/react'; +import { useStore } from '@nanostores/react'; import { useAppToaster } from 'app/components/Toaster'; +import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { @@ -7,6 +9,7 @@ import { isModalOpenChanged, } from 'features/changeBoardModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; @@ -32,10 +35,9 @@ import { useUnstarImagesMutation, } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; -import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; -import { workflowLoadRequested } from 'features/nodes/store/actions'; import { configSelector } from '../../../system/store/configSelectors'; -import { uiSelector } from '../../../ui/store/uiSelectors'; +import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; +import { flushSync } from 'react-dom'; type SingleSelectionMenuItemsProps = { imageDTO: ImageDTO; @@ -51,7 +53,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); - const { customStarUi } = useAppSelector(uiSelector); + const customStarUi = useStore($customStarUI); const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery( { image: imageDTO, shouldFetchMetadataFromApi }, @@ -114,8 +116,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToCanvas = useCallback(() => { dispatch(sentImageToCanvas()); + flushSync(() => { + dispatch(setActiveTab('unifiedCanvas')); + }); dispatch(setInitialCanvasImage(imageDTO)); - dispatch(setActiveTab('unifiedCanvas')); toaster({ title: t('toast.sentToUnifiedCanvas'), diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 6c34bc31b8..af01eeaea8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -1,4 +1,6 @@ import { Box, Flex } from '@chakra-ui/react'; +import { useStore } from '@nanostores/react'; +import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; @@ -10,6 +12,7 @@ import { } from 'features/dnd/types'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { MouseEvent, memo, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { FaTrash } from 'react-icons/fa'; import { MdStar, MdStarBorder } from 'react-icons/md'; import { @@ -18,8 +21,6 @@ import { useUnstarImagesMutation, } from 'services/api/endpoints/images'; import IAIDndImageIcon from '../../../../common/components/IAIDndImageIcon'; -import { uiSelector } from '../../../ui/store/uiSelectors'; -import { useTranslation } from 'react-i18next'; interface HoverableImageProps { imageName: string; @@ -35,7 +36,7 @@ const GalleryImage = (props: HoverableImageProps) => { const { handleClick, isSelected, selection, selectionCount } = useMultiselect(imageDTO); - const { customStarUi } = useAppSelector(uiSelector); + const customStarUi = useStore($customStarUI); const handleDelete = useCallback( (e: MouseEvent) => { diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index a12c1fbddc..0ab10db159 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -1,4 +1,5 @@ import { FieldType, FieldUIConfig } from './types'; +import { t } from 'i18next'; export const HANDLE_TOOLTIP_OPEN_DELAY = 500; export const COLOR_TOKEN_VALUE = 500; @@ -102,73 +103,73 @@ export const isPolymorphicItemType = ( export const FIELDS: Record = { boolean: { color: 'green.500', - description: 'Booleans are true or false.', - title: 'Boolean', + description: t('nodes.booleanDescription'), + title: t('nodes.boolean'), }, BooleanCollection: { color: 'green.500', - description: 'A collection of booleans.', - title: 'Boolean Collection', + description: t('nodes.booleanCollectionDescription'), + title: t('nodes.booleanCollection'), }, BooleanPolymorphic: { color: 'green.500', - description: 'A collection of booleans.', - title: 'Boolean Polymorphic', + description: t('nodes.booleanPolymorphicDescription'), + title: t('nodes.booleanPolymorphic'), }, ClipField: { color: 'green.500', - description: 'Tokenizer and text_encoder submodels.', - title: 'Clip', + description: t('nodes.clipFieldDescription'), + title: t('nodes.clipField'), }, Collection: { color: 'base.500', - description: 'TODO', - title: 'Collection', + description: t('nodes.collectionDescription'), + title: t('nodes.collection'), }, CollectionItem: { color: 'base.500', - description: 'TODO', - title: 'Collection Item', + description: t('nodes.collectionItemDescription'), + title: t('nodes.collectionItem'), }, ColorCollection: { color: 'pink.300', - description: 'A collection of colors.', - title: 'Color Collection', + description: t('nodes.colorCollectionDescription'), + title: t('nodes.colorCollection'), }, ColorField: { color: 'pink.300', - description: 'A RGBA color.', - title: 'Color', + description: t('nodes.colorFieldDescription'), + title: t('nodes.colorField'), }, ColorPolymorphic: { color: 'pink.300', - description: 'A collection of colors.', - title: 'Color Polymorphic', + description: t('nodes.colorPolymorphicDescription'), + title: t('nodes.colorPolymorphic'), }, ConditioningCollection: { color: 'cyan.500', - description: 'Conditioning may be passed between nodes.', - title: 'Conditioning Collection', + description: t('nodes.conditioningCollectionDescription'), + title: t('nodes.conditioningCollection'), }, ConditioningField: { color: 'cyan.500', - description: 'Conditioning may be passed between nodes.', - title: 'Conditioning', + description: t('nodes.conditioningFieldDescription'), + title: t('nodes.conditioningField'), }, ConditioningPolymorphic: { color: 'cyan.500', - description: 'Conditioning may be passed between nodes.', - title: 'Conditioning Polymorphic', + description: t('nodes.conditioningPolymorphicDescription'), + title: t('nodes.conditioningPolymorphic'), }, ControlCollection: { color: 'teal.500', - description: 'Control info passed between nodes.', - title: 'Control Collection', + description: t('nodes.controlCollectionDescription'), + title: t('nodes.controlCollection'), }, ControlField: { color: 'teal.500', - description: 'Control info passed between nodes.', - title: 'Control', + description: t('nodes.controlFieldDescription'), + title: t('nodes.controlField'), }, ControlNetModelField: { color: 'teal.500', @@ -182,132 +183,132 @@ export const FIELDS: Record = { }, DenoiseMaskField: { color: 'blue.300', - description: 'Denoise Mask may be passed between nodes', - title: 'Denoise Mask', + description: t('nodes.denoiseMaskFieldDescription'), + title: t('nodes.denoiseMaskField'), }, enum: { color: 'blue.500', - description: 'Enums are values that may be one of a number of options.', - title: 'Enum', + description: t('nodes.enumDescription'), + title: t('nodes.enum'), }, float: { color: 'orange.500', - description: 'Floats are numbers with a decimal point.', - title: 'Float', + description: t('nodes.floatDescription'), + title: t('nodes.float'), }, FloatCollection: { color: 'orange.500', - description: 'A collection of floats.', - title: 'Float Collection', + description: t('nodes.floatCollectionDescription'), + title: t('nodes.floatCollection'), }, FloatPolymorphic: { color: 'orange.500', - description: 'A collection of floats.', - title: 'Float Polymorphic', + description: t('nodes.floatPolymorphicDescription'), + title: t('nodes.floatPolymorphic'), }, ImageCollection: { color: 'purple.500', - description: 'A collection of images.', - title: 'Image Collection', + description: t('nodes.imageCollectionDescription'), + title: t('nodes.imageCollection'), }, ImageField: { color: 'purple.500', - description: 'Images may be passed between nodes.', - title: 'Image', + description: t('nodes.imageFieldDescription'), + title: t('nodes.imageField'), }, ImagePolymorphic: { color: 'purple.500', - description: 'A collection of images.', - title: 'Image Polymorphic', + description: t('nodes.imagePolymorphicDescription'), + title: t('nodes.imagePolymorphic'), }, integer: { color: 'red.500', - description: 'Integers are whole numbers, without a decimal point.', - title: 'Integer', + description: t('nodes.integerDescription'), + title: t('nodes.integer'), }, IntegerCollection: { color: 'red.500', - description: 'A collection of integers.', - title: 'Integer Collection', + description: t('nodes.integerCollectionDescription'), + title: t('nodes.integerCollection'), }, IntegerPolymorphic: { color: 'red.500', - description: 'A collection of integers.', - title: 'Integer Polymorphic', + description: t('nodes.integerPolymorphicDescription'), + title: t('nodes.integerPolymorphic'), }, LatentsCollection: { color: 'pink.500', - description: 'Latents may be passed between nodes.', - title: 'Latents Collection', + description: t('nodes.latentsCollectionDescription'), + title: t('nodes.latentsCollection'), }, LatentsField: { color: 'pink.500', - description: 'Latents may be passed between nodes.', - title: 'Latents', + description: t('nodes.latentsFieldDescription'), + title: t('nodes.latentsField'), }, LatentsPolymorphic: { color: 'pink.500', - description: 'Latents may be passed between nodes.', - title: 'Latents Polymorphic', + description: t('nodes.latentsPolymorphicDescription'), + title: t('nodes.latentsPolymorphic'), }, LoRAModelField: { color: 'teal.500', - description: 'TODO', - title: 'LoRA', + description: t('nodes.loRAModelFieldDescription'), + title: t('nodes.loRAModelField'), }, MainModelField: { color: 'teal.500', - description: 'TODO', - title: 'Model', + description: t('nodes.mainModelFieldDescription'), + title: t('nodes.mainModelField'), }, ONNXModelField: { color: 'teal.500', - description: 'ONNX model field.', - title: 'ONNX Model', + description: t('nodes.oNNXModelFieldDescription'), + title: t('nodes.oNNXModelField'), }, Scheduler: { color: 'base.500', - description: 'TODO', - title: 'Scheduler', + description: t('nodes.schedulerDescription'), + title: t('nodes.scheduler'), }, SDXLMainModelField: { color: 'teal.500', - description: 'SDXL model field.', - title: 'SDXL Model', + description: t('nodes.sDXLMainModelFieldDescription'), + title: t('nodes.sDXLMainModelField'), }, SDXLRefinerModelField: { color: 'teal.500', - description: 'TODO', - title: 'Refiner Model', + description: t('nodes.sDXLRefinerModelFieldDescription'), + title: t('nodes.sDXLRefinerModelField'), }, string: { color: 'yellow.500', - description: 'Strings are text.', - title: 'String', + description: t('nodes.stringDescription'), + title: t('nodes.string'), }, StringCollection: { color: 'yellow.500', - description: 'A collection of strings.', - title: 'String Collection', + description: t('nodes.stringCollectionDescription'), + title: t('nodes.stringCollection'), }, StringPolymorphic: { color: 'yellow.500', - description: 'A collection of strings.', - title: 'String Polymorphic', + description: t('nodes.stringPolymorphicDescription'), + title: t('nodes.stringPolymorphic'), }, UNetField: { color: 'red.500', - description: 'UNet submodel.', - title: 'UNet', + description: t('nodes.uNetFieldDescription'), + title: t('nodes.uNetField'), }, VaeField: { color: 'blue.500', - description: 'Vae submodel.', - title: 'Vae', + description: t('nodes.vaeFieldDescription'), + title: t('nodes.vaeField'), }, VaeModelField: { color: 'teal.500', - description: 'TODO', - title: 'VAE', + description: t('nodes.vaeModelFieldDescription'), + title: t('nodes.vaeModelField'), }, }; diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index b1ad6a7b96..a4b71457f7 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -20,6 +20,7 @@ import { import { O } from 'ts-toolbelt'; import { JsonObject } from 'type-fest'; import { z } from 'zod'; +import i18n from 'i18next'; export type NonNullableGraph = O.Required; @@ -1258,23 +1259,35 @@ export const zValidatedWorkflow = zWorkflow.transform((workflow) => { const targetNode = keyedNodes[edge.target]; const issues: string[] = []; if (!sourceNode) { - issues.push(`Output node ${edge.source} does not exist`); + issues.push( + `${i18n.t('nodes.outputNode')} ${edge.source} ${i18n.t( + 'nodes.doesNotExist' + )}` + ); } else if ( edge.type === 'default' && !(edge.sourceHandle in sourceNode.data.outputs) ) { issues.push( - `Output field "${edge.source}.${edge.sourceHandle}" does not exist` + `${i18n.t('nodes.outputField')}"${edge.source}.${ + edge.sourceHandle + }" ${i18n.t('nodes.doesNotExist')}` ); } if (!targetNode) { - issues.push(`Input node ${edge.target} does not exist`); + issues.push( + `${i18n.t('nodes.inputNode')} ${edge.target} ${i18n.t( + 'nodes.doesNotExist' + )}` + ); } else if ( edge.type === 'default' && !(edge.targetHandle in targetNode.data.inputs) ) { issues.push( - `Input field "${edge.target}.${edge.targetHandle}" does not exist` + `${i18n.t('nodes.inputField')} "${edge.target}.${ + edge.targetHandle + }" ${i18n.t('nodes.doesNotExist')}` ); } if (issues.length) { @@ -1282,7 +1295,9 @@ export const zValidatedWorkflow = zWorkflow.transform((workflow) => { const src = edge.type === 'default' ? edge.sourceHandle : edge.source; const tgt = edge.type === 'default' ? edge.targetHandle : edge.target; warnings.push({ - message: `Edge "${src} -> ${tgt}" skipped`, + message: `${i18n.t('nodes.edge')} "${src} -> ${tgt}" ${i18n.t( + 'nodes.skipped' + )}`, issues, data: edge, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts index b0ade42a9f..43ee75b735 100644 --- a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts @@ -3,6 +3,7 @@ import { NodesState } from '../store/types'; import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types'; import { fromZodError } from 'zod-validation-error'; import { parseify } from 'common/util/serialize'; +import i18n from 'i18next'; export const buildWorkflow = (nodesState: NodesState): Workflow => { const { workflow: workflowMeta, nodes, edges } = nodesState; @@ -20,7 +21,7 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => { const result = zWorkflowNode.safeParse(node); if (!result.success) { const { message } = fromZodError(result.error, { - prefix: 'Unable to parse node', + prefix: i18n.t('nodes.unableToParseNode'), }); logger('nodes').warn({ node: parseify(node) }, message); return; @@ -32,7 +33,7 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => { const result = zWorkflowEdge.safeParse(edge); if (!result.success) { const { message } = fromZodError(result.error, { - prefix: 'Unable to parse edge', + prefix: i18n.t('nodes.unableToParseEdge'), }); logger('nodes').warn({ edge: parseify(edge) }, message); return; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts index 58a7726410..bfedc03de4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts @@ -79,8 +79,8 @@ export const buildCanvasInpaintGraph = ( } = state.generation; if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); + log.error('No Image found in state'); + throw new Error('No Image found in state'); } // The bounding box determines width and height, not the width and height params diff --git a/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts index a3085d516b..14b90fc731 100644 --- a/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts @@ -7,6 +7,7 @@ import { isWorkflowInvocationNode, } from '../types/types'; import { parseify } from 'common/util/serialize'; +import i18n from 'i18next'; export const validateWorkflow = ( workflow: Workflow, @@ -25,8 +26,14 @@ export const validateWorkflow = ( const nodeTemplate = nodeTemplates[node.data.type]; if (!nodeTemplate) { errors.push({ - message: `Node "${node.data.type}" skipped`, - issues: [`Node type "${node.data.type}" does not exist`], + message: `${i18n.t('nodes.node')} "${node.data.type}" ${i18n.t( + 'nodes.skipped' + )}`, + issues: [ + `${i18n.t('nodes.nodeType')}"${node.data.type}" ${i18n.t( + 'nodes.doesNotExist' + )}`, + ], data: node, }); return; @@ -38,9 +45,13 @@ export const validateWorkflow = ( compareVersions(nodeTemplate.version, node.data.version) !== 0 ) { errors.push({ - message: `Node "${node.data.type}" has mismatched version`, + message: `${i18n.t('nodes.node')} "${node.data.type}" ${i18n.t( + 'nodes.mismatchedVersion' + )}`, issues: [ - `Node "${node.data.type}" v${node.data.version} may be incompatible with installed v${nodeTemplate.version}`, + `${i18n.t('nodes.node')} "${node.data.type}" v${ + node.data.version + } ${i18n.t('nodes.maybeIncompatible')} v${nodeTemplate.version}`, ], data: { node, nodeTemplate: parseify(nodeTemplate) }, }); @@ -52,33 +63,49 @@ export const validateWorkflow = ( const targetNode = keyedNodes[edge.target]; const issues: string[] = []; if (!sourceNode) { - issues.push(`Output node ${edge.source} does not exist`); + issues.push( + `${i18n.t('nodes.outputNode')} ${edge.source} ${i18n.t( + 'nodes.doesNotExist' + )}` + ); } else if ( edge.type === 'default' && !(edge.sourceHandle in sourceNode.data.outputs) ) { issues.push( - `Output field "${edge.source}.${edge.sourceHandle}" does not exist` + `${i18n.t('nodes.outputNodes')} "${edge.source}.${ + edge.sourceHandle + }" ${i18n.t('nodes.doesNotExist')}` ); } if (!targetNode) { - issues.push(`Input node ${edge.target} does not exist`); + issues.push( + `${i18n.t('nodes.inputNode')} ${edge.target} ${i18n.t( + 'nodes.doesNotExist' + )}` + ); } else if ( edge.type === 'default' && !(edge.targetHandle in targetNode.data.inputs) ) { issues.push( - `Input field "${edge.target}.${edge.targetHandle}" does not exist` + `${i18n.t('nodes.inputFeilds')} "${edge.target}.${ + edge.targetHandle + }" ${i18n.t('nodes.doesNotExist')}` ); } if (!nodeTemplates[sourceNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) { issues.push( - `Source node "${edge.source}" missing template "${sourceNode?.data.type}"` + `${i18n.t('nodes.sourceNode')} "${edge.source}" ${i18n.t( + 'nodes.missingTemplate' + )} "${sourceNode?.data.type}"` ); } if (!nodeTemplates[targetNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) { issues.push( - `Source node "${edge.target}" missing template "${targetNode?.data.type}"` + `${i18n.t('nodes.sourceNode')}"${edge.target}" ${i18n.t( + 'nodes.missingTemplate' + )} "${targetNode?.data.type}"` ); } if (issues.length) { diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 50d9894b27..dc79281a06 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -80,7 +80,7 @@ export const initialGenerationState: GenerationState = { scheduler: 'euler', maskBlur: 16, maskBlurMethod: 'box', - canvasCoherenceMode: 'edge', + canvasCoherenceMode: 'unmasked', canvasCoherenceSteps: 20, canvasCoherenceStrength: 0.3, seed: 0, diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 33edc6308f..82c9ef4e77 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -4,7 +4,7 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName } from './tabMap'; -import { CustomStarUi, UIState } from './uiTypes'; +import { UIState } from './uiTypes'; export const initialUIState: UIState = { activeTab: 0, @@ -19,7 +19,6 @@ export const initialUIState: UIState = { favoriteSchedulers: [], globalContextMenuCloseTrigger: 0, panels: {}, - customStarUi: undefined, }; export const uiSlice = createSlice({ @@ -71,9 +70,6 @@ export const uiSlice = createSlice({ ) => { state.panels[action.payload.name] = action.payload.value; }, - setCustomStarUi: (state, action: PayloadAction) => { - state.customStarUi = action.payload; - }, }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { @@ -95,7 +91,6 @@ export const { setShouldAutoChangeDimensions, contextMenusClosed, panelsChanged, - setCustomStarUi, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 8f7b1999a4..41a359a651 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,4 +1,3 @@ -import { MenuItemProps } from '@chakra-ui/react'; import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; export type Coordinates = { @@ -13,17 +12,6 @@ export type Dimensions = { export type Rect = Coordinates & Dimensions; -export type CustomStarUi = { - on: { - icon: MenuItemProps['icon']; - text: string; - }; - off: { - icon: MenuItemProps['icon']; - text: string; - }; -}; - export interface UIState { activeTab: number; shouldShowImageDetails: boolean; @@ -37,5 +25,4 @@ export interface UIState { favoriteSchedulers: SchedulerParam[]; globalContextMenuCloseTrigger: number; panels: Record; - customStarUi?: CustomStarUi; } diff --git a/invokeai/frontend/web/src/services/api/thunks/schema.ts b/invokeai/frontend/web/src/services/api/thunks/schema.ts index 7dc0bdc331..c209469e02 100644 --- a/invokeai/frontend/web/src/services/api/thunks/schema.ts +++ b/invokeai/frontend/web/src/services/api/thunks/schema.ts @@ -26,7 +26,8 @@ export const receivedOpenAPISchema = createAsyncThunk( 'nodes/receivedOpenAPISchema', async (_, { rejectWithValue }) => { try { - const response = await fetch(`openapi.json`); + const url = [window.location.origin, 'openapi.json'].join('/'); + const response = await fetch(url); const openAPISchema = await response.json(); const schemaJSON = JSON.parse(