Skip to content

Commit cd21816

Browse files
hipsterusernamepsychedelicious
authored andcommitted
Model Launchpad
1 parent 605b912 commit cd21816

File tree

4 files changed

+306
-11
lines changed

4 files changed

+306
-11
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@
767767
"convertToDiffusers": "Convert To Diffusers",
768768
"convertToDiffusersHelpText1": "This model will be converted to the 🧨 Diffusers format.",
769769
"convertToDiffusersHelpText2": "This process will replace your Model Manager entry with the Diffusers version of the same model.",
770-
"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.",
770+
"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.",
771771
"convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.",
772772
"convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.",
773773
"convertToDiffusersHelpText6": "Do you wish to convert this model?",
@@ -815,8 +815,7 @@
815815
"inplaceInstall": "In-place install",
816816
"inplaceInstallDesc": "Install models without copying the files. When using the model, it will be loaded from its this location. If disabled, the model file(s) will be copied into the Invoke-managed models directory during installation.",
817817
"install": "Install",
818-
"installAll": "Install All",
819-
"installRepo": "Install Repo",
818+
"installAll": "Install All", "installRepo": "Install Repo",
820819
"ipAdapters": "IP Adapters",
821820
"learnMoreAboutSupportedModels": "Learn more about the models we support",
822821
"load": "Load",
@@ -870,10 +869,25 @@
870869
"sigLip": "SigLIP",
871870
"spandrelImageToImage": "Image to Image (Spandrel)",
872871
"starterBundles": "Starter Bundles",
873-
"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.",
874-
"starterModels": "Starter Models",
872+
"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",
875873
"starterModelsInModelManager": "Starter Models can be found in Model Manager",
874+
"launchpadTab": "Launchpad",
875+
"launchpad": {
876+
"welcome": "Welcome to Model Management",
877+
"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.",
878+
"manualInstall": "Manual Installation",
879+
"urlDescription": "Install models from a URL or local file path. Perfect for specific models you want to add.",
880+
"huggingFaceDescription": "Browse and install models directly from HuggingFace repositories.",
881+
"scanFolderDescription": "Scan a local folder to automatically detect and install models.", "recommendedModels": "Recommended Models",
882+
"exploreStarter": "Browse all available starter models",
883+
"quickStart": "Quick Start Bundles",
884+
"bundleDescription": "Each bundle includes essential models for each model family and curated base models to get started",
885+
"browseAll": "Or browse all available models:"
886+
},
876887
"controlLora": "Control LoRA",
888+
"downloadBundle": "Download {bundleName} Bundle",
889+
"startDownload": "Start Download",
890+
"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?",
877891
"llavaOnevision": "LLaVA OneVision",
878892
"syncModels": "Sync Models",
879893
"textualInversions": "Textual Inversions",
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import { Box, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
2+
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
3+
import { flattenStarterModel, useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall';
4+
import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels';
5+
import { toast } from 'features/toast/toast';
6+
import { flatMap, negate, uniqWith } from 'lodash-es';
7+
import { memo, useCallback } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi';
10+
import { SiHuggingface } from "react-icons/si";
11+
import { useGetStarterModelsQuery, useInstallModelMutation } from 'services/api/endpoints/models';
12+
13+
export const LaunchpadForm = memo(() => {
14+
const { t } = useTranslation();
15+
const [installModel] = useInstallModelMutation();
16+
const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg();
17+
const { data: starterModelsData } = useGetStarterModelsQuery();
18+
// Function to install models from a bundle
19+
const installBundle = useCallback((bundleName: string) => {
20+
if (!starterModelsData?.starter_bundles) {
21+
return;
22+
}
23+
24+
const bundle = starterModelsData.starter_bundles[bundleName];
25+
if (!bundle) {
26+
return;
27+
}
28+
29+
// Flatten the models and remove duplicates, which is expected as models can have the same dependencies
30+
const flattenedModels = flatMap(bundle, flattenStarterModel);
31+
const uniqueModels = uniqWith(
32+
flattenedModels,
33+
(m1, m2) => m1.source === m2.source || (m1.name === m2.name && m1.base === m2.base && m1.type === m2.type)
34+
);
35+
// We want to install models that are not installed and skip models that are already installed
36+
const install = uniqueModels.filter(negate(getIsInstalled)).map(buildModelInstallArg);
37+
const skip = uniqueModels.filter(getIsInstalled).map(buildModelInstallArg);
38+
39+
if (install.length === 0) {
40+
toast({
41+
status: 'info',
42+
title: t('modelManager.bundleAlreadyInstalled', { bundleName }),
43+
description: t('modelManager.allModelsAlreadyInstalled'),
44+
});
45+
return;
46+
}
47+
48+
// Install all models in the bundle
49+
install.forEach(installModel);
50+
51+
let description = t('modelManager.installingXModels', { count: install.length });
52+
if (skip.length > 1) {
53+
description += t('modelManager.skippingXDuplicates', { count: skip.length - 1 });
54+
}
55+
56+
toast({
57+
status: 'info',
58+
title: t('modelManager.installingBundle'),
59+
description,
60+
});
61+
}, [starterModelsData, getIsInstalled, buildModelInstallArg, installModel, t]);
62+
63+
const navigateToUrlTab = useCallback(() => {
64+
$installModelsTab.set(1); // URL/Local Path tab (now index 1)
65+
}, []);
66+
67+
const navigateToHuggingFaceTab = useCallback(() => {
68+
$installModelsTab.set(2); // HuggingFace tab (now index 2)
69+
}, []);
70+
71+
const navigateToScanFolderTab = useCallback(() => {
72+
$installModelsTab.set(3); // Scan Folder tab (now index 3)
73+
}, []);
74+
75+
const navigateToStarterModelsTab = useCallback(() => {
76+
$installModelsTab.set(4); // Starter Models tab (now index 4)
77+
}, []);
78+
const handleSD15BundleClick = useCallback(() => {
79+
installBundle('sd-1');
80+
}, [installBundle]);
81+
82+
const handleSDXLBundleClick = useCallback(() => {
83+
installBundle('sdxl');
84+
}, [installBundle]);
85+
86+
const handleFluxBundleClick = useCallback(() => {
87+
installBundle('flux');
88+
}, [installBundle]); return (
89+
<Flex flexDir="column" height="100%" gap={3}>
90+
<ScrollableContent>
91+
<Flex flexDir="column" gap={6} p={3}>
92+
{/* Welcome Section */}
93+
<Box>
94+
<Heading size="md" mb={1}>
95+
{t('modelManager.launchpad.welcome')}
96+
</Heading>
97+
<Text color="base.300" fontSize="sm">
98+
{t('modelManager.launchpad.description')}
99+
</Text>
100+
</Box>
101+
102+
{/* Manual Installation Options */}
103+
<Box>
104+
<Heading size="sm" mb={2}>
105+
{t('modelManager.launchpad.manualInstall')}
106+
</Heading> <Grid templateColumns="repeat(auto-fit, minmax(280px, 1fr))" gap={3}>
107+
<LaunchpadCard
108+
title={t('modelManager.urlOrLocalPath')}
109+
description={t('modelManager.launchpad.urlDescription')}
110+
icon={<PiLinkBold size={24} />}
111+
onClick={navigateToUrlTab}
112+
/>
113+
<LaunchpadCard
114+
title={t('modelManager.huggingFace')}
115+
description={t('modelManager.launchpad.huggingFaceDescription')}
116+
icon={<SiHuggingface size={24} />}
117+
onClick={navigateToHuggingFaceTab}
118+
/>
119+
<LaunchpadCard
120+
title={t('modelManager.scanFolder')}
121+
description={t('modelManager.launchpad.scanFolderDescription')}
122+
icon={<PiFolderOpenBold size={24} />}
123+
onClick={navigateToScanFolderTab}
124+
/>
125+
</Grid>
126+
</Box> {/* Recommended Section */}
127+
<Box>
128+
<Heading size="sm" mb={2}>
129+
{t('modelManager.launchpad.recommendedModels')}
130+
</Heading>
131+
<Flex flexDir="column" gap={2}> {/* Starter Model Bundles - More Prominent */}
132+
<Box>
133+
<Heading size="xs" color="base.100" mb={1}>
134+
{t('modelManager.launchpad.quickStart')}
135+
</Heading>
136+
<Text fontSize="xs" color="base.300" mb={2}>
137+
{t('modelManager.launchpad.bundleDescription')}
138+
</Text> <Grid templateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap={2}>
139+
<LaunchpadBundleCard
140+
title="Stable Diffusion 1.5"
141+
onClick={handleSD15BundleClick}
142+
/>
143+
<LaunchpadBundleCard
144+
title="SDXL"
145+
onClick={handleSDXLBundleClick}
146+
/>
147+
<LaunchpadBundleCard
148+
title="FLUX.1 [dev]"
149+
onClick={handleFluxBundleClick}
150+
/>
151+
</Grid>
152+
</Box> {/* Browse All - Simple Link */}
153+
<Box pt={1} borderTop="1px solid" borderColor="base.700">
154+
<Text fontSize="xs" color="base.400" mb={1}>
155+
{t('modelManager.launchpad.browseAll')}
156+
</Text>
157+
<Button
158+
onClick={navigateToStarterModelsTab}
159+
variant="link" color="invokeBlue.300"
160+
fontSize="sm"
161+
fontWeight="medium"
162+
p={0}
163+
h="auto"
164+
leftIcon={<PiStarBold size={16} />}
165+
_hover={{
166+
color: "invokeBlue.200",
167+
textDecoration: "underline"
168+
}}
169+
>
170+
{t('modelManager.launchpad.exploreStarter')}
171+
</Button> </Box> </Flex> </Box>
172+
</Flex>
173+
</ScrollableContent>
174+
</Flex>
175+
);
176+
});
177+
178+
LaunchpadForm.displayName = 'LaunchpadForm';
179+
180+
interface LaunchpadCardProps {
181+
title: string;
182+
description: string;
183+
icon: React.ReactNode;
184+
onClick: () => void;
185+
variant?: 'default' | 'featured';
186+
}
187+
188+
const LaunchpadCard = memo(({ title, description, icon, onClick, variant = 'default' }: LaunchpadCardProps) => {
189+
return ( <Button
190+
onClick={onClick}
191+
variant="outline"
192+
h="auto"
193+
minH="50px"
194+
p={4}
195+
borderWidth={variant === 'featured' ? 2 : 1}
196+
borderColor={variant === 'featured' ? 'invokeBlue.300' : 'base.700'}
197+
bg={variant === 'featured' ? 'invokeBlue.900' : 'base.850'}
198+
_hover={{
199+
bg: variant === 'featured' ? 'invokeBlue.800' : 'base.800',
200+
borderColor: variant === 'featured' ? 'invokeBlue.200' : 'base.600',
201+
transform: 'translateY(-2px)',
202+
}}
203+
_active={{
204+
transform: 'translateY(0px)',
205+
}}
206+
transition="all 0.2s"
207+
cursor="pointer"
208+
textAlign="left"
209+
justifyContent="flex-start"
210+
alignItems="flex-start"
211+
flexDir="column"
212+
gap={2}
213+
borderRadius="lg"
214+
whiteSpace="normal"
215+
>
216+
<Flex alignItems="center" gap={2} w="full">
217+
<Box color={variant === 'featured' ? 'invokeBlue.200' : 'base.300'} flexShrink={0}>
218+
{icon}
219+
</Box>
220+
<Heading size="sm" color={variant === 'featured' ? 'invokeBlue.50' : 'base.100'} noOfLines={2}>
221+
{title}
222+
</Heading>
223+
</Flex>
224+
<Text
225+
fontSize="sm"
226+
color={variant === 'featured' ? 'invokeBlue.200' : 'base.400'}
227+
lineHeight="1.4"
228+
flex="1"
229+
whiteSpace="normal"
230+
wordBreak="break-word"
231+
>
232+
{description}
233+
</Text>
234+
</Button>
235+
);
236+
});
237+
238+
LaunchpadCard.displayName = 'LaunchpadCard';
239+
240+
interface LaunchpadBundleCardProps {
241+
title: string;
242+
onClick: () => void;
243+
}
244+
245+
const LaunchpadBundleCard = memo(({ title, onClick }: LaunchpadBundleCardProps) => {
246+
return (
247+
<Button
248+
onClick={onClick}
249+
variant="outline"
250+
h="auto"
251+
minH="40px"
252+
p={3}
253+
borderWidth={2}
254+
borderColor="invokeBlue.400"
255+
bg="invokeBlue.950"
256+
_hover={{
257+
bg: "invokeBlue.900",
258+
borderColor: "invokeBlue.300",
259+
transform: "translateY(-2px)",
260+
boxShadow: "0 4px 20px rgba(66, 153, 225, 0.15)",
261+
}}
262+
_active={{
263+
transform: "translateY(0px)",
264+
}}
265+
transition="all 0.2s"
266+
cursor="pointer"
267+
textAlign="center"
268+
justifyContent="center"
269+
alignItems="center" borderRadius="lg"
270+
whiteSpace="normal"
271+
>
272+
<Text fontSize="sm" fontWeight="bold" color="invokeBlue.100" noOfLines={1}>
273+
{title}
274+
</Text>
275+
</Button>
276+
);
277+
});
278+
279+
LaunchpadBundleCard.displayName = 'LaunchpadBundleCard';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LaunchpadForm } from './LaunchpadForm';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PiInfoBold } from 'react-icons/pi';
88

