Skip to content
Merged
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
9 changes: 9 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,15 @@
"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",
"invalidPathFormat": "Path must be an absolute path (e.g., C:\\Models\\... or /home/user/models/...)",
"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,136 @@
import {
Button,
Flex,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@invoke-ai/ui-library';
import { isExternalModel } from 'features/modelManagerV2/subpanels/ModelPanel/isExternalModel';
import { toast } from 'features/toast/toast';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useMemo, 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);
}, []);

// Validate that the new path is still a valid external model path format
const isValidPath = useMemo(() => isExternalModel(newPath.trim()), [newPath]);

const handleSubmit = useCallback(() => {
if (!newPath.trim() || newPath === modelConfig.path || !isValidPath) {
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, isValidPath]);

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

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 isInvalid={showError}>
<FormLabel>{t('modelManager.newPath')}</FormLabel>
<Input value={newPath} onChange={handlePathChange} placeholder={t('modelManager.newPathPlaceholder')} />
{showError && <FormErrorMessage>{t('modelManager.invalidPathFormat')}</FormErrorMessage>}
</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 || showError}
>
{t('common.save')}
</Button>
</Flex>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
});

ModelUpdatePathButton.displayName = 'ModelUpdatePathButton';
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { AnyModelConfig } from 'services/api/types';

import { isExternalModel } from './isExternalModel';
import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings';
import { ModelAttrView } from './ModelAttrView';
import { ModelDeleteButton } from './ModelDeleteButton';
import { ModelReidentifyButton } from './ModelReidentifyButton';
import { ModelUpdatePathButton } from './ModelUpdatePathButton';
import { RelatedModels } from './RelatedModels';

type Props = {
Expand All @@ -23,6 +25,9 @@ type Props = {
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 +49,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* 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".
*/
export 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;
}
// Windows UNC path (e.g., "\\ServerName\ShareName\..." or "//ServerName/ShareName/...")
if (path.startsWith('\\\\') || path.startsWith('//')) {
return true;
}
return false;
};