Merge branch 'main' into chainchompa/preselect-workflows

This commit is contained in:
chainchompa
2024-08-14 09:02:12 -04:00
committed by GitHub
96 changed files with 3012 additions and 1456 deletions

View File

@ -59,7 +59,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.20",
"@invoke-ai/ui-library": "^0.0.25",
"@invoke-ai/ui-library": "^0.0.29",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
@ -110,7 +110,6 @@
"zod-validation-error": "^3.3.1"
},
"peerDependencies": {
"@chakra-ui/react": "^2.8.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-toolbelt": "^9.6.0"

File diff suppressed because it is too large Load Diff

View File

@ -1689,6 +1689,42 @@
"missingUpscaleModel": "Missing upscale model",
"missingTileControlNetModel": "No valid tile ControlNet models installed"
},
"stylePresets": {
"active": "Active",
"choosePromptTemplate": "Choose Prompt Template",
"clearTemplateSelection": "Clear Template Selection",
"copyTemplate": "Copy Template",
"createPromptTemplate": "Create Prompt Template",
"defaultTemplates": "Default Templates",
"deleteImage": "Delete Image",
"deleteTemplate": "Delete Template",
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
"editTemplate": "Edit Template",
"flatten": "Flatten selected template into current prompt",
"insertPlaceholder": "Insert placeholder",
"myTemplates": "My Templates",
"name": "Name",
"negativePrompt": "Negative Prompt",
"noMatchingTemplates": "No matching templates",
"promptTemplatesDesc1": "Prompt templates add text to the prompts you write in the prompt box.",
"promptTemplatesDesc2": "Use the placeholder string <Pre>{{placeholder}}</Pre> to specify where your prompt should be included in the template.",
"promptTemplatesDesc3": "If you omit the placeholder, the template will be appended to the end of your prompt.",
"positivePrompt": "Positive Prompt",
"preview": "Preview",
"private": "Private",
"searchByName": "Search by name",
"shared": "Shared",
"sharedTemplates": "Shared Templates",
"templateDeleted": "Prompt template deleted",
"toggleViewMode": "Toggle View Mode",
"type": "Type",
"unableToDeleteTemplate": "Unable to delete prompt template",
"updatePromptTemplate": "Update Prompt Template",
"uploadImage": "Upload Image",
"useForTemplate": "Use For Prompt Template",
"viewList": "View Template List",
"viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box."
},
"upsell": {
"inviteTeammates": "Invite Teammates",
"professional": "Professional",

View File

@ -13,6 +13,7 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
@ -120,6 +121,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflow, destina
<DeleteImageModal />
<ChangeBoardModal />
<DynamicPromptsModal />
<StylePresetModal />
<PreselectedImage selectedImage={selectedImage} />
</ErrorBoundary>
);

View File

@ -11,6 +11,8 @@ import {
promptsChanged,
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { socketConnected } from 'services/events/actions';
@ -19,7 +21,8 @@ const matcher = isAnyOf(
combinatorialToggled,
maxPromptsChanged,
maxPromptsReset,
socketConnected
socketConnected,
activeStylePresetIdChanged
);
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
@ -28,7 +31,7 @@ export const addDynamicPromptsListener = (startAppListening: AppStartListening)
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
cancelActiveListeners();
const state = getState();
const { positivePrompt } = state.controlLayers.present;
const { positivePrompt } = getPresetModifiedPrompts(state);
const { maxPrompts } = state.dynamicPrompts;
if (state.config.disabledFeatures.includes('dynamicPrompting')) {

View File

@ -28,6 +28,7 @@ import { generationPersistConfig, generationSlice } from 'features/parameters/st
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { configSlice } from 'features/system/store/configSlice';
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
@ -69,6 +70,7 @@ const allReducers = {
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@ -114,6 +116,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {
@ -164,8 +167,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
reducer: rememberedRootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false,
serializableCheck: import.meta.env.MODE === 'development',
immutableCheck: import.meta.env.MODE === 'development',
})
.concat(api.middleware)
.concat(dynamicMiddlewares)

View File

@ -71,6 +71,7 @@ export type AppConfig = {
*/
maxUpscaleDimension?: number;
allowPrivateBoards: boolean;
allowPrivateStylePresets: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];

View File

@ -1,4 +1,4 @@
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
export const useCopyImageToClipboard = () => {
const { t } = useTranslation();
const imageUrlToBlob = useImageUrlToBlob();
const isClipboardAPIAvailable = useMemo(() => {
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
@ -23,7 +22,7 @@ export const useCopyImageToClipboard = () => {
});
}
try {
const blob = await imageUrlToBlob(image_url);
const blob = await convertImageUrlToBlob(image_url);
if (!blob) {
throw new Error('Unable to create Blob');
@ -45,7 +44,7 @@ export const useCopyImageToClipboard = () => {
});
}
},
[imageUrlToBlob, isClipboardAPIAvailable, t]
[isClipboardAPIAvailable, t]
);
return { isClipboardAPIAvailable, copyImageToClipboard };

View File

@ -1,40 +0,0 @@
import { $authToken } from 'app/store/nanostores/authToken';
import { useCallback } from 'react';
/**
* Converts an image URL to a Blob by creating an <img /> element, drawing it to canvas
* and then converting the canvas to a Blob.
*
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
*/
export const useImageUrlToBlob = () => {
const imageUrlToBlob = useCallback(
async (url: string) =>
new Promise<Blob | null>((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
if (!context) {
return;
}
context.drawImage(img, 0, 0);
resolve(
new Promise<Blob | null>((resolve) => {
canvas.toBlob(function (blob) {
resolve(blob);
}, 'image/png');
})
);
};
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
img.src = url;
}),
[]
);
return imageUrlToBlob;
};

View File

@ -0,0 +1,33 @@
import { $authToken } from 'app/store/nanostores/authToken';
/**
* Converts an image URL to a Blob by creating an <img /> element, drawing it to canvas
* and then converting the canvas to a Blob.
*
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
*/
export const convertImageUrlToBlob = async (url: string) =>
new Promise<Blob | null>((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
if (!context) {
return;
}
context.drawImage(img, 0, 0);
resolve(
new Promise<Blob | null>((resolve) => {
canvas.toBlob(function (blob) {
resolve(blob);
}, 'image/png');
})
);
};
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
img.src = url;
});

View File

