fix path for style_preset_images, fix png type when converting blobs to files, built view mode components

This commit is contained in:
Mary Hipp 2024-08-08 12:31:20 -04:00
parent 0b0abfbe8f
commit 9a4d075074
25 changed files with 417 additions and 155 deletions

View File

@ -47,11 +47,11 @@ async def get_style_preset(
},
)
async def update_style_preset(
image: Optional[UploadFile] = File(description="The image file to upload", default=None),
style_preset_id: str = Path(description="The id of the style preset to update"),
name: str = Form(description="The name of the style preset to create"),
positive_prompt: str = Form(description="The positive prompt of the style preset"),
negative_prompt: str = Form(description="The negative prompt of the style preset"),
image: Optional[UploadFile] = File(description="The image file to upload", default=None),
) -> StylePresetRecordWithImage:
"""Updates a style preset"""
if image is not None:
@ -73,8 +73,8 @@ async def update_style_preset(
else:
try:
ApiDependencies.invoker.services.style_preset_images_service.delete(style_preset_id)
except ValueError as e:
raise HTTPException(status_code=409, detail=str(e))
except StylePresetImageFileNotFoundException:
pass
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
changes = StylePresetChanges(name=name, preset_data=preset_data)

View File

@ -153,7 +153,7 @@ class InvokeAIAppConfig(BaseSettings):
db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.")
outputs_dir: Path = Field(default=Path("outputs"), description="Path to directory for outputs.")
custom_nodes_dir: Path = Field(default=Path("nodes"), description="Path to directory for custom nodes.")
style_preset_images_path: Path = Field(default=Path("style_preset_images"), description="Path to directory for style preset images.")
style_preset_images_dir: Path = Field(default=Path("style_preset_images"), description="Path to directory for style preset images.")
# LOGGING
log_handlers: list[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>".')
@ -301,6 +301,11 @@ class InvokeAIAppConfig(BaseSettings):
"""Path to the models directory, resolved to an absolute path.."""
return self._resolve(self.models_dir)
@property
def style_preset_images_path(self) -> Path:
"""Path to the style preset images directory, resolved to an absolute path.."""
return self._resolve(self.style_preset_images_dir)
@property
def convert_cache_path(self) -> Path:
"""Path to the converted cache models directory, resolved to an absolute path.."""

View File

@ -13,12 +13,14 @@ export const useImageUrlToBlob = () => {
new Promise<Blob | null>((resolve) => {
const img = new Image();
img.onload = () => {
console.log("on load")
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
if (!context) {
console.log("no context")
return;
}
context.drawImage(img, 0, 0);

View File

@ -6,6 +6,7 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { isModalOpenChanged as isStylePresetModalOpenChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
@ -58,8 +59,17 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { downloadImage } = useDownloadImage();
const templates = useStore($templates);
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
useImageActions(imageDTO?.image_name);
const {
recallAll,
remix,
recallSeed,
recallPrompts,
hasMetadata,
hasSeed,
hasPrompts,
isLoadingMetadata,
createAsPreset,
} = useImageActions(imageDTO?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
@ -133,11 +143,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
dispatch(setActiveTab('upscaling'));
}, [dispatch, imageDTO]);
const handleCreatePreset = useCallback(() => {
dispatch(createPresetFromImageChanged(imageDTO));
dispatch(isMenuOpenChanged(true));
}, [dispatch, imageDTO]);
return (
<>
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
@ -192,7 +197,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={handleCreatePreset}
onClickCapture={createAsPreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
Create Preset

View File

@ -1,8 +1,12 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import { useImageUrlToBlob } from '../../../common/hooks/useImageUrlToBlob';
import { prefilledFormDataChanged, isModalOpenChanged } from '../../stylePresets/store/stylePresetModalSlice';
import { useGetImageDTOQuery } from '../../../services/api/endpoints/images';
import { skipToken } from '@reduxjs/toolkit/query';
export const useImageActions = (image_name?: string) => {
const activeTabName = useAppSelector(activeTabNameSelector);
@ -10,6 +14,9 @@ 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)
useEffect(() => {
const parseMetadata = async () => {
@ -61,5 +68,17 @@ export const useImageActions = (image_name?: string) => {
parseAndRecallPrompts(metadata);
}, [metadata]);
return { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata };
const createAsPreset = useCallback(async () => {
if (image_name && metadata && imageDTO) {
const positivePrompt = await handlers.positivePrompt.parse(metadata)
const negativePrompt = await handlers.negativePrompt.parse(metadata)
const imageBlob = await imageUrlToBlob(imageDTO.image_url)
dispatch(prefilledFormDataChanged({ name: "", positivePrompt, negativePrompt, image: imageBlob ? new File([imageBlob], "stylePreset.png", { type: 'image/png', }) : null }))
dispatch(isModalOpenChanged(true))
}
}, [image_name, metadata, dispatch, imageDTO])
return { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata, createAsPreset };
};

View File

@ -125,7 +125,7 @@ const parseCreatedBy: MetadataParseFunc<string> = (metadata) => getProperty(meta
const parseGenerationMode: MetadataParseFunc<string> = (metadata) => getProperty(metadata, 'generation_mode', isString);
const parsePositivePrompt: MetadataParseFunc<ParameterPositivePrompt> = (metadata) =>
export const parsePositivePrompt: MetadataParseFunc<ParameterPositivePrompt> = (metadata) =>
getProperty(metadata, 'positive_prompt', isParameterPositivePrompt);
const parseNegativePrompt: MetadataParseFunc<ParameterNegativePrompt> = (metadata) =>

View File

@ -7,10 +7,15 @@ import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewModePrompt } from '../Prompts/ViewModePrompt';
const DEFAULT_HEIGHT = 20;
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 textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const _onChange = useCallback(
@ -25,6 +30,16 @@ export const ParamNegativePrompt = memo(() => {
onChange: _onChange,
});
if (viewMode) {
return (
<ViewModePrompt
prompt={prompt}
presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''}
height={DEFAULT_HEIGHT}
/>
);
}
return (
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
<Box pos="relative">
@ -39,6 +54,7 @@ export const ParamNegativePrompt = memo(() => {
fontSize="sm"
variant="darkFilled"
paddingRight={30}
minH={DEFAULT_HEIGHT}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />

View File

@ -11,11 +11,16 @@ import { memo, useCallback, useRef } from 'react';
import type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { ViewModePrompt } from '../Prompts/ViewModePrompt';
const DEFAULT_HEIGHT = 28;
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 textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
@ -41,6 +46,16 @@ export const ParamPositivePrompt = memo(() => {
useHotkeys('alt+a', focus, []);
if (viewMode) {
return (
<ViewModePrompt
prompt={prompt}
presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''}
height={DEFAULT_HEIGHT}
/>
);
}
return (
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
<Box pos="relative">
@ -51,7 +66,7 @@ export const ParamPositivePrompt = memo(() => {
value={prompt}
placeholder={t('parameters.globalPositivePromptPlaceholder')}
onChange={onChange}
minH={28}
minH={DEFAULT_HEIGHT}
onKeyDown={onKeyDown}
variant="darkFilled"
paddingRight={30}

View File

@ -7,7 +7,6 @@ import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPo
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { memo } from 'react';
const concatPromptsSelector = createSelector(
@ -19,14 +18,11 @@ const concatPromptsSelector = createSelector(
export const Prompts = memo(() => {
const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
return (
<Flex flexDir="column" gap={2}>
<ParamPositivePrompt />
<Flex>{presetModifiedPositivePrompt}</Flex>
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
<ParamNegativePrompt />
<Flex>{presetModifiedNegativePrompt}</Flex>
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
</Flex>
);

View File

@ -0,0 +1,67 @@
import { Flex, Icon, Text, Tooltip, Box } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from '../../../../app/store/storeHooks';
import { useCallback, useMemo } from 'react';
import { PiEyeBold, PiQuestionBold } from 'react-icons/pi';
import { viewModeChanged } from '../../../stylePresets/store/stylePresetSlice';
import { getViewModeChunks } from '../../../stylePresets/util/getViewModeChunks';
export const ViewModePrompt = ({
presetPrompt,
prompt,
height,
}: {
presetPrompt: string;
prompt: string;
height: number;
}) => {
const dispatch = useAppDispatch();
const presetChunks = useMemo(() => {
return getViewModeChunks(prompt, presetPrompt);
}, [presetPrompt, prompt]);
const handleExitViewMode = useCallback(() => {
dispatch(viewModeChanged(false));
}, [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'}>
{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={
'This is how your prompt will look with your currently selected preset. To edit your prompt, click anywhere in the text box.'
}
>
<Flex>
<Icon as={PiEyeBold} color="base.500" boxSize="12px" />
</Flex>
</Tooltip>
</Flex>
</Box>
</Flex>
);
};

View File

@ -0,0 +1,98 @@
import { useDisclosure } from '@invoke-ai/ui-library';
import { isNil } from 'lodash-es';
import type { ChangeEventHandler, FormEvent, FormEventHandler, KeyboardEventHandler, RefObject, SyntheticEvent } from 'react';
import { useCallback } from 'react';
import { flushSync } from 'react-dom';
type UseInsertTriggerArg = {
prompt: string;
paragraphRef: RefObject<HTMLParagraphElement>;
onChange: (v: string) => void;
};
export const usePromptContentEditable = ({ prompt, paragraphRef, onChange: _onChange }: UseInsertTriggerArg) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const onChange = useCallback(
(e: any) => {
e.preventDefault();
_onChange(e.data)
},
[_onChange]
);
const insertTrigger = useCallback(
(v: string) => {
const element = paragraphRef.current;
if (!element) {
return;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// Insert at the end if no selection found
const newPrompt = prompt + v;
flushSync(() => {
_onChange(newPrompt);
});
return;
}
const range = selection.getRangeAt(0);
const cursorPosition = range.startOffset;
console.log({ cursorPosition })
const updatedPrompt = prompt.slice(0, cursorPosition) + v + prompt.slice(cursorPosition);
console.log({ updatedPrompt })
flushSync(() => {
_onChange(updatedPrompt);
});
},
[paragraphRef, _onChange, prompt]
);
const onFocus = useCallback(() => {
paragraphRef.current?.focus();
}, [paragraphRef]);
const handleClosePopover = useCallback(() => {
onClose();
onFocus();
}, [onFocus, onClose]);
const onSelect = useCallback(
(v: string) => {
insertTrigger(v)
handleClosePopover();
},
[handleClosePopover, insertTrigger]
);
const onKeyDown: KeyboardEventHandler<HTMLParagraphElement> = useCallback(
(e) => {
if (e.key === '<') {
onOpen();
e.preventDefault();
}
},
[onOpen]
);
return {
onChange,
isOpen,
onClose,
onOpen,
onSelect,
onKeyDown,
onFocus,
};
};

View File

@ -2,14 +2,15 @@ import { Flex, IconButton, Text, Box, ButtonGroup } 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 } from 'features/stylePresets/store/stylePresetSlice';
import { activeStylePresetChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
import type { MouseEventHandler } from 'react';
import { useCallback } from 'react';
import { PiStackSimpleBold, PiXBold } from 'react-icons/pi';
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
import StylePresetImage from './StylePresetImage';
export const ActiveStylePreset = () => {
const { activeStylePreset } = useAppSelector((s) => s.stylePreset);
const { activeStylePreset, viewMode } = useAppSelector((s) => s.stylePreset);
const dispatch = useAppDispatch();
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
@ -17,6 +18,7 @@ export const ActiveStylePreset = () => {
const handleClearActiveStylePreset = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(viewModeChanged(false));
dispatch(activeStylePresetChanged(null));
},
[dispatch]
@ -27,11 +29,20 @@ export const ActiveStylePreset = () => {
e.stopPropagation();
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
dispatch(viewModeChanged(false));
dispatch(activeStylePresetChanged(null));
},
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
);
const handleToggleViewMode = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(viewModeChanged(!viewMode));
},
[dispatch, viewMode]
);
if (!activeStylePreset) {
return (
<Flex h="25px" alignItems="center">
@ -47,12 +58,20 @@ export const ActiveStylePreset = () => {
<Flex gap="2" alignItems="center">
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
<Flex flexDir="column">
<Text fontSize="sm" fontWeight="semibold" color="base.300">
<Text fontSize="sm" fontWeight="semibold" color="base.300" noOfLines={1}>
{activeStylePreset.name}
</Text>
</Flex>
</Flex>
<Flex gap="1">
<IconButton
onClick={handleToggleViewMode}
variant="outline"
size="sm"
aria-label="View"
colorScheme={viewMode ? 'invokeBlue' : 'base'}
icon={<PiEyeBold />}
/>
<IconButton
onClick={handleFlattenPrompts}
variant="outline"

View File

@ -1,13 +1,11 @@
import { Button, Flex, FormControl, FormLabel, Icon, Input, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useStylePresetFields } from 'features/stylePresets/hooks/useStylePresetFields';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { PiBracketsCurlyBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useCreateStylePresetMutation, useUpdateStylePresetMutation } from 'services/api/endpoints/stylePresets';
import { StylePresetPromptField } from './StylePresetPromptField';
@ -20,15 +18,20 @@ export type StylePresetFormData = {
image: File | null;
};
export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordWithImage | null }) => {
export const StylePresetForm = ({ updatingStylePresetId }: { updatingStylePresetId: string | null }) => {
const [createStylePreset] = useCreateStylePresetMutation();
const [updateStylePreset] = useUpdateStylePresetMutation();
const dispatch = useAppDispatch();
const stylePresetFieldDefaults = useStylePresetFields(updatingPreset);
const defaultValues = useAppSelector((s) => s.stylePresetModal.prefilledFormData);
const { handleSubmit, control, formState, reset, register } = useForm<StylePresetFormData>({
defaultValues: stylePresetFieldDefaults,
defaultValues: defaultValues || {
name: '',
positivePrompt: '',
negativePrompt: '',
image: null,
},
});
const handleClickSave = useCallback<SubmitHandler<StylePresetFormData>>(
@ -41,9 +44,9 @@ export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePrese
};
try {
if (updatingPreset) {
if (updatingStylePresetId) {
await updateStylePreset({
id: updatingPreset.id,
id: updatingStylePresetId,
...payload,
}).unwrap();
} else {
@ -56,10 +59,10 @@ export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePrese
});
}
dispatch(updatingStylePresetChanged(null));
dispatch(updatingStylePresetIdChanged(null));
dispatch(isModalOpenChanged(false));
},
[dispatch, updatingPreset, updateStylePreset, createStylePreset]
[dispatch, updatingStylePresetId, updateStylePreset, createStylePreset]
);
return (

View File

@ -1,31 +1,52 @@
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
import type { MouseEvent } from 'react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import {
isModalOpenChanged,
prefilledFormDataChanged,
updatingStylePresetIdChanged,
} from 'features/stylePresets/store/stylePresetModalSlice';
import { activeStylePresetChanged, isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
import { useCallback } from 'react';
import { PiPencilBold, PiTrashBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
import StylePresetImage from './StylePresetImage';
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
const dispatch = useAppDispatch();
const [deleteStylePreset] = useDeleteStylePresetMutation();
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
const { isOpen, onOpen, onClose } = useDisclosure();
const imageUrlToBlob = useImageUrlToBlob();
const handleClickEdit = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
dispatch(updatingStylePresetChanged(preset));
const { name, preset_data } = preset;
const { positive_prompt, negative_prompt } = preset_data;
let imageBlob = null;
if (preset.image) {
imageBlob = await imageUrlToBlob(preset.image);
}
dispatch(
prefilledFormDataChanged({
name,
positivePrompt: positive_prompt,
negativePrompt: negative_prompt,
image: imageBlob ? new File([imageBlob], `style_preset_${preset.id}.png`, { type: 'image/png' }) : null,
})
);
dispatch(updatingStylePresetIdChanged(preset.id));
dispatch(isModalOpenChanged(true));
},
[dispatch, preset]
);
const handleClickApply = useCallback(() => {
const handleClickApply = useCallback(async () => {
dispatch(activeStylePresetChanged(preset));
dispatch(isMenuOpenChanged(false));
}, [dispatch, preset]);
@ -53,14 +74,16 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
_hover={{ backgroundColor: 'base.750' }}
padding="10px"
borderRadius="base"
alignItems="center"
alignItems="flex-start"
w="full"
>
<StylePresetImage presetImageUrl={preset.image} />
<Flex flexDir="column" w="full">
<Flex w="full" justifyContent="space-between">
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
<Flex alignItems="center" gap="2">
<Text fontSize="md">{preset.name}</Text>
<Text fontSize="md" noOfLines={2}>
{preset.name}
</Text>
{activeStylePreset && activeStylePreset.id === preset.id && (
<Badge
color="invokeBlue.400"
@ -87,6 +110,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
variant="ghost"
aria-label="Delete"
onClick={handleClickDelete}
colorScheme="error"
icon={<PiTrashBold />}
/>
</Flex>

View File

@ -1,7 +1,11 @@
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import {
isModalOpenChanged,
prefilledFormDataChanged,
updatingStylePresetIdChanged,
} from 'features/stylePresets/store/stylePresetModalSlice';
import { useCallback } from 'react';
import { PiPlusBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
@ -40,12 +44,13 @@ export const StylePresetMenu = () => {
const dispatch = useAppDispatch();
const handleClickAddNew = useCallback(() => {
dispatch(updatingStylePresetChanged(null));
dispatch(prefilledFormDataChanged(null));
dispatch(updatingStylePresetIdChanged(null));
dispatch(isModalOpenChanged(true));
}, [dispatch]);
return (
<Flex flexDir="column" gap="2" padding="10px">
<Flex flexDir="column" gap="2" padding="10px" layerStyle="second">
<Flex alignItems="center" gap="10" w="full" justifyContent="space-between">
<StylePresetSearch />
<IconButton

View File

@ -8,6 +8,7 @@ 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 handleToggle = useCallback(() => {
@ -16,7 +17,7 @@ export const StylePresetMenuTrigger = () => {
return (
<Flex
as="button"
// as="button"
onClick={handleToggle}
backgroundColor="base.800"
justifyContent="space-between"
@ -24,6 +25,8 @@ export const StylePresetMenuTrigger = () => {
padding="5px 10px"
borderRadius="base"
gap="2"
borderTop="2px solid transparent"
borderColor={activeStylePreset ? 'invokeBlue.200' : 'transparent'}
>
<ActiveStylePreset />

View File

@ -8,7 +8,7 @@ import {
ModalOverlay,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { isModalOpenChanged, updatingStylePresetIdChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { useCallback, useMemo } from 'react';
import { StylePresetForm } from './StylePresetForm';
@ -16,14 +16,14 @@ import { StylePresetForm } from './StylePresetForm';
export const StylePresetModal = () => {
const dispatch = useAppDispatch();
const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
const updatingStylePreset = useAppSelector((s) => s.stylePresetModal.updatingStylePreset);
const updatingStylePresetId = useAppSelector((s) => s.stylePresetModal.updatingStylePresetId);
const modalTitle = useMemo(() => {
return updatingStylePreset ? `Update Style Preset` : `Create Style Preset`;
}, [updatingStylePreset]);
return updatingStylePresetId ? `Update Style Preset` : `Create Style Preset`;
}, [updatingStylePresetId]);
const handleCloseModal = useCallback(() => {
dispatch(updatingStylePresetChanged(null));
dispatch(updatingStylePresetIdChanged(null));
dispatch(isModalOpenChanged(false));
}, [dispatch]);
@ -34,7 +34,7 @@ export const StylePresetModal = () => {
<ModalHeader>{modalTitle}</ModalHeader>
<ModalCloseButton />
<ModalBody display="flex" flexDir="column" gap={4}>
<StylePresetForm updatingPreset={updatingStylePreset} />
<StylePresetForm updatingStylePresetId={updatingStylePresetId} />
</ModalBody>
<ModalFooter />
</ModalContent>

View File

@ -1,45 +0,0 @@
import { useCallback, useMemo } from 'react';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useAppSelector } from '../../../app/store/storeHooks';
import { useDebouncedMetadata } from '../../../services/api/hooks/useDebouncedMetadata';
import { handlers } from '../../metadata/util/handlers';
import { useImageUrlToBlob } from '../../../common/hooks/useImageUrlToBlob';
export const useStylePresetFields = (preset: StylePresetRecordWithImage | null) => {
const createPresetFromImage = useAppSelector(s => s.stylePresetModal.createPresetFromImage)
const imageUrlToBlob = useImageUrlToBlob();
const getStylePresetFieldDefaults = useCallback(async () => {
if (preset) {
let file: File | null = null;
if (preset.image) {
const blob = await imageUrlToBlob(preset.image);
if (blob) {
file = new File([blob], "name");
}
}
return {
name: preset.name,
positivePrompt: preset.preset_data.positive_prompt || "",
negativePrompt: preset.preset_data.negative_prompt || "",
image: file
};
}
return {
name: "",
positivePrompt: "",
negativePrompt: "",
image: null
};
}, [
preset
]);
return getStylePresetFieldDefaults;
};

View File

@ -1,16 +1,15 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import type { StylePresetModalState, StylePresetPrefillOptions } from './types';
import { ImageDTO } from '../../../services/api/types';
import type { StylePresetModalState } from './types';
import { StylePresetFormData } from '../components/StylePresetForm';
export const initialState: StylePresetModalState = {
isModalOpen: false,
updatingStylePreset: null,
createPresetFromImage: null
updatingStylePresetId: null,
prefilledFormData: null
};
@ -21,15 +20,15 @@ export const stylePresetModalSlice = createSlice({
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
},
updatingStylePresetChanged: (state, action: PayloadAction<StylePresetRecordWithImage | null>) => {
state.updatingStylePreset = action.payload;
updatingStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
state.updatingStylePresetId = action.payload;
},
createPresetFromImageChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.createPresetFromImage = action.payload;
prefilledFormDataChanged: (state, action: PayloadAction<StylePresetFormData | null>) => {
state.prefilledFormData = action.payload;
},
},
});
export const { isModalOpenChanged, updatingStylePresetChanged, createPresetFromImageChanged } = stylePresetModalSlice.actions;
export const { isModalOpenChanged, updatingStylePresetIdChanged, prefilledFormDataChanged } = stylePresetModalSlice.actions;
export const selectStylePresetModalSlice = (state: RootState) => state.stylePresetModal;

View File

@ -9,7 +9,8 @@ import type { StylePresetState } from './types';
export const initialState: StylePresetState = {
isMenuOpen: false,
activeStylePreset: null,
searchTerm: ""
searchTerm: "",
viewMode: false
};
@ -26,9 +27,12 @@ export const stylePresetSlice = createSlice({
searchTermChanged: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
viewModeChanged: (state, action: PayloadAction<boolean>) => {
state.viewMode = action.payload;
},
},
});
export const { isMenuOpenChanged, activeStylePresetChanged, searchTermChanged } = stylePresetSlice.actions;
export const { isMenuOpenChanged, activeStylePresetChanged, searchTermChanged, viewModeChanged } = stylePresetSlice.actions;
export const selectStylePresetSlice = (state: RootState) => state.stylePreset;

View File

@ -1,21 +1,17 @@
import type { StylePresetRecordWithImage } from "services/api/endpoints/stylePresets";
import { ImageDTO } from "../../../services/api/types";
import { StylePresetFormData } from "../components/StylePresetForm";
export type StylePresetModalState = {
isModalOpen: boolean;
updatingStylePreset: StylePresetRecordWithImage | null;
createPresetFromImage: ImageDTO | null
updatingStylePresetId: string | null;
prefilledFormData: StylePresetFormData | null
};
export type StylePresetPrefillOptions = {
positivePrompt: string;
negativePrompt: string;
image: File;
}
export type StylePresetState = {
isMenuOpen: boolean;
activeStylePreset: StylePresetRecordWithImage | null;
searchTerm: string
searchTerm: string;
viewMode: boolean;
}

View File

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

View File

@ -12,6 +12,8 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { StylePresetMenu } from '../../../stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from '../../../stylePresets/components/StylePresetMenuTrigger';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
@ -20,23 +22,33 @@ const overlayScrollbarsStyles: CSSProperties = {
const ParametersPanelCanvas = () => {
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<ControlSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
{isMenuOpen ? (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
) : (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<ControlSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
)}
</Box>
</Flex>
</Flex>

View File

@ -1,5 +1,5 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Portal, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
@ -69,22 +69,13 @@ const ParametersPanelTextToImage = () => {
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
<Portal containerRef={ref}>
{isMenuOpen && (
<Box position="absolute" top={0} left={0} right={0} bottom={0} layerStyle="second">
<OverlayScrollbarsComponent
defer
style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options}
>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
</Box>
)}
</Portal>
{!isMenuOpen && (
{isMenuOpen ? (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
) : (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />

View File

@ -8,6 +8,9 @@ import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { useAppSelector } from '../../../../app/store/storeHooks';
import { StylePresetMenu } from '../../../stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from '../../../stylePresets/components/StylePresetMenuTrigger';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
@ -15,19 +18,29 @@ const overlayScrollbarsStyles: CSSProperties = {
};
const ParametersPanelUpscale = () => {
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<UpscaleSettingsAccordion />
<GenerationSettingsAccordion />
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
{isMenuOpen ? (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
) : (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<UpscaleSettingsAccordion />
<GenerationSettingsAccordion />
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
)}
</Box>
</Flex>
</Flex>