all files migrated; tweaks needed

This commit is contained in:
Lincoln Stein
2023-03-03 00:02:15 -05:00
parent 3f0b0f3250
commit 6a990565ff
496 changed files with 276 additions and 934 deletions

View File

@ -0,0 +1,22 @@
import { Flex, Spinner } from '@chakra-ui/react';
const Loading = () => {
return (
<Flex
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
>
<Spinner
thickness="2px"
speed="1s"
emptyColor="gray.200"
color="gray.400"
size="xl"
/>
</Flex>
);
};
export default Loading;

View File

@ -0,0 +1,21 @@
@use '../styles/Mixins/' as *;
svg {
fill: var(--svg-color);
}
.App {
display: grid;
width: 100vw;
height: 100vh;
background-color: var(--background-color);
}
.app-content {
display: grid;
row-gap: 1rem;
padding: $app-padding;
grid-auto-rows: min-content auto;
width: $app-width;
height: $app-height;
}

View File

@ -0,0 +1,36 @@
import ImageUploader from 'common/components/ImageUploader';
import Console from 'features/system/components/Console';
import ProgressBar from 'features/system/components/ProgressBar';
import SiteHeader from 'features/system/components/SiteHeader';
import InvokeTabs from 'features/ui/components/InvokeTabs';
import { keepGUIAlive } from './utils';
import useToastWatcher from 'features/system/hooks/useToastWatcher';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
keepGUIAlive();
const App = () => {
useToastWatcher();
return (
<div className="App">
<ImageUploader>
<ProgressBar />
<div className="app-content">
<SiteHeader />
<InvokeTabs />
</div>
<div className="app-console">
<Console />
</div>
</ImageUploader>
<FloatingParametersPanelButtons />
<FloatingGalleryButton />
</div>
);
};
export default App;

View File

@ -0,0 +1,66 @@
// TODO: use Enums?
import { InProgressImageType } from 'features/system/store/systemSlice';
// Valid samplers
export const SAMPLERS: Array<string> = [
'ddim',
'plms',
'k_lms',
'k_dpm_2',
'k_dpm_2_a',
'k_dpmpp_2',
'k_dpmpp_2_a',
'k_euler',
'k_euler_a',
'k_heun',
];
// Valid Diffusers Samplers
export const DIFFUSERS_SAMPLERS: Array<string> = [
'ddim',
'plms',
'k_lms',
'dpmpp_2',
'k_dpm_2',
'k_dpm_2_a',
'k_dpmpp_2',
'k_euler',
'k_euler_a',
'k_heun',
];
// Valid image widths
export const WIDTHS: Array<number> = [
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
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, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792,
1856, 1920, 1984, 2048,
];
// Valid upscaling levels
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
{ key: '2x', value: 2 },
{ key: '4x', value: 4 },
];
export const NUMPY_RAND_MIN = 0;
export const NUMPY_RAND_MAX = 4294967295;
export const FACETOOL_TYPES = ['gfpgan', 'codeformer'] as const;
export const IN_PROGRESS_IMAGE_TYPES: Array<{
key: string;
value: InProgressImageType;
}> = [
{ key: 'None', value: 'none' },
{ key: 'Fast', value: 'latents' },
{ key: 'Accurate', value: 'full-res' },
];

View File

@ -0,0 +1,8 @@
import { createContext } from 'react';
type VoidFunc = () => void;
type ImageUploaderTriggerContextType = VoidFunc | null;
export const ImageUploaderTriggerContext =
createContext<ImageUploaderTriggerContextType>(null);

View File

