feat(ui): improved starter model buttons & tooltips

This commit is contained in:
psychedelicious
2025-06-27 13:46:03 +10:00
parent 920aea08cc
commit 9862ba9210
7 changed files with 144 additions and 95 deletions

View File

@@ -810,7 +810,11 @@
"urlUnauthorizedErrorMessage": "You may need to configure an API token to access this model.",
"urlUnauthorizedErrorMessage2": "Learn how here.",
"imageEncoderModelId": "Image Encoder Model ID",
"includesNModels": "Includes {{n}} models and their dependencies",
"installedModelsCount": "{{installed}} of {{total}} models installed.",
"includesNModels": "Includes {{n}} models and their dependencies.",
"allNModelsInstalled": "All {{count}} models installed",
"nToInstall": "{{count}} to install",
"nAlreadyInstalled": "{{count}} already installed",
"installQueue": "Install Queue",
"inplaceInstall": "In-place install",
"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.",

View File

@@ -0,0 +1,13 @@
import { useMemo } from 'react';
import type { S } from 'services/api/types';
import { useStarterBundleInstall } from './useStarterBundleInstall';
export const useStarterBundleInstallStatus = (bundle: S['StarterModelBundle']) => {
const { getModelsToInstall } = useStarterBundleInstall();
const total = useMemo(() => bundle.models.length, [bundle.models.length]);
const install = useMemo(() => getModelsToInstall(bundle).install, [bundle, getModelsToInstall]);
const skip = useMemo(() => getModelsToInstall(bundle).skip, [bundle, getModelsToInstall]);
return { total, skip, install };
};

View File

