feat(ui): wip mm tab design

This commit is contained in:
psychedelicious 2024-08-11 16:37:56 +10:00
parent 09d1e190e7
commit 375b8bc67d
17 changed files with 137 additions and 89 deletions

View File

@ -40,7 +40,7 @@ export const ModelInstallQueue = memo(() => {
}, [data]); }, [data]);
return ( return (
<Flex flexDir="column" p={3} h="full" gap={3}> <Flex flexDir="column" h="full" gap={2}>
<Flex justifyContent="space-between" alignItems="center"> <Flex justifyContent="space-between" alignItems="center">
<Heading size="sm">{t('modelManager.installQueue')}</Heading> <Heading size="sm">{t('modelManager.installQueue')}</Heading>
<Button <Button
@ -52,7 +52,7 @@ export const ModelInstallQueue = memo(() => {
{t('modelManager.prune')} {t('modelManager.prune')}
</Button> </Button>
</Flex> </Flex>
<Box layerStyle="first" p={3} borderRadius="base" w="full" h="full"> <Box layerStyle="first" borderRadius="base" w="full" h="full">
<ScrollableContent> <ScrollableContent>
<Flex flexDir="column-reverse" gap="2" w="full"> <Flex flexDir="column-reverse" gap="2" w="full">
{data?.map((model) => <ModelInstallQueueItem key={model.id} installJob={model} />)} {data?.map((model) => <ModelInstallQueueItem key={model.id} installJob={model} />)}

View File

@ -1,9 +1,11 @@
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm'; import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm'; import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
import { InstallModelForm } from './AddModelPanel/InstallModelForm'; import { InstallModelForm } from './AddModelPanel/InstallModelForm';
@ -20,9 +22,11 @@ export const InstallModels = memo(() => {
}, []); }, []);
return ( return (
<Flex layerStyle="first" borderRadius="base" w="full" h="full" flexDir="column" gap={4}> <PanelGroup direction="vertical">
<Panel>
<Flex w="full" h="full" flexDir="column" gap={2}>
<Heading fontSize="xl">{t('modelManager.addModel')}</Heading> <Heading fontSize="xl">{t('modelManager.addModel')}</Heading>
<Tabs variant="collapse" height="50%" display="flex" flexDir="column" index={index} onChange={onChange}> <Tabs variant="collapse" height="full" display="flex" flexDir="column" index={index} onChange={onChange}>
<TabList> <TabList>
<Tab>{t('modelManager.urlOrLocalPath')}</Tab> <Tab>{t('modelManager.urlOrLocalPath')}</Tab>
<Tab>{t('modelManager.huggingFace')}</Tab> <Tab>{t('modelManager.huggingFace')}</Tab>
@ -44,10 +48,15 @@ export const InstallModels = memo(() => {
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>
<Box layerStyle="second" borderRadius="base" h="50%">
<ModelInstallQueue />
</Box>
</Flex> </Flex>
</Panel>
<ResizeHandle orientation="horizontal" />
<Panel>
{/* <Box layerStyle="second" borderRadius="base" h="full"> */}
<ModelInstallQueue />
{/* </Box> */}
</Panel>
</PanelGroup>
); );
}); });

View File

@ -8,7 +8,7 @@ import { PiPlusBold } from 'react-icons/pi';
import ModelList from './ModelManagerPanel/ModelList'; import ModelList from './ModelManagerPanel/ModelList';
import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation'; import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation';
export const ModelManager = memo(() => { export const ModelManagerLeftPanel = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleClickAddModel = useCallback(() => { const handleClickAddModel = useCallback(() => {
@ -16,19 +16,17 @@ export const ModelManager = memo(() => {
}, [dispatch]); }, [dispatch]);
return ( return (
<Flex flexDir="column" layerStyle="first" p={4} gap={4} borderRadius="base" w="50%" h="full"> <Flex flexDir="column" gap={2} w="full" h="full">
<Flex w="full" gap={4} justifyContent="space-between" alignItems="center"> <Flex w="full" gap={4} justifyContent="space-between" alignItems="center">
<Heading fontSize="xl">{t('common.modelManager')}</Heading> <Heading fontSize="xl">{t('common.modelManager')}</Heading>
<Button size="sm" colorScheme="invokeYellow" leftIcon={<PiPlusBold />} onClick={handleClickAddModel}> <Button size="sm" colorScheme="invokeYellow" leftIcon={<PiPlusBold />} onClick={handleClickAddModel}>
{t('modelManager.addModels')} {t('modelManager.addModels')}
</Button> </Button>
</Flex> </Flex>
<Flex flexDir="column" layerStyle="second" p={4} gap={4} borderRadius="base" w="full" h="full">
<ModelListNavigation /> <ModelListNavigation />
<ModelList /> <ModelList />
</Flex> </Flex>
</Flex>
); );
}); });
ModelManager.displayName = 'ModelManager'; ModelManagerLeftPanel.displayName = 'ModelManagerLeftPanel';

