Merge branch 'main' into export-more-for-header

This commit is contained in:
Mary Hipp Rogers 2023-03-21 16:29:53 -04:00 committed by GitHub
commit 1b215059e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 645 additions and 558 deletions

View File

@ -17,7 +17,7 @@ notebooks.
You will need a GPU to perform training in a reasonable length of You will need a GPU to perform training in a reasonable length of
time, and at least 12 GB of VRAM. We recommend using the [`xformers` time, and at least 12 GB of VRAM. We recommend using the [`xformers`
library](../installation/070_INSTALL_XFORMERS) to accelerate the library](../installation/070_INSTALL_XFORMERS.md) to accelerate the
training process further. During training, about ~8 GB is temporarily training process further. During training, about ~8 GB is temporarily
needed in order to store intermediate models, checkpoints and logs. needed in order to store intermediate models, checkpoints and logs.

View File

@ -24,7 +24,7 @@ You need to have opencv installed so that pypatchmatch can be built:
brew install opencv brew install opencv
``` ```
The next time you start `invoke`, after sucesfully installing opencv, pypatchmatch will be built. The next time you start `invoke`, after successfully installing opencv, pypatchmatch will be built.
## Linux ## Linux
@ -56,7 +56,7 @@ Prior to installing PyPatchMatch, you need to take the following steps:
5. Confirm that pypatchmatch is installed. At the command-line prompt enter 5. Confirm that pypatchmatch is installed. At the command-line prompt enter
`python`, and then at the `>>>` line type `python`, and then at the `>>>` line type
`from patchmatch import patch_match`: It should look like the follwing: `from patchmatch import patch_match`: It should look like the following:
```py ```py
Python 3.9.5 (default, Nov 23 2021, 15:27:38) Python 3.9.5 (default, Nov 23 2021, 15:27:38)
@ -108,4 +108,4 @@ Prior to installing PyPatchMatch, you need to take the following steps:
[**Next, Follow Steps 4-6 from the Debian Section above**](#linux) [**Next, Follow Steps 4-6 from the Debian Section above**](#linux)
If you see no errors, then you're ready to go! If you see no errors you're ready to go!

View File

@ -4,6 +4,8 @@ from datetime import datetime, timezone
from typing import Any, Literal, Optional, Union from typing import Any, Literal, Optional, Union
import numpy as np import numpy as np
from torch import Tensor
from PIL import Image from PIL import Image
from pydantic import Field from pydantic import Field
from skimage.exposure.histogram_matching import match_histograms from skimage.exposure.histogram_matching import match_histograms
@ -12,7 +14,9 @@ from ..services.image_storage import ImageType
from ..services.invocation_services import InvocationServices from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, InvocationContext from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput from .image import ImageField, ImageOutput
from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator, Generator
from ...backend.stable_diffusion import PipelineIntermediateState
from ...backend.util.util import image_to_dataURL
SAMPLER_NAME_VALUES = Literal[ SAMPLER_NAME_VALUES = Literal[
tuple(InvokeAIGenerator.schedulers()) tuple(InvokeAIGenerator.schedulers())
@ -41,18 +45,32 @@ class TextToImageInvocation(BaseInvocation):
# TODO: pass this an emitter method or something? or a session for dispatching? # TODO: pass this an emitter method or something? or a session for dispatching?
def dispatch_progress( def dispatch_progress(
self, context: InvocationContext, sample: Any = None, step: int = 0 self, context: InvocationContext, sample: Tensor, step: int
) -> None: ) -> None:
# TODO: only output a preview image when requested
image = Generator.sample_to_lowres_estimated_image(sample)
(width, height) = image.size
width *= 8
height *= 8
dataURL = image_to_dataURL(image, image_format="JPEG")
context.services.events.emit_generator_progress( context.services.events.emit_generator_progress(
context.graph_execution_state_id, context.graph_execution_state_id,
self.id, self.id,
{
"width": width,
"height": height,
"dataURL": dataURL
},
step, step,
float(step) / float(self.steps), self.steps,
) )
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
def step_callback(sample, step=0): def step_callback(state: PipelineIntermediateState):
self.dispatch_progress(context, sample, step) self.dispatch_progress(context, state.latents, state.step)
# Handle invalid model parameter # Handle invalid model parameter
# TODO: figure out if this can be done via a validator that uses the model_cache # TODO: figure out if this can be done via a validator that uses the model_cache

View File

@ -1,7 +1,10 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
from typing import Any, Dict from typing import Any, Dict, TypedDict
ProgressImage = TypedDict(
"ProgressImage", {"dataURL": str, "width": int, "height": int}
)
class EventServiceBase: class EventServiceBase:
session_event: str = "session_event" session_event: str = "session_event"
@ -23,8 +26,9 @@ class EventServiceBase:
self, self,
graph_execution_state_id: str, graph_execution_state_id: str,
invocation_id: str, invocation_id: str,
progress_image: ProgressImage | None,
step: int, step: int,
percent: float, total_steps: int,
) -> None: ) -> None:
"""Emitted when there is generation progress""" """Emitted when there is generation progress"""
self.__emit_session_event( self.__emit_session_event(
@ -32,8 +36,9 @@ class EventServiceBase:
payload=dict( payload=dict(
graph_execution_state_id=graph_execution_state_id, graph_execution_state_id=graph_execution_state_id,
invocation_id=invocation_id, invocation_id=invocation_id,
progress_image=progress_image,
step=step, step=step,
percent=percent, total_steps=total_steps,
), ),
) )

View File

@ -773,6 +773,24 @@ class GraphExecutionState(BaseModel):
default_factory=dict, default_factory=dict,
) )
# Declare all fields as required; necessary for OpenAPI schema generation build.
# Technically only fields without a `default_factory` need to be listed here.
# See: https://github.com/pydantic/pydantic/discussions/4577
class Config:
schema_extra = {
'required': [
'id',
'graph',
'execution_graph',
'executed',
'executed_history',
'results',
'errors',
'prepared_source_mapping',
'source_prepared_mapping',
]
}
def next(self) -> BaseInvocation | None: def next(self) -> BaseInvocation | None:
"""Gets the next node ready to execute.""" """Gets the next node ready to execute."""

