chore(ui): cleanup (#5084)

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [x] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission

## Description

Bit of a cleanup. 

[chore(ui): delete unused
files](5eaea9dd64)

[feat(ui): add eslint rule
react/jsx-no-bind](3a0ec635c9)

This rule enforces no arrow functions in component props. In practice,
it means all functions passed as component props must be wrapped in
`useCallback()`.

This is a performance optimization to prevent unnecessary rerenders.

The rule is added and all violations have been fixed, whew!

[chore(ui): move useCopyImageToClipboard to
common/hooks/](f2d26a3a3c)

[chore(ui): move MM components & store to
features/](bb52861896)

Somehow they had ended up in `features/ui/tabs` which isn't right

## QA Instructions, Screenshots, Recordings

UI should still work.

It builds successfully, and I tested things out - looks good to me.
This commit is contained in:
blessedcoolant 2023-11-13 13:22:41 +05:30 committed by GitHub
commit 7fcb8959fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 944 additions and 3318 deletions

View File

@ -24,6 +24,7 @@ module.exports = {
root: true, root: true,
rules: { rules: {
curly: 'error', curly: 'error',
'react/jsx-no-bind': ['error', { allowBind: true }],
'react/jsx-curly-brace-presence': [ 'react/jsx-curly-brace-presence': [
'error', 'error',
{ props: 'never', children: 'never' }, { props: 'never', children: 'never' },

View File

@ -19,7 +19,7 @@ import sdxlReducer from 'features/sdxl/store/sdxlSlice';
import configReducer from 'features/system/store/configSlice'; import configReducer from 'features/system/store/configSlice';
import systemReducer from 'features/system/store/systemSlice'; import systemReducer from 'features/system/store/systemSlice';
import queueReducer from 'features/queue/store/queueSlice'; import queueReducer from 'features/queue/store/queueSlice';
import modelmanagerReducer from 'features/ui/components/tabs/ModelManager/store/modelManagerSlice'; import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice'; import uiReducer from 'features/ui/store/uiSlice';
import dynamicMiddlewares from 'redux-dynamic-middlewares'; import dynamicMiddlewares from 'redux-dynamic-middlewares';

View File

@ -8,7 +8,14 @@ import {
forwardRef, forwardRef,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { cloneElement, memo, ReactElement, ReactNode, useRef } from 'react'; import {
cloneElement,
memo,
ReactElement,
ReactNode,
useCallback,
useRef,
} from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IAIButton from './IAIButton'; import IAIButton from './IAIButton';
@ -38,15 +45,15 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef<HTMLButtonElement | null>(null); const cancelRef = useRef<HTMLButtonElement | null>(null);
const handleAccept = () => { const handleAccept = useCallback(() => {
acceptCallback(); acceptCallback();
onClose(); onClose();
}; }, [acceptCallback, onClose]);
const handleCancel = () => { const handleCancel = useCallback(() => {
cancelCallback && cancelCallback(); cancelCallback && cancelCallback();
onClose(); onClose();
}; }, [cancelCallback, onClose]);
return ( return (
<> <>

View File

@ -1,43 +0,0 @@
import { Box, Flex, Icon } from '@chakra-ui/react';
import { memo } from 'react';
import { FaExclamation } from 'react-icons/fa';
const IAIErrorLoadingImageFallback = () => {
return (
<Box
sx={{
position: 'relative',
height: 'full',
width: 'full',
'::before': {
content: "''",
display: 'block',
pt: '100%',
},
}}
>
<Flex
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
height: 'full',
width: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
bg: 'base.100',
color: 'base.500',
_dark: {
color: 'base.700',
bg: 'base.850',
},
}}
>
<Icon as={FaExclamation} boxSize={16} opacity={0.7} />
</Flex>
</Box>
);
};
export default memo(IAIErrorLoadingImageFallback);

View File

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

View File

@ -1,15 +0,0 @@
import { FormErrorMessage, FormErrorMessageProps } from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIFormErrorMessageProps = FormErrorMessageProps & {
children: ReactNode | string;
};
export default function IAIFormErrorMessage(props: IAIFormErrorMessageProps) {
const { children, ...rest } = props;
return (
<FormErrorMessage color="error.400" {...rest}>
{children}
</FormErrorMessage>
);
}

View File

@ -1,15 +0,0 @@
import { FormHelperText, FormHelperTextProps } from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIFormHelperTextProps = FormHelperTextProps & {
children: ReactNode | string;
};
export default function IAIFormHelperText(props: IAIFormHelperTextProps) {
const { children, ...rest } = props;
return (
<FormHelperText margin={0} color="base.400" {...rest}>
{children}
</FormHelperText>
);
}

View File

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

View File

@ -1,25 +0,0 @@
import {
Checkbox,
CheckboxProps,
FormControl,
FormControlProps,
FormLabel,
} from '@chakra-ui/react';
import { memo, ReactNode } from 'react';
type IAIFullCheckboxProps = CheckboxProps & {
label: string | ReactNode;
formControlProps?: FormControlProps;
};
const IAIFullCheckbox = (props: IAIFullCheckboxProps) => {
const { label, formControlProps, ...rest } = props;
return (
<FormControl {...formControlProps}>
<FormLabel>{label}</FormLabel>
<Checkbox colorScheme="accent" {...rest} />
</FormControl>
);
};
export default memo(IAIFullCheckbox);

View File

@ -1,6 +1,7 @@
import { useColorMode } from '@chakra-ui/react'; import { useColorMode } from '@chakra-ui/react';
import { TextInput, TextInputProps } from '@mantine/core'; import { TextInput, TextInputProps } from '@mantine/core';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { useCallback } from 'react';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
type IAIMantineTextInputProps = TextInputProps; type IAIMantineTextInputProps = TextInputProps;
@ -20,26 +21,37 @@ export default function IAIMantineTextInput(props: IAIMantineTextInputProps) {
} = useChakraThemeTokens(); } = useChakraThemeTokens();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
return ( const stylesFunc = useCallback(
<TextInput () => ({
styles={() => ({ input: {
input: { color: mode(base900, base100)(colorMode),
color: mode(base900, base100)(colorMode), backgroundColor: mode(base50, base900)(colorMode),
backgroundColor: mode(base50, base900)(colorMode), borderColor: mode(base200, base800)(colorMode),
borderColor: mode(base200, base800)(colorMode), borderWidth: 2,
borderWidth: 2, outline: 'none',
outline: 'none', ':focus': {
':focus': { borderColor: mode(accent300, accent500)(colorMode),
borderColor: mode(accent300, accent500)(colorMode),
},
}, },
label: { },
color: mode(base700, base300)(colorMode), label: {
fontWeight: 'normal', color: mode(base700, base300)(colorMode),
marginBottom: 4, fontWeight: 'normal' as const,
}, marginBottom: 4,
})} },
{...rest} }),
/> [
accent300,
accent500,
base100,
base200,
base300,
base50,
base700,
base800,
base900,
colorMode,
]
); );
return <TextInput styles={stylesFunc} {...rest} />;
} }

View File

@ -98,28 +98,34 @@ const IAINumberInput = forwardRef((props: Props, ref) => {
} }
}, [value, valueAsString]); }, [value, valueAsString]);
const handleOnChange = (v: string) => { const handleOnChange = useCallback(
setValueAsString(v); (v: string) => {
// This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc. setValueAsString(v);
if (!v.match(numberStringRegex)) { // This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc.
// Cast the value to number. Floor it if it should be an integer. if (!v.match(numberStringRegex)) {
onChange(isInteger ? Math.floor(Number(v)) : Number(v)); // Cast the value to number. Floor it if it should be an integer.
} onChange(isInteger ? Math.floor(Number(v)) : Number(v));
}; }
},
[isInteger, onChange]
);
/** /**
* Clicking the steppers allows the value to go outside bounds; we need to * Clicking the steppers allows the value to go outside bounds; we need to
* clamp it on blur and floor it if needed. * clamp it on blur and floor it if needed.
*/ */
const handleBlur = (e: FocusEvent<HTMLInputElement>) => { const handleBlur = useCallback(
const clamped = clamp( (e: FocusEvent<HTMLInputElement>) => {
isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value), const clamped = clamp(
min, isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value),
max min,
); max
setValueAsString(String(clamped)); );
onChange(clamped); setValueAsString(String(clamped));
}; onChange(clamped);
},
[isInteger, max, min, onChange]
);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {

View File

@ -6,7 +6,7 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo, MouseEvent } from 'react'; import { memo, MouseEvent, useCallback } from 'react';
import IAIOption from './IAIOption'; import IAIOption from './IAIOption';
type IAISelectProps = SelectProps & { type IAISelectProps = SelectProps & {
@ -33,15 +33,16 @@ const IAISelect = (props: IAISelectProps) => {
spaceEvenly, spaceEvenly,
...rest ...rest
} = props; } = props;
const handleClick = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}, []);
return ( return (
<FormControl <FormControl
isDisabled={isDisabled} isDisabled={isDisabled}
onClick={(e: MouseEvent<HTMLDivElement>) => { onClick={handleClick}
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
sx={ sx={
horizontal horizontal
? { ? {

View File

@ -186,6 +186,13 @@ const IAISlider = forwardRef((props: IAIFullSliderProps, ref) => {
[dispatch] [dispatch]
); );
const handleMouseEnter = useCallback(() => setShowTooltip(true), []);
const handleMouseLeave = useCallback(() => setShowTooltip(false), []);
const handleStepperClick = useCallback(
() => onChange(Number(localInputValue)),
[localInputValue, onChange]
);
return ( return (
<FormControl <FormControl
ref={ref} ref={ref}
@ -219,8 +226,8 @@ const IAISlider = forwardRef((props: IAIFullSliderProps, ref) => {
max={max} max={max}
step={step} step={step}
onChange={handleSliderChange} onChange={handleSliderChange}
onMouseEnter={() => setShowTooltip(true)} onMouseEnter={handleMouseEnter}
onMouseLeave={() => setShowTooltip(false)} onMouseLeave={handleMouseLeave}
focusThumbOnChange={false} focusThumbOnChange={false}
isDisabled={isDisabled} isDisabled={isDisabled}
{...rest} {...rest}
@ -332,12 +339,8 @@ const IAISlider = forwardRef((props: IAIFullSliderProps, ref) => {
{...sliderNumberInputFieldProps} {...sliderNumberInputFieldProps}
/> />
<NumberInputStepper {...sliderNumberInputStepperProps}> <NumberInputStepper {...sliderNumberInputStepperProps}>
<NumberIncrementStepper <NumberIncrementStepper onClick={handleStepperClick} />
onClick={() => onChange(Number(localInputValue))} <NumberDecrementStepper onClick={handleStepperClick} />
/>
<NumberDecrementStepper
onClick={() => onChange(Number(localInputValue))}
/>
</NumberInputStepper> </NumberInputStepper>
</NumberInput> </NumberInput>
)} )}

View File

@ -146,16 +146,15 @@ const ImageUploader = (props: ImageUploaderProps) => {
}; };
}, [inputRef]); }, [inputRef]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
// Bail out if user hits spacebar - do not open the uploader
if (e.key === ' ') {
return;
}
}, []);
return ( return (
<Box <Box {...getRootProps({ style: {} })} onKeyDown={handleKeyDown}>
{...getRootProps({ style: {} })}
onKeyDown={(e: KeyboardEvent) => {
// Bail out if user hits spacebar - do not open the uploader
if (e.key === ' ') {
return;
}
}}
>
<input {...getInputProps()} /> <input {...getInputProps()} />
{children} {children}
<AnimatePresence> <AnimatePresence>

View File

@ -1,23 +0,0 @@
import { Flex, Icon } from '@chakra-ui/react';
import { memo } from 'react';
import { FaImage } from 'react-icons/fa';
const SelectImagePlaceholder = () => {
return (
<Flex
sx={{
w: 'full',
h: 'full',
// bg: 'base.800',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
aspectRatio: '1/1',
}}
>
<Icon color="base.400" boxSize={32} as={FaImage}></Icon>
</Flex>
);
};
export default memo(SelectImagePlaceholder);

View File

@ -1,24 +0,0 @@
import { useBreakpoint } from '@chakra-ui/react';
export default function useResolution():
| 'mobile'
| 'tablet'
| 'desktop'
| 'unknown' {
const breakpointValue = useBreakpoint();
const mobileResolutions = ['base', 'sm'];
const tabletResolutions = ['md', 'lg'];
const desktopResolutions = ['xl', '2xl'];
if (mobileResolutions.includes(breakpointValue)) {
return 'mobile';
}
if (tabletResolutions.includes(breakpointValue)) {
return 'tablet';
}
if (desktopResolutions.includes(breakpointValue)) {
return 'desktop';
}
return 'unknown';
}

View File

@ -1,7 +0,0 @@
import dateFormat from 'dateformat';
/**
* Get a `now` timestamp with 1s precision, formatted as ISO datetime.
*/
export const getTimestamp = () =>
dateFormat(new Date(), `yyyy-mm-dd'T'HH:MM:ss:lo`);

View File

@ -1,71 +0,0 @@
// TODO: Restore variations
// Support code from v2.3 in here.
// export const stringToSeedWeights = (
// string: string
// ): InvokeAI.SeedWeights | boolean => {
// const stringPairs = string.split(',');
// const arrPairs = stringPairs.map((p) => p.split(':'));
// const pairs = arrPairs.map((p: Array<string>): InvokeAI.SeedWeightPair => {
// return { seed: Number(p[0]), weight: Number(p[1]) };
// });
// if (!validateSeedWeights(pairs)) {
// return false;
// }
// return pairs;
// };
// export const validateSeedWeights = (
// seedWeights: InvokeAI.SeedWeights | string
// ): boolean => {
// return typeof seedWeights === 'string'
// ? Boolean(stringToSeedWeights(seedWeights))
// : Boolean(
// seedWeights.length &&
// !seedWeights.some((pair: InvokeAI.SeedWeightPair) => {
// const { seed, weight } = pair;
// const isSeedValid = !isNaN(parseInt(seed.toString(), 10));
// const isWeightValid =
// !isNaN(parseInt(weight.toString(), 10)) &&
// weight >= 0 &&
// weight <= 1;
// return !(isSeedValid && isWeightValid);
// })
// );
// };
// export const seedWeightsToString = (
// seedWeights: InvokeAI.SeedWeights
// ): string => {
// return seedWeights.reduce((acc, pair, i, arr) => {
// const { seed, weight } = pair;
// acc += `${seed}:${weight}`;
// if (i !== arr.length - 1) {
// acc += ',';
// }
// return acc;
// }, '');
// };
// export const seedWeightsToArray = (
// seedWeights: InvokeAI.SeedWeights
// ): Array<Array<number>> => {
// return seedWeights.map((pair: InvokeAI.SeedWeightPair) => [
// pair.seed,
// pair.weight,
// ]);
// };
// export const stringToSeedWeightsArray = (
// string: string
// ): Array<Array<number>> => {
// const stringPairs = string.split(',');
// const arrPairs = stringPairs.map((p) => p.split(':'));
// return arrPairs.map(
// (p: Array<string>): Array<number> => [parseInt(p[0], 10), parseFloat(p[1])]
// );
// };
export default {};

View File

@ -5,17 +5,22 @@ import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
import { isStagingSelector } from '../store/canvasSelectors'; import { isStagingSelector } from '../store/canvasSelectors';
import { memo } from 'react'; import { memo, useCallback } from 'react';
const ClearCanvasHistoryButtonModal = () => { const ClearCanvasHistoryButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector); const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const acceptCallback = useCallback(
() => dispatch(clearCanvasHistory()),
[dispatch]
);
return ( return (
<IAIAlertDialog <IAIAlertDialog
title={t('unifiedCanvas.clearCanvasHistory')} title={t('unifiedCanvas.clearCanvasHistory')}
acceptCallback={() => dispatch(clearCanvasHistory())} acceptCallback={acceptCallback}
acceptButtonText={t('unifiedCanvas.clearHistory')} acceptButtonText={t('unifiedCanvas.clearHistory')}
triggerComponent={ triggerComponent={
<IAIButton size="sm" leftIcon={<FaTrash />} isDisabled={isStaging}> <IAIButton size="sm" leftIcon={<FaTrash />} isDisabled={isStaging}>

View File

@ -20,7 +20,8 @@ import {
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { memo } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { RgbaColor } from 'react-colorful';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -95,18 +96,35 @@ const IAICanvasMaskOptions = () => {
[isMaskEnabled] [isMaskEnabled]
); );
const handleToggleMaskLayer = () => { const handleToggleMaskLayer = useCallback(() => {
dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'));
}; }, [dispatch, layer]);
const handleClearMask = () => dispatch(clearMask()); const handleClearMask = useCallback(() => {
dispatch(clearMask());
}, [dispatch]);
const handleToggleEnableMask = () => const handleToggleEnableMask = useCallback(() => {
dispatch(setIsMaskEnabled(!isMaskEnabled)); dispatch(setIsMaskEnabled(!isMaskEnabled));
}, [dispatch, isMaskEnabled]);
const handleSaveMask = async () => { const handleSaveMask = useCallback(async () => {
dispatch(canvasMaskSavedToGallery()); dispatch(canvasMaskSavedToGallery());
}; }, [dispatch]);
const handleChangePreserveMaskedArea = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(setShouldPreserveMaskedArea(e.target.checked));
},
[dispatch]
);
const handleChangeMaskColor = useCallback(
(newColor: RgbaColor) => {
dispatch(setMaskColor(newColor));
},
[dispatch]
);
return ( return (
<IAIPopover <IAIPopover
@ -131,15 +149,10 @@ const IAICanvasMaskOptions = () => {
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.preserveMaskedArea')} label={t('unifiedCanvas.preserveMaskedArea')}
isChecked={shouldPreserveMaskedArea} isChecked={shouldPreserveMaskedArea}
onChange={(e) => onChange={handleChangePreserveMaskedArea}
dispatch(setShouldPreserveMaskedArea(e.target.checked))
}
/> />
<Box sx={{ paddingTop: 2, paddingBottom: 2 }}> <Box sx={{ paddingTop: 2, paddingBottom: 2 }}>
<IAIColorPicker <IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
color={maskColor}
onChange={(newColor) => dispatch(setMaskColor(newColor))}
/>
</Box> </Box>
<IAIButton size="sm" leftIcon={<FaSave />} onClick={handleSaveMask}> <IAIButton size="sm" leftIcon={<FaSave />} onClick={handleSaveMask}>
Save Mask Save Mask

View File

@ -10,6 +10,7 @@ import { redo } from 'features/canvas/store/canvasSlice';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCallback } from 'react';
const canvasRedoSelector = createSelector( const canvasRedoSelector = createSelector(
[stateSelector, activeTabNameSelector], [stateSelector, activeTabNameSelector],
@ -34,9 +35,9 @@ export default function IAICanvasRedoButton() {
const { t } = useTranslation(); const { t } = useTranslation();
const handleRedo = () => { const handleRedo = useCallback(() => {
dispatch(redo()); dispatch(redo());
}; }, [dispatch]);
useHotkeys( useHotkeys(
['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'], ['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'],

View File

@ -18,7 +18,7 @@ import {
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { ChangeEvent, memo } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa'; import { FaWrench } from 'react-icons/fa';
@ -86,8 +86,52 @@ const IAICanvasSettingsButtonPopover = () => {
[shouldSnapToGrid] [shouldSnapToGrid]
); );
const handleChangeShouldSnapToGrid = (e: ChangeEvent<HTMLInputElement>) => const handleChangeShouldSnapToGrid = useCallback(
dispatch(setShouldSnapToGrid(e.target.checked)); (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldSnapToGrid(e.target.checked)),
[dispatch]
);
const handleChangeShouldShowIntermediates = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldShowIntermediates(e.target.checked)),
[dispatch]
);
const handleChangeShouldShowGrid = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldShowGrid(e.target.checked)),
[dispatch]
);
const handleChangeShouldDarkenOutsideBoundingBox = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)),
[dispatch]
);
const handleChangeShouldAutoSave = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoSave(e.target.checked)),
[dispatch]
);
const handleChangeShouldCropToBoundingBoxOnSave = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)),
[dispatch]
);
const handleChangeShouldRestrictStrokesToBox = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRestrictStrokesToBox(e.target.checked)),
[dispatch]
);
const handleChangeShouldShowCanvasDebugInfo = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldShowCanvasDebugInfo(e.target.checked)),
[dispatch]
);
const handleChangeShouldAntialias = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAntialias(e.target.checked)),
[dispatch]
);
return ( return (
<IAIPopover <IAIPopover
@ -104,14 +148,12 @@ const IAICanvasSettingsButtonPopover = () => {
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.showIntermediates')} label={t('unifiedCanvas.showIntermediates')}
isChecked={shouldShowIntermediates} isChecked={shouldShowIntermediates}
onChange={(e) => onChange={handleChangeShouldShowIntermediates}
dispatch(setShouldShowIntermediates(e.target.checked))
}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.showGrid')} label={t('unifiedCanvas.showGrid')}
isChecked={shouldShowGrid} isChecked={shouldShowGrid}
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))} onChange={handleChangeShouldShowGrid}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.snapToGrid')} label={t('unifiedCanvas.snapToGrid')}
@ -121,41 +163,33 @@ const IAICanvasSettingsButtonPopover = () => {
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.darkenOutsideSelection')} label={t('unifiedCanvas.darkenOutsideSelection')}
isChecked={shouldDarkenOutsideBoundingBox} isChecked={shouldDarkenOutsideBoundingBox}
onChange={(e) => onChange={handleChangeShouldDarkenOutsideBoundingBox}
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.autoSaveToGallery')} label={t('unifiedCanvas.autoSaveToGallery')}
isChecked={shouldAutoSave} isChecked={shouldAutoSave}
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))} onChange={handleChangeShouldAutoSave}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.saveBoxRegionOnly')} label={t('unifiedCanvas.saveBoxRegionOnly')}
isChecked={shouldCropToBoundingBoxOnSave} isChecked={shouldCropToBoundingBoxOnSave}
onChange={(e) => onChange={handleChangeShouldCropToBoundingBoxOnSave}
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.limitStrokesToBox')} label={t('unifiedCanvas.limitStrokesToBox')}
isChecked={shouldRestrictStrokesToBox} isChecked={shouldRestrictStrokesToBox}
onChange={(e) => onChange={handleChangeShouldRestrictStrokesToBox}
dispatch(setShouldRestrictStrokesToBox(e.target.checked))
}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.showCanvasDebugInfo')} label={t('unifiedCanvas.showCanvasDebugInfo')}
isChecked={shouldShowCanvasDebugInfo} isChecked={shouldShowCanvasDebugInfo}
onChange={(e) => onChange={handleChangeShouldShowCanvasDebugInfo}
dispatch(setShouldShowCanvasDebugInfo(e.target.checked))
}
/> />
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('unifiedCanvas.antialiasing')} label={t('unifiedCanvas.antialiasing')}
isChecked={shouldAntialias} isChecked={shouldAntialias}
onChange={(e) => dispatch(setShouldAntialias(e.target.checked))} onChange={handleChangeShouldAntialias}
/> />
<ClearCanvasHistoryButtonModal /> <ClearCanvasHistoryButtonModal />
</Flex> </Flex>

View File

@ -15,7 +15,8 @@ import {
setTool, setTool,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { RgbaColor } from 'react-colorful';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -172,11 +173,33 @@ const IAICanvasToolChooserOptions = () => {
[brushColor] [brushColor]
); );
const handleSelectBrushTool = () => dispatch(setTool('brush')); const handleSelectBrushTool = useCallback(() => {
const handleSelectEraserTool = () => dispatch(setTool('eraser')); dispatch(setTool('brush'));
const handleSelectColorPickerTool = () => dispatch(setTool('colorPicker')); }, [dispatch]);
const handleFillRect = () => dispatch(addFillRect()); const handleSelectEraserTool = useCallback(() => {
const handleEraseBoundingBox = () => dispatch(addEraseRect()); dispatch(setTool('eraser'));
}, [dispatch]);
const handleSelectColorPickerTool = useCallback(() => {
dispatch(setTool('colorPicker'));
}, [dispatch]);
const handleFillRect = useCallback(() => {
dispatch(addFillRect());
}, [dispatch]);
const handleEraseBoundingBox = useCallback(() => {
dispatch(addEraseRect());
}, [dispatch]);
const handleChangeBrushSize = useCallback(
(newSize: number) => {
dispatch(setBrushSize(newSize));
},
[dispatch]
);
const handleChangeBrushColor = useCallback(
(newColor: RgbaColor) => {
dispatch(setBrushColor(newColor));
},
[dispatch]
);
return ( return (
<ButtonGroup isAttached> <ButtonGroup isAttached>
@ -233,7 +256,7 @@ const IAICanvasToolChooserOptions = () => {
label={t('unifiedCanvas.brushSize')} label={t('unifiedCanvas.brushSize')}
value={brushSize} value={brushSize}
withInput withInput
onChange={(newSize) => dispatch(setBrushSize(newSize))} onChange={handleChangeBrushSize}
sliderNumberInputProps={{ max: 500 }} sliderNumberInputProps={{ max: 500 }}
/> />
</Flex> </Flex>
@ -247,7 +270,7 @@ const IAICanvasToolChooserOptions = () => {
<IAIColorPicker <IAIColorPicker
withNumberInput={true} withNumberInput={true}
color={brushColor} color={brushColor}
onChange={(newColor) => dispatch(setBrushColor(newColor))} onChange={handleChangeBrushColor}
/> />
</Box> </Box>
</Flex> </Flex>

View File

@ -25,9 +25,9 @@ import {
LAYER_NAMES_DICT, LAYER_NAMES_DICT,
} from 'features/canvas/store/canvasTypes'; } from 'features/canvas/store/canvasTypes';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -151,7 +151,9 @@ const IAICanvasToolbar = () => {
[canvasBaseLayer] [canvasBaseLayer]
); );
const handleSelectMoveTool = () => dispatch(setTool('move')); const handleSelectMoveTool = useCallback(() => {
dispatch(setTool('move'));
}, [dispatch]);
const handleClickResetCanvasView = useSingleAndDoubleClick( const handleClickResetCanvasView = useSingleAndDoubleClick(
() => handleResetCanvasView(false), () => handleResetCanvasView(false),
@ -174,36 +176,39 @@ const IAICanvasToolbar = () => {
); );
}; };
const handleResetCanvas = () => { const handleResetCanvas = useCallback(() => {
dispatch(resetCanvas()); dispatch(resetCanvas());
}; }, [dispatch]);
const handleMergeVisible = () => { const handleMergeVisible = useCallback(() => {
dispatch(canvasMerged()); dispatch(canvasMerged());
}; }, [dispatch]);
const handleSaveToGallery = () => { const handleSaveToGallery = useCallback(() => {
dispatch(canvasSavedToGallery()); dispatch(canvasSavedToGallery());
}; }, [dispatch]);
const handleCopyImageToClipboard = () => { const handleCopyImageToClipboard = useCallback(() => {
if (!isClipboardAPIAvailable) { if (!isClipboardAPIAvailable) {
return; return;
} }
dispatch(canvasCopiedToClipboard()); dispatch(canvasCopiedToClipboard());
}; }, [dispatch, isClipboardAPIAvailable]);
const handleDownloadAsImage = () => { const handleDownloadAsImage = useCallback(() => {
dispatch(canvasDownloadedAsImage()); dispatch(canvasDownloadedAsImage());
}; }, [dispatch]);
const handleChangeLayer = (v: string) => { const handleChangeLayer = useCallback(
const newLayer = v as CanvasLayer; (v: string) => {
dispatch(setLayer(newLayer)); const newLayer = v as CanvasLayer;
if (newLayer === 'mask' && !isMaskEnabled) { dispatch(setLayer(newLayer));
dispatch(setIsMaskEnabled(true)); if (newLayer === 'mask' && !isMaskEnabled) {
} dispatch(setIsMaskEnabled(true));
}; }
},
[dispatch, isMaskEnabled]
);
return ( return (
<Flex <Flex

View File

@ -10,6 +10,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useCallback } from 'react';
const canvasUndoSelector = createSelector( const canvasUndoSelector = createSelector(
[stateSelector, activeTabNameSelector], [stateSelector, activeTabNameSelector],
@ -35,9 +36,9 @@ export default function IAICanvasUndoButton() {
const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector); const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector);
const handleUndo = () => { const handleUndo = useCallback(() => {
dispatch(undo()); dispatch(undo());
}; }, [dispatch]);
useHotkeys( useHotkeys(
['meta+z', 'ctrl+z'], ['meta+z', 'ctrl+z'],

View File

@ -1,16 +0,0 @@
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
/**
* Converts a Konva node to a dataURL
* @param node - The Konva node to convert to a dataURL
* @param boundingBox - The bounding box to crop to
* @returns A dataURL of the node cropped to the bounding box
*/
export const konvaNodeToDataURL = (
node: Konva.Node,
boundingBox: IRect
): string => {
// get a dataURL of the bbox'd region
return node.toDataURL(boundingBox);
};

View File

@ -87,6 +87,11 @@ const ChangeBoardModal = () => {
selectedBoard, selectedBoard,
]); ]);
const handleSetSelectedBoard = useCallback(
(v: string | null) => setSelectedBoard(v),
[]
);
const cancelRef = useRef<HTMLButtonElement>(null); const cancelRef = useRef<HTMLButtonElement>(null);
return ( return (
@ -113,7 +118,7 @@ const ChangeBoardModal = () => {
isFetching ? t('boards.loading') : t('boards.selectBoard') isFetching ? t('boards.loading') : t('boards.selectBoard')
} }
disabled={isFetching} disabled={isFetching}
onChange={(v) => setSelectedBoard(v)} onChange={handleSetSelectedBoard}
value={selectedBoard} value={selectedBoard}
data={data} data={data}
/> />

View File

@ -1,36 +0,0 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
import { memo, useCallback } from 'react';
import { useControlAdapterControlImage } from '../hooks/useControlAdapterControlImage';
import { controlAdapterImageProcessed } from '../store/actions';
type Props = {
id: string;
};
const ControlAdapterPreprocessButton = ({ id }: Props) => {
const controlImage = useControlAdapterControlImage(id);
const dispatch = useAppDispatch();
const isReady = useIsReadyToEnqueue();
const handleProcess = useCallback(() => {
dispatch(
controlAdapterImageProcessed({
id,
})
);
}, [id, dispatch]);
return (
<IAIButton
size="sm"
onClick={handleProcess}
isDisabled={Boolean(!controlImage) || !isReady}
>
Preprocess
</IAIButton>
);
};
export default memo(ControlAdapterPreprocessButton);

View File

@ -14,9 +14,9 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react'; import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren & { type Props = PropsWithChildren & {
onSelect: (v: string) => void; onSelect: (v: string) => void;
@ -78,6 +78,13 @@ const ParamEmbeddingPopover = (props: Props) => {
[onSelect] [onSelect]
); );
const filterFunc = useCallback(
(value: string, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()),
[]
);
return ( return (
<Popover <Popover
initialFocusRef={inputRef} initialFocusRef={inputRef}
@ -127,12 +134,7 @@ const ParamEmbeddingPopover = (props: Props) => {
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0} disabled={data.length === 0}
onDropdownClose={onClose} onDropdownClose={onClose}
filter={(value, item: SelectItem) => filter={filterFunc}
item.label
?.toLowerCase()
.includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
onChange={handleChange} onChange={handleChange}
/> />
)} )}

View File

@ -60,6 +60,13 @@ const BoardAutoAddSelect = () => {
[dispatch] [dispatch]
); );
const filterFunc = useCallback(
(value: string, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()),
[]
);
return ( return (
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('boards.autoAddBoard')} label={t('boards.autoAddBoard')}
@ -71,10 +78,7 @@ const BoardAutoAddSelect = () => {
nothingFound={t('boards.noMatching')} nothingFound={t('boards.noMatching')}
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={!hasBoards || autoAssignBoardOnClick} disabled={!hasBoards || autoAssignBoardOnClick}
filter={(value, item: SelectItem) => filter={filterFunc}
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
onChange={handleChange} onChange={handleChange}
/> />
); );

View File

@ -90,6 +90,50 @@ const BoardContextMenu = ({
e.preventDefault(); e.preventDefault();
}, []); }, []);
const renderMenuFunc = useCallback(
() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup title={boardName}>
<MenuItem
icon={<FaPlus />}
isDisabled={isAutoAdd || autoAssignBoardOnClick}
onClick={handleSetAutoAdd}
>
{t('boards.menuItemAutoAdd')}
</MenuItem>
{isBulkDownloadEnabled && (
<MenuItem icon={<FaDownload />} onClickCapture={handleBulkDownload}>
{t('boards.downloadBoard')}
</MenuItem>
)}
{!board && <NoBoardContextMenuItems />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuGroup>
</MenuList>
),
[
autoAssignBoardOnClick,
board,
boardName,
handleBulkDownload,
handleSetAutoAdd,
isAutoAdd,
isBulkDownloadEnabled,
setBoardToDelete,
skipEvent,
t,
]
);
return ( return (
<IAIContextMenu<HTMLDivElement> <IAIContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
@ -97,38 +141,7 @@ const BoardContextMenu = ({
bg: 'transparent', bg: 'transparent',
_hover: { bg: 'transparent' }, _hover: { bg: 'transparent' },
}} }}
renderMenu={() => ( renderMenu={renderMenuFunc}
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup title={boardName}>
<MenuItem
icon={<FaPlus />}
isDisabled={isAutoAdd || autoAssignBoardOnClick}
onClick={handleSetAutoAdd}
>
{t('boards.menuItemAutoAdd')}
</MenuItem>
{isBulkDownloadEnabled && (
<MenuItem
icon={<FaDownload />}
onClickCapture={handleBulkDownload}
>
{t('boards.downloadBoard')}
</MenuItem>
)}
{!board && <NoBoardContextMenuItems />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuGroup>
</MenuList>
)}
> >
{children} {children}
</IAIContextMenu> </IAIContextMenu>

View File

@ -1,108 +0,0 @@
import { As, Badge, Flex } from '@chakra-ui/react';
import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { TypesafeDroppableData } from 'features/dnd/types';
import { BoardId } from 'features/gallery/store/types';
import { ReactNode, memo } from 'react';
import BoardContextMenu from '../BoardContextMenu';
type GenericBoardProps = {
board_id: BoardId;
droppableData?: TypesafeDroppableData;
onClick: () => void;
isSelected: boolean;
icon: As;
label: string;
dropLabel?: ReactNode;
badgeCount?: number;
};
export const formatBadgeCount = (count: number) =>
Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
}).format(count);
const GenericBoard = (props: GenericBoardProps) => {
const {
board_id,
droppableData,
onClick,
isSelected,
icon,
label,
badgeCount,
dropLabel,
} = props;
return (
<BoardContextMenu board_id={board_id}>
{(ref) => (
<Flex
ref={ref}
sx={{
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}}
>
<Flex
onClick={onClick}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
shadow: isSelected ? 'selected.light' : undefined,
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
flexShrink: 0,
}}
>
<IAINoContentFallback
boxSize={8}
icon={icon}
sx={{
border: '2px solid var(--invokeai-colors-base-200)',
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
}}
/>
<Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
{badgeCount !== undefined && (
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
)}
</Flex>
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
</Flex>
<Flex
sx={{
h: 'full',
alignItems: 'center',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'sm',
color: isSelected ? 'base.900' : 'base.700',
_dark: { color: isSelected ? 'base.50' : 'base.200' },
}}
>
{label}
</Flex>
</Flex>
)}
</BoardContextMenu>
);
};
export default memo(GenericBoard);

