diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 362023adc4..605c1260a4 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1705,7 +1705,7 @@
"name": "Name",
"negativePrompt": "Negative Prompt",
"noMatchingTemplates": "No matching templates",
- "placeholderDirections": "Use the { } button to specify where your manual prompt should be included in the template. If you do not provide one, the template will be appended to your prompt.",
+ "placeholderDirections": "Use the button to specify where your manual prompt should be included in the template. If you do not provide one, the template will be appended to your prompt.",
"positivePrompt": "Positive Prompt",
"searchByName": "Search by name",
"templateDeleted": "Prompt template deleted",
@@ -1714,6 +1714,7 @@
"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": {
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts
index 098a92de41..513c087496 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts
@@ -12,7 +12,7 @@ import {
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
-import { activeStylePresetChanged } from 'features/stylePresets/store/stylePresetSlice';
+import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { socketConnected } from 'services/events/actions';
@@ -22,7 +22,7 @@ const matcher = isAnyOf(
maxPromptsChanged,
maxPromptsReset,
socketConnected,
- activeStylePresetChanged
+ activeStylePresetIdChanged
);
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index eba0ca4b15..e3a7f3702a 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -29,7 +29,7 @@ import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/up
import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetModalSlice } from 'features/stylePresets/store/stylePresetModalSlice';
-import { stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
+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';
@@ -118,6 +118,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) => {
@@ -168,8 +169,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)
diff --git a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts
index 233b841034..345ea98e13 100644
--- a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts
+++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts
@@ -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 };
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts b/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts
deleted file mode 100644
index 4916dc974e..0000000000
--- a/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { $authToken } from 'app/store/nanostores/authToken';
-import { useCallback } from 'react';
-
-/**
- * Converts an image URL to a Blob by creating an
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, dimension?: number) =>
- new Promise((resolve) => {
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- let width = img.width;
- let height = img.height;
-
- if (dimension) {
- const aspectRatio = img.width / img.height;
- if (img.width > img.height) {
- width = dimension;
- height = dimension / aspectRatio;
- } else {
- height = dimension;
- width = dimension * aspectRatio;
- }
- }
-
- canvas.width = width;
- canvas.height = height;
-
- const context = canvas.getContext('2d');
- if (!context) {
- return;
- }
- context.drawImage(img, 0, 0, width, height);
- resolve(
- new Promise((resolve) => {
- canvas.toBlob(function (blob) {
- resolve(blob);
- }, 'image/png');
- })
- );
- };
- img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
- img.src = url;
- }),
- []
- );
-
- return imageUrlToBlob;
-};
diff --git a/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts
new file mode 100644
index 0000000000..42fdd46609
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts
@@ -0,0 +1,33 @@
+import { $authToken } from 'app/store/nanostores/authToken';
+
+/**
+ * Converts an image URL to a Blob by creating an
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((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((resolve) => {
+ canvas.toBlob(function (blob) {
+ resolve(blob);
+ }, 'image/png');
+ })
+ );
+ };
+ img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
+ img.src = url;
+ });
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index d29f417517..b6086f846f 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -1,6 +1,5 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
import { isModalOpenChanged, prefilledFormDataChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
@@ -14,7 +13,6 @@ export const useImageActions = (image_name?: string) => {
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
const [hasPrompts, setHasPrompts] = useState(false);
- const imageUrlToBlob = useImageUrlToBlob();
const dispatch = useAppDispatch();
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
@@ -72,19 +70,18 @@ export const useImageActions = (image_name?: string) => {
if (image_name && metadata && imageDTO) {
const positivePrompt = await handlers.positivePrompt.parse(metadata);
const negativePrompt = await handlers.negativePrompt.parse(metadata);
- const imageBlob = await imageUrlToBlob(imageDTO.image_url, 100);
dispatch(
prefilledFormDataChanged({
name: '',
positivePrompt,
negativePrompt,
- image: imageBlob ? new File([imageBlob], 'stylePreset.png', { type: 'image/png' }) : null,
+ imageUrl: imageDTO.image_url,
})
);
dispatch(isModalOpenChanged(true));
}
- }, [image_name, metadata, dispatch, imageDTO, imageUrlToBlob]);
+ }, [image_name, metadata, dispatch, imageDTO]);
return {
recallAll,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
index 00bd213c62..ed6dfbc224 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -2,6 +2,7 @@ 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.
@@ -22,25 +23,31 @@ export const getPresetModifiedPrompts = (
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
state.controlLayers.present;
- const { activeStylePreset } = state.stylePreset;
+ const { activeStylePresetId } = state.stylePreset;
- if (activeStylePreset) {
- const presetModifiedPositivePrompt = buildPresetModifiedPrompt(
- activeStylePreset.preset_data.positive_prompt,
- positivePrompt
- );
+ if (activeStylePresetId) {
+ const { data } = stylePresetsApi.endpoints.listStylePresets.select()(state);
- const presetModifiedNegativePrompt = buildPresetModifiedPrompt(
- activeStylePreset.preset_data.negative_prompt,
- negativePrompt
- );
+ const activeStylePreset = data?.find((item) => item.id === activeStylePresetId);
- return {
- positivePrompt: presetModifiedPositivePrompt,
- negativePrompt: presetModifiedNegativePrompt,
- positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2,
- negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
- };
+ 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 {
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
index 0ab9f3c8f5..035dc417a7 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx
@@ -8,14 +8,24 @@ import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-
-const DEFAULT_HEIGHT = 20;
+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 activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
+ 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(null);
const { t } = useTranslation();
const _onChange = useCallback(
@@ -24,32 +34,18 @@ export const ParamNegativePrompt = memo(() => {
},
[dispatch]
);
- const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocusCursorAtEnd } = usePrompt({
+ const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt,
textareaRef,
onChange: _onChange,
});
- const handleFocus = useCallback(() => {
- setTimeout(() => {
- onFocusCursorAtEnd();
- }, 500);
- }, [onFocusCursorAtEnd]);
-
- if (viewMode) {
- return (
-
- );
- }
-
return (
+ {viewMode && (
+
+ )}