Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,14 @@
"reidentifySuccess": "Model reidentified successfully",
"reidentifyUnknown": "Unable to identify model",
"reidentifyError": "Error reidentifying model",
"updatePath": "Update Path",
"updatePathTooltip": "Update the file path for this model if you have moved the model files to a new location.",
"updatePathDescription": "Enter the new path to the model file or directory. Use this if you have manually moved the model files on disk.",
"currentPath": "Current Path",
"newPath": "New Path",
"newPathPlaceholder": "Enter new path...",
"pathUpdated": "Model path updated successfully",
"pathUpdateFailed": "Failed to update model path",
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
"convertToDiffusers": "Convert To Diffusers",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFolderOpenFill } from 'react-icons/pi';
import { useUpdateModelMutation } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';

interface Props {
modelConfig: AnyModelConfig;
}

export const ModelUpdatePathButton = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const [updateModel, { isLoading }] = useUpdateModelMutation();
const [newPath, setNewPath] = useState(modelConfig.path);

const handleOpen = useCallback(() => {
setNewPath(modelConfig.path);
onOpen();
}, [modelConfig.path, onOpen]);

const handlePathChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setNewPath(e.target.value);
}, []);

const handleSubmit = useCallback(() => {
if (!newPath.trim() || newPath === modelConfig.path) {
onClose();
return;
}

updateModel({
key: modelConfig.key,
body: { path: newPath.trim() },
})
.unwrap()
.then(() => {
toast({
id: 'MODEL_PATH_UPDATED',
title: t('modelManager.pathUpdated'),
status: 'success',
});
onClose();
})
.catch(() => {
toast({
id: 'MODEL_PATH_UPDATE_FAILED',
title: t('modelManager.pathUpdateFailed'),
status: 'error',
});
});
}, [newPath, modelConfig.path, modelConfig.key, updateModel, onClose, t]);

const hasChanges = newPath.trim() !== modelConfig.path;

return (
<>
<Button
onClick={handleOpen}
size="sm"
aria-label={t('modelManager.updatePathTooltip')}
tooltip={t('modelManager.updatePathTooltip')}
flexShrink={0}
leftIcon={<PiFolderOpenFill />}
>
{t('modelManager.updatePath')}
</Button>
<Modal isOpen={isOpen} onClose={onClose} isCentered size="lg" useInert={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('modelManager.updatePath')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex flexDirection="column" gap={4}>
<Text fontSize="sm" color="base.400">
{t('modelManager.updatePathDescription')}
</Text>
<FormControl>
<FormLabel>{t('modelManager.currentPath')}</FormLabel>
<Text fontSize="sm" color="base.300" wordBreak="break-all">
{modelConfig.path}
</Text>
</FormControl>
<FormControl>
<FormLabel>{t('modelManager.newPath')}</FormLabel>
<Input value={newPath} onChange={handlePathChange} placeholder={t('modelManager.newPathPlaceholder')} />
</FormControl>
</Flex>
</ModalBody>
<ModalFooter>
<Flex gap={2}>
<Button variant="ghost" onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="invokeYellow" onClick={handleSubmit} isLoading={isLoading} isDisabled={!hasChanges}>
{t('common.save')}
</Button>
</Flex>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
});

ModelUpdatePathButton.displayName = 'ModelUpdatePathButton';
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,36 @@ import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDe
import { ModelAttrView } from './ModelAttrView';
import { ModelDeleteButton } from './ModelDeleteButton';
import { ModelReidentifyButton } from './ModelReidentifyButton';
import { ModelUpdatePathButton } from './ModelUpdatePathButton';
import { RelatedModels } from './RelatedModels';

type Props = {
modelConfig: AnyModelConfig;
};

/**
* Checks if a model path is absolute (external model) or relative (Invoke-controlled).
* External models have absolute paths like "X:/ModelPath/model.safetensors" or "/home/user/models/model.safetensors".
* Invoke-controlled models have relative paths like "uuid/model.safetensors".
*/
const isExternalModel = (path: string): boolean => {
// Unix absolute path
if (path.startsWith('/')) {
return true;
}
// Windows absolute path (e.g., "X:/..." or "X:\...")
if (path.length > 1 && path[1] === ':') {
return true;
}
return false;
};

export const ModelView = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();

// Only allow path updates for external models (not Invoke-controlled)
const canUpdatePath = useMemo(() => isExternalModel(modelConfig.path), [modelConfig.path]);

const withSettings = useMemo(() => {
if (modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner') {
return true;
Expand All @@ -44,6 +65,7 @@ export const ModelView = memo(({ modelConfig }: Props) => {
return (
<Flex flexDir="column" gap={4} h="full">
<ModelHeader modelConfig={modelConfig}>
{canUpdatePath && <ModelUpdatePathButton modelConfig={modelConfig} />}
<ModelReidentifyButton modelConfig={modelConfig} />
{modelConfig.format === 'checkpoint' && modelConfig.type === 'main' && (
<ModelConvertButton modelConfig={modelConfig} />
Expand Down