@ -0,0 +1,94 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type FeatureHelpInfo = {
text: string;
href: string;
guideImage: string;
};
export enum Feature {
PROMPT,
GALLERY,
OTHER,
SEED,
VARIATIONS,
UPSCALE,
FACE_CORRECTION,
IMAGE_TO_IMAGE,
BOUNDING_BOX,
SEAM_CORRECTION,
INFILL_AND_SCALING,
}
/** For each tooltip in the UI, the below feature definitions & props will pull relevant information into the tooltip.
*
* To-do: href & GuideImages are placeholders, and are not currently utilized, but will be updated (along with the tooltip UI) as feature and UI develop and we get a better idea on where things "forever homes" will be .
*/
const useFeatures = (): Record<Feature, FeatureHelpInfo> => {
const { t } = useTranslation();
return useMemo(
() => ({
[Feature.PROMPT]: {
text: t('tooltip.feature.prompt'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.GALLERY]: {
text: t('tooltip.feature.gallery'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.OTHER]: {
text: t('tooltip.feature.other'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.SEED]: {
text: t('tooltip.feature.seed'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.VARIATIONS]: {
text: t('tooltip.feature.variations'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.UPSCALE]: {
text: t('tooltip.feature.upscale'),
href: 'link/to/docs/feature1.html',
guideImage: 'asset/path.gif',
},
[Feature.FACE_CORRECTION]: {
text: t('tooltip.feature.faceCorrection'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.IMAGE_TO_IMAGE]: {
text: t('tooltip.feature.imageToImage'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.BOUNDING_BOX]: {
text: t('tooltip.feature.boundingBox'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.SEAM_CORRECTION]: {
text: t('tooltip.feature.seamCorrection'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
[Feature.INFILL_AND_SCALING]: {
text: t('tooltip.feature.infillAndScaling'),
href: 'link/to/docs/feature3.html',
guideImage: 'asset/path.gif',
},
}),
[t]
);
};
export const useFeatureHelpInfo = (feature: Feature): FeatureHelpInfo => {
const features = useFeatures();
return features[feature];
};

View File

@ -0,0 +1,322 @@
/**
* Types for images, the things they are made of, and the things
* they make up.
*
* Generated images are txt2img and img2img images. They may have
* had additional postprocessing done on them when they were first
* generated.
*
* Postprocessed images are images which were not generated here
* but only postprocessed by the app. They only get postprocessing
* metadata and have a different image type, e.g. 'esrgan' or
* 'gfpgan'.
*/
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
/**
* TODO:
* Once an image has been generated, if it is postprocessed again,
* additional postprocessing steps are added to its postprocessing
* array.
*
* TODO: Better documentation of types.
*/
export declare type PromptItem = {
prompt: string;
weight: number;
};
// TECHDEBT: We need to retain compatibility with plain prompt strings and the structure Prompt type
export declare type Prompt = Array<PromptItem> | string;
export declare type SeedWeightPair = {
seed: number;
weight: number;
};
export declare type SeedWeights = Array<SeedWeightPair>;
// All generated images contain these metadata.
export declare type CommonGeneratedImageMetadata = {
postprocessing: null | Array<ESRGANMetadata | GFPGANMetadata>;
sampler:
| 'ddim'
| 'k_dpm_2_a'
| 'k_dpm_2'
| 'k_dpmpp_2_a'
| 'k_dpmpp_2'
| 'k_euler_a'
| 'k_euler'
| 'k_heun'
| 'k_lms'
| 'plms';
prompt: Prompt;
seed: number;
variations: SeedWeights;
steps: number;
cfg_scale: number;
width: number;
height: number;
seamless: boolean;
hires_fix: boolean;
extra: null | Record<string, never>; // Pending development of RFC #266
};
// txt2img and img2img images have some unique attributes.
export declare type Txt2ImgMetadata = GeneratedImageMetadata & {
type: 'txt2img';
};
export declare type Img2ImgMetadata = GeneratedImageMetadata & {
type: 'img2img';
orig_hash: string;
strength: number;
fit: boolean;
init_image_path: string;
mask_image_path?: string;
};
// Superset of generated image metadata types.
export declare type GeneratedImageMetadata = Txt2ImgMetadata | Img2ImgMetadata;
// All post processed images contain these metadata.
export declare type CommonPostProcessedImageMetadata = {
orig_path: string;
orig_hash: string;
};
// esrgan and gfpgan images have some unique attributes.
export declare type ESRGANMetadata = CommonPostProcessedImageMetadata & {
type: 'esrgan';
scale: 2 | 4;
strength: number;
denoise_str: number;
};
export declare type FacetoolMetadata = CommonPostProcessedImageMetadata & {
type: 'gfpgan' | 'codeformer';
strength: number;
fidelity?: number;
};
// Superset of all postprocessed image metadata types..
export declare type PostProcessedImageMetadata =
| ESRGANMetadata
| FacetoolMetadata;
// Metadata includes the system config and image metadata.
export declare type Metadata = SystemGenerationMetadata & {
image: GeneratedImageMetadata | PostProcessedImageMetadata;
};
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
export declare type Image = {
uuid: string;
url: string;
thumbnail: string;
mtime: number;
metadata?: Metadata;
width: number;
height: number;
category: GalleryCategory;
isBase64?: boolean;
dreamPrompt?: 'string';
};
// GalleryImages is an array of Image.
export declare type GalleryImages = {
images: Array<Image>;
};
/**
* Types related to the system status.
*/
// This represents the processing status of the backend.
export declare type SystemStatus = {
isProcessing: boolean;
currentStep: number;
totalSteps: number;
currentIteration: number;
totalIterations: number;
currentStatus: string;
currentStatusHasSteps: boolean;
hasError: boolean;
};
export declare type SystemGenerationMetadata = {
model: string;
model_weights?: string;
model_id?: string;
model_hash: string;
app_id: string;
app_version: string;
};
export declare type SystemConfig = SystemGenerationMetadata & {
model_list: ModelList;
infill_methods: string[];
};
export declare type ModelStatus = 'active' | 'cached' | 'not loaded';
export declare type Model = {
status: ModelStatus;
description: string;
weights: string;
config?: string;
vae?: string;
width?: number;
height?: number;
default?: boolean;
format?: string;
};
export declare type DiffusersModel = {
status: ModelStatus;
description: string;
repo_id?: string;
path?: string;
vae?: {
repo_id?: string;
path?: string;
};
format?: string;
default?: boolean;
};
export declare type ModelList = Record<string, Model & DiffusersModel>;
export declare type FoundModel = {
name: string;
location: string;
};
export declare type InvokeModelConfigProps = {
name: string | undefined;
description: string | undefined;
config: string | undefined;
weights: string | undefined;
vae: string | undefined;
width: number | undefined;
height: number | undefined;
default: boolean | undefined;
format: string | undefined;
};
export declare type InvokeDiffusersModelConfigProps = {
name: string | undefined;
description: string | undefined;
repo_id: string | undefined;
path: string | undefined;
default: boolean | undefined;
format: string | undefined;
vae: {
repo_id: string | undefined;
path: string | undefined;
};
};
export declare type InvokeModelConversionProps = {
model_name: string;
save_location: string;
custom_location: string | null;
};
export declare type InvokeModelMergingProps = {
models_to_merge: string[];
alpha: number;
interp: 'weighted_sum' | 'sigmoid' | 'inv_sigmoid' | 'add_difference';
force: boolean;
merged_model_name: string;
model_merge_save_path: string | null;
};
/**
* These types type data received from the server via socketio.
*/
export declare type ModelChangeResponse = {
model_name: string;
model_list: ModelList;
};
export declare type ModelConvertedResponse = {
converted_model_name: string;
model_list: ModelList;
};
export declare type ModelsMergedResponse = {
merged_models: string[];
merged_model_name: string;
model_list: ModelList;
};
export declare type ModelAddedResponse = {
new_model_name: string;
model_list: ModelList;
update: boolean;
};
export declare type ModelDeletedResponse = {
deleted_model_name: string;
model_list: ModelList;
};
export declare type FoundModelResponse = {
search_folder: string;
found_models: FoundModel[];
};
export declare type SystemStatusResponse = SystemStatus;
export declare type SystemConfigResponse = SystemConfig;
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
boundingBox?: IRect;
generationMode: InvokeTabName;
};
export declare type ImageUploadResponse = {
// image: Omit<Image, 'uuid' | 'metadata' | 'category'>;
url: string;
mtime: number;
width: number;
height: number;
thumbnail: string;
// bbox: [number, number, number, number];
};
export declare type ErrorResponse = {
message: string;
additionalData?: string;
};
export declare type GalleryImagesResponse = {
images: Array<Omit<Image, 'uuid'>>;
areMoreImagesAvailable: boolean;
category: GalleryCategory;
};
export declare type ImageDeletedResponse = {
uuid: string;
url: string;
category: GalleryCategory;
};
export declare type ImageUrlResponse = {
url: string;
};
export declare type UploadImagePayload = {
file: File;
destination?: ImageUploadDestination;
};
export declare type UploadOutpaintingMergeImagePayload = {
dataURL: string;
name: string;
};

View File

@ -0,0 +1,72 @@
import { createSelector } from '@reduxjs/toolkit';
import { validateSeedWeights } from 'common/util/seedWeightPairs';
import { initialCanvasImageSelector } from 'features/canvas/store/canvasSelectors';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
export const readinessSelector = createSelector(
[
generationSelector,
systemSelector,
initialCanvasImageSelector,
activeTabNameSelector,
],
(generation, system, initialCanvasImage, activeTabName) => {
const {
prompt,
shouldGenerateVariations,
seedWeights,
initialImage,
seed,
} = generation;
const { isProcessing, isConnected } = system;
let isReady = true;
const reasonsWhyNotReady: string[] = [];
// Cannot generate without a prompt
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
isReady = false;
reasonsWhyNotReady.push('Missing prompt');
}
if (activeTabName === 'img2img' && !initialImage) {
isReady = false;
reasonsWhyNotReady.push('No initial image selected');
}
// TODO: job queue
// Cannot generate if already processing an image
if (isProcessing) {
isReady = false;
reasonsWhyNotReady.push('System Busy');
}
// Cannot generate if not connected
if (!isConnected) {
isReady = false;
reasonsWhyNotReady.push('System Disconnected');
}
// Cannot generate variations without valid seed weights
if (
shouldGenerateVariations &&
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
) {
isReady = false;
reasonsWhyNotReady.push('Seed-Weights badly formatted.');
}
// All good
return { isReady, reasonsWhyNotReady };
},
{
memoizeOptions: {
equalityCheck: isEqual,
resultEqualityCheck: isEqual,
},
}
);

View File

@ -0,0 +1,61 @@
import { createAction } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import { GalleryCategory } from 'features/gallery/store/gallerySlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
/**
* We can't use redux-toolkit's createSlice() to make these actions,
* because they have no associated reducer. They only exist to dispatch
* requests to the server via socketio. These actions will be handled
* by the middleware.
*/
export const generateImage = createAction<InvokeTabName>(
'socketio/generateImage'
);
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
export const requestImages = createAction<GalleryCategory>(
'socketio/requestImages'
);
export const requestNewImages = createAction<GalleryCategory>(
'socketio/requestNewImages'
);
export const cancelProcessing = createAction<undefined>(
'socketio/cancelProcessing'
);
export const requestSystemConfig = createAction<undefined>(
'socketio/requestSystemConfig'
);
export const searchForModels = createAction<string>('socketio/searchForModels');
export const addNewModel = createAction<
InvokeAI.InvokeModelConfigProps | InvokeAI.InvokeDiffusersModelConfigProps
>('socketio/addNewModel');
export const deleteModel = createAction<string>('socketio/deleteModel');
export const convertToDiffusers =
createAction<InvokeAI.InvokeModelConversionProps>(
'socketio/convertToDiffusers'
);
export const mergeDiffusersModels =
createAction<InvokeAI.InvokeModelMergingProps>(
'socketio/mergeDiffusersModels'
);
export const requestModelChange = createAction<string>(
'socketio/requestModelChange'
);
export const saveStagingAreaImageToGallery = createAction<string>(
'socketio/saveStagingAreaImageToGallery'
);
export const emptyTempFolder = createAction<undefined>(
'socketio/requestEmptyTempFolder'
);

View File

@ -0,0 +1,208 @@
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import type { RootState } from 'app/store';
import {
frontendToBackendParameters,
FrontendToBackendParametersConfig,
} from 'common/util/parameterTranslation';
import dateFormat from 'dateformat';
import {
GalleryCategory,
GalleryState,
removeImage,
} from 'features/gallery/store/gallerySlice';
import {
addLogEntry,
generationRequested,
modelChangeRequested,
modelConvertRequested,
modelMergingRequested,
setIsProcessing,
} from 'features/system/store/systemSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { Socket } from 'socket.io-client';
/**
* Returns an object containing all functions which use `socketio.emit()`.
* i.e. those which make server requests.
*/
const makeSocketIOEmitters = (
store: MiddlewareAPI<Dispatch<AnyAction>, RootState>,
socketio: Socket
) => {
// We need to dispatch actions to redux and get pieces of state from the store.
const { dispatch, getState } = store;
return {
emitGenerateImage: (generationMode: InvokeTabName) => {
dispatch(setIsProcessing(true));
const state: RootState = getState();
const {
generation: generationState,
postprocessing: postprocessingState,
system: systemState,
canvas: canvasState,
} = state;
const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
{
generationMode,
generationState,
postprocessingState,
canvasState,
systemState,
};
dispatch(generationRequested());
const { generationParameters, esrganParameters, facetoolParameters } =
frontendToBackendParameters(frontendToBackendParametersConfig);
socketio.emit(
'generateImage',
generationParameters,
esrganParameters,
facetoolParameters
);
// we need to truncate the init_mask base64 else it takes up the whole log
// TODO: handle maintaining masks for reproducibility in future
if (generationParameters.init_mask) {
generationParameters.init_mask = generationParameters.init_mask
.substr(0, 64)
.concat('...');
}
if (generationParameters.init_img) {
generationParameters.init_img = generationParameters.init_img
.substr(0, 64)
.concat('...');
}
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image generation requested: ${JSON.stringify({
...generationParameters,
...esrganParameters,
...facetoolParameters,
})}`,
})
);
},
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
dispatch(setIsProcessing(true));
const {
postprocessing: {
upscalingLevel,
upscalingDenoising,
upscalingStrength,
},
} = getState();
const esrganParameters = {
upscale: [upscalingLevel, upscalingDenoising, upscalingStrength],
};
socketio.emit('runPostprocessing', imageToProcess, {
type: 'esrgan',
...esrganParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `ESRGAN upscale requested: ${JSON.stringify({
file: imageToProcess.url,
...esrganParameters,
})}`,
})
);
},
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
dispatch(setIsProcessing(true));
const {
postprocessing: { facetoolType, facetoolStrength, codeformerFidelity },
} = getState();
const facetoolParameters: Record<string, unknown> = {
facetool_strength: facetoolStrength,
};
if (facetoolType === 'codeformer') {
facetoolParameters.codeformer_fidelity = codeformerFidelity;
}
socketio.emit('runPostprocessing', imageToProcess, {
type: facetoolType,
...facetoolParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Face restoration (${facetoolType}) requested: ${JSON.stringify(
{
file: imageToProcess.url,
...facetoolParameters,
}
)}`,
})
);
},
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
const { url, uuid, category, thumbnail } = imageToDelete;
dispatch(removeImage(imageToDelete));
socketio.emit('deleteImage', url, thumbnail, uuid, category);
},
emitRequestImages: (category: GalleryCategory) => {
const gallery: GalleryState = getState().gallery;
const { earliest_mtime } = gallery.categories[category];
socketio.emit('requestImages', category, earliest_mtime);
},
emitRequestNewImages: (category: GalleryCategory) => {
const gallery: GalleryState = getState().gallery;
const { latest_mtime } = gallery.categories[category];
socketio.emit('requestLatestImages', category, latest_mtime);
},
emitCancelProcessing: () => {
socketio.emit('cancel');
},
emitRequestSystemConfig: () => {
socketio.emit('requestSystemConfig');
},
emitSearchForModels: (modelFolder: string) => {
socketio.emit('searchForModels', modelFolder);
},
emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => {
socketio.emit('addNewModel', modelConfig);
},
emitDeleteModel: (modelName: string) => {
socketio.emit('deleteModel', modelName);
},
emitConvertToDiffusers: (
modelToConvert: InvokeAI.InvokeModelConversionProps
) => {
dispatch(modelConvertRequested());
socketio.emit('convertToDiffusers', modelToConvert);
},
emitMergeDiffusersModels: (
modelMergeInfo: InvokeAI.InvokeModelMergingProps
) => {
dispatch(modelMergingRequested());
socketio.emit('mergeDiffusersModels', modelMergeInfo);
},
emitRequestModelChange: (modelName: string) => {
dispatch(modelChangeRequested());
socketio.emit('requestModelChange', modelName);
},
emitSaveStagingAreaImageToGallery: (url: string) => {
socketio.emit('requestSaveStagingAreaImageToGallery', url);
},
emitRequestEmptyTempFolder: () => {
socketio.emit('requestEmptyTempFolder');
},
};
};
export default makeSocketIOEmitters;

View File

@ -0,0 +1,498 @@
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
import dateFormat from 'dateformat';
import i18n from 'i18n';
import { v4 as uuidv4 } from 'uuid';
import * as InvokeAI from 'app/invokeai';
import {
addLogEntry,
addToast,
errorOccurred,
processingCanceled,
setCurrentStatus,
setFoundModels,
setIsCancelable,
setIsConnected,
setIsProcessing,
setModelList,
setSearchFolder,
setSystemConfig,
setSystemStatus,
} from 'features/system/store/systemSlice';
import {
addGalleryImages,
addImage,
clearIntermediateImage,
GalleryState,
removeImage,
setIntermediateImage,
} from 'features/gallery/store/gallerySlice';
import type { RootState } from 'app/store';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
clearInitialImage,
setInfillMethod,
setInitialImage,
setMaskPath,
} from 'features/parameters/store/generationSlice';
import { tabMap } from 'features/ui/store/tabMap';
import {
requestImages,
requestNewImages,
requestSystemConfig,
} from './actions';
/**
* Returns an object containing listener callbacks for socketio events.
* TODO: This file is large, but simple. Should it be split up further?
*/
const makeSocketIOListeners = (
store: MiddlewareAPI<Dispatch<AnyAction>, RootState>
) => {
const { dispatch, getState } = store;
return {
/**
* Callback to run when we receive a 'connect' event.
*/
onConnect: () => {
try {
dispatch(setIsConnected(true));
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
dispatch(requestSystemConfig());
const gallery: GalleryState = getState().gallery;
if (gallery.categories.result.latest_mtime) {
dispatch(requestNewImages('result'));
} else {
dispatch(requestImages('result'));
}
if (gallery.categories.user.latest_mtime) {
dispatch(requestNewImages('user'));
} else {
dispatch(requestImages('user'));
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'disconnect' event.
*/
onDisconnect: () => {
try {
dispatch(setIsConnected(false));
dispatch(setCurrentStatus(i18n.t('common.statusDisconnected')));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Disconnected from server`,
level: 'warning',
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'generationResult' event.
*/
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
try {
const state = getState();
const { activeTab } = state.ui;
const { shouldLoopback } = state.postprocessing;
const { boundingBox: _, generationMode, ...rest } = data;
const newImage = {
uuid: uuidv4(),
...rest,
};
if (['txt2img', 'img2img'].includes(generationMode)) {
dispatch(
addImage({
category: 'result',
image: { ...newImage, category: 'result' },
})
);
}
if (generationMode === 'unifiedCanvas' && data.boundingBox) {
const { boundingBox } = data;
dispatch(
addImageToStagingArea({
image: { ...newImage, category: 'temp' },
boundingBox,
})
);
if (state.canvas.shouldAutoSave) {
dispatch(
addImage({
image: { ...newImage, category: 'result' },
category: 'result',
})
);
}
}
if (shouldLoopback) {
const activeTabName = tabMap[activeTab];
switch (activeTabName) {
case 'img2img': {
dispatch(setInitialImage(newImage));
break;
}
}
}
dispatch(clearIntermediateImage());
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image generated: ${data.url}`,
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'intermediateResult' event.
*/
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
try {
dispatch(
setIntermediateImage({
uuid: uuidv4(),
...data,
category: 'result',
})
);
if (!data.isBase64) {
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Intermediate image generated: ${data.url}`,
})
);
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive an 'esrganResult' event.
*/
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
try {
dispatch(
addImage({
category: 'result',
image: {
uuid: uuidv4(),
...data,
category: 'result',
},
})
);
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Postprocessed: ${data.url}`,
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
* TODO: Add additional progress phases
*/
onProgressUpdate: (data: InvokeAI.SystemStatus) => {
try {
dispatch(setIsProcessing(true));
dispatch(setSystemStatus(data));
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
*/
onError: (data: InvokeAI.ErrorResponse) => {
const { message, additionalData } = data;
if (additionalData) {
// TODO: handle more data than short message
}
try {
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Server error: ${message}`,
level: 'error',
})
);
dispatch(errorOccurred());
dispatch(clearIntermediateImage());
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'galleryImages' event.
*/
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
const { images, areMoreImagesAvailable, category } = data;
/**
* the logic here ideally would be in the reducer but we have a side effect:
* generating a uuid. so the logic needs to be here, outside redux.
*/
// Generate a UUID for each image
const preparedImages = images.map((image): InvokeAI.Image => {
return {
uuid: uuidv4(),
...image,
};
});
dispatch(
addGalleryImages({
images: preparedImages,
areMoreImagesAvailable,
category,
})
);
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Loaded ${images.length} images`,
})
);
},
/**
* Callback to run when we receive a 'processingCanceled' event.
*/
onProcessingCanceled: () => {
dispatch(processingCanceled());
const { intermediateImage } = getState().gallery;
if (intermediateImage) {
if (!intermediateImage.isBase64) {
dispatch(
addImage({
category: 'result',
image: intermediateImage,
})
);
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Intermediate image saved: ${intermediateImage.url}`,
})
);
}
dispatch(clearIntermediateImage());
}
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Processing canceled`,
level: 'warning',
})
);
},
/**
* Callback to run when we receive a 'imageDeleted' event.
*/
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
const { url } = data;
// remove image from gallery
dispatch(removeImage(data));
// remove references to image in options
const {
generation: { initialImage, maskPath },
} = getState();
if (
initialImage === url ||
(initialImage as InvokeAI.Image)?.url === url
) {
dispatch(clearInitialImage());
}
if (maskPath === url) {
dispatch(setMaskPath(''));
}
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image deleted: ${url}`,
})
);
},
onSystemConfig: (data: InvokeAI.SystemConfig) => {
dispatch(setSystemConfig(data));
if (!data.infill_methods.includes('patchmatch')) {
dispatch(setInfillMethod(data.infill_methods[0]));
}
},
onFoundModels: (data: InvokeAI.FoundModelResponse) => {
const { search_folder, found_models } = data;
dispatch(setSearchFolder(search_folder));
dispatch(setFoundModels(found_models));
},
onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => {
const { new_model_name, model_list, update } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(setCurrentStatus(i18n.t('modelManager.modelAdded')));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model Added: ${new_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: !update
? `${i18n.t('modelManager.modelAdded')}: ${new_model_name}`
: `${i18n.t('modelManager.modelUpdated')}: ${new_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => {
const { deleted_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `${i18n.t(
'modelmanager:modelAdded'
)}: ${deleted_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t(
'modelmanager:modelEntryDeleted'
)}: ${deleted_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelConverted: (data: InvokeAI.ModelConvertedResponse) => {
const { converted_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusModelConverted')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model converted: ${converted_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t(
'modelmanager:modelConverted'
)}: ${converted_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelsMerged: (data: InvokeAI.ModelsMergedResponse) => {
const { merged_models, merged_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusMergedModels')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Models merged: ${merged_models}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t('modelManager.modelsMerged')}: ${merged_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
const { model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusModelChanged')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model changed: ${model_name}`,
level: 'info',
})
);
},
onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => {
const { model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(errorOccurred());
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model change failed: ${model_name}`,
level: 'error',
})
);
},
onTempFolderEmptied: () => {
dispatch(
addToast({
title: i18n.t('toast.tempFoldersEmptied'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
};
};
export default makeSocketIOListeners;

View File

@ -0,0 +1,244 @@
import { Middleware } from '@reduxjs/toolkit';
import { io } from 'socket.io-client';
import makeSocketIOEmitters from './emitters';
import makeSocketIOListeners from './listeners';
import * as InvokeAI from 'app/invokeai';
/**
* Creates a socketio middleware to handle communication with server.
*
* Special `socketio/actionName` actions are created in actions.ts and
* exported for use by the application, which treats them like any old
* action, using `dispatch` to dispatch them.
*
* These actions are intercepted here, where `socketio.emit()` calls are
* made on their behalf - see `emitters.ts`. The emitter functions
* are the outbound communication to the server.
*
* Listeners are also established here - see `listeners.ts`. The listener
* functions receive communication from the server and usually dispatch
* some new action to handle whatever data was sent from the server.
*/
export const socketioMiddleware = () => {
const { origin } = new URL(window.location.href);
const socketio = io(origin, {
timeout: 60000,
path: `${window.location.pathname}socket.io`,
});
let areListenersSet = false;
const middleware: Middleware = (store) => (next) => (action) => {
const {
onConnect,
onDisconnect,
onError,
onPostprocessingResult,
onGenerationResult,
onIntermediateResult,
onProgressUpdate,
onGalleryImages,
onProcessingCanceled,
onImageDeleted,
onSystemConfig,
onModelChanged,
onFoundModels,
onNewModelAdded,
onModelDeleted,
onModelConverted,
onModelsMerged,
onModelChangeFailed,
onTempFolderEmptied,
} = makeSocketIOListeners(store);
const {
emitGenerateImage,
emitRunESRGAN,
emitRunFacetool,
emitDeleteImage,
emitRequestImages,
emitRequestNewImages,
emitCancelProcessing,
emitRequestSystemConfig,
emitSearchForModels,
emitAddNewModel,
emitDeleteModel,
emitConvertToDiffusers,
emitMergeDiffusersModels,
emitRequestModelChange,
emitSaveStagingAreaImageToGallery,
emitRequestEmptyTempFolder,
} = makeSocketIOEmitters(store, socketio);
/**
* If this is the first time the middleware has been called (e.g. during store setup),
* initialize all our socket.io listeners.
*/
if (!areListenersSet) {
socketio.on('connect', () => onConnect());
socketio.on('disconnect', () => onDisconnect());
socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data));
socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) =>
onGenerationResult(data)
);
socketio.on(
'postprocessingResult',
(data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data)
);
socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
onIntermediateResult(data)
);
socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) =>
onProgressUpdate(data)
);
socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) =>
onGalleryImages(data)
);
socketio.on('processingCanceled', () => {
onProcessingCanceled();
});
socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
onImageDeleted(data);
});
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
onSystemConfig(data);
});
socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => {
onFoundModels(data);
});
socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => {
onNewModelAdded(data);
});
socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => {
onModelDeleted(data);
});
socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => {
onModelConverted(data);
});
socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => {
onModelsMerged(data);
});
socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
onModelChanged(data);
});
socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => {
onModelChangeFailed(data);
});
socketio.on('tempFolderEmptied', () => {
onTempFolderEmptied();
});
areListenersSet = true;
}
/**
* Handle redux actions caught by middleware.
*/
switch (action.type) {
case 'socketio/generateImage': {
emitGenerateImage(action.payload);
break;
}
case 'socketio/runESRGAN': {
emitRunESRGAN(action.payload);
break;
}
case 'socketio/runFacetool': {
emitRunFacetool(action.payload);
break;
}
case 'socketio/deleteImage': {
emitDeleteImage(action.payload);
break;
}
case 'socketio/requestImages': {
emitRequestImages(action.payload);
break;
}
case 'socketio/requestNewImages': {
emitRequestNewImages(action.payload);
break;
}
case 'socketio/cancelProcessing': {
emitCancelProcessing();
break;
}
case 'socketio/requestSystemConfig': {
emitRequestSystemConfig();
break;
}
case 'socketio/searchForModels': {
emitSearchForModels(action.payload);
break;
}
case 'socketio/addNewModel': {
emitAddNewModel(action.payload);
break;
}
case 'socketio/deleteModel': {
emitDeleteModel(action.payload);
break;
}
case 'socketio/convertToDiffusers': {
emitConvertToDiffusers(action.payload);
break;
}
case 'socketio/mergeDiffusersModels': {
emitMergeDiffusersModels(action.payload);
break;
}
case 'socketio/requestModelChange': {
emitRequestModelChange(action.payload);
break;
}
case 'socketio/saveStagingAreaImageToGallery': {
emitSaveStagingAreaImageToGallery(action.payload);
break;
}
case 'socketio/requestEmptyTempFolder': {
emitRequestEmptyTempFolder();
break;
}
}
next(action);
};
return middleware;
};

View File

@ -0,0 +1,109 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import { getPersistConfig } from 'redux-deep-persist';
import canvasReducer from 'features/canvas/store/canvasSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
import uiReducer from 'features/ui/store/uiSlice';
import { socketioMiddleware } from './socketio/middleware';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
*
* While we definitely want generation parameters to be persisted, there are a number
* of things we do *not* want to be persisted across reloads:
* - Gallery/selected image (user may add/delete images from disk between page loads)
* - Connection/processing status
* - Availability of external libraries like ESRGAN/GFPGAN
*
* These can be blacklisted in redux-persist.
*
* The necesssary nested persistors with blacklists are configured below.
*/
const canvasBlacklist = [
'cursorPosition',
'isCanvasInitialized',
'doesCanvasNeedScaling',
].map((blacklistItem) => `canvas.${blacklistItem}`);
const systemBlacklist = [
'currentIteration',
'currentStatus',
'currentStep',
'isCancelable',
'isConnected',
'isESRGANAvailable',
'isGFPGANAvailable',
'isProcessing',
'socketId',
'totalIterations',
'totalSteps',
'openModel',
'cancelOptions.cancelAfter',
].map((blacklistItem) => `system.${blacklistItem}`);
const galleryBlacklist = [
'categories',
'currentCategory',
'currentImage',
'currentImageUuid',
'shouldAutoSwitchToNewImages',
'shouldHoldGalleryOpen',
'intermediateImage',
].map((blacklistItem) => `gallery.${blacklistItem}`);
const rootReducer = combineReducers({
generation: generationReducer,
postprocessing: postprocessingReducer,
gallery: galleryReducer,
system: systemReducer,
canvas: canvasReducer,
ui: uiReducer,
lightbox: lightboxReducer,
});
const rootPersistConfig = getPersistConfig({
key: 'root',
storage,
rootReducer,
blacklist: [...canvasBlacklist, ...systemBlacklist, ...galleryBlacklist],
debounce: 300,
});
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
// Continue with store setup
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(socketioMiddleware()),
devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [
'canvas/setCursorPosition',
'canvas/setStageCoordinates',
'canvas/setStageScale',
'canvas/setIsDrawing',
'canvas/setBoundingBoxCoordinates',
'canvas/setBoundingBoxDimensions',
'canvas/setIsDrawing',
'canvas/addPointToCurrentLine',
],
},
});
export type AppGetState = typeof store.getState;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -0,0 +1,52 @@
import { extendTheme } from '@chakra-ui/react';
import type { StyleFunctionProps } from '@chakra-ui/styled-system';
export const theme = extendTheme({
config: {
initialColorMode: 'dark',
useSystemColorMode: false,
},
components: {
Tooltip: {
baseStyle: (props: StyleFunctionProps) => ({
textColor: props.colorMode === 'dark' ? 'gray.800' : 'gray.100',
}),
},
Accordion: {
baseStyle: (props: StyleFunctionProps) => ({
button: {
fontWeight: 'bold',
_hover: {
bgColor:
props.colorMode === 'dark'
? 'rgba(255,255,255,0.05)'
: 'rgba(0,0,0,0.05)',
},
},
panel: {
paddingBottom: 2,
},
}),
},
FormLabel: {
baseStyle: {
fontWeight: 'light',
},
},
Button: {
variants: {
imageHoverIconButton: (props: StyleFunctionProps) => ({
bg: props.colorMode === 'dark' ? 'blackAlpha.700' : 'whiteAlpha.800',
color:
props.colorMode === 'dark' ? 'whiteAlpha.700' : 'blackAlpha.700',
_hover: {
bg:
props.colorMode === 'dark' ? 'blackAlpha.800' : 'whiteAlpha.800',
color:
props.colorMode === 'dark' ? 'whiteAlpha.900' : 'blackAlpha.900',
},
}),
},
},
},
});

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.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

View File

@ -0,0 +1,77 @@
<?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 60 60" 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="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
<g transform="matrix(1,0,0,1,0,5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,35)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,40)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,45)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,50)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,55)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,60)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-35)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-40)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-45)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-50)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-55)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,0,-60)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,22 @@
import { Box, forwardRef, Icon } from '@chakra-ui/react';
import { Feature } from 'app/features';
import { IconType } from 'react-icons';
import { MdHelp } from 'react-icons/md';
import GuidePopover from './GuidePopover';
type GuideIconProps = {
feature: Feature;
icon?: IconType;
};
const GuideIcon = forwardRef(
({ feature, icon = MdHelp }: GuideIconProps, ref) => (
<GuidePopover feature={feature}>
<Box ref={ref}>
<Icon marginBottom="-.15rem" as={icon} />
</Box>
</GuidePopover>
)
);
export default GuideIcon;

View File

@ -0,0 +1,20 @@
.guide-popover-arrow {
background-color: var(--tab-panel-bg);
box-shadow: none;
}
.guide-popover-content {
background-color: var(--background-color-secondary);
border: none;
}
.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

@ -0,0 +1,49 @@
import {
Box,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { Feature, useFeatureHelpInfo } from 'app/features';
import { useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { SystemState } from 'features/system/store/systemSlice';
import { ReactElement } from 'react';
type GuideProps = {
children: ReactElement;
feature: Feature;
};
const guidePopoverSelector = createSelector(
systemSelector,
(system: SystemState) => system.shouldDisplayGuides
);
const GuidePopover = ({ children, feature }: GuideProps) => {
const shouldDisplayGuides = useAppSelector(guidePopoverSelector);
const { text } = useFeatureHelpInfo(feature);
if (!shouldDisplayGuides) return null;
return (
<Popover trigger="hover">
<PopoverTrigger>
<Box>{children}</Box>
</PopoverTrigger>
<PopoverContent
className="guide-popover-content"
maxWidth="400px"
onClick={(e) => e.preventDefault()}
cursor="initial"
>
<PopoverArrow className="guide-popover-arrow" />
<div className="guide-popover-guide-content">{text}</div>
</PopoverContent>
</Popover>
);
};
export default GuidePopover;

View File

@ -0,0 +1,86 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
forwardRef,
useDisclosure,
} from '@chakra-ui/react';
import { cloneElement, ReactElement, ReactNode, useRef } from 'react';
type Props = {
acceptButtonText?: string;
acceptCallback: () => void;
cancelButtonText?: string;
cancelCallback?: () => void;
children: ReactNode;
title: string;
triggerComponent: ReactElement;
};
const IAIAlertDialog = forwardRef((props: Props, ref) => {
const {
acceptButtonText = 'Accept',
acceptCallback,
cancelButtonText = 'Cancel',
cancelCallback,
children,
title,
triggerComponent,
} = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef<HTMLButtonElement | null>(null);
const handleAccept = () => {
acceptCallback();
onClose();
};
const handleCancel = () => {
cancelCallback && cancelCallback();
onClose();
};
return (
<>
{cloneElement(triggerComponent, {
onClick: onOpen,
ref: ref,
})}
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent className="modal">
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{children}</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={handleCancel}
className="modal-close-btn"
>
{cancelButtonText}
</Button>
<Button colorScheme="red" onClick={handleAccept} ml={3}>
{acceptButtonText}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
});
export default IAIAlertDialog;

View File

@ -0,0 +1,8 @@
.invokeai__button {
background-color: var(--btn-base-color);
place-content: center;
&:hover {
background-color: var(--btn-base-color-hover);
}
}

View File

@ -0,0 +1,32 @@
import {
Button,
ButtonProps,
forwardRef,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { ReactNode } from 'react';
export interface IAIButtonProps extends ButtonProps {
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
styleClass?: string;
children: ReactNode;
}
const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
const { children, tooltip = '', tooltipProps, styleClass, ...rest } = props;
return (
<Tooltip label={tooltip} {...tooltipProps}>
<Button
ref={forwardedRef}
className={['invokeai__button', styleClass].join(' ')}
{...rest}
>
{children}
</Button>
</Tooltip>
);
});
export default IAIButton;

View File

@ -0,0 +1,26 @@
.invokeai__checkbox {
.chakra-checkbox__label {
margin-top: 1px;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.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;
}
&[data-checked] {
color: var(--text-color);
background-color: var(--input-checkbox-checked-bg);
}
}
}

View File

@ -0,0 +1,18 @@
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
import type { ReactNode } from 'react';
type IAICheckboxProps = CheckboxProps & {
label: string | ReactNode;
styleClass?: string;
};
const IAICheckbox = (props: IAICheckboxProps) => {
const { label, styleClass, ...rest } = props;
return (
<Checkbox className={`invokeai__checkbox ${styleClass}`} {...rest}>
{label}
</Checkbox>
);
};
export default IAICheckbox;

View 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);
}
}

View 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;

View File

@ -0,0 +1,82 @@
@use '../../styles/Mixins/' as *;
.invokeai__icon-button {
background: var(--btn-base-color);
cursor: pointer;
&:hover {
background-color: var(--btn-base-color-hover);
}
&[data-selected='true'] {
background-color: var(--accent-color);
&:hover {
background-color: var(--accent-color-hover);
}
}
&[disabled] {
cursor: not-allowed;
}
&[data-variant='link'] {
background: none;
&:hover {
background: none;
}
}
// Check Box Style
&[data-as-checkbox='true'] {
background-color: var(--btn-base-color);
border: 3px solid var(--btn-base-color);
svg {
fill: var(--text-color);
}
&:hover {
background-color: var(--btn-base-color);
border-color: var(--btn-checkbox-border-hover);
svg {
fill: var(--text-color);
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
svg {
fill: var(--accent-color-hover);
}
&:hover {
svg {
fill: var(--accent-color-hover);
}
}
}
}
&[data-alert='true'] {
animation-name: pulseColor;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
&:hover {
animation: none;
background-color: var(--accent-color-hover);
}
}
}
@keyframes pulseColor {
0% {
background-color: var(--accent-color);
}
50% {
background-color: var(--accent-color-dim);
}
100% {
background-color: var(--accent-color);
}
}

View File

@ -0,0 +1,51 @@
import {
forwardRef,
IconButton,
IconButtonProps,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
export type IAIIconButtonProps = IconButtonProps & {
styleClass?: string;
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
asCheckbox?: boolean;
isChecked?: boolean;
};
const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
const {
tooltip = '',
styleClass,
tooltipProps,
asCheckbox,
isChecked,
...rest
} = props;
return (
<Tooltip
label={tooltip}
hasArrow
{...tooltipProps}
{...(tooltipProps?.placement
? { placement: tooltipProps.placement }
: { placement: 'top' })}
>
<IconButton
ref={forwardedRef}
className={
styleClass
? `invokeai__icon-button ${styleClass}`
: `invokeai__icon-button`
}
data-as-checkbox={asCheckbox}
data-selected={isChecked !== undefined ? isChecked : undefined}
{...rest}
/>
</Tooltip>
);
});
export default IAIIconButton;

View File

@ -0,0 +1,33 @@
.input {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center;
.input-label {
color: var(--text-color-secondary);
}
.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(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-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,47 @@
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;
size?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
}
export default function IAIInput(props: IAIInputProps) {
const {
label = '',
styleClass,
isDisabled = false,
fontSize = 'sm',
width,
size = 'sm',
isInvalid,
...rest
} = props;
return (
<FormControl
className={`input ${styleClass}`}
isInvalid={isInvalid}
isDisabled={isDisabled}
>
{label !== '' && (
<FormLabel
fontSize={fontSize}
fontWeight="bold"
alignItems="center"
whiteSpace="nowrap"
marginBottom={0}
marginRight={0}
className="input-label"
>
{label}
</FormLabel>
)}
<Input {...rest} className="input-entry" size={size} width={width} />
</FormControl>
);
}

View File

@ -0,0 +1,66 @@
.invokeai__number-input-form-control {
display: flex;
align-items: center;
column-gap: 1rem;
.invokeai__number-input-form-label {
color: var(--text-color-secondary);
&[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);
}
}
.invokeai__number-input-root {
height: 2rem;
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.3rem;
}
.invokeai__number-input-field {
border: none;
font-weight: bold;
width: 100%;
height: auto;
font-size: 0.9rem;
padding: 0 0.5rem;
&:focus {
outline: none;
box-shadow: none;
}
&:disabled {
opacity: 0.2;
}
}
.invokeai__number-input-stepper {
display: grid;
padding-right: 0.5rem;
.invokeai__number-input-stepper-button {
border: none;
// expand arrow hitbox
padding: 0 0.5rem;
margin: 0 -0.5rem;
svg {
width: 10px;
height: 10px;
}
}
}
}

View File

@ -0,0 +1,178 @@
import {
FormControl,
FormControlProps,
FormLabel,
FormLabelProps,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputFieldProps,
NumberInputProps,
NumberInputStepperProps,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { clamp } from 'lodash';
import { FocusEvent, useEffect, useState } from 'react';
const numberStringRegex = /^-?(0\.)?\.?$/;
interface Props extends Omit<NumberInputProps, 'onChange'> {
styleClass?: string;
label?: string;
labelFontSize?: string | number;
width?: string | number;
showStepper?: boolean;
value?: number;
onChange: (v: number) => void;
min: number;
max: number;
clamp?: boolean;
isInteger?: boolean;
formControlProps?: FormControlProps;
formLabelProps?: FormLabelProps;
numberInputProps?: NumberInputProps;
numberInputFieldProps?: NumberInputFieldProps;
numberInputStepperProps?: NumberInputStepperProps;
tooltipProps?: Omit<TooltipProps, 'children'>;
}
/**
* Customized Chakra FormControl + NumberInput multi-part component.
*/
const IAINumberInput = (props: Props) => {
const {
label,
labelFontSize = 'sm',
styleClass,
isDisabled = false,
showStepper = true,
width,
textAlign,
isInvalid,
value,
onChange,
min,
max,
isInteger = true,
formControlProps,
formLabelProps,
numberInputFieldProps,
numberInputStepperProps,
tooltipProps,
...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 (
<Tooltip {...tooltipProps}>
<FormControl
isDisabled={isDisabled}
isInvalid={isInvalid}
className={
styleClass
? `invokeai__number-input-form-control ${styleClass}`
: `invokeai__number-input-form-control`
}
{...formControlProps}
>
{label && (
<FormLabel
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
fontSize={labelFontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
{...formLabelProps}
>
{label}
</FormLabel>
)}
<NumberInput
className="invokeai__number-input-root"
value={valueAsString}
min={min}
max={max}
keepWithinRange={true}
clampValueOnBlur={false}
onChange={handleOnChange}
onBlur={handleBlur}
width={width}
{...rest}
>
<NumberInputField
className="invokeai__number-input-field"
textAlign={textAlign}
{...numberInputFieldProps}
/>
{showStepper && (
<div className="invokeai__number-input-stepper">
<NumberIncrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
<NumberDecrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
</div>
)}
</NumberInput>
</FormControl>
</Tooltip>
);
};
export default IAINumberInput;

View File

@ -0,0 +1,12 @@
.invokeai__popover-content {
min-width: unset;
width: unset;
padding: 1rem;
border-radius: 0.5rem;
background-color: var(--background-color);
border: 2px solid var(--border-color);
.invokeai__popover-arrow {
background-color: var(--background-color) !important;
}
}

View File

@ -0,0 +1,39 @@
import {
BoxProps,
Popover,
PopoverArrow,
PopoverContent,
PopoverProps,
PopoverTrigger,
} from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIPopoverProps = PopoverProps & {
triggerComponent: ReactNode;
triggerContainerProps?: BoxProps;
children: ReactNode;
styleClass?: string;
hasArrow?: boolean;
};
const IAIPopover = (props: IAIPopoverProps) => {
const {
triggerComponent,
children,
styleClass,
hasArrow = true,
...rest
} = props;
return (
<Popover {...rest}>
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
{hasArrow && <PopoverArrow className="invokeai__popover-arrow" />}
{children}
</PopoverContent>
</Popover>
);
};
export default IAIPopover;

View File

@ -0,0 +1,31 @@
@use '../../styles/Mixins/' as *;
.invokeai__select {
display: flex;
column-gap: 1rem;
align-items: center;
.invokeai__select-label {
color: var(--text-color-secondary);
}
.invokeai__select-picker {
border: 2px solid var(--border-color);
background-color: var(--background-color-secondary);
font-weight: bold;
font-size: 0.9rem;
height: 2rem;
border-radius: 0.2rem;
&:focus {
outline: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
}
.invokeai__select-option {
background-color: var(--background-color-secondary);
color: var(--text-color-secondary);
}
}

View File

@ -0,0 +1,86 @@
import {
FormControl,
FormLabel,
Select,
SelectProps,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { MouseEvent } from 'react';
type IAISelectProps = SelectProps & {
label?: string;
styleClass?: string;
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
validValues:
| Array<number | string>
| Array<{ key: string; value: string | number }>;
};
/**
* Customized Chakra FormControl + Select multi-part component.
*/
const IAISelect = (props: IAISelectProps) => {
const {
label,
isDisabled,
validValues,
tooltip,
tooltipProps,
size = 'sm',
fontSize = 'sm',
styleClass,
...rest
} = props;
return (
<FormControl
isDisabled={isDisabled}
className={`invokeai__select ${styleClass}`}
onClick={(e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
>
{label && (
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
>
{label}
</FormLabel>
)}
<Tooltip label={tooltip} {...tooltipProps}>
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
{opt.key}
</option>
);
})}
</Select>
</Tooltip>
</FormControl>
);
};
export default IAISelect;

View File

@ -0,0 +1,102 @@
import {
Menu,
MenuButton,
MenuItem,
MenuList,
MenuProps,
MenuButtonProps,
MenuListProps,
MenuItemProps,
} from '@chakra-ui/react';
import { MouseEventHandler, ReactNode } from 'react';
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
import IAIButton from './IAIButton';
import IAIIconButton from './IAIIconButton';
interface IAIMenuItem {
item: ReactNode | string;
onClick: MouseEventHandler<HTMLButtonElement> | undefined;
}
interface IAIMenuProps {
menuType?: 'icon' | 'regular';
buttonText?: string;
iconTooltip?: string;
menuItems: IAIMenuItem[];
menuProps?: MenuProps;
menuButtonProps?: MenuButtonProps;
menuListProps?: MenuListProps;
menuItemProps?: MenuItemProps;
}
export default function IAISimpleMenu(props: IAIMenuProps) {
const {
menuType = 'icon',
iconTooltip,
buttonText,
menuItems,
menuProps,
menuButtonProps,
menuListProps,
menuItemProps,
} = props;
const renderMenuItems = () => {
const menuItemsToRender: ReactNode[] = [];
menuItems.forEach((menuItem, index) => {
menuItemsToRender.push(
<MenuItem
key={index}
onClick={menuItem.onClick}
fontSize="0.9rem"
color="var(--text-color-secondary)"
backgroundColor="var(--background-color-secondary)"
_focus={{
color: 'var(--text-color)',
backgroundColor: 'var(--border-color)',
}}
{...menuItemProps}
>
{menuItem.item}
</MenuItem>
);
});
return menuItemsToRender;
};
return (
<Menu {...menuProps}>
{({ isOpen }) => (
<>
<MenuButton
as={menuType === 'icon' ? IAIIconButton : IAIButton}
tooltip={iconTooltip}
icon={isOpen ? <MdArrowDropUp /> : <MdArrowDropDown />}
padding={menuType === 'regular' ? '0 0.5rem' : 0}
backgroundColor="var(--btn-base-color)"
_hover={{
backgroundColor: 'var(--btn-base-color-hover)',
}}
minWidth="1rem"
minHeight="1rem"
fontSize="1.5rem"
{...menuButtonProps}
>
{menuType === 'regular' && buttonText}
</MenuButton>
<MenuList
zIndex={15}
padding={0}
borderRadius="0.5rem"
backgroundColor="var(--background-color-secondary)"
color="var(--text-color-secondary)"
borderColor="var(--border-color)"
{...menuListProps}
>
{renderMenuItems()}
</MenuList>
</>
)}
</Menu>
);
}

View File

@ -0,0 +1,60 @@
.invokeai__slider-component {
padding-bottom: 0.5rem;
border-radius: 0.5rem;
.invokeai__slider-component-label {
min-width: max-content;
margin: 0;
font-weight: bold;
color: var(--text-color-secondary);
}
.invokeai__slider_track {
background-color: var(--tab-color);
}
.invokeai__slider_track-filled {
background-color: var(--slider-color);
}
.invokeai__slider-thumb {
width: 4px;
}
.invokeai__slider-mark {
font-size: 0.75rem;
font-weight: bold;
color: var(--slider-mark-color);
margin-top: 0.3rem;
}
.invokeai__slider-number-input {
border: none;
font-size: 0.9rem;
font-weight: bold;
height: 2rem;
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
&:focus {
outline: none;
box-shadow: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
&:disabled {
opacity: 0.2;
}
}
.invokeai__slider-number-stepper {
border: none;
}
&[data-markers='true'] {
.invokeai__slider_container {
margin-top: -1rem;
}
}
}

View File

@ -0,0 +1,275 @@
import {
FormControl,
FormControlProps,
FormLabel,
FormLabelProps,
HStack,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputFieldProps,
NumberInputProps,
NumberInputStepper,
NumberInputStepperProps,
Slider,
SliderFilledTrack,
SliderMark,
SliderMarkProps,
SliderThumb,
SliderThumbProps,
SliderTrack,
SliderTrackProps,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { clamp } from 'lodash';
import { FocusEvent, useEffect, useMemo, useState } from 'react';
import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
export type IAIFullSliderProps = {
label: string;
value: number;
min?: number;
max?: number;
step?: number;
onChange: (v: number) => void;
withSliderMarks?: boolean;
sliderMarkLeftOffset?: number;
sliderMarkRightOffset?: number;
withInput?: boolean;
isInteger?: boolean;
width?: string | number;
inputWidth?: string | number;
inputReadOnly?: boolean;
withReset?: boolean;
handleReset?: () => void;
isResetDisabled?: boolean;
isSliderDisabled?: boolean;
isInputDisabled?: boolean;
tooltipSuffix?: string;
hideTooltip?: boolean;
isCompact?: boolean;
styleClass?: string;
sliderFormControlProps?: FormControlProps;
sliderFormLabelProps?: FormLabelProps;
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
sliderTrackProps?: SliderTrackProps;
sliderThumbProps?: SliderThumbProps;
sliderNumberInputProps?: NumberInputProps;
sliderNumberInputFieldProps?: NumberInputFieldProps;
sliderNumberInputStepperProps?: NumberInputStepperProps;
sliderTooltipProps?: Omit<TooltipProps, 'children'>;
sliderIAIIconButtonProps?: IAIIconButtonProps;
};
export default function IAISlider(props: IAIFullSliderProps) {
const [showTooltip, setShowTooltip] = useState(false);
const {
label,
value,
min = 1,
max = 100,
step = 1,
onChange,
width = '100%',
tooltipSuffix = '',
withSliderMarks = false,
sliderMarkLeftOffset = 0,
sliderMarkRightOffset = -1,
withInput = false,
isInteger = false,
inputWidth = '5.5rem',
inputReadOnly = false,
withReset = false,
hideTooltip = false,
isCompact = false,
handleReset,
isResetDisabled,
isSliderDisabled,
isInputDisabled,
styleClass,
sliderFormControlProps,
sliderFormLabelProps,
sliderMarkProps,
sliderTrackProps,
sliderThumbProps,
sliderNumberInputProps,
sliderNumberInputFieldProps,
sliderNumberInputStepperProps,
sliderTooltipProps,
sliderIAIIconButtonProps,
...rest
} = props;
const [localInputValue, setLocalInputValue] = useState<
string | number | undefined
>(String(value));
useEffect(() => {
setLocalInputValue(value);
}, [value]);
const numberInputMax = useMemo(
() => (sliderNumberInputProps?.max ? sliderNumberInputProps.max : max),
[max, sliderNumberInputProps?.max]
);
const handleSliderChange = (v: number) => {
onChange(v);
};
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
if (e.target.value === '') e.target.value = String(min);
const clamped = clamp(
isInteger ? Math.floor(Number(e.target.value)) : Number(localInputValue),
min,
numberInputMax
);
onChange(clamped);
};
const handleInputChange = (v: number | string) => {
setLocalInputValue(v);
};
const handleResetDisable = () => {
if (!handleReset) return;
handleReset();
};
return (
<FormControl
className={
styleClass
? `invokeai__slider-component ${styleClass}`
: `invokeai__slider-component`
}
data-markers={withSliderMarks}
style={
isCompact
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
columnGap: '1rem',
margin: 0,
padding: 0,
}
: {}
}
{...sliderFormControlProps}
>
<FormLabel
className="invokeai__slider-component-label"
fontSize="sm"
{...sliderFormLabelProps}
>
{label}
</FormLabel>
<HStack w="100%" gap={2} alignItems="center">
<Slider
aria-label={label}
value={value}
min={min}
max={max}
step={step}
onChange={handleSliderChange}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
focusThumbOnChange={false}
isDisabled={isSliderDisabled}
width={width}
{...rest}
>
{withSliderMarks && (
<>
<SliderMark
value={min}
className="invokeai__slider-mark invokeai__slider-mark-start"
ml={sliderMarkLeftOffset}
{...sliderMarkProps}
>
{min}
</SliderMark>
<SliderMark
value={max}
className="invokeai__slider-mark invokeai__slider-mark-end"
ml={sliderMarkRightOffset}
{...sliderMarkProps}
>
{max}
</SliderMark>
</>
)}
<SliderTrack className="invokeai__slider_track" {...sliderTrackProps}>
<SliderFilledTrack className="invokeai__slider_track-filled" />
</SliderTrack>
<Tooltip
hasArrow
className="invokeai__slider-component-tooltip"
placement="top"
isOpen={showTooltip}
label={`${value}${tooltipSuffix}`}
hidden={hideTooltip}
{...sliderTooltipProps}
>
<SliderThumb
className="invokeai__slider-thumb"
{...sliderThumbProps}
/>
</Tooltip>
</Slider>
{withInput && (
<NumberInput
min={min}
max={numberInputMax}
step={step}
value={localInputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
className="invokeai__slider-number-field"
isDisabled={isInputDisabled}
{...sliderNumberInputProps}
>
<NumberInputField
className="invokeai__slider-number-input"
width={inputWidth}
readOnly={inputReadOnly}
minWidth={inputWidth}
{...sliderNumberInputFieldProps}
/>
<NumberInputStepper {...sliderNumberInputStepperProps}>
<NumberIncrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
<NumberDecrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
</NumberInputStepper>
</NumberInput>
)}
{withReset && (
<IAIIconButton
size="sm"
aria-label="Reset"
tooltip="Reset"
icon={<BiReset />}
onClick={handleResetDisable}
isDisabled={isResetDisabled}
{...sliderIAIIconButtonProps}
/>
)}
</HStack>
</FormControl>
);
}

View File

@ -0,0 +1,24 @@
.invokeai__switch-form-control {
.invokeai__switch-form-label {
color: var(--text-color-secondary);
}
.invokeai__switch-root {
span {
background-color: var(--switch-bg-color);
span {
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
}
}
}

View File

@ -0,0 +1,60 @@
import {
FormControl,
FormControlProps,
FormLabel,
FormLabelProps,
Switch,
SwitchProps,
} from '@chakra-ui/react';
interface Props extends SwitchProps {
label?: string;
width?: string | number;
styleClass?: string;
formControlProps?: FormControlProps;
formLabelProps?: FormLabelProps;
}
/**
* Customized Chakra FormControl + Switch multi-part component.
*/
const IAISwitch = (props: Props) => {
const {
label,
isDisabled = false,
width = 'auto',
formControlProps,
formLabelProps,
styleClass,
...rest
} = props;
return (
<FormControl
isDisabled={isDisabled}
width={width}
className={`invokeai__switch-form-control ${styleClass}`}
display="flex"
columnGap="1rem"
alignItems="center"
justifyContent="space-between"
{...formControlProps}
>
<FormLabel
className="invokeai__switch-form-label"
whiteSpace="nowrap"
marginRight={0}
marginTop={0.5}
marginBottom={0.5}
fontSize="sm"
fontWeight="bold"
width="auto"
{...formLabelProps}
>
{label}
</FormLabel>
<Switch className="invokeai__switch-root" {...rest} />
</FormControl>
);
};
export default IAISwitch;

View File

@ -0,0 +1,39 @@
import { Heading } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook';
type ImageUploadOverlayProps = {
isDragAccept: boolean;
isDragReject: boolean;
overlaySecondaryText: string;
setIsHandlingUpload: (isHandlingUpload: boolean) => void;
};
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const {
isDragAccept,
isDragReject,
overlaySecondaryText,
setIsHandlingUpload,
} = props;
useHotkeys('esc', () => {
setIsHandlingUpload(false);
});
return (
<div className="dropzone-container">
{isDragAccept && (
<div className="dropzone-overlay is-drag-accept">
<Heading size="lg">Upload Image{overlaySecondaryText}</Heading>
</div>
)}
{isDragReject && (
<div className="dropzone-overlay is-drag-reject">
<Heading size="lg">Invalid Upload</Heading>
<Heading size="md">Must be single JPEG or PNG image</Heading>
</div>
)}
</div>
);
};
export default ImageUploadOverlay;

View File

@ -0,0 +1,74 @@
.dropzone-container {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
backdrop-filter: blur(20px);
.dropzone-overlay {
opacity: 0.5;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
row-gap: 1rem;
align-items: center;
justify-content: center;
background-color: var(--background-color);
&.is-drag-accept {
box-shadow: inset 0 0 20rem 1rem var(--accent-color);
}
&.is-drag-reject {
box-shadow: inset 0 0 20rem 1rem var(--status-bad-color);
}
&.is-handling-upload {
box-shadow: inset 0 0 20rem 1rem var(--status-working-color);
}
}
}
.image-uploader-button-outer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 0.5rem;
color: var(--tab-list-text-inactive);
background-color: var(--background-color);
&:hover {
background-color: var(--background-color-light);
}
}
.image-upload-button-inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.image-upload-button {
display: flex;
flex-direction: column;
row-gap: 2rem;
align-items: center;
justify-content: center;
text-align: center;
svg {
width: 4rem;
height: 4rem;
}
h2 {
font-size: 1.2rem;
}
}

View File

@ -0,0 +1,164 @@
import { useToast } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader';
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
import { tabDict } from 'features/ui/components/InvokeTabs';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import {
KeyboardEvent,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import ImageUploadOverlay from './ImageUploadOverlay';
type ImageUploaderProps = {
children: ReactNode;
};
const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props;
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const toast = useToast({});
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const { setOpenUploader } = useImageUploader();
const fileRejectionCallback = useCallback(
(rejection: FileRejection) => {
setIsHandlingUpload(true);
const msg = rejection.errors.reduce(
(acc: string, cur: { message: string }) => `${acc}\n${cur.message}`,
''
);
toast({
title: t('toast.uploadFailed'),
description: msg,
status: 'error',
isClosable: true,
});
},
[t, toast]
);
const fileAcceptedCallback = useCallback(
async (file: File) => {
dispatch(uploadImage({ imageFile: file }));
},
[dispatch]
);
const onDrop = useCallback(
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
fileRejections.forEach((rejection: FileRejection) => {
fileRejectionCallback(rejection);
});
acceptedFiles.forEach((file: File) => {
fileAcceptedCallback(file);
});
},
[fileAcceptedCallback, fileRejectionCallback]
);
const {
getRootProps,
getInputProps,
isDragAccept,
isDragReject,
isDragActive,
open,
} = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true,
onDrop,
onDragOver: () => setIsHandlingUpload(true),
maxFiles: 1,
});
setOpenUploader(open);
useEffect(() => {
const pasteImageListener = (e: ClipboardEvent) => {
const dataTransferItemList = e.clipboardData?.items;
if (!dataTransferItemList) return;
const imageItems: Array<DataTransferItem> = [];
for (const item of dataTransferItemList) {
if (
item.kind === 'file' &&
['image/png', 'image/jpg'].includes(item.type)
) {
imageItems.push(item);
}
}
if (!imageItems.length) return;
e.stopImmediatePropagation();
if (imageItems.length > 1) {
toast({
description: t('toast.uploadFailedMultipleImagesDesc'),
status: 'error',
isClosable: true,
});
return;
}
const file = imageItems[0].getAsFile();
if (!file) {
toast({
description: t('toast.uploadFailedUnableToLoadDesc'),
status: 'error',
isClosable: true,
});
return;
}
dispatch(uploadImage({ imageFile: file }));
};
document.addEventListener('paste', pasteImageListener);
return () => {
document.removeEventListener('paste', pasteImageListener);
};
}, [t, dispatch, toast, activeTabName]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
activeTabName
)
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
: ``;
return (
<ImageUploaderTriggerContext.Provider value={open}>
<div
{...getRootProps({ style: {} })}
onKeyDown={(e: KeyboardEvent) => {
// Bail out if user hits spacebar - do not open the uploader
if (e.key === ' ') return;
}}
>
<input {...getInputProps()} />
{children}
{isDragActive && isHandlingUpload && (
<ImageUploadOverlay
isDragAccept={isDragAccept}
isDragReject={isDragReject}
overlaySecondaryText={overlaySecondaryText}
setIsHandlingUpload={setIsHandlingUpload}
/>
)}
</div>
</ImageUploaderTriggerContext.Provider>
);
};
export default ImageUploader;

View File

@ -0,0 +1,31 @@
import { Heading } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa';
type ImageUploaderButtonProps = {
styleClass?: string;
};
const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
const { styleClass } = props;
const open = useContext(ImageUploaderTriggerContext);
const handleClickUpload = () => {
open && open();
};
return (
<div
className={`image-uploader-button-outer ${styleClass}`}
onClick={handleClickUpload}
>
<div className="image-upload-button">
<FaUpload />
<Heading size="lg">Click or Drag and Drop</Heading>
</div>
</div>
);
};
export default ImageUploaderButton;

View File

@ -0,0 +1,19 @@
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa';
import IAIIconButton from './IAIIconButton';
const ImageUploaderIconButton = () => {
const openImageUploader = useContext(ImageUploaderTriggerContext);
return (
<IAIIconButton
aria-label="Upload Image"
tooltip="Upload Image"
icon={<FaUpload />}
onClick={openImageUploader || undefined}
/>
);
};
export default ImageUploaderIconButton;

View File

@ -0,0 +1,55 @@
import { Box } from '@chakra-ui/react';
interface SubItemHookProps {
active?: boolean;
width?: string | number;
height?: string | number;
side?: 'left' | 'right';
}
export default function SubItemHook(props: SubItemHookProps) {
const {
active = true,
width = '1rem',
height = '1.3rem',
side = 'right',
} = props;
return (
<>
{side === 'right' && (
<Box
width={width}
height={height}
margin="-0.5rem 0.5rem 0 0.5rem"
borderLeft={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
borderBottom={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
/>
)}
{side === 'left' && (
<Box
width={width}
height={height}
margin="-0.5rem 0.5rem 0 0.5rem"
borderRight={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
borderBottom={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
/>
)}
</>
);
}

View File

@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';
export default function NodesWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>{t('common.nodes')}</h1>
<p>{t('common.nodesDesc')}</p>
</div>
);
}

View File

@ -0,0 +1,13 @@
import { useTranslation } from 'react-i18next';
export const PostProcessingWIP = () => {
const { t } = useTranslation();
return (
<div className="work-in-progress post-processing-work-in-progress">
<h1>{t('common.postProcessing')}</h1>
<p>{t('common.postProcessDesc1')}</p>
<p>{t('common.postProcessDesc2')}</p>
<p>{t('common.postProcessDesc3')}</p>
</div>
);
};

View File

@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next';
export default function TrainingWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>{t('common.training')}</h1>
<p>
{t('common.trainingDesc1')}
<br />
<br />
{t('common.trainingDesc2')}
</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

@ -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);
}
}
}
}

View File

@ -0,0 +1,44 @@
import { Tooltip } from '@chakra-ui/react';
import * as Slider from '@radix-ui/react-slider';
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;

View File

@ -0,0 +1,8 @@
.invokeai__tooltip-content {
padding: 0.5rem;
background-color: grey;
border-radius: 0.25rem;
.invokeai__tooltip-arrow {
background-color: grey;
}
}

View File

@ -0,0 +1,40 @@
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) => {
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;

View File

@ -0,0 +1,35 @@
import { RefObject, useEffect } from 'react';
const watchers: {
ref: RefObject<HTMLElement>;
enable: boolean;
callback: () => void;
}[] = [];
const useClickOutsideWatcher = () => {
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
watchers.forEach(({ ref, enable, callback }) => {
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
callback();
}
});
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return {
addWatcher: (watcher: {
ref: RefObject<HTMLElement>;
callback: () => void;
enable: boolean;
}) => {
watchers.push(watcher);
},
};
};
export default useClickOutsideWatcher;

View File

@ -0,0 +1,14 @@
let openFunction: () => void;
const useImageUploader = () => {
return {
setOpenUploader: (open?: () => void) => {
if (open) {
openFunction = open;
}
},
openUploader: openFunction,
};
};
export default useImageUploader;

View File

@ -0,0 +1,28 @@
// https://stackoverflow.com/a/73731908
import { useEffect, useState } from 'react';
export function useSingleAndDoubleClick(
handleSingleClick: () => void,
handleDoubleClick: () => void,
delay = 250
) {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
if (click === 1) {
handleSingleClick();
}
setClick(0);
}, delay);
if (click === 2) {
handleDoubleClick();
}
return () => clearTimeout(timer);
}, [click, handleSingleClick, handleDoubleClick, delay]);
return () => setClick((prev) => prev + 1);
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function useUpdateTranslations(fn: () => void) {
const { i18n } = useTranslation();
const currentLang = localStorage.getItem('i18nextLng');
React.useEffect(() => {
fn();
}, [fn]);
React.useEffect(() => {
i18n.on('languageChanged', () => {
fn();
});
}, [fn, i18n, currentLang]);
}

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,16 @@
import { createIcon } from '@chakra-ui/react';
const TrainingIcon = createIcon({
displayName: 'TrainingIcon',
viewBox: '0 0 3544 3544',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"
/>
),
});
export default TrainingIcon;

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

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

@ -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 3544 3544" 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="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,31 @@
import * as InvokeAI from 'app/invokeai';
import promptToString from './promptToString';
export function getPromptAndNegative(inputPrompt: InvokeAI.Prompt) {
let prompt: string =
typeof inputPrompt === 'string' ? inputPrompt : promptToString(inputPrompt);
let negativePrompt = '';
// Matches all negative prompts, 1st capturing group is the prompt itself
const negativePromptRegExp = new RegExp(/\[([^\][]*)]/, 'gi');
// Grab the actual prompt matches (capturing group 1 is 1st index of match)
const negativePromptMatches = [...prompt.matchAll(negativePromptRegExp)].map(
(match) => match[1]
);
if (negativePromptMatches.length) {
// Build the negative prompt itself
negativePrompt = negativePromptMatches.join(' ');
// Replace each match, including its surrounding brackets
// Remove each pair of empty brackets
// Trim whitespace
negativePromptMatches.forEach((match) => {
prompt = prompt.replace(`[${match}]`, '').replaceAll('[]', '').trim();
});
}
return [prompt, negativePrompt];
}

View File

@ -0,0 +1,21 @@
type Base64AndCaption = {
base64: string;
caption: string;
};
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
const w = window.open('');
if (!w) return;
images.forEach((i) => {
const image = new Image();
image.src = i.base64;
w.document.write(i.caption);
w.document.write('</br>');
w.document.write(image.outerHTML);
w.document.write('</br></br>');
});
};
export default openBase64ImageInTab;

View File

@ -0,0 +1,336 @@
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { Dimensions } from 'features/canvas/store/canvasTypes';
import { GenerationState } from 'features/parameters/store/generationSlice';
import { SystemState } from 'features/system/store/systemSlice';
import { Vector2d } from 'konva/lib/types';
import {
CanvasState,
isCanvasMaskLine,
} from 'features/canvas/store/canvasTypes';
import generateMask from 'features/canvas/util/generateMask';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import type {
FacetoolType,
UpscalingLevel,
} from 'features/parameters/store/postprocessingSlice';
import { PostprocessingState } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import openBase64ImageInTab from './openBase64ImageInTab';
import randomInt from './randomInt';
import { stringToSeedWeightsArray } from './seedWeightPairs';
export type FrontendToBackendParametersConfig = {
generationMode: InvokeTabName;
generationState: GenerationState;
postprocessingState: PostprocessingState;
canvasState: CanvasState;
systemState: SystemState;
imageToProcessUrl?: string;
};
export type BackendGenerationParameters = {
prompt: string;
iterations: number;
steps: number;
cfg_scale: number;
threshold: number;
perlin: number;
height: number;
width: number;
sampler_name: string;
seed: number;
progress_images: boolean;
progress_latents: boolean;
save_intermediates: number;
generation_mode: InvokeTabName;
init_mask: string;
init_img?: string;
fit?: boolean;
seam_size?: number;
seam_blur?: number;
seam_strength?: number;
seam_steps?: number;
tile_size?: number;
infill_method?: string;
force_outpaint?: boolean;
seamless?: boolean;
hires_fix?: boolean;
strength?: number;
invert_mask?: boolean;
inpaint_replace?: number;
bounding_box?: Vector2d & Dimensions;
inpaint_width?: number;
inpaint_height?: number;
with_variations?: Array<Array<number>>;
variation_amount?: number;
enable_image_debugging?: boolean;
h_symmetry_time_pct?: number;
v_symmetry_time_pct?: number;
};
export type BackendEsrGanParameters = {
level: UpscalingLevel;
denoise_str: number;
strength: number;
};
export type BackendFacetoolParameters = {
type: FacetoolType;
strength: number;
codeformer_fidelity?: number;
};
export type BackendParameters = {
generationParameters: BackendGenerationParameters;
esrganParameters: false | BackendEsrGanParameters;
facetoolParameters: false | BackendFacetoolParameters;
};
/**
* Translates/formats frontend state into parameters suitable
* for consumption by the API.
*/
export const frontendToBackendParameters = (
config: FrontendToBackendParametersConfig
): BackendParameters => {
const canvasBaseLayer = getCanvasBaseLayer();
const {
generationMode,
generationState,
postprocessingState,
canvasState,
systemState,
} = config;
const {
codeformerFidelity,
facetoolStrength,
facetoolType,
hiresFix,
hiresStrength,
shouldRunESRGAN,
shouldRunFacetool,
upscalingLevel,
upscalingStrength,
upscalingDenoising,
} = postprocessingState;
const {
cfgScale,
height,
img2imgStrength,
infillMethod,
initialImage,
iterations,
perlin,
prompt,
negativePrompt,
sampler,
seamBlur,
seamless,
seamSize,
seamSteps,
seamStrength,
seed,
seedWeights,
shouldFitToWidthHeight,
shouldGenerateVariations,
shouldRandomizeSeed,
steps,
threshold,
tileSize,
variationAmount,
width,
shouldUseSymmetry,
horizontalSymmetryTimePercentage,
verticalSymmetryTimePercentage,
} = generationState;
const {
shouldDisplayInProgressType,
saveIntermediatesInterval,
enableImageDebugging,
} = systemState;
const generationParameters: BackendGenerationParameters = {
prompt,
iterations,
steps,
cfg_scale: cfgScale,
threshold,
perlin,
height,
width,
sampler_name: sampler,
seed,
progress_images: shouldDisplayInProgressType === 'full-res',
progress_latents: shouldDisplayInProgressType === 'latents',
save_intermediates: saveIntermediatesInterval,
generation_mode: generationMode,
init_mask: '',
};
let esrganParameters: false | BackendEsrGanParameters = false;
let facetoolParameters: false | BackendFacetoolParameters = false;
if (negativePrompt !== '') {
generationParameters.prompt = `${prompt} [${negativePrompt}]`;
}
generationParameters.seed = shouldRandomizeSeed
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
: seed;
// Symmetry Settings
if (shouldUseSymmetry) {
if (horizontalSymmetryTimePercentage > 0) {
generationParameters.h_symmetry_time_pct = Math.max(
0,
Math.min(1, horizontalSymmetryTimePercentage / steps)
);
}
if (horizontalSymmetryTimePercentage > 0) {
generationParameters.v_symmetry_time_pct = Math.max(
0,
Math.min(1, verticalSymmetryTimePercentage / steps)
);
}
}
// txt2img exclusive parameters
if (generationMode === 'txt2img') {
generationParameters.hires_fix = hiresFix;
if (hiresFix) generationParameters.strength = hiresStrength;
}
// parameters common to txt2img and img2img
if (['txt2img', 'img2img'].includes(generationMode)) {
generationParameters.seamless = seamless;
if (shouldRunESRGAN) {
esrganParameters = {
level: upscalingLevel,
denoise_str: upscalingDenoising,
strength: upscalingStrength,
};
}
if (shouldRunFacetool) {
facetoolParameters = {
type: facetoolType,
strength: facetoolStrength,
};
if (facetoolType === 'codeformer') {
facetoolParameters.codeformer_fidelity = codeformerFidelity;
}
}
}
// img2img exclusive parameters
if (generationMode === 'img2img' && initialImage) {
generationParameters.init_img =
typeof initialImage === 'string' ? initialImage : initialImage.url;
generationParameters.strength = img2imgStrength;
generationParameters.fit = shouldFitToWidthHeight;
}
// inpainting exclusive parameters
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
const {
layerState: { objects },
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
isMaskEnabled,
shouldPreserveMaskedArea,
boundingBoxScaleMethod: boundingBoxScale,
scaledBoundingBoxDimensions,
} = canvasState;
const boundingBox = {
...boundingBoxCoordinates,
...boundingBoxDimensions,
};
const maskDataURL = generateMask(
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
boundingBox
);
generationParameters.init_mask = maskDataURL;
generationParameters.fit = false;
generationParameters.strength = img2imgStrength;
generationParameters.invert_mask = shouldPreserveMaskedArea;
generationParameters.bounding_box = boundingBox;
const tempScale = canvasBaseLayer.scale();
canvasBaseLayer.scale({
x: 1 / stageScale,
y: 1 / stageScale,
});
const absPos = canvasBaseLayer.getAbsolutePosition();
const imageDataURL = canvasBaseLayer.toDataURL({
x: boundingBox.x + absPos.x,
y: boundingBox.y + absPos.y,
width: boundingBox.width,
height: boundingBox.height,
});
if (enableImageDebugging) {
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask sent as init_mask' },
{ base64: imageDataURL, caption: 'image sent as init_img' },
]);
}
canvasBaseLayer.scale(tempScale);
generationParameters.init_img = imageDataURL;
generationParameters.progress_images = false;
if (boundingBoxScale !== 'none') {
generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
}
generationParameters.seam_size = seamSize;
generationParameters.seam_blur = seamBlur;
generationParameters.seam_strength = seamStrength;
generationParameters.seam_steps = seamSteps;
generationParameters.tile_size = tileSize;
generationParameters.infill_method = infillMethod;
generationParameters.force_outpaint = false;
}
if (shouldGenerateVariations) {
generationParameters.variation_amount = variationAmount;
if (seedWeights) {
generationParameters.with_variations =
stringToSeedWeightsArray(seedWeights);
}
} else {
generationParameters.variation_amount = 0;
}
if (enableImageDebugging) {
generationParameters.enable_image_debugging = enableImageDebugging;
}
return {
generationParameters,
esrganParameters,
facetoolParameters,
};
};

View File

@ -0,0 +1,20 @@
import * as InvokeAI from 'app/invokeai';
const promptToString = (prompt: InvokeAI.Prompt): string => {
if (typeof prompt === 'string') {
return prompt;
}
if (prompt.length === 1) {
return prompt[0].prompt;
}
return prompt
.map(
(promptItem: InvokeAI.PromptItem): string =>
`${promptItem.prompt}:${promptItem.weight}`
)
.join(' ');
};
export default promptToString;

View File

@ -0,0 +1,5 @@
const randomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export default randomInt;

View File

@ -0,0 +1,7 @@
export const roundDownToMultiple = (num: number, multiple: number): number => {
return Math.floor(num / multiple) * multiple;
};
export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple;
};

View File

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

View File

@ -0,0 +1,31 @@
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIAlertDialog from 'common/components/IAIAlertDialog';
import IAIButton from 'common/components/IAIButton';
import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { isStagingSelector } from '../store/canvasSelectors';
const ClearCanvasHistoryButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<IAIAlertDialog
title={t('unifiedCanvas.clearCanvasHistory')}
acceptCallback={() => dispatch(clearCanvasHistory())}
acceptButtonText={t('unifiedCanvas.clearHistory')}
triggerComponent={
<IAIButton size="sm" leftIcon={<FaTrash />} isDisabled={isStaging}>
{t('unifiedCanvas.clearCanvasHistory')}
</IAIButton>
}
>
<p>{t('unifiedCanvas.clearCanvasHistoryMessage')}</p>
<br />
<p>{t('unifiedCanvas.clearCanvasHistoryConfirm')}</p>
</IAIAlertDialog>
);
};
export default ClearCanvasHistoryButtonModal;

View File

@ -0,0 +1,208 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import {
canvasSelector,
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node';
import { Vector2d } from 'konva/lib/types';
import { isEqual } from 'lodash';
import { useCallback, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
import useCanvasDragMove from '../hooks/useCanvasDragMove';
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
import useCanvasMouseDown from '../hooks/useCanvasMouseDown';
import useCanvasMouseMove from '../hooks/useCanvasMouseMove';
import useCanvasMouseOut from '../hooks/useCanvasMouseOut';
import useCanvasMouseUp from '../hooks/useCanvasMouseUp';
import useCanvasWheel from '../hooks/useCanvasZoom';
import {
setCanvasBaseLayer,
setCanvasStage,
} from '../util/konvaInstanceProvider';
import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay';
import IAICanvasGrid from './IAICanvasGrid';
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
import IAICanvasMaskLines from './IAICanvasMaskLines';
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
import IAICanvasStagingArea from './IAICanvasStagingArea';
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import IAICanvasToolPreview from './IAICanvasToolPreview';
const selector = createSelector(
[canvasSelector, isStagingSelector],
(canvas, isStaging) => {
const {
isMaskEnabled,
stageScale,
shouldShowBoundingBox,
isTransformingBoundingBox,
isMouseOverBoundingBox,
isMovingBoundingBox,
stageDimensions,
stageCoordinates,
tool,
isMovingStage,
shouldShowIntermediates,
shouldShowGrid,
shouldRestrictStrokesToBox,
} = canvas;
let stageCursor: string | undefined = 'none';
if (tool === 'move' || isStaging) {
if (isMovingStage) {
stageCursor = 'grabbing';
} else {
stageCursor = 'grab';
}
} else if (isTransformingBoundingBox) {
stageCursor = undefined;
} else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) {
stageCursor = 'default';
}
return {
isMaskEnabled,
isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox,
shouldShowBoundingBox,
shouldShowGrid,
stageCoordinates,
stageCursor,
stageDimensions,
stageScale,
tool,
isStaging,
shouldShowIntermediates,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const IAICanvas = () => {
const {
isMaskEnabled,
isModifyingBoundingBox,
shouldShowBoundingBox,
shouldShowGrid,
stageCoordinates,
stageCursor,
stageDimensions,
stageScale,
tool,
isStaging,
shouldShowIntermediates,
} = useAppSelector(selector);
useCanvasHotkeys();
const stageRef = useRef<Konva.Stage | null>(null);
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
setCanvasStage(el as Konva.Stage);
stageRef.current = el;
}, []);
const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => {
setCanvasBaseLayer(el as Konva.Layer);
canvasBaseLayerRef.current = el;
}, []);
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
// Use refs for values that do not affect rendering, other values in redux
const didMouseMoveRef = useRef<boolean>(false);
const handleWheel = useCanvasWheel(stageRef);
const handleMouseDown = useCanvasMouseDown(stageRef);
const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef);
const handleMouseMove = useCanvasMouseMove(
stageRef,
didMouseMoveRef,
lastCursorPositionRef
);
const handleMouseOut = useCanvasMouseOut();
const { handleDragStart, handleDragMove, handleDragEnd } =
useCanvasDragMove();
return (
<div className="inpainting-canvas-container">
<div className="inpainting-canvas-wrapper">
<Stage
tabIndex={-1}
ref={canvasStageRefCallback}
className="inpainting-canvas-stage"
style={{
...(stageCursor ? { cursor: stageCursor } : {}),
}}
x={stageCoordinates.x}
y={stageCoordinates.y}
width={stageDimensions.width}
height={stageDimensions.height}
scale={{ x: stageScale, y: stageScale }}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onMouseLeave={handleMouseOut}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
onContextMenu={(e: KonvaEventObject<MouseEvent>) =>
e.evt.preventDefault()
}
onWheel={handleWheel}
draggable={(tool === 'move' || isStaging) && !isModifyingBoundingBox}
>
<Layer id="grid" visible={shouldShowGrid}>
<IAICanvasGrid />
</Layer>
<Layer
id="base"
ref={canvasBaseLayerRefCallback}
listening={false}
imageSmoothingEnabled={false}
>
<IAICanvasObjectRenderer />
</Layer>
<Layer id="mask" visible={isMaskEnabled} listening={false}>
<IAICanvasMaskLines visible={true} listening={false} />
<IAICanvasMaskCompositer listening={false} />
</Layer>
<Layer>
<IAICanvasBoundingBoxOverlay />
</Layer>
<Layer id="preview" imageSmoothingEnabled={false}>
{!isStaging && (
<IAICanvasToolPreview
visible={tool !== 'move'}
listening={false}
/>
)}
<IAICanvasStagingArea visible={isStaging} />
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
<IAICanvasBoundingBox
visible={shouldShowBoundingBox && !isStaging}
/>
</Layer>
</Stage>
<IAICanvasStatusText />
<IAICanvasStagingAreaToolbar />
</div>
</div>
);
};
export default IAICanvas;

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