@@ -1,7 +1,9 @@
import { Button, Flex, Grid, Heading, Icon, Text } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useStarterBundleInstall } from 'features/modelManagerV2/hooks/useStarterBundleInstall';
import { map } from 'es-toolkit/compat';
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
import { StarterBundleButton } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundle';
import { StarterBundleTooltipContentCompact } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleTooltipContentCompact';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi';
@@ -10,26 +12,8 @@ import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
export const LaunchpadForm = memo(() => {
const { t } = useTranslation();
const { installBundle } = useStarterBundleInstall();
const { data: starterModelsData } = useGetStarterModelsQuery();
// Function to install models from a bundle
const handleBundleInstall = useCallback(
(bundleName: string) => {
if (!starterModelsData?.starter_bundles) {
return;
}
const bundle = starterModelsData.starter_bundles[bundleName];
if (!bundle) {
return;
}
installBundle(bundle);
},
[starterModelsData, installBundle]
);
const navigateToUrlTab = useCallback(() => {
setInstallModelsTabByName('urlOrLocal');
}, []);
@@ -46,18 +30,6 @@ export const LaunchpadForm = memo(() => {
setInstallModelsTabByName('starterModels');
}, []);
const handleSD15BundleClick = useCallback(() => {
handleBundleInstall('sd-1');
}, [handleBundleInstall]);
const handleSDXLBundleClick = useCallback(() => {
handleBundleInstall('sdxl');
}, [handleBundleInstall]);
const handleFluxBundleClick = useCallback(() => {
handleBundleInstall('flux');
}, [handleBundleInstall]);
return (
<Flex flexDir="column" height="100%" gap={3}>
<ScrollableContent>
@@ -145,32 +117,36 @@ export const LaunchpadForm = memo(() => {
</Grid>
</Flex>
{/* Recommended Section */}
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Heading size="sm">{t('modelManager.launchpad.recommendedModels')}</Heading>
{/* Starter Model Bundles - More Prominent */}
<Text color="base.300">{t('modelManager.launchpad.bundleDescription')}</Text>
<Grid templateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap={2} w="full">
<Button onClick={handleSD15BundleClick} variant="outline" p={6}>
{t('modelManager.launchpad.stableDiffusion15')}
{starterModelsData && (
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Heading size="sm">{t('modelManager.launchpad.recommendedModels')}</Heading>
{/* Starter Model Bundles - More Prominent */}
<Text color="base.300">{t('modelManager.launchpad.bundleDescription')}</Text>
<Grid templateColumns="repeat(auto-fit, minmax(180px, 1fr))" gap={2} w="full">
{map(starterModelsData.starter_bundles, (bundle) => (
<StarterBundleButton
size="md"
tooltip={<StarterBundleTooltipContentCompact bundle={bundle} />}
key={bundle.name}
bundle={bundle}
variant="outline"
p={4}
h="unset"
/>
))}
</Grid>
{/* Browse All - Simple Link */}
<Button
onClick={navigateToStarterModelsTab}
variant="link"
size="sm"
leftIcon={<PiStarBold />}
colorScheme="invokeBlue"
>
{t('modelManager.launchpad.exploreStarter')}
</Button>
<Button onClick={handleSDXLBundleClick} variant="outline" p={6}>
{t('modelManager.launchpad.sdxl')}
</Button>
<Button onClick={handleFluxBundleClick} variant="outline" p={6}>
{t('modelManager.launchpad.fluxDev')}
</Button>
</Grid>
{/* Browse All - Simple Link */}
<Button
onClick={navigateToStarterModelsTab}
variant="link"
size="sm"
leftIcon={<PiStarBold />}
colorScheme="invokeBlue"
>
{t('modelManager.launchpad.exploreStarter')}
</Button>
</Flex>
</Flex>
)}
</Flex>
</ScrollableContent>
</Flex>

View File

@@ -1,47 +1,21 @@
import { Button, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
import type { ButtonProps } from '@invoke-ai/ui-library';
import { Button } from '@invoke-ai/ui-library';
import { useStarterBundleInstall } from 'features/modelManagerV2/hooks/useStarterBundleInstall';
import { isMainModelBase } from 'features/nodes/types/common';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
import { useCallback } from 'react';
import type { S } from 'services/api/types';
export const StarterBundle = ({ bundle }: { bundle: S['StarterModelBundle'] }) => {
const { installBundle, getModelsToInstall } = useStarterBundleInstall();
const { t } = useTranslation();
const modelsToInstall = useMemo(() => getModelsToInstall(bundle), [bundle, getModelsToInstall]);
export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterModelBundle'] } & ButtonProps) => {
const { installBundle } = useStarterBundleInstall();
const { install } = useStarterBundleInstallStatus(bundle);
const handleClickBundle = useCallback(() => {
installBundle(bundle);
}, [installBundle, bundle]);
return (
<Tooltip
label={
<Flex flexDir="column" p={1}>
<Text>{t('modelManager.includesNModels', { n: bundle.models.length })}:</Text>
<UnorderedList>
{bundle.models.map((model, index) => (
<ListItem key={index} wordBreak="break-all">
{model.name}
</ListItem>
))}
</UnorderedList>
</Flex>
}
>
<Button size="sm" onClick={handleClickBundle} py={6} isDisabled={modelsToInstall.install.length === 0}>
<Flex flexDir="column">
<Text>{isMainModelBase(bundle.name) ? MODEL_TYPE_SHORT_MAP[bundle.name] : bundle.name}</Text>
{modelsToInstall.install.length > 0 && (
<Text fontSize="xs">
({bundle.models.length} {t('settings.models')})
</Text>
)}
{modelsToInstall.install.length === 0 && <Text fontSize="xs">{t('common.installed')}</Text>}
</Flex>
</Button>
</Tooltip>
<Button onClick={handleClickBundle} isDisabled={install.length === 0} {...rest}>
{bundle.name}
</Button>
);
};

View File

@@ -0,0 +1,53 @@
import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import type { S } from 'services/api/types';
export const StarterBundleTooltipContent = memo(({ bundle }: { bundle: S['StarterModelBundle'] }) => {
const { t } = useTranslation();
const { total, install, skip } = useStarterBundleInstallStatus(bundle);
return (
<Flex flexDir="column" p={1} gap={2}>
<Text>{t('modelManager.includesNModels', { n: total })}</Text>
{install.length === 0 && (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('modelManager.allNModelsInstalled', { count: total })}.</Text>
<UnorderedList>
{skip.map((model, index) => (
<ListItem key={index} wordBreak="break-all">
{model.config.name}
</ListItem>
))}
</UnorderedList>
</Flex>
)}
{install.length > 0 && (
<>
<Flex flexDir="column">
<Text fontWeight="semibold">{t('modelManager.nToInstall', { count: install.length })}:</Text>
<UnorderedList>
{install.map((model, index) => (
<ListItem key={index} wordBreak="break-all">
{model.config.name}
</ListItem>
))}
</UnorderedList>
</Flex>
<Flex flexDir="column">
<Text fontWeight="semibold">{t('modelManager.nAlreadyInstalled', { count: skip.length })}:</Text>
<UnorderedList>
{skip.map((model, index) => (
<ListItem key={index} wordBreak="break-all">
{model.config.name}
</ListItem>
))}
</UnorderedList>
</Flex>
</>
)}
</Flex>
);
});
StarterBundleTooltipContent.displayName = 'StarterBundleTooltipContent';

View File

@@ -0,0 +1,23 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/useStarterBundleInstallStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import type { S } from 'services/api/types';
export const StarterBundleTooltipContentCompact = memo(({ bundle }: { bundle: S['StarterModelBundle'] }) => {
const { t } = useTranslation();
const { total, install, skip } = useStarterBundleInstallStatus(bundle);
return (
<Flex flexDir="column" gap={1} p={1}>
<Text>{t('modelManager.includesNModels', { n: total })}</Text>
{install.length === 0 && (
<Text fontWeight="semibold">{t('modelManager.allNModelsInstalled', { count: total })}.</Text>
)}
{install.length > 0 && (
<Text fontWeight="semibold">{t('modelManager.installedModelsCount', { installed: skip.length, total })}</Text>
)}
</Flex>
);
});
StarterBundleTooltipContentCompact.displayName = 'StarterBundleTooltipContentCompact';

View File

@@ -7,7 +7,8 @@ import { useTranslation } from 'react-i18next';
import { PiInfoBold, PiXBold } from 'react-icons/pi';
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
import { StarterBundle } from './StarterBundle';
import { StarterBundleButton } from './StarterBundle';
import { StarterBundleTooltipContent } from './StarterBundleTooltipContent';
import { StarterModelsResultItem } from './StarterModelsResultItem';
type StarterModelsResultsProps = {
@@ -62,7 +63,12 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
</Flex>
<Flex gap={2}>
{map(results.starter_bundles, (bundle) => (
<StarterBundle key={bundle.name} bundle={bundle} />
<StarterBundleButton
key={bundle.name}
bundle={bundle}
tooltip={<StarterBundleTooltipContent bundle={bundle} />}
size="sm"
/>
))}
</Flex>
</Flex>