mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
clean up, add context menu to import/download templates
This commit is contained in:
parent
68dac6349d
commit
24f298283f
@ -128,9 +128,9 @@ async def parse_presets_from_file(file: UploadFile) -> list[StylePresetWithoutId
|
|||||||
style_presets.append(style_preset)
|
style_presets.append(style_preset)
|
||||||
except pydantic.ValidationError as e:
|
except pydantic.ValidationError as e:
|
||||||
if file.content_type == "text/csv":
|
if file.content_type == "text/csv":
|
||||||
msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt'"
|
msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
|
||||||
else: # file.content_type == "application/json":
|
else: # file.content_type == "application/json":
|
||||||
msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt'"
|
msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
|
||||||
raise InvalidPresetImportDataError(msg) from e
|
raise InvalidPresetImportDataError(msg) from e
|
||||||
finally:
|
finally:
|
||||||
file.file.close()
|
file.file.close()
|
||||||
|
@ -1701,10 +1701,14 @@
|
|||||||
"deleteImage": "Delete Image",
|
"deleteImage": "Delete Image",
|
||||||
"deleteTemplate": "Delete Template",
|
"deleteTemplate": "Delete Template",
|
||||||
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
|
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
|
||||||
|
"downloadCsv": "Download CSV",
|
||||||
"editTemplate": "Edit Template",
|
"editTemplate": "Edit Template",
|
||||||
|
"exportDownloaded": "Export Downloaded",
|
||||||
|
"exportFailed": "Unable to generate and download CSV",
|
||||||
"flatten": "Flatten selected template into current prompt",
|
"flatten": "Flatten selected template into current prompt",
|
||||||
"importTemplates": "Import Prompt Templates",
|
"importTemplates": "Import Prompt Templates",
|
||||||
"importTemplatesDesc": "Format must be either a CSV with columns: 'name', 'prompt' or 'positive_prompt', and 'negative_prompt' included, or a JSON file with keys 'name', 'prompt' or 'positive_prompt', and 'negative_prompt",
|
"importTemplatesDesc": "Accepted formats: CSV or JSON",
|
||||||
|
"importTemplatesDesc2": "Accepted columns/keys: 'name', 'prompt' or 'positive_prompt', 'negative_prompt",
|
||||||
"insertPlaceholder": "Insert placeholder",
|
"insertPlaceholder": "Insert placeholder",
|
||||||
"myTemplates": "My Templates",
|
"myTemplates": "My Templates",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@ -1719,6 +1723,7 @@
|
|||||||
"searchByName": "Search by name",
|
"searchByName": "Search by name",
|
||||||
"shared": "Shared",
|
"shared": "Shared",
|
||||||
"sharedTemplates": "Shared Templates",
|
"sharedTemplates": "Shared Templates",
|
||||||
|
"templateActions": "Template Actions",
|
||||||
"templateDeleted": "Prompt template deleted",
|
"templateDeleted": "Prompt template deleted",
|
||||||
"toggleViewMode": "Toggle View Mode",
|
"toggleViewMode": "Toggle View Mode",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
|
||||||
import { IconButton, spinAnimation, Text } from '@invoke-ai/ui-library';
|
|
||||||
import { toast } from 'features/toast/toast';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useDropzone } from 'react-dropzone';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiSpinner, PiUploadBold } from 'react-icons/pi';
|
|
||||||
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
|
|
||||||
|
|
||||||
const loadingStyles: SystemStyleObject = {
|
|
||||||
svg: { animation: spinAnimation },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StylePresetImport = () => {
|
|
||||||
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const onDropAccepted = useCallback(
|
|
||||||
async (files: File[]) => {
|
|
||||||
const file = files[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await importStylePresets(file).unwrap();
|
|
||||||
toast({
|
|
||||||
status: 'success',
|
|
||||||
title: t('toast.importSuccessful'),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
status: 'error',
|
|
||||||
title: t('toast.importFailed'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[importStylePresets, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getInputProps, getRootProps } = useDropzone({
|
|
||||||
accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
|
|
||||||
onDropAccepted,
|
|
||||||
noDrag: true,
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
icon={!isLoading ? <PiUploadBold /> : <PiSpinner />}
|
|
||||||
tooltip={
|
|
||||||
<>
|
|
||||||
<Text fontWeight="semibold">{t('stylePresets.importTemplates')}</Text>
|
|
||||||
<Text>{t('stylePresets.importTemplatesDesc')}</Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
aria-label={t('stylePresets.importTemplates')}
|
|
||||||
size="md"
|
|
||||||
variant="link"
|
|
||||||
w={8}
|
|
||||||
h={8}
|
|
||||||
sx={isLoading ? loadingStyles : undefined}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
{...getRootProps()}
|
|
||||||
/>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,27 +1,13 @@
|
|||||||
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiDownloadBold, PiPlusBold } from 'react-icons/pi';
|
|
||||||
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
||||||
import { useLazyExportStylePresetsQuery, useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
import { StylePresetImport } from './StylePresetImport';
|
|
||||||
import { StylePresetList } from './StylePresetList';
|
import { StylePresetList } from './StylePresetList';
|
||||||
|
import { StylePresetMenuActions } from './StylePresetMenuActions/StylePresetMenuActions';
|
||||||
import StylePresetSearch from './StylePresetSearch';
|
import StylePresetSearch from './StylePresetSearch';
|
||||||
import { toast } from '../../toast/toast';
|
|
||||||
|
|
||||||
const generateCSV = (data: any[]) => {
|
|
||||||
const header = ['Column1', 'Column2', 'Column3'];
|
|
||||||
const csvRows = [
|
|
||||||
header.join(','), // add header row
|
|
||||||
...data.map((row) => row.join(',')), // add data rows
|
|
||||||
];
|
|
||||||
|
|
||||||
return csvRows.join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StylePresetMenu = () => {
|
export const StylePresetMenu = () => {
|
||||||
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||||
@ -59,73 +45,12 @@ export const StylePresetMenu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
|
|
||||||
|
|
||||||
const handleClickAddNew = useCallback(() => {
|
|
||||||
$stylePresetModalState.set({
|
|
||||||
prefilledFormData: null,
|
|
||||||
updatingStylePresetId: null,
|
|
||||||
isModalOpen: true,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleClickDownloadCsv = useCallback(async () => {
|
|
||||||
let blob;
|
|
||||||
try {
|
|
||||||
const response = await exportStylePresets().unwrap();
|
|
||||||
blob = new Blob([response], { type: 'text/csv' });
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
status: 'error',
|
|
||||||
title: 'Unable to generate and download export',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blob) {
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'data.csv';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
toast({
|
|
||||||
status: 'success',
|
|
||||||
title: 'Export downloaded',
|
|
||||||
});
|
|
||||||
}, [exportStylePresets]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||||
<StylePresetSearch />
|
<StylePresetSearch />
|
||||||
<Flex alignItems="center" justifyContent="space-between">
|
<StylePresetMenuActions />
|
||||||
<StylePresetImport />
|
|
||||||
<IconButton
|
|
||||||
icon={<PiDownloadBold />}
|
|
||||||
tooltip={t('stylePresets.createPromptTemplate')}
|
|
||||||
aria-label={t('stylePresets.createPromptTemplate')}
|
|
||||||
onClick={handleClickDownloadCsv}
|
|
||||||
size="md"
|
|
||||||
variant="link"
|
|
||||||
w={8}
|
|
||||||
h={8}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
icon={<PiPlusBold />}
|
|
||||||
tooltip={t('stylePresets.createPromptTemplate')}
|
|
||||||
aria-label={t('stylePresets.createPromptTemplate')}
|
|
||||||
onClick={handleClickAddNew}
|
|
||||||
size="md"
|
|
||||||
variant="link"
|
|
||||||
w={8}
|
|
||||||
h={8}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
||||||
@ -135,11 +60,9 @@ export const StylePresetMenu = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} />
|
<StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} />
|
||||||
|
|
||||||
{allowPrivateStylePresets && (
|
{allowPrivateStylePresets && (
|
||||||
<StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} />
|
<StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} />
|
<StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiDownloadSimpleBold, PiSpinner } from 'react-icons/pi';
|
||||||
|
import { useLazyExportStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
export const StylePresetExport = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleClickDownloadCsv = useCallback(async () => {
|
||||||
|
let blob;
|
||||||
|
try {
|
||||||
|
const response = await exportStylePresets().unwrap();
|
||||||
|
blob = new Blob([response], { type: 'text/csv' });
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: t('stylePresets.exportFailed'),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blob) {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'data.csv';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('stylePresets.exportDownloaded'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [exportStylePresets, onClose, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem icon={!isLoading ? <PiDownloadSimpleBold /> : <PiSpinner />} onClickCapture={handleClickDownloadCsv}>
|
||||||
|
{t('stylePresets.downloadCsv')}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Flex, MenuItem, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiSpinner, PiUploadSimpleBold } from 'react-icons/pi';
|
||||||
|
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
export const StylePresetImport = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onDropAccepted = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
const file = files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importStylePresets(file)
|
||||||
|
.unwrap()
|
||||||
|
.then(() => {
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('toast.importSuccessful'),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: t('toast.importFailed'),
|
||||||
|
description: error ? `${error.data.detail}` : undefined,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[importStylePresets, t, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getInputProps, getRootProps } = useDropzone({
|
||||||
|
accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
|
||||||
|
onDropAccepted,
|
||||||
|
noDrag: true,
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem icon={!isLoading ? <PiUploadSimpleBold /> : <PiSpinner />} alignItems="flex-start" {...getRootProps()}>
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text>{t('stylePresets.importTemplates')}</Text>
|
||||||
|
<Text maxW="200px" fontSize="xs" variant="subtext">
|
||||||
|
{t('stylePresets.importTemplatesDesc')}
|
||||||
|
</Text>
|
||||||
|
<Text maxW="200px" fontSize="xs" variant="subtext">
|
||||||
|
{t('stylePresets.importTemplatesDesc2')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Flex, IconButton, Menu, MenuButton, MenuList, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiDotsThreeOutlineFill, PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import { StylePresetExport } from './StylePresetExport';
|
||||||
|
import { StylePresetImport } from './StylePresetImport';
|
||||||
|
|
||||||
|
export const StylePresetMenuActions = () => {
|
||||||
|
const { isOpen, onClose, onToggle } = useDisclosure();
|
||||||
|
const handleClickAddNew = useCallback(() => {
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: null,
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: true,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex alignItems="center" justifyContent="space-between">
|
||||||
|
<IconButton
|
||||||
|
icon={<PiPlusBold />}
|
||||||
|
tooltip={t('stylePresets.createPromptTemplate')}
|
||||||
|
aria-label={t('stylePresets.createPromptTemplate')}
|
||||||
|
onClick={handleClickAddNew}
|
||||||
|
size="md"
|
||||||
|
variant="ghost"
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
/>
|
||||||
|
<Menu isOpen={isOpen}>
|
||||||
|
<MenuButton
|
||||||
|
variant="ghost"
|
||||||
|
as={IconButton}
|
||||||
|
aria-label={t('stylePresets.templateActions')}
|
||||||
|
tooltip={t('stylePresets.templateActions')}
|
||||||
|
icon={<PiDotsThreeOutlineFill />}
|
||||||
|
onClick={onToggle}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<StylePresetImport onClose={onClose} />
|
||||||
|
<StylePresetExport onClose={onClose} />
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -92,13 +92,10 @@ export const stylePresetsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
||||||
}),
|
}),
|
||||||
exportStylePresets: build.query<
|
exportStylePresets: build.query<string, void>({
|
||||||
string,
|
|
||||||
void
|
|
||||||
>({
|
|
||||||
query: () => ({
|
query: () => ({
|
||||||
url: buildStylePresetsUrl("/export"),
|
url: buildStylePresetsUrl('/export'),
|
||||||
responseHandler: response => response.text()
|
responseHandler: (response) => response.text(),
|
||||||
}),
|
}),
|
||||||
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user