feat(ui): improved model list styling

This commit is contained in:
psychedelicious 2024-03-07 15:14:38 +11:00
parent b0add805c5
commit 2f0a653a7f
6 changed files with 131 additions and 58 deletions

View File

@ -0,0 +1,18 @@
import { Badge } from '@invoke-ai/ui-library';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
import { memo } from 'react';
import type { BaseModelType } from 'services/api/types';
type Props = {
base: BaseModelType;
};
const ModelBaseBadge = ({ base }: Props) => {
return (
<Badge flexGrow={0} colorScheme="invokeBlue" variant="subtle">
{MODEL_TYPE_SHORT_MAP[base]}
</Badge>
);
};
export default memo(ModelBaseBadge);

View File

@ -0,0 +1,26 @@
import { Badge } from '@invoke-ai/ui-library';
import { memo } from 'react';
import type { AnyModelConfig } from 'services/api/types';
type Props = {
format: AnyModelConfig['format'];
};
const MODEL_FORMAT_NAME_MAP = {
diffusers: 'diffusers',
lycoris: 'lycoris',
checkpoint: 'checkpoint',
invokeai: 'internal',
embedding_file: 'embedding',
embedding_folder: 'embedding',
};
const ModelFormatBadge = ({ format }: Props) => {
return (
<Badge flexGrow={0} colorScheme="base" variant="subtle">
{MODEL_FORMAT_NAME_MAP[format]}
</Badge>
);
};
export default memo(ModelFormatBadge);

View File