View File

@ -16,6 +16,10 @@ const BASE_COLOR_MAP: Record<BaseModelType, string> = {
}; };
const ModelBaseBadge = ({ base }: Props) => { const ModelBaseBadge = ({ base }: Props) => {
if (base === 'any') {
return null;
}
return ( return (
<Badge flexGrow={0} colorScheme={BASE_COLOR_MAP[base]} variant="subtle" h="min-content"> <Badge flexGrow={0} colorScheme={BASE_COLOR_MAP[base]} variant="subtle" h="min-content">
{MODEL_TYPE_SHORT_MAP[base]} {MODEL_TYPE_SHORT_MAP[base]}

View File

@ -15,12 +15,12 @@ const ModelImage = ({ image_url }: Props) => {
<Flex <Flex
height={MODEL_IMAGE_THUMBNAIL_SIZE} height={MODEL_IMAGE_THUMBNAIL_SIZE}
minWidth={MODEL_IMAGE_THUMBNAIL_SIZE} minWidth={MODEL_IMAGE_THUMBNAIL_SIZE}
bg="base.650"
borderRadius="base" borderRadius="base"
bg="baseAlpha.200"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
> >
<Icon color="base.500" as={PiImage} boxSize={FALLBACK_ICON_SIZE} /> <Icon color="baseAlpha.700" as={PiImage} boxSize={FALLBACK_ICON_SIZE} />
</Flex> </Flex>
); );
} }

View File

@ -106,7 +106,7 @@ const ModelList = () => {
return ( return (
<ScrollableContent> <ScrollableContent>
<Flex flexDirection="column" w="full" h="full" gap={4}> <Flex flexDirection="column" w="full" h="full" gap={2}>
{/* Main Model List */} {/* Main Model List */}
{isLoadingMainModels && <FetchingModelsLoader loadingMessage="Loading Main Models..." />} {isLoadingMainModels && <FetchingModelsLoader loadingMessage="Loading Main Models..." />}
{!isLoadingMainModels && filteredMainModels.length > 0 && ( {!isLoadingMainModels && filteredMainModels.length > 0 && (

View File

@ -20,8 +20,8 @@ type ModelListItemProps = {
}; };
const sx: SystemStyleObject = { const sx: SystemStyleObject = {
_hover: { bg: 'base.700' }, _hover: { bg: 'baseAlpha.100' },
"&[aria-selected='true']": { bg: 'base.700' }, "&[aria-selected='true']": { bg: 'baseAlpha.100' },
}; };
const ModelListItem = ({ model }: ModelListItemProps) => { const ModelListItem = ({ model }: ModelListItemProps) => {
@ -82,8 +82,8 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
w="full" w="full"
alignItems="center" alignItems="center"
gap={2} gap={2}
cursor="pointer"
onClick={handleSelectModel} onClick={handleSelectModel}
role="button"
> >
<Flex gap={2} w="full" h="full" minW={0}> <Flex gap={2} w="full" h="full" minW={0}>
<ModelImage image_url={model.cover_image} /> <ModelImage image_url={model.cover_image} />

View File

@ -30,12 +30,12 @@ export const ModelListNavigation = memo(() => {
<InputGroup maxW="400px"> <InputGroup maxW="400px">
<Input <Input
placeholder={t('modelManager.search')} placeholder={t('modelManager.search')}
value={searchTerm || ''} value={searchTerm}
data-testid="board-search-input" data-testid="board-search-input"
onChange={handleSearch} onChange={handleSearch}
/> />
{!!searchTerm?.length && ( {Boolean(searchTerm) && (
<InputRightElement h="full" pe={2}> <InputRightElement h="full" pe={2}>
<IconButton <IconButton
size="sm" size="sm"

View File

@ -1,4 +1,4 @@
import { StickyScrollable } from 'features/system/components/StickyScrollable'; import { Box, Flex, Heading } from '@invoke-ai/ui-library';
import { memo } from 'react'; import { memo } from 'react';
import type { AnyModelConfig } from 'services/api/types'; import type { AnyModelConfig } from 'services/api/types';
@ -12,11 +12,16 @@ type ModelListWrapperProps = {
export const ModelListWrapper = memo((props: ModelListWrapperProps) => { export const ModelListWrapper = memo((props: ModelListWrapperProps) => {
const { title, modelList } = props; const { title, modelList } = props;
return ( return (
<StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}> <Box>
<Box pb={2} position="sticky" zIndex={1} top={0} bg="base.900">
<Heading size="sm">{title}</Heading>
</Box>
<Flex flexDir="column" gap={1}>
{modelList.map((model) => ( {modelList.map((model) => (
<ModelListItem key={model.key} model={model} /> <ModelListItem key={model.key} model={model} />
))} ))}
</StickyScrollable> </Flex>
</Box>
); );
}); });

View File

@ -1,4 +1,3 @@
import { Box } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react'; import { memo } from 'react';
@ -7,11 +6,11 @@ import { Model } from './ModelPanel/Model';
export const ModelPane = memo(() => { export const ModelPane = memo(() => {
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
return ( if (selectedModelKey) {
<Box layerStyle="first" p={4} borderRadius="base" w="50%" h="full"> return <Model key={selectedModelKey} />;
{selectedModelKey ? <Model key={selectedModelKey} /> : <InstallModels />} }
</Box>
); return <InstallModels />;
}); });
ModelPane.displayName = 'ModelPane'; ModelPane.displayName = 'ModelPane';

View File

@ -6,14 +6,15 @@ import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'; import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi';
import { useDeleteModelImageMutation, useUpdateModelImageMutation } from 'services/api/endpoints/models'; import { useDeleteModelImageMutation, useUpdateModelImageMutation } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
type Props = { type Props = {
model_key: string | null; modelConfig: AnyModelConfig;
model_image?: string | null;
}; };
const ModelImageUpload = ({ model_key, model_image }: Props) => { const ModelImageUpload = ({ modelConfig }: Props) => {
const [image, setImage] = useState<string | null>(model_image || null); const { key, cover_image } = modelConfig;
const [image, setImage] = useState<string | null>(cover_image || null);
const { t } = useTranslation(); const { t } = useTranslation();
const [updateModelImage] = useUpdateModelImageMutation(); const [updateModelImage] = useUpdateModelImageMutation();
@ -23,11 +24,11 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
(files: File[]) => { (files: File[]) => {
const file = files[0]; const file = files[0];
if (!file || !model_key) { if (!file) {
return; return;
} }
updateModelImage({ key: model_key, image: file }) updateModelImage({ key, image: file })
.unwrap() .unwrap()
.then(() => { .then(() => {
setImage(URL.createObjectURL(file)); setImage(URL.createObjectURL(file));
@ -45,15 +46,12 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
}); });
}); });
}, },
[model_key, t, updateModelImage] [key, t, updateModelImage]
); );
const handleResetImage = useCallback(() => { const handleResetImage = useCallback(() => {
if (!model_key) {
return;
}
setImage(null); setImage(null);
deleteModelImage(model_key) deleteModelImage(key)
.unwrap() .unwrap()
.then(() => { .then(() => {
toast({ toast({
@ -69,7 +67,7 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
status: 'error', status: 'error',
}); });
}); });
}, [model_key, t, deleteModelImage]); }, [deleteModelImage, key, t]);
const { getInputProps, getRootProps } = useDropzone({ const { getInputProps, getRootProps } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },

View File

@ -13,10 +13,10 @@ export const ModelHeader = memo(({ modelConfig, children }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Flex alignItems="flex-start" gap={4}> <Flex alignItems="flex-start" gap={4}>
<ModelImageUpload model_key={modelConfig.key} model_image={modelConfig.cover_image} /> <ModelImageUpload modelConfig={modelConfig} />
<Flex flexDir="column" gap={1} flexGrow={1} minW={0}> <Flex flexDir="column" gap={1} flexGrow={1} minW={0}>
<Flex gap={2}> <Flex gap={2}>
<Heading as="h2" fontSize="lg" noOfLines={1} wordBreak="break-all"> <Heading size="md" noOfLines={1} wordBreak="break-all">
{modelConfig.name} {modelConfig.name}
</Heading> </Heading>
<Spacer /> <Spacer />

View File

@ -50,15 +50,17 @@ export const ModelView = memo(({ modelConfig }: Props) => {
)} )}
</SimpleGrid> </SimpleGrid>
</Box> </Box>
<Box layerStyle="second" borderRadius="base" p={4}> <Flex flexDir="column" layerStyle="second" borderRadius="base" p={4} gap={4}>
{modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && ( {modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && (
<MainModelDefaultSettings modelConfig={modelConfig} /> <MainModelDefaultSettings modelConfig={modelConfig} />
)} )}
{(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && ( {(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && (
<ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} /> <ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} />
)} )}
</Flex>
<Flex flexDir="column" layerStyle="second" borderRadius="base" p={4} gap={4}>
{(modelConfig.type === 'main' || modelConfig.type === 'lora') && <TriggerPhrases modelConfig={modelConfig} />} {(modelConfig.type === 'main' || modelConfig.type === 'lora') && <TriggerPhrases modelConfig={modelConfig} />}
</Box> </Flex>
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -0,0 +1,4 @@
import { createContext } from 'react';
import type { AnyModelConfig } from 'services/api/types';
export const SelectedModelContext = createContext<AnyModelConfig | null>(null);

View File

@ -99,16 +99,39 @@ export const TriggerPhrases = memo(({ modelConfig }: Props) => {
</FormControl> </FormControl>
</form> </form>
{triggerPhrases.length > 0 && (
<Flex gap="4" flexWrap="wrap"> <Flex gap="4" flexWrap="wrap">
{triggerPhrases.map((phrase, index) => ( {triggerPhrases.map((phrase, index) => (
<Tag size="md" key={index} py={2} px={4} bg="base.700"> <TriggerPhrasesTag
<TagLabel>{phrase}</TagLabel> key={`${phrase}_${index}`}
<TagCloseButton onClick={removeTriggerPhrase.bind(null, phrase)} isDisabled={isLoading} /> phrase={phrase}
</Tag> onRemoveTriggerPhrase={removeTriggerPhrase}
isLoading={isLoading}
/>
))} ))}
</Flex> </Flex>
)}
</Flex> </Flex>
); );
}); });
TriggerPhrases.displayName = 'TriggerPhrases'; TriggerPhrases.displayName = 'TriggerPhrases';
type TriggerPhrasesTagProps = {
phrase: string;
onRemoveTriggerPhrase: (phrase: string) => void;
isLoading: boolean;
};
const TriggerPhrasesTag = memo(({ phrase, onRemoveTriggerPhrase, isLoading }: TriggerPhrasesTagProps) => {
const onClick = useCallback(() => {
onRemoveTriggerPhrase(phrase);
}, [onRemoveTriggerPhrase, phrase]);
return (
<Tag size="md" py={2} px={4} bg="base.700">
<TagLabel>{phrase}</TagLabel>
<TagCloseButton onClick={onClick} isDisabled={isLoading} />
</Tag>
);
});
TriggerPhrasesTag.displayName = 'TriggerPhrasesTag';

View File

@ -1,14 +1,20 @@
import { Flex } from '@invoke-ai/ui-library'; import { ModelManagerLeftPanel } from 'features/modelManagerV2/subpanels/ModelManagerLeftPanel';
import { ModelManager } from 'features/modelManagerV2/subpanels/ModelManager';
import { ModelPane } from 'features/modelManagerV2/subpanels/ModelPane'; import { ModelPane } from 'features/modelManagerV2/subpanels/ModelPane';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo } from 'react'; import { memo } from 'react';
import { Panel, PanelGroup } from 'react-resizable-panels';
const ModelManagerTab = () => { const ModelManagerTab = () => {
return ( return (
<Flex w="full" h="full" gap="2"> <PanelGroup direction="horizontal">
<ModelManager /> <Panel>
<ModelManagerLeftPanel />
</Panel>
<ResizeHandle orientation="vertical" />
<Panel>
<ModelPane /> <ModelPane />
</Flex> </Panel>
</PanelGroup>
); );
}; };

View File

@ -17,7 +17,7 @@ const ResizeHandle = (props: ResizeHandleProps) => {
<ChakraPanelResizeHandle {...rest}> <ChakraPanelResizeHandle {...rest}>
<Flex sx={sx} data-orientation={orientation}> <Flex sx={sx} data-orientation={orientation}>
<Box className="resize-handle-inner" data-orientation={orientation} /> <Box className="resize-handle-inner" data-orientation={orientation} />
<Box className="resize-handle-drag-handle" data-orientation={orientation} /> {/* <Box className="resize-handle-drag-handle" data-orientation={orientation} /> */}
</Flex> </Flex>
</ChakraPanelResizeHandle> </ChakraPanelResizeHandle>
); );