Add New WebUI and Desktop Mode

Co-Authored-By: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
blessedcoolant 2022-10-04 05:15:26 +13:00 committed by Lincoln Stein
parent 40828df663
commit b8e4c13746
157 changed files with 4775 additions and 2622 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
frontend/dist/assets/Inter.b9a8e5e2.ttf vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

483
frontend/dist/assets/index.1dc2a85b.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.checkerboard{background-position:0px 0px,10px 10px;background-size:20px 20px;background-image:linear-gradient(45deg,#eee 25%,transparent 25%,transparent 75%,#eee 75%,#eee 100%),linear-gradient(45deg,#eee 25%,white 25%,white 75%,#eee 75%,#eee 100%)}

File diff suppressed because one or more lines are too long

BIN
frontend/dist/assets/logo.13003d72.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,14 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI Stable Diffusion Dream Server</title>
<script type="module" crossorigin src="/assets/index.1332a4e9.js"></script>
<link rel="stylesheet" href="/assets/index.447eb2a9.css">
</head>
<body>
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="/assets/index.1dc2a85b.js"></script>
<link rel="stylesheet" href="/assets/index.853a336f.css">
</head>
<body>
<div id="root"></div>
</body>
</body>
</html>

BIN
frontend/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -1,12 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI Stable Diffusion Dream Server</title>
</head>
<body>
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="favicon.ico" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</body>
</html>

View File

@ -19,6 +19,7 @@
"dateformat": "^5.0.3",
"framer-motion": "^7.2.1",
"lodash": "^4.17.21",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.2",
@ -38,6 +39,7 @@
"eslint": "^8.23.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react-hooks": "^4.6.0",
"sass": "^1.55.0",
"tsc-watch": "^5.0.3",
"typescript": "^4.6.4",
"vite": "^3.0.7",

17
frontend/src/app/App.scss Normal file
View File

@ -0,0 +1,17 @@
@use '../styles/Mixins/' as *;
.App {
display: grid;
}
.app-content {
display: grid;
row-gap: 1rem;
margin: 0.6rem;
padding: 1rem;
border-radius: 0.5rem;
background-color: var(--background-color);
grid-auto-rows: max-content;
width: $app-width;
height: $app-height;
}

View File

@ -1,16 +1,14 @@
import { Grid, GridItem } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import CurrentImageDisplay from '../features/gallery/CurrentImageDisplay';
import ImageGallery from '../features/gallery/ImageGallery';
import ProgressBar from '../features/system/ProgressBar';
import SiteHeader from '../features/system/SiteHeader';
import OptionsAccordion from '../features/options/OptionsAccordion';
import ProcessButtons from '../features/options/ProcessButtons';
import PromptInput from '../features/options/PromptInput';
import LogViewer from '../features/system/LogViewer';
import Console from '../features/system/Console';
import Loading from '../Loading';
import { useAppDispatch } from './store';
import { requestSystemConfig } from './socketio/actions';
import { keepGUIAlive } from './utils';
import InvokeTabs from '../features/tabs/InvokeTabs';
keepGUIAlive();
const App = () => {
const dispatch = useAppDispatch();
@ -22,43 +20,14 @@ const App = () => {
}, [dispatch]);
return isReady ? (
<>
<Grid
width="100vw"
height="100vh"
templateAreas={`
"header header header header"
"progressBar progressBar progressBar progressBar"
"menu prompt processButtons imageRoll"
"menu currentImage currentImage imageRoll"`}
gridTemplateRows={'36px 10px 100px auto'}
gridTemplateColumns={'350px auto 100px 388px'}
gap={2}
>
<GridItem area={'header'} pt={1}>
<SiteHeader />
</GridItem>
<GridItem area={'progressBar'}>
<div className="App">
<ProgressBar />
</GridItem>
<GridItem pl="2" area={'menu'} overflowY="scroll">
<OptionsAccordion />
</GridItem>
<GridItem area={'prompt'}>
<PromptInput />
</GridItem>
<GridItem area={'processButtons'}>
<ProcessButtons />
</GridItem>
<GridItem area={'currentImage'}>
<CurrentImageDisplay />
</GridItem>
<GridItem pr="2" area={'imageRoll'} overflowY="scroll">
<ImageGallery />
</GridItem>
</Grid>
<LogViewer />
</>
<div className="app-content">
<SiteHeader />
<InvokeTabs />
</div>
<Console />
</div>
) : (
<Loading />
);

View File

@ -15,13 +15,15 @@ export const SAMPLERS: Array<string> = [
// Valid image widths
export const WIDTHS: Array<number> = [
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
1024,
1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792,
1856, 1920, 1984, 2048,
];
// Valid image heights
export const HEIGHTS: Array<number> = [
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
1024,
1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792,
1856, 1920, 1984, 2048,
];
// Valid upscaling levels

View File

@ -7,12 +7,12 @@ type FeatureHelpInfo = {
export enum Feature {
PROMPT,
GALLERY,
OUTPUT,
SEED_AND_VARIATION,
ESRGAN,
OTHER,
SEED,
VARIATIONS,
UPSCALE,
FACE_CORRECTION,
IMAGE_TO_IMAGE,
SAMPLER,
}
export const FEATURES: Record<Feature, FeatureHelpInfo> = {
@ -26,18 +26,23 @@ export const FEATURES: Record<Feature, FeatureHelpInfo> = {
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.OUTPUT]: {
text: 'The Height and Width of generations can be controlled here. If you experience errors, you may be generating an image too large for your system. The seamless option will more often result in repeating patterns in outputs.',
[Feature.OTHER]: {
text: 'Additional Options',
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.SEED_AND_VARIATION]: {
text: 'Seed values provide an initial set of noise which guide the denoising process. Try a variation with an amount of between 0 and 1 to change the output image for that seed.',
[Feature.SEED]: {
text: 'Seed values provide an initial set of noise which guide the denoising process.',
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.ESRGAN]: {
text: 'The ESRGAN setting can be used to increase the output resolution without requiring a higher width/height in the initial generation.',
[Feature.VARIATIONS]: {
text: 'Try a variation with an amount of between 0 and 1 to change the output image for the set seed.',
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.UPSCALE]: {
text: 'Using ESRGAN you can increase the output resolution without requiring a higher width/height in the initial generation.',
href: 'link/to/docs/feature1.html',
guideImage: 'asset/path.gif',
},
@ -51,9 +56,4 @@ export const FEATURES: Record<Feature, FeatureHelpInfo> = {
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.SAMPLER]: {
text: 'This setting allows for different denoising samplers to be used, as well as the number of denoising steps used, which will change the resulting output.',
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
};

View File

@ -129,6 +129,7 @@ export declare type SystemStatus = {
totalIterations: number;
currentStatus: string;
currentStatusHasSteps: boolean;
hasError: boolean;
};
export declare type SystemConfig = {
@ -160,9 +161,7 @@ export declare type ErrorResponse = {
export declare type GalleryImagesResponse = {
images: Array<Omit<Image, 'uuid'>>;
nextPage: number;
offset: number;
onlyNewImages: boolean;
areMoreImagesAvailable: boolean;
};
export declare type ImageUrlAndUuidResponse = {

View File

@ -50,7 +50,10 @@ const makeSocketIOEmitters = (
const esrganParameters = {
upscale: [upscalingLevel, upscalingStrength],
};
socketio.emit('runESRGAN', imageToProcess, esrganParameters);
socketio.emit('runPostprocessing', imageToProcess, {
type: 'esrgan',
...esrganParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
@ -68,7 +71,10 @@ const makeSocketIOEmitters = (
const gfpganParameters = {
gfpgan_strength: gfpganStrength,
};
socketio.emit('runGFPGAN', imageToProcess, gfpganParameters);
socketio.emit('runPostprocessing', imageToProcess, {
type: 'gfpgan',
...gfpganParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
@ -84,16 +90,12 @@ const makeSocketIOEmitters = (
socketio.emit('deleteImage', url, uuid);
},
emitRequestImages: () => {
const { nextPage, offset } = getState().gallery;
socketio.emit('requestImages', nextPage, offset);
const { earliest_mtime } = getState().gallery;
socketio.emit('requestImages', earliest_mtime);
},
emitRequestNewImages: () => {
const { nextPage, offset, images } = getState().gallery;
if (images.length > 0) {
socketio.emit('requestImages', nextPage, offset, images[0].mtime);
} else {
socketio.emit('requestImages', nextPage, offset);
}
const { latest_mtime } = getState().gallery;
socketio.emit('requestLatestImages', latest_mtime);
},
emitCancelProcessing: () => {
socketio.emit('cancel');

View File

@ -11,6 +11,8 @@ import {
setSystemStatus,
setCurrentStatus,
setSystemConfig,
processingCanceled,
errorOccurred,
} from '../../features/system/systemSlice';
import {
@ -25,7 +27,7 @@ import {
setInitialImagePath,
setMaskPath,
} from '../../features/options/optionsSlice';
import { requestNewImages } from './actions';
import { requestImages, requestNewImages } from './actions';
/**
* Returns an object containing listener callbacks for socketio events.
@ -44,7 +46,11 @@ const makeSocketIOListeners = (
try {
dispatch(setIsConnected(true));
dispatch(setCurrentStatus('Connected'));
if (getState().gallery.latest_mtime) {
dispatch(requestNewImages());
} else {
dispatch(requestImages());
}
} catch (e) {
console.error(e);
}
@ -90,7 +96,6 @@ const makeSocketIOListeners = (
message: `Image generated: ${url}`,
})
);
dispatch(setIsProcessing(false));
} catch (e) {
console.error(e);
}
@ -116,7 +121,6 @@ const makeSocketIOListeners = (
message: `Intermediate image generated: ${url}`,
})
);
dispatch(setIsProcessing(false));
} catch (e) {
console.error(e);
}
@ -124,7 +128,7 @@ const makeSocketIOListeners = (
/**
* Callback to run when we receive an 'esrganResult' event.
*/
onESRGANResult: (data: InvokeAI.ImageResultResponse) => {
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
try {
const { url, metadata, mtime } = data;
@ -140,10 +144,9 @@ const makeSocketIOListeners = (
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Upscaled: ${url}`,
message: `Postprocessed: ${url}`,
})
);
dispatch(setIsProcessing(false));
} catch (e) {
console.error(e);
}
@ -204,7 +207,7 @@ const makeSocketIOListeners = (
level: 'error',
})
);
dispatch(setIsProcessing(false));
dispatch(errorOccurred());
dispatch(clearIntermediateImage());
} catch (e) {
console.error(e);
@ -214,7 +217,7 @@ const makeSocketIOListeners = (
* Callback to run when we receive a 'galleryImages' event.
*/
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
const { images, nextPage, offset } = data;
const { images, areMoreImagesAvailable } = data;
/**
* the logic here ideally would be in the reducer but we have a side effect:
@ -232,7 +235,9 @@ const makeSocketIOListeners = (
};
});
dispatch(addGalleryImages({ images: preparedImages, nextPage, offset }));
dispatch(
addGalleryImages({ images: preparedImages, areMoreImagesAvailable })
);
dispatch(
addLogEntry({
@ -245,7 +250,7 @@ const makeSocketIOListeners = (
* Callback to run when we receive a 'processingCanceled' event.
*/
onProcessingCanceled: () => {
dispatch(setIsProcessing(false));
dispatch(processingCanceled());
const { intermediateImage } = getState().gallery;
@ -259,6 +264,7 @@ const makeSocketIOListeners = (
);
dispatch(clearIntermediateImage());
}
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),

View File

@ -35,8 +35,7 @@ export const socketioMiddleware = () => {
onConnect,
onDisconnect,
onError,
onESRGANResult,
onGFPGANResult,
onPostprocessingResult,
onGenerationResult,
onIntermediateResult,
onProgressUpdate,
@ -76,12 +75,9 @@ export const socketioMiddleware = () => {
onGenerationResult(data)
);
socketio.on('esrganResult', (data: InvokeAI.ImageResultResponse) =>
onESRGANResult(data)
);
socketio.on('gfpganResult', (data: InvokeAI.ImageResultResponse) =>
onGFPGANResult(data)
socketio.on(
'postprocessingResult',
(data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data)
);
socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
@ -153,7 +149,6 @@ export const socketioMiddleware = () => {
break;
}
case 'socketio/cancelProcessing': {
emitCancelProcessing();
break;

View File

@ -7,6 +7,7 @@ import storage from 'redux-persist/lib/storage'; // defaults to localStorage for
import optionsReducer from '../features/options/optionsSlice';
import galleryReducer from '../features/gallery/gallerySlice';
import systemReducer from '../features/system/systemSlice';
import { socketioMiddleware } from './socketio/middleware';

25
frontend/src/app/utils.ts Normal file
View File

@ -0,0 +1,25 @@
export function keepGUIAlive() {
async function getRequest(url = '') {
const response = await fetch(url, {
method: 'GET',
cache: 'no-cache',
});
return response;
}
const keepAliveServer = () => {
const url = document.location;
const route = '/flaskwebgui-keep-server-alive';
getRequest(url + route).then((data) => {
return data;
});
};
if (!import.meta.env.NODE_ENV || import.meta.env.NODE_ENV === 'production') {
document.addEventListener('DOMContentLoaded', () => {
const intervalRequest = 3 * 1000;
keepAliveServer();
setInterval(keepAliveServer, intervalRequest);
});
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,20 @@
.guide-popover-arrow {
background-color: var(--tab-panel-bg) !important;
box-shadow: none !important;
}
.guide-popover-content {
background-color: var(--background-color-secondary) !important;
border: none !important;
}
.guide-popover-guide-content {
background: var(--tab-panel-bg);
border: 2px solid var(--tab-hover-color);
border-radius: 0.4rem;
padding: 0.75rem 1rem 0.75rem 1rem;
display: grid;
grid-template-rows: repeat(auto-fill, 1fr);
grid-row-gap: 0.5rem;
justify-content: space-between;
}

View File

@ -3,8 +3,6 @@ import {
PopoverArrow,
PopoverContent,
PopoverTrigger,
PopoverHeader,
Flex,
Box,
} from '@chakra-ui/react';
import { SystemState } from '../../features/system/systemSlice';
@ -33,14 +31,13 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
<Box>{children}</Box>
</PopoverTrigger>
<PopoverContent
className={`guide-popover-content`}
maxWidth="400px"
onClick={(e) => e.preventDefault()}
cursor={'initial'}
>
<PopoverArrow />
<Flex alignItems={'center'} gap={2} p={4}>
{text}
</Flex>
<PopoverArrow className="guide-popover-arrow" />
<div className="guide-popover-guide-content">{text}</div>
</PopoverContent>
</Popover>
) : (

View File

@ -0,0 +1,24 @@
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
interface Props extends ButtonProps {
label: string;
tooltip?: string;
}
/**
* Reusable customized button component. Originally was more customized - now probably unecessary.
*
* TODO: Get rid of this.
*/
const IAIButton = (props: Props) => {
const { label, tooltip = '', size = 'sm', ...rest } = props;
return (
<Tooltip label={tooltip}>
<Button size={size} {...rest}>
{label}
</Button>
</Tooltip>
);
};
export default IAIButton;

View File

@ -0,0 +1,21 @@
import { IconButtonProps, IconButton, Tooltip } from '@chakra-ui/react';
interface Props extends IconButtonProps {
tooltip?: string;
}
/**
* Reusable customized button component. Originally was more customized - now probably unecessary.
*
* TODO: Get rid of this.
*/
const IAIIconButton = (props: Props) => {
const { tooltip = '', onClick, ...rest } = props;
return (
<Tooltip label={tooltip}>
<IconButton {...rest} cursor={onClick ? 'pointer' : 'unset'} onClick={onClick}/>
</Tooltip>
);
};
export default IAIIconButton;

View File

@ -0,0 +1,34 @@
.input {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center;
.input-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.input-entry {
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
border-radius: 0.2rem;
font-weight: bold;
&:focus {
outline: none;
border: 2px solid var(--prompt-border-color);
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
}
&:disabled {
opacity: 0.2;
}
&[aria-invalid='true'] {
outline: none;
border: 2px solid var(--border-color-invalid);
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
}
}
}

View File

@ -0,0 +1,41 @@
import { FormControl, FormLabel, Input, InputProps } from '@chakra-ui/react';
import { ChangeEvent } from 'react';
interface IAIInputProps extends InputProps {
styleClass?: string;
label?: string;
width?: string | number;
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
export default function IAIInput(props: IAIInputProps) {
const {
label,
styleClass,
isDisabled = false,
fontSize = '1rem',
width,
isInvalid,
...rest
} = props;
return (
<FormControl
className={`input ${styleClass}`}
isInvalid={isInvalid}
isDisabled={isDisabled}
flexGrow={1}
>
<FormLabel
fontSize={fontSize}
marginBottom={1}
whiteSpace="nowrap"
className="input-label"
>
{label}
</FormLabel>
<Input {...rest} className="input-entry" size={'sm'} width={width} />
</FormControl>
);
}

View File

@ -0,0 +1,52 @@
.number-input {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center;
.number-input-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.number-input-field {
display: grid;
grid-template-columns: auto max-content;
column-gap: 0.5rem;
align-items: center;
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
border-radius: 0.2rem;
}
.number-input-entry {
border: none;
font-weight: bold;
width: 100%;
padding-inline-end: 0;
&:focus {
outline: none;
border: 2px solid var(--prompt-border-color);
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
}
&:disabled {
opacity: 0.2;
}
}
.number-input-stepper {
display: grid;
padding-right: 0.7rem;
svg {
width: 12px;
height: 12px;
}
.number-input-stepper-button {
border: none;
}
}
}

View File

@ -0,0 +1,141 @@
import {
FormControl,
NumberInput,
NumberInputField,
NumberIncrementStepper,
NumberDecrementStepper,
NumberInputProps,
FormLabel,
} from '@chakra-ui/react';
import _ from 'lodash';
import { FocusEvent, useEffect, useState } from 'react';
const numberStringRegex = /^-?(0\.)?\.?$/;
interface Props extends Omit<NumberInputProps, 'onChange'> {
styleClass?: string;
label?: string;
width?: string | number;
showStepper?: boolean;
value: number;
onChange: (v: number) => void;
min: number;
max: number;
clamp?: boolean;
isInteger?: boolean;
}
/**
* Customized Chakra FormControl + NumberInput multi-part component.
*/
const IAINumberInput = (props: Props) => {
const {
label,
styleClass,
isDisabled = false,
showStepper = true,
fontSize = '1rem',
size = 'sm',
width,
textAlign,
isInvalid,
value,
onChange,
min,
max,
isInteger = true,
...rest
} = props;
/**
* Using a controlled input with a value that accepts decimals needs special
* handling. If the user starts to type in "1.5", by the time they press the
* 5, the value has been parsed from "1." to "1" and they end up with "15".
*
* To resolve this, this component keeps a the value as a string internally,
* and the UI component uses that. When a change is made, that string is parsed
* as a number and given to the `onChange` function.
*/
const [valueAsString, setValueAsString] = useState<string>(String(value));
/**
* When `value` changes (e.g. from a diff source than this component), we need
* to update the internal `valueAsString`, but only if the actual value is different
* from the current value.
*/
useEffect(() => {
if (!valueAsString.match(numberStringRegex) && value !== Number(valueAsString)) {
setValueAsString(String(value));
}
}, [value, valueAsString]);
const handleOnChange = (v: string) => {
setValueAsString(v);
// This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc.
if (!v.match(numberStringRegex)) {
// Cast the value to number. Floor it if it should be an integer.
onChange(isInteger ? Math.floor(Number(v)) : Number(v));
}
};
/**
* Clicking the steppers allows the value to go outside bounds; we need to
* clamp it on blur and floor it if needed.
*/
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
const clamped = _.clamp(
isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value),
min,
max
);
setValueAsString(String(clamped));
onChange(clamped);
};
return (
<FormControl
isDisabled={isDisabled}
isInvalid={isInvalid}
className={`number-input ${styleClass}`}
>
{label && (
<FormLabel
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
className="number-input-label"
>
{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"
width={width}
textAlign={textAlign}
/>
<div
className="number-input-stepper"
style={showStepper ? { display: 'block' } : { display: 'none' }}
>
<NumberIncrementStepper className="number-input-stepper-button" />
<NumberDecrementStepper className="number-input-stepper-button" />
</div>
</NumberInput>
</FormControl>
);
};
export default IAINumberInput;

View File

@ -0,0 +1,28 @@
.iai-select {
display: grid;
grid-template-columns: repeat(2, max-content);
column-gap: 1rem;
align-items: center;
width: max-content;
.iai-select-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.iai-select-picker {
border: 2px solid var(--border-color);
background-color: var(--background-color-secondary);
font-weight: bold;
&:focus {
outline: none;
border: 2px solid var(--prompt-border-color);
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
}
}
.iai-select-option {
background-color: var(--background-color-secondary);
}
}

View File

@ -0,0 +1,56 @@
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
interface Props extends SelectProps {
label: string;
styleClass?: string;
validValues:
| Array<number | string>
| Array<{ key: string; value: string | number }>;
}
/**
* Customized Chakra FormControl + Select multi-part component.
*/
const IAISelect = (props: Props) => {
const {
label,
isDisabled,
validValues,
size = 'sm',
fontSize = 'md',
styleClass,
...rest
} = props;
return (
<FormControl isDisabled={isDisabled} className={`iai-select ${styleClass}`}>
<FormLabel
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
className="iai-select-label"
>
{label}
</FormLabel>
<Select
fontSize={fontSize}
size={size}
{...rest}
className="iai-select-picker"
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="iai-select-option">
{opt}
</option>
) : (
<option key={opt.value} value={opt.value}>
{opt.key}
</option>
);
})}
</Select>
</FormControl>
);
};
export default IAISelect;

View File

@ -0,0 +1,18 @@
.chakra-switch,
.switch-button {
span {
background-color: var(--switch-bg-color);
span {
background-color: var(--white);
}
}
span[data-checked] {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
}

View File

@ -14,13 +14,13 @@ interface Props extends SwitchProps {
/**
* Customized Chakra FormControl + Switch multi-part component.
*/
const SDSwitch = (props: Props) => {
const IAISwitch = (props: Props) => {
const {
label,
isDisabled = false,
fontSize = 'md',
size = 'md',
width,
width = 'auto',
...rest
} = props;
return (
@ -36,10 +36,10 @@ const SDSwitch = (props: Props) => {
{label}
</FormLabel>
)}
<Switch size={size} {...rest} />
<Switch size={size} className="switch-button" {...rest} />
</Flex>
</FormControl>
);
};
export default SDSwitch;
export default IAISwitch;

View File

@ -1,21 +0,0 @@
import { Button, ButtonProps } from '@chakra-ui/react';
interface Props extends ButtonProps {
label: string;
}
/**
* Reusable customized button component. Originally was more customized - now probably unecessary.
*
* TODO: Get rid of this.
*/
const SDButton = (props: Props) => {
const { label, size = 'sm', ...rest } = props;
return (
<Button size={size} {...rest}>
{label}
</Button>
);
};
export default SDButton;

View File

@ -1,59 +0,0 @@
import {
FormControl,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Text,
FormLabel,
NumberInputProps,
Flex,
} from '@chakra-ui/react';
interface Props extends NumberInputProps {
label?: string;
width?: string | number;
}
/**
* Customized Chakra FormControl + NumberInput multi-part component.
*/
const SDNumberInput = (props: Props) => {
const {
label,
isDisabled = false,
fontSize = 'md',
size = 'sm',
width,
isInvalid,
...rest
} = props;
return (
<FormControl isDisabled={isDisabled} width={width} isInvalid={isInvalid}>
<Flex gap={2} justifyContent={'space-between'} alignItems={'center'}>
{label && (
<FormLabel marginBottom={1}>
<Text fontSize={fontSize} whiteSpace="nowrap">
{label}
</Text>
</FormLabel>
)}
<NumberInput
size={size}
{...rest}
keepWithinRange={false}
clampValueOnBlur={true}
>
<NumberInputField fontSize={'md'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Flex>
</FormControl>
);
};
export default SDNumberInput;

View File

@ -1,56 +0,0 @@
import {
Flex,
FormControl,
FormLabel,
Select,
SelectProps,
Text,
} from '@chakra-ui/react';
interface Props extends SelectProps {
label: string;
validValues:
| Array<number | string>
| Array<{ key: string; value: string | number }>;
}
/**
* Customized Chakra FormControl + Select multi-part component.
*/
const SDSelect = (props: Props) => {
const {
label,
isDisabled,
validValues,
size = 'sm',
fontSize = 'md',
marginBottom = 1,
whiteSpace = 'nowrap',
...rest
} = props;
return (
<FormControl isDisabled={isDisabled}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<FormLabel marginBottom={marginBottom}>
<Text fontSize={fontSize} whiteSpace={whiteSpace}>
{label}
</Text>
</FormLabel>
<Select fontSize={fontSize} size={size} {...rest}>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt}>
{opt}
</option>
) : (
<option key={opt.value} value={opt.value}>
{opt.key}
</option>
);
})}
</Select>
</Flex>
</FormControl>
);
};
export default SDSelect;

View File

@ -0,0 +1,16 @@
import React from 'react';
import Img2ImgPlaceHolder from '../../../assets/images/image2img.png';
export const ImageToImageWIP = () => {
return (
<div className="work-in-progress txt2img-work-in-progress">
<img src={Img2ImgPlaceHolder} alt="img2img_placeholder" />
<h1>Image To Image</h1>
<p>
Image to Image is already available in the WebUI. You can access it from
the Text to Image - Advanced Options menu. A dedicated UI for Image To
Image will be released soon.
</p>
</div>
);
};

View File

@ -0,0 +1,14 @@
import React from 'react';
export default function InpaintingWIP() {
return (
<div className="work-in-progress inpainting-work-in-progress">
<h1>Inpainting</h1>
<p>
Inpainting is available as a part of the Invoke AI Command Line
Interface. A dedicated WebUI interface will be released in the near
future.
</p>
</div>
);
}

View File

@ -0,0 +1,13 @@
import React from 'react';
export default function NodesWIP() {
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>Nodes</h1>
<p>
A node based system for the generation of images is under development
currently. Stay tuned for updates about this amazing feature.
</p>
</div>
);
}

View File

@ -0,0 +1,14 @@
import React from 'react';
export default function OutpaintingWIP() {
return (
<div className="work-in-progress outpainting-work-in-progress">
<h1>Outpainting</h1>
<p>
Outpainting is available as a part of the Invoke AI Command Line
Interface. A dedicated WebUI interface will be released in the near
future.
</p>
</div>
);
}

View File

@ -0,0 +1,19 @@
import React from 'react';
export const PostProcessingWIP = () => {
return (
<div className="work-in-progress post-processing-work-in-progress">
<h1>Post Processing</h1>
<p>
Invoke AI offers a wide variety of post processing features. Image
Upscaling and Face Restoration are already available in the WebUI. You
can access them from the Advanced Options menu of the Text To Image tab.
A dedicated UI will be released soon.
</p>
<p>
The Invoke AI Command Line Interface offers various other features
including Embiggen, High Resolution Fixing and more.
</p>
</div>
);
};

View File

@ -0,0 +1,24 @@
@use '../../../styles/Mixins/' as *;
.work-in-progress {
display: grid;
width: 100%;
height: $app-content-height;
grid-auto-rows: max-content;
background-color: var(--background-color-secondary);
border-radius: 0.4rem;
place-content: center;
place-items: center;
row-gap: 1rem;
h1 {
font-size: 2rem;
font-weight: bold;
}
p {
text-align: center;
max-width: 50rem;
color: var(--subtext-color-bright);
}
}

View File

@ -4,10 +4,11 @@ import { useMemo } from 'react';
import { useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { OptionsState } from '../../features/options/optionsSlice';
import { SystemState } from '../../features/system/systemSlice';
import { validateSeedWeights } from '../util/seedWeightPairs';
const optionsSelector = createSelector(
export const optionsSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => {
return {
@ -26,7 +27,7 @@ const optionsSelector = createSelector(
}
);
const systemSelector = createSelector(
export const systemSelector = createSelector(
(state: RootState) => state.system,
(system: SystemState) => {
return {
@ -46,8 +47,9 @@ const systemSelector = createSelector(
* This is used to prevent the 'Generate' button from being clicked.
*/
const useCheckParameters = (): boolean => {
const { prompt } = useAppSelector(optionsSelector);
const {
prompt,
shouldGenerateVariations,
seedWeights,
maskPath,
@ -59,7 +61,7 @@ const useCheckParameters = (): boolean => {
return useMemo(() => {
// Cannot generate without a prompt
if (!prompt) {
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
return false;
}

View File

@ -0,0 +1,17 @@
import { createIcon } from "@chakra-ui/react";
const ImageToImageIcon = createIcon({
displayName: 'ImageToImageIcon',
viewBox: '0 0 3543 3543',
path: (
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"
/>
</g>
),
});
export default ImageToImageIcon;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
import { createIcon } from "@chakra-ui/react";
const NodesIcon = createIcon({
displayName: 'NodesIcon',
viewBox: '0 0 3543 3543',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"
/>
),
});
export default NodesIcon;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
import { createIcon } from '@chakra-ui/react';
const PostprocessingIcon = createIcon({
displayName: 'PostprocessingIcon',
viewBox: '0 0 3543 3543',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z"
/>
),
});
export default PostprocessingIcon;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
<path d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z" style="stroke:white;stroke-opacity:0;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -6,6 +6,7 @@
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,

View File

@ -6,7 +6,7 @@ export const stringToSeedWeights = (
const stringPairs = string.split(',');
const arrPairs = stringPairs.map((p) => p.split(':'));
const pairs = arrPairs.map((p: Array<string>): InvokeAI.SeedWeightPair => {
return { seed: parseInt(p[0]), weight: parseFloat(p[1]) };
return { seed: Number(p[0]), weight: Number(p[1]) };
});
if (!validateSeedWeights(pairs)) {

View File

@ -1,4 +1,3 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
@ -13,8 +12,13 @@ import {
} from '../options/optionsSlice';
import DeleteImageModal from './DeleteImageModal';
import { SystemState } from '../system/systemSlice';
import SDButton from '../../common/components/SDButton';
import IAIButton from '../../common/components/IAIButton';
import { runESRGAN, runGFPGAN } from '../../app/socketio/actions';
import IAIIconButton from '../../common/components/IAIIconButton';
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
import InvokePopover from './InvokePopover';
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
const systemSelector = createSelector(
(state: RootState) => state.system,
@ -50,12 +54,16 @@ const CurrentImageButtons = ({
}: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch();
const { intermediateImage } = useAppSelector(
(state: RootState) => state.gallery
const intermediateImage = useAppSelector(
(state: RootState) => state.gallery.intermediateImage
);
const { upscalingLevel, gfpganStrength } = useAppSelector(
(state: RootState) => state.options
const upscalingLevel = useAppSelector(
(state: RootState) => state.options.upscalingLevel
);
const gfpganStrength = useAppSelector(
(state: RootState) => state.options.gfpganStrength
);
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
@ -78,51 +86,34 @@ const CurrentImageButtons = ({
setShouldShowImageDetails(!shouldShowImageDetails);
return (
<Flex gap={2}>
<SDButton
label="Use as initial image"
colorScheme={'gray'}
flexGrow={1}
variant={'outline'}
<div className="current-image-options">
<IAIIconButton
icon={<MdImage />}
tooltip="Use As Initial Image"
aria-label="Use As Initial Image"
onClick={handleClickUseAsInitialImage}
/>
<SDButton
label="Use all"
colorScheme={'gray'}
flexGrow={1}
variant={'outline'}
isDisabled={!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)}
<IAIButton
label="Use All"
isDisabled={
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
}
onClick={handleClickUseAllParameters}
/>
<SDButton
label="Use seed"
colorScheme={'gray'}
flexGrow={1}
variant={'outline'}
<IAIButton
label="Use Seed"
isDisabled={!image?.metadata?.image?.seed}
onClick={handleClickUseSeed}
/>
<SDButton
label="Upscale"
colorScheme={'gray'}
flexGrow={1}
variant={'outline'}
isDisabled={
!isESRGANAvailable ||
Boolean(intermediateImage) ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
/>
<SDButton
label="Fix faces"
colorScheme={'gray'}
flexGrow={1}
variant={'outline'}
<InvokePopover
title="Restore Faces"
popoverOptions={<FaceRestoreOptions />}
actionButton={
<IAIButton
label={'Restore Faces'}
isDisabled={
!isGFPGANAvailable ||
Boolean(intermediateImage) ||
@ -131,24 +122,47 @@ const CurrentImageButtons = ({
}
onClick={handleClickFixFaces}
/>
<SDButton
label="Details"
colorScheme={'gray'}
variant={shouldShowImageDetails ? 'solid' : 'outline'}
borderWidth={1}
flexGrow={1}
}
>
<IAIIconButton icon={<MdFace />} aria-label="Restore Faces" />
</InvokePopover>
<InvokePopover
title="Upscale"
styleClass="upscale-popover"
popoverOptions={<UpscaleOptions />}
actionButton={
<IAIButton
label={'Upscale Image'}
isDisabled={
!isESRGANAvailable ||
Boolean(intermediateImage) ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
/>
}
>
<IAIIconButton icon={<MdHd />} aria-label="Upscale" />
</InvokePopover>
<IAIIconButton
icon={<MdInfo />}
tooltip="Details"
aria-label="Details"
onClick={handleClickShowImageDetails}
/>
<DeleteImageModal image={image}>
<SDButton
label="Delete"
colorScheme={'red'}
flexGrow={1}
variant={'outline'}
<IAIIconButton
icon={<MdDelete />}
tooltip="Delete Image"
aria-label="Delete Image"
isDisabled={Boolean(intermediateImage)}
/>
</DeleteImageModal>
</Flex>
</div>
);
};

View File

@ -0,0 +1,91 @@
@use '../../styles/Mixins/' as *;
.current-image-display {
display: grid;
grid-template-areas:
'current-image-tools'
'current-image-preview';
grid-template-rows: auto 1fr;
justify-items: center;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
}
.current-image-display-placeholder {
background-color: var(--background-color-secondary);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
svg {
width: 10rem;
height: 10rem;
color: var(--svg-color);
}
}
.current-image-tools {
grid-area: current-image-tools;
width: 100%;
height: 100%;
display: grid;
justify-content: center;
}
.current-image-options {
display: grid;
grid-auto-flow: column;
padding: 1rem;
height: fit-content;
gap: 0.5rem;
button {
@include Button(
$btn-width: 3rem,
$icon-size: 22px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}
}
.current-image-preview {
grid-area: current-image-preview;
position: relative;
justify-content: center;
align-items: center;
display: grid;
width: 100%;
img {
border-radius: 0.5rem;
object-fit: contain;
width: auto;
max-height: $app-gallery-height;
}
}
.current-image-metadata-viewer {
border-radius: 0.5rem;
position: absolute;
top: 0;
left: 0;
width: calc(100% - 2rem);
padding: 0.5rem;
margin-left: 1rem;
background-color: var(--metadata-bg-color);
z-index: 1;
overflow: scroll;
height: calc($app-metadata-height - 1rem);
}
.current-image-json-viewer {
border-radius: 0.5rem;
margin: 0 0.5rem 1rem 0.5rem;
padding: 1rem;
overflow-x: scroll;
word-break: break-all;
background-color: var(--metadata-json-bg-color);
}

View File

@ -1,12 +1,10 @@
import { Center, Flex, Image, Text, useColorModeValue } from '@chakra-ui/react';
import { Image } from '@chakra-ui/react';
import { useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { useState } from 'react';
import ImageMetadataViewer from './ImageMetadataViewer';
import CurrentImageButtons from './CurrentImageButtons';
// TODO: With CSS Grid I had a hard time centering the image in a grid item. This is needed for that.
const height = 'calc(100vh - 238px)';
import { MdPhoto } from 'react-icons/md';
/**
* Displays the current image if there is one, plus associated actions.
@ -16,24 +14,21 @@ const CurrentImageDisplay = () => {
(state: RootState) => state.gallery
);
const bgColor = useColorModeValue(
'rgba(255, 255, 255, 0.85)',
'rgba(0, 0, 0, 0.8)'
);
const [shouldShowImageDetails, setShouldShowImageDetails] =
useState<boolean>(false);
const imageToDisplay = intermediateImage || currentImage;
return imageToDisplay ? (
<Flex direction={'column'} borderWidth={1} rounded={'md'} p={2} gap={2}>
<div className="current-image-display">
<div className="current-image-tools">
<CurrentImageButtons
image={imageToDisplay}
shouldShowImageDetails={shouldShowImageDetails}
setShouldShowImageDetails={setShouldShowImageDetails}
/>
<Center height={height} position={'relative'}>
</div>
<div className="current-image-preview">
<Image
src={imageToDisplay.url}
fit="contain"
@ -41,26 +36,16 @@ const CurrentImageDisplay = () => {
maxHeight={'100%'}
/>
{shouldShowImageDetails && (
<Flex
width={'100%'}
height={'100%'}
position={'absolute'}
top={0}
left={0}
p={3}
boxSizing="border-box"
backgroundColor={bgColor}
overflow="scroll"
>
<div className="current-image-metadata-viewer">
<ImageMetadataViewer image={imageToDisplay} />
</Flex>
</div>
)}
</Center>
</Flex>
</div>
</div>
) : (
<Center height={'100%'} position={'relative'}>
<Text size={'xl'}>No image selected</Text>
</Center>
<div className="current-image-display-placeholder">
<MdPhoto />
</div>
);
};

View File

@ -119,7 +119,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
/>
</Tooltip>
)}
{image?.metadata?.image?.seed && (
{image?.metadata?.image?.seed !== undefined && (
<Tooltip label="Use seed">
<IconButton
aria-label="Use seed"

View File

@ -0,0 +1,51 @@
@use '../../styles/Mixins/' as *;
.image-gallery-container {
display: grid;
row-gap: 1rem;
grid-auto-rows: max-content;
min-width: 16rem;
}
.image-gallery-container-placeholder {
display: grid;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
place-items: center;
padding: 2rem 0;
p {
color: var(--subtext-color-bright);
}
svg {
width: 5rem;
height: 5rem;
color: var(--svg-color);
}
}
.image-gallery {
display: grid;
grid-template-columns: repeat(2, max-content);
gap: 0.6rem;
justify-items: center;
max-height: $app-gallery-height;
overflow-y: scroll;
@include HideScrollbar;
}
.image-gallery-load-more-btn {
background-color: var(--btn-load-more) !important;
font-size: 0.85rem !important;
&:disabled {
&:hover {
background-color: var(--btn-load-more) !important;
}
}
&:hover {
background-color: var(--btn-load-more-hover) !important;
}
}

View File

@ -1,4 +1,5 @@
import { Button, Center, Flex, Text } from '@chakra-ui/react';
import { Button } from '@chakra-ui/react';
import { MdPhotoLibrary } from 'react-icons/md';
import { requestImages } from '../../app/socketio/actions';
import { RootState, useAppDispatch } from '../../app/store';
import { useAppSelector } from '../../app/store';
@ -8,7 +9,7 @@ import HoverableImage from './HoverableImage';
* Simple image gallery.
*/
const ImageGallery = () => {
const { images, currentImageUuid } = useAppSelector(
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
(state: RootState) => state.gallery
);
const dispatch = useAppDispatch();
@ -24,23 +25,41 @@ const ImageGallery = () => {
dispatch(requestImages());
};
return images.length ? (
<Flex direction={'column'} gap={2} pb={2}>
<Flex gap={2} wrap="wrap">
return (
<div className="image-gallery-container">
{images.length ? (
<>
<p>
<strong>Your Invocations</strong>
</p>
<div className="image-gallery">
{images.map((image) => {
const { uuid } = image;
const isSelected = currentImageUuid === uuid;
return (
<HoverableImage key={uuid} image={image} isSelected={isSelected} />
<HoverableImage
key={uuid}
image={image}
isSelected={isSelected}
/>
);
})}
</Flex>
<Button onClick={handleClickLoadMore}>Load more...</Button>
</Flex>
</div>
</>
) : (
<Center height={'100%'} position={'relative'}>
<Text size={'xl'}>No images in gallery</Text>
</Center>
<div className="image-gallery-container-placeholder">
<MdPhotoLibrary />
<p>No Images In Gallery</p>
</div>
)}
<Button
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
>
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
</Button>
</div>
);
};

View File

@ -1,12 +1,11 @@
import {
Box,
Center,
Flex,
Heading,
IconButton,
Link,
Text,
Tooltip,
useColorModeValue,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { memo } from 'react';
@ -39,12 +38,19 @@ type MetadataItemProps = {
label: string;
onClick?: () => void;
value: number | string | boolean;
labelPosition?: string;
};
/**
* Component to display an individual metadata item or parameter.
*/
const MetadataItem = ({ label, value, onClick, isLink }: MetadataItemProps) => {
const MetadataItem = ({
label,
value,
onClick,
isLink,
labelPosition,
}: MetadataItemProps) => {
return (
<Flex gap={2}>
{onClick && (
@ -59,7 +65,8 @@ const MetadataItem = ({ label, value, onClick, isLink }: MetadataItemProps) => {
/>
</Tooltip>
)}
<Text fontWeight={'semibold'} whiteSpace={'nowrap'}>
<Flex direction={labelPosition ? 'column' : 'row'}>
<Text fontWeight={'semibold'} whiteSpace={'nowrap'} pr={2}>
{label}:
</Text>
{isLink ? (
@ -67,11 +74,12 @@ const MetadataItem = ({ label, value, onClick, isLink }: MetadataItemProps) => {
{value.toString()} <ExternalLinkIcon mx="2px" />
</Link>
) : (
<Text maxHeight={100} overflowY={'scroll'} wordBreak={'break-all'}>
<Text overflowY={'scroll'} wordBreak={'break-all'}>
{value.toString()}
</Text>
)}
</Flex>
</Flex>
);
};
@ -93,7 +101,7 @@ const memoEqualityCheck = (
*/
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch();
const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const metadata = image?.metadata?.image || {};
const {
@ -119,7 +127,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const metadataJSON = JSON.stringify(metadata, null, 2);
return (
<Flex gap={1} direction={'column'} overflowY={'scroll'} width={'100%'}>
<Flex gap={1} direction={'column'} width={'100%'}>
<Flex gap={2}>
<Text fontWeight={'semibold'}>File:</Text>
<Link href={image.url} isExternal>
@ -129,25 +137,25 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
</Flex>
{Object.keys(metadata).length > 0 ? (
<>
{type && <MetadataItem label="Type" value={type} />}
{type && <MetadataItem label="Generation type" value={type} />}
{['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} isLink />
<MetadataItem label="Original image" value={orig_path} />
)}
{type === 'gfpgan' && strength && (
{type === 'gfpgan' && strength !== undefined && (
<MetadataItem
label="Fix faces strength"
value={strength}
onClick={() => dispatch(setGfpganStrength(strength))}
/>
)}
{type === 'esrgan' && scale && (
{type === 'esrgan' && scale !== undefined && (
<MetadataItem
label="Upscaling scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
)}
{type === 'esrgan' && strength && (
{type === 'esrgan' && strength !== undefined && (
<MetadataItem
label="Upscaling strength"
value={strength}
@ -157,11 +165,12 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
{prompt && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={promptToString(prompt)}
onClick={() => dispatch(setPrompt(prompt))}
/>
)}
{seed && (
{seed !== undefined && (
<MetadataItem
label="Seed"
value={seed}
@ -182,7 +191,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
onClick={() => dispatch(setSteps(steps))}
/>
)}
{cfg_scale && (
{cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={cfg_scale}
@ -249,38 +258,53 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
/>
)}
{postprocessing &&
postprocessing.length > 0 &&
postprocessing.map(
(postprocess: InvokeAI.PostProcessedImageMetadata) => {
{postprocessing && postprocessing.length > 0 && (
<>
<Heading size={'sm'}>Postprocessing</Heading>
{postprocessing.map(
(
postprocess: InvokeAI.PostProcessedImageMetadata,
i: number
) => {
if (postprocess.type === 'esrgan') {
const { scale, strength } = postprocess;
return (
<>
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
<Text size={'md'}>{`${i + 1}: Upscale (ESRGAN)`}</Text>
<MetadataItem
label="Upscaling scale"
label="Scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
<MetadataItem
label="Upscaling strength"
label="Strength"
value={strength}
onClick={() => dispatch(setUpscalingStrength(strength))}
onClick={() =>
dispatch(setUpscalingStrength(strength))
}
/>
</>
</Flex>
);
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
<Text size={'md'}>{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
<MetadataItem
label="Fix faces strength"
label="Strength"
value={strength}
onClick={() => dispatch(setGfpganStrength(strength))}
/>
</Flex>
);
}
}
)}
</>
)}
<Flex gap={2} direction={'column'}>
<Flex gap={2}>
<Tooltip label={`Copy metadata JSON`}>
@ -295,16 +319,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
</Tooltip>
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
</Flex>
<Box
// maxHeight={200}
overflow={'scroll'}
flexGrow={3}
wordBreak={'break-all'}
bgColor={jsonBgColor}
padding={2}
>
<div className={'current-image-json-viewer'}>
<pre>{metadataJSON}</pre>
</Box>
</div>
</Flex>
</>
) : (

View File

@ -0,0 +1,35 @@
.popover-content {
background-color: var(--background-color-secondary) !important;
border: none !important;
border-top: 0px;
background-color: var(--tab-hover-color);
border-radius: 0 0 0.4rem 0.4rem;
}
.popover-arrow {
background: var(--tab-hover-color) !important;
box-shadow: none;
}
.popover-options {
background: var(--tab-panel-bg);
border-radius: 0 0 0.4rem 0.4rem;
border: 2px solid var(--tab-hover-color);
padding: .75rem 1rem .75rem 1rem;
display: grid;
grid-template-rows: repeat(auto-fill, 1fr);
grid-row-gap: 0.5rem;
justify-content: space-between;
}
.popover-header {
background: var(--tab-hover-color);
border-radius: 0.4rem 0.4rem 0 0;
font-weight: bold;
border: none;
padding-left: 1rem !important;
}
.upscale-popover {
width: 23rem !important;
}

View File

@ -0,0 +1,45 @@
import {
Box,
Popover,
PopoverArrow,
PopoverContent,
PopoverHeader,
PopoverTrigger,
} from '@chakra-ui/react';
import React, { ReactNode } from 'react';
type PopoverProps = {
title?: string;
delay?: number;
styleClass?: string;
popoverOptions?: ReactNode;
actionButton?: ReactNode;
children: ReactNode;
};
const InvokePopover = ({
title = 'Popup',
styleClass,
delay = 50,
popoverOptions,
actionButton,
children,
}: PopoverProps) => {
return (
<Popover trigger={'hover'} closeDelay={delay}>
<PopoverTrigger>
<Box>{children}</Box>
</PopoverTrigger>
<PopoverContent className={`popover-content ${styleClass}`}>
<PopoverArrow className="popover-arrow" />
<PopoverHeader className="popover-header">{title}</PopoverHeader>
<div className="popover-options">
{popoverOptions ? popoverOptions : null}
{actionButton}
</div>
</PopoverContent>
</Popover>
);
};
export default InvokePopover;

View File

@ -8,15 +8,15 @@ export interface GalleryState {
currentImageUuid: string;
images: Array<InvokeAI.Image>;
intermediateImage?: InvokeAI.Image;
nextPage: number;
offset: number;
areMoreImagesAvailable: boolean;
latest_mtime?: number;
earliest_mtime?: number;
}
const initialState: GalleryState = {
currentImageUuid: '',
images: [],
nextPage: 1,
offset: 0,
areMoreImagesAvailable: true,
};
export const gallerySlice = createSlice({
@ -71,11 +71,13 @@ export const gallerySlice = createSlice({
state.images = newImages;
},
addImage: (state, action: PayloadAction<InvokeAI.Image>) => {
state.images.unshift(action.payload);
state.currentImageUuid = action.payload.uuid;
const newImage = action.payload;
const { uuid, mtime } = newImage;
state.images.unshift(newImage);
state.currentImageUuid = uuid;
state.intermediateImage = undefined;
state.currentImage = action.payload;
state.offset += 1
state.currentImage = newImage;
state.latest_mtime = mtime;
},
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
state.intermediateImage = action.payload;
@ -87,20 +89,27 @@ export const gallerySlice = createSlice({
state,
action: PayloadAction<{
images: Array<InvokeAI.Image>;
nextPage: number;
offset: number;
areMoreImagesAvailable: boolean;
}>
) => {
const { images, nextPage, offset } = action.payload;
if (images.length) {
const newCurrentImage = images[0];
const { images, areMoreImagesAvailable } = action.payload;
if (images.length > 0) {
state.images = state.images
.concat(images)
.sort((a, b) => b.mtime - a.mtime);
if (!state.currentImage) {
const newCurrentImage = images[0];
state.currentImage = newCurrentImage;
state.currentImageUuid = newCurrentImage.uuid;
state.nextPage = nextPage;
state.offset = offset;
}
// keep track of the timestamps of latest and earliest images received
state.latest_mtime = images[0].mtime;
state.earliest_mtime = images[images.length - 1].mtime;
}
if (areMoreImagesAvailable !== undefined) {
state.areMoreImagesAvailable = areMoreImagesAvailable;
}
},
},

View File

@ -0,0 +1,38 @@
@use '../../../styles/Mixins/' as *;
.advanced-settings {
display: grid;
row-gap: 0.5rem;
}
.advanced-settings-item {
display: grid;
max-width: $options-bar-max-width;
border: none;
border-top: 0px;
border-radius: 0.4rem;
&[aria-expanded='true'] {
background-color: var(--tab-hover-color);
border-radius: 0 0 0.4rem 0.4rem;
}
}
.advanced-settings-panel {
background-color: var(--tab-panel-bg);
border-radius: 0 0 0.4rem 0.4rem;
border: 2px solid var(--tab-hover-color);
}
.advanced-settings-header {
border-radius: 0.4rem;
&[aria-expanded='true'] {
background-color: var(--tab-color);
border-radius: 0.4rem 0.4rem 0 0;
}
&:hover {
background-color: var(--tab-hover-color) !important;
}
}

View File

@ -0,0 +1,34 @@
import {
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
} from '@chakra-ui/react';
import React, { ReactElement } from 'react';
import { Feature } from '../../../app/features';
import GuideIcon from '../../../common/components/GuideIcon';
interface InvokeAccordionItemProps {
header: ReactElement;
feature: Feature;
options: ReactElement;
}
export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
const { header, feature, options } = props;
return (
<AccordionItem className="advanced-settings-item">
<h2>
<AccordionButton className="advanced-settings-header">
{header}
<GuideIcon feature={feature} />
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel className="advanced-settings-panel">
{options}
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -0,0 +1,40 @@
import { Flex } from '@chakra-ui/layout';
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldRunGFPGAN } from '../../optionsSlice';
export default function FaceRestore() {
const isGFPGANAvailable = useAppSelector(
(state: RootState) => state.system.isGFPGANAvailable
);
const shouldRunGFPGAN = useAppSelector(
(state: RootState) => state.options.shouldRunGFPGAN
);
const dispatch = useAppDispatch();
const handleChangeShouldRunGFPGAN = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRunGFPGAN(e.target.checked));
return (
<Flex
justifyContent={'space-between'}
alignItems={'center'}
width={'100%'}
mr={2}
>
<p>Restore Face</p>
<IAISwitch
isDisabled={!isGFPGANAvailable}
isChecked={shouldRunGFPGAN}
onChange={handleChangeShouldRunGFPGAN}
/>
</Flex>
);
}

View File

@ -1,15 +1,14 @@
import { Flex } from '@chakra-ui/react';
import { RootState } from '../../app/store';
import { useAppDispatch, useAppSelector } from '../../app/store';
import { OptionsState, setGfpganStrength } from '../options/optionsSlice';
import { RootState } from '../../../../app/store';
import { useAppDispatch, useAppSelector } from '../../../../app/store';
import { OptionsState, setGfpganStrength } from '../../optionsSlice';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { SystemState } from '../system/systemSlice';
import SDNumberInput from '../../common/components/SDNumberInput';
import { SystemState } from '../../../system/systemSlice';
import IAINumberInput from '../../../../common/components/IAINumberInput';
const optionsSelector = createSelector(
(state: RootState) => state.options,
@ -42,17 +41,16 @@ const systemSelector = createSelector(
/**
* Displays face-fixing/GFPGAN options (strength).
*/
const GFPGANOptions = () => {
const FaceRestoreOptions = () => {
const dispatch = useAppDispatch();
const { gfpganStrength } = useAppSelector(optionsSelector);
const { isGFPGANAvailable } = useAppSelector(systemSelector);
const handleChangeStrength = (v: string | number) =>
dispatch(setGfpganStrength(Number(v)));
const handleChangeStrength = (v: number) => dispatch(setGfpganStrength(v));
return (
<Flex direction={'column'} gap={2}>
<SDNumberInput
<IAINumberInput
isDisabled={!isGFPGANAvailable}
label="Strength"
step={0.05}
@ -60,9 +58,11 @@ const GFPGANOptions = () => {
max={1}
onChange={handleChangeStrength}
value={gfpganStrength}
width="90px"
isInteger={false}
/>
</Flex>
);
};
export default GFPGANOptions;
export default FaceRestoreOptions;

View File

@ -0,0 +1,27 @@
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldFitToWidthHeight } from '../../optionsSlice';
export default function ImageFit() {
const dispatch = useAppDispatch();
const shouldFitToWidthHeight = useAppSelector(
(state: RootState) => state.options.shouldFitToWidthHeight
);
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldFitToWidthHeight(e.target.checked));
return (
<IAISwitch
label="Fit initial image to output size"
isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit}
/>
);
}

View File

@ -0,0 +1,39 @@
import { Flex } from '@chakra-ui/layout';
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldUseInitImage } from '../../optionsSlice';
export default function ImageToImage() {
const dispatch = useAppDispatch();
const initialImagePath = useAppSelector(
(state: RootState) => state.options.initialImagePath
);
const shouldUseInitImage = useAppSelector(
(state: RootState) => state.options.shouldUseInitImage
);
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseInitImage(e.target.checked));
return (
<Flex
justifyContent={'space-between'}
alignItems={'center'}
width={'100%'}
mr={2}
>
<p>Image to Image</p>
<IAISwitch
isDisabled={!initialImagePath}
isChecked={shouldUseInitImage}
onChange={handleChangeShouldUseInitImage}
/>
</Flex>
);
}

View File

@ -0,0 +1,19 @@
import { Flex } from '@chakra-ui/react';
import InitAndMaskImage from '../../InitAndMaskImage';
import ImageFit from './ImageFit';
import ImageToImageStrength from './ImageToImageStrength';
/**
* Options for img2img generation (strength, fit, init/mask upload).
*/
const ImageToImageOptions = () => {
return (
<Flex direction={'column'} gap={2}>
<ImageToImageStrength />
<ImageFit />
<InitAndMaskImage />
</Flex>
);
};
export default ImageToImageOptions;

View File

@ -0,0 +1,31 @@
import React from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAINumberInput from '../../../../common/components/IAINumberInput';
import { setImg2imgStrength } from '../../optionsSlice';
export default function ImageToImageStrength() {
const img2imgStrength = useAppSelector(
(state: RootState) => state.options.img2imgStrength
);
const dispatch = useAppDispatch();
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v));
return (
<IAINumberInput
label="Strength"
step={0.01}
min={0.01}
max={0.99}
onChange={handleChangeStrength}
value={img2imgStrength}
width="90px"
isInteger={false}
/>
);
}

View File

@ -0,0 +1,28 @@
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldRandomizeSeed } from '../../optionsSlice';
export default function RandomizeSeed() {
const dispatch = useAppDispatch();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.options.shouldRandomizeSeed
);
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRandomizeSeed(e.target.checked));
return (
<IAISwitch
label="Randomize Seed"
isChecked={shouldRandomizeSeed}
onChange={handleChangeShouldRandomizeSeed}
/>
);
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../../../app/constants';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAINumberInput from '../../../../common/components/IAINumberInput';
import { setSeed } from '../../optionsSlice';
export default function Seed() {
const seed = useAppSelector((state: RootState) => state.options.seed);
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.options.shouldRandomizeSeed
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.options.shouldGenerateVariations
);
const dispatch = useAppDispatch();
const handleChangeSeed = (v: number) => dispatch(setSeed(v));
return (
<IAINumberInput
label="Seed"
step={1}
precision={0}
flexGrow={1}
min={NUMPY_RAND_MIN}
max={NUMPY_RAND_MAX}
isDisabled={shouldRandomizeSeed}
isInvalid={seed < 0 && shouldGenerateVariations}
onChange={handleChangeSeed}
value={seed}
width="10rem"
/>
);
}

View File

@ -0,0 +1,21 @@
import { Flex } from '@chakra-ui/react';
import RandomizeSeed from './RandomizeSeed';
import Seed from './Seed';
import ShuffleSeed from './ShuffleSeed';
/**
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
*/
const SeedOptions = () => {
return (
<Flex gap={2} direction={'column'}>
<RandomizeSeed />
<Flex gap={2}>
<Seed />
<ShuffleSeed />
</Flex>
</Flex>
);
};
export default SeedOptions;

View File

@ -0,0 +1,30 @@
import { Button } from '@chakra-ui/react';
import React from 'react';
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../../../app/constants';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import randomInt from '../../../../common/util/randomInt';
import { setSeed } from '../../optionsSlice';
export default function ShuffleSeed() {
const dispatch = useAppDispatch();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.options.shouldRandomizeSeed
);
const handleClickRandomizeSeed = () =>
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
return (
<Button
size={'sm'}
isDisabled={shouldRandomizeSeed}
onClick={handleClickRandomizeSeed}
>
<p>Shuffle</p>
</Button>
);
}

View File

@ -0,0 +1,38 @@
import { Flex } from '@chakra-ui/layout';
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldRunESRGAN } from '../../optionsSlice';
export default function Upscale() {
const isESRGANAvailable = useAppSelector(
(state: RootState) => state.system.isESRGANAvailable
);
const shouldRunESRGAN = useAppSelector(
(state: RootState) => state.options.shouldRunESRGAN
);
const dispatch = useAppDispatch();
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRunESRGAN(e.target.checked));
return (
<Flex
justifyContent={'space-between'}
alignItems={'center'}
width={'100%'}
mr={2}
>
<p>Upscale</p>
<IAISwitch
isDisabled={!isESRGANAvailable}
isChecked={shouldRunESRGAN}
onChange={handleChangeShouldRunESRGAN}
/>
</Flex>
);
}

View File

@ -0,0 +1,5 @@
.upscale-options {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 1rem;
}

View File

@ -1,23 +1,20 @@
import { Flex } from '@chakra-ui/react';
import { RootState } from '../../app/store';
import { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../../../app/store';
import { useAppDispatch, useAppSelector } from '../../../../app/store';
import {
setUpscalingLevel,
setUpscalingStrength,
UpscalingLevel,
OptionsState,
} from '../options/optionsSlice';
} from '../../optionsSlice';
import { UPSCALING_LEVELS } from '../../app/constants';
import { UPSCALING_LEVELS } from '../../../../app/constants';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { SystemState } from '../system/systemSlice';
import { SystemState } from '../../../system/systemSlice';
import { ChangeEvent } from 'react';
import SDNumberInput from '../../common/components/SDNumberInput';
import SDSelect from '../../common/components/SDSelect';
import IAINumberInput from '../../../../common/components/IAINumberInput';
import IAISelect from '../../../../common/components/IAISelect';
const optionsSelector = createSelector(
(state: RootState) => state.options,
@ -51,27 +48,27 @@ const systemSelector = createSelector(
/**
* Displays upscaling/ESRGAN options (level and strength).
*/
const ESRGANOptions = () => {
const UpscaleOptions = () => {
const dispatch = useAppDispatch();
const { upscalingLevel, upscalingStrength } = useAppSelector(optionsSelector);
const { isESRGANAvailable } = useAppSelector(systemSelector);
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
const handleChangeStrength = (v: string | number) =>
dispatch(setUpscalingStrength(Number(v)));
const handleChangeStrength = (v: number) => dispatch(setUpscalingStrength(v));
return (
<Flex direction={'column'} gap={2}>
<SDSelect
<div className='upscale-options'>
<IAISelect
isDisabled={!isESRGANAvailable}
label="Scale"
value={upscalingLevel}
onChange={handleChangeLevel}
validValues={UPSCALING_LEVELS}
/>
<SDNumberInput
<IAINumberInput
isDisabled={!isESRGANAvailable}
label="Strength"
step={0.05}
@ -79,9 +76,10 @@ const ESRGANOptions = () => {
max={1}
onChange={handleChangeStrength}
value={upscalingStrength}
isInteger={false}
/>
</Flex>
</div>
);
};
export default ESRGANOptions;
export default UpscaleOptions;

View File

@ -0,0 +1,28 @@
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldGenerateVariations } from '../../optionsSlice';
export default function GenerateVariations() {
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.options.shouldGenerateVariations
);
const dispatch = useAppDispatch();
const handleChangeShouldGenerateVariations = (
e: ChangeEvent<HTMLInputElement>
) => dispatch(setShouldGenerateVariations(e.target.checked));
return (
<IAISwitch
isChecked={shouldGenerateVariations}
width={'auto'}
onChange={handleChangeShouldGenerateVariations}
/>
);
}

View File

@ -0,0 +1,37 @@
import React, { ChangeEvent } from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIInput from '../../../../common/components/IAIInput';
import { validateSeedWeights } from '../../../../common/util/seedWeightPairs';
import { setSeedWeights } from '../../optionsSlice';
export default function SeedWeights() {
const seedWeights = useAppSelector(
(state: RootState) => state.options.seedWeights
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.options.shouldGenerateVariations
);
const dispatch = useAppDispatch();
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setSeedWeights(e.target.value));
return (
<IAIInput
label={'Seed Weights'}
value={seedWeights}
isInvalid={
shouldGenerateVariations &&
!(validateSeedWeights(seedWeights) || seedWeights === '')
}
isDisabled={!shouldGenerateVariations}
onChange={handleChangeSeedWeights}
/>
);
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAINumberInput from '../../../../common/components/IAINumberInput';
import { setVariationAmount } from '../../optionsSlice';
export default function VariationAmount() {
const variationAmount = useAppSelector(
(state: RootState) => state.options.variationAmount
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.options.shouldGenerateVariations
);
const dispatch = useAppDispatch();
const handleChangevariationAmount = (v: number) =>
dispatch(setVariationAmount(v));
return (
<IAINumberInput
label="Variation Amount"
value={variationAmount}
step={0.01}
min={0}
max={1}
isDisabled={!shouldGenerateVariations}
onChange={handleChangevariationAmount}
isInteger={false}
/>
);
}

View File

@ -0,0 +1,17 @@
import { Flex } from '@chakra-ui/react';
import React from 'react';
import GenerateVariations from './GenerateVariations';
export default function Variations() {
return (
<Flex
justifyContent={'space-between'}
alignItems={'center'}
width={'100%'}
mr={2}
>
<p>Variations</p>
<GenerateVariations />
</Flex>
);
}

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