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",
|
||||
"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": {
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
|
@ -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 };
|
||||
|
@ -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 { 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,
|
||||
|
@ -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 {
|
||||
|
@ -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<HTMLTextAreaElement>(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 (
|
||||
<ViewModePrompt
|
||||
prompt={prompt}
|
||||
presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''}
|
||||
height={DEFAULT_HEIGHT}
|
||||
onExit={handleFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||
<Box pos="relative" w="full">
|
||||
{viewMode && (
|
||||
<ViewModePrompt prompt={prompt} presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''} />
|
||||
)}
|
||||
<Textarea
|
||||
id="negativePrompt"
|
||||
name="negativePrompt"
|
||||
@ -61,7 +57,6 @@ export const ParamNegativePrompt = memo(() => {
|
||||
fontSize="sm"
|
||||
variant="darkFilled"
|
||||
paddingRight={30}
|
||||
minH={DEFAULT_HEIGHT}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
|
@ -12,15 +12,24 @@ import { memo, useCallback, useRef } from 'react';
|
||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const DEFAULT_HEIGHT = 28;
|
||||
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||
|
||||
export const ParamPositivePrompt = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
||||
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
||||
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
||||
const 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 { t } = useTranslation();
|
||||
@ -30,18 +39,12 @@ export const ParamPositivePrompt = memo(() => {
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus, onFocusCursorAtEnd } = usePrompt({
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
||||
prompt,
|
||||
textareaRef: textareaRef,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
onFocusCursorAtEnd();
|
||||
}, 500);
|
||||
}, [onFocusCursorAtEnd]);
|
||||
|
||||
const focus: HotkeyCallback = useCallback(
|
||||
(e) => {
|
||||
onFocus();
|
||||
@ -52,20 +55,12 @@ export const ParamPositivePrompt = memo(() => {
|
||||
|
||||
useHotkeys('alt+a', focus, []);
|
||||
|
||||
if (viewMode) {
|
||||
return (
|
||||
<ViewModePrompt
|
||||
prompt={prompt}
|
||||
presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''}
|
||||
height={DEFAULT_HEIGHT}
|
||||
onExit={handleFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||
<Box pos="relative">
|
||||
{viewMode && (
|
||||
<ViewModePrompt prompt={prompt} presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''} />
|
||||
)}
|
||||
<Textarea
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
@ -73,7 +68,7 @@ export const ParamPositivePrompt = memo(() => {
|
||||
value={prompt}
|
||||
placeholder={t('parameters.globalPositivePromptPlaceholder')}
|
||||
onChange={onChange}
|
||||
minH={DEFAULT_HEIGHT}
|
||||
minH={28}
|
||||
onKeyDown={onKeyDown}
|
||||
variant="darkFilled"
|
||||
paddingRight={30}
|
||||
|
@ -6,17 +6,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold } from 'react-icons/pi';
|
||||
|
||||
export const ViewModePrompt = ({
|
||||
presetPrompt,
|
||||
prompt,
|
||||
height,
|
||||
onExit,
|
||||
}: {
|
||||
presetPrompt: string;
|
||||
prompt: string;
|
||||
height: number;
|
||||
onExit: () => void;
|
||||
}) => {
|
||||
export const ViewModePrompt = ({ presetPrompt, prompt }: { presetPrompt: string; prompt: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -26,43 +16,43 @@ export const ViewModePrompt = ({
|
||||
|
||||
const handleExitViewMode = useCallback(() => {
|
||||
dispatch(viewModeChanged(false));
|
||||
onExit();
|
||||
}, [dispatch, onExit]);
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDir="column"
|
||||
layerStyle="second"
|
||||
padding="8px 10px"
|
||||
borderRadius="base"
|
||||
height={height}
|
||||
onClick={handleExitViewMode}
|
||||
justifyContent="space-between"
|
||||
position="relative"
|
||||
>
|
||||
<Flex overflow="scroll">
|
||||
<Text fontSize="sm" lineHeight="1rem">
|
||||
{presetChunks.map((chunk, index) => {
|
||||
return (
|
||||
chunk && (
|
||||
<Text as="span" color={index === 1 ? 'white' : 'base.300'} key={index}>
|
||||
{chunk.trim()}{' '}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Text>
|
||||
</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')}>
|
||||
<Flex>
|
||||
<Icon as={PiEyeBold} color="base.500" boxSize="12px" />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Box position="absolute" top={0} bottom={0} left={0} right={0} zIndex={1} layerStyle="second" borderRadius="base">
|
||||
<Flex flexDir="column" onClick={handleExitViewMode} justifyContent="space-between" h="full" padding="8px 10px">
|
||||
<Flex overflow="scroll">
|
||||
<Text fontSize="sm" lineHeight="1rem" w="full">
|
||||
{presetChunks.map((chunk, index) => (
|
||||
<Text
|
||||
as="span"
|
||||
color={index === 1 ? 'white' : 'base.300'}
|
||||
fontWeight={index === 1 ? 'semibold' : 'normal'}
|
||||
key={index}
|
||||
>
|
||||
{chunk}
|
||||
</Text>
|
||||
))}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Tooltip label={t('stylePresets.viewModeTooltip')}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
insetInlineEnd={0}
|
||||
insetBlockStart={0}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={2}
|
||||
bg="base.900"
|
||||
opacity={0.8}
|
||||
backgroundClip="border-box"
|
||||
borderBottomStartRadius="base"
|
||||
>
|
||||
<Icon as={PiEyeBold} color="base.500" boxSize={4} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -58,13 +58,6 @@ export const usePrompt = ({ prompt, textareaRef, onChange: _onChange }: UseInser
|
||||
textareaRef.current?.focus();
|
||||
}, [textareaRef]);
|
||||
|
||||
const onFocusCursorAtEnd = useCallback(() => {
|
||||
onFocus();
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.setSelectionRange(prompt.length, prompt.length);
|
||||
}
|
||||
}, [onFocus, textareaRef, prompt]);
|
||||
|
||||
const handleClosePopover = useCallback(() => {
|
||||
onClose();
|
||||
onFocus();
|
||||
@ -96,6 +89,5 @@ export const usePrompt = ({ prompt, textareaRef, onChange: _onChange }: UseInser
|
||||
onSelect,
|
||||
onKeyDown,
|
||||
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 { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
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 { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
|
||||
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||
|
||||
import StylePresetImage from './StylePresetImage';
|
||||
|
||||
export const ActiveStylePreset = () => {
|
||||
const { 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 { t } = useTranslation();
|
||||
@ -22,7 +35,7 @@ export const ActiveStylePreset = () => {
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(viewModeChanged(false));
|
||||
dispatch(activeStylePresetChanged(null));
|
||||
dispatch(activeStylePresetIdChanged(null));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -33,7 +46,7 @@ export const ActiveStylePreset = () => {
|
||||
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
|
||||
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
|
||||
dispatch(viewModeChanged(false));
|
||||
dispatch(activeStylePresetChanged(null));
|
||||
dispatch(activeStylePresetIdChanged(null));
|
||||
},
|
||||
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
|
||||
);
|
||||
@ -48,7 +61,7 @@ export const ActiveStylePreset = () => {
|
||||
|
||||
if (!activeStylePreset) {
|
||||
return (
|
||||
<Flex h="25px" alignItems="center">
|
||||
<Flex h={25} alignItems="center">
|
||||
<Text fontSize="sm" fontWeight="semibold" color="base.300">
|
||||
{t('stylePresets.choosePromptTemplate')}
|
||||
</Text>
|
||||
@ -56,47 +69,45 @@ export const ActiveStylePreset = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||
<Flex flexDir="column">
|
||||
<Text fontSize="sm" fontWeight="semibold" color="base.300" noOfLines={1}>
|
||||
{activeStylePreset.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="1">
|
||||
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||
<IconButton
|
||||
onClick={handleToggleViewMode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.toggleViewMode')}
|
||||
colorScheme={viewMode ? 'invokeBlue' : 'base'}
|
||||
icon={<PiEyeBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.flatten')}>
|
||||
<IconButton
|
||||
onClick={handleFlattenPrompts}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.flatten')}
|
||||
icon={<PiStackSimpleBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
|
||||
<IconButton
|
||||
onClick={handleClearActiveStylePreset}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.clearTemplateSelection')}
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<Flex gap={2} alignItems="center">
|
||||
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||
<Flex flexDir="column">
|
||||
<Badge colorScheme="invokeBlue" variant="subtle">
|
||||
{activeStylePreset.name}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
<Flex gap={1}>
|
||||
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||
<IconButton
|
||||
onClick={handleToggleViewMode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.toggleViewMode')}
|
||||
colorScheme={viewMode ? 'invokeBlue' : 'base'}
|
||||
icon={<PiEyeBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.flatten')}>
|
||||
<IconButton
|
||||
onClick={handleFlattenPrompts}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.flatten')}
|
||||
icon={<PiStackSimpleBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
|
||||
<IconButton
|
||||
onClick={handleClearActiveStylePreset}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.clearTemplateSelection')}
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
@ -18,16 +18,20 @@ export type StylePresetFormData = {
|
||||
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 [updateStylePreset] = useUpdateStylePresetMutation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultValues = useAppSelector((s) => s.stylePresetModal.prefilledFormData);
|
||||
|
||||
const { handleSubmit, control, register, formState } = useForm<StylePresetFormData>({
|
||||
defaultValues: defaultValues || {
|
||||
defaultValues: formData || {
|
||||
name: '',
|
||||
positivePrompt: '',
|
||||
negativePrompt: '',
|
||||
@ -68,12 +72,12 @@ export const StylePresetForm = ({ updatingStylePresetId }: { updatingStylePreset
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap="4">
|
||||
<Flex alignItems="center" gap="4">
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Flex alignItems="center" gap={4}>
|
||||
<StylePresetImageField control={control} name="image" />
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel>{t('stylePresets.name')}</FormLabel>
|
||||
<Input size="md" {...register('name', { required: true, minLength: 1 })} required={true} />
|
||||
<Input size="md" {...register('name', { required: true, minLength: 1 })} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'
|
||||
|
||||
import type { StylePresetFormData } from './StylePresetForm';
|
||||
|
||||
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData>) => {
|
||||
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData, 'image'>) => {
|
||||
const { field } = useController(props);
|
||||
const { t } = useTranslation();
|
||||
const onDropAccepted = useCallback(
|
||||
@ -36,7 +36,7 @@ export const StylePresetImageField = (props: UseControllerProps<StylePresetFormD
|
||||
return (
|
||||
<Box position="relative" flexShrink={0}>
|
||||
<Image
|
||||
src={URL.createObjectURL(field.value as File)}
|
||||
src={URL.createObjectURL(field.value)}
|
||||
objectFit="cover"
|
||||
objectPosition="50% 50%"
|
||||
w={65}
|
||||
|
@ -14,7 +14,7 @@ export const StylePresetList = ({ title, data }: { title: string; data: StylePre
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<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" />
|
||||
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
||||
{title}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
|
||||
import {
|
||||
isModalOpenChanged,
|
||||
prefilledFormDataChanged,
|
||||
updatingStylePresetIdChanged,
|
||||
} 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 type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
@ -20,40 +19,35 @@ import StylePresetImage from './StylePresetImage';
|
||||
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [deleteStylePreset] = useDeleteStylePresetMutation();
|
||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
||||
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const imageUrlToBlob = useImageUrlToBlob();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClickEdit = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
const { name, preset_data } = preset;
|
||||
const { positive_prompt, negative_prompt } = preset_data;
|
||||
let imageBlob = null;
|
||||
if (preset.image) {
|
||||
imageBlob = await imageUrlToBlob(preset.image, 100);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
prefilledFormDataChanged({
|
||||
name,
|
||||
positivePrompt: positive_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(isModalOpenChanged(true));
|
||||
},
|
||||
[dispatch, preset, imageUrlToBlob]
|
||||
[dispatch, preset]
|
||||
);
|
||||
|
||||
const handleClickApply = useCallback(async () => {
|
||||
dispatch(activeStylePresetChanged(preset));
|
||||
dispatch(activeStylePresetIdChanged(preset.id));
|
||||
dispatch(isMenuOpenChanged(false));
|
||||
}, [dispatch, preset]);
|
||||
}, [dispatch, preset.id]);
|
||||
|
||||
const handleClickDelete = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
@ -81,11 +75,12 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
gap="4"
|
||||
gap={4}
|
||||
onClick={handleClickApply}
|
||||
cursor="pointer"
|
||||
_hover={{ backgroundColor: 'base.750' }}
|
||||
padding="10px"
|
||||
py={3}
|
||||
px={2}
|
||||
borderRadius="base"
|
||||
alignItems="flex-start"
|
||||
w="full"
|
||||
@ -93,11 +88,11 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
<StylePresetImage presetImageUrl={preset.image} />
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
|
||||
<Flex alignItems="center" gap="2">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Text fontSize="md" noOfLines={2}>
|
||||
{preset.name}
|
||||
</Text>
|
||||
{activeStylePreset && activeStylePreset.id === preset.id && (
|
||||
{activeStylePresetId === preset.id && (
|
||||
<Badge
|
||||
color="invokeBlue.400"
|
||||
borderColor="invokeBlue.700"
|
||||
@ -111,7 +106,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
</Flex>
|
||||
|
||||
{!preset.is_default && (
|
||||
<Flex alignItems="center" gap="1">
|
||||
<Flex alignItems="center" gap={1}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@ -131,7 +126,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex flexDir="column" gap="1">
|
||||
<Flex flexDir="column" gap={1}>
|
||||
<Text fontSize="xs">
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{t('stylePresets.positivePrompt')}:
|
||||
|
@ -18,7 +18,7 @@ import StylePresetSearch from './StylePresetSearch';
|
||||
export const StylePresetMenu = () => {
|
||||
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||
const { data } = useListStylePresetsQuery(undefined, {
|
||||
selectFromResult: ({ data, error, isLoading }) => {
|
||||
selectFromResult: ({ data }) => {
|
||||
const filteredData =
|
||||
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
|
||||
|
||||
@ -36,8 +36,6 @@ export const StylePresetMenu = () => {
|
||||
|
||||
return {
|
||||
data: groupedData,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -52,8 +50,8 @@ export const StylePresetMenu = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap="2" padding="10px" layerStyle="second">
|
||||
<Flex alignItems="center" gap="10" w="full" justifyContent="space-between">
|
||||
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||
<StylePresetSearch />
|
||||
<IconButton
|
||||
icon={<PiPlusBold />}
|
||||
@ -68,7 +66,7 @@ export const StylePresetMenu = () => {
|
||||
</Flex>
|
||||
|
||||
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
||||
<Text m="20px" textAlign="center">
|
||||
<Text p={10} textAlign="center">
|
||||
{t('stylePresets.noMatchingTemplates')}
|
||||
</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 { isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
import { ActiveStylePreset } from './ActiveStylePreset';
|
||||
|
||||
export const StylePresetMenuTrigger = () => {
|
||||
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
|
||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
dispatch(isMenuOpenChanged(!isMenuOpen));
|
||||
@ -17,20 +18,19 @@ export const StylePresetMenuTrigger = () => {
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="button"
|
||||
onClick={handleToggle}
|
||||
backgroundColor="base.800"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
padding="5px 10px"
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius="base"
|
||||
gap="2"
|
||||
borderTop="2px solid transparent"
|
||||
borderColor={activeStylePreset ? 'invokeBlue.200' : 'transparent'}
|
||||
gap={1}
|
||||
role="button"
|
||||
>
|
||||
<ActiveStylePreset />
|
||||
|
||||
<Icon boxSize="15px" as={PiCaretDownBold} color="base.300" />
|
||||
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -6,29 +6,65 @@ import {
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||
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 type { StylePresetFormData } from './StylePresetForm';
|
||||
import { StylePresetForm } from './StylePresetForm';
|
||||
|
||||
export const StylePresetModal = () => {
|
||||
const [formData, setFormData] = useState<StylePresetFormData | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
|
||||
const updatingStylePresetId = useAppSelector((s) => s.stylePresetModal.updatingStylePresetId);
|
||||
const prefilledFormData = useAppSelector((s) => s.stylePresetModal.prefilledFormData);
|
||||
|
||||
const modalTitle = useMemo(() => {
|
||||
return updatingStylePresetId ? t('stylePresets.updatePromptTemplate') : t('stylePresets.createPromptTemplate');
|
||||
}, [updatingStylePresetId, t]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
dispatch(prefilledFormDataChanged(null));
|
||||
dispatch(updatingStylePresetIdChanged(null));
|
||||
dispatch(isModalOpenChanged(false));
|
||||
}, [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 (
|
||||
<Modal isOpen={isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
|
||||
<ModalOverlay />
|
||||
@ -36,9 +72,13 @@ export const StylePresetModal = () => {
|
||||
<ModalHeader>{modalTitle}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody display="flex" flexDir="column" gap={4}>
|
||||
<StylePresetForm updatingStylePresetId={updatingStylePresetId} />
|
||||
{!prefilledFormData || formData ? (
|
||||
<StylePresetForm updatingStylePresetId={updatingStylePresetId} formData={formData} />
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
<ModalFooter p={2} />
|
||||
</ModalContent>
|
||||
</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 type { ChangeEventHandler } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { UseControllerProps } from 'react-hook-form';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBracketsCurlyBold } from 'react-icons/pi';
|
||||
|
||||
import type { StylePresetFormData } from './StylePresetForm';
|
||||
|
||||
interface Props extends UseControllerProps<StylePresetFormData> {
|
||||
interface Props extends UseControllerProps<StylePresetFormData, 'negativePrompt' | 'positivePrompt'> {
|
||||
label: string;
|
||||
}
|
||||
|
||||
@ -26,7 +25,7 @@ export const StylePresetPromptField = (props: Props) => {
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return field.value as string;
|
||||
return field.value;
|
||||
}, [field.value]);
|
||||
|
||||
const insertPromptPlaceholder = useCallback(() => {
|
||||
@ -45,16 +44,17 @@ export const StylePresetPromptField = (props: Props) => {
|
||||
const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex alignItems="center" gap="1">
|
||||
<FormControl orientation="vertical" gap={3}>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<FormLabel>{props.label}</FormLabel>
|
||||
<IconButton
|
||||
<Button
|
||||
onClick={insertPromptPlaceholder}
|
||||
size="sm"
|
||||
icon={<PiBracketsCurlyBold />}
|
||||
size="xs"
|
||||
aria-label={t('stylePresets.insertPlaceholder')}
|
||||
isDisabled={isPromptPresent}
|
||||
/>
|
||||
>
|
||||
{t('stylePresets.insertPlaceholder')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
|
||||
|
@ -1,6 +1,7 @@
|
||||
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) => {
|
||||
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
||||
@ -9,9 +10,20 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
|
||||
};
|
||||
|
||||
export const usePresetModifiedPrompts = () => {
|
||||
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
|
||||
const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present);
|
||||
|
||||
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||
|
||||
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
let activeStylePreset = null;
|
||||
if (data) {
|
||||
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
|
||||
}
|
||||
return { activeStylePreset };
|
||||
},
|
||||
});
|
||||
|
||||
if (!activeStylePreset) {
|
||||
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt };
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import type { PayloadAction } 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 = {
|
||||
isModalOpen: false,
|
||||
@ -20,7 +19,7 @@ export const stylePresetModalSlice = createSlice({
|
||||
updatingStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||
state.updatingStylePresetId = action.payload;
|
||||
},
|
||||
prefilledFormDataChanged: (state, action: PayloadAction<StylePresetFormData | null>) => {
|
||||
prefilledFormDataChanged: (state, action: PayloadAction<PrefilledFormData | null>) => {
|
||||
state.prefilledFormData = action.payload;
|
||||
},
|
||||
},
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type { PayloadAction } 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';
|
||||
|
||||
const initialState: StylePresetState = {
|
||||
isMenuOpen: false,
|
||||
activeStylePreset: null,
|
||||
activeStylePresetId: null,
|
||||
searchTerm: '',
|
||||
viewMode: false,
|
||||
};
|
||||
@ -18,8 +18,8 @@ export const stylePresetSlice = createSlice({
|
||||
isMenuOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isMenuOpen = action.payload;
|
||||
},
|
||||
activeStylePresetChanged: (state, action: PayloadAction<StylePresetRecordWithImage | null>) => {
|
||||
state.activeStylePreset = action.payload;
|
||||
activeStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||
state.activeStylePresetId = action.payload;
|
||||
},
|
||||
searchTermChanged: (state, action: PayloadAction<string>) => {
|
||||
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;
|
||||
|
||||
/* 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 = {
|
||||
isModalOpen: boolean;
|
||||
updatingStylePresetId: string | null;
|
||||
prefilledFormData: StylePresetFormData | null;
|
||||
prefilledFormData: PrefilledFormData | null;
|
||||
};
|
||||
|
||||
export type PrefilledFormData = {
|
||||
name: string;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
imageUrl: string | null;
|
||||
};
|
||||
|
||||
export type StylePresetState = {
|
||||
isMenuOpen: boolean;
|
||||
activeStylePreset: StylePresetRecordWithImage | null;
|
||||
activeStylePresetId: string | null;
|
||||
searchTerm: string;
|
||||
viewMode: boolean;
|
||||
};
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||
|
||||
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string) => {
|
||||
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
|
||||
if (!presetPrompt || !presetPrompt.length) {
|
||||
return ['', currentPrompt, ''];
|
||||
}
|
||||
|
||||
const chunks = presetPrompt.split(PRESET_PLACEHOLDER);
|
||||
|
||||
if (chunks.length === 1) {
|
||||
return ['', currentPrompt, chunks[0]];
|
||||
return ['', currentPrompt, chunks[0] ?? ''];
|
||||
} 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 stylePresetsApi = api.injectEndpoints({
|
||||
export const stylePresetsApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getStylePreset: build.query<
|
||||
paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'],
|
||||
|
Loading…
Reference in New Issue
Block a user