mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
UI updates per PR feedback
This commit is contained in:
parent
8eb5d08499
commit
12ba15bfa9
@ -1705,7 +1705,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"negativePrompt": "Negative Prompt",
|
"negativePrompt": "Negative Prompt",
|
||||||
"noMatchingTemplates": "No matching templates",
|
"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",
|
"positivePrompt": "Positive Prompt",
|
||||||
"searchByName": "Search by name",
|
"searchByName": "Search by name",
|
||||||
"templateDeleted": "Prompt template deleted",
|
"templateDeleted": "Prompt template deleted",
|
||||||
@ -1714,6 +1714,7 @@
|
|||||||
"updatePromptTemplate": "Update Prompt Template",
|
"updatePromptTemplate": "Update Prompt Template",
|
||||||
"uploadImage": "Upload Image",
|
"uploadImage": "Upload Image",
|
||||||
"useForTemplate": "Use For Prompt Template",
|
"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."
|
"viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box."
|
||||||
},
|
},
|
||||||
"upsell": {
|
"upsell": {
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
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 { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||||
import { socketConnected } from 'services/events/actions';
|
import { socketConnected } from 'services/events/actions';
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const matcher = isAnyOf(
|
|||||||
maxPromptsChanged,
|
maxPromptsChanged,
|
||||||
maxPromptsReset,
|
maxPromptsReset,
|
||||||
socketConnected,
|
socketConnected,
|
||||||
activeStylePresetChanged
|
activeStylePresetIdChanged
|
||||||
);
|
);
|
||||||
|
|
||||||
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
||||||
|
@ -29,7 +29,7 @@ import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/up
|
|||||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||||
import { stylePresetModalSlice } from 'features/stylePresets/store/stylePresetModalSlice';
|
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 { configSlice } from 'features/system/store/configSlice';
|
||||||
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
||||||
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
|
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
|
||||||
@ -118,6 +118,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
||||||
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
||||||
[upscalePersistConfig.name]: upscalePersistConfig,
|
[upscalePersistConfig.name]: upscalePersistConfig,
|
||||||
|
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
@ -168,8 +169,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
|||||||
reducer: rememberedRootReducer,
|
reducer: rememberedRootReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
serializableCheck: false,
|
serializableCheck: import.meta.env.MODE === 'development',
|
||||||
immutableCheck: false,
|
immutableCheck: import.meta.env.MODE === 'development',
|
||||||
})
|
})
|
||||||
.concat(api.middleware)
|
.concat(api.middleware)
|
||||||
.concat(dynamicMiddlewares)
|
.concat(dynamicMiddlewares)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
|
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||||
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
export const useCopyImageToClipboard = () => {
|
export const useCopyImageToClipboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const imageUrlToBlob = useImageUrlToBlob();
|
|
||||||
|
|
||||||
const isClipboardAPIAvailable = useMemo(() => {
|
const isClipboardAPIAvailable = useMemo(() => {
|
||||||
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
|
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
|
||||||
@ -23,7 +22,7 @@ export const useCopyImageToClipboard = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const blob = await imageUrlToBlob(image_url);
|
const blob = await convertImageUrlToBlob(image_url);
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error('Unable to create Blob');
|
throw new Error('Unable to create Blob');
|
||||||
@ -45,7 +44,7 @@ export const useCopyImageToClipboard = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[imageUrlToBlob, isClipboardAPIAvailable, t]
|
[isClipboardAPIAvailable, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { isClipboardAPIAvailable, copyImageToClipboard };
|
return { isClipboardAPIAvailable, copyImageToClipboard };
|
||||||
|
@ -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 <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, dimension?: number) =>
|
|
||||||
new Promise<Blob | null>((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<Blob | null>((resolve) => {
|
|
||||||
canvas.toBlob(function (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
}, 'image/png');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
|
||||||
img.src = url;
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return imageUrlToBlob;
|
|
||||||
};
|
|
@ -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;
|
||||||
|
});
|
@ -1,6 +1,5 @@
|
|||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
|
|
||||||
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
||||||
import { isModalOpenChanged, prefilledFormDataChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
import { isModalOpenChanged, prefilledFormDataChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
@ -14,7 +13,6 @@ export const useImageActions = (image_name?: string) => {
|
|||||||
const [hasMetadata, setHasMetadata] = useState(false);
|
const [hasMetadata, setHasMetadata] = useState(false);
|
||||||
const [hasSeed, setHasSeed] = useState(false);
|
const [hasSeed, setHasSeed] = useState(false);
|
||||||
const [hasPrompts, setHasPrompts] = useState(false);
|
const [hasPrompts, setHasPrompts] = useState(false);
|
||||||
const imageUrlToBlob = useImageUrlToBlob();
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
|
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
|
||||||
|
|
||||||
@ -72,19 +70,18 @@ export const useImageActions = (image_name?: string) => {
|
|||||||
if (image_name && metadata && imageDTO) {
|
if (image_name && metadata && imageDTO) {
|
||||||
const positivePrompt = await handlers.positivePrompt.parse(metadata);
|
const positivePrompt = await handlers.positivePrompt.parse(metadata);
|
||||||
const negativePrompt = await handlers.negativePrompt.parse(metadata);
|
const negativePrompt = await handlers.negativePrompt.parse(metadata);
|
||||||
const imageBlob = await imageUrlToBlob(imageDTO.image_url, 100);
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
prefilledFormDataChanged({
|
prefilledFormDataChanged({
|
||||||
name: '',
|
name: '',
|
||||||
positivePrompt,
|
positivePrompt,
|
||||||
negativePrompt,
|
negativePrompt,
|
||||||
image: imageBlob ? new File([imageBlob], 'stylePreset.png', { type: 'image/png' }) : null,
|
imageUrl: imageDTO.image_url,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(isModalOpenChanged(true));
|
dispatch(isModalOpenChanged(true));
|
||||||
}
|
}
|
||||||
}, [image_name, metadata, dispatch, imageDTO, imageUrlToBlob]);
|
}, [image_name, metadata, dispatch, imageDTO]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recallAll,
|
recallAll,
|
||||||
|
@ -2,6 +2,7 @@ import type { RootState } from 'app/store/store';
|
|||||||
import type { BoardField } from 'features/nodes/types/common';
|
import type { BoardField } from 'features/nodes/types/common';
|
||||||
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the board field, based on the autoAddBoardId setting.
|
* Gets the board field, based on the autoAddBoardId setting.
|
||||||
@ -22,7 +23,12 @@ export const getPresetModifiedPrompts = (
|
|||||||
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
|
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
|
||||||
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
|
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
|
||||||
state.controlLayers.present;
|
state.controlLayers.present;
|
||||||
const { activeStylePreset } = state.stylePreset;
|
const { activeStylePresetId } = state.stylePreset;
|
||||||
|
|
||||||
|
if (activeStylePresetId) {
|
||||||
|
const { data } = stylePresetsApi.endpoints.listStylePresets.select()(state);
|
||||||
|
|
||||||
|
const activeStylePreset = data?.find((item) => item.id === activeStylePresetId);
|
||||||
|
|
||||||
if (activeStylePreset) {
|
if (activeStylePreset) {
|
||||||
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(
|
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(
|
||||||
@ -42,6 +48,7 @@ export const getPresetModifiedPrompts = (
|
|||||||
negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
|
negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
positivePrompt,
|
positivePrompt,
|
||||||
|
@ -8,14 +8,24 @@ import { PromptPopover } from 'features/prompt/PromptPopover';
|
|||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
const DEFAULT_HEIGHT = 20;
|
|
||||||
|
|
||||||
export const ParamNegativePrompt = memo(() => {
|
export const ParamNegativePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
|
||||||
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
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<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const _onChange = useCallback(
|
const _onChange = useCallback(
|
||||||
@ -24,32 +34,18 @@ export const ParamNegativePrompt = memo(() => {
|
|||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocusCursorAtEnd } = usePrompt({
|
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
|
||||||
prompt,
|
prompt,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onChange: _onChange,
|
onChange: _onChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
onFocusCursorAtEnd();
|
|
||||||
}, 500);
|
|
||||||
}, [onFocusCursorAtEnd]);
|
|
||||||
|
|
||||||
if (viewMode) {
|
|
||||||
return (
|
|
||||||
<ViewModePrompt
|
|
||||||
prompt={prompt}
|
|
||||||
presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''}
|
|
||||||
height={DEFAULT_HEIGHT}
|
|
||||||
onExit={handleFocus}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||||
<Box pos="relative" w="full">
|
<Box pos="relative" w="full">
|
||||||
|
{viewMode && (
|
||||||
|
<ViewModePrompt prompt={prompt} presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''} />
|
||||||
|
)}
|
||||||
<Textarea
|
<Textarea
|
||||||
id="negativePrompt"
|
id="negativePrompt"
|
||||||
name="negativePrompt"
|
name="negativePrompt"
|
||||||
@ -61,7 +57,6 @@ export const ParamNegativePrompt = memo(() => {
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
minH={DEFAULT_HEIGHT}
|
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
|
@ -12,15 +12,24 @@ import { memo, useCallback, useRef } from 'react';
|
|||||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
const DEFAULT_HEIGHT = 28;
|
|
||||||
|
|
||||||
export const ParamPositivePrompt = memo(() => {
|
export const ParamPositivePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
||||||
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
||||||
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
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<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -30,18 +39,12 @@ export const ParamPositivePrompt = memo(() => {
|
|||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus, onFocusCursorAtEnd } = usePrompt({
|
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
||||||
prompt,
|
prompt,
|
||||||
textareaRef: textareaRef,
|
textareaRef: textareaRef,
|
||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
onFocusCursorAtEnd();
|
|
||||||
}, 500);
|
|
||||||
}, [onFocusCursorAtEnd]);
|
|
||||||
|
|
||||||
const focus: HotkeyCallback = useCallback(
|
const focus: HotkeyCallback = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
onFocus();
|
onFocus();
|
||||||
@ -52,20 +55,12 @@ export const ParamPositivePrompt = memo(() => {
|
|||||||
|
|
||||||
useHotkeys('alt+a', focus, []);
|
useHotkeys('alt+a', focus, []);
|
||||||
|
|
||||||
if (viewMode) {
|
|
||||||
return (
|
|
||||||
<ViewModePrompt
|
|
||||||
prompt={prompt}
|
|
||||||
presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''}
|
|
||||||
height={DEFAULT_HEIGHT}
|
|
||||||
onExit={handleFocus}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||||
<Box pos="relative">
|
<Box pos="relative">
|
||||||
|
{viewMode && (
|
||||||
|
<ViewModePrompt prompt={prompt} presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''} />
|
||||||
|
)}
|
||||||
<Textarea
|
<Textarea
|
||||||
id="prompt"
|
id="prompt"
|
||||||
name="prompt"
|
name="prompt"
|
||||||
@ -73,7 +68,7 @@ export const ParamPositivePrompt = memo(() => {
|
|||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.globalPositivePromptPlaceholder')}
|
placeholder={t('parameters.globalPositivePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
minH={DEFAULT_HEIGHT}
|
minH={28}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
|
@ -6,17 +6,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyeBold } from 'react-icons/pi';
|
import { PiEyeBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ViewModePrompt = ({
|
export const ViewModePrompt = ({ presetPrompt, prompt }: { presetPrompt: string; prompt: string }) => {
|
||||||
presetPrompt,
|
|
||||||
prompt,
|
|
||||||
height,
|
|
||||||
onExit,
|
|
||||||
}: {
|
|
||||||
presetPrompt: string;
|
|
||||||
prompt: string;
|
|
||||||
height: number;
|
|
||||||
onExit: () => void;
|
|
||||||
}) => {
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -26,43 +16,43 @@ export const ViewModePrompt = ({
|
|||||||
|
|
||||||
const handleExitViewMode = useCallback(() => {
|
const handleExitViewMode = useCallback(() => {
|
||||||
dispatch(viewModeChanged(false));
|
dispatch(viewModeChanged(false));
|
||||||
onExit();
|
}, [dispatch]);
|
||||||
}, [dispatch, onExit]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Box position="absolute" top={0} bottom={0} left={0} right={0} zIndex={1} layerStyle="second" borderRadius="base">
|
||||||
flexDir="column"
|
<Flex flexDir="column" onClick={handleExitViewMode} justifyContent="space-between" h="full" padding="8px 10px">
|
||||||
layerStyle="second"
|
|
||||||
padding="8px 10px"
|
|
||||||
borderRadius="base"
|
|
||||||
height={height}
|
|
||||||
onClick={handleExitViewMode}
|
|
||||||
justifyContent="space-between"
|
|
||||||
position="relative"
|
|
||||||
>
|
|
||||||
<Flex overflow="scroll">
|
<Flex overflow="scroll">
|
||||||
<Text fontSize="sm" lineHeight="1rem">
|
<Text fontSize="sm" lineHeight="1rem" w="full">
|
||||||
{presetChunks.map((chunk, index) => {
|
{presetChunks.map((chunk, index) => (
|
||||||
return (
|
<Text
|
||||||
chunk && (
|
as="span"
|
||||||
<Text as="span" color={index === 1 ? 'white' : 'base.300'} key={index}>
|
color={index === 1 ? 'white' : 'base.300'}
|
||||||
{chunk.trim()}{' '}
|
fontWeight={index === 1 ? 'semibold' : 'normal'}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{chunk}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box position="absolute" top={0} right={0} backgroundColor="rgba(0,0,0,0.75)" padding="2px 5px">
|
|
||||||
<Flex alignItems="center" gap="1">
|
|
||||||
<Tooltip label={t('stylePresets.viewModeTooltip')}>
|
<Tooltip label={t('stylePresets.viewModeTooltip')}>
|
||||||
<Flex>
|
<Flex
|
||||||
<Icon as={PiEyeBold} color="base.500" boxSize="12px" />
|
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>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -58,13 +58,6 @@ export const usePrompt = ({ prompt, textareaRef, onChange: _onChange }: UseInser
|
|||||||
textareaRef.current?.focus();
|
textareaRef.current?.focus();
|
||||||
}, [textareaRef]);
|
}, [textareaRef]);
|
||||||
|
|
||||||
const onFocusCursorAtEnd = useCallback(() => {
|
|
||||||
onFocus();
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.setSelectionRange(prompt.length, prompt.length);
|
|
||||||
}
|
|
||||||
}, [onFocus, textareaRef, prompt]);
|
|
||||||
|
|
||||||
const handleClosePopover = useCallback(() => {
|
const handleClosePopover = useCallback(() => {
|
||||||
onClose();
|
onClose();
|
||||||
onFocus();
|
onFocus();
|
||||||
@ -96,6 +89,5 @@ export const usePrompt = ({ prompt, textareaRef, onChange: _onChange }: UseInser
|
|||||||
onSelect,
|
onSelect,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onFocus,
|
onFocus,
|
||||||
onFocusCursorAtEnd,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
import { Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
import { Badge, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
import { activeStylePresetChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
|
import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
import type { MouseEventHandler } from 'react';
|
import type { MouseEventHandler } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
|
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
import StylePresetImage from './StylePresetImage';
|
import StylePresetImage from './StylePresetImage';
|
||||||
|
|
||||||
export const ActiveStylePreset = () => {
|
export const ActiveStylePreset = () => {
|
||||||
const { activeStylePreset, viewMode } = useAppSelector((s) => s.stylePreset);
|
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 dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -22,7 +35,7 @@ export const ActiveStylePreset = () => {
|
|||||||
(e) => {
|
(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dispatch(viewModeChanged(false));
|
dispatch(viewModeChanged(false));
|
||||||
dispatch(activeStylePresetChanged(null));
|
dispatch(activeStylePresetIdChanged(null));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -33,7 +46,7 @@ export const ActiveStylePreset = () => {
|
|||||||
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
|
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
|
||||||
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
|
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
|
||||||
dispatch(viewModeChanged(false));
|
dispatch(viewModeChanged(false));
|
||||||
dispatch(activeStylePresetChanged(null));
|
dispatch(activeStylePresetIdChanged(null));
|
||||||
},
|
},
|
||||||
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
|
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
|
||||||
);
|
);
|
||||||
@ -48,7 +61,7 @@ export const ActiveStylePreset = () => {
|
|||||||
|
|
||||||
if (!activeStylePreset) {
|
if (!activeStylePreset) {
|
||||||
return (
|
return (
|
||||||
<Flex h="25px" alignItems="center">
|
<Flex h={25} alignItems="center">
|
||||||
<Text fontSize="sm" fontWeight="semibold" color="base.300">
|
<Text fontSize="sm" fontWeight="semibold" color="base.300">
|
||||||
{t('stylePresets.choosePromptTemplate')}
|
{t('stylePresets.choosePromptTemplate')}
|
||||||
</Text>
|
</Text>
|
||||||
@ -56,17 +69,16 @@ export const ActiveStylePreset = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap={2} alignItems="center">
|
||||||
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||||
<Flex flexDir="column">
|
<Flex flexDir="column">
|
||||||
<Text fontSize="sm" fontWeight="semibold" color="base.300" noOfLines={1}>
|
<Badge colorScheme="invokeBlue" variant="subtle">
|
||||||
{activeStylePreset.name}
|
{activeStylePreset.name}
|
||||||
</Text>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="1">
|
<Flex gap={1}>
|
||||||
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleToggleViewMode}
|
onClick={handleToggleViewMode}
|
||||||
@ -97,6 +109,5 @@ export const ActiveStylePreset = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Flex, FormControl, FormLabel, Input, Text } from '@invoke-ai/ui-library';
|
import { Button, Flex, FormControl, FormLabel, Input, Text } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -18,16 +18,20 @@ export type StylePresetFormData = {
|
|||||||
image: File | null;
|
image: File | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StylePresetForm = ({ updatingStylePresetId }: { updatingStylePresetId: string | null }) => {
|
export const StylePresetForm = ({
|
||||||
|
updatingStylePresetId,
|
||||||
|
formData,
|
||||||
|
}: {
|
||||||
|
updatingStylePresetId: string | null;
|
||||||
|
formData: StylePresetFormData | null;
|
||||||
|
}) => {
|
||||||
const [createStylePreset] = useCreateStylePresetMutation();
|
const [createStylePreset] = useCreateStylePresetMutation();
|
||||||
const [updateStylePreset] = useUpdateStylePresetMutation();
|
const [updateStylePreset] = useUpdateStylePresetMutation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const defaultValues = useAppSelector((s) => s.stylePresetModal.prefilledFormData);
|
|
||||||
|
|
||||||
const { handleSubmit, control, register, formState } = useForm<StylePresetFormData>({
|
const { handleSubmit, control, register, formState } = useForm<StylePresetFormData>({
|
||||||
defaultValues: defaultValues || {
|
defaultValues: formData || {
|
||||||
name: '',
|
name: '',
|
||||||
positivePrompt: '',
|
positivePrompt: '',
|
||||||
negativePrompt: '',
|
negativePrompt: '',
|
||||||
@ -68,12 +72,12 @@ export const StylePresetForm = ({ updatingStylePresetId }: { updatingStylePreset
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap="4">
|
<Flex flexDir="column" gap={4}>
|
||||||
<Flex alignItems="center" gap="4">
|
<Flex alignItems="center" gap={4}>
|
||||||
<StylePresetImageField control={control} name="image" />
|
<StylePresetImageField control={control} name="image" />
|
||||||
<FormControl orientation="vertical">
|
<FormControl orientation="vertical">
|
||||||
<FormLabel>{t('stylePresets.name')}</FormLabel>
|
<FormLabel>{t('stylePresets.name')}</FormLabel>
|
||||||
<Input size="md" {...register('name', { required: true, minLength: 1 })} required={true} />
|
<Input size="md" {...register('name', { required: true, minLength: 1 })} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'
|
|||||||
|
|
||||||
import type { StylePresetFormData } from './StylePresetForm';
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
|
||||||
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData>) => {
|
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData, 'image'>) => {
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onDropAccepted = useCallback(
|
const onDropAccepted = useCallback(
|
||||||
@ -36,7 +36,7 @@ export const StylePresetImageField = (props: UseControllerProps<StylePresetFormD
|
|||||||
return (
|
return (
|
||||||
<Box position="relative" flexShrink={0}>
|
<Box position="relative" flexShrink={0}>
|
||||||
<Image
|
<Image
|
||||||
src={URL.createObjectURL(field.value as File)}
|
src={URL.createObjectURL(field.value)}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
objectPosition="50% 50%"
|
objectPosition="50% 50%"
|
||||||
w={65}
|
w={65}
|
||||||
|
@ -14,7 +14,7 @@ export const StylePresetList = ({ title, data }: { title: string; data: StylePre
|
|||||||
return (
|
return (
|
||||||
<Flex flexDir="column">
|
<Flex flexDir="column">
|
||||||
<Button variant="unstyled" onClick={onToggle}>
|
<Button variant="unstyled" onClick={onToggle}>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap={2} alignItems="center">
|
||||||
<Icon boxSize={4} as={PiCaretDownBold} transform={isOpen ? undefined : 'rotate(-90deg)'} fill="base.500" />
|
<Icon boxSize={4} as={PiCaretDownBold} transform={isOpen ? undefined : 'rotate(-90deg)'} fill="base.500" />
|
||||||
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
||||||
{title}
|
{title}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
|
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
|
|
||||||
import {
|
import {
|
||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
prefilledFormDataChanged,
|
prefilledFormDataChanged,
|
||||||
updatingStylePresetIdChanged,
|
updatingStylePresetIdChanged,
|
||||||
} from 'features/stylePresets/store/stylePresetModalSlice';
|
} from 'features/stylePresets/store/stylePresetModalSlice';
|
||||||
import { activeStylePresetChanged, isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
|
import { activeStylePresetIdChanged, isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -20,40 +19,35 @@ import StylePresetImage from './StylePresetImage';
|
|||||||
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
|
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [deleteStylePreset] = useDeleteStylePresetMutation();
|
const [deleteStylePreset] = useDeleteStylePresetMutation();
|
||||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const imageUrlToBlob = useImageUrlToBlob();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleClickEdit = useCallback(
|
const handleClickEdit = useCallback(
|
||||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const { name, preset_data } = preset;
|
const { name, preset_data } = preset;
|
||||||
const { positive_prompt, negative_prompt } = preset_data;
|
const { positive_prompt, negative_prompt } = preset_data;
|
||||||
let imageBlob = null;
|
|
||||||
if (preset.image) {
|
|
||||||
imageBlob = await imageUrlToBlob(preset.image, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
prefilledFormDataChanged({
|
prefilledFormDataChanged({
|
||||||
name,
|
name,
|
||||||
positivePrompt: positive_prompt,
|
positivePrompt: positive_prompt,
|
||||||
negativePrompt: negative_prompt,
|
negativePrompt: negative_prompt,
|
||||||
image: imageBlob ? new File([imageBlob], `style_preset_${preset.id}.png`, { type: 'image/png' }) : null,
|
imageUrl: preset.image,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(updatingStylePresetIdChanged(preset.id));
|
dispatch(updatingStylePresetIdChanged(preset.id));
|
||||||
dispatch(isModalOpenChanged(true));
|
dispatch(isModalOpenChanged(true));
|
||||||
},
|
},
|
||||||
[dispatch, preset, imageUrlToBlob]
|
[dispatch, preset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickApply = useCallback(async () => {
|
const handleClickApply = useCallback(async () => {
|
||||||
dispatch(activeStylePresetChanged(preset));
|
dispatch(activeStylePresetIdChanged(preset.id));
|
||||||
dispatch(isMenuOpenChanged(false));
|
dispatch(isMenuOpenChanged(false));
|
||||||
}, [dispatch, preset]);
|
}, [dispatch, preset.id]);
|
||||||
|
|
||||||
const handleClickDelete = useCallback(
|
const handleClickDelete = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -81,11 +75,12 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
gap="4"
|
gap={4}
|
||||||
onClick={handleClickApply}
|
onClick={handleClickApply}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
_hover={{ backgroundColor: 'base.750' }}
|
_hover={{ backgroundColor: 'base.750' }}
|
||||||
padding="10px"
|
py={3}
|
||||||
|
px={2}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
w="full"
|
w="full"
|
||||||
@ -93,11 +88,11 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
|||||||
<StylePresetImage presetImageUrl={preset.image} />
|
<StylePresetImage presetImageUrl={preset.image} />
|
||||||
<Flex flexDir="column" w="full">
|
<Flex flexDir="column" w="full">
|
||||||
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
|
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
|
||||||
<Flex alignItems="center" gap="2">
|
<Flex alignItems="center" gap={2}>
|
||||||
<Text fontSize="md" noOfLines={2}>
|
<Text fontSize="md" noOfLines={2}>
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</Text>
|
</Text>
|
||||||
{activeStylePreset && activeStylePreset.id === preset.id && (
|
{activeStylePresetId === preset.id && (
|
||||||
<Badge
|
<Badge
|
||||||
color="invokeBlue.400"
|
color="invokeBlue.400"
|
||||||
borderColor="invokeBlue.700"
|
borderColor="invokeBlue.700"
|
||||||
@ -111,7 +106,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!preset.is_default && (
|
{!preset.is_default && (
|
||||||
<Flex alignItems="center" gap="1">
|
<Flex alignItems="center" gap={1}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -131,7 +126,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex flexDir="column" gap="1">
|
<Flex flexDir="column" gap={1}>
|
||||||
<Text fontSize="xs">
|
<Text fontSize="xs">
|
||||||
<Text as="span" fontWeight="semibold">
|
<Text as="span" fontWeight="semibold">
|
||||||
{t('stylePresets.positivePrompt')}:
|
{t('stylePresets.positivePrompt')}:
|
||||||
|
@ -18,7 +18,7 @@ import StylePresetSearch from './StylePresetSearch';
|
|||||||
export const StylePresetMenu = () => {
|
export const StylePresetMenu = () => {
|
||||||
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||||
const { data } = useListStylePresetsQuery(undefined, {
|
const { data } = useListStylePresetsQuery(undefined, {
|
||||||
selectFromResult: ({ data, error, isLoading }) => {
|
selectFromResult: ({ data }) => {
|
||||||
const filteredData =
|
const filteredData =
|
||||||
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
|
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
|
||||||
|
|
||||||
@ -36,8 +36,6 @@ export const StylePresetMenu = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: groupedData,
|
data: groupedData,
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -52,8 +50,8 @@ export const StylePresetMenu = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap="2" padding="10px" layerStyle="second">
|
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||||
<Flex alignItems="center" gap="10" w="full" justifyContent="space-between">
|
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||||
<StylePresetSearch />
|
<StylePresetSearch />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<PiPlusBold />}
|
icon={<PiPlusBold />}
|
||||||
@ -68,7 +66,7 @@ export const StylePresetMenu = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
||||||
<Text m="20px" textAlign="center">
|
<Text p={10} textAlign="center">
|
||||||
{t('stylePresets.noMatchingTemplates')}
|
{t('stylePresets.noMatchingTemplates')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { Flex, Icon } from '@invoke-ai/ui-library';
|
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
|
import { isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretDownBold } from 'react-icons/pi';
|
import { PiCaretDownBold } from 'react-icons/pi';
|
||||||
|
|
||||||
import { ActiveStylePreset } from './ActiveStylePreset';
|
import { ActiveStylePreset } from './ActiveStylePreset';
|
||||||
|
|
||||||
export const StylePresetMenuTrigger = () => {
|
export const StylePresetMenuTrigger = () => {
|
||||||
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
|
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
|
||||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggle = useCallback(() => {
|
||||||
dispatch(isMenuOpenChanged(!isMenuOpen));
|
dispatch(isMenuOpenChanged(!isMenuOpen));
|
||||||
@ -17,20 +18,19 @@ export const StylePresetMenuTrigger = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
as="button"
|
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
backgroundColor="base.800"
|
backgroundColor="base.800"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
padding="5px 10px"
|
py={2}
|
||||||
|
px={3}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
gap="2"
|
gap={1}
|
||||||
borderTop="2px solid transparent"
|
role="button"
|
||||||
borderColor={activeStylePreset ? 'invokeBlue.200' : 'transparent'}
|
|
||||||
>
|
>
|
||||||
<ActiveStylePreset />
|
<ActiveStylePreset />
|
||||||
|
|
||||||
<Icon boxSize="15px" as={PiCaretDownBold} color="base.300" />
|
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,29 +6,65 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
|
Spinner,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||||
import { useCallback, useMemo } from 'react';
|
import {
|
||||||
|
isModalOpenChanged,
|
||||||
|
prefilledFormDataChanged,
|
||||||
|
updatingStylePresetIdChanged,
|
||||||
|
} from 'features/stylePresets/store/stylePresetModalSlice';
|
||||||
|
import type { PrefilledFormData } from 'features/stylePresets/store/types';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
import { StylePresetForm } from './StylePresetForm';
|
import { StylePresetForm } from './StylePresetForm';
|
||||||
|
|
||||||
export const StylePresetModal = () => {
|
export const StylePresetModal = () => {
|
||||||
|
const [formData, setFormData] = useState<StylePresetFormData | null>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
|
const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
|
||||||
const updatingStylePresetId = useAppSelector((s) => s.stylePresetModal.updatingStylePresetId);
|
const updatingStylePresetId = useAppSelector((s) => s.stylePresetModal.updatingStylePresetId);
|
||||||
|
const prefilledFormData = useAppSelector((s) => s.stylePresetModal.prefilledFormData);
|
||||||
|
|
||||||
const modalTitle = useMemo(() => {
|
const modalTitle = useMemo(() => {
|
||||||
return updatingStylePresetId ? t('stylePresets.updatePromptTemplate') : t('stylePresets.createPromptTemplate');
|
return updatingStylePresetId ? t('stylePresets.updatePromptTemplate') : t('stylePresets.createPromptTemplate');
|
||||||
}, [updatingStylePresetId, t]);
|
}, [updatingStylePresetId, t]);
|
||||||
|
|
||||||
const handleCloseModal = useCallback(() => {
|
const handleCloseModal = useCallback(() => {
|
||||||
|
dispatch(prefilledFormDataChanged(null));
|
||||||
dispatch(updatingStylePresetIdChanged(null));
|
dispatch(updatingStylePresetIdChanged(null));
|
||||||
dispatch(isModalOpenChanged(false));
|
dispatch(isModalOpenChanged(false));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
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(prefilledFormData);
|
||||||
|
}, [prefilledFormData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
|
<Modal isOpen={isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
@ -36,9 +72,13 @@ export const StylePresetModal = () => {
|
|||||||
<ModalHeader>{modalTitle}</ModalHeader>
|
<ModalHeader>{modalTitle}</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody display="flex" flexDir="column" gap={4}>
|
<ModalBody display="flex" flexDir="column" gap={4}>
|
||||||
<StylePresetForm updatingStylePresetId={updatingStylePresetId} />
|
{!prefilledFormData || formData ? (
|
||||||
|
<StylePresetForm updatingStylePresetId={updatingStylePresetId} formData={formData} />
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter />
|
<ModalFooter p={2} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { Flex, FormControl, FormLabel, IconButton, Textarea } from '@invoke-ai/ui-library';
|
import { Button, Flex, FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiBracketsCurlyBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import type { StylePresetFormData } from './StylePresetForm';
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
|
||||||
interface Props extends UseControllerProps<StylePresetFormData> {
|
interface Props extends UseControllerProps<StylePresetFormData, 'negativePrompt' | 'positivePrompt'> {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ export const StylePresetPromptField = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
return field.value as string;
|
return field.value;
|
||||||
}, [field.value]);
|
}, [field.value]);
|
||||||
|
|
||||||
const insertPromptPlaceholder = useCallback(() => {
|
const insertPromptPlaceholder = useCallback(() => {
|
||||||
@ -45,16 +44,17 @@ export const StylePresetPromptField = (props: Props) => {
|
|||||||
const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]);
|
const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl orientation="vertical">
|
<FormControl orientation="vertical" gap={3}>
|
||||||
<Flex alignItems="center" gap="1">
|
<Flex alignItems="center" gap={2}>
|
||||||
<FormLabel>{props.label}</FormLabel>
|
<FormLabel>{props.label}</FormLabel>
|
||||||
<IconButton
|
<Button
|
||||||
onClick={insertPromptPlaceholder}
|
onClick={insertPromptPlaceholder}
|
||||||
size="sm"
|
size="xs"
|
||||||
icon={<PiBracketsCurlyBold />}
|
|
||||||
aria-label={t('stylePresets.insertPlaceholder')}
|
aria-label={t('stylePresets.insertPlaceholder')}
|
||||||
isDisabled={isPromptPresent}
|
isDisabled={isPromptPresent}
|
||||||
/>
|
>
|
||||||
|
{t('stylePresets.insertPlaceholder')}
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
|
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
export const PRESET_PLACEHOLDER = `{prompt}`;
|
export const PRESET_PLACEHOLDER = `[prompt]`;
|
||||||
|
|
||||||
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
|
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
|
||||||
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
||||||
@ -9,9 +10,20 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const usePresetModifiedPrompts = () => {
|
export const usePresetModifiedPrompts = () => {
|
||||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
|
||||||
const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present);
|
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) {
|
if (!activeStylePreset) {
|
||||||
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt };
|
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt };
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { StylePresetFormData } from 'features/stylePresets/components/StylePresetForm';
|
|
||||||
|
|
||||||
import type { StylePresetModalState } from './types';
|
import type { PrefilledFormData, StylePresetModalState } from './types';
|
||||||
|
|
||||||
const initialState: StylePresetModalState = {
|
const initialState: StylePresetModalState = {
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
@ -20,7 +19,7 @@ export const stylePresetModalSlice = createSlice({
|
|||||||
updatingStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
updatingStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||||
state.updatingStylePresetId = action.payload;
|
state.updatingStylePresetId = action.payload;
|
||||||
},
|
},
|
||||||
prefilledFormDataChanged: (state, action: PayloadAction<StylePresetFormData | null>) => {
|
prefilledFormDataChanged: (state, action: PayloadAction<PrefilledFormData | null>) => {
|
||||||
state.prefilledFormData = action.payload;
|
state.prefilledFormData = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
import type { PersistConfig } from 'app/store/store';
|
||||||
|
|
||||||
import type { StylePresetState } from './types';
|
import type { StylePresetState } from './types';
|
||||||
|
|
||||||
const initialState: StylePresetState = {
|
const initialState: StylePresetState = {
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
activeStylePreset: null,
|
activeStylePresetId: null,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
};
|
};
|
||||||
@ -18,8 +18,8 @@ export const stylePresetSlice = createSlice({
|
|||||||
isMenuOpenChanged: (state, action: PayloadAction<boolean>) => {
|
isMenuOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isMenuOpen = action.payload;
|
state.isMenuOpen = action.payload;
|
||||||
},
|
},
|
||||||
activeStylePresetChanged: (state, action: PayloadAction<StylePresetRecordWithImage | null>) => {
|
activeStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||||
state.activeStylePreset = action.payload;
|
state.activeStylePresetId = action.payload;
|
||||||
},
|
},
|
||||||
searchTermChanged: (state, action: PayloadAction<string>) => {
|
searchTermChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.searchTerm = action.payload;
|
state.searchTerm = action.payload;
|
||||||
@ -30,5 +30,20 @@ export const stylePresetSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { isMenuOpenChanged, activeStylePresetChanged, searchTermChanged, viewModeChanged } =
|
export const { isMenuOpenChanged, activeStylePresetIdChanged, searchTermChanged, viewModeChanged } =
|
||||||
stylePresetSlice.actions;
|
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: [],
|
||||||
|
};
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import type { StylePresetFormData } from 'features/stylePresets/components/StylePresetForm';
|
|
||||||
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
|
||||||
|
|
||||||
export type StylePresetModalState = {
|
export type StylePresetModalState = {
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
updatingStylePresetId: string | null;
|
updatingStylePresetId: string | null;
|
||||||
prefilledFormData: StylePresetFormData | null;
|
prefilledFormData: PrefilledFormData | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrefilledFormData = {
|
||||||
|
name: string;
|
||||||
|
positivePrompt: string;
|
||||||
|
negativePrompt: string;
|
||||||
|
imageUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StylePresetState = {
|
export type StylePresetState = {
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
activeStylePreset: StylePresetRecordWithImage | null;
|
activeStylePresetId: string | null;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
viewMode: boolean;
|
viewMode: boolean;
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
|
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
|
||||||
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string) => {
|
|
||||||
if (!presetPrompt || !presetPrompt.length) {
|
if (!presetPrompt || !presetPrompt.length) {
|
||||||
return ['', currentPrompt, ''];
|
return ['', currentPrompt, ''];
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks = presetPrompt.split(PRESET_PLACEHOLDER);
|
const chunks = presetPrompt.split(PRESET_PLACEHOLDER);
|
||||||
|
|
||||||
if (chunks.length === 1) {
|
if (chunks.length === 1) {
|
||||||
return ['', currentPrompt, chunks[0]];
|
return ['', currentPrompt, chunks[0] ?? ''];
|
||||||
} else {
|
} else {
|
||||||
return [chunks[0], currentPrompt, chunks[1]];
|
return [chunks[0] ?? '', currentPrompt, chunks[1] ?? ''];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ export type StylePresetRecordWithImage =
|
|||||||
*/
|
*/
|
||||||
const buildStylePresetsUrl = (path: string = '') => buildV1Url(`style_presets/${path}`);
|
const buildStylePresetsUrl = (path: string = '') => buildV1Url(`style_presets/${path}`);
|
||||||
|
|
||||||
const stylePresetsApi = api.injectEndpoints({
|
export const stylePresetsApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
getStylePreset: build.query<
|
getStylePreset: build.query<
|
||||||
paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'],
|
paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'],
|
||||||
|
Loading…
Reference in New Issue
Block a user