From 81544ac2368b24dac5df5c490fd02c0efc2723d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 19:13:27 +0000 Subject: [PATCH 1/4] feat(model_manager): add export/import for model settings Add the ability to export model settings (default_settings, trigger_phrases, cpu_only) as JSON and import them back. The model name is used as the filename for exports. https://claude.ai/code/session_01LXKjbRjfzcG3d3vzk3xRCh --- invokeai/frontend/web/public/locales/en.json | 8 +- .../ModelPanel/ModelSettingsExportButton.tsx | 70 +++++++++++ .../ModelPanel/ModelSettingsImportButton.tsx | 113 ++++++++++++++++++ .../subpanels/ModelPanel/ModelView.tsx | 4 + 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b004fe818a0..a9927edaf3a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1100,7 +1100,13 @@ "installingXModels_other": "Installing {{count}} models", "skippingXDuplicates_one": ", skipping {{count}} duplicate", "skippingXDuplicates_other": ", skipping {{count}} duplicates", - "manageModels": "Manage Models" + "manageModels": "Manage Models", + "exportSettings": "Export Settings", + "importSettings": "Import Settings", + "settingsExported": "Model settings exported", + "settingsImported": "Model settings imported", + "settingsImportFailed": "Failed to import model settings", + "settingsImportInvalidFile": "Invalid settings file" }, "models": { "addLora": "Add LoRA", diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx new file mode 100644 index 00000000000..50a7ee07c6f --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx @@ -0,0 +1,70 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { toast } from 'features/toast/toast'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDownloadSimpleBold } from 'react-icons/pi'; +import type { AnyModelConfig } from 'services/api/types'; + +type Props = { + modelConfig: AnyModelConfig; +}; + +const buildExportData = (modelConfig: AnyModelConfig): Record => { + const data: Record = {}; + + if ('default_settings' in modelConfig && modelConfig.default_settings) { + data.default_settings = modelConfig.default_settings; + } + + if ('trigger_phrases' in modelConfig && modelConfig.trigger_phrases) { + data.trigger_phrases = modelConfig.trigger_phrases; + } + + if ('cpu_only' in modelConfig && modelConfig.cpu_only != null) { + data.cpu_only = modelConfig.cpu_only; + } + + return data; +}; + +const sanitizeFilename = (name: string): string => { + return name.replace(/[<>:"/\\|?*]/g, '_'); +}; + +export const ModelSettingsExportButton = memo(({ modelConfig }: Props) => { + const { t } = useTranslation(); + + const handleExport = useCallback(() => { + const data = buildExportData(modelConfig); + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const filename = `${sanitizeFilename(modelConfig.name)}.json`; + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast({ + id: 'SETTINGS_EXPORTED', + title: t('modelManager.settingsExported'), + status: 'success', + }); + }, [modelConfig, t]); + + return ( + } + aria-label={t('modelManager.exportSettings')} + tooltip={t('modelManager.exportSettings')} + onClick={handleExport} + /> + ); +}); + +ModelSettingsExportButton.displayName = 'ModelSettingsExportButton'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx new file mode 100644 index 00000000000..6d3cb733b44 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx @@ -0,0 +1,113 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { toast } from 'features/toast/toast'; +import { memo, useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiUploadSimpleBold } from 'react-icons/pi'; +import { useUpdateModelMutation } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; + +type Props = { + modelConfig: AnyModelConfig; +}; + +export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { + const { t } = useTranslation(); + const fileInputRef = useRef(null); + const [updateModel] = useUpdateModelMutation(); + + const applySettings = useCallback( + async (data: Record) => { + const body: Record = {}; + + if ('default_settings' in data && data.default_settings != null) { + body.default_settings = data.default_settings; + } + + if ('trigger_phrases' in data && Array.isArray(data.trigger_phrases)) { + body.trigger_phrases = data.trigger_phrases; + } + + if ('cpu_only' in data && data.cpu_only != null) { + body.cpu_only = data.cpu_only; + } + + if (Object.keys(body).length === 0) { + return; + } + + await updateModel({ + key: modelConfig.key, + body, + }) + .unwrap() + .then(() => { + toast({ + id: 'SETTINGS_IMPORTED', + title: t('modelManager.settingsImported'), + status: 'success', + }); + }) + .catch((error) => { + toast({ + id: 'SETTINGS_IMPORT_FAILED', + title: `${t('modelManager.settingsImportFailed')}: ${error.data?.detail ?? ''}`, + status: 'error', + }); + }); + }, + [modelConfig.key, updateModel, t] + ); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const json = JSON.parse(event.target?.result as string); + applySettings(json); + } catch { + toast({ + id: 'SETTINGS_IMPORT_INVALID', + title: t('modelManager.settingsImportInvalidFile'), + status: 'error', + }); + } + }; + reader.readAsText(file); + + // Reset the input so the same file can be re-selected + e.target.value = ''; + }, + [applySettings, t] + ); + + const handleClick = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + return ( + <> + } + aria-label={t('modelManager.importSettings')} + tooltip={t('modelManager.importSettings')} + onClick={handleClick} + /> + + + ); +}); + +ModelSettingsImportButton.displayName = 'ModelSettingsImportButton'; 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 c2d20ccbbda..e1a6fcf7cb6 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx @@ -5,6 +5,8 @@ import { LoRAModelDefaultSettings } from 'features/modelManagerV2/subpanels/Mode import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton'; import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton'; import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader'; +import { ModelSettingsExportButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton'; +import { ModelSettingsImportButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton'; import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases'; import { filesize } from 'filesize'; import { memo, useMemo } from 'react'; @@ -86,6 +88,8 @@ export const ModelView = memo(({ modelConfig }: Props) => { {modelConfig.format === 'checkpoint' && modelConfig.type === 'main' && ( )} + {withSettings && } + {withSettings && } From 4320879464ebe9693e0bdaed2a84fcfb672ea810 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Fri, 13 Feb 2026 21:44:28 +0100 Subject: [PATCH 2/4] fix(ui): reset settings forms after import so updated values display immediately The useForm defaultValues only apply on mount, so importing model settings updated the backend but the forms kept showing stale values. Added useEffect to reset forms when the underlying model config changes. Also fixed lint errors (strict equality, missing React import). --- .../ControlAdapterModelDefaultSettings.tsx | 6 +++++- .../EncoderModelSettings/EncoderModelSettings.tsx | 6 +++++- .../LoRAModelDefaultSettings.tsx | 6 +++++- .../MainModelDefaultSettings.tsx | 6 +++++- .../ModelPanel/ModelSettingsExportButton.tsx | 2 +- .../ModelPanel/ModelSettingsImportButton.tsx | 15 +++++---------- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/ControlAdapterModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/ControlAdapterModelDefaultSettings.tsx index 0bcd5b27161..90469da1453 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/ControlAdapterModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/ControlAdapterModelDefaultSettings.tsx @@ -3,7 +3,7 @@ import { useControlAdapterModelDefaultSettings } from 'features/modelManagerV2/h import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlAdapterModelDefaultSettings/DefaultPreprocessor'; import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; import { toast } from 'features/toast/toast'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useEffect } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -30,6 +30,10 @@ export const ControlAdapterModelDefaultSettings = memo(({ modelConfig }: Props) defaultValues: defaultSettingsDefaults, }); + useEffect(() => { + reset(defaultSettingsDefaults); + }, [defaultSettingsDefaults, reset]); + const onSubmit = useCallback>( (data) => { const body = { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx index bf1690359e5..e10766214f4 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx @@ -6,7 +6,7 @@ import { selectSelectedModelKey } from 'features/modelManagerV2/store/modelManag import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; import { toast } from 'features/toast/toast'; import type { ChangeEvent } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import type { Control, SubmitHandler } from 'react-hook-form'; import { useController, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -80,6 +80,10 @@ export const EncoderModelSettings = memo(({ modelConfig }: Props) => { defaultValues: settingsDefaults, }); + useEffect(() => { + reset(settingsDefaults); + }, [settingsDefaults, reset]); + const onSubmit = useCallback>( (data) => { if (!selectedModelKey) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/LoRAModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/LoRAModelDefaultSettings.tsx index a012460161f..63268555ba1 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/LoRAModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/LoRAModelDefaultSettings.tsx @@ -3,7 +3,7 @@ import { useLoRAModelDefaultSettings } from 'features/modelManagerV2/hooks/useLo import { DefaultWeight } from 'features/modelManagerV2/subpanels/ModelPanel/LoRAModelDefaultSettings/DefaultWeight'; import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; import { toast } from 'features/toast/toast'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useEffect } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -30,6 +30,10 @@ export const LoRAModelDefaultSettings = memo(({ modelConfig }: Props) => { defaultValues: defaultSettingsDefaults, }); + useEffect(() => { + reset(defaultSettingsDefaults); + }, [defaultSettingsDefaults, reset]); + const onSubmit = useCallback>( (data) => { const body = { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx index 4fa0f29beb4..7f26febff8b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx @@ -7,7 +7,7 @@ import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainM import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { toast } from 'features/toast/toast'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -63,6 +63,10 @@ export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => { defaultValues: defaultSettingsDefaults, }); + useEffect(() => { + reset(defaultSettingsDefaults); + }, [defaultSettingsDefaults, reset]); + const onSubmit = useCallback>( (data) => { if (!selectedModelKey) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx index 50a7ee07c6f..d0e0d9efed7 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx @@ -20,7 +20,7 @@ const buildExportData = (modelConfig: AnyModelConfig): Record = data.trigger_phrases = modelConfig.trigger_phrases; } - if ('cpu_only' in modelConfig && modelConfig.cpu_only != null) { + if ('cpu_only' in modelConfig && modelConfig.cpu_only !== null) { data.cpu_only = modelConfig.cpu_only; } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx index 6d3cb733b44..5d23878d71a 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { toast } from 'features/toast/toast'; +import type { ChangeEvent } from 'react'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PiUploadSimpleBold } from 'react-icons/pi'; @@ -19,7 +20,7 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { async (data: Record) => { const body: Record = {}; - if ('default_settings' in data && data.default_settings != null) { + if ('default_settings' in data && data.default_settings !== null) { body.default_settings = data.default_settings; } @@ -27,7 +28,7 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { body.trigger_phrases = data.trigger_phrases; } - if ('cpu_only' in data && data.cpu_only != null) { + if ('cpu_only' in data && data.cpu_only !== null) { body.cpu_only = data.cpu_only; } @@ -59,7 +60,7 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { ); const handleFileChange = useCallback( - (e: React.ChangeEvent) => { + (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) { return; @@ -99,13 +100,7 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { tooltip={t('modelManager.importSettings')} onClick={handleClick} /> - + ); }); From c71a19c0e6fd488c717011d9cc6bbb42f40e9144 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sat, 28 Feb 2026 04:15:33 +0100 Subject: [PATCH 3/4] fix(ui): harden model settings export/import Prevent cross-model-type import errors by filtering imported fields against the target model's supported fields, showing clear warnings for incompatible or partially compatible settings instead of raw pydantic validation errors. Also fix falsy checks for empty arrays and objects in export, disable export button when nothing to export, add client-side validation and FileReader error handling on import. --- invokeai/frontend/web/public/locales/en.json | 2 + .../ModelPanel/ModelSettingsExportButton.tsx | 9 +- .../ModelPanel/ModelSettingsImportButton.tsx | 94 +++++++++++++++---- 3 files changed, 85 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a9927edaf3a..f941d729193 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1105,7 +1105,9 @@ "importSettings": "Import Settings", "settingsExported": "Model settings exported", "settingsImported": "Model settings imported", + "settingsImportedPartial": "Model settings partially imported. Incompatible settings were skipped: {{fields}}", "settingsImportFailed": "Failed to import model settings", + "settingsImportIncompatible": "The settings file contains no compatible settings for this model type", "settingsImportInvalidFile": "Invalid settings file" }, "models": { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx index d0e0d9efed7..a7f9b8957d5 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { toast } from 'features/toast/toast'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDownloadSimpleBold } from 'react-icons/pi'; import type { AnyModelConfig } from 'services/api/types'; @@ -12,11 +12,11 @@ type Props = { const buildExportData = (modelConfig: AnyModelConfig): Record => { const data: Record = {}; - if ('default_settings' in modelConfig && modelConfig.default_settings) { + if ('default_settings' in modelConfig && modelConfig.default_settings !== undefined && modelConfig.default_settings !== null) { data.default_settings = modelConfig.default_settings; } - if ('trigger_phrases' in modelConfig && modelConfig.trigger_phrases) { + if ('trigger_phrases' in modelConfig && modelConfig.trigger_phrases !== undefined && modelConfig.trigger_phrases !== null) { data.trigger_phrases = modelConfig.trigger_phrases; } @@ -34,6 +34,8 @@ const sanitizeFilename = (name: string): string => { export const ModelSettingsExportButton = memo(({ modelConfig }: Props) => { const { t } = useTranslation(); + const hasExportableData = useMemo(() => Object.keys(buildExportData(modelConfig)).length > 0, [modelConfig]); + const handleExport = useCallback(() => { const data = buildExportData(modelConfig); const json = JSON.stringify(data, null, 2); @@ -63,6 +65,7 @@ export const ModelSettingsExportButton = memo(({ modelConfig }: Props) => { aria-label={t('modelManager.exportSettings')} tooltip={t('modelManager.exportSettings')} onClick={handleExport} + isDisabled={!hasExportableData} /> ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx index 5d23878d71a..f45e3426b68 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx @@ -7,6 +7,34 @@ import { PiUploadSimpleBold } from 'react-icons/pi'; import { useUpdateModelMutation } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; +const validateImportData = (data: unknown): data is Record => { + if (typeof data !== 'object' || data === null || Array.isArray(data)) { + return false; + } + + const obj = data as Record; + + if ('trigger_phrases' in obj && obj.trigger_phrases !== undefined) { + if (!Array.isArray(obj.trigger_phrases) || !obj.trigger_phrases.every((p) => typeof p === 'string')) { + return false; + } + } + + if ('default_settings' in obj && obj.default_settings !== undefined) { + if (typeof obj.default_settings !== 'object' || obj.default_settings === null || Array.isArray(obj.default_settings)) { + return false; + } + } + + if ('cpu_only' in obj && obj.cpu_only !== undefined) { + if (typeof obj.cpu_only !== 'boolean') { + return false; + } + } + + return true; +}; + type Props = { modelConfig: AnyModelConfig; }; @@ -19,20 +47,29 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { const applySettings = useCallback( async (data: Record) => { const body: Record = {}; + const skippedFields: string[] = []; - if ('default_settings' in data && data.default_settings !== null) { - body.default_settings = data.default_settings; - } - - if ('trigger_phrases' in data && Array.isArray(data.trigger_phrases)) { - body.trigger_phrases = data.trigger_phrases; - } + const importableFields = ['default_settings', 'trigger_phrases', 'cpu_only'] as const; - if ('cpu_only' in data && data.cpu_only !== null) { - body.cpu_only = data.cpu_only; + for (const field of importableFields) { + if (!(field in data) || data[field] === undefined || data[field] === null) { + continue; + } + if (field in modelConfig) { + body[field] = data[field]; + } else { + skippedFields.push(field); + } } if (Object.keys(body).length === 0) { + if (skippedFields.length > 0) { + toast({ + id: 'SETTINGS_IMPORT_INCOMPATIBLE', + title: t('modelManager.settingsImportIncompatible'), + status: 'warning', + }); + } return; } @@ -42,21 +79,29 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { }) .unwrap() .then(() => { - toast({ - id: 'SETTINGS_IMPORTED', - title: t('modelManager.settingsImported'), - status: 'success', - }); + if (skippedFields.length > 0) { + toast({ + id: 'SETTINGS_IMPORTED', + title: t('modelManager.settingsImportedPartial', { fields: skippedFields.join(', ') }), + status: 'warning', + }); + } else { + toast({ + id: 'SETTINGS_IMPORTED', + title: t('modelManager.settingsImported'), + status: 'success', + }); + } }) - .catch((error) => { + .catch((_error) => { toast({ id: 'SETTINGS_IMPORT_FAILED', - title: `${t('modelManager.settingsImportFailed')}: ${error.data?.detail ?? ''}`, + title: t('modelManager.settingsImportFailed'), status: 'error', }); }); }, - [modelConfig.key, updateModel, t] + [modelConfig, updateModel, t] ); const handleFileChange = useCallback( @@ -70,6 +115,14 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { reader.onload = (event) => { try { const json = JSON.parse(event.target?.result as string); + if (!validateImportData(json)) { + toast({ + id: 'SETTINGS_IMPORT_INVALID', + title: t('modelManager.settingsImportInvalidFile'), + status: 'error', + }); + return; + } applySettings(json); } catch { toast({ @@ -79,6 +132,13 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => { }); } }; + reader.onerror = () => { + toast({ + id: 'SETTINGS_IMPORT_INVALID', + title: t('modelManager.settingsImportInvalidFile'), + status: 'error', + }); + }; reader.readAsText(file); // Reset the input so the same file can be re-selected From ec7467a1c03c9e42ce845b2a86ba52f7fc33c346 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sat, 28 Feb 2026 04:27:05 +0100 Subject: [PATCH 4/4] Chore pnpm fix --- .../ModelPanel/ModelSettingsExportButton.tsx | 12 ++++++++++-- .../ModelPanel/ModelSettingsImportButton.tsx | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx index a7f9b8957d5..c5f2ae0a0b1 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsExportButton.tsx @@ -12,11 +12,19 @@ type Props = { const buildExportData = (modelConfig: AnyModelConfig): Record => { const data: Record = {}; - if ('default_settings' in modelConfig && modelConfig.default_settings !== undefined && modelConfig.default_settings !== null) { + if ( + 'default_settings' in modelConfig && + modelConfig.default_settings !== undefined && + modelConfig.default_settings !== null + ) { data.default_settings = modelConfig.default_settings; } - if ('trigger_phrases' in modelConfig && modelConfig.trigger_phrases !== undefined && modelConfig.trigger_phrases !== null) { + if ( + 'trigger_phrases' in modelConfig && + modelConfig.trigger_phrases !== undefined && + modelConfig.trigger_phrases !== null + ) { data.trigger_phrases = modelConfig.trigger_phrases; } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx index f45e3426b68..474afcd74ea 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelSettingsImportButton.tsx @@ -21,7 +21,11 @@ const validateImportData = (data: unknown): data is Record => { } if ('default_settings' in obj && obj.default_settings !== undefined) { - if (typeof obj.default_settings !== 'object' || obj.default_settings === null || Array.isArray(obj.default_settings)) { + if ( + typeof obj.default_settings !== 'object' || + obj.default_settings === null || + Array.isArray(obj.default_settings) + ) { return false; } }