mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): improved model list styling
This commit is contained in:
parent
b0add805c5
commit
2f0a653a7f
@ -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);
|
@ -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);
|
@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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} />
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user