Skip to content

Commit 88150c7

Browse files
committed
feat(model manager): 💄 update model manager ui, initial commit
1 parent ff8948b commit 88150c7

18 files changed

+318
-121
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const LaunchpadForm = memo(() => {
3535
return (
3636
<Flex flexDir="column" height="100%" gap={3}>
3737
<ScrollableContent>
38-
<Flex flexDir="column" gap={6} p={3}>
38+
<Flex flexDir="column" gap={6} py={2}>
3939
{/* Welcome Section */}
4040
<Flex flexDir="column" gap={2} alignItems="flex-start">
4141
<Heading size="md">{t('modelManager.launchpad.welcome')}</Heading>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Badge, Button, Flex } from '@invoke-ai/ui-library';
2+
import { memo } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import { PiCheckBold, PiPlusBold } from 'react-icons/pi';
5+
6+
type Props = {
7+
handleInstall: () => void;
8+
isInstalled: boolean;
9+
};
10+
11+
export const ModelResultItemActions = memo(({ handleInstall, isInstalled }: Props) => {
12+
const { t } = useTranslation();
13+
14+
return (
15+
<Flex gap={2} shrink={0} pt={1}>
16+
{isInstalled ? (
17+
// TODO: Add a link button to navigate to model
18+
<Badge
19+
variant="subtle"
20+
colorScheme="green"
21+
display="flex"
22+
gap={1}
23+
alignItems="center"
24+
borderRadius="base"
25+
h="24px"
26+
>
27+
<PiCheckBold size="14px" />
28+
</Badge>
29+
) : (
30+
<Button
31+
onClick={handleInstall}
32+
rightIcon={<PiPlusBold size="14px" />}
33+
textTransform="uppercase"
34+
letterSpacing="wider"
35+
fontSize="9px"
36+
size="sm"
37+
>
38+
{t('modelManager.install')}
39+
</Button>
40+
)}
41+
</Flex>
42+
);
43+
});
44+
45+
ModelResultItemActions.displayName = 'ModelResultItemActions';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResultItem.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,56 @@
1-
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
2-
import { memo, useCallback } from 'react';
3-
import { useTranslation } from 'react-i18next';
4-
import { PiPlusBold } from 'react-icons/pi';
1+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
2+
import { Flex, Text } from '@invoke-ai/ui-library';
3+
import { ModelResultItemActions } from 'features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions';
4+
import { memo, useCallback, useMemo } from 'react';
55
import type { ScanFolderResponse } from 'services/api/endpoints/models';
66

77
type Props = {
88
result: ScanFolderResponse[number];
99
installModel: (source: string) => void;
1010
};
11-
export const ScanModelResultItem = memo(({ result, installModel }: Props) => {
12-
const { t } = useTranslation();
1311

12+
const scanFolderResultItemSx: SystemStyleObject = {
13+
alignItems: 'center',
14+
justifyContent: 'space-between',
15+
w: '100%',
16+
py: 2,
17+
px: 1,
18+
gap: 3,
19+
borderBottomWidth: '1px',
20+
borderColor: 'base.700',
21+
};
22+
23+
export const ScanModelResultItem = memo(({ result, installModel }: Props) => {
1424
const handleInstall = useCallback(() => {
1525
installModel(result.path);
1626
}, [installModel, result]);
1727

28+
const modelDisplayName = useMemo(() => {
29+
const normalizedPath = result.path.replace(/\\/g, '/').replace(/\/+$/, '');
30+
31+
// Extract filename/folder name from path
32+
const lastSlashIndex = normalizedPath.lastIndexOf('/');
33+
return lastSlashIndex === -1 ? normalizedPath : normalizedPath.slice(lastSlashIndex + 1);
34+
}, [result.path]);
35+
36+
const modelPathParts = result.path.split(/[/\\]/);
37+
1838
return (
19-
<Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}>
39+
<Flex sx={scanFolderResultItemSx}>
2040
<Flex fontSize="sm" flexDir="column">
21-
<Text fontWeight="semibold">{result.path.split('\\').slice(-1)[0]}</Text>
22-
<Text variant="subtext">{result.path}</Text>
41+
{/* Model Title */}
42+
<Text fontWeight="semibold">{modelDisplayName}</Text>
43+
{/* Model Path */}
44+
<Flex flexWrap="wrap" color="base.200">
45+
{modelPathParts.map((part, index) => (
46+
<Text key={index} variant="subtext">
47+
{part}
48+
{index < modelPathParts.length - 1 && '/'}
49+
</Text>
50+
))}
51+
</Flex>
2352
</Flex>
24-
<Box>
25-
{result.is_installed ? (
26-
<Badge>{t('common.installed')}</Badge>
27-
) : (
28-
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={handleInstall} size="sm" />
29-
)}
30-
</Box>
53+
<ModelResultItemActions handleInstall={handleInstall} isInstalled={result.is_installed} />
3154
</Flex>
3255
);
3356
});

invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ export const ScanModelsResults = memo(({ results }: ScanModelResultsProps) => {
113113
</InputGroup>
114114
</Flex>
115115
</Flex>
116-
<Flex height="100%" layerStyle="third" borderRadius="base" p={3}>
116+
<Flex height="100%" layerStyle="second" borderRadius="base" px={2}>
117117
<ScrollableContent>
118-
<Flex flexDir="column" gap={3}>
118+
<Flex flexDir="column">
119119
{filteredResults.map((result) => (
120120
<ScanModelResultItem key={result.path} result={result} installModel={handleInstallOne} />
121121
))}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/use
1313
import { t } from 'i18next';
1414
import type { MouseEvent } from 'react';
1515
import { useCallback } from 'react';
16+
import { PiDownloadSimpleBold } from 'react-icons/pi';
1617
import type { S } from 'services/api/types';
1718

1819
export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterModelBundle'] } & ButtonProps) => {
@@ -33,8 +34,16 @@ export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterMod
3334

3435
return (
3536
<>
36-
<Button onClick={onClickBundle} isDisabled={install.length === 0} {...rest}>
37-
{bundle.name}
37+
<Button
38+
display="flex"
39+
justifyContent="space-between"
40+
gap={2}
41+
onClick={onClickBundle}
42+
isDisabled={install.length === 0}
43+
{...rest}
44+
>
45+
<span>{bundle.name}</span>
46+
<PiDownloadSimpleBold size="16px" />
3847
</Button>
3948
<ConfirmationAlertDialog
4049
isOpen={isOpen}

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
1-
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
1+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
2+
import { Badge, Flex, Text } from '@invoke-ai/ui-library';
23
import { negate } from 'es-toolkit/compat';
34
import { flattenStarterModel, useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall';
45
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
6+
import { ModelResultItemActions } from 'features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions';
57
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
68
import { toast } from 'features/toast/toast';
79
import { memo, useCallback, useMemo } from 'react';
810
import { useTranslation } from 'react-i18next';
9-
import { PiPlusBold } from 'react-icons/pi';
1011
import type { StarterModel } from 'services/api/types';
1112

13+
const starterModelResultItemSx: SystemStyleObject = {
14+
alignItems: 'start',
15+
justifyContent: 'space-between',
16+
w: '100%',
17+
py: 2,
18+
px: 1,
19+
gap: 2,
20+
borderBottomWidth: '1px',
21+
borderColor: 'base.700',
22+
};
23+
1224
type Props = {
1325
starterModel: StarterModel;
1426
};
27+
1528
export const StarterModelsResultItem = memo(({ starterModel }: Props) => {
1629
const { t } = useTranslation();
1730
const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg();
@@ -40,22 +53,16 @@ export const StarterModelsResultItem = memo(({ starterModel }: Props) => {
4053
}, [modelsToInstall, installModel, t]);
4154

4255
return (
43-
<Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}>
56+
<Flex sx={starterModelResultItemSx}>
4457
<Flex fontSize="sm" flexDir="column">
45-
<Flex gap={3}>
58+
<Text fontWeight="semibold">{starterModel.name}</Text>
59+
<Text variant="subtext">{starterModel.description}</Text>
60+
<Flex gap={1} py={1} alignItems="center">
4661
<Badge h="min-content">{starterModel.type.replaceAll('_', ' ')}</Badge>
4762
<ModelBaseBadge base={starterModel.base} />
48-
<Text fontWeight="semibold">{starterModel.name}</Text>
4963
</Flex>
50-
<Text variant="subtext">{starterModel.description}</Text>
5164
</Flex>
52-
<Box>
53-
{isInstalled ? (
54-
<Badge>{t('common.installed')}</Badge>
55-
) : (
56-
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={onClick} size="sm" />
57-
)}
58-
</Box>
65+
<ModelResultItemActions handleInstall={onClick} isInstalled={isInstalled} />
5966
</Flex>
6067
);
6168
});

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
4848

4949
return (
5050
<Flex flexDir="column" gap={3} height="100%">
51-
<Flex justifyContent="space-between" alignItems="center">
51+
<Flex gap={3} direction="column">
5252
{size(results.starter_bundles) > 0 && (
53-
<Flex gap={4} alignItems="center">
53+
<Flex gap={4} alignItems="center" justifyContent="space-between" p={4} borderWidth="1px" rounded="base">
5454
<Flex gap={2} alignItems="center">
5555
<Text color="base.200" fontWeight="semibold">
5656
{t('modelManager.starterBundles')}
@@ -73,7 +73,8 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
7373
</Flex>
7474
</Flex>
7575
)}
76-
<InputGroup w={64} size="xs">
76+
77+
<InputGroup w="100%" size="xs">
7778
<Input
7879
placeholder={t('modelManager.search')}
7980
value={searchTerm}
@@ -96,9 +97,10 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps
9697
)}
9798
</InputGroup>
9899
</Flex>
99-
<Flex height="100%" layerStyle="third" borderRadius="base" p={3}>
100+
101+
<Flex height="100%" layerStyle="second" borderRadius="base" px={2}>
100102
<ScrollableContent>
101-
<Flex flexDir="column" gap={3}>
103+
<Flex flexDir="column">
102104
{filteredResults.map((result) => (
103105
<StarterModelsResultItem key={result.source} starterModel={result} />
104106
))}

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
12
import { Box, Button, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library';
23
import { useStore } from '@nanostores/react';
34
import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore';
45
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
56
import { memo, useCallback } from 'react';
67
import { useTranslation } from 'react-i18next';
7-
import { PiInfoBold } from 'react-icons/pi';
8+
import { PiCubeBold, PiFolderOpenBold, PiInfoBold, PiLinkSimpleBold, PiShootingStarBold } from 'react-icons/pi';
9+
import { SiHuggingface } from 'react-icons/si';
810

911
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
1012
import { InstallModelForm } from './AddModelPanel/InstallModelForm';
1113
import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm';
1214
import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue';
1315
import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
1416

17+
const installModelsTabSx: SystemStyleObject = {
18+
display: 'flex',
19+
gap: 2,
20+
px: 2,
21+
};
22+
1523
export const InstallModels = memo(() => {
1624
const { t } = useTranslation();
1725
const tabIndex = useStore($installModelsTabIndex);
@@ -29,21 +37,36 @@ export const InstallModels = memo(() => {
2937
</Button>
3038
</Flex>
3139
<Tabs
32-
variant="collapse"
33-
height="50%"
40+
variant="line"
41+
height="100%"
3442
display="flex"
3543
flexDir="column"
3644
index={tabIndex}
3745
onChange={$installModelsTabIndex.set}
3846
>
3947
<TabList>
40-
<Tab>{t('modelManager.launchpadTab')}</Tab>
41-
<Tab>{t('modelManager.urlOrLocalPath')}</Tab>
42-
<Tab>{t('modelManager.huggingFace')}</Tab>
43-
<Tab>{t('modelManager.scanFolder')}</Tab>
44-
<Tab>{t('modelManager.starterModels')}</Tab>
48+
<Tab sx={installModelsTabSx}>
49+
<PiCubeBold />
50+
{t('modelManager.launchpadTab')}
51+
</Tab>
52+
<Tab sx={installModelsTabSx}>
53+
<PiLinkSimpleBold />
54+
{t('modelManager.urlOrLocalPath')}
55+
</Tab>
56+
<Tab sx={installModelsTabSx}>
57+
<SiHuggingface />
58+
{t('modelManager.huggingFace')}
59+
</Tab>
60+
<Tab sx={installModelsTabSx}>
61+
<PiFolderOpenBold />
62+
{t('modelManager.scanFolder')}
63+
</Tab>
64+
<Tab sx={installModelsTabSx}>
65+
<PiShootingStarBold />
66+
{t('modelManager.starterModels')}
67+
</Tab>
4568
</TabList>
46-
<TabPanels p={3} height="100%">
69+
<TabPanels height="100%">
4770
<TabPanel height="100%">
4871
<LaunchpadForm />
4972
</TabPanel>

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
12
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
23
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
34
import { selectSelectedModelKey, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
@@ -8,6 +9,16 @@ import { PiPlusBold } from 'react-icons/pi';
89
import ModelList from './ModelManagerPanel/ModelList';
910
import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation';
1011

12+
const modelManagerSx: SystemStyleObject = {
13+
flexDir: 'column',
14+
p: 4,
15+
gap: 4,
16+
borderRadius: 'base',
17+
w: '50%',
18+
minWidth: '360px',
19+
h: 'full',
20+
};
21+
1122
export const ModelManager = memo(() => {
1223
const { t } = useTranslation();
1324
const dispatch = useAppDispatch();
@@ -17,7 +28,7 @@ export const ModelManager = memo(() => {
1728
const selectedModelKey = useAppSelector(selectSelectedModelKey);
1829

1930
return (
20-
<Flex flexDir="column" layerStyle="first" p={4} gap={4} borderRadius="base" w="50%" h="full">
31+
<Flex sx={modelManagerSx}>
2132
<Flex w="full" gap={4} justifyContent="space-between" alignItems="center">
2233
<Heading fontSize="xl" py={1}>
2334
{t('common.modelManager')}
@@ -28,7 +39,7 @@ export const ModelManager = memo(() => {
2839
</Button>
2940
)}
3041
</Flex>
31-
<Flex flexDir="column" layerStyle="second" p={4} gap={4} borderRadius="base" w="full" h="full">
42+
<Flex flexDir="column" gap={4} w="full" h="full">
3243
<ModelListNavigation />
3344
<ModelList />
3445
</Flex>

0 commit comments

Comments
 (0)