View File

@ -497,7 +497,8 @@ class Generator:
matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask) matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask)
return matched_result return matched_result
def sample_to_lowres_estimated_image(self, samples): @staticmethod
def sample_to_lowres_estimated_image(samples):
# origingally adapted from code by @erucipe and @keturn here: # origingally adapted from code by @erucipe and @keturn here:
# https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/7 # https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/7

View File

@ -3,6 +3,9 @@ import math
import multiprocessing as mp import multiprocessing as mp
import os import os
import re import re
import io
import base64
from collections import abc from collections import abc
from inspect import isfunction from inspect import isfunction
from pathlib import Path from pathlib import Path
@ -364,3 +367,16 @@ def url_attachment_name(url: str) -> dict:
def download_with_progress_bar(url: str, dest: Path) -> bool: def download_with_progress_bar(url: str, dest: Path) -> bool:
result = download_with_resume(url, dest, access_token=None) result = download_with_resume(url, dest, access_token=None)
return result is not None return result is not None
def image_to_dataURL(image: Image.Image, image_format: str = "PNG") -> str:
"""
Converts an image into a base64 image dataURL.
"""
buffered = io.BytesIO()
image.save(buffered, format=image_format)
mime_type = Image.MIME.get(image_format.upper(), "image/" + image_format.lower())
image_base64 = f"data:{mime_type};base64," + base64.b64encode(
buffered.getvalue()
).decode("UTF-8")
return image_base64

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import{j as y,cN as Ie,r as _,cO as bt,q as Lr,cP as o,cQ as b,cR as v,cS as S,cT as Vr,cU as ut,cV as vt,cM as ft,cW as mt,n as gt,cX as ht,E as pt}from"./index-d64f4654.js";import{d as yt,i as St,T as xt,j as $t,h as kt}from"./storeHooks-0eed8e9f.js";var Or=` import{j as y,cN as Ie,r as _,cO as bt,q as Lr,cP as o,cQ as b,cR as v,cS as S,cT as Vr,cU as ut,cV as vt,cM as ft,cW as mt,n as gt,cX as ht,E as pt}from"./index-f7f41e1f.js";import{d as yt,i as St,T as xt,j as $t,h as kt}from"./storeHooks-eaf47ae3.js";var Or=`
:root { :root {
--chakra-vh: 100vh; --chakra-vh: 100vh;
} }

View File

@ -12,7 +12,7 @@
margin: 0; margin: 0;
} }
</style> </style>
<script type="module" crossorigin src="./assets/index-d64f4654.js"></script> <script type="module" crossorigin src="./assets/index-f7f41e1f.js"></script>
<link rel="stylesheet" href="./assets/index-5483945c.css"> <link rel="stylesheet" href="./assets/index-5483945c.css">
</head> </head>

View File

@ -64,6 +64,8 @@
"trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.", "trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.",
"upload": "Upload", "upload": "Upload",
"close": "Close", "close": "Close",
"cancel": "Cancel",
"accept": "Accept",
"load": "Load", "load": "Load",
"back": "Back", "back": "Back",
"statusConnected": "Connected", "statusConnected": "Connected",
@ -333,6 +335,7 @@
"addNewModel": "Add New Model", "addNewModel": "Add New Model",
"addCheckpointModel": "Add Checkpoint / Safetensor Model", "addCheckpointModel": "Add Checkpoint / Safetensor Model",
"addDiffuserModel": "Add Diffusers", "addDiffuserModel": "Add Diffusers",
"scanForModels": "Scan For Models",
"addManually": "Add Manually", "addManually": "Add Manually",
"manual": "Manual", "manual": "Manual",
"name": "Name", "name": "Name",

View File

@ -64,6 +64,8 @@
"trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.", "trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.",
"upload": "Upload", "upload": "Upload",
"close": "Close", "close": "Close",
"cancel": "Cancel",
"accept": "Accept",
"load": "Load", "load": "Load",
"back": "Back", "back": "Back",
"statusConnected": "Connected", "statusConnected": "Connected",
@ -333,6 +335,7 @@
"addNewModel": "Add New Model", "addNewModel": "Add New Model",
"addCheckpointModel": "Add Checkpoint / Safetensor Model", "addCheckpointModel": "Add Checkpoint / Safetensor Model",
"addDiffuserModel": "Add Diffusers", "addDiffuserModel": "Add Diffusers",
"scanForModels": "Scan For Models",
"addManually": "Add Manually", "addManually": "Add Manually",
"manual": "Manual", "manual": "Manual",
"name": "Name", "name": "Name",

View File

