feat(ui): toggleable negative prompt

This commit is contained in:
psychedelicious
2025-06-16 17:03:19 +10:00
parent 450a0bf142
commit 0f1a69a0c3
13 changed files with 94 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import {
selectIsChatGTP4o,
selectIsChatGPT4o,
selectIsCogView4,
selectIsFluxKontext,
selectIsImagen3,
@@ -17,8 +17,8 @@ export const useIsEntityTypeEnabled = (entityType: CanvasEntityType) => {
const isCogView4 = useAppSelector(selectIsCogView4);
const isImagen3 = useAppSelector(selectIsImagen3);
const isImagen4 = useAppSelector(selectIsImagen4);
const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
const isEntityTypeEnabled = useMemo<boolean>(() => {
switch (entityType) {

View File

@@ -14,6 +14,8 @@ import type {
ParameterControlLoRAModel,
ParameterGuidance,
ParameterModel,
ParameterNegativePrompt,
ParameterPositivePrompt,
ParameterPrecision,
ParameterScheduler,
ParameterSDXLRefinerModel,
@@ -124,10 +126,10 @@ export const paramsSlice = createSlice({
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
state.shouldUseCpuNoise = action.payload;
},
positivePromptChanged: (state, action: PayloadAction<string>) => {
positivePromptChanged: (state, action: PayloadAction<ParameterPositivePrompt>) => {
state.positivePrompt = action.payload;
},
negativePromptChanged: (state, action: PayloadAction<string>) => {
negativePromptChanged: (state, action: PayloadAction<ParameterNegativePrompt>) => {
state.negativePrompt = action.payload;
},
positivePrompt2Changed: (state, action: PayloadAction<string>) => {
@@ -273,8 +275,8 @@ export const selectIsSD3 = createParamsSelector((params) => params.model?.base =
export const selectIsCogView4 = createParamsSelector((params) => params.model?.base === 'cogview4');
export const selectIsImagen3 = createParamsSelector((params) => params.model?.base === 'imagen3');
export const selectIsImagen4 = createParamsSelector((params) => params.model?.base === 'imagen4');
export const selectIsChatGTP4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o');
export const selectIsFluxKontext = createParamsSelector((params) => params.model?.base === 'flux-kontext');
export const selectIsChatGPT4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o');
export const selectModel = createParamsSelector((params) => params.model);
export const selectModelKey = createParamsSelector((params) => params.model?.key);
@@ -306,6 +308,12 @@ export const selectImg2imgStrength = createParamsSelector((params) => params.img
export const selectOptimizedDenoisingEnabled = createParamsSelector((params) => params.optimizedDenoisingEnabled);
export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt);
export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt);
export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? '');
export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null);
export const selectModelSupportsNegativePrompt = createSelector(
[selectIsFLUX, selectIsChatGPT4o],
(isFLUX, isChatGPT4o) => !isFLUX && !isChatGPT4o
);
export const selectPositivePrompt2 = createParamsSelector((params) => params.positivePrompt2);
export const selectNegativePrompt2 = createParamsSelector((params) => params.negativePrompt2);
export const selectShouldConcatPrompts = createParamsSelector((params) => params.shouldConcatPrompts);

View File

@@ -522,7 +522,8 @@ const zParamsState = z.object({
clipSkip: z.number().default(0),
shouldUseCpuNoise: z.boolean().default(true),
positivePrompt: zParameterPositivePrompt.default(''),
negativePrompt: zParameterNegativePrompt.default(''),
// Negative prompt may be disabled, in which case it will be null
negativePrompt: zParameterNegativePrompt.default(null),
positivePrompt2: zParameterPositiveStylePromptSDXL.default(''),
negativePrompt2: zParameterNegativeStylePromptSDXL.default(''),
shouldConcatPrompts: z.boolean().default(true),

View File

@@ -121,8 +121,8 @@ export const useImageActions = (imageDTO: ImageDTO) => {
if (!metadata) {
return;
}
let positivePrompt;
let negativePrompt;
let positivePrompt: string;
let negativePrompt: string;
try {
positivePrompt = await handlers.positivePrompt.parse(metadata);
@@ -130,7 +130,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
positivePrompt = '';
}
try {
negativePrompt = await handlers.negativePrompt.parse(metadata);
negativePrompt = (await handlers.negativePrompt.parse(metadata)) ?? '';
} catch (error) {
negativePrompt = '';
}

View File

@@ -56,7 +56,8 @@ export const selectPresetModifiedPrompts = createSelector(
selectStylePresetSlice,
selectListStylePresetsRequestState,
(params, stylePresetSlice, listStylePresetsRequestState) => {
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = params;
const negativePrompt = params.negativePrompt ?? '';
const { positivePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = params;
const { activeStylePresetId } = stylePresetSlice;
if (activeStylePresetId) {
@@ -72,7 +73,7 @@ export const selectPresetModifiedPrompts = createSelector(
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(
activeStylePreset.preset_data.negative_prompt,
negativePrompt
negativePrompt ?? ''
);
return {

View File

@@ -4,7 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectIsChatGTP4o,
selectIsChatGPT4o,
selectIsFluxKontext,
selectIsImagen3,
selectIsImagen4,
@@ -28,7 +28,7 @@ export const BboxAspectRatioSelect = memo(() => {
const id = useAppSelector(selectAspectRatioID);
const isStaging = useAppSelector(selectIsStaging);
const isImagen3 = useAppSelector(selectIsImagen3);
const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
const isImagen4 = useAppSelector(selectIsImagen4);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
const options = useMemo(() => {

View File

@@ -0,0 +1,42 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusMinusBold } from 'react-icons/pi';
export const NegativePromptToggleButton = memo(() => {
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const onClick = useCallback(() => {
if (hasNegativePrompt) {
dispatch(negativePromptChanged(null));
} else {
dispatch(negativePromptChanged(''));
}
}, [dispatch, hasNegativePrompt]);
const label = useMemo(
() => (hasNegativePrompt ? 'Remove Negative Prompt' : 'Add Negative Prompt'),
[hasNegativePrompt]
);
return (
<Tooltip label={label}>
<IconButton
aria-label={label}
onClick={onClick}
icon={<PiPlusMinusBold size={14} />}
variant="promptOverlay"
fontSize={12}
px={0.5}
colorScheme={hasNegativePrompt ? 'invokeBlue' : 'base'}
/>
</Tooltip>
);
});
NegativePromptToggleButton.displayName = 'NegativePromptToggleButton';

View File

@@ -1,7 +1,10 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
import { negativePromptChanged, selectNegativePrompt } from 'features/controlLayers/store/paramsSlice';
import {
negativePromptChanged,
selectNegativePromptWithFallback,
} from 'features/controlLayers/store/paramsSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
@@ -23,7 +26,7 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch();
const prompt = useAppSelector(selectNegativePrompt);
const prompt = useAppSelector(selectNegativePromptWithFallback);
const viewMode = useAppSelector(selectStylePresetViewMode);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);

View File

@@ -1,8 +1,14 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import {
positivePromptChanged,
selectBase,
selectModelSupportsNegativePrompt,
selectPositivePrompt,
} from 'features/controlLayers/store/paramsSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { NegativePromptToggleButton } from 'features/parameters/components/Core/NegativePromptToggleButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
@@ -32,6 +38,7 @@ export const ParamPositivePrompt = memo(() => {
const baseModel = useAppSelector(selectBase);
const viewMode = useAppSelector(selectStylePresetViewMode);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
const textareaRef = useRef<HTMLTextAreaElement>(null);
usePersistedTextAreaSize('positive_prompt', textareaRef, persistOptions);
@@ -98,8 +105,9 @@ export const ParamPositivePrompt = memo(() => {
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
{baseModel === 'sdxl' && <SDXLConcatButton />}
<ShowDynamicPromptsPreviewButton />
{modelSupportsNegativePrompt && <NegativePromptToggleButton />}
</PromptOverlayButtonWrapper>
<PromptLabel label={t('parameters.positivePromptPlaceholder')} />
<PromptLabel label="Prompt" />
{viewMode && (
<ViewModePrompt
prompt={prompt}

View File

@@ -1,7 +1,11 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { RefImageList } from 'features/controlLayers/components/RefImage/RefImageList';
import { createParamsSelector, selectIsChatGTP4o, selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import {
createParamsSelector,
selectHasNegativePrompt,
selectModelSupportsNegativePrompt,
} from 'features/controlLayers/store/paramsSlice';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
@@ -16,15 +20,15 @@ const selectWithStylePrompts = createParamsSelector((params) => {
export const Prompts = memo(() => {
const withStylePrompts = useAppSelector(selectWithStylePrompts);
const isFLUX = useAppSelector(selectIsFLUX);
const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
return (
<Flex flexDir="column" gap={2}>
<ParamPositivePrompt />
{withStylePrompts && <ParamSDXLPositiveStylePrompt />}
<RefImageList />
{!isFLUX && !isChatGPT4o && <ParamNegativePrompt />}
{modelSupportsNegativePrompt && hasNegativePrompt && <ParamNegativePrompt />}
{withStylePrompts && <ParamSDXLNegativeStylePrompt />}
<RefImageList />
</Flex>
);
});

View File

@@ -1,6 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import {
selectIsChatGTP4o,
selectIsChatGPT4o,
selectIsFluxKontext,
selectIsImagen3,
selectIsImagen4,
@@ -9,8 +9,8 @@ import {
export const useIsApiModel = () => {
const isImagen3 = useAppSelector(selectIsImagen3);
const isImagen4 = useAppSelector(selectIsImagen4);
const isChatGPT4o = useAppSelector(selectIsChatGTP4o);
const isFluxKontext = useAppSelector(selectIsFluxKontext);
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
return isImagen3 || isImagen4 || isChatGPT4o || isFluxKontext;
};

View File

@@ -29,7 +29,7 @@ export type ParameterPositivePrompt = z.infer<typeof zParameterPositivePrompt>;
// #endregion
// #region Negative prompt
export const [zParameterNegativePrompt, isParameterNegativePrompt] = buildParameter(z.string());
export const [zParameterNegativePrompt, isParameterNegativePrompt] = buildParameter(z.string().nullable());
export type ParameterNegativePrompt = z.infer<typeof zParameterNegativePrompt>;
// #endregion

View File

@@ -1,5 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { selectNegativePrompt, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { selectNegativePromptWithFallback, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { selectStylePresetActivePresetId } from 'features/stylePresets/store/stylePresetSlice';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
@@ -13,7 +13,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
export const usePresetModifiedPrompts = () => {
const positivePrompt = useAppSelector(selectPositivePrompt);
const negativePrompt = useAppSelector(selectNegativePrompt);
const negativePrompt = useAppSelector(selectNegativePromptWithFallback);
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {