Compare commits

...

24 Commits

Author SHA1 Message Date
psychedelicious
c451f52ea3 chore(ui): lint 2024-08-22 21:00:09 +10:00
psychedelicious
8a2c78f2e1 fix(ui): dynamic prompts not recalculating when deleting or updating a style preset
The root cause was the active style preset not being reset when it was deleted, or no longer present in the list of style presets.

- Add extra reducer to `stylePresetSlice` to reset the active preset if it is deleted or otherwise unavailable
- Update the dynamic prompts listener to trigger on delete/update/list of style presets
2024-08-22 21:00:09 +10:00
psychedelicious
bcc78bde9b chore: bump version to v4.2.8 2024-08-22 21:00:09 +10:00
Васянатор
054bb6fe0a translationBot(ui): update translation (Russian)
Currently translated at 100.0% (1367 of 1367 strings)

Co-authored-by: Васянатор <ilabulanov339@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/
Translation: InvokeAI/Web UI
2024-08-22 13:09:56 +10:00
Riccardo Giovanetti
4f4aa6d92e translationBot(ui): update translation (Italian)
Currently translated at 98.4% (1346 of 1367 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (1346 of 1367 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-08-22 13:09:56 +10:00
Hosted Weblate
eac51ac6f5 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-08-22 13:09:56 +10:00
psychedelicious
9f349a7c0a fix(ui): do not constrain width of hide/show boards button
lets translations display fully
2024-08-22 11:36:07 +10:00
psychedelicious
918afa5b15 fix(ui): show more of current board name 2024-08-22 11:36:07 +10:00
psychedelicious
eb1113f95c feat(ui): add translation string for "Upscale" 2024-08-22 11:36:07 +10:00
psychedelicious
4f4ba7b462 tidy(ui): clean up ActiveStylePreset markup 2024-08-21 09:06:41 +10:00
Mary Hipp
2298be0e6b fix(ui): error handling if unable to convert image URL to blob 2024-08-21 09:06:41 +10:00
Mary Hipp
63494dfca7 remove extra slash in exports path 2024-08-21 09:06:41 +10:00
Mary Hipp
36a1d39454 fix(ui): handle badge styling when template name is long 2024-08-21 09:06:41 +10:00
Mary Hipp
a6f6d5c400 fix(ui): add loading state to button when creating or updating a style preset 2024-08-21 09:06:41 +10:00
Mary Hipp
e85f221aca fix(ui): clear prompt template when prompts are recalled 2024-08-21 09:04:35 +10:00
Mary Hipp
d4797e37dc fix(ui): properly unwrap delete style preset API request so that error is caught 2024-08-19 16:12:39 -04:00
Mary Hipp
3e7923d072 fix(api): allow updating of type for style preset 2024-08-19 16:12:39 -04:00
psychedelicious
a85d69ce3d tidy(ui): getViewModeChunks.tsx -> .ts 2024-08-19 08:25:39 +10:00
psychedelicious
96db006c99 fix(ui): edge case with getViewModeChunks 2024-08-19 08:25:39 +10:00
psychedelicious
8ca57d03d8 tests(ui): add tests for getViewModeChunks 2024-08-19 08:25:39 +10:00
psychedelicious
6c404ce5f8 fix(ui): prompt template preset preview out of order 2024-08-19 08:25:39 +10:00
psychedelicious
584e07182b fix(ui): use translations for style preset strings 2024-08-17 21:27:53 +10:00
psychedelicious
f787e9acf6 chore: bump version v4.2.8rc2 2024-08-16 21:47:06 +10:00
psychedelicious
5a24b89e54 fix(app): include style preset defaults in build 2024-08-16 21:47:06 +10:00
25 changed files with 345 additions and 104 deletions

View File

@@ -26,13 +26,10 @@ from invokeai.app.services.style_preset_records.style_preset_records_common impo
)
class StylePresetUpdateFormData(BaseModel):
class StylePresetFormData(BaseModel):
name: str = Field(description="Preset name")
positive_prompt: str = Field(description="Positive prompt")
negative_prompt: str = Field(description="Negative prompt")
class StylePresetCreateFormData(StylePresetUpdateFormData):
type: PresetType = Field(description="Preset type")
@@ -95,9 +92,10 @@ async def update_style_preset(
try:
parsed_data = json.loads(data)
validated_data = StylePresetUpdateFormData(**parsed_data)
validated_data = StylePresetFormData(**parsed_data)
name = validated_data.name
type = validated_data.type
positive_prompt = validated_data.positive_prompt
negative_prompt = validated_data.negative_prompt
@@ -105,7 +103,7 @@ async def update_style_preset(
raise HTTPException(status_code=400, detail="Invalid preset data")
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
changes = StylePresetChanges(name=name, preset_data=preset_data)
changes = StylePresetChanges(name=name, preset_data=preset_data, type=type)
style_preset_image = ApiDependencies.invoker.services.style_preset_image_files.get_url(style_preset_id)
style_preset = ApiDependencies.invoker.services.style_preset_records.update(
@@ -145,7 +143,7 @@ async def create_style_preset(
try:
parsed_data = json.loads(data)
validated_data = StylePresetCreateFormData(**parsed_data)
validated_data = StylePresetFormData(**parsed_data)
name = validated_data.name
type = validated_data.type

View File

@@ -32,6 +32,7 @@ class PresetType(str, Enum, metaclass=MetaEnum):
class StylePresetChanges(BaseModel, extra="forbid"):
name: Optional[str] = Field(default=None, description="The style preset's new name.")
preset_data: Optional[PresetData] = Field(default=None, description="The updated data for style preset.")
type: Optional[PresetType] = Field(description="The updated type of the style preset")
class StylePresetWithoutId(BaseModel):

View File

@@ -1675,6 +1675,7 @@
"layers_other": "Layers"
},
"upscaling": {
"upscale": "Upscale",
"creativity": "Creativity",
"exceedsMaxSize": "Upscale settings exceed max size limit",
"exceedsMaxSizeDetails": "Max upscale limit is {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Please try a smaller image or decrease your scale selection.",
@@ -1723,6 +1724,7 @@
"positivePrompt": "Positive Prompt",
"preview": "Preview",
"private": "Private",
"promptTemplateCleared": "Prompt Template Cleared",
"searchByName": "Search by name",
"shared": "Shared",
"sharedTemplates": "Shared Templates",

View File

@@ -929,7 +929,7 @@
"missingInvocationTemplate": "Modello di invocazione mancante",
"missingFieldTemplate": "Modello di campo mancante",
"singleFieldType": "{{name}} (Singola)",
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino delle impostazioni predefinite",
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti"
},
@@ -1782,7 +1782,13 @@
"updatePromptTemplate": "Aggiorna il modello di prompt",
"type": "Tipo",
"promptTemplatesDesc2": "Utilizza la stringa segnaposto <Pre>{{placeholder}}</Pre> per specificare dove inserire il tuo prompt nel modello.",
"importTemplates": "Importa modelli di prompt",
"importTemplatesDesc": "Il formato deve essere un CSV con colonne 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt' incluse, oppure un file JSON con chiavi 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt"
"importTemplates": "Importa modelli di prompt (CSV/JSON)",
"exportDownloaded": "Esportazione completata",
"exportFailed": "Impossibile generare e scaricare il file CSV",
"exportPromptTemplates": "Esporta i miei modelli di prompt (CSV)",
"positivePromptColumn": "'prompt' o 'positive_prompt'",
"noTemplates": "Nessun modello",
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
"templateActions": "Azioni modello"
}
}

View File

@@ -91,7 +91,8 @@
"enabled": "Включено",
"disabled": "Отключено",
"comparingDesc": "Сравнение двух изображений",
"comparing": "Сравнение"
"comparing": "Сравнение",
"dontShowMeThese": "Не показывай мне это"
},
"gallery": {
"galleryImageSize": "Размер изображений",
@@ -153,7 +154,11 @@
"showArchivedBoards": "Показать архивированные доски",
"searchImages": "Поиск по метаданным",
"displayBoardSearch": "Отобразить поиск досок",
"displaySearch": "Отобразить поиск"
"displaySearch": "Отобразить поиск",
"exitBoardSearch": "Выйти из поиска досок",
"go": "Перейти",
"exitSearch": "Выйти из поиска",
"jump": "Пыгнуть"
},
"hotkeys": {
"keyboardShortcuts": "Горячие клавиши",
@@ -376,6 +381,10 @@
"toggleViewer": {
"title": "Переключить просмотр изображений",
"desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки."
},
"postProcess": {
"desc": "Обработайте текущее изображение с помощью выбранной модели постобработки",
"title": "Обработать изображение"
}
},
"modelManager": {
@@ -589,7 +598,10 @@
"infillColorValue": "Цвет заливки",
"globalSettings": "Глобальные настройки",
"globalNegativePromptPlaceholder": "Глобальный негативный запрос",
"globalPositivePromptPlaceholder": "Глобальный запрос"
"globalPositivePromptPlaceholder": "Глобальный запрос",
"postProcessing": "Постобработка (Shift + U)",
"processImage": "Обработка изображения",
"sendToUpscale": "Отправить на увеличение"
},
"settings": {
"models": "Модели",
@@ -623,7 +635,9 @@
"intermediatesCleared_many": "Очищено {{count}} промежуточных",
"clearIntermediatesDesc1": "Очистка промежуточных элементов приведет к сбросу состояния Canvas и ControlNet.",
"intermediatesClearedFailed": "Проблема очистки промежуточных",
"reloadingIn": "Перезагрузка через"
"reloadingIn": "Перезагрузка через",
"informationalPopoversDisabled": "Информационные всплывающие окна отключены",
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках."
},
"toast": {
"uploadFailed": "Загрузка не удалась",
@@ -694,7 +708,9 @@
"sessionRef": "Сессия: {{sessionId}}",
"outOfMemoryError": "Ошибка нехватки памяти",
"outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.",
"somethingWentWrong": "Что-то пошло не так"
"somethingWentWrong": "Что-то пошло не так",
"importFailed": "Импорт неудачен",
"importSuccessful": "Импорт успешен"
},
"tooltip": {
"feature": {
@@ -1017,7 +1033,8 @@
"composition": "Только композиция",
"hed": "HED",
"beginEndStepPercentShort": "Начало/конец %",
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)"
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)",
"depthAnythingSmallV2": "Small V2"
},
"boards": {
"autoAddBoard": "Авто добавление Доски",
@@ -1042,7 +1059,7 @@
"downloadBoard": "Скачать доску",
"deleteBoard": "Удалить доску",
"deleteBoardAndImages": "Удалить доску и изображения",
"deletedBoardsCannotbeRestored": "Удаленные доски не подлежат восстановлению",
"deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.",
"assetsWithCount_one": "{{count}} ассет",
"assetsWithCount_few": "{{count}} ассета",
"assetsWithCount_many": "{{count}} ассетов",
@@ -1057,7 +1074,11 @@
"boards": "Доски",
"addPrivateBoard": "Добавить личную доску",
"private": "Личные доски",
"shared": "Общие доски"
"shared": "Общие доски",
"hideBoards": "Скрыть доски",
"viewBoards": "Просмотреть доски",
"noBoards": "Нет досок {{boardType}}",
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения."
},
"dynamicPrompts": {
"seedBehaviour": {
@@ -1417,6 +1438,30 @@
"paragraphs": [
"Метод, с помощью которого применяется текущий IP-адаптер."
]
},
"structure": {
"paragraphs": [
"Структура контролирует, насколько точно выходное изображение будет соответствовать макету оригинала. Низкая структура допускает значительные изменения, в то время как высокая структура строго сохраняет исходную композицию и макет."
],
"heading": "Структура"
},
"scale": {
"paragraphs": [
"Масштаб управляет размером выходного изображения и основывается на кратном разрешении входного изображения. Например, при увеличении в 2 раза изображения 1024x1024 на выходе получится 2048 x 2048."
],
"heading": "Масштаб"
},
"creativity": {
"paragraphs": [
"Креативность контролирует степень свободы, предоставляемой модели при добавлении деталей. При низкой креативности модель остается близкой к оригинальному изображению, в то время как высокая креативность позволяет вносить больше изменений. При использовании подсказки высокая креативность увеличивает влияние подсказки."
],
"heading": "Креативность"
},
"upscaleModel": {
"heading": "Модель увеличения",
"paragraphs": [
"Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков."
]
}
},
"metadata": {
@@ -1693,7 +1738,78 @@
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Очередь"
"queue": "Очередь",
"upscaling": "Увеличение",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"upscaling": {
"exceedsMaxSize": "Параметры масштабирования превышают максимальный размер",
"exceedsMaxSizeDetails": "Максимальный предел масштабирования составляет {{maxUpscaleDimension}}x{{maxUpscaleDimension}} пикселей. Пожалуйста, попробуйте использовать меньшее изображение или уменьшите масштаб.",
"structure": "Структура",
"missingTileControlNetModel": "Не установлены подходящие модели ControlNet",
"missingUpscaleInitialImage": "Отсутствует увеличиваемое изображение",
"missingUpscaleModel": "Отсутствует увеличивающая модель",
"creativity": "Креативность",
"upscaleModel": "Модель увеличения",
"scale": "Масштаб",
"mainModelDesc": "Основная модель (архитектура SD1.5 или SDXL)",
"upscaleModelDesc": "Модель увеличения (img2img)",
"postProcessingModel": "Модель постобработки",
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
},
"stylePresets": {
"noMatchingTemplates": "Нет подходящих шаблонов",
"promptTemplatesDesc1": "Шаблоны подсказок добавляют текст к подсказкам, которые вы пишете в окне подсказок.",
"sharedTemplates": "Общие шаблоны",
"templateDeleted": "Шаблон запроса удален",
"toggleViewMode": "Переключить режим просмотра",
"type": "Тип",
"unableToDeleteTemplate": "Не получилось удалить шаблон запроса",
"viewModeTooltip": "Вот как будет выглядеть ваш запрос с выбранным шаблоном. Чтобы его отредактировать, щелкните в любом месте текстового поля.",
"viewList": "Просмотреть список шаблонов",
"active": "Активно",
"choosePromptTemplate": "Выберите шаблон запроса",
"defaultTemplates": "Стандартные шаблоны",
"deleteImage": "Удалить изображение",
"deleteTemplate": "Удалить шаблон",
"deleteTemplate2": "Вы уверены, что хотите удалить этот шаблон? Это нельзя отменить.",
"editTemplate": "Редактировать шаблон",
"exportPromptTemplates": "Экспорт моих шаблонов запроса (CSV)",
"exportDownloaded": "Экспорт скачан",
"exportFailed": "Невозможно сгенерировать и загрузить CSV",
"flatten": "Объединить выбранный шаблон с текущим запросом",
"acceptedColumnsKeys": "Принимаемые столбцы/ключи:",
"positivePromptColumn": "'prompt' или 'positive_prompt'",
"insertPlaceholder": "Вставить заполнитель",
"name": "Имя",
"negativePrompt": "Негативный запрос",
"promptTemplatesDesc3": "Если вы не используете заполнитель, шаблон будет добавлен в конец запроса.",
"positivePrompt": "Позитивный запрос",
"preview": "Предпросмотр",
"private": "Приватный",
"templateActions": "Действия с шаблоном",
"updatePromptTemplate": "Обновить шаблон запроса",
"uploadImage": "Загрузить изображение",
"useForTemplate": "Использовать для шаблона запроса",
"clearTemplateSelection": "Очистить выбор шаблона",
"copyTemplate": "Копировать шаблон",
"createPromptTemplate": "Создать шаблон запроса",
"importTemplates": "Импортировать шаблоны запроса (CSV/JSON)",
"nameColumn": "'name'",
"negativePromptColumn": "'negative_prompt'",
"myTemplates": "Мои шаблоны",
"noTemplates": "Нет шаблонов",
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
"searchByName": "Поиск по имени",
"shared": "Общий"
},
"upsell": {
"inviteTeammates": "Пригласите членов команды",
"professional": "Профессионал",
"professionalUpsell": "Доступно в профессиональной версии Invoke. Нажмите здесь или посетите invoke.com/pricing для получения более подробной информации.",
"shareAccess": "Поделиться доступом"
}
}

View File

@@ -13,6 +13,7 @@ import {
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { socketConnected } from 'services/events/actions';
@@ -22,7 +23,10 @@ const matcher = isAnyOf(
maxPromptsChanged,
maxPromptsReset,
socketConnected,
activeStylePresetIdChanged
activeStylePresetIdChanged,
stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled,
stylePresetsApi.endpoints.updateStylePreset.matchFulfilled,
stylePresetsApi.endpoints.listStylePresets.matchFulfilled
);
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {

View File

@@ -8,7 +8,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
*/
export const convertImageUrlToBlob = async (url: string) =>
new Promise<Blob | null>((resolve) => {
new Promise<Blob | null>((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
@@ -17,17 +17,23 @@ export const convertImageUrlToBlob = async (url: string) =>
const context = canvas.getContext('2d');
if (!context) {
reject(new Error('Failed to get canvas context'));
return;
}
context.drawImage(img, 0, 0);
resolve(
new Promise<Blob | null>((resolve) => {
canvas.toBlob(function (blob) {
resolve(blob);
}, 'image/png');
})
);
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to convert image to blob'));
}
}, 'image/png');
};
img.onerror = () => {
reject(new Error('Image failed to load. The URL may be invalid or the object may not exist.'));
};
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
img.src = url;
});