@ -31,18 +31,14 @@ export const DIFFUSERS_SAMPLERS: Array<string> = [
]; ];
// Valid image widths // Valid image widths
export const WIDTHS: Array<number> = [ export const WIDTHS: Array<number> = Array.from(Array(65)).map(
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, (_x, i) => i * 64
1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, );
1856, 1920, 1984, 2048,
];
// Valid image heights // Valid image heights
export const HEIGHTS: Array<number> = [ export const HEIGHTS: Array<number> = Array.from(Array(65)).map(
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, (_x, i) => i * 64
1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, );
1856, 1920, 1984, 2048,
];
// Valid upscaling levels // Valid upscaling levels
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [ export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [

View File

@ -9,6 +9,7 @@ import {
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { cloneElement, memo, ReactElement, ReactNode, useRef } from 'react'; import { cloneElement, memo, ReactElement, ReactNode, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import IAIButton from './IAIButton'; import IAIButton from './IAIButton';
type Props = { type Props = {
@ -22,10 +23,12 @@ type Props = {
}; };
const IAIAlertDialog = forwardRef((props: Props, ref) => { const IAIAlertDialog = forwardRef((props: Props, ref) => {
const { t } = useTranslation();
const { const {
acceptButtonText = 'Accept', acceptButtonText = t('common.accept'),
acceptCallback, acceptCallback,
cancelButtonText = 'Cancel', cancelButtonText = t('common.cancel'),
cancelCallback, cancelCallback,
children, children,
title, title,
@ -56,6 +59,7 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
isOpen={isOpen} isOpen={isOpen}
leastDestructiveRef={cancelRef} leastDestructiveRef={cancelRef}
onClose={onClose} onClose={onClose}
isCentered
> >
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent> <AlertDialogContent>

View File

@ -0,0 +1,8 @@
import { chakra } from '@chakra-ui/react';
/**
* Chakra-enabled <form />
*/
const IAIForm = chakra.form;
export default IAIForm;

View File

@ -0,0 +1,23 @@
import { Flex } from '@chakra-ui/react';
import { ReactElement } from 'react';
export function IAIFormItemWrapper({
children,
}: {
children: ReactElement | ReactElement[];
}) {
return (
<Flex
sx={{
flexDirection: 'column',
padding: 4,
rowGap: 4,
borderRadius: 'base',
width: 'full',
bg: 'base.900',
}}
>
{children}
</Flex>
);
}

View File

@ -104,7 +104,6 @@ const IAICanvasMaskOptions = () => {
return ( return (
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<ButtonGroup> <ButtonGroup>
<IAIIconButton <IAIIconButton

View File

@ -88,7 +88,7 @@ const IAICanvasSettingsButtonPopover = () => {
return ( return (
<IAIPopover <IAIPopover
trigger="hover" isLazy={false}
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
tooltip={t('unifiedCanvas.canvasSettings')} tooltip={t('unifiedCanvas.canvasSettings')}

View File

@ -219,7 +219,6 @@ const IAICanvasToolChooserOptions = () => {
onClick={handleSelectColorPickerTool} onClick={handleSelectColorPickerTool}
/> />
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
aria-label={t('unifiedCanvas.brushOptions')} aria-label={t('unifiedCanvas.brushOptions')}

View File

@ -405,7 +405,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
> >
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
aria-label={`${t('parameters.sendTo')}...`} aria-label={`${t('parameters.sendTo')}...`}
@ -505,7 +504,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
icon={<FaGrinStars />} icon={<FaGrinStars />}
@ -535,7 +533,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</IAIPopover> </IAIPopover>
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
icon={<FaExpandArrowsAlt />} icon={<FaExpandArrowsAlt />}

View File

@ -0,0 +1,24 @@
import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react';
type CurrentImageFallbackProps = SpinnerProps;
const CurrentImageFallback = (props: CurrentImageFallbackProps) => {
const { size = 'xl', ...rest } = props;
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
color: 'base.400',
}}
>
<Spinner size={size} {...rest} />
</Flex>
);
};
export default CurrentImageFallback;

View File

@ -7,6 +7,7 @@ import { isEqual } from 'lodash';
import { APP_METADATA_HEIGHT } from 'theme/util/constants'; import { APP_METADATA_HEIGHT } from 'theme/util/constants';
import { gallerySelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import CurrentImageFallback from './CurrentImageFallback';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
@ -48,6 +49,7 @@ export default function CurrentImagePreview() {
src={imageToDisplay.url} src={imageToDisplay.url}
width={imageToDisplay.width} width={imageToDisplay.width}
height={imageToDisplay.height} height={imageToDisplay.height}
fallback={!isIntermediate ? <CurrentImageFallback /> : undefined}
sx={{ sx={{
objectFit: 'contain', objectFit: 'contain',
maxWidth: '100%', maxWidth: '100%',

View File

@ -55,7 +55,6 @@ export default function LanguagePicker() {
return ( return (
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
aria-label={t('common.languagePickerLabel')} aria-label={t('common.languagePickerLabel')}

View File

@ -1,4 +1,5 @@
import { import {
Flex,
FormControl, FormControl,
FormErrorMessage, FormErrorMessage,
FormHelperText, FormHelperText,
@ -25,10 +26,10 @@ import { useTranslation } from 'react-i18next';
import type { InvokeModelConfigProps } from 'app/invokeai'; import type { InvokeModelConfigProps } from 'app/invokeai';
import type { RootState } from 'app/store'; import type { RootState } from 'app/store';
import IAIIconButton from 'common/components/IAIIconButton';
import { setAddNewModelUIOption } from 'features/ui/store/uiSlice'; import { setAddNewModelUIOption } from 'features/ui/store/uiSlice';
import type { FieldInputProps, FormikProps } from 'formik'; import type { FieldInputProps, FormikProps } from 'formik';
import { BiArrowBack } from 'react-icons/bi'; import IAIForm from 'common/components/IAIForm';
import { IAIFormItemWrapper } from 'common/components/IAIForms/IAIFormItemWrapper';
const MIN_MODEL_SIZE = 64; const MIN_MODEL_SIZE = 64;
const MAX_MODEL_SIZE = 2048; const MAX_MODEL_SIZE = 2048;
@ -72,243 +73,250 @@ export default function AddCheckpointModel() {
return ( return (
<VStack gap={2} alignItems="flex-start"> <VStack gap={2} alignItems="flex-start">
<IAIIconButton <Flex columnGap={4}>
aria-label={t('common.back')} <IAICheckbox
tooltip={t('common.back')} isChecked={!addManually}
onClick={() => dispatch(setAddNewModelUIOption(null))} label={t('modelManager.scanForModels')}
width="max-content" onChange={() => setAddmanually(!addManually)}
position="absolute" />
zIndex={1} <IAICheckbox
size="sm" label={t('modelManager.addManually')}
insetInlineEnd={12} isChecked={addManually}
top={3} onChange={() => setAddmanually(!addManually)}
icon={<BiArrowBack />} />
/> </Flex>
<SearchModels /> {addManually ? (
<IAICheckbox
label={t('modelManager.addManually')}
isChecked={addManually}
onChange={() => setAddmanually(!addManually)}
/>
{addManually && (
<Formik <Formik
initialValues={addModelFormValues} initialValues={addModelFormValues}
onSubmit={addModelFormSubmitHandler} onSubmit={addModelFormSubmitHandler}
> >
{({ handleSubmit, errors, touched }) => ( {({ handleSubmit, errors, touched }) => (
<form onSubmit={handleSubmit}> <IAIForm onSubmit={handleSubmit} sx={{ w: 'full' }}>
<VStack rowGap={2}> <VStack rowGap={2}>
<Text fontSize={20} fontWeight="bold" alignSelf="start"> <Text fontSize={20} fontWeight="bold" alignSelf="start">
{t('modelManager.manual')} {t('modelManager.manual')}
</Text> </Text>
{/* Name */} {/* Name */}
<FormControl <IAIFormItemWrapper>
isInvalid={!!errors.name && touched.name} <FormControl
isRequired isInvalid={!!errors.name && touched.name}
> isRequired
<FormLabel htmlFor="name" fontSize="sm"> >
{t('modelManager.name')} <FormLabel htmlFor="name" fontSize="sm">
</FormLabel> {t('modelManager.name')}
<VStack alignItems="start"> </FormLabel>
<Field <VStack alignItems="start">
as={IAIInput} <Field
id="name" as={IAIInput}
name="name" id="name"
type="text" name="name"
validate={baseValidation} type="text"
width="2xl" validate={baseValidation}
/> width="full"
{!!errors.name && touched.name ? ( />
<FormErrorMessage>{errors.name}</FormErrorMessage> {!!errors.name && touched.name ? (
) : ( <FormErrorMessage>{errors.name}</FormErrorMessage>
<FormHelperText margin={0}> ) : (
{t('modelManager.nameValidationMsg')} <FormHelperText margin={0}>
</FormHelperText> {t('modelManager.nameValidationMsg')}
)} </FormHelperText>
</VStack> )}
</FormControl> </VStack>
</FormControl>
</IAIFormItemWrapper>
{/* Description */} {/* Description */}
<FormControl <IAIFormItemWrapper>
isInvalid={!!errors.description && touched.description} <FormControl
isRequired isInvalid={!!errors.description && touched.description}
> isRequired
<FormLabel htmlFor="description" fontSize="sm"> >
{t('modelManager.description')} <FormLabel htmlFor="description" fontSize="sm">
</FormLabel> {t('modelManager.description')}
<VStack alignItems="start"> </FormLabel>
<Field <VStack alignItems="start">
as={IAIInput} <Field
id="description" as={IAIInput}
name="description" id="description"
type="text" name="description"
width="2xl" type="text"
/> width="full"
{!!errors.description && touched.description ? ( />
<FormErrorMessage>{errors.description}</FormErrorMessage> {!!errors.description && touched.description ? (
) : ( <FormErrorMessage>
<FormHelperText margin={0}> {errors.description}
{t('modelManager.descriptionValidationMsg')} </FormErrorMessage>
</FormHelperText> ) : (
)} <FormHelperText margin={0}>
</VStack> {t('modelManager.descriptionValidationMsg')}
</FormControl> </FormHelperText>
)}
</VStack>
</FormControl>
</IAIFormItemWrapper>
{/* Config */} {/* Config */}
<FormControl <IAIFormItemWrapper>
isInvalid={!!errors.config && touched.config} <FormControl
isRequired isInvalid={!!errors.config && touched.config}
> isRequired
<FormLabel htmlFor="config" fontSize="sm"> >
{t('modelManager.config')} <FormLabel htmlFor="config" fontSize="sm">
</FormLabel> {t('modelManager.config')}
<VStack alignItems="start"> </FormLabel>
<Field <VStack alignItems="start">
as={IAIInput} <Field
id="config" as={IAIInput}
name="config" id="config"
type="text" name="config"
width="2xl" type="text"
/> width="full"
{!!errors.config && touched.config ? ( />
<FormErrorMessage>{errors.config}</FormErrorMessage> {!!errors.config && touched.config ? (
) : ( <FormErrorMessage>{errors.config}</FormErrorMessage>
<FormHelperText margin={0}> ) : (
{t('modelManager.configValidationMsg')} <FormHelperText margin={0}>
</FormHelperText> {t('modelManager.configValidationMsg')}
)} </FormHelperText>
</VStack> )}
</FormControl> </VStack>
</FormControl>
</IAIFormItemWrapper>
{/* Weights */} {/* Weights */}
<FormControl <IAIFormItemWrapper>
isInvalid={!!errors.weights && touched.weights} <FormControl
isRequired isInvalid={!!errors.weights && touched.weights}
> isRequired
<FormLabel htmlFor="config" fontSize="sm"> >
{t('modelManager.modelLocation')} <FormLabel htmlFor="config" fontSize="sm">
</FormLabel> {t('modelManager.modelLocation')}
<VStack alignItems="start"> </FormLabel>
<Field <VStack alignItems="start">
as={IAIInput} <Field
id="weights" as={IAIInput}
name="weights" id="weights"
type="text" name="weights"
width="2xl" type="text"
/> width="full"
{!!errors.weights && touched.weights ? ( />
<FormErrorMessage>{errors.weights}</FormErrorMessage> {!!errors.weights && touched.weights ? (
) : ( <FormErrorMessage>{errors.weights}</FormErrorMessage>
<FormHelperText margin={0}> ) : (
{t('modelManager.modelLocationValidationMsg')} <FormHelperText margin={0}>
</FormHelperText> {t('modelManager.modelLocationValidationMsg')}
)} </FormHelperText>
</VStack> )}
</FormControl> </VStack>
</FormControl>
</IAIFormItemWrapper>
{/* VAE */} {/* VAE */}
<FormControl isInvalid={!!errors.vae && touched.vae}> <IAIFormItemWrapper>
<FormLabel htmlFor="vae" fontSize="sm"> <FormControl isInvalid={!!errors.vae && touched.vae}>
{t('modelManager.vaeLocation')} <FormLabel htmlFor="vae" fontSize="sm">
</FormLabel> {t('modelManager.vaeLocation')}
<VStack alignItems="start"> </FormLabel>
<Field <VStack alignItems="start">
as={IAIInput} <Field
id="vae" as={IAIInput}
name="vae" id="vae"
type="text" name="vae"
width="2xl" type="text"
/> width="full"
{!!errors.vae && touched.vae ? ( />
<FormErrorMessage>{errors.vae}</FormErrorMessage> {!!errors.vae && touched.vae ? (
) : ( <FormErrorMessage>{errors.vae}</FormErrorMessage>
<FormHelperText margin={0}> ) : (
{t('modelManager.vaeLocationValidationMsg')} <FormHelperText margin={0}>
</FormHelperText> {t('modelManager.vaeLocationValidationMsg')}
)} </FormHelperText>
</VStack> )}
</FormControl> </VStack>
</FormControl>
</IAIFormItemWrapper>
<HStack width="100%"> <HStack width="100%">
{/* Width */} {/* Width */}
<FormControl isInvalid={!!errors.width && touched.width}> <IAIFormItemWrapper>
<FormLabel htmlFor="width" fontSize="sm"> <FormControl isInvalid={!!errors.width && touched.width}>
{t('modelManager.width')} <FormLabel htmlFor="width" fontSize="sm">
</FormLabel> {t('modelManager.width')}
<VStack alignItems="start"> </FormLabel>
<Field id="width" name="width"> <VStack alignItems="start">
{({ <Field id="width" name="width">
field, {({
form, field,
}: { form,
field: FieldInputProps<number>; }: {
form: FormikProps<InvokeModelConfigProps>; field: FieldInputProps<number>;
}) => ( form: FormikProps<InvokeModelConfigProps>;
<IAINumberInput }) => (
id="width" <IAINumberInput
name="width" id="width"
min={MIN_MODEL_SIZE} name="width"
max={MAX_MODEL_SIZE} min={MIN_MODEL_SIZE}
step={64} max={MAX_MODEL_SIZE}
width="90%" step={64}
value={form.values.width} value={form.values.width}
onChange={(value) => onChange={(value) =>
form.setFieldValue(field.name, Number(value)) form.setFieldValue(field.name, Number(value))
} }
/> />
)} )}
</Field> </Field>
{!!errors.width && touched.width ? ( {!!errors.width && touched.width ? (
<FormErrorMessage>{errors.width}</FormErrorMessage> <FormErrorMessage>{errors.width}</FormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <FormHelperText margin={0}>
{t('modelManager.widthValidationMsg')} {t('modelManager.widthValidationMsg')}
</FormHelperText> </FormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</IAIFormItemWrapper>
{/* Height */} {/* Height */}
<FormControl isInvalid={!!errors.height && touched.height}> <IAIFormItemWrapper>
<FormLabel htmlFor="height" fontSize="sm"> <FormControl isInvalid={!!errors.height && touched.height}>
{t('modelManager.height')} <FormLabel htmlFor="height" fontSize="sm">
</FormLabel> {t('modelManager.height')}
<VStack alignItems="start"> </FormLabel>
<Field id="height" name="height"> <VStack alignItems="start">
{({ <Field id="height" name="height">
field, {({
form, field,
}: { form,
field: FieldInputProps<number>; }: {
form: FormikProps<InvokeModelConfigProps>; field: FieldInputProps<number>;
}) => ( form: FormikProps<InvokeModelConfigProps>;
<IAINumberInput }) => (
id="height" <IAINumberInput
name="height" id="height"
min={MIN_MODEL_SIZE} name="height"
max={MAX_MODEL_SIZE} min={MIN_MODEL_SIZE}
width="90%" max={MAX_MODEL_SIZE}
step={64} step={64}
value={form.values.height} value={form.values.height}
onChange={(value) => onChange={(value) =>
form.setFieldValue(field.name, Number(value)) form.setFieldValue(field.name, Number(value))
} }
/> />
)} )}
</Field> </Field>
{!!errors.height && touched.height ? ( {!!errors.height && touched.height ? (
<FormErrorMessage>{errors.height}</FormErrorMessage> <FormErrorMessage>{errors.height}</FormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <FormHelperText margin={0}>
{t('modelManager.heightValidationMsg')} {t('modelManager.heightValidationMsg')}
</FormHelperText> </FormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</IAIFormItemWrapper>
</HStack> </HStack>
<IAIButton <IAIButton
@ -319,9 +327,11 @@ export default function AddCheckpointModel() {
{t('modelManager.addModel')} {t('modelManager.addModel')}
</IAIButton> </IAIButton>
</VStack> </VStack>
</form> </IAIForm>
)} )}
</Formik> </Formik>
) : (
<SearchModels />
)} )}
</VStack> </VStack>
); );

View File

@ -11,36 +11,14 @@ import { InvokeDiffusersModelConfigProps } from 'app/invokeai';
import { addNewModel } from 'app/socketio/actions'; import { addNewModel } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIInput from 'common/components/IAIInput'; import IAIInput from 'common/components/IAIInput';
import { setAddNewModelUIOption } from 'features/ui/store/uiSlice'; import { setAddNewModelUIOption } from 'features/ui/store/uiSlice';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BiArrowBack } from 'react-icons/bi';
import type { RootState } from 'app/store'; import type { RootState } from 'app/store';
import type { ReactElement } from 'react'; import IAIForm from 'common/components/IAIForm';
import { IAIFormItemWrapper } from 'common/components/IAIForms/IAIFormItemWrapper';
function FormItemWrapper({
children,
}: {
children: ReactElement | ReactElement[];
}) {
return (
<Flex
sx={{
flexDirection: 'column',
padding: 4,
rowGap: 4,
borderRadius: 'base',
width: 'full',
bg: 'base.900',
}}
>
{children}
</Flex>
);
}
export default function AddDiffusersModel() { export default function AddDiffusersModel() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -89,26 +67,14 @@ export default function AddDiffusersModel() {
return ( return (
<Flex> <Flex>
<IAIIconButton
aria-label={t('common.back')}
tooltip={t('common.back')}
onClick={() => dispatch(setAddNewModelUIOption(null))}
width="max-content"
position="absolute"
zIndex={1}
size="sm"
insetInlineEnd={12}
top={3}
icon={<BiArrowBack />}
/>
<Formik <Formik
initialValues={addModelFormValues} initialValues={addModelFormValues}
onSubmit={addModelFormSubmitHandler} onSubmit={addModelFormSubmitHandler}
> >
{({ handleSubmit, errors, touched }) => ( {({ handleSubmit, errors, touched }) => (
<form onSubmit={handleSubmit}> <IAIForm onSubmit={handleSubmit}>
<VStack rowGap={2}> <VStack rowGap={2}>
<FormItemWrapper> <IAIFormItemWrapper>
{/* Name */} {/* Name */}
<FormControl <FormControl
isInvalid={!!errors.name && touched.name} isInvalid={!!errors.name && touched.name}
@ -136,9 +102,9 @@ export default function AddDiffusersModel() {
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</FormItemWrapper> </IAIFormItemWrapper>
<FormItemWrapper> <IAIFormItemWrapper>
{/* Description */} {/* Description */}
<FormControl <FormControl
isInvalid={!!errors.description && touched.description} isInvalid={!!errors.description && touched.description}
@ -165,9 +131,9 @@ export default function AddDiffusersModel() {
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</FormItemWrapper> </IAIFormItemWrapper>
<FormItemWrapper> <IAIFormItemWrapper>
<Text fontWeight="bold" fontSize="sm"> <Text fontWeight="bold" fontSize="sm">
{t('modelManager.formMessageDiffusersModelLocation')} {t('modelManager.formMessageDiffusersModelLocation')}
</Text> </Text>
@ -226,9 +192,9 @@ export default function AddDiffusersModel() {
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</FormItemWrapper> </IAIFormItemWrapper>
<FormItemWrapper> <IAIFormItemWrapper>
{/* VAE Path */} {/* VAE Path */}
<Text fontWeight="bold"> <Text fontWeight="bold">
{t('modelManager.formMessageDiffusersVAELocation')} {t('modelManager.formMessageDiffusersVAELocation')}
@ -290,13 +256,13 @@ export default function AddDiffusersModel() {
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
</FormItemWrapper> </IAIFormItemWrapper>
<IAIButton type="submit" isLoading={isProcessing}> <IAIButton type="submit" isLoading={isProcessing}>
{t('modelManager.addModel')} {t('modelManager.addModel')}
</IAIButton> </IAIButton>
</VStack> </VStack>
</form> </IAIForm>
)} )}
</Formik> </Formik>
</Flex> </Flex>

View File

@ -14,7 +14,7 @@ import {
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { FaPlus } from 'react-icons/fa'; import { FaArrowLeft, FaPlus } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -23,6 +23,7 @@ import type { RootState } from 'app/store';
import { setAddNewModelUIOption } from 'features/ui/store/uiSlice'; import { setAddNewModelUIOption } from 'features/ui/store/uiSlice';
import AddCheckpointModel from './AddCheckpointModel'; import AddCheckpointModel from './AddCheckpointModel';
import AddDiffusersModel from './AddDiffusersModel'; import AddDiffusersModel from './AddDiffusersModel';
import IAIIconButton from 'common/components/IAIIconButton';
function AddModelBox({ function AddModelBox({
text, text,
@ -83,8 +84,22 @@ export default function AddModel() {
closeOnOverlayClick={false} closeOnOverlayClick={false}
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent margin="auto" paddingInlineEnd={4}> <ModalContent margin="auto">
<ModalHeader>{t('modelManager.addNewModel')}</ModalHeader> <ModalHeader>{t('modelManager.addNewModel')} </ModalHeader>
{addNewModelUIOption !== null && (
<IAIIconButton
aria-label={t('common.back')}
tooltip={t('common.back')}
onClick={() => dispatch(setAddNewModelUIOption(null))}
position="absolute"
variant="ghost"
zIndex={1}
size="sm"
insetInlineEnd={12}
top={2}
icon={<FaArrowLeft />}
/>
)}
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
{addNewModelUIOption == null && ( {addNewModelUIOption == null && (

View File

@ -28,6 +28,7 @@ import { isEqual, pickBy } from 'lodash';
import ModelConvert from './ModelConvert'; import ModelConvert from './ModelConvert';
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText'; import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage'; import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
import IAIForm from 'common/components/IAIForm';
const selector = createSelector( const selector = createSelector(
[systemSelector], [systemSelector],
@ -120,7 +121,7 @@ export default function CheckpointModelEdit() {
onSubmit={editModelFormSubmitHandler} onSubmit={editModelFormSubmitHandler}
> >
{({ handleSubmit, errors, touched }) => ( {({ handleSubmit, errors, touched }) => (
<form onSubmit={handleSubmit}> <IAIForm onSubmit={handleSubmit}>
<VStack rowGap={2} alignItems="start"> <VStack rowGap={2} alignItems="start">
{/* Description */} {/* Description */}
<FormControl <FormControl
@ -317,7 +318,7 @@ export default function CheckpointModelEdit() {
{t('modelManager.updateModel')} {t('modelManager.updateModel')}
</IAIButton> </IAIButton>
</VStack> </VStack>
</form> </IAIForm>
)} )}
</Formik> </Formik>
</Flex> </Flex>

View File

@ -18,6 +18,7 @@ import type { RootState } from 'app/store';
import { isEqual, pickBy } from 'lodash'; import { isEqual, pickBy } from 'lodash';
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText'; import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage'; import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
import IAIForm from 'common/components/IAIForm';
const selector = createSelector( const selector = createSelector(
[systemSelector], [systemSelector],
@ -116,7 +117,7 @@ export default function DiffusersModelEdit() {
onSubmit={editModelFormSubmitHandler} onSubmit={editModelFormSubmitHandler}
> >
{({ handleSubmit, errors, touched }) => ( {({ handleSubmit, errors, touched }) => (
<form onSubmit={handleSubmit}> <IAIForm onSubmit={handleSubmit}>
<VStack rowGap={2} alignItems="start"> <VStack rowGap={2} alignItems="start">
{/* Description */} {/* Description */}
<FormControl <FormControl
@ -259,7 +260,7 @@ export default function DiffusersModelEdit() {
{t('modelManager.updateModel')} {t('modelManager.updateModel')}
</IAIButton> </IAIButton>
</VStack> </VStack>
</form> </IAIForm>
)} )}
</Formik> </Formik>
</Flex> </Flex>

View File

@ -12,14 +12,13 @@ import {
RadioGroup, RadioGroup,
Spacer, Spacer,
Text, Text,
VStack,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaPlus, FaSearch } from 'react-icons/fa'; import { FaSearch, FaTrash } from 'react-icons/fa';
import { addNewModel, searchForModels } from 'app/socketio/actions'; import { addNewModel, searchForModels } from 'app/socketio/actions';
import { import {
@ -34,7 +33,7 @@ import IAIInput from 'common/components/IAIInput';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import { forEach, remove } from 'lodash'; import { forEach, remove } from 'lodash';
import type { ChangeEvent, ReactNode } from 'react'; import type { ChangeEvent, ReactNode } from 'react';
import { BiReset } from 'react-icons/bi'; import IAIForm from 'common/components/IAIForm';
const existingModelsSelector = createSelector([systemSelector], (system) => { const existingModelsSelector = createSelector([systemSelector], (system) => {
const { model_list } = system; const { model_list } = system;
@ -71,34 +70,32 @@ function SearchModelEntry({
}; };
return ( return (
<VStack> <Flex
<Flex flexDirection="column"
flexDirection="column" gap={2}
gap={2} backgroundColor={
backgroundColor={ modelsToAdd.includes(model.name) ? 'accent.650' : 'base.800'
modelsToAdd.includes(model.name) ? 'accent.650' : 'base.800' }
} paddingX={4}
paddingX={4} paddingY={2}
paddingY={2} borderRadius={4}
borderRadius={4} >
> <Flex gap={4} alignItems="center" justifyContent="space-between">
<Flex gap={4}> <IAICheckbox
<IAICheckbox value={model.name}
value={model.name} label={<Text fontWeight={500}>{model.name}</Text>}
label={<Text fontWeight={500}>{model.name}</Text>} isChecked={modelsToAdd.includes(model.name)}
isChecked={modelsToAdd.includes(model.name)} isDisabled={existingModels.includes(model.location)}
isDisabled={existingModels.includes(model.location)} onChange={foundModelsChangeHandler}
onChange={foundModelsChangeHandler} ></IAICheckbox>
></IAICheckbox> {existingModels.includes(model.location) && (
{existingModels.includes(model.location) && ( <Badge colorScheme="accent">{t('modelManager.modelExists')}</Badge>
<Badge colorScheme="accent">{t('modelManager.modelExists')}</Badge> )}
)}
</Flex>
<Text fontStyle="italic" variant="subtext">
{model.location}
</Text>
</Flex> </Flex>
</VStack> <Text fontStyle="italic" variant="subtext">
{model.location}
</Text>
</Flex>
); );
} }
@ -215,10 +212,10 @@ export default function SearchModels() {
} }
return ( return (
<> <Flex flexDirection="column" rowGap={4}>
{newFoundModels} {newFoundModels}
{shouldShowExistingModelsInSearch && existingFoundModels} {shouldShowExistingModelsInSearch && existingFoundModels}
</> </Flex>
); );
}; };
@ -245,26 +242,26 @@ export default function SearchModels() {
<Text <Text
sx={{ sx={{
fontWeight: 500, fontWeight: 500,
fontSize: 'sm',
}} }}
variant="subtext" variant="subtext"
> >
{t('modelManager.checkpointFolder')} {t('modelManager.checkpointFolder')}
</Text> </Text>
<Text sx={{ fontWeight: 500, fontSize: 'sm' }}>{searchFolder}</Text> <Text sx={{ fontWeight: 500 }}>{searchFolder}</Text>
</Flex> </Flex>
<Spacer /> <Spacer />
<IAIIconButton <IAIIconButton
aria-label={t('modelManager.scanAgain')} aria-label={t('modelManager.scanAgain')}
tooltip={t('modelManager.scanAgain')} tooltip={t('modelManager.scanAgain')}
icon={<BiReset />} icon={<FaSearch />}
fontSize={18} fontSize={18}
disabled={isProcessing} disabled={isProcessing}
onClick={() => dispatch(searchForModels(searchFolder))} onClick={() => dispatch(searchForModels(searchFolder))}
/> />
<IAIIconButton <IAIIconButton
aria-label={t('modelManager.clearCheckpointFolder')} aria-label={t('modelManager.clearCheckpointFolder')}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />} tooltip={t('modelManager.clearCheckpointFolder')}
icon={<FaTrash />}
onClick={resetSearchModelHandler} onClick={resetSearchModelHandler}
/> />
</Flex> </Flex>
@ -276,9 +273,9 @@ export default function SearchModels() {
}} }}
> >
{({ handleSubmit }) => ( {({ handleSubmit }) => (
<form onSubmit={handleSubmit}> <IAIForm onSubmit={handleSubmit} width="100%">
<HStack columnGap={2} alignItems="flex-end" width="100%"> <HStack columnGap={2} alignItems="flex-end">
<FormControl isRequired width="lg"> <FormControl flexGrow={1}>
<Field <Field
as={IAIInput} as={IAIInput}
id="checkpointFolder" id="checkpointFolder"
@ -294,12 +291,12 @@ export default function SearchModels() {
tooltip={t('modelManager.findModels')} tooltip={t('modelManager.findModels')}
type="submit" type="submit"
disabled={isProcessing} disabled={isProcessing}
paddingX={10} px={8}
> >
{t('modelManager.findModels')} {t('modelManager.findModels')}
</IAIButton> </IAIButton>
</HStack> </HStack>
</form> </IAIForm>
)} )}
</Formik> </Formik>
)} )}
@ -410,7 +407,6 @@ export default function SearchModels() {
maxHeight={72} maxHeight={72}
overflowY="scroll" overflowY="scroll"
borderRadius="sm" borderRadius="sm"
paddingInlineEnd={4}
gap={2} gap={2}
> >
{foundModels.length > 0 ? ( {foundModels.length > 0 ? (

View File

@ -50,7 +50,6 @@ export default function ThemeChanger() {
return ( return (
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
aria-label={t('common.themeLabel')} aria-label={t('common.themeLabel')}

View File

@ -166,20 +166,8 @@ export default function InvokeTabs() {
[] []
); );
/**
* isLazy means the tabs are mounted and unmounted when changing them. There is a tradeoff here,
* as mounting is expensive, but so is retaining all tabs in the DOM at all times.
*
* Removing isLazy messes with the outside click watcher, which is used by ResizableDrawer.
* Because you have multiple handlers listening for an outside click, any click anywhere triggers
* the watcher for the hidden drawers, closing the open drawer.
*
* TODO: Add logic to the `useOutsideClick` in ResizableDrawer to enable it only for the active
* tab's drawer.
*/
return ( return (
<Tabs <Tabs
isLazy
defaultIndex={activeTab} defaultIndex={activeTab}
index={activeTab} index={activeTab}
onChange={(index: number) => { onChange={(index: number) => {

View File

@ -93,12 +93,9 @@ const ResizableDrawer = ({
useOutsideClick({ useOutsideClick({
ref: outsideClickRef, ref: outsideClickRef,
handler: () => { handler: () => {
if (isPinned) {
return;
}
onClose(); onClose();
}, },
enabled: isOpen && !isPinned,
}); });
const handleEnables = useMemo( const handleEnables = useMemo(

View File

@ -77,7 +77,6 @@ export default function UnifiedCanvasColorPicker() {
return ( return (
<IAIPopover <IAIPopover
trigger="hover"
triggerComponent={ triggerComponent={
<Box <Box
sx={{ sx={{

View File

@ -56,7 +56,7 @@ const UnifiedCanvasSettings = () => {
return ( return (
<IAIPopover <IAIPopover
trigger="hover" isLazy={false}
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
tooltip={t('unifiedCanvas.canvasSettings')} tooltip={t('unifiedCanvas.canvasSettings')}

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,7 @@ dependencies = [
"albumentations", "albumentations",
"click", "click",
"clip_anytorch", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "clip_anytorch", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip",
"compel==0.1.10", "compel==1.0.1",
"datasets", "datasets",
"diffusers[torch]~=0.14", "diffusers[torch]~=0.14",
"dnspython==2.2.1", "dnspython==2.2.1",