diff --git a/invokeai/frontend/public/locales/options/en.json b/invokeai/frontend/public/locales/options/en.json
index cc38efc0b7..32cadf1998 100644
--- a/invokeai/frontend/public/locales/options/en.json
+++ b/invokeai/frontend/public/locales/options/en.json
@@ -43,6 +43,7 @@
"invoke": "Invoke",
"cancel": "Cancel",
"promptPlaceholder": "Type prompt here. [negative tokens], (upweight)++, (downweight)--, swap and blend are available (see docs)",
+ "negativePrompts": "Negative Prompts",
"sendTo": "Send to",
"sendToImg2Img": "Send to Image to Image",
"sendToUnifiedCanvas": "Send To Unified Canvas",
diff --git a/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts b/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts
index 1f20c06302..3dd4c7cfc7 100644
--- a/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts
+++ b/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts
@@ -11,7 +11,6 @@ const useClickOutsideWatcher = () => {
function handleClickOutside(e: MouseEvent) {
watchers.forEach(({ ref, enable, callback }) => {
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
- console.log('callback');
callback();
}
});
diff --git a/invokeai/frontend/src/common/util/getPromptAndNegative.ts b/invokeai/frontend/src/common/util/getPromptAndNegative.ts
new file mode 100644
index 0000000000..8f47687929
--- /dev/null
+++ b/invokeai/frontend/src/common/util/getPromptAndNegative.ts
@@ -0,0 +1,20 @@
+import * as InvokeAI from 'app/invokeai';
+import promptToString from './promptToString';
+
+export function getPromptAndNegative(input_prompt: InvokeAI.Prompt) {
+ let prompt: string = promptToString(input_prompt);
+ let negativePrompt: string | null = null;
+
+ const negativePromptRegExp = new RegExp(/(?<=\[)[^\][]*(?=])/, 'gi');
+ const negativePromptMatches = [...prompt.matchAll(negativePromptRegExp)];
+
+ if (negativePromptMatches && negativePromptMatches.length > 0) {
+ negativePrompt = negativePromptMatches.join(', ');
+ prompt = prompt
+ .replaceAll(negativePromptRegExp, '')
+ .replaceAll('[]', '')
+ .trim();
+ }
+
+ return [prompt, negativePrompt];
+}
diff --git a/invokeai/frontend/src/common/util/parameterTranslation.ts b/invokeai/frontend/src/common/util/parameterTranslation.ts
index 02ae01fdba..2853b21b1d 100644
--- a/invokeai/frontend/src/common/util/parameterTranslation.ts
+++ b/invokeai/frontend/src/common/util/parameterTranslation.ts
@@ -106,6 +106,7 @@ export const frontendToBackendParameters = (
iterations,
perlin,
prompt,
+ negativePrompt,
sampler,
seamBlur,
seamless,
@@ -155,6 +156,10 @@ export const frontendToBackendParameters = (
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;
diff --git a/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx
index 6612bea8d2..6f078dabde 100644
--- a/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx
@@ -9,6 +9,7 @@ import {
setAllParameters,
setInitialImage,
setIsLightBoxOpen,
+ setNegativePrompt,
setPrompt,
setSeed,
setShouldShowImageDetails,
@@ -44,6 +45,7 @@ import { GalleryState } from 'features/gallery/store/gallerySlice';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import IAIPopover from 'common/components/IAIPopover';
import { useTranslation } from 'react-i18next';
+import { getPromptAndNegative } from 'common/util/getPromptAndNegative';
const systemSelector = createSelector(
[
@@ -241,9 +243,18 @@ const CurrentImageButtons = () => {
[currentImage]
);
- const handleClickUsePrompt = () =>
- currentImage?.metadata?.image?.prompt &&
- dispatch(setPrompt(currentImage.metadata.image.prompt));
+ const handleClickUsePrompt = () => {
+ if (currentImage?.metadata?.image?.prompt) {
+ const [prompt, negativePrompt] = getPromptAndNegative(
+ currentImage?.metadata?.image?.prompt
+ );
+
+ prompt && dispatch(setPrompt(prompt));
+ negativePrompt
+ ? dispatch(setNegativePrompt(negativePrompt))
+ : dispatch(setNegativePrompt(''));
+ }
+ };
useHotkeys(
'p',
diff --git a/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx
index aca85899d8..c06dbc515a 100644
--- a/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx
@@ -10,9 +10,10 @@ import { DragEvent, memo, useState } from 'react';
import {
setActiveTab,
setAllImageToImageParameters,
- setAllTextToImageParameters,
+ setAllParameters,
setInitialImage,
setIsLightBoxOpen,
+ setNegativePrompt,
setPrompt,
setSeed,
} from 'features/options/store/optionsSlice';
@@ -24,6 +25,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { hoverableImageSelector } from 'features/gallery/store/gallerySliceSelectors';
import { useTranslation } from 'react-i18next';
+import { getPromptAndNegative } from 'common/util/getPromptAndNegative';
interface HoverableImageProps {
image: InvokeAI.Image;
@@ -62,7 +64,17 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false);
const handleUsePrompt = () => {
- image.metadata && dispatch(setPrompt(image.metadata.image.prompt));
+ if (image.metadata) {
+ const [prompt, negativePrompt] = getPromptAndNegative(
+ image.metadata?.image?.prompt
+ );
+
+ prompt && dispatch(setPrompt(prompt));
+ negativePrompt
+ ? dispatch(setNegativePrompt(negativePrompt))
+ : dispatch(setNegativePrompt(''));
+ }
+
toast({
title: t('toast:promptSet'),
status: 'success',
@@ -115,7 +127,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
const handleUseAllParameters = () => {
- metadata && dispatch(setAllTextToImageParameters(metadata));
+ metadata && dispatch(setAllParameters(metadata));
toast({
title: t('toast:parametersSet'),
status: 'success',
diff --git a/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts b/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts
index 81aa6846c2..ad7de71d38 100644
--- a/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts
+++ b/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts
@@ -38,7 +38,6 @@ export const uploadImage =
});
const image = (await response.json()) as InvokeAI.ImageUploadResponse;
- console.log(image);
const newImage: InvokeAI.Image = {
uuid: uuidv4(),
category: 'user',
diff --git a/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx b/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx
new file mode 100644
index 0000000000..43acd85313
--- /dev/null
+++ b/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx
@@ -0,0 +1,38 @@
+import { FormControl, Textarea } from '@chakra-ui/react';
+import type { RootState } from 'app/store';
+import { useAppDispatch, useAppSelector } from 'app/storeHooks';
+import { setNegativePrompt } from 'features/options/store/optionsSlice';
+import { useTranslation } from 'react-i18next';
+
+export function NegativePromptInput() {
+ const negativePrompt = useAppSelector(
+ (state: RootState) => state.options.negativePrompt
+ );
+
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+
+ return (
+
+
+ );
+}
diff --git a/invokeai/frontend/src/features/options/store/optionsSlice.ts b/invokeai/frontend/src/features/options/store/optionsSlice.ts
index dd7bff87a0..b26f650695 100644
--- a/invokeai/frontend/src/features/options/store/optionsSlice.ts
+++ b/invokeai/frontend/src/features/options/store/optionsSlice.ts
@@ -5,6 +5,7 @@ import promptToString from 'common/util/promptToString';
import { seedWeightsToString } from 'common/util/seedWeightPairs';
import { FACETOOL_TYPES } from 'app/constants';
import { InvokeTabName, tabMap } from 'features/tabs/tabMap';
+import { getPromptAndNegative } from 'common/util/getPromptAndNegative';
export type UpscalingLevel = 2 | 4;
@@ -28,6 +29,7 @@ export interface OptionsState {
optionsPanelScrollPosition: number;
perlin: number;
prompt: string;
+ negativePrompt: string;
sampler: string;
seamBlur: number;
seamless: boolean;
@@ -77,6 +79,7 @@ const initialOptionsState: OptionsState = {
optionsPanelScrollPosition: 0,
perlin: 0,
prompt: '',
+ negativePrompt: '',
sampler: 'k_lms',
seamBlur: 16,
seamless: false,
@@ -123,6 +126,17 @@ export const optionsSlice = createSlice({
state.prompt = promptToString(newPrompt);
}
},
+ setNegativePrompt: (
+ state,
+ action: PayloadAction
+ ) => {
+ const newPrompt = action.payload;
+ if (typeof newPrompt === 'string') {
+ state.negativePrompt = newPrompt;
+ } else {
+ state.negativePrompt = promptToString(newPrompt);
+ }
+ },
setIterations: (state, action: PayloadAction) => {
state.iterations = action.payload;
},
@@ -307,7 +321,14 @@ export const optionsSlice = createSlice({
state.shouldRandomizeSeed = false;
}
- if (prompt) state.prompt = promptToString(prompt);
+ if (prompt) {
+ const [promptOnly, negativePrompt] = getPromptAndNegative(prompt);
+ if (promptOnly) state.prompt = promptOnly;
+ negativePrompt
+ ? (state.negativePrompt = negativePrompt)
+ : (state.negativePrompt = '');
+ }
+
if (sampler) state.sampler = sampler;
if (steps) state.steps = steps;
if (cfg_scale) state.cfgScale = cfg_scale;
@@ -448,6 +469,7 @@ export const {
setParameter,
setPerlin,
setPrompt,
+ setNegativePrompt,
setSampler,
setSeamBlur,
setSeamless,
diff --git a/invokeai/frontend/src/features/tabs/components/ImageToImage/ImageToImagePanel.tsx b/invokeai/frontend/src/features/tabs/components/ImageToImage/ImageToImagePanel.tsx
index cfe97c38dc..0e99c221af 100644
--- a/invokeai/frontend/src/features/tabs/components/ImageToImage/ImageToImagePanel.tsx
+++ b/invokeai/frontend/src/features/tabs/components/ImageToImage/ImageToImagePanel.tsx
@@ -19,6 +19,8 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { useTranslation } from 'react-i18next';
+import { Flex } from '@chakra-ui/react';
+import { NegativePromptInput } from 'features/options/components/PromptInput/NegativePromptInput';
export default function ImageToImagePanel() {
const { t } = useTranslation();
@@ -67,7 +69,10 @@ export default function ImageToImagePanel() {
return (
-
+
+
+
+
-
+
+
+
+
diff --git a/invokeai/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx b/invokeai/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx
index c0f8cef2cb..8ecadbc996 100644
--- a/invokeai/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx
+++ b/invokeai/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx
@@ -13,6 +13,8 @@ import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel';
import BoundingBoxSettings from 'features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings';
import InfillAndScalingOptions from 'features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions';
import { useTranslation } from 'react-i18next';
+import { Flex } from '@chakra-ui/react';
+import { NegativePromptInput } from 'features/options/components/PromptInput/NegativePromptInput';
export default function UnifiedCanvasPanel() {
const { t } = useTranslation();
@@ -48,7 +50,10 @@ export default function UnifiedCanvasPanel() {
return (
-
+
+
+
+