diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueue.tsx
similarity index 78%
rename from invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue.tsx
rename to invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueue.tsx
index 804a728cbd..cd2208c972 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueue.tsx
@@ -4,10 +4,9 @@ import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
-import { RiSparklingFill } from 'react-icons/ri';
import { useGetModelImportsQuery, usePruneModelImportsMutation } from 'services/api/endpoints/models';
-import { ImportQueueModel } from './ImportQueueModel';
+import { ImportQueueItem } from './ImportQueueItem';
export const ImportQueue = () => {
const dispatch = useAppDispatch();
@@ -51,20 +50,15 @@ export const ImportQueue = () => {
return (
-
+
{t('modelManager.importQueue')}
- }
- >
+
-
- {data?.map((model) => )}
+
+ {data?.map((model) => )}
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueBadge.tsx
new file mode 100644
index 0000000000..5b363c6ff0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueBadge.tsx
@@ -0,0 +1,28 @@
+import { Badge, Tooltip } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ModelInstallStatus } from '../../../../../services/api/types';
+
+const STATUSES = {
+ waiting: { colorScheme: 'cyan', translationKey: 'queue.pending' },
+ downloading: { colorScheme: 'yellow', translationKey: 'queue.in_progress' },
+ running: { colorScheme: 'yellow', translationKey: 'queue.in_progress' },
+ completed: { colorScheme: 'green', translationKey: 'queue.completed' },
+ error: { colorScheme: 'red', translationKey: 'queue.failed' },
+ cancelled: { colorScheme: 'orange', translationKey: 'queue.canceled' },
+};
+
+const ImportQueueBadge = ({ status, detail }: { status?: ModelInstallStatus; detail?: string }) => {
+ const { t } = useTranslation();
+
+ if (!status) {
+ return <>>;
+ }
+
+ return (
+
+ {t(STATUSES[status].translationKey)}
+
+ );
+};
+export default memo(ImportQueueBadge);
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueItem.tsx
new file mode 100644
index 0000000000..1a7803dda3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueue/ImportQueueItem.tsx
@@ -0,0 +1,136 @@
+import { Box, Flex, IconButton, Progress, Tag, Text, Tooltip } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { addToast } from 'features/system/store/systemSlice';
+import { makeToast } from 'features/system/util/makeToast';
+import { t } from 'i18next';
+import { useCallback, useMemo } from 'react';
+import { PiXBold } from 'react-icons/pi';
+import { useDeleteModelImportMutation } from 'services/api/endpoints/models';
+import type { ModelInstallJob, HFModelSource, LocalModelSource, URLModelSource } from 'services/api/types';
+import ImportQueueBadge from './ImportQueueBadge';
+
+type ModelListItemProps = {
+ model: ModelInstallJob;
+};
+
+const formatBytes = (bytes: number) => {
+ const units = ['b', 'kb', 'mb', 'gb', 'tb'];
+
+ let i = 0;
+
+ for (i; bytes >= 1024 && i < 4; i++) {
+ bytes /= 1024;
+ }
+
+ return `${bytes.toFixed(2)} ${units[i]}`;
+};
+
+export const ImportQueueItem = (props: ModelListItemProps) => {
+ const { model } = props;
+ const dispatch = useAppDispatch();
+
+ const [deleteImportModel] = useDeleteModelImportMutation();
+
+ const source = useMemo(() => {
+ if (model.source.type === 'hf') {
+ return model.source as HFModelSource;
+ } else if (model.source.type === 'local') {
+ return model.source as LocalModelSource;
+ } else if (model.source.type === 'url') {
+ return model.source as URLModelSource;
+ } else {
+ return model.source as LocalModelSource;
+ }
+ }, [model.source]);
+
+ const handleDeleteModelImport = useCallback(() => {
+ deleteImportModel(model.id)
+ .unwrap()
+ .then((_) => {
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('toast.modelImportCanceled'),
+ status: 'success',
+ })
+ )
+ );
+ })
+ .catch((error) => {
+ if (error) {
+ dispatch(
+ addToast(
+ makeToast({
+ title: `${error.data.detail} `,
+ status: 'error',
+ })
+ )
+ );
+ }
+ });
+ }, [deleteImportModel, model, dispatch]);
+
+ const modelName = useMemo(() => {
+ switch (source.type) {
+ case 'hf':
+ return source.repo_id;
+ case 'url':
+ return source.url;
+ case 'local':
+ return source.path.substring(source.path.lastIndexOf('/') + 1);
+ default:
+ return '';
+ }
+ }, [source]);
+
+ const progressValue = useMemo(() => {
+ if (model.bytes === undefined || model.total_bytes === undefined) {
+ return 0;
+ }
+
+ return (model.bytes / model.total_bytes) * 100;
+ }, [model.bytes, model.total_bytes]);
+
+ const progressString = useMemo(() => {
+ if (model.status !== 'downloading' || model.bytes === undefined || model.total_bytes === undefined) {
+ return '';
+ }
+ return `${formatBytes(model.bytes)} / ${formatBytes(model.total_bytes)}`;
+ }, [model.bytes, model.total_bytes, model.status]);
+
+ return (
+
+
+
+ {modelName}
+
+
+
+
+
+
+
+
+
+
+
+
+ {(model.status === 'downloading' || model.status === 'waiting') && (
+ }
+ onClick={handleDeleteModelImport}
+ />
+ )}
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueueModel.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueueModel.tsx
deleted file mode 100644
index ebd5b3ec55..0000000000
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ImportQueueModel.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Box, Flex, IconButton, Progress, Text } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { addToast } from 'features/system/store/systemSlice';
-import { makeToast } from 'features/system/util/makeToast';
-import { t } from 'i18next';
-import { useCallback, useMemo } from 'react';
-import { PiXBold } from 'react-icons/pi';
-import { useDeleteModelImportMutation } from 'services/api/endpoints/models';
-import type { ImportModelConfig } from 'services/api/types';
-
-type ModelListItemProps = {
- model: ImportModelConfig;
-};
-
-export const ImportQueueModel = (props: ModelListItemProps) => {
- const { model } = props;
- const dispatch = useAppDispatch();
-
- const [deleteImportModel] = useDeleteModelImportMutation();
-
- const handleDeleteModelImport = useCallback(() => {
- deleteImportModel({ key: model.id })
- .unwrap()
- .then((_) => {
- dispatch(
- addToast(
- makeToast({
- title: t('toast.modelImportCanceled'),
- status: 'success',
- })
- )
- );
- })
- .catch((error) => {
- if (error) {
- dispatch(
- addToast(
- makeToast({
- title: `${error.data.detail} `,
- status: 'error',
- })
- )
- );
- }
- });
- }, [deleteImportModel, model, dispatch]);
-
- const formatBytes = (bytes: number) => {
- const units = ['b', 'kb', 'mb', 'gb', 'tb'];
-
- let i = 0;
-
- for (i; bytes >= 1024 && i < 4; i++) {
- bytes /= 1024;
- }
-
- return `${bytes.toFixed(2)} ${units[i]}`;
- };
-
- const modelName = useMemo(() => {
- return model.source.repo_id || model.source.url || model.source.path.substring(model.source.path.lastIndexOf('/') + 1);
- }, [model.source]);
-
- const progressValue = useMemo(() => {
- return (model.bytes / model.total_bytes) * 100;
- }, [model.bytes, model.total_bytes]);
-
- const progressString = useMemo(() => {
- if (model.status !== 'downloading') {
- return '--';
- }
- return `${formatBytes(model.bytes)} / ${formatBytes(model.total_bytes)}`;
- }, [model.bytes, model.total_bytes, model.status]);
-
- return (
-
-
- {modelName}
-
-
-
- {progressString}
-
- {model.status[0].toUpperCase() + model.status.slice(1)}
-
- {(model.status === 'downloading' || model.status === 'waiting') && (
- }
- onClick={handleDeleteModelImport}
- />
- )}
-
-
- );
-};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelResultItem.tsx
new file mode 100644
index 0000000000..98ad8b0751
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelResultItem.tsx
@@ -0,0 +1,55 @@
+import { Flex, Text, Box, Button, IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { useTranslation } from 'react-i18next';
+import { IoAdd } from 'react-icons/io5';
+import { useAppDispatch } from '../../../../../app/store/storeHooks';
+import { useImportMainModelsMutation } from '../../../../../services/api/endpoints/models';
+import { addToast } from '../../../../system/store/systemSlice';
+import { makeToast } from '../../../../system/util/makeToast';
+
+export const ScanModelResultItem = ({ result }: { result: string }) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+
+ const [importMainModel, { isLoading }] = useImportMainModelsMutation();
+
+ const handleQuickAdd = () => {
+ importMainModel({ source: result, config: undefined })
+ .unwrap()
+ .then((_) => {
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('toast.modelAddedSimple'),
+ status: 'success',
+ })
+ )
+ );
+ })
+ .catch((error) => {
+ if (error) {
+ dispatch(
+ addToast(
+ makeToast({
+ title: `${error.data.detail} `,
+ status: 'error',
+ })
+ )
+ );
+ }
+ });
+ };
+
+ return (
+
+
+ {result.split('\\').slice(-1)[0]}
+ {result}
+
+
+
+ } onClick={handleQuickAdd} />
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelsResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelsResults.tsx
index ef6f386e31..74a374516a 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelsResults.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanModels/ScanModelsResults.tsx
@@ -1,18 +1,10 @@
-import {
- Divider,
- Flex,
- Heading,
- IconButton,
- Input,
- InputGroup,
- InputRightElement,
- Text,
-} from '@invoke-ai/ui-library';
+import { Divider, Flex, Heading, IconButton, Input, InputGroup, InputRightElement, Text } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { t } from 'i18next';
-import type { ChangeEventHandler} from 'react';
+import type { ChangeEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { PiXBold } from 'react-icons/pi';
+import { ScanModelResultItem } from './ScanModelResultItem';
export const ScanModelsResults = ({ results }: { results: string[] }) => {
const [searchTerm, setSearchTerm] = useState('');
@@ -67,12 +59,11 @@ export const ScanModelsResults = ({ results }: { results: string[] }) => {
- {filteredResults.map((result) => (
-
- {result.split('\\').slice(-1)[0]}
- {result}
-
- ))}
+
+ {filteredResults.map((result) => (
+
+ ))}
+
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx
index d7e9fda679..49e4faf893 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx
@@ -1,9 +1,10 @@
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { AdvancedImport } from './AddModelPanel/AdvancedImport';
-import { ImportQueue } from './AddModelPanel/ImportQueue';
+import { ImportQueue } from './AddModelPanel/ImportQueue/ImportQueue';
import { ScanModels } from './AddModelPanel/ScanModels/ScanModels';
import { SimpleImport } from './AddModelPanel/SimpleImport';
+import { ScanModelsForm } from './AddModelPanel/ScanModels/ScanModelsForm';
export const ImportModels = () => {
return (
@@ -23,10 +24,10 @@ export const ImportModels = () => {
-
+
-
+
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index cbe8bf3544..43ea132213 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -127,25 +127,25 @@ export const vaeModelsAdapterSelectors = vaeModelsAdapter.getSelectors(undefined
const buildProvidesTags =
(tagType: (typeof tagTypes)[number]) =>
- (result: EntityState | undefined) => {
- const tags: ApiTagDescription[] = [{ type: tagType, id: LIST_TAG }, 'Model'];
- if (result) {
- tags.push(
- ...result.ids.map((id) => ({
- type: tagType,
- id,
- }))
- );
- }
+ (result: EntityState | undefined) => {
+ const tags: ApiTagDescription[] = [{ type: tagType, id: LIST_TAG }, 'Model'];
+ if (result) {
+ tags.push(
+ ...result.ids.map((id) => ({
+ type: tagType,
+ id,
+ }))
+ );
+ }
- return tags;
- };
+ return tags;
+ };
const buildTransformResponse =
(adapter: EntityAdapter) =>
- (response: { models: T[] }) => {
- return adapter.setAll(adapter.getInitialState(), response.models);
- };
+ (response: { models: T[] }) => {
+ return adapter.setAll(adapter.getInitialState(), response.models);
+ };
/**
* Builds an endpoint URL for the models router
@@ -305,10 +305,10 @@ export const modelsApi = api.injectEndpoints({
},
providesTags: ['ModelImports'],
}),
- deleteModelImport: build.mutation({
- query: ({ key }) => {
+ deleteModelImport: build.mutation({
+ query: (id) => {
return {
- url: buildModelsUrl(`import/${key}`),
+ url: buildModelsUrl(`import/${id}`),
method: 'DELETE',
};
},
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 6f6018d974..02860be0c8 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -117,6 +117,13 @@ export const isRefinerMainModelModelConfig = (config: AnyModelConfig): config is
export type MergeModelConfig = S['Body_merge'];
export type ImportModelConfig = S['Body_import_model'];
+export type ModelInstallJob = S['ModelInstallJob']
+export type ModelInstallStatus = S["InstallStatus"]
+
+export type HFModelSource = S['HFModelSource'];
+export type CivitaiModelSource = S['CivitaiModelSource'];
+export type LocalModelSource = S['LocalModelSource'];
+export type URLModelSource = S['URLModelSource'];
// Graphs
export type Graph = S['Graph'];