@ -30,6 +30,7 @@ import {
PiFlowArrowBold,
PiFoldersBold,
PiImagesBold,
PiPaintBrushBold,
PiPlantBold,
PiQuotesBold,
PiShareFatBold,
@ -55,8 +56,17 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { downloadImage } = useDownloadImage();
const templates = useStore($templates);
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
useImageActions(imageDTO?.image_name);
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
@ -182,6 +192,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
{t('stylePresets.useForTemplate')}
</MenuItem>
<MenuDivider />
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
{t('parameters.sendToImg2Img')}

View File

@ -1,7 +1,10 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
export const useImageActions = (image_name?: string) => {
@ -10,6 +13,7 @@ export const useImageActions = (image_name?: string) => {
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
const [hasPrompts, setHasPrompts] = useState(false);
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
useEffect(() => {
const parseMetadata = async () => {
@ -61,5 +65,34 @@ export const useImageActions = (image_name?: string) => {
parseAndRecallPrompts(metadata);
}, [metadata]);
return { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata };
const createAsPreset = useCallback(async () => {
if (image_name && metadata && imageDTO) {
const positivePrompt = await handlers.positivePrompt.parse(metadata);
const negativePrompt = await handlers.negativePrompt.parse(metadata);
$stylePresetModalState.set({
prefilledFormData: {
name: '',
positivePrompt,
negativePrompt,
imageUrl: imageDTO.image_url,
type: 'user',
},
updatingStylePresetId: null,
isModalOpen: true,
});
}
}, [image_name, metadata, imageDTO]);
return {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
};
};

View File

@ -22,11 +22,10 @@ import {
} from './constants';
import { addLoRAs } from './generation/addLoRAs';
import { addSDXLLoRas } from './generation/addSDXLLoRAs';
import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils';
export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise<GraphType> => {
const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale;
assert(model, 'No model found in state');
@ -99,7 +98,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
let modelNode;
if (model.base === 'sdxl') {
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } =
getPresetModifiedPrompts(state);
posCondNode = g.addNode({
type: 'sdxl_compel_prompt',
@ -132,6 +132,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
negative_style_prompt: negativeStylePrompt,
});
} else {
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
posCondNode = g.addNode({
type: 'compel',
id: POSITIVE_CONDITIONING,

View File

@ -16,7 +16,7 @@ import {
SDXL_REFINER_POSITIVE_CONDITIONING,
SDXL_REFINER_SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { NonNullableGraph } from 'services/api/types';
import { isRefinerMainModelModelConfig } from 'services/api/types';
@ -59,7 +59,7 @@ export const addSDXLRefinerToGraph = async (
const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId : SDXL_MODEL_LOADER;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
// Unplug SDXL Latents Generation To Latents To Image
graph.edges = graph.edges.filter((e) => !(e.source.node_id === baseNodeId && ['latents'].includes(e.source.field)));

View File

@ -16,7 +16,11 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
@ -51,7 +55,6 @@ export const buildCanvasImageToImageGraph = async (
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -71,6 +74,8 @@ export const buildCanvasImageToImageGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node

View File

@ -19,7 +19,11 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -58,7 +62,6 @@ export const buildCanvasInpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
if (!model) {
log.error('No model found in state');
@ -79,6 +82,8 @@ export const buildCanvasInpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: CANVAS_INPAINT_GRAPH,
nodes: {

View File

@ -23,7 +23,11 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -70,7 +74,6 @@ export const buildCanvasOutpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
if (!model) {
log.error('No model found in state');
@ -91,6 +94,8 @@ export const buildCanvasOutpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: CANVAS_OUTPAINT_GRAPH,
nodes: {

View File

@ -16,7 +16,11 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
@ -51,7 +55,6 @@ export const buildCanvasSDXLImageToImageGraph = async (
seamlessYAxis,
img2imgStrength: strength,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -75,7 +78,7 @@ export const buildCanvasSDXLImageToImageGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the

View File

@ -19,7 +19,11 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -58,7 +62,6 @@ export const buildCanvasSDXLInpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -83,7 +86,7 @@ export const buildCanvasSDXLInpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: SDXL_CANVAS_INPAINT_GRAPH,

View File

@ -23,7 +23,11 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -70,7 +74,6 @@ export const buildCanvasSDXLOutpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -94,7 +97,7 @@ export const buildCanvasSDXLOutpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: SDXL_CANVAS_OUTPAINT_GRAPH,

View File

@ -14,7 +14,11 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -44,7 +48,6 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -67,7 +70,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
let modelLoaderNodeId = SDXL_MODEL_LOADER;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the

View File

@ -14,7 +14,11 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import {
getBoardField,
getIsIntermediate,
getPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -44,7 +48,6 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -64,6 +67,8 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
let modelLoaderNodeId = MAIN_MODEL_LOADER;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node

View File

@ -22,7 +22,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
import type { GraphType } from 'features/nodes/util/graph/generation/Graph';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@ -40,11 +40,12 @@ export const buildGenerationTabGraph = async (state: RootState): Promise<GraphTy
seed,
vae,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { width, height } = state.controlLayers.present.size;
assert(model, 'No model found in state');
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const g = new Graph(CONTROL_LAYERS_GRAPH);
const modelLoader = g.addNode({
type: 'main_model_loader',

View File

@ -19,7 +19,7 @@ import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefi
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import { getBoardField, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@ -36,14 +36,13 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
vaePrecision,
vae,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { width, height } = state.controlLayers.present.size;
const { refinerModel, refinerStart } = state.sdxl;
assert(model, 'No model found in state');
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH);
const modelLoader = g.addNode({

View File

@ -1,6 +1,8 @@
import type { RootState } from 'app/store/store';
import type { BoardField } from 'features/nodes/types/common';
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
/**
* Gets the board field, based on the autoAddBoardId setting.
@ -14,13 +16,43 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
};
/**
* Gets the SDXL style prompts, based on the concat setting.
* Gets the prompts, modified for the active style preset.
*/
export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => {
export const getPresetModifiedPrompts = (
state: RootState
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
state.controlLayers.present;
const { activeStylePresetId } = state.stylePreset;
if (activeStylePresetId) {
const { data } = stylePresetsApi.endpoints.listStylePresets.select()(state);
const activeStylePreset = data?.find((item) => item.id === activeStylePresetId);
if (activeStylePreset) {
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(
activeStylePreset.preset_data.positive_prompt,
positivePrompt
);
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(
activeStylePreset.preset_data.negative_prompt,
negativePrompt
);
return {
positivePrompt: presetModifiedPositivePrompt,
negativePrompt: presetModifiedNegativePrompt,
positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2,
negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
};
}
}
return {
positivePrompt,
negativePrompt,
positiveStylePrompt: shouldConcatPrompts ? positivePrompt : positivePrompt2,
negativeStylePrompt: shouldConcatPrompts ? negativePrompt : negativePrompt2,
};

View File

@ -1,16 +1,32 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
let activeStylePreset = null;
if (data) {
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
}
return { activeStylePreset };
},
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const _onChange = useCallback(
@ -27,22 +43,34 @@ export const ParamNegativePrompt = memo(() => {
return (
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
<Box pos="relative">
<Box pos="relative" w="full">
<Textarea
id="negativePrompt"
name="negativePrompt"
ref={textareaRef}
value={prompt}
placeholder={t('parameters.globalNegativePromptPlaceholder')}
onChange={onChange}
onKeyDown={onKeyDown}
fontSize="sm"
variant="darkFilled"
paddingRight={30}
minH={28}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper>
<PromptLabel label={t('parameters.negativePromptPlaceholder')} />
{viewMode && (
<ViewModePrompt
prompt={prompt}
presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''}
label={`${t('parameters.negativePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
</Box>
</PromptPopover>
);

View File

@ -2,7 +2,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
@ -11,11 +13,24 @@ import { memo, useCallback, useRef } from 'react';
import type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamPositivePrompt = memo(() => {
const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
const baseModel = useAppSelector((s) => s.generation.model)?.base;
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
let activeStylePreset = null;
if (data) {
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
}
return { activeStylePreset };
},
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
@ -49,18 +64,29 @@ export const ParamPositivePrompt = memo(() => {
name="prompt"
ref={textareaRef}
value={prompt}
placeholder={t('parameters.globalPositivePromptPlaceholder')}
onChange={onChange}
minH={28}
minH={40}
onKeyDown={onKeyDown}
variant="darkFilled"
paddingRight={30}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
{baseModel === 'sdxl' && <SDXLConcatButton />}
<ShowDynamicPromptsPreviewButton />
</PromptOverlayButtonWrapper>
<PromptLabel label={t('parameters.positivePromptPlaceholder')} />
{viewMode && (
<ViewModePrompt
prompt={prompt}
presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''}
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
</Box>
</PromptPopover>
);

View File

@ -0,0 +1,9 @@
import { Text } from '@invoke-ai/ui-library';
export const PromptLabel = ({ label }: { label: string }) => {
return (
<Text variant="subtext" fontWeight="semibold" pos="absolute" top={1} left={2}>
{label}
</Text>
);
};

View File

@ -1,13 +1,29 @@
import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
import { memo } from 'react';
const concatPromptsSelector = createSelector(
[selectGenerationSlice, selectControlLayersSlice],
(generation, controlLayers) => {
return generation.model?.base !== 'sdxl' || controlLayers.present.shouldConcatPrompts;
}
);
export const Prompts = memo(() => {
const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
return (
<Flex flexDir="column" gap={2}>
<ParamPositivePrompt />
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
<ParamNegativePrompt />
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
</Flex>
);
});

View File

@ -0,0 +1,81 @@
import { Box, Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
import { getViewModeChunks } from 'features/stylePresets/util/getViewModeChunks';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold } from 'react-icons/pi';
import { PromptLabel } from './PromptLabel';
export const ViewModePrompt = ({
presetPrompt,
prompt,
label,
}: {
presetPrompt: string;
prompt: string;
label: string;
}) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const presetChunks = useMemo(() => {
return getViewModeChunks(prompt, presetPrompt);
}, [presetPrompt, prompt]);
const handleExitViewMode = useCallback(() => {
dispatch(viewModeChanged(false));
}, [dispatch]);
return (
<Box position="absolute" top={0} bottom={0} left={0} right={0} layerStyle="second" borderRadius="base">
<Flex
flexDir="column"
onClick={handleExitViewMode}
justifyContent="space-between"
h="full"
borderWidth={1}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
borderColor="transparent"
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
>
<PromptLabel label={label} />
<Flex overflow="scroll">
<Text w="full" lineHeight="short">
{presetChunks.map((chunk, index) => (
<Text
as="span"
color={index === 1 ? 'white' : 'base.200'}
fontWeight={index === 1 ? 'semibold' : 'normal'}
key={index}
>
{chunk}
</Text>
))}
</Text>
</Flex>
<Tooltip label={t('stylePresets.viewModeTooltip')}>
<Flex
position="absolute"
insetInlineEnd={0}
insetBlockStart={0}
alignItems="center"
justifyContent="center"
p={2}
bg="base.900"
opacity={0.8}
backgroundClip="border-box"
borderBottomStartRadius="base"
>
<Icon as={PiEyeBold} color="base.500" boxSize={4} />
</Flex>
</Tooltip>
</Flex>
</Box>
);
};

View File

@ -1,6 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -36,16 +37,21 @@ export const ParamSDXLNegativeStylePrompt = memo(() => {
name="prompt"
ref={textareaRef}
value={prompt}
placeholder={t('sdxl.negStylePrompt')}
onChange={onChange}
onKeyDown={onKeyDown}
fontSize="sm"
variant="darkFilled"
paddingRight={30}
minH={24}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper>
<PromptLabel label={t('sdxl.negStylePrompt')} />
</Box>
</PromptPopover>
);

View File

@ -1,6 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { positivePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -33,16 +34,21 @@ export const ParamSDXLPositiveStylePrompt = memo(() => {
name="prompt"
ref={textareaRef}
value={prompt}
placeholder={t('sdxl.posStylePrompt')}
onChange={onChange}
onKeyDown={onKeyDown}
fontSize="sm"
variant="darkFilled"
paddingRight={30}
minH={24}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper>
<PromptLabel label={t('sdxl.posStylePrompt')} />
</Box>
</PromptPopover>
);

View File

@ -1,20 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SDXLPrompts } from './SDXLPrompts';
const meta: Meta<typeof SDXLPrompts> = {
title: 'Feature/Prompt/SDXLPrompts',
tags: ['autodocs'],
component: SDXLPrompts,
};
export default meta;
type Story = StoryObj<typeof SDXLPrompts>;
const Component = () => {
return <SDXLPrompts />;
};
export const Default: Story = {
render: Component,
};

View File

@ -1,22 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { memo } from 'react';
import { ParamSDXLNegativeStylePrompt } from './ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from './ParamSDXLPositiveStylePrompt';
export const SDXLPrompts = memo(() => {
const shouldConcatPrompts = useAppSelector((s) => s.controlLayers.present.shouldConcatPrompts);
return (
<Flex flexDir="column" gap={2} pos="relative">
<ParamPositivePrompt />
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
<ParamNegativePrompt />
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
</Flex>
);
});
SDXLPrompts.displayName = 'SDXLPrompts';

View File

@ -0,0 +1,113 @@
import { Badge, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
import type { MouseEventHandler } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import StylePresetImage from './StylePresetImage';
export const ActiveStylePreset = () => {
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
let activeStylePreset = null;
if (data) {
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
}
return { activeStylePreset };
},
});
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
const handleClearActiveStylePreset = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(viewModeChanged(false));
dispatch(activeStylePresetIdChanged(null));
},
[dispatch]
);
const handleFlattenPrompts = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
dispatch(viewModeChanged(false));
dispatch(activeStylePresetIdChanged(null));
},
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
);
const handleToggleViewMode = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(viewModeChanged(!viewMode));
},
[dispatch, viewMode]
);
if (!activeStylePreset) {
return (
<Flex h={25} alignItems="center">
<Text fontSize="sm" fontWeight="semibold" color="base.300">
{t('stylePresets.choosePromptTemplate')}
</Text>
</Flex>
);
}
return (
<Flex justifyContent="space-between" w="full" alignItems="center">
<Flex gap={2} alignItems="center">
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
<Flex flexDir="column">
<Badge colorScheme="invokeBlue" variant="subtle">
{activeStylePreset.name}
</Badge>
</Flex>
</Flex>
<Flex gap={1}>
<Tooltip label={t('stylePresets.toggleViewMode')}>
<IconButton
onClick={handleToggleViewMode}
variant="outline"
size="sm"
aria-label={t('stylePresets.toggleViewMode')}
colorScheme={viewMode ? 'invokeBlue' : 'base'}
icon={<PiEyeBold />}
/>
</Tooltip>
<Tooltip label={t('stylePresets.flatten')}>
<IconButton
onClick={handleFlattenPrompts}
variant="outline"
size="sm"
aria-label={t('stylePresets.flatten')}
icon={<PiStackSimpleBold />}
/>
</Tooltip>
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
<IconButton
onClick={handleClearActiveStylePreset}
variant="outline"
size="sm"
aria-label={t('stylePresets.clearTemplateSelection')}
icon={<PiXBold />}
/>
</Tooltip>
</Flex>
</Flex>
);
};

View File

@ -0,0 +1,124 @@
import { Box, Button, Flex, FormControl, FormLabel, Input, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { toast } from 'features/toast/toast';
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import type { PresetType } from 'services/api/endpoints/stylePresets';
import { useCreateStylePresetMutation, useUpdateStylePresetMutation } from 'services/api/endpoints/stylePresets';
import { StylePresetImageField } from './StylePresetImageField';
import { StylePresetPromptField } from './StylePresetPromptField';
import { StylePresetTypeField } from './StylePresetTypeField';
export type StylePresetFormData = {
name: string;
positivePrompt: string;
negativePrompt: string;
image: File | null;
type: PresetType;
};
export const StylePresetForm = ({
updatingStylePresetId,
formData,
}: {
updatingStylePresetId: string | null;
formData: StylePresetFormData | null;
}) => {
const [createStylePreset] = useCreateStylePresetMutation();
const [updateStylePreset] = useUpdateStylePresetMutation();
const { t } = useTranslation();
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
const { handleSubmit, control, register, formState } = useForm<StylePresetFormData>({
defaultValues: formData || {
name: '',
positivePrompt: '',
negativePrompt: '',
image: null,
type: 'user',
},
mode: 'onChange',
});
const handleClickSave = useCallback<SubmitHandler<StylePresetFormData>>(
async (data) => {
const payload = {
data: {
name: data.name,
positive_prompt: data.positivePrompt,
negative_prompt: data.negativePrompt,
type: data.type,
},
image: data.image,
};
try {
if (updatingStylePresetId) {
await updateStylePreset({
id: updatingStylePresetId,
...payload,
}).unwrap();
} else {
await createStylePreset(payload).unwrap();
}
} catch (error) {
toast({
status: 'error',
title: 'Failed to save style preset',
});
}
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: false,
});
},
[updatingStylePresetId, updateStylePreset, createStylePreset]
);
return (
<Flex flexDir="column" gap={4}>
<Flex alignItems="center" gap={4}>
<StylePresetImageField control={control} name="image" />
<FormControl orientation="vertical">
<FormLabel>{t('stylePresets.name')}</FormLabel>
<Input size="md" {...register('name', { required: true, minLength: 1 })} />
</FormControl>
</Flex>
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
<Box>
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc1')}</Text>
<Text variant="subtext">
<Trans
i18nKey="stylePresets.promptTemplatesDesc2"
components={{ Pre: <Pre /> }}
values={{ placeholder: PRESET_PLACEHOLDER }}
/>
</Text>
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc3')}</Text>
</Box>
<Flex justifyContent="space-between" alignItems="flex-end" gap={10}>
{allowPrivateStylePresets ? <StylePresetTypeField control={control} name="type" /> : <Spacer />}
<Button onClick={handleSubmit(handleClickSave)} isDisabled={!formState.isValid}>
{t('common.save')}
</Button>
</Flex>
</Flex>
);
};
const Pre = (props: PropsWithChildren) => (
<Text as="span" fontFamily="monospace" fontWeight="semibold">
{props.children}
</Text>
);

View File

@ -0,0 +1,82 @@
import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import type { UseControllerProps } from 'react-hook-form';
import { useController } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi';
import type { StylePresetFormData } from './StylePresetForm';
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData, 'image'>) => {
const { field } = useController(props);
const { t } = useTranslation();
const onDropAccepted = useCallback(
(files: File[]) => {
const file = files[0];
if (file) {
field.onChange(file);
}
},
[field]
);
const handleResetImage = useCallback(() => {
field.onChange(null);
}, [field]);
const { getInputProps, getRootProps } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
onDropAccepted,
noDrag: true,
multiple: false,
});
if (field.value) {
return (
<Box position="relative" flexShrink={0}>
<Image
src={URL.createObjectURL(field.value)}
objectFit="cover"
objectPosition="50% 50%"
w={65}
h={65}
minWidth={65}
borderRadius="base"
/>
<IconButton
position="absolute"
insetInlineEnd={0}
insetBlockStart={0}
onClick={handleResetImage}
aria-label={t('stylePresets.deleteImage')}
tooltip={t('stylePresets.deleteImage')}
icon={<PiArrowCounterClockwiseBold />}
size="md"
variant="ghost"
/>
</Box>
);
}
return (
<>
<Tooltip label={t('stylePresets.uploadImage')}>
<Flex
as={Button}
w={65}
h={65}
opacity={0.3}
borderRadius="base"
alignItems="center"
justifyContent="center"
flexShrink={0}
{...getRootProps()}
>
<Icon as={PiUploadSimpleBold} w={8} h={8} />
</Flex>
</Tooltip>
<input {...getInputProps()} />
</>
);
};

View File

@ -0,0 +1,82 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spinner,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
import type { PrefilledFormData } from 'features/stylePresets/store/stylePresetModal';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { StylePresetFormData } from './StylePresetForm';
import { StylePresetForm } from './StylePresetForm';
export const StylePresetModal = () => {
const [formData, setFormData] = useState<StylePresetFormData | null>(null);
const { t } = useTranslation();
const stylePresetModalState = useStore($stylePresetModalState);
const modalTitle = useMemo(() => {
return stylePresetModalState.updatingStylePresetId
? t('stylePresets.updatePromptTemplate')
: t('stylePresets.createPromptTemplate');
}, [stylePresetModalState.updatingStylePresetId, t]);
const handleCloseModal = useCallback(() => {
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: false,
});
}, []);
useEffect(() => {
setFormData(null);
}, []);
useEffect(() => {
const convertImageToBlob = async (data: PrefilledFormData | null) => {
if (!data) {
setFormData(null);
} else {
let file = null;
if (data.imageUrl) {
const blob = await convertImageUrlToBlob(data.imageUrl);
if (blob) {
file = new File([blob], 'style_preset.png', { type: 'image/png' });
}
}
setFormData({
...data,
image: file,
});
}
};
convertImageToBlob(stylePresetModalState.prefilledFormData);
}, [stylePresetModalState.prefilledFormData]);
return (
<Modal isOpen={stylePresetModalState.isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>{modalTitle}</ModalHeader>
<ModalCloseButton />
<ModalBody display="flex" flexDir="column" gap={4}>
{!stylePresetModalState.prefilledFormData || formData ? (
<StylePresetForm updatingStylePresetId={stylePresetModalState.updatingStylePresetId} formData={formData} />
) : (
<Spinner />
)}
</ModalBody>
<ModalFooter p={2} />
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,63 @@
import { Button, Flex, FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import type { ChangeEventHandler } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import type { UseControllerProps } from 'react-hook-form';
import { useController } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type { StylePresetFormData } from './StylePresetForm';
interface Props extends UseControllerProps<StylePresetFormData, 'negativePrompt' | 'positivePrompt'> {
label: string;
}
export const StylePresetPromptField = (props: Props) => {
const { field } = useController(props);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const onChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
(v) => {
field.onChange(v.target.value);
},
[field]
);
const value = useMemo(() => {
return field.value;
}, [field.value]);
const insertPromptPlaceholder = useCallback(() => {
if (textareaRef.current) {
const cursorPos = textareaRef.current.selectionStart;
const textBeforeCursor = value.slice(0, cursorPos);
const textAfterCursor = value.slice(cursorPos);
const newValue = textBeforeCursor + PRESET_PLACEHOLDER + textAfterCursor;
field.onChange(newValue);
} else {
field.onChange(value + PRESET_PLACEHOLDER);
}
}, [value, field, textareaRef]);
const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]);
return (
<FormControl orientation="vertical" gap={3}>
<Flex alignItems="center" gap={2}>
<FormLabel>{props.label}</FormLabel>
<Button
onClick={insertPromptPlaceholder}
size="xs"
aria-label={t('stylePresets.insertPlaceholder')}
isDisabled={isPromptPresent}
>
{t('stylePresets.insertPlaceholder')}
</Button>
</Flex>
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
</FormControl>
);
};

View File

@ -0,0 +1,46 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
import type { UseControllerProps } from 'react-hook-form';
import { useController } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type { StylePresetFormData } from './StylePresetForm';
const OPTIONS = [
{ label: t('stylePresets.private'), value: 'user' },
{ label: t('stylePresets.shared'), value: 'project' },
];
export const StylePresetTypeField = (props: UseControllerProps<StylePresetFormData, 'type'>) => {
const { field } = useController(props);
const stylePresetModalState = useStore($stylePresetModalState);
const { t } = useTranslation();
const onChange = useCallback<ComboboxOnChange>(
(v) => {
if (v) {
field.onChange(v.value);
}
},
[field]
);
const value = useMemo(() => {
return OPTIONS.find((opt) => opt.value === field.value);
}, [field.value]);
return (
<FormControl
orientation="vertical"
maxW={48}
isDisabled={stylePresetModalState.prefilledFormData?.type === 'project'}
>
<FormLabel>{t('stylePresets.type')}</FormLabel>
<Combobox value={value} options={OPTIONS} onChange={onChange} />
</FormControl>
);
};

View File

@ -0,0 +1,52 @@
import { Flex, Icon, Image, Tooltip } from '@invoke-ai/ui-library';
import { typedMemo } from 'common/util/typedMemo';
import { PiImage } from 'react-icons/pi';
const IMAGE_THUMBNAIL_SIZE = '40px';
const FALLBACK_ICON_SIZE = '24px';
const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: string | null; imageWidth?: number }) => {
return (
<Tooltip
label={
presetImageUrl && (
<Image
src={presetImageUrl}
draggable={false}
objectFit="cover"
maxW={150}
aspectRatio="1/1"
borderRadius="base"
borderBottomRadius="lg"
/>
)
}
>
<Image
src={presetImageUrl || ''}
fallbackStrategy="beforeLoadOrError"
fallback={
<Flex
height={imageWidth || IMAGE_THUMBNAIL_SIZE}
minWidth={imageWidth || IMAGE_THUMBNAIL_SIZE}
bg="base.650"
borderRadius="base"
alignItems="center"
justifyContent="center"
>
<Icon color="base.500" as={PiImage} boxSize={imageWidth ? imageWidth / 2 : FALLBACK_ICON_SIZE} />
</Flex>
}
objectFit="cover"
objectPosition="50% 50%"
height={imageWidth || IMAGE_THUMBNAIL_SIZE}
width={imageWidth || IMAGE_THUMBNAIL_SIZE}
minHeight={imageWidth || IMAGE_THUMBNAIL_SIZE}
minWidth={imageWidth || IMAGE_THUMBNAIL_SIZE}
borderRadius="base"
/>
</Tooltip>
);
};
export default typedMemo(StylePresetImage);

View File

@ -0,0 +1,31 @@
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { PiCaretDownBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { StylePresetListItem } from './StylePresetListItem';
export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordWithImage[] }) => {
const { onToggle, isOpen } = useDisclosure({ defaultIsOpen: true });
if (!data.length) {
return <></>;
}
return (
<Flex flexDir="column">
<Button variant="unstyled" onClick={onToggle}>
<Flex gap={2} alignItems="center">
<Icon boxSize={4} as={PiCaretDownBold} transform={isOpen ? undefined : 'rotate(-90deg)'} fill="base.500" />
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
{title}
</Text>
</Flex>
</Button>
<Collapse in={isOpen}>
{data.map((preset) => (
<StylePresetListItem preset={preset} key={preset.id} />
))}
</Collapse>
</Flex>
);
};

View File

@ -0,0 +1,183 @@
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold, PiPencilBold, PiTrashBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
import StylePresetImage from './StylePresetImage';
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
const dispatch = useAppDispatch();
const [deleteStylePreset] = useDeleteStylePresetMutation();
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation();
const handleClickEdit = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const { name, preset_data } = preset;
const { positive_prompt, negative_prompt } = preset_data;
$stylePresetModalState.set({
prefilledFormData: {
name,
positivePrompt: positive_prompt || '',
negativePrompt: negative_prompt || '',
imageUrl: preset.image,
type: preset.type,
},
updatingStylePresetId: preset.id,
isModalOpen: true,
});
},
[preset]
);
const handleClickApply = useCallback(async () => {
dispatch(activeStylePresetIdChanged(preset.id));
$isMenuOpen.set(false);
}, [dispatch, preset.id]);
const handleClickDelete = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
},
[onOpen]
);
const handleClickCopy = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const { name, preset_data } = preset;
const { positive_prompt, negative_prompt } = preset_data;
$stylePresetModalState.set({
prefilledFormData: {
name: `${name} (${t('common.copy')})`,
positivePrompt: positive_prompt || '',
negativePrompt: negative_prompt || '',
imageUrl: preset.image,
type: 'user',
},
updatingStylePresetId: null,
isModalOpen: true,
});
},
[preset, t]
);
const handleDeletePreset = useCallback(async () => {
try {
await deleteStylePreset(preset.id);
toast({
status: 'success',
title: t('stylePresets.templateDeleted'),
});
} catch (error) {
toast({
status: 'error',
title: t('stylePresets.unableToDeleteTemplate'),
});
}
}, [preset, t, deleteStylePreset]);
return (
<>
<Flex
gap={4}
onClick={handleClickApply}
cursor="pointer"
_hover={{ backgroundColor: 'base.750' }}
py={3}
px={2}
borderRadius="base"
alignItems="flex-start"
w="full"
>
<StylePresetImage presetImageUrl={preset.image} />
<Flex flexDir="column" w="full">
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
<Flex alignItems="center" gap={2}>
<Text fontSize="md" noOfLines={2}>
{preset.name}
</Text>
{activeStylePresetId === preset.id && (
<Badge
color="invokeBlue.400"
borderColor="invokeBlue.700"
borderWidth={1}
bg="transparent"
flexShrink={0}
>
{t('stylePresets.active')}
</Badge>
)}
</Flex>
<Flex alignItems="center" gap={1}>
<IconButton
size="sm"
variant="ghost"
aria-label={t('stylePresets.copyTemplate')}
onClick={handleClickCopy}
icon={<PiCopyBold />}
/>
{preset.type !== 'default' && (
<>
<IconButton
size="sm"
variant="ghost"
aria-label={t('stylePresets.editTemplate')}
onClick={handleClickEdit}
icon={<PiPencilBold />}
/>
<IconButton
size="sm"
variant="ghost"
aria-label={t('stylePresets.deleteTemplate')}
onClick={handleClickDelete}
colorScheme="error"
icon={<PiTrashBold />}
/>
</>
)}
</Flex>
</Flex>
<Flex flexDir="column" gap={1}>
<Text fontSize="xs">
<Text as="span" fontWeight="semibold">
{t('stylePresets.positivePrompt')}:
</Text>{' '}
{preset.preset_data.positive_prompt}
</Text>
<Text fontSize="xs">
<Text as="span" fontWeight="semibold">
{t('stylePresets.negativePrompt')}:
</Text>{' '}
{preset.preset_data.negative_prompt}
</Text>
</Flex>
</Flex>
</Flex>
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('stylePresets.deleteTemplate')}
acceptCallback={handleDeletePreset}
acceptButtonText="Delete"
>
<p>{t('stylePresets.deleteTemplate2')}</p>
</ConfirmationAlertDialog>
</>
);
};

View File

@ -0,0 +1,90 @@
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import { StylePresetList } from './StylePresetList';
import StylePresetSearch from './StylePresetSearch';
export const StylePresetMenu = () => {
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
const { data } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
const filteredData =
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
const groupedData = filteredData.reduce(
(
acc: {
defaultPresets: StylePresetRecordWithImage[];
sharedPresets: StylePresetRecordWithImage[];
presets: StylePresetRecordWithImage[];
},
preset
) => {
if (preset.type === 'default') {
acc.defaultPresets.push(preset);
} else if (preset.type === 'project') {
acc.sharedPresets.push(preset);
} else {
acc.presets.push(preset);
}
return acc;
},
{ defaultPresets: [], sharedPresets: [], presets: [] }
);
return {
data: groupedData,
};
},
});
const { t } = useTranslation();
const handleClickAddNew = useCallback(() => {
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: true,
});
}, []);
return (
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
<StylePresetSearch />
<IconButton
icon={<PiPlusBold />}
tooltip={t('stylePresets.createPromptTemplate')}
aria-label={t('stylePresets.createPromptTemplate')}
onClick={handleClickAddNew}
size="md"
variant="link"
w={8}
h={8}
/>
</Flex>
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
<Text p={10} textAlign="center">
{t('stylePresets.noMatchingTemplates')}
</Text>
)}
<StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} />
{allowPrivateStylePresets && (
<StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} />
)}
<StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} />
</Flex>
);
};

View File

@ -0,0 +1,35 @@
import { Flex, IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
import { ActiveStylePreset } from './ActiveStylePreset';
export const StylePresetMenuTrigger = () => {
const isMenuOpen = useStore($isMenuOpen);
const { t } = useTranslation();
const handleToggle = useCallback(() => {
$isMenuOpen.set(!isMenuOpen);
}, [isMenuOpen]);
return (
<Flex
onClick={handleToggle}
backgroundColor="base.800"
justifyContent="space-between"
alignItems="center"
py={2}
px={3}
borderRadius="base"
gap={1}
role="button"
>
<ActiveStylePreset />
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
</Flex>
);
};

View File

@ -0,0 +1,65 @@
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { searchTermChanged } from 'features/stylePresets/store/stylePresetSlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
const StylePresetSearch = () => {
const dispatch = useAppDispatch();
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
const { t } = useTranslation();
const handlePresetSearch = useCallback(
(newSearchTerm: string) => {
dispatch(searchTermChanged(newSearchTerm));
},
[dispatch]
);
const clearPresetSearch = useCallback(() => {
dispatch(searchTermChanged(''));
}, [dispatch]);
const handleKeydown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
// exit search mode on escape
if (e.key === 'Escape') {
clearPresetSearch();
}
},
[clearPresetSearch]
);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
handlePresetSearch(e.target.value);
},
[handlePresetSearch]
);
return (
<InputGroup>
<Input
placeholder={t('stylePresets.searchByName')}
value={searchTerm}
onKeyDown={handleKeydown}
onChange={handleChange}
/>
{searchTerm && searchTerm.length && (
<InputRightElement h="full" pe={2}>
<IconButton
onClick={clearPresetSearch}
size="sm"
variant="link"
aria-label={t('boards.clearSearch')}
icon={<PiXBold />}
/>
</InputRightElement>
)}
</InputGroup>
);
};
export default memo(StylePresetSearch);

View File

@ -0,0 +1,39 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const PRESET_PLACEHOLDER = `[prompt]`;
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
return presetPrompt.includes(PRESET_PLACEHOLDER)
? presetPrompt.replace(PRESET_PLACEHOLDER, currentPrompt)
: `${currentPrompt} ${presetPrompt}`;
};
export const usePresetModifiedPrompts = () => {
const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
let activeStylePreset = null;
if (data) {
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
}
return { activeStylePreset };
},
});
if (!activeStylePreset) {
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt };
}
const { positive_prompt: presetPositivePrompt, negative_prompt: presetNegativePrompt } =
activeStylePreset.preset_data;
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(presetPositivePrompt, positivePrompt);
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(presetNegativePrompt, negativePrompt);
return { presetModifiedPositivePrompt, presetModifiedNegativePrompt };
};

View File

@ -0,0 +1,6 @@
import { atom } from 'nanostores';
/**
* Tracks whether or not the style preset menu is open.
*/
export const $isMenuOpen = atom<boolean>(false);

View File

@ -0,0 +1,27 @@
import { atom } from 'nanostores';
import type { PresetType } from 'services/api/endpoints/stylePresets';
const initialState: StylePresetModalState = {
isModalOpen: false,
updatingStylePresetId: null,
prefilledFormData: null,
};
/**
* Tracks the state for the style preset modal.
*/
export const $stylePresetModalState = atom<StylePresetModalState>(initialState);
type StylePresetModalState = {
isModalOpen: boolean;
updatingStylePresetId: string | null;
prefilledFormData: PrefilledFormData | null;
};
export type PrefilledFormData = {
name: string;
positivePrompt: string;
negativePrompt: string;
imageUrl: string | null;
type: PresetType;
};

View File

@ -0,0 +1,44 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig } from 'app/store/store';
import type { StylePresetState } from './types';
const initialState: StylePresetState = {
activeStylePresetId: null,
searchTerm: '',
viewMode: false,
};
export const stylePresetSlice = createSlice({
name: 'stylePreset',
initialState: initialState,
reducers: {
activeStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
state.activeStylePresetId = action.payload;
},
searchTermChanged: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
viewModeChanged: (state, action: PayloadAction<boolean>) => {
state.viewMode = action.payload;
},
},
});
export const { activeStylePresetIdChanged, searchTermChanged, viewModeChanged } = stylePresetSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrateStylePresetState = (state: any): any => {
if (!('_version' in state)) {
state._version = 1;
}
return state;
};
export const stylePresetPersistConfig: PersistConfig<StylePresetState> = {
name: stylePresetSlice.name,
initialState,
migrate: migrateStylePresetState,
persistDenylist: [],
};

View File

@ -0,0 +1,5 @@
export type StylePresetState = {
activeStylePresetId: string | null;
searchTerm: string;
viewMode: boolean;
};

View File

@ -0,0 +1,15 @@
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
if (!presetPrompt || !presetPrompt.length) {
return ['', currentPrompt, ''];
}
const [before, after] = presetPrompt.split(PRESET_PLACEHOLDER, 2);
if (!before || !after) {
return ['', `${currentPrompt} `, before || after || ''];
}
return [before ?? '', currentPrompt, after ?? ''];
};

View File

@ -19,6 +19,7 @@ const initialConfigState: AppConfig = {
shouldUpdateImagesOnConnect: false,
shouldFetchMetadataFromApi: false,
allowPrivateBoards: false,
allowPrivateStylePresets: false,
disabledTabs: [],
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],

View File

@ -1,15 +1,18 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
@ -21,15 +24,24 @@ const overlayScrollbarsStyles: CSSProperties = {
const ParametersPanelCanvas = () => {
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
const isMenuOpen = useStore($isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{isMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
{isSDXL ? <SDXLPrompts /> : <Prompts />}
<Prompts />
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<ControlSettingsAccordion />

View File

@ -1,5 +1,6 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
@ -7,17 +8,19 @@ import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSli
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const overlayScrollbarsStyles: CSSProperties = {
@ -59,14 +62,25 @@ const ParametersPanelTextToImage = () => {
[dispatch]
);
const ref = useRef<HTMLDivElement>(null);
const isMenuOpen = useStore($isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
{isMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
{isSDXL ? <SDXLPrompts /> : <Prompts />}
<Prompts />
<Tabs
defaultIndex={0}
variant="enclosed"

View File

@ -1,12 +1,14 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useStore } from '@nanostores/react';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion';
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
@ -17,16 +19,23 @@ const overlayScrollbarsStyles: CSSProperties = {
};
const ParametersPanelUpscale = () => {
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
const isMenuOpen = useStore($isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{isMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
{isSDXL ? <SDXLPrompts /> : <Prompts />}
<Prompts />
<UpscaleSettingsAccordion />
<GenerationSettingsAccordion />
<AdvancedSettingsAccordion />

View File

@ -0,0 +1,103 @@
import type { paths } from 'services/api/schema';
import type { S } from 'services/api/types';
import { api, buildV1Url, LIST_TAG } from '..';
export type StylePresetRecordWithImage =
paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'];
export type PresetType = S['PresetType'];
/**
* Builds an endpoint URL for the style_presets router
* @example
* buildStylePresetsUrl('some-path')
* // '/api/v1/style_presets/some-path'
*/
const buildStylePresetsUrl = (path: string = '') => buildV1Url(`style_presets/${path}`);
export const stylePresetsApi = api.injectEndpoints({
endpoints: (build) => ({
getStylePreset: build.query<
paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'],
string
>({
query: (style_preset_id) => buildStylePresetsUrl(`i/${style_preset_id}`),
providesTags: (result, error, style_preset_id) => [
{ type: 'StylePreset', id: style_preset_id },
'FetchOnReconnect',
],
}),
deleteStylePreset: build.mutation<void, string>({
query: (style_preset_id) => ({
url: buildStylePresetsUrl(`i/${style_preset_id}`),
method: 'DELETE',
}),
invalidatesTags: (result, error, style_preset_id) => [
{ type: 'StylePreset', id: LIST_TAG },
{ type: 'StylePreset', id: style_preset_id },
],
}),
createStylePreset: build.mutation<
paths['/api/v1/style_presets/']['post']['responses']['200']['content']['application/json'],
{ data: { name: string; positive_prompt: string; negative_prompt: string; type: PresetType }; image: Blob | null }
>({
query: ({ data, image }) => {
const formData = new FormData();
if (image) {
formData.append('image', image);
}
formData.append('data', JSON.stringify(data));
return {
url: buildStylePresetsUrl(),
method: 'POST',
body: formData,
};
},
invalidatesTags: [
{ type: 'StylePreset', id: LIST_TAG },
{ type: 'StylePreset', id: LIST_TAG },
],
}),
updateStylePreset: build.mutation<
paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['responses']['200']['content']['application/json'],
{ data: { name: string; positive_prompt: string; negative_prompt: string }; image: Blob | null; id: string }
>({
query: ({ id, data, image }) => {
const formData = new FormData();
if (image) {
formData.append('image', image);
}
formData.append('data', JSON.stringify(data));
return {
url: buildStylePresetsUrl(`i/${id}`),
method: 'PATCH',
body: formData,
};
},
invalidatesTags: (response, error, { id }) => [
{ type: 'StylePreset', id: LIST_TAG },
{ type: 'StylePreset', id: id },
],
}),
listStylePresets: build.query<
paths['/api/v1/style_presets/']['get']['responses']['200']['content']['application/json'],
void
>({
query: () => ({
url: buildStylePresetsUrl(),
}),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
}),
}),
});
export const {
useCreateStylePresetMutation,
useDeleteStylePresetMutation,
useUpdateStylePresetMutation,
useListStylePresetsQuery,
} = stylePresetsApi;

View File

@ -40,6 +40,7 @@ const tagTypes = [
'SDXLRefinerModel',
'Workflow',
'WorkflowsRecent',
'StylePreset',
'Schema',
// This is invalidated on reconnect. It should be used for queries that have changing data,
// especially related to the queue and generation.

View File

@ -1272,6 +1272,78 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v1/style_presets/i/{style_preset_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Style Preset
* @description Gets a style preset
*/
get: operations["get_style_preset"];
put?: never;
post?: never;
/**
* Delete Style Preset
* @description Deletes a style preset
*/
delete: operations["delete_style_preset"];
options?: never;
head?: never;
/**
* Update Style Preset
* @description Updates a style preset
*/
patch: operations["update_style_preset"];
trace?: never;
};
"/api/v1/style_presets/": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List Style Presets
* @description Gets a page of style presets
*/
get: operations["list_style_presets"];
put?: never;
/**
* Create Style Preset
* @description Creates a style preset
*/
post: operations["create_style_preset"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/style_presets/i/{style_preset_id}/image": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Style Preset Image
* @description Gets an image file that previews the model
*/
get: operations["get_style_preset_image"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
};
export type webhooks = Record<string, never>;
export type components = {
@ -1851,6 +1923,19 @@ export type components = {
*/
batch_ids: string[];
};
/** Body_create_style_preset */
Body_create_style_preset: {
/**
* Image
* @description The image file to upload
*/
image?: Blob | null;
/**
* Data
* @description The data of the style preset to create
*/
data: string;
};
/** Body_create_workflow */
Body_create_workflow: {
/** @description The workflow to create */
@ -1973,6 +2058,19 @@ export type components = {
*/
image: Blob;
};
/** Body_update_style_preset */
Body_update_style_preset: {
/**
* Image
* @description The image file to upload
*/
image?: Blob | null;
/**
* Data
* @description The data of the style preset to update
*/
data: string;
};
/** Body_update_workflow */
Body_update_workflow: {
/** @description The updated workflow */
@ -11206,6 +11304,24 @@ export type components = {
*/
type: "pidi_image_processor";
};
/** PresetData */
PresetData: {
/**
* Positive Prompt
* @description Positive prompt
*/
positive_prompt: string;
/**
* Negative Prompt
* @description Negative prompt
*/
negative_prompt: string;
};
/**
* PresetType
* @enum {string}
*/
PresetType: "user" | "default" | "project";
/**
* ProgressImage
* @description The progress image sent intermittently during processing
@ -13605,6 +13721,28 @@ export type components = {
*/
type: "string_split_neg";
};
/** StylePresetRecordWithImage */
StylePresetRecordWithImage: {
/**
* Name
* @description The name of the style preset.
*/
name: string;
/** @description The preset data */
preset_data: components["schemas"]["PresetData"];
/** @description The type of style preset */
type: components["schemas"]["PresetType"];
/**
* Id
* @description The style preset ID.
*/
id: string;
/**
* Image
* @description The path for image
*/
image: string | null;
};
/**
* SubModelType
* @description Submodel type.
@ -17746,4 +17884,203 @@ export interface operations {
};
};
};
get_style_preset: {
parameters: {
query?: never;
header?: never;
path: {
/** @description The style preset to get */
style_preset_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["StylePresetRecordWithImage"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_style_preset: {
parameters: {
query?: never;
header?: never;
path: {
/** @description The style preset to delete */
style_preset_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_style_preset: {
parameters: {
query?: never;
header?: never;
path: {
/** @description The id of the style preset to update */
style_preset_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["Body_update_style_preset"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["StylePresetRecordWithImage"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_style_presets: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["StylePresetRecordWithImage"][];
};
};
};
};
create_style_preset: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["Body_create_style_preset"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["StylePresetRecordWithImage"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_style_preset_image: {
parameters: {
query?: never;
header?: never;
path: {
/** @description The id of the style preset image to get */
style_preset_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description The style preset image was fetched successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Bad request */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description The style preset image could not be found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}