Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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