mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-08 02:45:06 -05:00
Merges development
This commit is contained in:
@@ -25,7 +25,10 @@ const systemSelector = createSelector(
|
||||
const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
const shouldDisplayGuides = useAppSelector(systemSelector);
|
||||
const { text } = FEATURES[feature];
|
||||
return shouldDisplayGuides ? (
|
||||
|
||||
if (!shouldDisplayGuides) return null;
|
||||
|
||||
return (
|
||||
<Popover trigger={'hover'}>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
@@ -40,8 +43,6 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
<div className="guide-popover-guide-content">{text}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
24
frontend/src/common/components/IAICheckbox.scss
Normal file
24
frontend/src/common/components/IAICheckbox.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.invokeai__checkbox {
|
||||
.chakra-checkbox__label {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.chakra-checkbox__control {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: none;
|
||||
border-radius: 0.2rem;
|
||||
background-color: var(--input-checkbox-bg);
|
||||
|
||||
svg {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
stroke-width: 3px !important;
|
||||
}
|
||||
|
||||
&[data-checked] {
|
||||
color: var(--text-color);
|
||||
background-color: var(--input-checkbox-checked-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||
|
||||
type IAICheckboxProps = CheckboxProps & {
|
||||
label: string;
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
const IAICheckbox = (props: IAICheckboxProps) => {
|
||||
const { label, styleClass, ...rest } = props;
|
||||
return (
|
||||
<Checkbox className={`invokeai__checkbox ${styleClass}`} {...rest}>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAICheckbox;
|
||||
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.invokeai__color-picker {
|
||||
.react-colorful__hue-pointer,
|
||||
.react-colorful__saturation-pointer {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-color: var(--white);
|
||||
}
|
||||
}
|
||||
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RgbaColorPicker } from 'react-colorful';
|
||||
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
|
||||
|
||||
type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
const { styleClass, ...rest } = props;
|
||||
|
||||
return (
|
||||
<RgbaColorPicker
|
||||
className={`invokeai__color-picker ${styleClass}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIColorPicker;
|
||||
20
frontend/src/common/components/IAIIconButton.scss
Normal file
20
frontend/src/common/components/IAIIconButton.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.icon-button {
|
||||
background-color: var(--btn-grey);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-grey-hover);
|
||||
}
|
||||
|
||||
&[data-selected=true] {
|
||||
background-color: var(--accent-color);
|
||||
&:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,28 @@ import {
|
||||
interface Props extends IconButtonProps {
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: PlacementWithLogical | undefined;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*
|
||||
* TODO: Get rid of this.
|
||||
*/
|
||||
const IAIIconButton = (props: Props) => {
|
||||
const { tooltip = '', tooltipPlacement = 'bottom', onClick, ...rest } = props;
|
||||
const {
|
||||
tooltip = '',
|
||||
tooltipPlacement = 'top',
|
||||
styleClass,
|
||||
onClick,
|
||||
cursor,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
||||
<IconButton
|
||||
className={`icon-button ${styleClass}`}
|
||||
{...rest}
|
||||
cursor={onClick ? 'pointer' : 'unset'}
|
||||
cursor={cursor ? cursor : onClick ? 'pointer' : 'unset'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
.number-input {
|
||||
.invokeai__number-input-form-control {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.number-input-label {
|
||||
.invokeai__number-input-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
flex-grow: 2;
|
||||
white-space: nowrap;
|
||||
|
||||
&[data-focus] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
|
||||
&[aria-invalid='true'] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
border: 2px solid var(--border-color-invalid);
|
||||
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
|
||||
}
|
||||
}
|
||||
|
||||
.number-input-field {
|
||||
.invokeai__number-input-root {
|
||||
height: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
column-gap: 0.5rem;
|
||||
@@ -19,34 +36,45 @@
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.number-input-entry {
|
||||
.invokeai__number-input-field {
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
padding-inline-end: 0;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.number-input-stepper {
|
||||
.invokeai__number-input-stepper {
|
||||
display: grid;
|
||||
padding-right: 0.7rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.number-input-stepper-button {
|
||||
.invokeai__number-input-stepper-button {
|
||||
border: none;
|
||||
// expand arrow hitbox
|
||||
padding: 0 0.5rem;
|
||||
margin: 0 -0.5rem;
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
// fill: ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
NumberDecrementStepper,
|
||||
NumberInputProps,
|
||||
FormLabel,
|
||||
NumberInputFieldProps,
|
||||
NumberInputStepperProps,
|
||||
FormControlProps,
|
||||
FormLabelProps,
|
||||
TooltipProps,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import _ from 'lodash';
|
||||
import { FocusEvent, useEffect, useState } from 'react';
|
||||
@@ -23,6 +29,12 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||
max: number;
|
||||
clamp?: boolean;
|
||||
isInteger?: boolean;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
numberInputProps?: NumberInputProps;
|
||||
numberInputFieldProps?: NumberInputFieldProps;
|
||||
numberInputStepperProps?: NumberInputStepperProps;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,8 +46,6 @@ const IAINumberInput = (props: Props) => {
|
||||
styleClass,
|
||||
isDisabled = false,
|
||||
showStepper = true,
|
||||
fontSize = '1rem',
|
||||
size = 'sm',
|
||||
width,
|
||||
textAlign,
|
||||
isInvalid,
|
||||
@@ -44,6 +54,11 @@ const IAINumberInput = (props: Props) => {
|
||||
min,
|
||||
max,
|
||||
isInteger = true,
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
numberInputFieldProps,
|
||||
numberInputStepperProps,
|
||||
tooltipProps,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -65,7 +80,10 @@ const IAINumberInput = (props: Props) => {
|
||||
* from the current value.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!valueAsString.match(numberStringRegex) && value !== Number(valueAsString)) {
|
||||
if (
|
||||
!valueAsString.match(numberStringRegex) &&
|
||||
value !== Number(valueAsString)
|
||||
) {
|
||||
setValueAsString(String(value));
|
||||
}
|
||||
}, [value, valueAsString]);
|
||||
@@ -94,47 +112,51 @@ const IAINumberInput = (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
className={`number-input ${styleClass}`}
|
||||
>
|
||||
{label && (
|
||||
<Tooltip {...tooltipProps}>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
className={`invokeai__number-input-form-control ${styleClass}`}
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
className="number-input-label"
|
||||
className="invokeai__number-input-form-label"
|
||||
style={{ display: label ? 'block' : 'none' }}
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<NumberInput
|
||||
size={size}
|
||||
{...rest}
|
||||
className="number-input-field"
|
||||
value={valueAsString}
|
||||
keepWithinRange={true}
|
||||
clampValueOnBlur={false}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<NumberInputField
|
||||
fontSize={fontSize}
|
||||
className="number-input-entry"
|
||||
<NumberInput
|
||||
className="invokeai__number-input-root"
|
||||
value={valueAsString}
|
||||
keepWithinRange={true}
|
||||
clampValueOnBlur={false}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleBlur}
|
||||
width={width}
|
||||
textAlign={textAlign}
|
||||
/>
|
||||
<div
|
||||
className="number-input-stepper"
|
||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||
{...rest}
|
||||
>
|
||||
<NumberIncrementStepper className="number-input-stepper-button" />
|
||||
<NumberDecrementStepper className="number-input-stepper-button" />
|
||||
</div>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
<NumberInputField
|
||||
className="invokeai__number-input-field"
|
||||
textAlign={textAlign}
|
||||
{...numberInputFieldProps}
|
||||
/>
|
||||
<div
|
||||
className="invokeai__number-input-stepper"
|
||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||
>
|
||||
<NumberIncrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
<NumberDecrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
</div>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
12
frontend/src/common/components/IAIPopover.scss
Normal file
12
frontend/src/common/components/IAIPopover.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.invokeai__popover-content {
|
||||
min-width: unset;
|
||||
width: unset !important;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem !important;
|
||||
background-color: var(--background-color) !important;
|
||||
border: 2px solid var(--border-color) !important;
|
||||
|
||||
.invokeai__popover-arrow {
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
}
|
||||
39
frontend/src/common/components/IAIPopover.tsx
Normal file
39
frontend/src/common/components/IAIPopover.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { PopoverProps } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIPopoverProps = PopoverProps & {
|
||||
triggerComponent: ReactNode;
|
||||
children: ReactNode;
|
||||
styleClass?: string;
|
||||
hasArrow?: boolean;
|
||||
};
|
||||
|
||||
const IAIPopover = (props: IAIPopoverProps) => {
|
||||
const {
|
||||
triggerComponent,
|
||||
children,
|
||||
styleClass,
|
||||
hasArrow = true,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<Popover {...rest}>
|
||||
<PopoverTrigger>
|
||||
<Box>{triggerComponent}</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAIPopover;
|
||||
@@ -1,28 +1,32 @@
|
||||
.iai-select {
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__select {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
|
||||
.iai-select-label {
|
||||
.invokeai__select-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.iai-select-picker {
|
||||
.invokeai__select-picker {
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: var(--background-color-secondary);
|
||||
font-weight: bold;
|
||||
height: 2rem;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
border: 2px solid var(--input-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.iai-select-option {
|
||||
.invokeai__select-option {
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ const IAISelect = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} className={`iai-select ${styleClass}`}>
|
||||
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
className="iai-select-label"
|
||||
className="invokeai__select-label"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
@@ -35,11 +35,11 @@ const IAISelect = (props: Props) => {
|
||||
fontSize={fontSize}
|
||||
size={size}
|
||||
{...rest}
|
||||
className="iai-select-picker"
|
||||
className="invokeai__select-picker"
|
||||
>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt} className="iai-select-option">
|
||||
<option key={opt} value={opt} className="invokeai__select-option">
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
|
||||
40
frontend/src/common/components/IAISlider.scss
Normal file
40
frontend/src/common/components/IAISlider.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__slider-form-control {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
.invokeai__slider-inner-container {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.invokeai__slider-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.invokeai__slider-root {
|
||||
.invokeai__slider-filled-track {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
.invokeai__slider-track {
|
||||
background-color: var(--text-color-secondary);
|
||||
height: 5px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb-tooltip {
|
||||
}
|
||||
88
frontend/src/common/components/IAISlider.tsx
Normal file
88
frontend/src/common/components/IAISlider.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Tooltip,
|
||||
SliderProps,
|
||||
FormControlProps,
|
||||
FormLabelProps,
|
||||
SliderTrackProps,
|
||||
SliderThumbProps,
|
||||
TooltipProps,
|
||||
SliderInnerTrackProps,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
type IAISliderProps = SliderProps & {
|
||||
label?: string;
|
||||
styleClass?: string;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
sliderTrackProps?: SliderTrackProps;
|
||||
sliderInnerTrackProps?: SliderInnerTrackProps;
|
||||
sliderThumbProps?: SliderThumbProps;
|
||||
sliderThumbTooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
};
|
||||
|
||||
const IAISlider = (props: IAISliderProps) => {
|
||||
const {
|
||||
label,
|
||||
styleClass,
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
sliderTrackProps,
|
||||
sliderInnerTrackProps,
|
||||
sliderThumbProps,
|
||||
sliderThumbTooltipProps,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl
|
||||
className={`invokeai__slider-form-control ${styleClass}`}
|
||||
{...formControlProps}
|
||||
>
|
||||
<div className="invokeai__slider-inner-container">
|
||||
<FormLabel
|
||||
className={`invokeai__slider-form-label`}
|
||||
whiteSpace="nowrap"
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
|
||||
<Slider
|
||||
className={`invokeai__slider-root`}
|
||||
aria-label={label}
|
||||
focusThumbOnChange={false}
|
||||
{...rest}
|
||||
>
|
||||
<SliderTrack
|
||||
className={`invokeai__slider-track`}
|
||||
{...sliderTrackProps}
|
||||
>
|
||||
<SliderFilledTrack
|
||||
className={`invokeai__slider-filled-track`}
|
||||
{...sliderInnerTrackProps}
|
||||
/>
|
||||
</SliderTrack>
|
||||
|
||||
<Tooltip
|
||||
className={`invokeai__slider-thumb-tooltip`}
|
||||
placement="top"
|
||||
hasArrow
|
||||
{...sliderThumbTooltipProps}
|
||||
>
|
||||
<SliderThumb
|
||||
className={`invokeai__slider-thumb`}
|
||||
{...sliderThumbProps}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Slider>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAISlider;
|
||||
@@ -1,18 +1,32 @@
|
||||
.chakra-switch,
|
||||
.switch-button {
|
||||
span {
|
||||
background-color: var(--switch-bg-color);
|
||||
.invokeai__switch-form-control {
|
||||
.invokeai__switch-form-label {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 1rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0.1rem;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
.invokeai__switch-root {
|
||||
span {
|
||||
background-color: var(--switch-bg-color);
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
span[data-checked] {
|
||||
background: var(--switch-bg-active-color);
|
||||
&[data-checked] {
|
||||
span {
|
||||
background: var(--switch-bg-active-color);
|
||||
|
||||
span {
|
||||
background-color: var(--white);
|
||||
span {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,24 @@ const IAISwitch = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} width={width}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
{label && (
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Switch size={size} className="switch-button" {...rest} />
|
||||
</Flex>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
width={width}
|
||||
className="invokeai__switch-form-control"
|
||||
>
|
||||
<FormLabel
|
||||
className="invokeai__switch-form-label"
|
||||
fontSize={fontSize}
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{label}
|
||||
<Switch
|
||||
className="invokeai__switch-root"
|
||||
size={size}
|
||||
// className="switch-button"
|
||||
{...rest}
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
.invokeai__slider-root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
width: 200px;
|
||||
|
||||
&[data-orientation='horizontal'] {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&[data-orientation='vertical'] {
|
||||
width: 20px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.invokeai__slider-track {
|
||||
background-color: black;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
border-radius: 9999px;
|
||||
|
||||
&[data-orientation='horizontal'] {
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
&[data-orientation='vertical'] {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.invokeai__slider-range {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border-radius: 9999px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.invokeai__slider-thumb-div {
|
||||
all: unset;
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 10px rgba(0, 2, 10, 0.3);
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: violet;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 5px rgba(0, 2, 10, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import * as Slider from '@radix-ui/react-slider';
|
||||
import React from 'react';
|
||||
import IAITooltip from './IAITooltip';
|
||||
|
||||
type IAISliderProps = Slider.SliderProps & {
|
||||
value: number[];
|
||||
tooltipLabel?: string;
|
||||
orientation?: 'horizontal' | 'vertial';
|
||||
trackProps?: Slider.SliderTrackProps;
|
||||
rangeProps?: Slider.SliderRangeProps;
|
||||
thumbProps?: Slider.SliderThumbProps;
|
||||
};
|
||||
|
||||
const _IAISlider = (props: IAISliderProps) => {
|
||||
const {
|
||||
value,
|
||||
tooltipLabel,
|
||||
orientation,
|
||||
trackProps,
|
||||
rangeProps,
|
||||
thumbProps,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<Slider.Root
|
||||
className="invokeai__slider-root"
|
||||
{...rest}
|
||||
data-orientation={orientation || 'horizontal'}
|
||||
>
|
||||
<Slider.Track {...trackProps} className="invokeai__slider-track">
|
||||
<Slider.Range {...rangeProps} className="invokeai__slider-range" />
|
||||
</Slider.Track>
|
||||
<Tooltip label={tooltipLabel ?? value[0]} placement="top">
|
||||
<Slider.Thumb {...thumbProps} className="invokeai__slider-thumb">
|
||||
<div className="invokeai__slider-thumb-div" />
|
||||
{/*<IAITooltip trigger={<div className="invokeai__slider-thumb-div" />}>
|
||||
{value && value[0]}
|
||||
</IAITooltip>*/}
|
||||
</Slider.Thumb>
|
||||
</Tooltip>
|
||||
</Slider.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default _IAISlider;
|
||||
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.invokeai__tooltip-content {
|
||||
padding: 0.5rem;
|
||||
background-color: grey;
|
||||
border-radius: 0.25rem;
|
||||
.invokeai__tooltip-arrow {
|
||||
background-color: grey;
|
||||
}
|
||||
}
|
||||
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAITooltipProps = Tooltip.TooltipProps & {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
triggerProps?: Tooltip.TooltipTriggerProps;
|
||||
contentProps?: Tooltip.TooltipContentProps;
|
||||
arrowProps?: Tooltip.TooltipArrowProps;
|
||||
};
|
||||
|
||||
const IAITooltip = (props: IAITooltipProps) => {
|
||||
const { trigger, children, triggerProps, contentProps, arrowProps, ...rest } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root {...rest} delayDuration={0}>
|
||||
<Tooltip.Trigger {...triggerProps}>{trigger}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
{...contentProps}
|
||||
onPointerDownOutside={(e: any) => {e.preventDefault()}}
|
||||
className="invokeai__tooltip-content"
|
||||
>
|
||||
<Tooltip.Arrow {...arrowProps} className="invokeai__tooltip-arrow" />
|
||||
{children}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAITooltip;
|
||||
@@ -3,9 +3,12 @@ import { isEqual } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { GalleryState } from '../../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||
|
||||
export const optionsSelector = createSelector(
|
||||
@@ -18,7 +21,7 @@ export const optionsSelector = createSelector(
|
||||
maskPath: options.maskPath,
|
||||
initialImagePath: options.initialImagePath,
|
||||
seed: options.seed,
|
||||
activeTab: options.activeTab,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
};
|
||||
},
|
||||
{
|
||||
@@ -43,31 +46,66 @@ export const systemSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const inpaintingSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
return {
|
||||
isMaskEmpty: inpainting.lines.length === 0,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const gallerySelector = createSelector(
|
||||
(state: RootState) => state.gallery,
|
||||
(gallery: GalleryState) => {
|
||||
return {
|
||||
hasCurrentImage: Boolean(gallery.currentImage),
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
* This is used to prevent the 'Generate' button from being clicked.
|
||||
*/
|
||||
const useCheckParameters = (): boolean => {
|
||||
const { prompt } = useAppSelector(optionsSelector);
|
||||
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
seed,
|
||||
activeTab,
|
||||
activeTabName,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
|
||||
const { isMaskEmpty } = useAppSelector(inpaintingSelector);
|
||||
|
||||
const { hasCurrentImage } = useAppSelector(gallerySelector);
|
||||
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prompt && !initialImagePath && activeTab === 1) {
|
||||
if (activeTabName === 'img2img' && !initialImagePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'inpainting' && (!hasCurrentImage || isMaskEmpty)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -106,7 +144,9 @@ const useCheckParameters = (): boolean => {
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
activeTab,
|
||||
activeTabName,
|
||||
hasCurrentImage,
|
||||
isMaskEmpty,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
/*
|
||||
These functions translate frontend state into parameters
|
||||
suitable for consumption by the backend, and vice-versa.
|
||||
*/
|
||||
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
|
||||
import {
|
||||
seedWeightsToString,
|
||||
stringToSeedWeightsArray,
|
||||
} from './seedWeightPairs';
|
||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||
import randomInt from './randomInt';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
||||
|
||||
export type FrontendToBackendParametersConfig = {
|
||||
generationMode: InvokeTabName;
|
||||
optionsState: OptionsState;
|
||||
inpaintingState: InpaintingState;
|
||||
systemState: SystemState;
|
||||
imageToProcessUrl?: string;
|
||||
maskImageElement?: HTMLImageElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates/formats frontend state into parameters suitable
|
||||
* for consumption by the API.
|
||||
*/
|
||||
export const frontendToBackendParameters = (
|
||||
optionsState: OptionsState,
|
||||
systemState: SystemState
|
||||
config: FrontendToBackendParametersConfig
|
||||
): { [key: string]: any } => {
|
||||
const {
|
||||
generationMode,
|
||||
optionsState,
|
||||
inpaintingState,
|
||||
systemState,
|
||||
imageToProcessUrl,
|
||||
maskImageElement,
|
||||
} = config;
|
||||
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
@@ -30,10 +46,8 @@ export const frontendToBackendParameters = (
|
||||
seed,
|
||||
seamless,
|
||||
hiresFix,
|
||||
shouldUseInitImage,
|
||||
img2imgStrength,
|
||||
initialImagePath,
|
||||
maskPath,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
@@ -61,8 +75,6 @@ export const frontendToBackendParameters = (
|
||||
width,
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
seamless,
|
||||
hires_fix: hiresFix,
|
||||
progress_images: shouldDisplayInProgress,
|
||||
};
|
||||
|
||||
@@ -70,13 +82,45 @@ export const frontendToBackendParameters = (
|
||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||
: seed;
|
||||
|
||||
if (shouldUseInitImage) {
|
||||
// parameters common to txt2img and img2img
|
||||
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||
generationParameters.seamless = seamless;
|
||||
generationParameters.hires_fix = hiresFix;
|
||||
}
|
||||
|
||||
// img2img exclusive parameters
|
||||
if (generationMode === 'img2img') {
|
||||
generationParameters.init_img = initialImagePath;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = shouldFitToWidthHeight;
|
||||
if (maskPath) {
|
||||
generationParameters.init_mask = maskPath;
|
||||
}
|
||||
}
|
||||
|
||||
// inpainting exclusive parameters
|
||||
if (generationMode === 'inpainting' && maskImageElement) {
|
||||
const {
|
||||
lines,
|
||||
boundingBoxCoordinate: { x, y },
|
||||
boundingBoxDimensions: { width, height },
|
||||
} = inpaintingState;
|
||||
|
||||
const boundingBox = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
generationParameters.init_img = imageToProcessUrl;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = false;
|
||||
|
||||
const maskDataURL = generateMask(maskImageElement, lines, boundingBox);
|
||||
|
||||
generationParameters.init_mask = maskDataURL.split(
|
||||
'data:image/png;base64,'
|
||||
)[1];
|
||||
|
||||
generationParameters.bounding_box = boundingBox;
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
@@ -105,7 +149,7 @@ export const frontendToBackendParameters = (
|
||||
strength: facetoolStrength,
|
||||
};
|
||||
if (facetoolType === 'codeformer') {
|
||||
facetoolParameters.codeformer_fidelity = codeformerFidelity
|
||||
facetoolParameters.codeformer_fidelity = codeformerFidelity;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
frontend/src/common/util/roundDownToMultiple.ts
Normal file
3
frontend/src/common/util/roundDownToMultiple.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const roundDownToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.floor(num / multiple) * multiple;
|
||||
};
|
||||
Reference in New Issue
Block a user