mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip mm tab design
This commit is contained in:
parent
09d1e190e7
commit
375b8bc67d
@ -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} />)}
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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';
|
@ -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]}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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'] },
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
|
||||
export const SelectedModelContext = createContext<AnyModelConfig | null>(null);
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user