Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@
},
"modelManager": {
"active": "active",
"actions": "Bulk Actions",
"addModel": "Add Model",
"addModels": "Add Models",
"advanced": "Advanced",
Expand Down Expand Up @@ -899,6 +900,7 @@
"delete": "Delete",
"deleteConfig": "Delete Config",
"deleteModel": "Delete Model",
"deleteModels": "Delete Models",
"deleteModelImage": "Delete Model Image",
"deleteMsg1": "Are you sure you want to delete this model from InvokeAI?",
"deleteMsg2": "This WILL delete the model from disk if it is in the InvokeAI root folder. If you are using a custom location, then the model WILL NOT be deleted from disk.",
Expand Down Expand Up @@ -1030,6 +1032,7 @@
"triggerPhrases": "Trigger Phrases",
"loraTriggerPhrases": "LoRA Trigger Phrases",
"mainModelTriggerPhrases": "Main Model Trigger Phrases",
"selectAll": "Select All",
"typePhraseHere": "Type phrase here",
"t5Encoder": "T5 Encoder",
"qwen3Encoder": "Qwen3 Encoder",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Flex, Text, useDisclosure, useToast } from '@invoke-ai/ui-library';
import { Flex, Text, useToast } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { buildUseDisclosure } from 'common/hooks/useBoolean';
import { MODEL_CATEGORIES_AS_LIST } from 'features/modelManagerV2/models';
import {
clearModelSelection,
Expand All @@ -23,19 +24,20 @@ import type { AnyModelConfig } from 'services/api/types';

import { BulkDeleteModelsModal } from './BulkDeleteModelsModal';
import { FetchingModelsLoader } from './FetchingModelsLoader';
import { ModelListHeader } from './ModelListHeader';
import { ModelListWrapper } from './ModelListWrapper';

const log = logger('models');

export const [useBulkDeleteModal] = buildUseDisclosure(false);

const ModelList = () => {
const dispatch = useAppDispatch();
const filteredModelType = useAppSelector(selectFilteredModelType);
const searchTerm = useAppSelector(selectSearchTerm);
const selectedModelKeys = useAppSelector(selectSelectedModelKeys);
const { t } = useTranslation();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen, close } = useBulkDeleteModal();
const [isDeleting, setIsDeleting] = useState(false);

const { data, isLoading } = useGetModelConfigsQuery();
Expand All @@ -62,10 +64,6 @@ const ModelList = () => {
return { total, byCategory };
}, [data, filteredModelType, searchTerm]);

const handleBulkDelete = useCallback(() => {
onOpen();
}, [onOpen]);