View File

@ -1,53 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIButton from 'common/components/IAIButton';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useBoardName } from 'services/api/hooks/useBoardName';
type Props = {
board_id: 'images' | 'assets' | 'no_board';
};
const SystemBoardButton = ({ board_id }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
[stateSelector],
({ gallery }) => {
const { selectedBoardId } = gallery;
return { isSelected: selectedBoardId === board_id };
},
defaultSelectorOptions
),
[board_id]
);
const { isSelected } = useAppSelector(selector);
const boardName = useBoardName(board_id);
const handleClick = useCallback(() => {
dispatch(boardIdSelected({ boardId: board_id }));
}, [board_id, dispatch]);
return (
<IAIButton
onClick={handleClick}
size="sm"
isChecked={isSelected}
sx={{
flexGrow: 1,
borderRadius: 'base',
}}
>
{boardName}
</IAIButton>
);
};
export default memo(SystemBoardButton);

View File

@ -1,22 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { FaEyeSlash } from 'react-icons/fa';
const CurrentImageHidden = () => {
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
color: 'base.400',
}}
>
<FaEyeSlash fontSize="25vh" />
</Flex>
);
};
export default memo(CurrentImageHidden);

View File

@ -61,6 +61,12 @@ const GallerySettingsPopover = () => {
[dispatch] [dispatch]
); );
const handleChangeAutoAssignBoardOnClick = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(autoAssignBoardOnClickChanged(e.target.checked)),
[dispatch]
);
return ( return (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
@ -91,9 +97,7 @@ const GallerySettingsPopover = () => {
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('gallery.autoAssignBoardOnClick')} label={t('gallery.autoAssignBoardOnClick')}
isChecked={autoAssignBoardOnClick} isChecked={autoAssignBoardOnClick}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={handleChangeAutoAssignBoardOnClick}
dispatch(autoAssignBoardOnClickChanged(e.target.checked))
}
/> />
<BoardAutoAddSelect /> <BoardAutoAddSelect />
</Flex> </Flex>

View File

@ -35,6 +35,34 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
e.preventDefault(); e.preventDefault();
}, []); }, []);
const renderMenuFunc = useCallback(() => {
if (!imageDTO) {
return null;
}
if (selectionCount > 1) {
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MultipleSelectionMenuItems />
</MenuList>
);
}
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<SingleSelectionMenuItems imageDTO={imageDTO} />
</MenuList>
);
}, [imageDTO, selectionCount, skipEvent]);
return ( return (
<IAIContextMenu<HTMLDivElement> <IAIContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
@ -42,33 +70,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
bg: 'transparent', bg: 'transparent',
_hover: { bg: 'transparent' }, _hover: { bg: 'transparent' },
}} }}
renderMenu={() => { renderMenu={renderMenuFunc}
if (!imageDTO) {
return null;
}
if (selectionCount > 1) {
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MultipleSelectionMenuItems />
</MenuList>
);
}
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<SingleSelectionMenuItems imageDTO={imageDTO} />
</MenuList>
);
}}
> >
{children} {children}
</IAIContextMenu> </IAIContextMenu>

