Skip to content

Commit 9862ba9

Browse files
feat(ui): improved starter model buttons & tooltips
1 parent 920aea0 commit 9862ba9

File tree

7 files changed

+144
-95
lines changed

7 files changed

+144
-95
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,11 @@
810810
"urlUnauthorizedErrorMessage": "You may need to configure an API token to access this model.",
811811
"urlUnauthorizedErrorMessage2": "Learn how here.",
812812
"imageEncoderModelId": "Image Encoder Model ID",
813-
"includesNModels": "Includes {{n}} models and their dependencies",
813+
"installedModelsCount": "{{installed}} of {{total}} models installed.",
814+
"includesNModels": "Includes {{n}} models and their dependencies.",
815+
"allNModelsInstalled": "All {{count}} models installed",
816+
"nToInstall": "{{count}} to install",
817+
"nAlreadyInstalled": "{{count}} already installed",
814818
"installQueue": "Install Queue",
815819
"inplaceInstall": "In-place install",
816820
"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.",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useMemo } from 'react';
2+
import type { S } from 'services/api/types';
3+
4+
import { useStarterBundleInstall } from './useStarterBundleInstall';
5+
6+
export const useStarterBundleInstallStatus = (bundle: S['StarterModelBundle']) => {
7+
const { getModelsToInstall } = useStarterBundleInstall();
8+
const total = useMemo(() => bundle.models.length, [bundle.models.length]);
9+
const install = useMemo(() => getModelsToInstall(bundle).install, [bundle, getModelsToInstall]);
10+
const skip = useMemo(() => getModelsToInstall(bundle).skip, [bundle, getModelsToInstall]);
11+
12+
return { total, skip, install };
13+
};

invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Button, Flex, Grid, Heading, Icon, Text } from '@invoke-ai/ui-library';
22
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
3-
import { useStarterBundleInstall } from 'features/modelManagerV2/hooks/useStarterBundleInstall';
3+
import { map } from 'es-toolkit/compat';
44
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
5+
import { StarterBundleButton } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundle';
6+
import { StarterBundleTooltipContentCompact } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleTooltipContentCompact';
57
import { memo, useCallback } from 'react';
68
import { useTranslation } from 'react-i18next';
79
import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi';
@@ -10,26 +12,8 @@ import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
1012