View File

@@ -66,7 +66,7 @@ export const Gallery = () => {
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2">
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
{boardName}
</Text>
<Spacer />

View File

@@ -60,7 +60,6 @@ const ImageGalleryContent = () => {
<GalleryHeader />
<Flex alignItems="center" justifyContent="space-between" w="full">
<Button
w={112}
size="sm"
variant="ghost"
onClick={handleToggleBoardPanel}

View File

@@ -1,14 +1,20 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
export const useImageActions = (image_name?: string) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const activeTabName = useAppSelector(activeTabNameSelector);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name);
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
@@ -46,14 +52,26 @@ export const useImageActions = (image_name?: string) => {
parseMetadata();
}, [metadata]);
const clearStylePreset = useCallback(() => {
if (activeStylePresetId) {
dispatch(activeStylePresetIdChanged(null));
toast({
status: 'info',
title: t('stylePresets.promptTemplateCleared'),
});
}
}, [dispatch, activeStylePresetId, t]);
const recallAll = useCallback(() => {
parseAndRecallAllMetadata(metadata, activeTabName === 'generation');
}, [activeTabName, metadata]);
clearStylePreset();
}, [activeTabName, metadata, clearStylePreset]);
const remix = useCallback(() => {
// Recalls all metadata parameters except seed
parseAndRecallAllMetadata(metadata, activeTabName === 'generation', ['seed']);
}, [activeTabName, metadata]);
clearStylePreset();
}, [activeTabName, metadata, clearStylePreset]);
const recallSeed = useCallback(() => {
handlers.seed.parse(metadata).then((seed) => {
@@ -63,7 +81,8 @@ export const useImageActions = (image_name?: string) => {
const recallPrompts = useCallback(() => {
parseAndRecallPrompts(metadata);
}, [metadata]);
clearStylePreset();
}, [metadata, clearStylePreset]);
const createAsPreset = useCallback(async () => {
if (image_name && metadata && imageDTO) {

View File

@@ -48,7 +48,12 @@ export const UpscaleSettingsAccordion = memo(() => {
});
return (
<StandaloneAccordion label="Upscale" badges={badges} isOpen={isOpenAccordion} onToggle={onToggleAccordion}>
<StandaloneAccordion
label={t('upscaling.upscale')}
badges={badges}
isOpen={isOpenAccordion}
onToggle={onToggleAccordion}
>
<Flex pt={4} px={4} w="full" h="full" flexDir="column" data-testid="upscale-settings-accordion">
<Flex flexDir="column" gap={4}>
<Flex gap={4}>

View File

@@ -1,4 +1,4 @@
import { Badge, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
import { Badge, Flex, IconButton, Spacer, 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';
@@ -69,45 +69,40 @@ 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">
<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 w="full" alignItems="center" gap={2} minW={0}>
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
<Badge colorScheme="invokeBlue" variant="subtle" justifySelf="flex-start">
{activeStylePreset.name}
</Badge>
<Spacer />
<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>
);
};

View File

@@ -30,8 +30,8 @@ export const StylePresetForm = ({
updatingStylePresetId: string | null;
formData: StylePresetFormData | null;
}) => {
const [createStylePreset] = useCreateStylePresetMutation();
const [updateStylePreset] = useUpdateStylePresetMutation();
const [createStylePreset, { isLoading: isCreating }] = useCreateStylePresetMutation();
const [updateStylePreset, { isLoading: isUpdating }] = useUpdateStylePresetMutation();
const { t } = useTranslation();
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
@@ -93,8 +93,8 @@ export const StylePresetForm = ({
</FormControl>
</Flex>
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
<StylePresetPromptField label={t('stylePresets.positivePrompt')} control={control} name="positivePrompt" />
<StylePresetPromptField label={t('stylePresets.negativePrompt')} control={control} name="negativePrompt" />
<Box>
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc1')}</Text>
<Text variant="subtext">
@@ -109,7 +109,11 @@ export const StylePresetForm = ({
<Flex justifyContent="space-between" alignItems="flex-end" gap={10}>
{allowPrivateStylePresets ? <StylePresetTypeField control={control} name="type" /> : <Spacer />}
<Button onClick={handleSubmit(handleClickSave)} isDisabled={!formState.isValid}>
<Button
onClick={handleSubmit(handleClickSave)}
isDisabled={!formState.isValid}
isLoading={isCreating || isUpdating}
>
{t('common.save')}
</Button>
</Flex>

View File

@@ -48,9 +48,13 @@ export const StylePresetModal = () => {
} 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' });
try {
const blob = await convertImageUrlToBlob(data.imageUrl);
if (blob) {
file = new File([blob], 'style_preset.png', { type: 'image/png' });
}
} catch (error) {
// do nothing
}
}
setFormData({

View File

@@ -21,6 +21,7 @@ const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: stri
/>
)
}
p={2}
>
<Image
src={presetImageUrl || ''}

View File

@@ -77,7 +77,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
const handleDeletePreset = useCallback(async () => {
try {
await deleteStylePreset(preset.id);
await deleteStylePreset(preset.id).unwrap();
toast({
status: 'success',
title: t('stylePresets.templateDeleted'),
@@ -174,7 +174,8 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
onClose={onClose}
title={t('stylePresets.deleteTemplate')}
acceptCallback={handleDeletePreset}
acceptButtonText="Delete"
acceptButtonText={t('common.delete')}
cancelButtonText={t('common.cancel')}
>
<p>{t('stylePresets.deleteTemplate2')}</p>
</ConfirmationAlertDialog>

View File

@@ -29,14 +29,14 @@ export const StylePresetMenuTrigger = () => {
py={2}
px={3}
borderRadius="base"
gap={1}
gap={2}
role="button"
_hover={_hover}
transitionProperty="background-color"
transitionDuration="normal"
w="full"
>
<ActiveStylePreset />
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
</Flex>
);

View File

@@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig } from 'app/store/store';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
import type { StylePresetState } from './types';
@@ -24,6 +25,26 @@ export const stylePresetSlice = createSlice({
state.viewMode = action.payload;
},
},
extraReducers(builder) {
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {
if (state.activeStylePresetId === null) {
return;
}
const deletedId = action.meta.arg.originalArgs;
if (state.activeStylePresetId === deletedId) {
state.activeStylePresetId = null;
}
});
builder.addMatcher(stylePresetsApi.endpoints.listStylePresets.matchFulfilled, (state, action) => {
if (state.activeStylePresetId === null) {
return;
}
const ids = action.payload.map((preset) => preset.id);
if (!ids.includes(state.activeStylePresetId)) {
state.activeStylePresetId = null;
}
});
},
});
export const { activeStylePresetIdChanged, searchTermChanged, viewModeChanged } = stylePresetSlice.actions;

View File

@@ -0,0 +1,53 @@
import { getViewModeChunks } from 'features/stylePresets/util/getViewModeChunks';
import { describe, expect, it } from 'vitest';
describe('getViewModeChunks', () => {
it('should return empty strings when presetPrompt is not provided', () => {
const currentPrompt = 'current prompt';
const presetPrompt = undefined;
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['', currentPrompt, '']);
});
it('should return empty strings when presetPrompt is empty', () => {
const currentPrompt = 'current prompt';
const presetPrompt = '';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['', currentPrompt, '']);
});
it('should append presetPrompt to currentPrompt when presetPrompt does not contain PRESET_PLACEHOLDER', () => {
const currentPrompt = 'current prompt';
const presetPrompt = 'preset prompt';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['', `${currentPrompt} `, presetPrompt]);
});
it('should split presetPrompt into 3 parts when presetPrompt contains PRESET_PLACEHOLDER', () => {
const currentPrompt = 'current prompt';
const presetPrompt = 'before {prompt} after';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['before ', currentPrompt, ' after']);
});
it('should split presetPrompt into 3 parts when presetPrompt contains multiple PRESET_PLACEHOLDER', () => {
const currentPrompt = 'current prompt';
const presetPrompt = 'before {prompt} middle {prompt} after';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['before ', currentPrompt, ' middle {prompt} after']);
});
it('should handle the PRESET_PLACEHOLDER being at the start of the presetPrompt', () => {
const currentPrompt = 'current prompt';
const presetPrompt = '{prompt} after';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['', currentPrompt, ' after']);
});
it('should handle the PRESET_PLACEHOLDER being at the end of the presetPrompt', () => {
const currentPrompt = 'current prompt';
const presetPrompt = 'before {prompt}';
const result = getViewModeChunks(currentPrompt, presetPrompt);
expect(result).toEqual(['before ', currentPrompt, '']);
});
});

View File

@@ -0,0 +1,17 @@
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
if (!presetPrompt || !presetPrompt.length) {
return ['', currentPrompt, ''];
}
// When preset prompt does not contain the placeholder, we append the preset to the current prompt
if (!presetPrompt.includes(PRESET_PLACEHOLDER)) {
return ['', `${currentPrompt} `, presetPrompt];
}
// Otherwise, we split the preset prompt into 3 parts: before, current, and after the placeholder
const [before, ...after] = presetPrompt.split(PRESET_PLACEHOLDER);
return [before || '', currentPrompt, after.join(PRESET_PLACEHOLDER) || ''];
};

View File

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

View File

@@ -94,7 +94,7 @@ export const stylePresetsApi = api.injectEndpoints({
}),
exportStylePresets: build.query<string, void>({
query: () => ({
url: buildStylePresetsUrl('/export'),
url: buildStylePresetsUrl('export'),
responseHandler: (response) => response.text(),
}),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],

View File

@@ -1 +1 @@
__version__ = "4.2.8rc1"
__version__ = "4.2.8"

View File

@@ -158,6 +158,10 @@ version = { attr = "invokeai.version.__version__" }
[tool.setuptools.package-data]
"invokeai.app.assets" = ["**/*.png"]
"invokeai.app.services.workflow_records.default_workflows" = ["*.json"]
"invokeai.app.services.style_preset_records" = ["*.json"]
"invokeai.app.services.style_preset_images.default_style_preset_images" = [
"*.png",
]
"invokeai.assets.fonts" = ["**/*.ttf"]
"invokeai.backend" = ["**.png"]
"invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"]