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'];