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]);
return (
<Flex flexDir="column" p={3} h="full" gap={3}>
<Flex flexDir="column" h="full" gap={2}>
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm">{t('modelManager.installQueue')}</Heading>
<Button
@ -52,7 +52,7 @@ export const ModelInstallQueue = memo(() => {
{t('modelManager.prune')}
</Button>
</Flex>
<Box layerStyle="first" p={3} borderRadius="base" w="full" h="full">
<Box layerStyle="first" borderRadius="base" w="full" h="full">
<ScrollableContent>
<Flex flexDir="column-reverse" gap="2" w="full">
{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 { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { atom } from 'nanostores';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
import { InstallModelForm } from './AddModelPanel/InstallModelForm';
@ -20,34 +22,41 @@ export const InstallModels = memo(() => {
}, []);
return (
<Flex layerStyle="first" borderRadius="base" w="full" h="full" flexDir="column" gap={4}>
<Heading fontSize="xl">{t('modelManager.addModel')}</Heading>
<Tabs variant="collapse" height="50%" display="flex" flexDir="column" index={index} onChange={onChange}>
<TabList>
<Tab>{t('modelManager.urlOrLocalPath')}</Tab>
<Tab>{t('modelManager.huggingFace')}</Tab>
<Tab>{t('modelManager.scanFolder')}</Tab>
<Tab>{t('modelManager.starterModels')}</Tab>
</TabList>
<TabPanels p={3} height="100%">
<TabPanel>
<InstallModelForm />
</TabPanel>
<TabPanel height="100%">
<HuggingFaceForm />
</TabPanel>
<TabPanel height="100%">
<ScanModelsForm />
</TabPanel>
<TabPanel height="100%">
<StarterModelsForm />
</TabPanel>
</TabPanels>
</Tabs>
<Box layerStyle="second" borderRadius="base" h="50%">
<ModelInstallQueue />
</Box>
</Flex>
<PanelGroup direction="vertical">
<Panel>
<Flex w="full" h="full" flexDir="column" gap={2}>
<Heading fontSize="xl">{t('modelManager.addModel')}</Heading>
<Tabs variant="collapse" height="full" display="flex" flexDir="column" index={index} onChange={onChange}>
<TabList>
<Tab>{t('modelManager.urlOrLocalPath')}</Tab>
<Tab>{t('modelManager.huggingFace')}</Tab>
<Tab>{t('modelManager.scanFolder')}</Tab>
<Tab>{t('modelManager.starterModels')}</Tab>
</TabList>
<TabPanels p={3} height="100%">
<TabPanel>
<InstallModelForm />
</TabPanel>
<TabPanel height="100%">
<HuggingFaceForm />
</TabPanel>
<TabPanel height="100%">
<ScanModelsForm />
</TabPanel>
<TabPanel height="100%">
<StarterModelsForm />
</TabPanel>
</TabPanels>
</Tabs>
</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 { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation';
export const ModelManager = memo(() => {
export const ModelManagerLeftPanel = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleClickAddModel = useCallback(() => {
@ -16,19 +16,17 @@ export const ModelManager = memo(() => {
}, [dispatch]);
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">
<Heading fontSize="xl">{t('common.modelManager')}</Heading>
<Button size="sm" colorScheme="invokeYellow" leftIcon={<PiPlusBold />} onClick={handleClickAddModel}>
{t('modelManager.addModels')}
</Button>
</Flex>
<Flex flexDir="column" layerStyle="second" p={4} gap={4} borderRadius="base" w="full" h="full">
<ModelListNavigation />
<ModelList />
</Flex>
<ModelListNavigation />
<ModelList />
</Flex>
);
});
ModelManager.displayName = 'ModelManager';
ModelManagerLeftPanel.displayName = 'ModelManagerLeftPanel';

View File

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

View File

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

View File

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

View File

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

View File

@ -30,12 +30,12 @@ export const ModelListNavigation = memo(() => {
<InputGroup maxW="400px">
<Input
placeholder={t('modelManager.search')}
value={searchTerm || ''}
value={searchTerm}
data-testid="board-search-input"
onChange={handleSearch}
/>
{!!searchTerm?.length && (
{Boolean(searchTerm) && (
<InputRightElement h="full" pe={2}>
<IconButton
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 type { AnyModelConfig } from 'services/api/types';
@ -12,11 +12,16 @@ type ModelListWrapperProps = {
export const ModelListWrapper = memo((props: ModelListWrapperProps) => {
const { title, modelList } = props;
return (
<StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}>
{modelList.map((model) => (
<ModelListItem key={model.key} model={model} />
))}
</StickyScrollable>
<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) => (
<ModelListItem key={model.key} model={model} />
))}
</Flex>
</Box>
);
});

View File

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

View File

@ -6,14 +6,15 @@ import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi';
import { useDeleteModelImageMutation, useUpdateModelImageMutation } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
type Props = {
model_key: string | null;
model_image?: string | null;
modelConfig: AnyModelConfig;
};
const ModelImageUpload = ({ model_key, model_image }: Props) => {
const [image, setImage] = useState<string | null>(model_image || null);
const ModelImageUpload = ({ modelConfig }: Props) => {
const { key, cover_image } = modelConfig;
const [image, setImage] = useState<string | null>(cover_image || null);
const { t } = useTranslation();
const [updateModelImage] = useUpdateModelImageMutation();
@ -23,11 +24,11 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
(files: File[]) => {
const file = files[0];
if (!file || !model_key) {
if (!file) {
return;
}
updateModelImage({ key: model_key, image: file })
updateModelImage({ key, image: file })
.unwrap()
.then(() => {
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(() => {
if (!model_key) {
return;
}
setImage(null);
deleteModelImage(model_key)
deleteModelImage(key)
.unwrap()
.then(() => {
toast({
@ -69,7 +67,7 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
status: 'error',
});
});
}, [model_key, t, deleteModelImage]);
}, [deleteModelImage, key, t]);
const { getInputProps, getRootProps } = useDropzone({
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();
return (
<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 gap={2}>
<Heading as="h2" fontSize="lg" noOfLines={1} wordBreak="break-all">
<Heading size="md" noOfLines={1} wordBreak="break-all">
{modelConfig.name}
</Heading>
<Spacer />

View File

@ -50,15 +50,17 @@ export const ModelView = memo(({ modelConfig }: Props) => {
)}
</SimpleGrid>
</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' && (
<MainModelDefaultSettings modelConfig={modelConfig} />
)}
{(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && (
<ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} />
)}
</Flex>
<Flex flexDir="column" layerStyle="second" borderRadius="base" p={4} gap={4}>
{(modelConfig.type === 'main' || modelConfig.type === 'lora') && <TriggerPhrases modelConfig={modelConfig} />}
</Box>
</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>
</form>
<Flex gap="4" flexWrap="wrap">
{triggerPhrases.map((phrase, index) => (
<Tag size="md" key={index} py={2} px={4} bg="base.700">
<TagLabel>{phrase}</TagLabel>
<TagCloseButton onClick={removeTriggerPhrase.bind(null, phrase)} isDisabled={isLoading} />
</Tag>
))}
</Flex>
{triggerPhrases.length > 0 && (
<Flex gap="4" flexWrap="wrap">
{triggerPhrases.map((phrase, index) => (
<TriggerPhrasesTag
key={`${phrase}_${index}`}
phrase={phrase}
onRemoveTriggerPhrase={removeTriggerPhrase}
isLoading={isLoading}
/>
))}
</Flex>
)}
</Flex>
);
});
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 { ModelManager } from 'features/modelManagerV2/subpanels/ModelManager';
import { ModelManagerLeftPanel } from 'features/modelManagerV2/subpanels/ModelManagerLeftPanel';
import { ModelPane } from 'features/modelManagerV2/subpanels/ModelPane';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo } from 'react';
import { Panel, PanelGroup } from 'react-resizable-panels';
const ModelManagerTab = () => {
return (
<Flex w="full" h="full" gap="2">
<ModelManager />
<ModelPane />
</Flex>
<PanelGroup direction="horizontal">
<Panel>
<ModelManagerLeftPanel />
</Panel>
<ResizeHandle orientation="vertical" />
<Panel>
<ModelPane />
</Panel>
</PanelGroup>
);
};

View File

@ -17,7 +17,7 @@ const ResizeHandle = (props: ResizeHandleProps) => {
<ChakraPanelResizeHandle {...rest}>
<Flex sx={sx} 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>
</ChakraPanelResizeHandle>
);