diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx index df04b4e4584..fc99bcec7bf 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx @@ -35,7 +35,7 @@ export const LaunchpadForm = memo(() => { return ( - + {/* Welcome Section */} {t('modelManager.launchpad.welcome')} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions.tsx new file mode 100644 index 00000000000..28a8b15f55f --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions.tsx @@ -0,0 +1,45 @@ +import { Badge, Button, Flex } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCheckBold, PiPlusBold } from 'react-icons/pi'; + +type Props = { + handleInstall: () => void; + isInstalled: boolean; +}; + +export const ModelResultItemActions = memo(({ handleInstall, isInstalled }: Props) => { + const { t } = useTranslation(); + + return ( + + {isInstalled ? ( + // TODO: Add a link button to navigate to model + + + + ) : ( + + )} + + ); +}); + +ModelResultItemActions.displayName = 'ModelResultItemActions'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResultItem.tsx index 9f8c1bdc843..31683900860 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResultItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResultItem.tsx @@ -1,33 +1,56 @@ -import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex, Text } from '@invoke-ai/ui-library'; +import { ModelResultItemActions } from 'features/modelManagerV2/subpanels/AddModelPanel/ModelResultItemActions'; +import { memo, useCallback, useMemo } from 'react'; import type { ScanFolderResponse } from 'services/api/endpoints/models'; type Props = { result: ScanFolderResponse[number]; installModel: (source: string) => void; }; -export const ScanModelResultItem = memo(({ result, installModel }: Props) => { - const { t } = useTranslation(); +const scanFolderResultItemSx: SystemStyleObject = { + alignItems: 'center', + justifyContent: 'space-between', + w: '100%', + py: 2, + px: 1, + gap: 3, + borderBottomWidth: '1px', + borderColor: 'base.700', +}; + +export const ScanModelResultItem = memo(({ result, installModel }: Props) => { const handleInstall = useCallback(() => { installModel(result.path); }, [installModel, result]); + const modelDisplayName = useMemo(() => { + const normalizedPath = result.path.replace(/\\/g, '/').replace(/\/+$/, ''); + + // Extract filename/folder name from path + const lastSlashIndex = normalizedPath.lastIndexOf('/'); + return lastSlashIndex === -1 ? normalizedPath : normalizedPath.slice(lastSlashIndex + 1); + }, [result.path]); + + const modelPathParts = result.path.split(/[/\\]/); + return ( - + - {result.path.split('\\').slice(-1)[0]} - {result.path} + {/* Model Title */} + {modelDisplayName} + {/* Model Path */} + + {modelPathParts.map((part, index) => ( + + {part} + {index < modelPathParts.length - 1 && '/'} + + ))} + - - {result.is_installed ? ( - {t('common.installed')} - ) : ( - } onClick={handleInstall} size="sm" /> - )} - + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx index 49159aab5e1..c57eee257d4 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx @@ -113,9 +113,9 @@ export const ScanModelsResults = memo(({ results }: ScanModelResultsProps) => { - + - + {filteredResults.map((result) => ( ))} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleButton.tsx index 83fe058baf3..b50e2777f8a 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleButton.tsx @@ -13,6 +13,7 @@ import { useStarterBundleInstallStatus } from 'features/modelManagerV2/hooks/use import { t } from 'i18next'; import type { MouseEvent } from 'react'; import { useCallback } from 'react'; +import { PiDownloadSimpleBold } from 'react-icons/pi'; import type { S } from 'services/api/types'; export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterModelBundle'] } & ButtonProps) => { @@ -33,8 +34,16 @@ export const StarterBundleButton = ({ bundle, ...rest }: { bundle: S['StarterMod return ( <> - { const { t } = useTranslation(); const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg(); @@ -40,22 +53,16 @@ export const StarterModelsResultItem = memo(({ starterModel }: Props) => { }, [modelsToInstall, installModel, t]); return ( - + - + {starterModel.name} + {starterModel.description} + {starterModel.type.replaceAll('_', ' ')} - {starterModel.name} - {starterModel.description} - - {isInstalled ? ( - {t('common.installed')} - ) : ( - } onClick={onClick} size="sm" /> - )} - + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx index 126706fbd76..86350c54c42 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsResults.tsx @@ -48,9 +48,9 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps return ( - + {size(results.starter_bundles) > 0 && ( - + {t('modelManager.starterBundles')} @@ -73,7 +73,8 @@ export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps )} - + + - + + - + {filteredResults.map((result) => ( ))} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx index 9cc7812014b..eed27c5fd60 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx @@ -1,10 +1,12 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box, Button, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore'; import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiInfoBold } from 'react-icons/pi'; +import { PiCubeBold, PiFolderOpenBold, PiInfoBold, PiLinkSimpleBold, PiShootingStarBold } from 'react-icons/pi'; +import { SiHuggingface } from 'react-icons/si'; import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm'; import { InstallModelForm } from './AddModelPanel/InstallModelForm'; @@ -12,6 +14,12 @@ import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm'; import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue'; import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm'; +const installModelsTabSx: SystemStyleObject = { + display: 'flex', + gap: 2, + px: 2, +}; + export const InstallModels = memo(() => { const { t } = useTranslation(); const tabIndex = useStore($installModelsTabIndex); @@ -29,21 +37,36 @@ export const InstallModels = memo(() => { - {t('modelManager.launchpadTab')} - {t('modelManager.urlOrLocalPath')} - {t('modelManager.huggingFace')} - {t('modelManager.scanFolder')} - {t('modelManager.starterModels')} + + + {t('modelManager.launchpadTab')} + + + + {t('modelManager.urlOrLocalPath')} + + + + {t('modelManager.huggingFace')} + + + + {t('modelManager.scanFolder')} + + + + {t('modelManager.starterModels')} + - + diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx index 8133960c516..9447bd4145f 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Button, Flex, Heading } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectSelectedModelKey, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; @@ -8,6 +9,16 @@ import { PiPlusBold } from 'react-icons/pi'; import ModelList from './ModelManagerPanel/ModelList'; import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation'; +const modelManagerSx: SystemStyleObject = { + flexDir: 'column', + p: 4, + gap: 4, + borderRadius: 'base', + w: '50%', + minWidth: '360px', + h: 'full', +}; + export const ModelManager = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -17,7 +28,7 @@ export const ModelManager = memo(() => { const selectedModelKey = useAppSelector(selectSelectedModelKey); return ( - + {t('common.modelManager')} @@ -28,7 +39,7 @@ export const ModelManager = memo(() => { )} - + diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx index e9f062eb2d9..14a80a41029 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Flex, Icon, Image } from '@invoke-ai/ui-library'; import { typedMemo } from 'common/util/typedMemo'; import { PiImage } from 'react-icons/pi'; @@ -6,19 +7,23 @@ type Props = { image_url?: string | null; }; -export const MODEL_IMAGE_THUMBNAIL_SIZE = '40px'; -const FALLBACK_ICON_SIZE = '24px'; +const MODEL_IMAGE_THUMBNAIL_SIZE = '54px'; +const FALLBACK_ICON_SIZE = '28px'; + +const sharedSx: SystemStyleObject = { + rounded: 'base', + height: MODEL_IMAGE_THUMBNAIL_SIZE, + minWidth: MODEL_IMAGE_THUMBNAIL_SIZE, + bg: 'base.850', + borderWidth: '1px', + borderColor: 'base.750', + borderStyle: 'solid', +}; const ModelImage = ({ image_url }: Props) => { if (!image_url) { return ( - + ); @@ -29,16 +34,14 @@ const ModelImage = ({ image_url }: Props) => { src={image_url} objectFit="cover" objectPosition="50% 50%" - height={MODEL_IMAGE_THUMBNAIL_SIZE} width={MODEL_IMAGE_THUMBNAIL_SIZE} minHeight={MODEL_IMAGE_THUMBNAIL_SIZE} - minWidth={MODEL_IMAGE_THUMBNAIL_SIZE} - borderRadius="base" + sx={sharedSx} fallback={ diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index 16a4b7fa4f3..dc7b2122a89 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -1,32 +1,57 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library'; +import { Flex, Spacer, Text } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectModelManagerV2Slice, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge'; -import { toast } from 'features/toast/toast'; +import { ModelDeleteButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelDeleteButton'; import { filesize } from 'filesize'; -import type { MouseEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleBold } from 'react-icons/pi'; -import { useDeleteModelsMutation } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; -import ModelImage, { MODEL_IMAGE_THUMBNAIL_SIZE } from './ModelImage'; +import ModelImage from './ModelImage'; type ModelListItemProps = { model: AnyModelConfig; }; const sx: SystemStyleObject = { - _hover: { bg: 'base.700' }, - "&[aria-selected='true']": { bg: 'base.700' }, + paddingInline: 3, + paddingBlock: 2, + position: 'relative', + rounded: 'base', + '&:after,&:before': { + content: `''`, + position: 'absolute', + pointerEvents: 'none', + }, + '&:after': { + h: '1px', + bottom: '-0.5px', + insetInline: 3, + bg: 'base.850', + }, + '&:before': { + left: 1, + w: 1, + insetBlock: 2, + rounded: 'base', + }, + _hover: { + bg: 'base.850', + '& .delete-button': { opacity: 1 }, + }, + '& .delete-button': { opacity: 0 }, + "&[aria-selected='false']:hover:before": { bg: 'base.750' }, + "&[aria-selected='true']": { + bg: 'base.800', + '& .delete-button': { opacity: 1 }, + }, + "&[aria-selected='true']:before": { bg: 'invokeBlue.300' }, }; const ModelListItem = ({ model }: ModelListItemProps) => { - const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectIsSelected = useMemo( () => @@ -37,58 +62,25 @@ const ModelListItem = ({ model }: ModelListItemProps) => { [model.key] ); const isSelected = useAppSelector(selectIsSelected); - const [deleteModel] = useDeleteModelsMutation(); - const { isOpen, onOpen, onClose } = useDisclosure(); const handleSelectModel = useCallback(() => { dispatch(setSelectedModelKey(model.key)); }, [model.key, dispatch]); - const onClickDeleteButton = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - onOpen(); - }, - [onOpen] - ); - const handleModelDelete = useCallback(() => { - deleteModel({ key: model.key }) - .unwrap() - .then((_) => { - toast({ - id: 'MODEL_DELETED', - title: `${t('modelManager.modelDeleted')}: ${model.name}`, - status: 'success', - }); - }) - .catch((error) => { - if (error) { - toast({ - id: 'MODEL_DELETE_FAILED', - title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, - status: 'error', - }); - } - }); - dispatch(setSelectedModelKey(null)); - }, [deleteModel, model, dispatch, t]); - return ( - + {model.name} @@ -101,39 +93,15 @@ const ModelListItem = ({ model }: ModelListItemProps) => { {model.description || 'No Description'} - - - - + + + + - } - aria-label={t('modelManager.deleteConfig')} - colorScheme="error" - h={MODEL_IMAGE_THUMBNAIL_SIZE} - w={MODEL_IMAGE_THUMBNAIL_SIZE} - /> - - - {t('modelManager.deleteMsg1')} - {t('modelManager.deleteMsg2')} - - + + + ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx index c0a85216a98..0d9c9259d48 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx @@ -1,4 +1,4 @@ -import { Flex, IconButton, Input, InputGroup, InputRightElement, Spacer } from '@invoke-ai/ui-library'; +import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectSearchTerm, setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; @@ -25,9 +25,7 @@ export const ModelListNavigation = memo(() => { return ( - - - + { )} + + + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx index bde79e45540..08c3f4568d2 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { StickyScrollable } from 'features/system/components/StickyScrollable'; import { memo } from 'react'; import type { AnyModelConfig } from 'services/api/types'; @@ -9,10 +10,23 @@ type ModelListWrapperProps = { modelList: AnyModelConfig[]; }; +const headingSx = { + bg: 'base.900', + pb: 3, + pl: 3, +} satisfies SystemStyleObject; + +const contentSx = { + gap: 0, + p: 0, + bg: 'base.900', + borderRadius: '0', +} satisfies SystemStyleObject; + export const ModelListWrapper = memo((props: ModelListWrapperProps) => { const { title, modelList } = props; return ( - + {modelList.map((model) => ( ))} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx index 415257a0585..92a632d5205 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; @@ -6,13 +7,22 @@ import { memo } from 'react'; import { InstallModels } from './InstallModels'; import { Model } from './ModelPanel/Model'; +const modelPaneSx: SystemStyleObject = { + layerStyle: 'first', + p: 4, + borderRadius: 'base', + w: { + base: '50%', + lg: '75%', + '2xl': '85%', + }, + h: 'full', + minWidth: '300px', +}; + export const ModelPane = memo(() => { const selectedModelKey = useAppSelector(selectSelectedModelKey); - return ( - - {selectedModelKey ? : } - - ); + return {selectedModelKey ? : }; }); ModelPane.displayName = 'ModelPane'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx index cde99552362..0e28c1802cb 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx @@ -1,3 +1,4 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box, IconButton, Image } from '@invoke-ai/ui-library'; import { dropzoneAccept } from 'common/hooks/useImageUploadButton'; import { typedMemo } from 'common/util/typedMemo'; @@ -8,6 +9,21 @@ import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiUploadBold } from 'react-icons/pi'; import { useDeleteModelImageMutation, useUpdateModelImageMutation } from 'services/api/endpoints/models'; +const sharedSx: SystemStyleObject = { + w: 108, + h: 108, + fontSize: 36, + borderRadius: 'base', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + bg: 'base.800', + borderWidth: '1px', + borderStyle: 'solid', + borderColor: 'base.700', + flexShrink: 0, +}; + type Props = { model_key: string | null; model_image?: string | null; @@ -86,10 +102,9 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => { src={image} objectFit="cover" objectPosition="50% 50%" - height={108} - width={108} minWidth={108} borderRadius="base" + sx={sharedSx} /> { variant="ghost" aria-label={t('modelManager.uploadImage')} tooltip={t('modelManager.uploadImage')} - w={108} - h={108} fontSize={36} icon={} + sx={sharedSx} isLoading={request.isLoading} {...getRootProps()} /> diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx index 14119374d46..0d428d2adb7 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx @@ -52,6 +52,7 @@ export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => { return ( <> + ) : ( + } + aria-label={t('modelManager.deleteConfig')} + colorScheme="error" + /> + )} + + + + {t('modelManager.deleteMsg1')} + {t('modelManager.deleteMsg2')} + + + + ); +}); + +ModelDeleteButton.displayName = 'ModelDeleteButton'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx index 89571e6202e..ff5c680325c 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx @@ -24,6 +24,7 @@ import type { AnyModelConfig } from 'services/api/types'; import BaseModelSelect from './Fields/BaseModelSelect'; import ModelVariantSelect from './Fields/ModelVariantSelect'; import PredictionTypeSelect from './Fields/PredictionTypeSelect'; +import { ModelFooter } from './ModelFooter'; type Props = { modelConfig: AnyModelConfig; @@ -158,6 +159,7 @@ export const ModelEdit = memo(({ modelConfig }: Props) => { + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelFooter.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelFooter.tsx new file mode 100644 index 00000000000..f31609c4017 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelFooter.tsx @@ -0,0 +1,66 @@ +import { Flex, Heading, type SystemStyleObject } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AnyModelConfig } from 'services/api/types'; + +import { ModelConvertButton } from './ModelConvertButton'; +import { ModelDeleteButton } from './ModelDeleteButton'; +import { ModelEditButton } from './ModelEditButton'; + +const footerRowSx: SystemStyleObject = { + justifyContent: 'space-between', + alignItems: 'center', + gap: 3, + '&:not(:last-of-type)': { + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + borderBottomColor: 'border', + }, + p: 3, +}; + +type Props = { + modelConfig: AnyModelConfig; + isEditing: boolean; +}; + +export const ModelFooter = memo(({ modelConfig, isEditing }: Props) => { + const { t } = useTranslation(); + + const shouldShowConvertOption = !isEditing && modelConfig.format === 'checkpoint' && modelConfig.type === 'main'; + + return ( + + {shouldShowConvertOption && ( + + + {t('modelManager.convertToDiffusers')} + + + + + + )} + {!isEditing && ( + + + {t('modelManager.edit')} + + + + + + )} + + + {t('modelManager.deleteModel')} + + + + + + + ); +}); + +ModelFooter.displayName = 'ModelFooter'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx index 391edae1c51..cd85742e3ab 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, SimpleGrid } from '@invoke-ai/ui-library'; +import { Box, Divider, Flex, SimpleGrid } from '@invoke-ai/ui-library'; import { ControlAdapterModelDefaultSettings } from 'features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/ControlAdapterModelDefaultSettings'; import { LoRAModelDefaultSettings } from 'features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/LoRAModelDefaultSettings'; import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton'; @@ -12,6 +12,7 @@ import type { AnyModelConfig } from 'services/api/types'; import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings'; import { ModelAttrView } from './ModelAttrView'; +import { ModelFooter } from './ModelFooter'; import { RelatedModels } from './RelatedModels'; type Props = { @@ -46,8 +47,9 @@ export const ModelView = memo(({ modelConfig }: Props) => { )} + - + @@ -73,26 +75,33 @@ export const ModelView = memo(({ modelConfig }: Props) => { {withSettings && ( - - {modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && ( - - )} - {(modelConfig.type === 'controlnet' || - modelConfig.type === 't2i_adapter' || - modelConfig.type === 'control_lora') && } - {modelConfig.type === 'lora' && ( - <> - - - - )} - {modelConfig.type === 'main' && } - + <> + + + {modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && ( + + )} + {(modelConfig.type === 'controlnet' || + modelConfig.type === 't2i_adapter' || + modelConfig.type === 'control_lora') && ( + + )} + {modelConfig.type === 'lora' && ( + <> + + + + )} + {modelConfig.type === 'main' && } + + )} - + + + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx index 5931822e9e5..57cb3181d9a 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx @@ -74,6 +74,8 @@ export const TriggerPhrases = memo(({ modelConfig }: Props) => { [addTriggerPhrase] ); + const hasTriggerPhrases = triggerPhrases.length > 0; + return (
@@ -99,14 +101,16 @@ export const TriggerPhrases = memo(({ modelConfig }: Props) => {
- - {triggerPhrases.map((phrase, index) => ( - - {phrase} - - - ))} - + {hasTriggerPhrases && ( + + {triggerPhrases.map((phrase, index) => ( + + {phrase} + + + ))} + + )}
); });