Skip to content

Commit 9d6290a

Browse files
Merge branch 'feat/z-image-regional-guidance' of https://github.com/Pfannkuchensack/InvokeAI into feat/z-image-regional-guidance
2 parents 4a25b9a + a8da845 commit 9d6290a

File tree

9 files changed

+583
-44
lines changed

9 files changed

+583
-44
lines changed

invokeai/app/api/routers/model_manager.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,59 @@ async def delete_model(
447447
raise HTTPException(status_code=404, detail=str(e))
448448

449449

450+
class BulkDeleteModelsRequest(BaseModel):
451+
"""Request body for bulk model deletion."""
452+
453+
keys: List[str] = Field(description="List of model keys to delete")
454+
455+
456+
class BulkDeleteModelsResponse(BaseModel):
457+
"""Response body for bulk model deletion."""
458+
459+
deleted: List[str] = Field(description="List of successfully deleted model keys")
460+
failed: List[dict] = Field(description="List of failed deletions with error messages")
461+
462+
463+
@model_manager_router.post(
464+
"/i/bulk_delete",
465+
operation_id="bulk_delete_models",
466+
responses={
467+
200: {"description": "Models deleted (possibly with some failures)"},
468+
},
469+
status_code=200,
470+
)
471+
async def bulk_delete_models(
472+
request: BulkDeleteModelsRequest = Body(description="List of model keys to delete"),
473+
) -> BulkDeleteModelsResponse:
474+
"""
475+
Delete multiple model records from database.
476+
477+
The configuration records will be removed. The corresponding weights files will be
478+
deleted as well if they reside within the InvokeAI "models" directory.
479+
Returns a list of successfully deleted keys and failed deletions with error messages.
480+
"""
481+
logger = ApiDependencies.invoker.services.logger
482+
installer = ApiDependencies.invoker.services.model_manager.install
483+
484+
deleted = []
485+
failed = []
486+
487+
for key in request.keys:
488+
try:
489+
installer.delete(key)
490+
deleted.append(key)
491+
logger.info(f"Deleted model: {key}")
492+
except UnknownModelException as e:
493+
logger.error(f"Failed to delete model {key}: {str(e)}")
494+
failed.append({"key": key, "error": str(e)})
495+
except Exception as e:
496+
logger.error(f"Failed to delete model {key}: {str(e)}")
497+
failed.append({"key": key, "error": str(e)})
498+
499+
logger.info(f"Bulk delete completed: {len(deleted)} deleted, {len(failed)} failed")
500+
return BulkDeleteModelsResponse(deleted=deleted, failed=failed)
501+
502+
450503
@model_manager_router.delete(
451504
"/i/{key}/image",
452505
operation_id="delete_model_image",

invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const zModelManagerState = z.object({
1818
filteredModelType: zFilterableModelType.nullable(),
1919
scanPath: z.string().optional(),
2020
shouldInstallInPlace: z.boolean(),
21+
selectedModelKeys: z.array(z.string()),
2122
});
2223

2324
type ModelManagerState = z.infer<typeof zModelManagerState>;
@@ -30,6 +31,7 @@ const getInitialState = (): ModelManagerState => ({
3031
searchTerm: '',
3132
scanPath: undefined,
3233
shouldInstallInPlace: true,
34+
selectedModelKeys: [],
3335
});
3436

3537
const slice = createSlice({
@@ -55,6 +57,20 @@ const slice = createSlice({
5557
shouldInstallInPlaceChanged: (state, action: PayloadAction<boolean>) => {
5658
state.shouldInstallInPlace = action.payload;
5759
},
60+
modelSelectionChanged: (state, action: PayloadAction<string[]>) => {
61+
state.selectedModelKeys = action.payload;
62+
},
63+
toggleModelSelection: (state, action: PayloadAction<string>) => {
64+
const index = state.selectedModelKeys.indexOf(action.payload);
65+
if (index > -1) {
66+
state.selectedModelKeys.splice(index, 1);
67+
} else {
68+
state.selectedModelKeys.push(action.payload);
69+
}
70+
},
71+
clearModelSelection: (state) => {
72+
state.selectedModelKeys = [];
73+
},
5874
},
5975
});
6076

@@ -65,6 +81,9 @@ export const {
6581
setSelectedModelMode,
6682
setScanPath,
6783
shouldInstallInPlaceChanged,
84+
modelSelectionChanged,
85+
toggleModelSelection,
86+
clearModelSelection,
6887
} = slice.actions;
6988

7089
export const modelManagerSliceConfig: SliceConfig<typeof slice> = {
@@ -79,7 +98,7 @@ export const modelManagerSliceConfig: SliceConfig<typeof slice> = {
7998
}
8099
return zModelManagerState.parse(state);
81100
},
82-
persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'],
101+
persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm', 'selectedModelKeys'],
83102
},
84103
};
85104

@@ -93,3 +112,4 @@ export const selectSelectedModelMode = createModelManagerSelector((modelManager)
93112
export const selectSearchTerm = createModelManagerSelector((mm) => mm.searchTerm);
94113
export const selectFilteredModelType = createModelManagerSelector((mm) => mm.filteredModelType);
95114
export const selectShouldInstallInPlace = createModelManagerSelector((mm) => mm.shouldInstallInPlace);
115+
export const selectSelectedModelKeys = createModelManagerSelector((mm) => mm.selectedModelKeys);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogBody,
4+
AlertDialogContent,
5+
AlertDialogFooter,
6+
AlertDialogHeader,
7+
AlertDialogOverlay,
8+
Button,
9+
Flex,
10+
Text,
11+
} from '@invoke-ai/ui-library';
12+
import { memo, useRef } from 'react';
13+
import { useTranslation } from 'react-i18next';
14+
15+
type BulkDeleteModelsModalProps = {
16+
isOpen: boolean;
17+
onClose: () => void;
18+
onConfirm: () => void;
19+
modelCount: number;
20+
isDeleting?: boolean;
21+
};
22+
23+
export const BulkDeleteModelsModal = memo(
24+
({ isOpen, onClose, onConfirm, modelCount, isDeleting = false }: BulkDeleteModelsModalProps) => {
25+
const { t } = useTranslation();
26+
const cancelRef = useRef<HTMLButtonElement>(null);
27+
28+
return (
29+
<AlertDialog isOpen={isOpen} onClose={onClose} leastDestructiveRef={cancelRef} isCentered>
30+
<AlertDialogOverlay>
31+
<AlertDialogContent>
32+
<AlertDialogHeader fontSize="lg" fontWeight="bold">
33+
{t('modelManager.deleteModels', { count: modelCount })}
34+
</AlertDialogHeader>
35+
36+
<AlertDialogBody>
37+
<Flex flexDir="column" gap={3}>
38+
<Text>
39+
{t('modelManager.deleteModelsConfirm', {
40+
count: modelCount,
41+
defaultValue: `Are you sure you want to delete ${modelCount} model(s)? This action cannot be undone.`,
42+
})}
43+
</Text>
44+
<Text fontWeight="semibold" color="error.400">
45+
{t('modelManager.deleteWarning', {
46+
defaultValue: 'Models in your Invoke models directory will be permanently deleted from disk.',
47+
})}
48+
</Text>
49+
</Flex>
50+
</AlertDialogBody>
51+
52+
<AlertDialogFooter>
53+
<Button ref={cancelRef} onClick={onClose} isDisabled={isDeleting}>
54+
{t('common.cancel')}
55+
</Button>
56+
<Button colorScheme="error" onClick={onConfirm} ml={3} isLoading={isDeleting}>
57+
{t('common.delete')}
58+
</Button>
59+
</AlertDialogFooter>
60+
</AlertDialogContent>
61+
</AlertDialogOverlay>
62+
</AlertDialog>
63+
);
64+
}
65+
);
66+
67+
BulkDeleteModelsModal.displayName = 'BulkDeleteModelsModal';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
1-
import { Flex, Text } from '@invoke-ai/ui-library';
1+
import { Flex, Text, useDisclosure, useToast } from '@invoke-ai/ui-library';
22
import { logger } from 'app/logging/logger';
3-
import { useAppSelector } from 'app/store/storeHooks';
3+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
55
import { MODEL_CATEGORIES_AS_LIST } from 'features/modelManagerV2/models';
66
import {
7+
clearModelSelection,
78
type FilterableModelType,
89
selectFilteredModelType,
910
selectSearchTerm,
11+
selectSelectedModelKeys,
12+
setSelectedModelKey,
1013
} from 'features/modelManagerV2/store/modelManagerV2Slice';
11-
import { memo, useMemo } from 'react';
14+
import { memo, useCallback, useMemo, useState } from 'react';
1215
import { useTranslation } from 'react-i18next';
13-
import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models';
16+
import { serializeError } from 'serialize-error';
17+
import {
18+
modelConfigsAdapterSelectors,
19+
useBulkDeleteModelsMutation,
20+
useGetModelConfigsQuery,
21+
} from 'services/api/endpoints/models';
1422
import type { AnyModelConfig } from 'services/api/types';
1523

24+
import { BulkDeleteModelsModal } from './BulkDeleteModelsModal';
1625
import { FetchingModelsLoader } from './FetchingModelsLoader';
26+
import { ModelListHeader } from './ModelListHeader';
1727
import { ModelListWrapper } from './ModelListWrapper';
1828

1929
const log = logger('models');
2030

2131
const ModelList = () => {
32+
const dispatch = useAppDispatch();
2233
const filteredModelType = useAppSelector(selectFilteredModelType);
2334
const searchTerm = useAppSelector(selectSearchTerm);
35+
const selectedModelKeys = useAppSelector(selectSelectedModelKeys);
2436
const { t } = useTranslation();
37+
const toast = useToast();
38+
const { isOpen, onOpen, onClose } = useDisclosure();
39+
const [isDeleting, setIsDeleting] = useState(false);
2540

2641
const { data, isLoading } = useGetModelConfigsQuery();
42+
const [bulkDeleteModels] = useBulkDeleteModelsMutation();
2743

2844
const models = useMemo(() => {
2945
const modelConfigs = modelConfigsAdapterSelectors.selectAll(data ?? { ids: [], entities: {} });
@@ -46,20 +62,99 @@ const ModelList = () => {
4662
return { total, byCategory };
4763
}, [data, filteredModelType, searchTerm]);
4864

65+
const handleBulkDelete = useCallback(() => {
66+
onOpen();
67+
}, [onOpen]);
68+
69+
const handleConfirmBulkDelete = useCallback(async () => {
70+
setIsDeleting(true);
71+
try {
72+
const result = await bulkDeleteModels({ keys: selectedModelKeys }).unwrap();
73+
74+
// Clear selection and close modal
75+
dispatch(clearModelSelection());
76+
dispatch(setSelectedModelKey(null));
77+
onClose();
78+
79+
// Show success/failure toast
80+
if (result.failed.length === 0) {
81+
toast({
82+
id: 'BULK_DELETE_SUCCESS',
83+
title: t('modelManager.modelsDeleted', {
84+
count: result.deleted.length,
85+
defaultValue: `Successfully deleted ${result.deleted.length} model(s)`,
86+
}),
87+
status: 'success',
88+
});
89+
} else if (result.deleted.length === 0) {
90+
toast({
91+
id: 'BULK_DELETE_FAILED',
92+
title: t('modelManager.modelsDeleteFailed', {
93+
defaultValue: 'Failed to delete models',
94+
}),
95+
description: t('modelManager.someModelsFailedToDelete', {
96+
count: result.failed.length,
97+
defaultValue: `${result.failed.length} model(s) could not be deleted`,
98+
}),
99+
status: 'error',
100+
});
101+
} else {
102+
// Partial success
103+
toast({
104+
id: 'BULK_DELETE_PARTIAL',
105+
title: t('modelManager.modelsDeletedPartial', {
106+
defaultValue: 'Partially completed',
107+
}),
108+
description: t('modelManager.someModelsDeleted', {
109+
deleted: result.deleted.length,
110+
failed: result.failed.length,
111+
defaultValue: `${result.deleted.length} deleted, ${result.failed.length} failed`,
112+
}),
113+
status: 'warning',
114+
});
115+
}
116+
117+
log.info(`Bulk delete completed: ${result.deleted.length} deleted, ${result.failed.length} failed`);
118+
} catch (err) {
119+
log.error({ error: serializeError(err as Error) }, 'Bulk delete error');
120+
toast({
121+
id: 'BULK_DELETE_ERROR',
122+
title: t('modelManager.modelsDeleteError', {
123+
defaultValue: 'Error deleting models',
124+
}),
125+
status: 'error',
126+
});
127+
} finally {
128+
setIsDeleting(false);
129+
}
130+
}, [bulkDeleteModels, selectedModelKeys, dispatch, onClose, toast, t]);
131+
49132
return (
50-
<ScrollableContent>
51-
<Flex flexDirection="column" w="full" h="full" gap={4}>
52-
{isLoading && <FetchingModelsLoader loadingMessage="Loading..." />}
53-
{models.byCategory.map(({ i18nKey, configs }) => (
54-
<ModelListWrapper key={i18nKey} title={t(i18nKey)} modelList={configs} />
55-
))}
56-
{!isLoading && models.total === 0 && (
57-
<Flex w="full" h="full" alignItems="center" justifyContent="center">
58-
<Text>{t('modelManager.noMatchingModels')}</Text>
133+
<>
134+
<Flex flexDirection="column" w="full" h="full">
135+
<ModelListHeader onBulkDelete={handleBulkDelete} />
136+
<ScrollableContent>
137+
<Flex flexDirection="column" w="full" h="full" gap={4}>
138+
{isLoading && <FetchingModelsLoader loadingMessage="Loading..." />}
139+
{models.byCategory.map(({ i18nKey, configs }) => (
140+
<ModelListWrapper key={i18nKey} title={t(i18nKey)} modelList={configs} />
141+
))}
142+
{!isLoading && models.total === 0 && (
143+
<Flex w="full" h="full" alignItems="center" justifyContent="center">
144+
<Text>{t('modelManager.noMatchingModels')}</Text>
145+
</Flex>
146+
)}
59147
</Flex>
60-
)}
148+
</ScrollableContent>
61149
</Flex>
62-
</ScrollableContent>
150+
<BulkDeleteModelsModal
151+
isOpen={isOpen}
152+
onClose={onClose}
153+
onConfirm={handleConfirmBulkDelete}
154+
modelCount={selectedModelKeys.length}
155+
isDeleting={isDeleting}
156+
/>
157+
</>
63158
);
64159
};
65160

0 commit comments

Comments
 (0)