const handleConfirmBulkDelete = useCallback(async () => {
setIsDeleting(true);
try {
Expand All @@ -74,7 +72,7 @@ const ModelList = () => {
// Clear selection and close modal
dispatch(clearModelSelection());
dispatch(setSelectedModelKey(null));
onClose();
close();

// Show success/failure toast
if (result.failed.length === 0) {
Expand Down Expand Up @@ -127,12 +125,11 @@ const ModelList = () => {
} finally {
setIsDeleting(false);
}
}, [bulkDeleteModels, selectedModelKeys, dispatch, onClose, toast, t]);
}, [bulkDeleteModels, selectedModelKeys, dispatch, close, toast, t]);

return (
<>
<Flex flexDirection="column" w="full" h="full">
<ModelListHeader onBulkDelete={handleBulkDelete} />
<ScrollableContent>
<Flex flexDirection="column" w="full" h="full" gap={4}>
{isLoading && <FetchingModelsLoader loadingMessage="Loading..." />}
Expand All @@ -147,9 +144,10 @@ const ModelList = () => {
</Flex>
</ScrollableContent>
</Flex>

<BulkDeleteModelsModal
isOpen={isOpen}
onClose={onClose}
onClose={close}
onConfirm={handleConfirmBulkDelete}
modelCount={selectedModelKeys.length}
isDeleting={isDeleting}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Checkbox, Flex, Menu, MenuButton, MenuItem, MenuList, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice';
import {
modelSelectionChanged,
selectFilteredModelType,
selectSearchTerm,
selectSelectedModelKeys,
} from 'features/modelManagerV2/store/modelManagerV2Slice';
import { t } from 'i18next';
import { memo, useCallback, useMemo } from 'react';
import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi';
import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';

import { useBulkDeleteModal } from './ModelList';

const ModelListBulkActionsSx: SystemStyleObject = {
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
};

type ModelListBulkActionsProps = {
sx?: SystemStyleObject;
};

export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => {
const dispatch = useAppDispatch();
const filteredModelType = useAppSelector(selectFilteredModelType);
const selectedModelKeys = useAppSelector(selectSelectedModelKeys);
const searchTerm = useAppSelector(selectSearchTerm);
const { data } = useGetModelConfigsQuery();
const bulkDeleteModal = useBulkDeleteModal();

const handleBulkDelete = useCallback(() => {
bulkDeleteModal.open();
}, [bulkDeleteModal]);

// Calculate displayed (filtered) model keys
const displayedModelKeys = useMemo(() => {
const modelConfigs = modelConfigsAdapterSelectors.selectAll(data ?? { ids: [], entities: {} });
const filteredModels = modelsFilter(modelConfigs, searchTerm, filteredModelType);
return filteredModels.map((m) => m.key);
}, [data, searchTerm, filteredModelType]);

const { allSelected, someSelected } = useMemo(() => {
if (displayedModelKeys.length === 0) {
return { allSelected: false, someSelected: false };
}
const selectedSet = new Set(selectedModelKeys);
const displayedSelectedCount = displayedModelKeys.filter((key) => selectedSet.has(key)).length;
return {
allSelected: displayedSelectedCount === displayedModelKeys.length,
someSelected: displayedSelectedCount > 0 && displayedSelectedCount < displayedModelKeys.length,
};
}, [displayedModelKeys, selectedModelKeys]);

const handleToggleAll = useCallback(() => {
if (allSelected) {
// Deselect all displayed models
const displayedSet = new Set(displayedModelKeys);
const newSelection = selectedModelKeys.filter((key) => !displayedSet.has(key));
dispatch(modelSelectionChanged(newSelection));
} else {
// Select all displayed models (merge with existing selection)
const selectedSet = new Set(selectedModelKeys);
displayedModelKeys.forEach((key) => selectedSet.add(key));
dispatch(modelSelectionChanged(Array.from(selectedSet)));
}
}, [allSelected, displayedModelKeys, selectedModelKeys, dispatch]);

const selectionCount = selectedModelKeys.length;

return (
<Flex sx={{ ...ModelListBulkActionsSx, sx }}>
<Checkbox
isChecked={allSelected}
isIndeterminate={someSelected}
onChange={handleToggleAll}
isDisabled={displayedModelKeys.length === 0}
aria-label={t('modelManager.selectAll')}
>
<Text variant="subtext1" color="base.400">
{t('modelManager.selectAll')}
</Text>
</Checkbox>

<Flex alignItems="center" gap={4}>
<Text variant="subtext" color="base.400">
{selectionCount} {t('common.selected')}
</Text>
<Menu placement="bottom-end">
<MenuButton
as={Button}
disabled={selectionCount === 0}
size="sm"
rightIcon={<PiCaretDownBold />}
flexShrink={0}
variant="outline"
>
{t('modelManager.actions')}
</MenuButton>
<MenuList>
<MenuItem icon={<PiTrashSimpleBold />} onClick={handleBulkDelete} color="error.300">
{t('modelManager.deleteModels', { count: selectionCount })}
</MenuItem>
</MenuList>
</Menu>
</Flex>
</Flex>
);
});

ModelListBulkActions.displayName = 'ModelListBulkActions';

const modelsFilter = <T extends AnyModelConfig>(
data: T[],
nameFilter: string,
filteredModelType: FilterableModelType | null
): T[] => {
return data.filter((model) => {
const matchesFilter =
model.name.toLowerCase().includes(nameFilter.toLowerCase()) ||
model.base.toLowerCase().includes(nameFilter.toLowerCase()) ||
model.type.toLowerCase().includes(nameFilter.toLowerCase()) ||
model.description?.toLowerCase().includes(nameFilter.toLowerCase()) ||
model.format.toLowerCase().includes(nameFilter.toLowerCase());

const matchesType = getMatchesType(model, filteredModelType);

return matchesFilter && matchesType;
});
};

const getMatchesType = (modelConfig: AnyModelConfig, filteredModelType: FilterableModelType | null): boolean => {
if (filteredModelType === 'refiner') {
return modelConfig.base === 'sdxl-refiner';
}

if (filteredModelType === 'main' && modelConfig.base === 'sdxl-refiner') {
return false;
}

return filteredModelType ? modelConfig.type === filteredModelType : true;
};

This file was deleted.

Loading