View File

@ -13,7 +13,7 @@ import { workflowLoadRequested } from 'features/nodes/store/actions';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';

View File

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

View File

@ -20,6 +20,7 @@ import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import GalleryImage from './GalleryImage'; import GalleryImage from './GalleryImage';
import ImageGridItemContainer from './ImageGridItemContainer'; import ImageGridItemContainer from './ImageGridItemContainer';
import ImageGridListContainer from './ImageGridListContainer'; import ImageGridListContainer from './ImageGridListContainer';
import { EntityId } from '@reduxjs/toolkit';
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = { const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
defer: true, defer: true,
@ -71,6 +72,13 @@ const GalleryImageGrid = () => {
}); });
}, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]); }, [areMoreAvailable, listImages, queryArgs, currentData?.ids.length]);
const itemContentFunc = useCallback(
(index: number, imageName: EntityId) => (
<GalleryImage key={imageName} imageName={imageName as string} />
),
[]
);
useEffect(() => { useEffect(() => {
// Initialize the gallery's custom scrollbar // Initialize the gallery's custom scrollbar
const { current: root } = rootRef; const { current: root } = rootRef;
@ -131,9 +139,7 @@ const GalleryImageGrid = () => {
List: ImageGridListContainer, List: ImageGridListContainer,
}} }}
scrollerRef={setScroller} scrollerRef={setScroller}
itemContent={(index, imageName) => ( itemContent={itemContentFunc}
<GalleryImage key={imageName} imageName={imageName as string} />
)}
/> />
</Box> </Box>
<IAIButton <IAIButton

View File

@ -279,7 +279,7 @@ const ImageMetadataActions = (props: Props) => {
key={index} key={index}
label="LoRA" label="LoRA"
value={`${lora.lora.model_name} - ${lora.weight}`} value={`${lora.lora.model_name} - ${lora.weight}`}
onClick={() => handleRecallLoRA(lora)} onClick={handleRecallLoRA.bind(null, lora)}
/> />
); );
} }
@ -289,7 +289,7 @@ const ImageMetadataActions = (props: Props) => {
key={index} key={index}
label="ControlNet" label="ControlNet"
value={`${controlnet.control_model?.model_name} - ${controlnet.control_weight}`} value={`${controlnet.control_model?.model_name} - ${controlnet.control_weight}`}
onClick={() => handleRecallControlNet(controlnet)} onClick={handleRecallControlNet.bind(null, controlnet)}
/> />
))} ))}
{validIPAdapters.map((ipAdapter, index) => ( {validIPAdapters.map((ipAdapter, index) => (
@ -297,7 +297,7 @@ const ImageMetadataActions = (props: Props) => {
key={index} key={index}
label="IP Adapter" label="IP Adapter"
value={`${ipAdapter.ip_adapter_model?.model_name} - ${ipAdapter.weight}`} value={`${ipAdapter.ip_adapter_model?.model_name} - ${ipAdapter.weight}`}
onClick={() => handleRecallIPAdapter(ipAdapter)} onClick={handleRecallIPAdapter.bind(null, ipAdapter)}
/> />
))} ))}
{validT2IAdapters.map((t2iAdapter, index) => ( {validT2IAdapters.map((t2iAdapter, index) => (
@ -305,7 +305,7 @@ const ImageMetadataActions = (props: Props) => {
key={index} key={index}
label="T2I Adapter" label="T2I Adapter"
value={`${t2iAdapter.t2i_adapter_model?.model_name} - ${t2iAdapter.weight}`} value={`${t2iAdapter.t2i_adapter_model?.model_name} - ${t2iAdapter.weight}`}
onClick={() => handleRecallT2IAdapter(t2iAdapter)} onClick={handleRecallT2IAdapter.bind(null, t2iAdapter)}
/> />
))} ))}
</> </>

View File

@ -1,6 +1,6 @@
import { ExternalLinkIcon } from '@chakra-ui/icons'; import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react'; import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa'; import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { IoArrowUndoCircleOutline } from 'react-icons/io5';
@ -27,6 +27,11 @@ const ImageMetadataItem = ({
}: MetadataItemProps) => { }: MetadataItemProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const handleCopy = useCallback(
() => navigator.clipboard.writeText(value.toString()),
[value]
);
if (!value) { if (!value) {
return null; return null;
} }
@ -53,7 +58,7 @@ const ImageMetadataItem = ({
size="xs" size="xs"
variant="ghost" variant="ghost"
fontSize={14} fontSize={14}
onClick={() => navigator.clipboard.writeText(value.toString())} onClick={handleCopy}
/> />
</Tooltip> </Tooltip>
)} )}

View File