99
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
1010
import { InstallModelForm } from './AddModelPanel/InstallModelForm';
11+
import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm';
1112
import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue';
1213
import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
1314

@@ -31,16 +32,16 @@ export const InstallModels = memo(() => {
3132
<Button alignItems="center" variant="link" leftIcon={<PiInfoBold />} onClick={onClickLearnMore}>
3233
<Text variant="subtext">{t('modelManager.learnMoreAboutSupportedModels')}</Text>
3334
</Button>
34-
</Flex>
35-
36-
<Tabs variant="collapse" height="50%" display="flex" flexDir="column" index={index} onChange={onChange}>
37-
<TabList>
35+
</Flex> <Tabs variant="collapse" height="50%" display="flex" flexDir="column" index={index} onChange={onChange}> <TabList>
36+
<Tab>{t('modelManager.launchpadTab')}</Tab>
3837
<Tab>{t('modelManager.urlOrLocalPath')}</Tab>
3938
<Tab>{t('modelManager.huggingFace')}</Tab>
4039
<Tab>{t('modelManager.scanFolder')}</Tab>
4140
<Tab>{t('modelManager.starterModels')}</Tab>
42-
</TabList>
43-
<TabPanels p={3} height="100%">
41+
</TabList> <TabPanels p={3} height="100%">
42+
<TabPanel height="100%">
43+
<LaunchpadForm />
44+
</TabPanel>
4445
<TabPanel>
4546
<InstallModelForm />
4647
</TabPanel>

0 commit comments

Comments
 (0)