1113
export const LaunchpadForm = memo(() => {
1214
const { t } = useTranslation();
13-
const { installBundle } = useStarterBundleInstall();
1415
const { data: starterModelsData } = useGetStarterModelsQuery();
1516

16-
// Function to install models from a bundle
17-
const handleBundleInstall = useCallback(
18-
(bundleName: string) => {
19-
if (!starterModelsData?.starter_bundles) {
20-
return;
21-
}
22-
23-
const bundle = starterModelsData.starter_bundles[bundleName];
24-
if (!bundle) {
25-
return;
26-
}
27-
28-
installBundle(bundle);
29-
},
30-
[starterModelsData, installBundle]
31-
);
32-
3317
const navigateToUrlTab = useCallback(() => {
3418
setInstallModelsTabByName('urlOrLocal');
3519
}, []);
@@ -46,18 +30,6 @@ export const LaunchpadForm = memo(() => {
4630
setInstallModelsTabByName('starterModels');
4731
}, []);
4832

49-
const handleSD15BundleClick = useCallback(() => {
50-
handleBundleInstall('sd-1');
51-
}, [handleBundleInstall]);
52-
53-
const handleSDXLBundleClick = useCallback(() => {
54-
handleBundleInstall('sdxl');
55-
}, [handleBundleInstall]);
56-
57-
const handleFluxBundleClick = useCallback(() => {
58-
handleBundleInstall('flux');
59-
}, [handleBundleInstall]);
60-
6133
return (
6234
<Flex flexDir="column" height="100%" gap={3}>
6335
<ScrollableContent>
@@ -145,32 +117,36 @@ export const LaunchpadForm = memo(() => {
145117
</Grid>
146118
</Flex>
147119
{/* Recommended Section */}
148-
<Flex flexDir="column" gap={2} alignItems="flex-start">
149-
<Heading size="sm">{t('modelManager.launchpad.recommendedModels')}</Heading>
150-
{/* Starter Model Bundles - More Prominent */}
151-
<Text color="base.300">{t('modelManager.launchpad.bundleDescription')}</Text>
152-
<Grid templateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap={2} w="full">
153-
<Button onClick={handleSD15BundleClick} variant="outline" p={6}>
154-
{t('modelManager.launchpad.stableDiffusion15')}
155-
</Button>
156-
<Button onClick={handleSDXLBundleClick} variant="outline" p={6}>
157-
{t('modelManager.launchpad.sdxl')}
158-
</Button>
159-
<Button onClick={handleFluxBundleClick} variant="outline" p={6}>
160-
{t('modelManager.launchpad.fluxDev')}
120+
{starterModelsData && (
121+
<Flex flexDir="column" gap={2} alignItems="flex-start">
122+
<Heading size="sm">{t('modelManager.launchpad.recommendedModels')}</Heading>
123+
{/* Starter Model Bundles - More Prominent */}
124+
<Text color="base.300">{t('modelManager.launchpad.bundleDescription')}</Text>
125+
<Grid templateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap={2} w="full">
126+
{map(starterModelsData.starter_bundles, (bundle) => (
127+
<StarterBundleButton
128+
size="md"
129+
tooltip={<StarterBundleTooltipContentCompact bundle={bundle} />}
130+
key={bundle.name}
131+
bundle={bundle}
132+
variant="outline"
133+
p={4}
134+
h="unset"
135+
/>
136+
))}
137+
</Grid>
138+
{/* Browse All - Simple Link */}
139+
<Button
140+
onClick={navigateToStarterModelsTab}
141+
variant="link"
142+
size="sm"
143+
leftIcon={<PiStarBold />}
144+
colorScheme="invokeBlue"
145+
>
146+
{t('modelManager.launchpad.exploreStarter')}
161147
</Button>
162-
</Grid>
163-
{/* Browse All - Simple Link */}
164-
<Button
165-
onClick={navigateToStarterModelsTab}
166-
variant="link"
167-
size="sm"
168-
leftIcon={<PiStarBold />}
169-
colorScheme="invokeBlue"
170-
>
171-
{t('modelManager.launchpad.exploreStarter')}
172-
</Button>
173-
</Flex>
148+
</Flex>
149+
)}
174150
</Flex>
175151
</ScrollableContent>
176152
</Flex>
Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,21 @@
1-
import { Button, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
1+
import type { ButtonProps } from '@invoke-ai/ui-library';
2+
import { Button } from '@invoke-ai/ui-library';
23
import { useStarterBundleInstall } from 'features/modelManagerV2/hooks/useStarterBundleInstall';
3-
import { isMainModelBase } from 'features/nodes/types/common';
4-
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
5-
import { useCallback, useMemo } from 'react';
6-
import { useTranslation } from 'react-i18next';
4+
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
5+
import { useCallback } from 'react';
76
import type { S } from 'services/api/types';
87

9-
export const StarterBundle = ({ bundle }: { bundle: S['StarterModelBundle'] }) => {
10-
const { installBundle, getModelsToInstall } = useStarterBundleInstall();
11-
const { t } = useTranslation();
12-
13-
const modelsToInstall = useMemo(() => getModelsToInstall(bundle), [bundle, getModelsToInstall]);
8+
export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterModelBundle'] } & ButtonProps) => {
9+
const { installBundle } = useStarterBundleInstall();
10+
const { install } = useStarterBundleInstallStatus(bundle);
1411

1512
const handleClickBundle = useCallback(() => {
1613
installBundle(bundle);
1714
}, [installBundle, bundle]);
1815

1916
return (
20-
<Tooltip
21-
label={
22-
<Flex flexDir="column" p={1}>
23-
<Text>{t('modelManager.includesNModels', { n: bundle.models.length })}:</Text>
24-
<UnorderedList>
25-
{bundle.models.map((model, index) => (
26-
<ListItem key={index} wordBreak="break-all">
27-
{model.name}
28-
</ListItem>
29-
))}
30-
</UnorderedList>
31-
</Flex>
32-
}
33-
>
34-
<Button size="sm" onClick={handleClickBundle} py={6} isDisabled={modelsToInstall.install.length === 0}>
35-
<Flex flexDir="column">
36-
<Text>{isMainModelBase(bundle.name) ? MODEL_TYPE_SHORT_MAP[bundle.name] : bundle.name}</Text>
37-
{modelsToInstall.install.length > 0 && (
38-
<Text fontSize="xs">
39-
({bundle.models.length} {t('settings.models')})
40-
</Text>
41-
)}
42-
{modelsToInstall.install.length === 0 && <Text fontSize="xs">{t('common.installed')}</Text>}
43-
</Flex>
44-
</Button>
45-
</Tooltip>
17+
<Button onClick={handleClickBundle} isDisabled={install.length === 0} {...rest}>
18+
{bundle.name}
19+
</Button>
4620
);
4721
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
2+
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import type { S } from 'services/api/types';
6+
7+
export const StarterBundleTooltipContent = memo(({ bundle }: { bundle: S['StarterModelBundle'] }) => {
8+
const { t } = useTranslation();
9+
const { total, install, skip } = useStarterBundleInstallStatus(bundle);
10+
11+
return (
12+
<Flex flexDir="column" p={1} gap={2}>
13+
<Text>{t('modelManager.includesNModels', { n: total })}</Text>
14+
{install.length === 0 && (
15+
<Flex flexDir="column">
16+
<Text fontWeight="semibold">{t('modelManager.allNModelsInstalled', { count: total })}.</Text>
17+
<UnorderedList>
18+
{skip.map((model, index) => (
19+
<ListItem key={index} wordBreak="break-all">
20+
{model.config.name}
21+
</ListItem>
22+
))}
23+
</UnorderedList>
24+
</Flex>
25+
)}
26+
{install.length > 0 && (
27+
<>
28+
<Flex flexDir="column">
29+
<Text fontWeight="semibold">{t('modelManager.nToInstall', { count: install.length })}:</Text>
30+
<UnorderedList>
31+
{install.map((model, index) => (
32+
<ListItem key={index} wordBreak="break-all">
33+
{model.config.name}
34+
</ListItem>
35+
))}
36+
</UnorderedList>
37+
</Flex>
38+
<Flex flexDir="column">
39+
<Text fontWeight="semibold">{t('modelManager.nAlreadyInstalled', { count: skip.length })}:</Text>
40+
<UnorderedList>
41+
{skip.map((model, index) => (
42+
<ListItem key={index} wordBreak="break-all">
43+
{model.config.name}
44+
</ListItem>
45+
))}
46+
</UnorderedList>
47+
</Flex>
48+
</>
49+
)}
50+
</Flex>
51+
);
52+
});
53+
StarterBundleTooltipContent.displayName = 'StarterBundleTooltipContent';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Flex, Text } from '@invoke-ai/ui-library';
2+
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import type { S } from 'services/api/types';
6+
7+
export const StarterBundleTooltipContentCompact = memo(({ bundle }: { bundle: S['StarterModelBundle'] }) => {
8+
const { t } = useTranslation();
9+
const { total, install, skip } = useStarterBundleInstallStatus(bundle);
10+
11+
return (
12+
<Flex flexDir="column" gap={1} p={1}>
13+
<Text>{t('modelManager.includesNModels', { n: total })}</Text>
14+
{install.length === 0 && (
15+
<Text fontWeight="semibold">{t('modelManager.allNModelsInstalled', { count: total })}.</Text>
16+
)}
17+
{install.length > 0 && (
18+
<Text fontWeight="semibold">{t('modelManager.installedModelsCount', { installed: skip.length, total })}</Text>
19+
)}
20+
</Flex>
21+
);
22+
});
23+
StarterBundleTooltipContentCompact.displayName = 'StarterBundleTooltipContentCompact';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { useTranslation } from 'react-i18next';
77
import { PiInfoBold, PiXBold } from 'react-icons/pi';
88
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
99

10-
import { StarterBundle } from './StarterBundle';
10+
import { StarterBundleButton } from './StarterBundle';
11+
import { StarterBundleTooltipContent } from './StarterBundleTooltipContent';
1112
import { StarterModelsResultItem } from './StarterModelsResultItem';
1213

1314
type StarterModelsResultsProps = {
@@ -62,7 +63,12 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
6263
</Flex>
6364
<Flex gap={2}>
6465
{map(results.starter_bundles, (bundle) => (
65-
<StarterBundle key={bundle.name} bundle={bundle} />
66+
<StarterBundleButton
67+
key={bundle.name}
68+
bundle={bundle}
69+
tooltip={<StarterBundleTooltipContent bundle={bundle} />}
70+
size="sm"
71+
/>
6672
))}
6773
</Flex>
6874
</Flex>

0 commit comments

Comments
 (0)