@ -76,6 +76,13 @@ const ParamLoRASelect = () => {
[dispatch, loraModels?.entities] [dispatch, loraModels?.entities]
); );
const filterFunc = useCallback(
(value: string, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()),
[]
);
if (loraModels?.ids.length === 0) { if (loraModels?.ids.length === 0) {
return ( return (
<Flex sx={{ justifyContent: 'center', p: 2 }}> <Flex sx={{ justifyContent: 'center', p: 2 }}>
@ -94,10 +101,7 @@ const ParamLoRASelect = () => {
nothingFound="No matching LoRAs" nothingFound="No matching LoRAs"
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0} disabled={data.length === 0}
filter={(value, item: SelectItem) => filter={filterFunc}
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
onChange={handleChange} onChange={handleChange}
data-testid="add-lora" data-testid="add-lora"
/> />

View File

@ -1,6 +1,6 @@
import { ButtonGroup, Flex } from '@chakra-ui/react'; import { ButtonGroup, Flex } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { useState } from 'react'; import { useCallback, useState } from 'react';
import AdvancedAddModels from './AdvancedAddModels'; import AdvancedAddModels from './AdvancedAddModels';
import SimpleAddModels from './SimpleAddModels'; import SimpleAddModels from './SimpleAddModels';
@ -8,6 +8,11 @@ export default function AddModels() {
const [addModelMode, setAddModelMode] = useState<'simple' | 'advanced'>( const [addModelMode, setAddModelMode] = useState<'simple' | 'advanced'>(
'simple' 'simple'
); );
const handleAddModelSimple = useCallback(() => setAddModelMode('simple'), []);
const handleAddModelAdvanced = useCallback(
() => setAddModelMode('advanced'),
[]
);
return ( return (
<Flex <Flex
flexDirection="column" flexDirection="column"
@ -20,14 +25,14 @@ export default function AddModels() {
<IAIButton <IAIButton
size="sm" size="sm"
isChecked={addModelMode == 'simple'} isChecked={addModelMode == 'simple'}
onClick={() => setAddModelMode('simple')} onClick={handleAddModelSimple}
> >
Simple Simple
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
isChecked={addModelMode == 'advanced'} isChecked={addModelMode == 'advanced'}
onClick={() => setAddModelMode('advanced')} onClick={handleAddModelAdvanced}
> >
Advanced Advanced
</IAIButton> </IAIButton>

View File

@ -6,7 +6,7 @@ import IAIMantineTextInput from 'common/components/IAIMantineInput';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { useState } from 'react'; import { FocusEventHandler, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAddMainModelsMutation } from 'services/api/endpoints/models'; import { useAddMainModelsMutation } from 'services/api/endpoints/models';
import { CheckpointModelConfig } from 'services/api/types'; import { CheckpointModelConfig } from 'services/api/types';
@ -83,6 +83,27 @@ export default function AdvancedAddCheckpoint(
}); });
}; };
const handleBlurModelLocation: FocusEventHandler<HTMLInputElement> =
useCallback(
(e) => {
if (advancedAddCheckpointForm.values['model_name'] === '') {
const modelName = getModelName(e.currentTarget.value);
if (modelName) {
advancedAddCheckpointForm.setFieldValue(
'model_name',
modelName as string
);
}
}
},
[advancedAddCheckpointForm]
);
const handleChangeUseCustomConfig = useCallback(
() => setUseCustomConfig((prev) => !prev),
[]
);
return ( return (
<form <form
onSubmit={advancedAddCheckpointForm.onSubmit((v) => onSubmit={advancedAddCheckpointForm.onSubmit((v) =>
@ -104,17 +125,7 @@ export default function AdvancedAddCheckpoint(
label={t('modelManager.modelLocation')} label={t('modelManager.modelLocation')}
required required
{...advancedAddCheckpointForm.getInputProps('path')} {...advancedAddCheckpointForm.getInputProps('path')}
onBlur={(e) => { onBlur={handleBlurModelLocation}
if (advancedAddCheckpointForm.values['model_name'] === '') {
const modelName = getModelName(e.currentTarget.value);
if (modelName) {
advancedAddCheckpointForm.setFieldValue(
'model_name',
modelName as string
);
}
}
}}
/> />
<IAIMantineTextInput <IAIMantineTextInput
label={t('modelManager.description')} label={t('modelManager.description')}
@ -144,7 +155,7 @@ export default function AdvancedAddCheckpoint(
)} )}
<IAISimpleCheckbox <IAISimpleCheckbox
isChecked={useCustomConfig} isChecked={useCustomConfig}
onChange={() => setUseCustomConfig(!useCustomConfig)} onChange={handleChangeUseCustomConfig}
label={t('modelManager.useCustomConfig')} label={t('modelManager.useCustomConfig')}
/> />
<IAIButton mt={2} type="submit"> <IAIButton mt={2} type="submit">

View File

@ -12,6 +12,7 @@ import { setAdvancedAddScanModel } from '../../store/modelManagerSlice';
import BaseModelSelect from '../shared/BaseModelSelect'; import BaseModelSelect from '../shared/BaseModelSelect';
import ModelVariantSelect from '../shared/ModelVariantSelect'; import ModelVariantSelect from '../shared/ModelVariantSelect';
import { getModelName } from './util'; import { getModelName } from './util';
import { FocusEventHandler, useCallback } from 'react';
type AdvancedAddDiffusersProps = { type AdvancedAddDiffusersProps = {
model_path?: string; model_path?: string;
@ -74,6 +75,22 @@ export default function AdvancedAddDiffusers(props: AdvancedAddDiffusersProps) {
}); });
}; };
const handleBlurModelLocation: FocusEventHandler<HTMLInputElement> =
useCallback(
(e) => {
if (advancedAddDiffusersForm.values['model_name'] === '') {
const modelName = getModelName(e.currentTarget.value, false);
if (modelName) {
advancedAddDiffusersForm.setFieldValue(
'model_name',
modelName as string
);
}
}
},
[advancedAddDiffusersForm]
);
return ( return (
<form <form
onSubmit={advancedAddDiffusersForm.onSubmit((v) => onSubmit={advancedAddDiffusersForm.onSubmit((v) =>
@ -96,17 +113,7 @@ export default function AdvancedAddDiffusers(props: AdvancedAddDiffusersProps) {
label={t('modelManager.modelLocation')} label={t('modelManager.modelLocation')}
placeholder={t('modelManager.modelLocationValidationMsg')} placeholder={t('modelManager.modelLocationValidationMsg')}
{...advancedAddDiffusersForm.getInputProps('path')} {...advancedAddDiffusersForm.getInputProps('path')}
onBlur={(e) => { onBlur={handleBlurModelLocation}
if (advancedAddDiffusersForm.values['model_name'] === '') {
const modelName = getModelName(e.currentTarget.value, false);
if (modelName) {
advancedAddDiffusersForm.setFieldValue(
'model_name',
modelName as string
);
}
}
}}
/> />
<IAIMantineTextInput <IAIMantineTextInput
label={t('modelManager.description')} label={t('modelManager.description')}

View File

@ -1,7 +1,7 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { SelectItem } from '@mantine/core'; import { SelectItem } from '@mantine/core';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useState } from 'react'; import { useCallback, useState } from 'react';
import AdvancedAddCheckpoint from './AdvancedAddCheckpoint'; import AdvancedAddCheckpoint from './AdvancedAddCheckpoint';
import AdvancedAddDiffusers from './AdvancedAddDiffusers'; import AdvancedAddDiffusers from './AdvancedAddDiffusers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -18,6 +18,12 @@ export default function AdvancedAddModels() {
useState<ManualAddMode>('diffusers'); useState<ManualAddMode>('diffusers');
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback((v: string | null) => {
if (!v) {
return;
}
setAdvancedAddMode(v as ManualAddMode);
}, []);
return ( return (
<Flex flexDirection="column" gap={4} width="100%"> <Flex flexDirection="column" gap={4} width="100%">
@ -25,12 +31,7 @@ export default function AdvancedAddModels() {
label={t('modelManager.modelType')} label={t('modelManager.modelType')}
value={advancedAddMode} value={advancedAddMode}
data={advancedAddModeData} data={advancedAddModeData}
onChange={(v) => { onChange={handleChange}
if (!v) {
return;
}
setAdvancedAddMode(v as ManualAddMode);
}}
/> />
<Flex <Flex

View File

@ -92,6 +92,11 @@ export default function FoundModelsList() {
setNameFilter(e.target.value); setNameFilter(e.target.value);
}, []); }, []);
const handleClickSetAdvanced = useCallback(
(model: string) => dispatch(setAdvancedAddScanModel(model)),
[dispatch]
);
const renderModels = ({ const renderModels = ({
models, models,
showActions = true, showActions = true,
@ -140,7 +145,7 @@ export default function FoundModelsList() {
{t('modelManager.quickAdd')} {t('modelManager.quickAdd')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
onClick={() => dispatch(setAdvancedAddScanModel(model))} onClick={handleClickSetAdvanced.bind(null, model)}
isLoading={isLoading} isLoading={isLoading}
> >
{t('modelManager.advanced')} {t('modelManager.advanced')}

View File

@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from 'react-icons/fa';
import { setAdvancedAddScanModel } from '../../store/modelManagerSlice'; import { setAdvancedAddScanModel } from '../../store/modelManagerSlice';
import AdvancedAddCheckpoint from './AdvancedAddCheckpoint'; import AdvancedAddCheckpoint from './AdvancedAddCheckpoint';
@ -35,6 +35,23 @@ export default function ScanAdvancedAddModels() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleClickSetAdvanced = useCallback(
() => dispatch(setAdvancedAddScanModel(null)),
[dispatch]
);
const handleChangeAddMode = useCallback((v: string | null) => {
if (!v) {
return;
}
setAdvancedAddMode(v as ManualAddMode);
if (v === 'checkpoint') {
setIsCheckpoint(true);
} else {
setIsCheckpoint(false);
}
}, []);
if (!advancedAddScanModel) { if (!advancedAddScanModel) {
return null; return null;
} }
@ -68,7 +85,7 @@ export default function ScanAdvancedAddModels() {
<IAIIconButton <IAIIconButton
icon={<FaTimes />} icon={<FaTimes />}
aria-label={t('modelManager.closeAdvanced')} aria-label={t('modelManager.closeAdvanced')}
onClick={() => dispatch(setAdvancedAddScanModel(null))} onClick={handleClickSetAdvanced}
size="sm" size="sm"
/> />
</Flex> </Flex>
@ -76,17 +93,7 @@ export default function ScanAdvancedAddModels() {
label={t('modelManager.modelType')} label={t('modelManager.modelType')}
value={advancedAddMode} value={advancedAddMode}
data={advancedAddModeData} data={advancedAddModeData}
onChange={(v) => { onChange={handleChangeAddMode}
if (!v) {
return;
}
setAdvancedAddMode(v as ManualAddMode);
if (v === 'checkpoint') {
setIsCheckpoint(true);
} else {
setIsCheckpoint(false);
}
}}
/> />
{isCheckpoint ? ( {isCheckpoint ? (
<AdvancedAddCheckpoint <AdvancedAddCheckpoint

View File

@ -42,9 +42,14 @@ function SearchFolderForm() {
[dispatch] [dispatch]
); );
const scanAgainHandler = () => { const scanAgainHandler = useCallback(() => {
refetchFoundModels(); refetchFoundModels();
}; }, [refetchFoundModels]);
const handleClickClearCheckpointFolder = useCallback(() => {
dispatch(setSearchFolder(null));
dispatch(setAdvancedAddScanModel(null));
}, [dispatch]);
return ( return (
<form <form
@ -123,10 +128,7 @@ function SearchFolderForm() {
tooltip={t('modelManager.clearCheckpointFolder')} tooltip={t('modelManager.clearCheckpointFolder')}
icon={<FaTrash />} icon={<FaTrash />}
size="sm" size="sm"
onClick={() => { onClick={handleClickClearCheckpointFolder}
dispatch(setSearchFolder(null));
dispatch(setAdvancedAddScanModel(null));
}}
isDisabled={!searchFolder} isDisabled={!searchFolder}
colorScheme="red" colorScheme="red"
/> />

View File

@ -1,6 +1,6 @@
import { ButtonGroup, Flex } from '@chakra-ui/react'; import { ButtonGroup, Flex } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import AddModels from './AddModelsPanel/AddModels'; import AddModels from './AddModelsPanel/AddModels';
import ScanModels from './AddModelsPanel/ScanModels'; import ScanModels from './AddModelsPanel/ScanModels';
@ -11,11 +11,14 @@ export default function ImportModelsPanel() {
const [addModelTab, setAddModelTab] = useState<AddModelTabs>('add'); const [addModelTab, setAddModelTab] = useState<AddModelTabs>('add');
const { t } = useTranslation(); const { t } = useTranslation();
const handleClickAddTab = useCallback(() => setAddModelTab('add'), []);
const handleClickScanTab = useCallback(() => setAddModelTab('scan'), []);
return ( return (
<Flex flexDirection="column" gap={4}> <Flex flexDirection="column" gap={4}>
<ButtonGroup isAttached> <ButtonGroup isAttached>
<IAIButton <IAIButton
onClick={() => setAddModelTab('add')} onClick={handleClickAddTab}
isChecked={addModelTab == 'add'} isChecked={addModelTab == 'add'}
size="sm" size="sm"
width="100%" width="100%"
@ -23,7 +26,7 @@ export default function ImportModelsPanel() {
{t('modelManager.addModel')} {t('modelManager.addModel')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
onClick={() => setAddModelTab('scan')} onClick={handleClickScanTab}
isChecked={addModelTab == 'scan'} isChecked={addModelTab == 'scan'}
size="sm" size="sm"
width="100%" width="100%"

View File

@ -9,7 +9,7 @@ import IAISlider from 'common/components/IAISlider';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { pickBy } from 'lodash-es'; import { pickBy } from 'lodash-es';
import { useMemo, useState } from 'react'; import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ALL_BASE_MODELS } from 'services/api/constants'; import { ALL_BASE_MODELS } from 'services/api/constants';
import { import {
@ -94,13 +94,58 @@ export default function MergeModelsPanel() {
modelsMap[baseModel as keyof typeof modelsMap] modelsMap[baseModel as keyof typeof modelsMap]
).filter((model) => model !== modelOne && model !== modelTwo); ).filter((model) => model !== modelOne && model !== modelTwo);
const handleBaseModelChange = (v: string) => { const handleBaseModelChange = useCallback((v: string) => {
setBaseModel(v as BaseModelType); setBaseModel(v as BaseModelType);
setModelOne(null); setModelOne(null);
setModelTwo(null); setModelTwo(null);
}; }, []);
const mergeModelsHandler = () => { const handleChangeModelOne = useCallback((v: string) => {
setModelOne(v);
}, []);
const handleChangeModelTwo = useCallback((v: string) => {
setModelTwo(v);
}, []);
const handleChangeModelThree = useCallback((v: string) => {
if (!v) {
setModelThree(null);
setModelMergeInterp('add_difference');
} else {
setModelThree(v);
setModelMergeInterp('weighted_sum');
}
}, []);
const handleChangeMergedModelName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setMergedModelName(e.target.value),
[]
);
const handleChangeModelMergeAlpha = useCallback(
(v: number) => setModelMergeAlpha(v),
[]
);
const handleResetModelMergeAlpha = useCallback(
() => setModelMergeAlpha(0.5),
[]
);
const handleChangeMergeInterp = useCallback(
(v: MergeInterpolationMethods) => setModelMergeInterp(v),
[]
);
const handleChangeMergeSaveLocType = useCallback(
(v: 'root' | 'custom') => setModelMergeSaveLocType(v),
[]
);
const handleChangeMergeCustomSaveLoc = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
setModelMergeCustomSaveLoc(e.target.value),
[]
);
const handleChangeModelMergeForce = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setModelMergeForce(e.target.checked),
[]
);
const mergeModelsHandler = useCallback(() => {
const models_names: string[] = []; const models_names: string[] = [];
let modelsToMerge: (string | null)[] = [modelOne, modelTwo, modelThree]; let modelsToMerge: (string | null)[] = [modelOne, modelTwo, modelThree];
@ -150,7 +195,21 @@ export default function MergeModelsPanel() {
); );
} }
}); });
}; }, [
baseModel,
dispatch,
mergeModels,
mergedModelName,
modelMergeAlpha,
modelMergeCustomSaveLoc,
modelMergeForce,
modelMergeInterp,
modelMergeSaveLocType,
modelOne,
modelThree,
modelTwo,
t,
]);
return ( return (
<Flex flexDirection="column" rowGap={4}> <Flex flexDirection="column" rowGap={4}>
@ -180,7 +239,7 @@ export default function MergeModelsPanel() {
value={modelOne} value={modelOne}
placeholder={t('modelManager.selectModel')} placeholder={t('modelManager.selectModel')}
data={modelOneList} data={modelOneList}
onChange={(v) => setModelOne(v)} onChange={handleChangeModelOne}
/> />
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('modelManager.modelTwo')} label={t('modelManager.modelTwo')}
@ -188,7 +247,7 @@ export default function MergeModelsPanel() {
placeholder={t('modelManager.selectModel')} placeholder={t('modelManager.selectModel')}
value={modelTwo} value={modelTwo}
data={modelTwoList} data={modelTwoList}
onChange={(v) => setModelTwo(v)} onChange={handleChangeModelTwo}
/> />
<IAIMantineSearchableSelect <IAIMantineSearchableSelect
label={t('modelManager.modelThree')} label={t('modelManager.modelThree')}
@ -196,22 +255,14 @@ export default function MergeModelsPanel() {
w="100%" w="100%"
placeholder={t('modelManager.selectModel')} placeholder={t('modelManager.selectModel')}
clearable clearable
onChange={(v) => { onChange={handleChangeModelThree}
if (!v) {
setModelThree(null);
setModelMergeInterp('add_difference');
} else {
setModelThree(v);
setModelMergeInterp('weighted_sum');
}
}}
/> />
</Flex> </Flex>
<IAIInput <IAIInput
label={t('modelManager.mergedModelName')} label={t('modelManager.mergedModelName')}
value={mergedModelName} value={mergedModelName}
onChange={(e) => setMergedModelName(e.target.value)} onChange={handleChangeMergedModelName}
/> />
<Flex <Flex
@ -232,10 +283,10 @@ export default function MergeModelsPanel() {
max={0.99} max={0.99}
step={0.01} step={0.01}
value={modelMergeAlpha} value={modelMergeAlpha}
onChange={(v) => setModelMergeAlpha(v)} onChange={handleChangeModelMergeAlpha}
withInput withInput
withReset withReset
handleReset={() => setModelMergeAlpha(0.5)} handleReset={handleResetModelMergeAlpha}
withSliderMarks withSliderMarks
/> />
<Text variant="subtext" fontSize="sm"> <Text variant="subtext" fontSize="sm">
@ -257,10 +308,7 @@ export default function MergeModelsPanel() {
<Text fontWeight={500} fontSize="sm" variant="subtext"> <Text fontWeight={500} fontSize="sm" variant="subtext">
{t('modelManager.interpolationType')} {t('modelManager.interpolationType')}
</Text> </Text>
<RadioGroup <RadioGroup value={modelMergeInterp} onChange={handleChangeMergeInterp}>
value={modelMergeInterp}
onChange={(v: MergeInterpolationMethods) => setModelMergeInterp(v)}
>
<Flex columnGap={4}> <Flex columnGap={4}>
{modelThree === null ? ( {modelThree === null ? (
<> <>
@ -305,7 +353,7 @@ export default function MergeModelsPanel() {
</Text> </Text>
<RadioGroup <RadioGroup
value={modelMergeSaveLocType} value={modelMergeSaveLocType}
onChange={(v: 'root' | 'custom') => setModelMergeSaveLocType(v)} onChange={handleChangeMergeSaveLocType}
> >
<Flex columnGap={4}> <Flex columnGap={4}>
<Radio value="root"> <Radio value="root">
@ -323,7 +371,7 @@ export default function MergeModelsPanel() {
<IAIInput <IAIInput
label={t('modelManager.mergedModelCustomSaveLocation')} label={t('modelManager.mergedModelCustomSaveLocation')}
value={modelMergeCustomSaveLoc} value={modelMergeCustomSaveLoc}
onChange={(e) => setModelMergeCustomSaveLoc(e.target.value)} onChange={handleChangeMergeCustomSaveLoc}
/> />
)} )}
</Flex> </Flex>
@ -331,7 +379,7 @@ export default function MergeModelsPanel() {
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('modelManager.ignoreMismatch')} label={t('modelManager.ignoreMismatch')}
isChecked={modelMergeForce} isChecked={modelMergeForce}
onChange={(e) => setModelMergeForce(e.target.checked)} onChange={handleChangeModelMergeForce}
fontWeight="500" fontWeight="500"
/> />

View File

@ -59,6 +59,11 @@ export default function CheckpointModelEdit(props: CheckpointModelEditProps) {
}, },
}); });
const handleChangeUseCustomConfig = useCallback(
() => setUseCustomConfig((prev) => !prev),
[]
);
const editModelFormSubmitHandler = useCallback( const editModelFormSubmitHandler = useCallback(
(values: CheckpointModelConfig) => { (values: CheckpointModelConfig) => {
const responseBody = { const responseBody = {
@ -181,7 +186,7 @@ export default function CheckpointModelEdit(props: CheckpointModelEditProps) {
)} )}
<IAISimpleCheckbox <IAISimpleCheckbox
isChecked={useCustomConfig} isChecked={useCustomConfig}
onChange={() => setUseCustomConfig(!useCustomConfig)} onChange={handleChangeUseCustomConfig}
label="Use Custom Config" label="Use Custom Config"
/> />
</Flex> </Flex>

View File

@ -14,7 +14,7 @@ import IAIAlertDialog from 'common/components/IAIAlertDialog';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIInput from 'common/components/IAIInput'; import IAIInput from 'common/components/IAIInput';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { useEffect, useState } from 'react'; import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useConvertMainModelsMutation } from 'services/api/endpoints/models'; import { useConvertMainModelsMutation } from 'services/api/endpoints/models';
@ -42,11 +42,21 @@ export default function ModelConvert(props: ModelConvertProps) {
setSaveLocation('InvokeAIRoot'); setSaveLocation('InvokeAIRoot');
}, [model]); }, [model]);
const modelConvertCancelHandler = () => { const modelConvertCancelHandler = useCallback(() => {
setSaveLocation('InvokeAIRoot'); setSaveLocation('InvokeAIRoot');
}; }, []);
const modelConvertHandler = () => { const handleChangeSaveLocation = useCallback((v: string) => {
setSaveLocation(v as SaveLocation);
}, []);
const handleChangeCustomSaveLocation = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setCustomSaveLocation(e.target.value);
},
[]
);
const modelConvertHandler = useCallback(() => {
const queryArg = { const queryArg = {
base_model: model.base_model, base_model: model.base_model,
model_name: model.model_name, model_name: model.model_name,
@ -101,7 +111,15 @@ export default function ModelConvert(props: ModelConvertProps) {
) )
); );
}); });
}; }, [
convertModel,
customSaveLocation,
dispatch,
model.base_model,
model.model_name,
saveLocation,
t,
]);
return ( return (
<IAIAlertDialog <IAIAlertDialog
@ -137,10 +155,7 @@ export default function ModelConvert(props: ModelConvertProps) {
<Text fontWeight="600"> <Text fontWeight="600">
{t('modelManager.convertToDiffusersSaveLocation')} {t('modelManager.convertToDiffusersSaveLocation')}
</Text> </Text>
<RadioGroup <RadioGroup value={saveLocation} onChange={handleChangeSaveLocation}>
value={saveLocation}
onChange={(v) => setSaveLocation(v as SaveLocation)}
>
<Flex gap={4}> <Flex gap={4}>
<Radio value="InvokeAIRoot"> <Radio value="InvokeAIRoot">
<Tooltip label="Save converted model in the InvokeAI root folder"> <Tooltip label="Save converted model in the InvokeAI root folder">
@ -162,9 +177,7 @@ export default function ModelConvert(props: ModelConvertProps) {
</Text> </Text>
<IAIInput <IAIInput
value={customSaveLocation} value={customSaveLocation}
onChange={(e) => { onChange={handleChangeCustomSaveLocation}
setCustomSaveLocation(e.target.value);
}}
width="full" width="full"
/> />
</Flex> </Flex>

View File

@ -100,7 +100,7 @@ const ModelList = (props: ModelListProps) => {
<Flex flexDirection="column" gap={4} paddingInlineEnd={4}> <Flex flexDirection="column" gap={4} paddingInlineEnd={4}>
<ButtonGroup isAttached> <ButtonGroup isAttached>
<IAIButton <IAIButton
onClick={() => setModelFormatFilter('all')} onClick={setModelFormatFilter.bind(null, 'all')}
isChecked={modelFormatFilter === 'all'} isChecked={modelFormatFilter === 'all'}
size="sm" size="sm"
> >
@ -108,35 +108,35 @@ const ModelList = (props: ModelListProps) => {
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
onClick={() => setModelFormatFilter('diffusers')} onClick={setModelFormatFilter.bind(null, 'diffusers')}
isChecked={modelFormatFilter === 'diffusers'} isChecked={modelFormatFilter === 'diffusers'}
> >
{t('modelManager.diffusersModels')} {t('modelManager.diffusersModels')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
onClick={() => setModelFormatFilter('checkpoint')} onClick={setModelFormatFilter.bind(null, 'checkpoint')}
isChecked={modelFormatFilter === 'checkpoint'} isChecked={modelFormatFilter === 'checkpoint'}
> >
{t('modelManager.checkpointModels')} {t('modelManager.checkpointModels')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
onClick={() => setModelFormatFilter('onnx')} onClick={setModelFormatFilter.bind(null, 'onnx')}
isChecked={modelFormatFilter === 'onnx'} isChecked={modelFormatFilter === 'onnx'}
> >
{t('modelManager.onnxModels')} {t('modelManager.onnxModels')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
onClick={() => setModelFormatFilter('olive')} onClick={setModelFormatFilter.bind(null, 'olive')}
isChecked={modelFormatFilter === 'olive'} isChecked={modelFormatFilter === 'olive'}
> >
{t('modelManager.oliveModels')} {t('modelManager.oliveModels')}
</IAIButton> </IAIButton>
<IAIButton <IAIButton
size="sm" size="sm"
onClick={() => setModelFormatFilter('lora')} onClick={setModelFormatFilter.bind(null, 'lora')}
isChecked={modelFormatFilter === 'lora'} isChecked={modelFormatFilter === 'lora'}
> >
{t('modelManager.loraModels')} {t('modelManager.loraModels')}

View File

@ -4,6 +4,7 @@ import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa'; import { FaSync } from 'react-icons/fa';
import { useSyncModelsMutation } from 'services/api/endpoints/models'; import { useSyncModelsMutation } from 'services/api/endpoints/models';
@ -19,7 +20,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) {
const [syncModels, { isLoading }] = useSyncModelsMutation(); const [syncModels, { isLoading }] = useSyncModelsMutation();
const syncModelsHandler = () => { const syncModelsHandler = useCallback(() => {
syncModels() syncModels()
.unwrap() .unwrap()
.then((_) => { .then((_) => {
@ -44,7 +45,7 @@ export default function SyncModelsButton(props: SyncModelsButtonProps) {
); );
} }
}); });
}; }, [dispatch, syncModels, t]);
return !iconMode ? ( return !iconMode ? (
<IAIButton <IAIButton

View File

@ -1,4 +1,4 @@
import { useState, PropsWithChildren, memo } from 'react'; import { useState, PropsWithChildren, memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { Flex, Image, Text } from '@chakra-ui/react'; import { Flex, Image, Text } from '@chakra-ui/react';
@ -59,13 +59,13 @@ export default memo(CurrentImageNode);
const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => { const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const handleMouseEnter = () => { const handleMouseEnter = useCallback(() => {
setIsHovering(true); setIsHovering(true);
}; }, []);
const handleMouseLeave = () => { const handleMouseLeave = useCallback(() => {
setIsHovering(false); setIsHovering(false);
}; }, []);
return ( return (
<NodeWrapper <NodeWrapper

View File

@ -104,6 +104,24 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
nodeId, nodeId,
]); ]);
const renderMenuFunc = useCallback(
() =>
!menuItems.length ? null : (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup
title={label || fieldTemplateTitle || t('nodes.unknownField')}
>
{menuItems}
</MenuGroup>
</MenuList>
),
[fieldTemplateTitle, label, menuItems, skipEvent, t]
);
return ( return (
<IAIContextMenu<HTMLDivElement> <IAIContextMenu<HTMLDivElement>
menuProps={{ menuProps={{
@ -114,21 +132,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
bg: 'transparent', bg: 'transparent',
_hover: { bg: 'transparent' }, _hover: { bg: 'transparent' },
}} }}
renderMenu={() => renderMenu={renderMenuFunc}
!menuItems.length ? null : (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup
title={label || fieldTemplateTitle || t('nodes.unknownField')}
>
{menuItems}
</MenuGroup>
</MenuList>
)
}
> >
{children} {children}
</IAIContextMenu> </IAIContextMenu>

View File

@ -1,14 +0,0 @@
import {
ClipInputFieldTemplate,
ClipInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const ClipInputFieldComponent = (
_props: FieldComponentProps<ClipInputFieldValue, ClipInputFieldTemplate>
) => {
return null;
};
export default memo(ClipInputFieldComponent);

View File

@ -1,17 +0,0 @@
import {
CollectionInputFieldTemplate,
CollectionInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const CollectionInputFieldComponent = (
_props: FieldComponentProps<
CollectionInputFieldValue,
CollectionInputFieldTemplate
>
) => {
return null;
};
export default memo(CollectionInputFieldComponent);

View File

@ -1,17 +0,0 @@
import {
CollectionItemInputFieldTemplate,
CollectionItemInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const CollectionItemInputFieldComponent = (
_props: FieldComponentProps<
CollectionItemInputFieldValue,
CollectionItemInputFieldTemplate
>
) => {
return null;
};
export default memo(CollectionItemInputFieldComponent);

View File

@ -1,17 +0,0 @@
import {
ConditioningInputFieldTemplate,
ConditioningInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const ConditioningInputFieldComponent = (
_props: FieldComponentProps<
ConditioningInputFieldValue,
ConditioningInputFieldTemplate
>
) => {
return null;
};
export default memo(ConditioningInputFieldComponent);

View File

@ -1,19 +0,0 @@
import {
ControlInputFieldTemplate,
ControlInputFieldValue,
ControlPolymorphicInputFieldTemplate,
ControlPolymorphicInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const ControlInputFieldComponent = (
_props: FieldComponentProps<
ControlInputFieldValue | ControlPolymorphicInputFieldValue,
ControlInputFieldTemplate | ControlPolymorphicInputFieldTemplate
>
) => {
return null;
};
export default memo(ControlInputFieldComponent);

View File

@ -1,17 +0,0 @@
import {
DenoiseMaskInputFieldTemplate,
DenoiseMaskInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const DenoiseMaskInputFieldComponent = (
_props: FieldComponentProps<
DenoiseMaskInputFieldValue,
DenoiseMaskInputFieldTemplate
>
) => {
return null;
};
export default memo(DenoiseMaskInputFieldComponent);

View File

@ -1,19 +0,0 @@
import {
IPAdapterInputFieldTemplate,
IPAdapterInputFieldValue,
FieldComponentProps,
IPAdapterPolymorphicInputFieldValue,
IPAdapterPolymorphicInputFieldTemplate,
} from 'features/nodes/types/types';
import { memo } from 'react';
const IPAdapterInputFieldComponent = (
_props: FieldComponentProps<
IPAdapterInputFieldValue | IPAdapterPolymorphicInputFieldValue,
IPAdapterInputFieldTemplate | IPAdapterPolymorphicInputFieldTemplate
>
) => {
return null;
};
export default memo(IPAdapterInputFieldComponent);

View File

@ -1,94 +0,0 @@
import {
ImageCollectionInputFieldTemplate,
ImageCollectionInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
import { Flex } from '@chakra-ui/react';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDropOverlay from 'common/components/IAIDropOverlay';
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import { NodesMultiImageDropData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
const ImageCollectionInputFieldComponent = (
props: FieldComponentProps<
ImageCollectionInputFieldValue,
ImageCollectionInputFieldTemplate
>
) => {
const { nodeId, field } = props;
// const dispatch = useAppDispatch();
// const handleDrop = useCallback(
// ({ image_name }: ImageDTO) => {
// dispatch(
// fieldValueChanged({
// nodeId,
// fieldName: field.name,
// value: uniqBy([...(field.value ?? []), { image_name }], 'image_name'),
// })
// );
// },
// [dispatch, field.name, field.value, nodeId]
// );
const droppableData: NodesMultiImageDropData = {
id: `node-${nodeId}-${field.name}`,
actionType: 'SET_MULTI_NODES_IMAGE',
context: { nodeId: nodeId, fieldName: field.name },
};
const {
isOver,
setNodeRef: setDroppableRef,
active,
} = useDroppableTypesafe({
id: `node_${nodeId}`,
data: droppableData,
});
// const handleReset = useCallback(() => {
// dispatch(
// fieldValueChanged({
// nodeId,
// fieldName: field.name,
// value: undefined,
// })
// );
// }, [dispatch, field.name, nodeId]);
return (
<Flex
ref={setDroppableRef}
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
minH: '10rem',
}}
>
{field.value?.map(({ image_name }) => (
<ImageSubField key={image_name} imageName={image_name} />
))}
{isValidDrop(droppableData, active) && <IAIDropOverlay isOver={isOver} />}
</Flex>
);
};
export default memo(ImageCollectionInputFieldComponent);
type ImageSubFieldProps = { imageName: string };
const ImageSubField = (props: ImageSubFieldProps) => {
const { currentData: image } = useGetImageDTOQuery(props.imageName);
return (
<IAIDndImage imageDTO={image} isDropDisabled={true} isDragDisabled={true} />
);
};

View File

@ -1,19 +0,0 @@
import {
LatentsInputFieldTemplate,
LatentsInputFieldValue,
FieldComponentProps,
LatentsPolymorphicInputFieldValue,
LatentsPolymorphicInputFieldTemplate,
} from 'features/nodes/types/types';
import { memo } from 'react';
const LatentsInputFieldComponent = (
_props: FieldComponentProps<
LatentsInputFieldValue | LatentsPolymorphicInputFieldValue,
LatentsInputFieldTemplate | LatentsPolymorphicInputFieldTemplate
>
) => {
return null;
};
export default memo(LatentsInputFieldComponent);

View File

@ -80,6 +80,13 @@ const LoRAModelInputFieldComponent = (
[dispatch, field.name, nodeId] [dispatch, field.name, nodeId]
); );
const filterFunc = useCallback(
(value: string, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()),
[]
);
if (loraModels?.ids.length === 0) { if (loraModels?.ids.length === 0) {
return ( return (
<Flex sx={{ justifyContent: 'center', p: 2 }}> <Flex sx={{ justifyContent: 'center', p: 2 }}>
@ -101,10 +108,7 @@ const LoRAModelInputFieldComponent = (
nothingFound={t('models.noMatchingLoRAs')} nothingFound={t('models.noMatchingLoRAs')}
itemComponent={IAIMantineSelectItemWithTooltip} itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0} disabled={data.length === 0}
filter={(value, item: SelectItem) => filter={filterFunc}
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
error={!selectedLoRAModel} error={!selectedLoRAModel}
onChange={handleChange} onChange={handleChange}
sx={{ sx={{

View File

@ -11,7 +11,7 @@ import {
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton'; import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { NON_SDXL_MAIN_MODELS } from 'services/api/constants'; import { NON_SDXL_MAIN_MODELS } from 'services/api/constants';

View File

@ -19,7 +19,7 @@ import {
IntegerPolymorphicInputFieldTemplate, IntegerPolymorphicInputFieldTemplate,
IntegerPolymorphicInputFieldValue, IntegerPolymorphicInputFieldValue,
} from 'features/nodes/types/types'; } from 'features/nodes/types/types';
import { memo, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
const NumberInputFieldComponent = ( const NumberInputFieldComponent = (
props: FieldComponentProps< props: FieldComponentProps<
@ -43,20 +43,23 @@ const NumberInputFieldComponent = (
[fieldTemplate.type] [fieldTemplate.type]
); );
const handleValueChanged = (v: string) => { const handleValueChanged = useCallback(
setValueAsString(v); (v: string) => {
// This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc. setValueAsString(v);
if (!v.match(numberStringRegex)) { // This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc.
// Cast the value to number. Floor it if it should be an integer. if (!v.match(numberStringRegex)) {
dispatch( // Cast the value to number. Floor it if it should be an integer.
fieldNumberValueChanged({ dispatch(
nodeId, fieldNumberValueChanged({
fieldName: field.name, nodeId,
value: isIntegerField ? Math.floor(Number(v)) : Number(v), fieldName: field.name,
}) value: isIntegerField ? Math.floor(Number(v)) : Number(v),
); })
} );
}; }
},
[dispatch, field.name, isIntegerField, nodeId]
);
useEffect(() => { useEffect(() => {
if ( if (

View File

@ -11,7 +11,7 @@ import {
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton'; import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View File

@ -11,7 +11,7 @@ import {
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam'; import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/ui/components/tabs/ModelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton'; import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View File

@ -1,19 +0,0 @@
import {
T2IAdapterInputFieldTemplate,
T2IAdapterInputFieldValue,
T2IAdapterPolymorphicInputFieldTemplate,
T2IAdapterPolymorphicInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const T2IAdapterInputFieldComponent = (
_props: FieldComponentProps<
T2IAdapterInputFieldValue | T2IAdapterPolymorphicInputFieldValue,
T2IAdapterInputFieldTemplate | T2IAdapterPolymorphicInputFieldTemplate
>
) => {
return null;
};
export default memo(T2IAdapterInputFieldComponent);

View File

@ -1,14 +0,0 @@
import {
UNetInputFieldTemplate,
UNetInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const UNetInputFieldComponent = (
_props: FieldComponentProps<UNetInputFieldValue, UNetInputFieldTemplate>
) => {
return null;
};
export default memo(UNetInputFieldComponent);

View File

@ -1,14 +0,0 @@
import {
VaeInputFieldTemplate,
VaeInputFieldValue,
FieldComponentProps,
} from 'features/nodes/types/types';
import { memo } from 'react';
const VaeInputFieldComponent = (
_props: FieldComponentProps<VaeInputFieldValue, VaeInputFieldTemplate>
) => {
return null;
};
export default memo(VaeInputFieldComponent);

View File

@ -1,27 +0,0 @@
import { NODE_MIN_WIDTH } from 'features/nodes/types/constants';
import { memo } from 'react';
import { NodeResizeControl, NodeResizerProps } from 'reactflow';
// this causes https://github.com/invoke-ai/InvokeAI/issues/4140
// not using it for now
const NodeResizer = (props: NodeResizerProps) => {
const { ...rest } = props;
return (
<NodeResizeControl
style={{
position: 'absolute',
border: 'none',
background: 'transparent',
width: 15,
height: 15,
bottom: 0,
right: 0,
}}
minWidth={NODE_MIN_WIDTH}
{...rest}
></NodeResizeControl>
);
};
export default memo(NodeResizer);

View File

@ -1,78 +0,0 @@
import { Box, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { InvocationTemplate, NodeData } from 'features/nodes/types/types';
import { memo } from 'react';
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
import NodeTitle from '../../flow/nodes/common/NodeTitle';
import ScrollableContent from '../ScrollableContent';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
stateSelector,
({ nodes }) => {
const lastSelectedNodeId =
nodes.selectedNodes[nodes.selectedNodes.length - 1];
const lastSelectedNode = nodes.nodes.find(
(node) => node.id === lastSelectedNodeId
);
const lastSelectedNodeTemplate = lastSelectedNode
? nodes.nodeTemplates[lastSelectedNode.data.type]
: undefined;
return {
data: lastSelectedNode?.data,
template: lastSelectedNodeTemplate,
};
},
defaultSelectorOptions
);
const InspectorDetailsTab = () => {
const { data, template } = useAppSelector(selector);
const { t } = useTranslation();
if (!template || !data) {
return (
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
);
}
return <Content data={data} template={template} />;
};
export default memo(InspectorDetailsTab);
const Content = (props: { data: NodeData; template: InvocationTemplate }) => {
const { data } = props;
return (
<Box
sx={{
position: 'relative',
w: 'full',
h: 'full',
}}
>
<ScrollableContent>
<Flex
sx={{
flexDir: 'column',
position: 'relative',
p: 1,
gap: 2,
w: 'full',
}}
>
<NodeTitle nodeId={data.id} />
<NotesTextarea nodeId={data.id} />
</Flex>
</ScrollableContent>
</Box>
);
};

View File

@ -1,51 +0,0 @@
import { Box, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import { workflowNotesChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
const selector = createSelector(stateSelector, ({ nodes }) => {
const { notes } = nodes.workflow;
return {
notes,
};
});
const WorkflowNotesTab = () => {
const { notes } = useAppSelector(selector);
const dispatch = useAppDispatch();
const handleChangeNotes = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(workflowNotesChanged(e.target.value));
},
[dispatch]
);
return (
<Box sx={{ pos: 'relative', h: 'full' }}>
<IAITextarea
onChange={handleChangeNotes}
value={notes}
fontSize="sm"
sx={{ h: 'full', resize: 'none' }}
/>
<Box sx={{ pos: 'absolute', bottom: 2, right: 2 }}>
<Text
sx={{
fontSize: 'xs',
opacity: 0.5,
userSelect: 'none',
}}
>
{notes.length}
</Text>
</Box>
</Box>
);
};
export default memo(WorkflowNotesTab);

View File

@ -1,11 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { AnyInvocationType } from 'services/events/types';
export const makeTemplateSelector = (type: AnyInvocationType) =>
createSelector(
stateSelector,
({ nodes }) => nodes.nodeTemplates[type],
defaultSelectorOptions
);

View File

@ -1,21 +0,0 @@
import { Flex } from '@chakra-ui/react';
import IAICollapse from 'common/components/IAICollapse';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
const ParamBoundingBoxCollapse = () => {
const { t } = useTranslation();
return (
<IAICollapse label={t('parameters.boundingBoxHeader')}>
<Flex sx={{ gap: 2, flexDirection: 'column' }}>
<ParamBoundingBoxWidth />
<ParamBoundingBoxHeight />
</Flex>
</IAICollapse>
);
};
export default memo(ParamBoundingBoxCollapse);

View File

@ -6,7 +6,7 @@ import IAISlider from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -36,25 +36,28 @@ const ParamBoundingBoxWidth = () => {
? 1024 ? 1024
: 512; : 512;
const handleChangeHeight = (v: number) => { const handleChangeHeight = useCallback(
dispatch( (v: number) => {
setBoundingBoxDimensions({
...boundingBoxDimensions,
height: Math.floor(v),
})
);
if (aspectRatio) {
const newWidth = roundToMultiple(v * aspectRatio, 64);
dispatch( dispatch(
setBoundingBoxDimensions({ setBoundingBoxDimensions({
width: newWidth, ...boundingBoxDimensions,
height: Math.floor(v), height: Math.floor(v),
}) })
); );
} if (aspectRatio) {
}; const newWidth = roundToMultiple(v * aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: newWidth,
height: Math.floor(v),
})
);
}
},
[aspectRatio, boundingBoxDimensions, dispatch]
);
const handleResetHeight = () => { const handleResetHeight = useCallback(() => {
dispatch( dispatch(
setBoundingBoxDimensions({ setBoundingBoxDimensions({
...boundingBoxDimensions, ...boundingBoxDimensions,
@ -70,7 +73,7 @@ const ParamBoundingBoxWidth = () => {
}) })
); );
} }
}; }, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
return ( return (
<IAISlider <IAISlider

View File

@ -6,7 +6,7 @@ import IAISlider from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -36,25 +36,28 @@ const ParamBoundingBoxWidth = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeWidth = (v: number) => { const handleChangeWidth = useCallback(
dispatch( (v: number) => {
setBoundingBoxDimensions({
...boundingBoxDimensions,
width: Math.floor(v),
})
);
if (aspectRatio) {
const newHeight = roundToMultiple(v / aspectRatio, 64);
dispatch( dispatch(
setBoundingBoxDimensions({ setBoundingBoxDimensions({
...boundingBoxDimensions,
width: Math.floor(v), width: Math.floor(v),
height: newHeight,
}) })
); );
} if (aspectRatio) {
}; const newHeight = roundToMultiple(v / aspectRatio, 64);
dispatch(
setBoundingBoxDimensions({
width: Math.floor(v),
height: newHeight,
})
);
}
},
[aspectRatio, boundingBoxDimensions, dispatch]
);
const handleResetWidth = () => { const handleResetWidth = useCallback(() => {
dispatch( dispatch(
setBoundingBoxDimensions({ setBoundingBoxDimensions({
...boundingBoxDimensions, ...boundingBoxDimensions,
@ -70,7 +73,7 @@ const ParamBoundingBoxWidth = () => {
}) })
); );
} }
}; }, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
return ( return (
<IAISlider <IAISlider

View File

@ -6,7 +6,7 @@ import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice';
import { CanvasCoherenceModeParam } from 'features/parameters/types/parameterSchemas'; import { CanvasCoherenceModeParam } from 'features/parameters/types/parameterSchemas';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const coherenceModeSelectData: IAISelectDataType[] = [ const coherenceModeSelectData: IAISelectDataType[] = [
@ -22,13 +22,16 @@ const ParamCanvasCoherenceMode = () => {
); );
const { t } = useTranslation(); const { t } = useTranslation();
const handleCoherenceModeChange = (v: string | null) => { const handleCoherenceModeChange = useCallback(
if (!v) { (v: string | null) => {
return; if (!v) {
} return;
}
dispatch(setCanvasCoherenceMode(v as CanvasCoherenceModeParam)); dispatch(setCanvasCoherenceMode(v as CanvasCoherenceModeParam));
}; },
[dispatch]
);
return ( return (
<IAIInformationalPopover feature="compositingCoherenceMode"> <IAIInformationalPopover feature="compositingCoherenceMode">

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setCanvasCoherenceSteps } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceSteps } from 'features/parameters/store/generationSlice';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const ParamCanvasCoherenceSteps = () => { const ParamCanvasCoherenceSteps = () => {
@ -13,6 +13,17 @@ const ParamCanvasCoherenceSteps = () => {
); );
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setCanvasCoherenceSteps(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setCanvasCoherenceSteps(20));
}, [dispatch]);
return ( return (
<IAIInformationalPopover feature="compositingCoherenceSteps"> <IAIInformationalPopover feature="compositingCoherenceSteps">
<IAISlider <IAISlider
@ -22,15 +33,11 @@ const ParamCanvasCoherenceSteps = () => {
step={1} step={1}
sliderNumberInputProps={{ max: 999 }} sliderNumberInputProps={{ max: 999 }}
value={canvasCoherenceSteps} value={canvasCoherenceSteps}
onChange={(v) => { onChange={handleChange}
dispatch(setCanvasCoherenceSteps(v));
}}
withInput withInput
withSliderMarks withSliderMarks
withReset withReset
handleReset={() => { handleReset={handleReset}
dispatch(setCanvasCoherenceSteps(20));
}}
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
); );

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setCanvasCoherenceStrength } from 'features/parameters/store/generationSlice'; import { setCanvasCoherenceStrength } from 'features/parameters/store/generationSlice';
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const ParamCanvasCoherenceStrength = () => { const ParamCanvasCoherenceStrength = () => {
@ -13,6 +13,16 @@ const ParamCanvasCoherenceStrength = () => {
); );
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setCanvasCoherenceStrength(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setCanvasCoherenceStrength(0.3));
}, [dispatch]);
return ( return (
<IAIInformationalPopover feature="compositingStrength"> <IAIInformationalPopover feature="compositingStrength">
<IAISlider <IAISlider
@ -22,15 +32,11 @@ const ParamCanvasCoherenceStrength = () => {
step={0.01} step={0.01}
sliderNumberInputProps={{ max: 999 }} sliderNumberInputProps={{ max: 999 }}
value={canvasCoherenceStrength} value={canvasCoherenceStrength}
onChange={(v) => { onChange={handleChange}
dispatch(setCanvasCoherenceStrength(v));
}}
withInput withInput
withSliderMarks withSliderMarks
withReset withReset
handleReset={() => { handleReset={handleReset}
dispatch(setCanvasCoherenceStrength(0.3));
}}
/> />
</IAIInformationalPopover> </IAIInformationalPopover>
); );

Some files were not shown because too many files have changed in this diff Show More