diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 760eddbee8..07b959e684 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -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/StylePresetModal';
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
@@ -104,6 +105,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, destination }: Props) =>
+
);
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 6ae2011355..f061d0e59f 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -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 { stylePresetModalSlice } from 'features/stylePresets/store/slice';
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,
+ [stylePresetModalSlice.name]: stylePresetModalSlice.reducer
};
const rootReducer = combineReducers(allReducers);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
index c41f929ae9..39746c9ba6 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
@@ -7,6 +7,7 @@ import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPo
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
+import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { memo } from 'react';
const concatPromptsSelector = createSelector(
@@ -20,6 +21,7 @@ export const Prompts = memo(() => {
const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
return (
+
{!shouldConcatPrompts && }
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx
new file mode 100644
index 0000000000..201c1e27cc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx
@@ -0,0 +1,88 @@
+import { Button, Flex, FormControl, FormLabel, Input, Textarea } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { isModalOpenChanged,updatingStylePresetChanged } from 'features/stylePresets/store/slice';
+import { toast } from 'features/toast/toast';
+import type { ChangeEventHandler} from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import type {
+ StylePresetRecordDTO} from 'services/api/endpoints/stylePresets';
+import {
+ useCreateStylePresetMutation,
+ useUpdateStylePresetMutation,
+} from 'services/api/endpoints/stylePresets';
+
+export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordDTO | null }) => {
+ const [createStylePreset] = useCreateStylePresetMutation();
+ const [updateStylePreset] = useUpdateStylePresetMutation();
+ const dispatch = useAppDispatch();
+
+ const [name, setName] = useState(updatingPreset ? updatingPreset.name : '');
+ const [posPrompt, setPosPrompt] = useState(updatingPreset ? updatingPreset.preset_data.positive_prompt : '');
+ const [negPrompt, setNegPrompt] = useState(updatingPreset ? updatingPreset.preset_data.negative_prompt : '');
+
+ const handleChangeName = useCallback>((e) => {
+ setName(e.target.value);
+ }, []);
+
+ const handleChangePosPrompt = useCallback>((e) => {
+ setPosPrompt(e.target.value);
+ }, []);
+
+ const handleChangeNegPrompt = useCallback>((e) => {
+ setNegPrompt(e.target.value);
+ }, []);
+
+ useEffect(() => {
+ if (updatingPreset) {
+ setName(updatingPreset.name);
+ setPosPrompt(updatingPreset.preset_data.positive_prompt);
+ setNegPrompt(updatingPreset.preset_data.negative_prompt);
+ } else {
+ setName('');
+ setPosPrompt('');
+ setNegPrompt('');
+ }
+ }, [updatingPreset]);
+
+ const handleClickSave = useCallback(async () => {
+ try {
+ if (updatingPreset) {
+ await updateStylePreset({
+ id: updatingPreset.id,
+ changes: { name, preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt } },
+ }).unwrap();
+ } else {
+ await createStylePreset({
+ name: name,
+ preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt },
+ }).unwrap();
+ }
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: 'Failed to save style preset',
+ });
+ }
+
+ dispatch(updatingStylePresetChanged(null));
+ dispatch(isModalOpenChanged(false));
+ }, [dispatch, updatingPreset, name, posPrompt, negPrompt, updateStylePreset, createStylePreset]);
+
+ return (
+
+
+ Name
+
+
+
+ Positive Prompt
+
+
+
+ Negative Prompt
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx
new file mode 100644
index 0000000000..86a5b78b04
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx
@@ -0,0 +1,46 @@
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
+import { useCallback } from 'react';
+import type { StylePresetRecordDTO} from 'services/api/endpoints/stylePresets';
+import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
+
+export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }) => {
+ const dispatch = useAppDispatch();
+ const [deleteStylePreset] = useDeleteStylePresetMutation();
+
+ const handleClickEdit = useCallback(() => {
+ dispatch(updatingStylePresetChanged(preset));
+ dispatch(isModalOpenChanged(true));
+ }, [dispatch, preset]);
+
+ const handleDeletePreset = useCallback(async () => {
+ try {
+ await deleteStylePreset(preset.id);
+ } catch (error) {}
+ }, [preset]);
+
+ return (
+ <>
+
+ {preset.name}
+
+
+
+ Positive prompt:
+ {' '}
+ {preset.preset_data.positive_prompt}
+
+
+
+ Negative prompt:
+ {' '}
+ {preset.preset_data.negative_prompt}
+
+
+
+
+
+ >
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx
new file mode 100644
index 0000000000..e4fd240820
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx
@@ -0,0 +1,34 @@
+import { Button, Flex, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
+import { useCallback } from 'react';
+import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
+
+import { StylePresetListItem } from './StylePresetListItem';
+
+export const StylePresetMenu = () => {
+ const { data } = useListStylePresetsQuery({});
+ const dispatch = useAppDispatch();
+
+ const handleClickAddNew = useCallback(() => {
+ dispatch(updatingStylePresetChanged(null));
+ dispatch(isModalOpenChanged(true));
+ }, [dispatch]);
+
+ return (
+ <>
+
+
+
+ Style Presets
+
+
+
+
+ {data?.items.map((preset) => )}
+
+ >
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx
new file mode 100644
index 0000000000..0b5fe7b9be
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx
@@ -0,0 +1,24 @@
+import {
+ Button,
+ Popover,
+ PopoverBody,
+ PopoverContent,
+ PopoverTrigger,
+} from '@invoke-ai/ui-library';
+
+import { StylePresetMenu } from './StylePresetMenu';
+
+export const StylePresetMenuTrigger = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetModal.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetModal.tsx
new file mode 100644
index 0000000000..a783d93db2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetModal.tsx
@@ -0,0 +1,43 @@
+import {
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+} from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
+import { useCallback, useMemo } from 'react';
+
+import { StylePresetForm } from './StylePresetForm';
+
+export const StylePresetModal = () => {
+ const dispatch = useAppDispatch();
+ const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
+ const updatingStylePreset = useAppSelector((s) => s.stylePresetModal.updatingStylePreset);
+
+ const modalTitle = useMemo(() => {
+ return updatingStylePreset ? `Update Style Preset` : `Create Style Preset`;
+ }, [updatingStylePreset]);
+
+ const handleCloseModal = useCallback(() => {
+ dispatch(updatingStylePresetChanged(null));
+ dispatch(isModalOpenChanged(false));
+ }, [dispatch]);
+
+ return (
+
+
+
+ {modalTitle}
+
+
+
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/store/slice.ts b/invokeai/frontend/web/src/features/stylePresets/store/slice.ts
new file mode 100644
index 0000000000..4ba8b881c3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/store/slice.ts
@@ -0,0 +1,30 @@
+import type { PayloadAction } from '@reduxjs/toolkit';
+import { createSlice } from '@reduxjs/toolkit';
+import type { RootState } from 'app/store/store';
+import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
+
+import type { StylePresetState } from './types';
+
+
+export const initialState: StylePresetState = {
+ isModalOpen: false,
+ updatingStylePreset: null,
+};
+
+
+export const stylePresetModalSlice = createSlice({
+ name: 'stylePresetModal',
+ initialState: initialState,
+ reducers: {
+ isModalOpenChanged: (state, action: PayloadAction) => {
+ state.isModalOpen = action.payload;
+ },
+ updatingStylePresetChanged: (state, action: PayloadAction) => {
+ state.updatingStylePreset = action.payload;
+ },
+ },
+});
+
+export const { isModalOpenChanged, updatingStylePresetChanged } = stylePresetModalSlice.actions;
+
+export const selectStylePresetModalSlice = (state: RootState) => state.stylePresetModal;
diff --git a/invokeai/frontend/web/src/features/stylePresets/store/types.ts b/invokeai/frontend/web/src/features/stylePresets/store/types.ts
new file mode 100644
index 0000000000..0ea2f8e71c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/store/types.ts
@@ -0,0 +1,8 @@
+import type { StylePresetRecordDTO } from "services/api/endpoints/stylePresets";
+
+export type StylePresetState = {
+ isModalOpen: boolean;
+ updatingStylePreset: StylePresetRecordDTO | null;
+};
+
+
diff --git a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
index 7aa4d8478c..6b9ce6fff5 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
@@ -2,6 +2,8 @@ import type { paths } from 'services/api/schema';
import { api, buildV1Url, LIST_TAG } from '..';
+export type StylePresetRecordDTO = paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json']
+
/**
* Builds an endpoint URL for the style_presets router
* @example
@@ -17,7 +19,10 @@ export const stylePresetsApi = api.injectEndpoints({
string
>({
query: (style_preset_id) => buildStylePresetsUrl(`i/${style_preset_id}`),
- providesTags: (result, error, style_preset_id) => [{ type: 'StylePreset', id: style_preset_id }, 'FetchOnReconnect'],
+ providesTags: (result, error, style_preset_id) => [
+ { type: 'StylePreset', id: style_preset_id },
+ 'FetchOnReconnect',
+ ],
}),
deleteStylePreset: build.mutation({
query: (style_preset_id) => ({
@@ -45,14 +50,17 @@ export const stylePresetsApi = api.injectEndpoints({
}),
updateStylePreset: build.mutation<
paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['responses']['200']['content']['application/json'],
- { id: string, changes: paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['application/json']['changes'] }
+ {
+ id: string;
+ changes: paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['application/json']['changes'];
+ }
>({
query: ({ id, changes }) => ({
url: buildStylePresetsUrl(`i/${id}`),
method: 'PATCH',
body: { changes },
}),
- invalidatesTags: (response, error, { id, changes }) => [
+ invalidatesTags: (response, error, { id }) => [
{ type: 'StylePreset', id: LIST_TAG },
{ type: 'StylePreset', id: id },
],