feat(ui): iterate on model combobox (wip)

This commit is contained in:
psychedelicious
2025-04-15 12:12:51 +10:00
parent f6eb4d9a6b
commit 4dbf07c3e0
2 changed files with 83 additions and 77 deletions

View File

@@ -1,11 +1,8 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Input, Modal, ModalBody, ModalContent, ModalOverlay, Spacer, Text } from '@invoke-ai/ui-library';
import { Box, Flex, Input, Modal, ModalBody, ModalContent, ModalOverlay } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { ModelComboboxItem } from 'common/components/ModelCombobox/ModelComboboxItem';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
import { filesize } from 'filesize';
import { atom } from 'nanostores';
import type { ChangeEvent, RefObject } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
@@ -17,6 +14,8 @@ export type ModelComboboxOptions = {
modelConfigs: AnyModelConfig[];
onSelect: (modelConfig: AnyModelConfig) => void;
onClose?: () => void;
noModelsInstalledFallback?: React.ReactNode;
noModelsFoundFallback?: React.ReactNode;
};
const $modelComboboxState = atom<ModelComboboxOptions | null>(null);
@@ -274,7 +273,7 @@ const ModelComboboxContent = memo(
bottom={0}
left={0}
icon={null}
label="No matching items"
label="No matching models"
/>
)}
{items.length > 0 &&
@@ -295,74 +294,3 @@ const ModelComboboxContent = memo(
}
);
ModelComboboxContent.displayName = 'ModelComboboxContent';
const itemSx: SystemStyleObject = {
display: 'flex',
flexDir: 'column',
p: 2,
cursor: 'pointer',
borderRadius: 'base',
'&[data-selected="true"]': {
bg: 'base.700',
},
'&[data-disabled="true"]': {
cursor: 'not-allowed',
opacity: 0.5,
},
};
const ModelComboboxItem = memo(
(props: {
model: AnyModelConfig;
setActive: (key: string) => void;
onSelect: (key: string) => void;
isSelected: boolean;
isDisabled: boolean;
}) => {
const { model, setActive, onSelect, isDisabled, isSelected } = props;
const onPointerMove = useCallback(() => {
setActive(model.key);
}, [model.key, setActive]);
const onClick = useCallback(() => {
onSelect(model.key);
}, [model.key, onSelect]);
return (
<Box
role="option"
sx={itemSx}
id={model.key}
aria-disabled={isDisabled}
aria-selected={isSelected}
data-disabled={isDisabled}
data-selected={isSelected}
onPointerMove={isDisabled ? undefined : onPointerMove}
onClick={isDisabled ? undefined : onClick}
>
<ModelComboboxItemContent model={model} />
</Box>
);
}
);
ModelComboboxItem.displayName = 'ModelComboboxItem';
const ModelComboboxItemContent = memo(({ model }: { model: AnyModelConfig }) => {
return (
<Flex tabIndex={-1} gap={2}>
<ModelImage image_url={model.cover_image} />
<Flex flexDir="column" gap={2} flex={1}>
<Flex gap={2} alignItems="center">
<Text fontSize="sm" fontWeight="semibold">
{model.name}
</Text>
<Spacer />
<Text variant="subtext" fontStyle="italic">
{filesize(model.file_size)}
</Text>
<ModelBaseBadge base={model.base} />
</Flex>
{model.description && <Text color="base.200">{model.description}</Text>}
</Flex>
</Flex>
);
});
ModelComboboxItemContent.displayName = 'ModelComboboxItemContent';

View File

@@ -0,0 +1,78 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Spacer, Text } from '@invoke-ai/ui-library';
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
import { filesize } from 'filesize';
import { memo, useCallback } from 'react';
import type { AnyModelConfig } from 'services/api/types';
const itemSx: SystemStyleObject = {
display: 'flex',
flexDir: 'column',
p: 2,
cursor: 'pointer',
borderRadius: 'base',
'&[data-selected="true"]': {
bg: 'base.700',
},
'&[data-disabled="true"]': {
cursor: 'not-allowed',
opacity: 0.5,
},
};
export const ModelComboboxItem = memo(
(props: {
model: AnyModelConfig;
setActive: (key: string) => void;
onSelect: (key: string) => void;
isSelected: boolean;
isDisabled: boolean;
}) => {
const { model, setActive, onSelect, isDisabled, isSelected } = props;
const onPointerMove = useCallback(() => {
setActive(model.key);
}, [model.key, setActive]);
const onClick = useCallback(() => {
onSelect(model.key);
}, [model.key, onSelect]);
return (
<Box
role="option"
sx={itemSx}
id={model.key}
aria-disabled={isDisabled}
aria-selected={isSelected}
data-disabled={isDisabled}
data-selected={isSelected}
onPointerMove={isDisabled ? undefined : onPointerMove}
onClick={isDisabled ? undefined : onClick}
>
<ModelComboboxItemContent model={model} />
</Box>
);
}
);
ModelComboboxItem.displayName = 'ModelComboboxItem';
const ModelComboboxItemContent = memo(({ model }: { model: AnyModelConfig }) => {
return (
<Flex tabIndex={-1} gap={2}>
<ModelImage image_url={model.cover_image} />
<Flex flexDir="column" gap={2} flex={1}>
<Flex gap={2} alignItems="center">
<Text fontSize="sm" fontWeight="semibold">
{model.name}
</Text>
<Spacer />
<Text variant="subtext" fontStyle="italic">
{filesize(model.file_size)}
</Text>
<ModelBaseBadge base={model.base} />
</Flex>
{model.description && <Text color="base.200">{model.description}</Text>}
</Flex>
</Flex>
);
});
ModelComboboxItemContent.displayName = 'ModelComboboxItemContent';