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;