diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 5f34bbb2e78..db91c0c1a30 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -763,7 +763,7 @@ "convertToDiffusers": "Convert To Diffusers", "convertToDiffusersHelpText1": "This model will be converted to the 🧨 Diffusers format.", "convertToDiffusersHelpText2": "This process will replace your Model Manager entry with the Diffusers version of the same model.", - "convertToDiffusersHelpText3": "Your checkpoint file on disk WILL be deleted if it is in InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.", + "convertToDiffusersHelpText3": "Your checkpoint file on disk WILL be deleted if it is in the InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.", "convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.", "convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.", "convertToDiffusersHelpText6": "Do you wish to convert this model?", @@ -869,7 +869,24 @@ "starterBundleHelpText": "Easily install all models needed to get started with a base model, including a main model, controlnets, IP adapters, and more. Selecting a bundle will skip any models that you already have installed.", "starterModels": "Starter Models", "starterModelsInModelManager": "Starter Models can be found in Model Manager", + "launchpadTab": "Launchpad", + "launchpad": { + "welcome": "Welcome to Model Management", + "description": "Invoke requires you to install or import models to utilize many of the features in the platform. In order to get started, you can begin by adding models to your InvokeAI installation. Choose from manual installation options or explore curated starter models.", + "manualInstall": "Manual Installation", + "urlDescription": "Install models from a URL or local file path. Perfect for specific models you want to add.", + "huggingFaceDescription": "Browse and install models directly from HuggingFace repositories.", + "scanFolderDescription": "Scan a local folder to automatically detect and install models.", + "recommendedModels": "Recommended Models", + "exploreStarter": "Browse all available starter models", + "quickStart": "Quick Start Bundles", + "bundleDescription": "Each bundle includes essential models for each model family and curated base models to get started", + "browseAll": "Or browse all available models:" + }, "controlLora": "Control LoRA", + "downloadBundle": "Download {bundleName} Bundle", + "startDownload": "Start Download", + "bundleDownloadConfirmation": "This will download all models associated with the {bundleName} bundle and may take some time. Model downloads can be cancelled at any time. Are you ready to begin?", "llavaOnevision": "LLaVA OneVision", "syncModels": "Sync Models", "textualInversions": "Textual Inversions", diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx index 0fc6612ad13..9a333138081 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon'; import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages'; -import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectIsLocal } from 'features/system/store/configSlice'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 101394f85ac..331644593bb 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx @@ -1,6 +1,6 @@ import { Button, Text, useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useCallback, useEffect, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts new file mode 100644 index 00000000000..c6813c422f8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts @@ -0,0 +1,7 @@ +import { atom } from 'nanostores'; + +/** + * Atom to manage the active tab index for the Install Models panel. + * Moved to separate file to avoid circular dependencies. + */ +export const $installModelsTab = atom(0); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx new file mode 100644 index 00000000000..a1bcd365284 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx @@ -0,0 +1,285 @@ +import { Box, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { flattenStarterModel, useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; +import { toast } from 'features/toast/toast'; +import { flatMap, negate, uniqWith } from 'lodash-es'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi'; +import { SiHuggingface } from 'react-icons/si'; +import { useGetStarterModelsQuery, useInstallModelMutation } from 'services/api/endpoints/models'; + +export const LaunchpadForm = memo(() => { + const { t } = useTranslation(); + const [installModel] = useInstallModelMutation(); + const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg(); + const { data: starterModelsData } = useGetStarterModelsQuery(); + // Function to install models from a bundle + const installBundle = useCallback( + (bundleName: string) => { + if (!starterModelsData?.starter_bundles) { + return; + } + + const bundle = starterModelsData.starter_bundles[bundleName]; + if (!bundle) { + return; + } + + // Flatten the models and remove duplicates, which is expected as models can have the same dependencies + const flattenedModels = flatMap(bundle, flattenStarterModel); + const uniqueModels = uniqWith( + flattenedModels, + (m1, m2) => m1.source === m2.source || (m1.name === m2.name && m1.base === m2.base && m1.type === m2.type) + ); + // We want to install models that are not installed and skip models that are already installed + const install = uniqueModels.filter(negate(getIsInstalled)).map(buildModelInstallArg); + const skip = uniqueModels.filter(getIsInstalled).map(buildModelInstallArg); + + if (install.length === 0) { + toast({ + status: 'info', + title: t('modelManager.bundleAlreadyInstalled', { bundleName }), + description: t('modelManager.allModelsAlreadyInstalled'), + }); + return; + } + + // Install all models in the bundle + install.forEach(installModel); + + let description = t('modelManager.installingXModels', { count: install.length }); + if (skip.length > 1) { + description += t('modelManager.skippingXDuplicates', { count: skip.length - 1 }); + } + + toast({ + status: 'info', + title: t('modelManager.installingBundle'), + description, + }); + }, + [starterModelsData, getIsInstalled, buildModelInstallArg, installModel, t] + ); + + const navigateToUrlTab = useCallback(() => { + $installModelsTab.set(1); // URL/Local Path tab (now index 1) + }, []); + + const navigateToHuggingFaceTab = useCallback(() => { + $installModelsTab.set(2); // HuggingFace tab (now index 2) + }, []); + + const navigateToScanFolderTab = useCallback(() => { + $installModelsTab.set(3); // Scan Folder tab (now index 3) + }, []); + + const navigateToStarterModelsTab = useCallback(() => { + $installModelsTab.set(4); // Starter Models tab (now index 4) + }, []); + const handleSD15BundleClick = useCallback(() => { + installBundle('sd-1'); + }, [installBundle]); + + const handleSDXLBundleClick = useCallback(() => { + installBundle('sdxl'); + }, [installBundle]); + + const handleFluxBundleClick = useCallback(() => { + installBundle('flux'); + }, [installBundle]); + return ( + + + + {/* Welcome Section */} + + + {t('modelManager.launchpad.welcome')} + + + {t('modelManager.launchpad.description')} + + + {/* Manual Installation Options */} + + + {t('modelManager.launchpad.manualInstall')} + {' '} + + } + onClick={navigateToUrlTab} + /> + } + onClick={navigateToHuggingFaceTab} + /> + } + onClick={navigateToScanFolderTab} + /> + + {' '} + {/* Recommended Section */} + + + {t('modelManager.launchpad.recommendedModels')} + + + {' '} + {/* Starter Model Bundles - More Prominent */} + + + {t('modelManager.launchpad.quickStart')} + + + {t('modelManager.launchpad.bundleDescription')} + {' '} + + + + + + {' '} + {/* Browse All - Simple Link */} + + + {t('modelManager.launchpad.browseAll')} + + {' '} + {' '} + {' '} + + + + + ); +}); + +LaunchpadForm.displayName = 'LaunchpadForm'; + +interface LaunchpadCardProps { + title: string; + description: string; + icon: React.ReactNode; + onClick: () => void; + variant?: 'default' | 'featured'; +} + +const LaunchpadCard = memo(({ title, description, icon, onClick, variant = 'default' }: LaunchpadCardProps) => { + return ( + + ); +}); + +LaunchpadCard.displayName = 'LaunchpadCard'; + +interface LaunchpadBundleCardProps { + title: string; + onClick: () => void; +} + +const LaunchpadBundleCard = memo(({ title, onClick }: LaunchpadBundleCardProps) => { + return ( + + ); +}); + +LaunchpadBundleCard.displayName = 'LaunchpadBundleCard'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx index 754dab8c782..92a9019228d 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx @@ -1,18 +1,17 @@ import { Box, Button, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm'; -import { atom } from 'nanostores'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiInfoBold } from 'react-icons/pi'; import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm'; import { InstallModelForm } from './AddModelPanel/InstallModelForm'; +import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm'; import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue'; import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm'; -export const $installModelsTab = atom(0); - export const InstallModels = memo(() => { const { t } = useTranslation(); const index = useStore($installModelsTab); @@ -31,16 +30,20 @@ export const InstallModels = memo(() => { - - + {' '} + {' '} + {t('modelManager.launchpadTab')} {t('modelManager.urlOrLocalPath')} {t('modelManager.huggingFace')} {t('modelManager.scanFolder')} {t('modelManager.starterModels')} - + {' '} + + + diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx index d8665295f19..bd15e246aad 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx @@ -18,7 +18,7 @@ import type { Group, PickerContextState } from 'common/components/Picker/Picker' import { buildGroup, getRegex, Picker, usePickerContext } from 'common/components/Picker/Picker'; import { useDisclosure } from 'common/hooks/useBoolean'; import { typedMemo } from 'common/util/typedMemo'; -import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import { BASE_COLOR_MAP } from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage'; import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton'; diff --git a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx index 2d8503775dc..2c491e43bd5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx @@ -11,7 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import ParamPostProcessingModel from 'features/parameters/components/PostProcessing/ParamPostProcessingModel'; import { selectPostProcessingModel } from 'features/parameters/store/upscaleSlice'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx index c2ea2c0930f..49344b3934f 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx @@ -1,7 +1,7 @@ import { Button, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectModel } from 'features/controlLayers/store/paramsSlice'; -import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; +import { $installModelsTab } from 'features/modelManagerV2/store/installModelsStore'; import { useIsTooLargeToUpscale } from 'features/parameters/hooks/useIsTooLargeToUpscale'; import { selectTileControlNetModel,