diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 216b0d34c0..0cfaf912f0 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -688,6 +688,7 @@
"settings": "Settings",
"simpleModelPlaceholder": "URL or path to a local file or diffusers folder",
"source": "Source",
+ "starterModels": "Starter Models",
"syncModels": "Sync Models",
"triggerPhrases": "Trigger Phrases",
"loraTriggerPhrases": "LoRA Trigger Phrases",
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx
new file mode 100644
index 0000000000..00bcbafc0b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx
@@ -0,0 +1,75 @@
+import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
+import { addToast } from 'features/system/store/systemSlice';
+import { makeToast } from 'features/system/util/makeToast';
+import { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiPlusBold } from 'react-icons/pi';
+import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
+import { useInstallModelMutation } from 'services/api/endpoints/models';
+
+type Props = {
+ result: GetStarterModelsResponse[number];
+};
+export const StarterModelsResultItem = ({ result }: Props) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const allSources = useMemo(() => {
+ const _allSources = [result.source];
+ if (result.dependencies) {
+ _allSources.push(...result.dependencies);
+ }
+ return _allSources;
+ }, [result]);
+ const [installModel] = useInstallModelMutation();
+
+ const handleQuickAdd = useCallback(() => {
+ for (const source of allSources) {
+ installModel({ source })
+ .unwrap()
+ .then((_) => {
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('toast.modelAddedSimple'),
+ status: 'success',
+ })
+ )
+ );
+ })
+ .catch((error) => {
+ if (error) {
+ dispatch(
+ addToast(
+ makeToast({
+ title: `${error.data.detail} `,
+ status: 'error',
+ })
+ )
+ );
+ }
+ });
+ }
+ }, [allSources, installModel, dispatch, t]);
+
+ return (
+
+
+
+ {result.type.replace('_', ' ')}
+
+ {result.name}
+
+ {result.description}
+
+
+ {result.is_installed ? (
+ {t('common.installed')}
+ ) : (
+ } onClick={handleQuickAdd} size="sm" />
+ )}
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm.tsx
new file mode 100644
index 0000000000..3198f1df78
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm.tsx
@@ -0,0 +1,16 @@
+import { Flex } from '@invoke-ai/ui-library';
+import { FetchingModelsLoader } from 'features/modelManagerV2/subpanels/ModelManagerPanel/FetchingModelsLoader';
+import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
+
+import { StarterModelsResults } from './StarterModelsResults';
+
+export const StarterModelsForm = () => {
+ const { isLoading, data } = useGetStarterModelsQuery();
+
+ return (
+
+ {isLoading && }
+ {data && }
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx
new file mode 100644
index 0000000000..7aa05af300
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx
@@ -0,0 +1,72 @@
+import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import type { ChangeEventHandler } from 'react';
+import { useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
+
+import { StarterModelsResultItem } from './StartModelsResultItem';
+
+type StarterModelsResultsProps = {
+ results: NonNullable;
+};
+
+export const StarterModelsResults = ({ results }: StarterModelsResultsProps) => {
+ const { t } = useTranslation();
+ const [searchTerm, setSearchTerm] = useState('');
+
+ const filteredResults = useMemo(() => {
+ return results.filter((result) => {
+ const name = result.name.toLowerCase();
+ const type = result.type.toLowerCase();
+ return name.includes(searchTerm.toLowerCase()) || type.includes(searchTerm.toLowerCase());
+ });
+ }, [results, searchTerm]);
+
+ const handleSearch: ChangeEventHandler = useCallback((e) => {
+ setSearchTerm(e.target.value.trim());
+ }, []);
+
+ const clearSearch = useCallback(() => {
+ setSearchTerm('');
+ }, []);
+
+ return (
+
+
+
+
+
+ {searchTerm && (
+
+ }
+ onClick={clearSearch}
+ flexShrink={0}
+ />
+
+ )}
+
+
+
+
+
+ {filteredResults.map((result) => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
index e338a43c87..d09ab67fa4 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
@@ -1,5 +1,8 @@
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
+import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
+import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
+import { useMainModels } from 'services/api/hooks/modelsByType';
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
import { InstallModelForm } from './AddModelPanel/InstallModelForm';
@@ -8,14 +11,23 @@ import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
export const InstallModels = () => {
const { t } = useTranslation();
+ const [mainModels, { data }] = useMainModels();
+ const defaultIndex = useMemo(() => {
+ if (data && mainModels.length) {
+ return 0;
+ }
+ return 3;
+ }, [data, mainModels.length]);
+
return (
{t('modelManager.addModel')}
-
+
{t('modelManager.urlOrLocalPath')}
{t('modelManager.huggingFace')}
{t('modelManager.scanFolder')}
+ {t('modelManager.starterModels')}
@@ -27,6 +39,9 @@ export const InstallModels = () => {
+
+
+
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index 6dd74b22d9..520208bf5b 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -27,6 +27,9 @@ type GetModelConfigsResponse = NonNullable<
paths['/api/v2/models/']['get']['responses']['200']['content']['application/json']
>;
+export type GetStarterModelsResponse =
+ paths['/api/v2/models/starter_models']['get']['responses']['200']['content']['application/json'];
+
type DeleteModelArg = {
key: string;
};
@@ -259,6 +262,9 @@ export const modelsApi = api.injectEndpoints({
});
},
}),
+ getStarterModels: build.query({
+ query: () => buildModelsUrl('starter_models'),
+ }),
}),
});
@@ -277,4 +283,5 @@ export const {
useListModelInstallsQuery,
useCancelModelInstallMutation,
usePruneCompletedModelInstallsMutation,
+ useGetStarterModelsQuery,
} = modelsApi;