@ -1,13 +1,28 @@
import { Box, Image } from '@invoke-ai/ui-library'; import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { typedMemo } from 'common/util/typedMemo'; import { typedMemo } from 'common/util/typedMemo';
import { PiImage } from 'react-icons/pi';
type Props = { type Props = {
image_url?: string; image_url?: string | null;
}; };
export const MODEL_IMAGE_THUMBNAIL_SIZE = '40px';
const FALLBACK_ICON_SIZE = '24px';
const ModelImage = ({ image_url }: Props) => { const ModelImage = ({ image_url }: Props) => {
if (!image_url) { if (!image_url) {
return <Box height="50px" minWidth="50px" />; return (
<Flex
height={MODEL_IMAGE_THUMBNAIL_SIZE}
minWidth={MODEL_IMAGE_THUMBNAIL_SIZE}
bg="base.650"
borderRadius="base"
alignItems="center"
justifyContent="center"
>
<Icon color="base.500" as={PiImage} boxSize={FALLBACK_ICON_SIZE} />
</Flex>
);
} }
return ( return (
@ -15,10 +30,10 @@ const ModelImage = ({ image_url }: Props) => {
src={image_url} src={image_url}
objectFit="cover" objectFit="cover"
objectPosition="50% 50%" objectPosition="50% 50%"
height="50px" height={MODEL_IMAGE_THUMBNAIL_SIZE}
width="50px" width={MODEL_IMAGE_THUMBNAIL_SIZE}
minHeight="50px" minHeight={MODEL_IMAGE_THUMBNAIL_SIZE}
minWidth="50px" minWidth={MODEL_IMAGE_THUMBNAIL_SIZE}
borderRadius="base" borderRadius="base"
/> />
); );

View File

@ -1,33 +1,29 @@
import { import type { SystemStyleObject } from '@invoke-ai/ui-library';
Badge, import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library';
Box,
Button,
ConfirmationAlertDialog,
Flex,
Icon,
IconButton,
Text,
Tooltip,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IoWarning } from 'react-icons/io5';
import { PiTrashSimpleBold } from 'react-icons/pi'; import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteModelsMutation } from 'services/api/endpoints/models'; import { useDeleteModelsMutation } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types'; import type { AnyModelConfig } from 'services/api/types';
import ModelImage from './ModelImage'; import ModelImage, { MODEL_IMAGE_THUMBNAIL_SIZE } from './ModelImage';
type ModelListItemProps = { type ModelListItemProps = {
model: AnyModelConfig; model: AnyModelConfig;
}; };
const sx: SystemStyleObject = {
_hover: { bg: 'base.700' },
"&[aria-selected='true']": { bg: 'base.700' },
};
const ModelListItem = (props: ModelListItemProps) => { const ModelListItem = (props: ModelListItemProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -41,6 +37,14 @@ const ModelListItem = (props: ModelListItemProps) => {
dispatch(setSelectedModelKey(model.key)); dispatch(setSelectedModelKey(model.key));
}, [model.key, dispatch]); }, [model.key, dispatch]);
const onClickDeleteButton = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
},
[onOpen]
);
const isSelected = useMemo(() => { const isSelected = useMemo(() => {
return selectedModelKey === model.key; return selectedModelKey === model.key;
}, [selectedModelKey, model.key]); }, [selectedModelKey, model.key]);
@ -74,41 +78,47 @@ const ModelListItem = (props: ModelListItemProps) => {
}, [deleteModel, model, dispatch, t]); }, [deleteModel, model, dispatch, t]);
return ( return (
<Flex gap={2} alignItems="center" w="full"> <Flex
<ModelImage image_url={model.cover_image || ''} /> sx={sx}
<Flex aria-selected={isSelected}
as={Button} justifyContent="flex-start"
isChecked={isSelected} p={2}
variant={isSelected ? 'solid' : 'ghost'} borderRadius="base"
justifyContent="start" w="full"
p={2} alignItems="center"
borderRadius="base" gap={2}
w="full" cursor="pointer"
alignItems="center" onClick={handleSelectModel}
onClick={handleSelectModel} >
> <Flex gap={2} w="full" h="full">
<Flex gap={4} alignItems="center"> <ModelImage image_url={model.cover_image} />
<Badge minWidth={14} p={0.5} fontSize="sm" variant="solid"> <Flex gap={1} alignItems="flex-start" flexDir="column" w="full">
{MODEL_TYPE_SHORT_MAP[model.base as keyof typeof MODEL_TYPE_SHORT_MAP]} <Flex gap={2} w="full" alignItems="flex-start">
</Badge> <Text fontWeight="semibold">{model.name}</Text>
<Tooltip label={model.description} placement="bottom"> <Spacer />
<Text>{model.name}</Text> </Flex>
</Tooltip> <Text variant="subtext" noOfLines={1}>
{model.format === 'checkpoint' && ( {model.description || 'No Description'}
<Tooltip label="Checkpoint"> </Text>
<Box> </Flex>
<Icon as={IoWarning} /> <Flex
</Box> h={MODEL_IMAGE_THUMBNAIL_SIZE}
</Tooltip> flexDir="column"
)} alignItems="flex-end"
justifyContent="space-between"
gap={2}
>
<ModelBaseBadge base={model.base} />
<ModelFormatBadge format={model.format} />
</Flex> </Flex>
</Flex> </Flex>
<IconButton <IconButton
onClick={onOpen} onClick={onClickDeleteButton}
icon={<PiTrashSimpleBold />} icon={<PiTrashSimpleBold size={16} />}
aria-label={t('modelManager.deleteConfig')} aria-label={t('modelManager.deleteConfig')}
colorScheme="error" colorScheme="error"
h={MODEL_IMAGE_THUMBNAIL_SIZE}
w={MODEL_IMAGE_THUMBNAIL_SIZE}
/> />
<ConfirmationAlertDialog <ConfirmationAlertDialog
isOpen={isOpen} isOpen={isOpen}

View File

@ -11,7 +11,7 @@ type ModelListWrapperProps = {
export const ModelListWrapper = (props: ModelListWrapperProps) => { export const ModelListWrapper = (props: ModelListWrapperProps) => {
const { title, modelList } = props; const { title, modelList } = props;
return ( return (
<StickyScrollable title={title}> <StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}>
{modelList.map((model) => ( {modelList.map((model) => (
<ModelListItem key={model.key} model={model} /> <ModelListItem key={model.key} model={model} />
))} ))}

View File

@ -1,14 +1,16 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Heading } from '@invoke-ai/ui-library'; import { Flex, Heading } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { memo } from 'react'; import { memo } from 'react';
type StickyScrollableHeadingProps = { type StickyScrollableHeadingProps = {
title: string; title: string;
sx?: SystemStyleObject;
}; };
const StickyScrollableHeading = memo((props: StickyScrollableHeadingProps) => { const StickyScrollableHeading = memo((props: StickyScrollableHeadingProps) => {
return ( return (
<Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800"> <Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800" sx={props.sx}>
<Heading size="sm">{props.title}</Heading> <Heading size="sm">{props.title}</Heading>
</Flex> </Flex>
); );
@ -16,11 +18,11 @@ const StickyScrollableHeading = memo((props: StickyScrollableHeadingProps) => {
StickyScrollableHeading.displayName = 'StickyScrollableHeading'; StickyScrollableHeading.displayName = 'StickyScrollableHeading';
type StickyScrollableContentProps = PropsWithChildren; type StickyScrollableContentProps = PropsWithChildren<{ sx?: SystemStyleObject }>;
const StickyScrollableContent = memo((props: StickyScrollableContentProps) => { const StickyScrollableContent = memo((props: StickyScrollableContentProps) => {
return ( return (
<Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4}> <Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4} sx={props.sx}>
{props.children} {props.children}
</Flex> </Flex>
); );
@ -30,13 +32,15 @@ StickyScrollableContent.displayName = 'StickyScrollableContent';
type StickyScrollableProps = PropsWithChildren<{ type StickyScrollableProps = PropsWithChildren<{
title: string; title: string;
headingSx?: SystemStyleObject;
contentSx?: SystemStyleObject;
}>; }>;
export const StickyScrollable = memo((props: StickyScrollableProps) => { export const StickyScrollable = memo((props: StickyScrollableProps) => {
return ( return (
<Flex key={props.title} flexDir="column"> <Flex key={props.title} flexDir="column">
<StickyScrollableHeading title={props.title} /> <StickyScrollableHeading title={props.title} sx={props.headingSx} />
<StickyScrollableContent>{props.children}</StickyScrollableContent> <StickyScrollableContent sx={props.contentSx}>{props.children}</StickyScrollableContent>
</Flex> </Flex>
); );
}); });