mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-03 00:35:32 -03:00
feat(downloads): allow manual checkpoint/diffusion root override in download modal
This commit is contained in:
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "Wählen Sie ein Stammverzeichnis",
|
"selectRootDirectory": "Wählen Sie ein Stammverzeichnis",
|
||||||
"selectModelRoot": "Modell-Stammverzeichnis auswählen:",
|
"selectModelRoot": "Modell-Stammverzeichnis auswählen:",
|
||||||
"selectTypeRoot": "{type}-Stammverzeichnis auswählen:",
|
"selectTypeRoot": "{type}-Stammverzeichnis auswählen:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "Zielordnerpfad:",
|
"targetFolderPath": "Zielordnerpfad:",
|
||||||
"browseFolders": "Ordner durchsuchen:",
|
"browseFolders": "Ordner durchsuchen:",
|
||||||
"createNewFolder": "Neuen Ordner erstellen",
|
"createNewFolder": "Neuen Ordner erstellen",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "Select a root directory",
|
"selectRootDirectory": "Select a root directory",
|
||||||
"selectModelRoot": "Select Model Root:",
|
"selectModelRoot": "Select Model Root:",
|
||||||
"selectTypeRoot": "Select {type} Root:",
|
"selectTypeRoot": "Select {type} Root:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "Destination type:",
|
||||||
|
"tooltip": "Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "Target Folder Path:",
|
"targetFolderPath": "Target Folder Path:",
|
||||||
"browseFolders": "Browse Folders:",
|
"browseFolders": "Browse Folders:",
|
||||||
"createNewFolder": "Create new folder",
|
"createNewFolder": "Create new folder",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "Selecciona un directorio raíz",
|
"selectRootDirectory": "Selecciona un directorio raíz",
|
||||||
"selectModelRoot": "Seleccionar raíz del modelo:",
|
"selectModelRoot": "Seleccionar raíz del modelo:",
|
||||||
"selectTypeRoot": "Seleccionar raíz de {type}:",
|
"selectTypeRoot": "Seleccionar raíz de {type}:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "Ruta de carpeta de destino:",
|
"targetFolderPath": "Ruta de carpeta de destino:",
|
||||||
"browseFolders": "Explorar carpetas:",
|
"browseFolders": "Explorar carpetas:",
|
||||||
"createNewFolder": "Crear nueva carpeta",
|
"createNewFolder": "Crear nueva carpeta",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "Sélectionner un répertoire racine",
|
"selectRootDirectory": "Sélectionner un répertoire racine",
|
||||||
"selectModelRoot": "Sélectionner la racine du modèle :",
|
"selectModelRoot": "Sélectionner la racine du modèle :",
|
||||||
"selectTypeRoot": "Sélectionner la racine {type} :",
|
"selectTypeRoot": "Sélectionner la racine {type} :",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "Chemin du dossier cible :",
|
"targetFolderPath": "Chemin du dossier cible :",
|
||||||
"browseFolders": "Parcourir les dossiers :",
|
"browseFolders": "Parcourir les dossiers :",
|
||||||
"createNewFolder": "Créer un nouveau dossier",
|
"createNewFolder": "Créer un nouveau dossier",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "בחר ספריית שורש",
|
"selectRootDirectory": "בחר ספריית שורש",
|
||||||
"selectModelRoot": "בחר שורש מודל:",
|
"selectModelRoot": "בחר שורש מודל:",
|
||||||
"selectTypeRoot": "בחר שורש {type}:",
|
"selectTypeRoot": "בחר שורש {type}:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "נתיב תיקיית יעד:",
|
"targetFolderPath": "נתיב תיקיית יעד:",
|
||||||
"browseFolders": "דפדף בתיקיות:",
|
"browseFolders": "דפדף בתיקיות:",
|
||||||
"createNewFolder": "צור תיקייה חדשה",
|
"createNewFolder": "צור תיקייה חדשה",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "ルートディレクトリを選択",
|
"selectRootDirectory": "ルートディレクトリを選択",
|
||||||
"selectModelRoot": "モデルルートを選択:",
|
"selectModelRoot": "モデルルートを選択:",
|
||||||
"selectTypeRoot": "{type}ルートを選択:",
|
"selectTypeRoot": "{type}ルートを選択:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "ターゲットフォルダパス:",
|
"targetFolderPath": "ターゲットフォルダパス:",
|
||||||
"browseFolders": "フォルダを参照:",
|
"browseFolders": "フォルダを参照:",
|
||||||
"createNewFolder": "新しいフォルダを作成",
|
"createNewFolder": "新しいフォルダを作成",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "루트 디렉토리를 선택하세요",
|
"selectRootDirectory": "루트 디렉토리를 선택하세요",
|
||||||
"selectModelRoot": "모델 루트 선택:",
|
"selectModelRoot": "모델 루트 선택:",
|
||||||
"selectTypeRoot": "{type} 루트 선택:",
|
"selectTypeRoot": "{type} 루트 선택:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "대상 폴더 경로:",
|
"targetFolderPath": "대상 폴더 경로:",
|
||||||
"browseFolders": "폴더 탐색:",
|
"browseFolders": "폴더 탐색:",
|
||||||
"createNewFolder": "새 폴더 만들기",
|
"createNewFolder": "새 폴더 만들기",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "Выберите корневую папку",
|
"selectRootDirectory": "Выберите корневую папку",
|
||||||
"selectModelRoot": "Выберите корень моделей:",
|
"selectModelRoot": "Выберите корень моделей:",
|
||||||
"selectTypeRoot": "Выберите корень {type}:",
|
"selectTypeRoot": "Выберите корень {type}:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "Путь к целевой папке:",
|
"targetFolderPath": "Путь к целевой папке:",
|
||||||
"browseFolders": "Обзор папок:",
|
"browseFolders": "Обзор папок:",
|
||||||
"createNewFolder": "Создать новую папку",
|
"createNewFolder": "Создать новую папку",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "选择根目录",
|
"selectRootDirectory": "选择根目录",
|
||||||
"selectModelRoot": "选择模型根目录:",
|
"selectModelRoot": "选择模型根目录:",
|
||||||
"selectTypeRoot": "选择 {type} 根目录:",
|
"selectTypeRoot": "选择 {type} 根目录:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "目标文件夹路径:",
|
"targetFolderPath": "目标文件夹路径:",
|
||||||
"browseFolders": "浏览文件夹:",
|
"browseFolders": "浏览文件夹:",
|
||||||
"createNewFolder": "新建文件夹",
|
"createNewFolder": "新建文件夹",
|
||||||
|
|||||||
@@ -1563,6 +1563,10 @@
|
|||||||
"selectRootDirectory": "選擇根目錄",
|
"selectRootDirectory": "選擇根目錄",
|
||||||
"selectModelRoot": "選擇模型根目錄:",
|
"selectModelRoot": "選擇模型根目錄:",
|
||||||
"selectTypeRoot": "選擇 {type} 根目錄:",
|
"selectTypeRoot": "選擇 {type} 根目錄:",
|
||||||
|
"routingOverride": {
|
||||||
|
"label": "[TODO: Translate] Destination type:",
|
||||||
|
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||||
|
},
|
||||||
"targetFolderPath": "目標資料夾路徑:",
|
"targetFolderPath": "目標資料夾路徑:",
|
||||||
"browseFolders": "瀏覽資料夾:",
|
"browseFolders": "瀏覽資料夾:",
|
||||||
"createNewFolder": "建立新資料夾",
|
"createNewFolder": "建立新資料夾",
|
||||||
|
|||||||
@@ -1088,3 +1088,56 @@
|
|||||||
[data-theme="dark"] .batch-preview-group-body {
|
[data-theme="dark"] .batch-preview-group-body {
|
||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Routing override segmented toggle (Checkpoint / Diffusion Model) shown on
|
||||||
|
the checkpoints page of the download modal when auto-routing can misdetect. */
|
||||||
|
.routing-override-group {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-option {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.75;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-option:first-child {
|
||||||
|
border-radius: var(--border-radius-xs) 0 0 var(--border-radius-xs);
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-option:last-child {
|
||||||
|
border-radius: 0 var(--border-radius-xs) var(--border-radius-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-option:not(.active):hover,
|
||||||
|
.routing-override-option:not(.active):focus-visible {
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.routing-override-option.active {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
opacity: 1;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export class DownloadManager {
|
|||||||
this.selectedFolder = '';
|
this.selectedFolder = '';
|
||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
this.useDefaultPath = false;
|
this.useDefaultPath = false;
|
||||||
|
// Manual checkpoint/diffusion root override on the location step:
|
||||||
|
// null follows the auto classification, true/false is user-picked.
|
||||||
|
this._routingOverride = null;
|
||||||
|
|
||||||
// Multi-file selection state: selectedFile stays the first selected
|
// Multi-file selection state: selectedFile stays the first selected
|
||||||
// file for backward compatibility with single-file flows (#1058).
|
// file for backward compatibility with single-file flows (#1058).
|
||||||
@@ -127,6 +130,11 @@ export class DownloadManager {
|
|||||||
// Default path toggle handler
|
// Default path toggle handler
|
||||||
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
||||||
|
|
||||||
|
// Checkpoint/diffusion routing override toggle (checkpoints page only)
|
||||||
|
document.querySelectorAll('#routingOverrideGroup .routing-override-option').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => this.handleRoutingOverrideChange(btn.dataset.routing === 'diffusion'));
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-append newline after pasting a URL so users can paste multiple URLs in succession
|
// Auto-append newline after pasting a URL so users can paste multiple URLs in succession
|
||||||
setupAutoNewlineOnPaste('modelUrl');
|
setupAutoNewlineOnPaste('modelUrl');
|
||||||
}
|
}
|
||||||
@@ -180,6 +188,7 @@ export class DownloadManager {
|
|||||||
this.selectedFiles = [];
|
this.selectedFiles = [];
|
||||||
this._lastDownloadError = null;
|
this._lastDownloadError = null;
|
||||||
this._isDiffusionModel = false;
|
this._isDiffusionModel = false;
|
||||||
|
this._routingOverride = null;
|
||||||
|
|
||||||
this.selectedFolder = '';
|
this.selectedFolder = '';
|
||||||
this.batchModels = [];
|
this.batchModels = [];
|
||||||
@@ -974,48 +983,12 @@ export class DownloadManager {
|
|||||||
try {
|
try {
|
||||||
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
||||||
this._otherSubType = await this._resolveOtherSubType();
|
this._otherSubType = await this._resolveOtherSubType();
|
||||||
|
// Each location-step entry starts from the auto classification;
|
||||||
|
// the user can override it via the checkpoint/diffusion toggle.
|
||||||
|
this._routingOverride = null;
|
||||||
|
this._syncRoutingOverrideToggle();
|
||||||
|
|
||||||
let rootsData;
|
await this._populateModelRoots();
|
||||||
if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') {
|
|
||||||
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
|
||||||
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
|
||||||
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
|
||||||
} else {
|
|
||||||
// An undecidable other sub_type (null) intentionally lands
|
|
||||||
// here: fetchModelRoots() lists all other roots so the user
|
|
||||||
// can pick manually.
|
|
||||||
rootsData = await this.apiClient.fetchModelRoots();
|
|
||||||
}
|
|
||||||
const modelRoot = document.getElementById('modelRoot');
|
|
||||||
modelRoot.innerHTML = rootsData.roots.map(root =>
|
|
||||||
`<option value="${root}">${root}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
let defaultRoot;
|
|
||||||
let subtypeDisplay;
|
|
||||||
if (this.apiClient.modelType === 'other') {
|
|
||||||
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
|
||||||
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
|
||||||
subtypeDisplay = this._otherSubType
|
|
||||||
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
|
||||||
: this.apiClient.apiConfig.config.displayName;
|
|
||||||
} else {
|
|
||||||
const singularType = this._isDiffusionModel
|
|
||||||
? 'unet'
|
|
||||||
: this.apiClient.modelType.replace(/s$/, '');
|
|
||||||
const defaultRootKey = `default_${singularType}_root`;
|
|
||||||
defaultRoot = state.global.settings[defaultRootKey];
|
|
||||||
subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
|
||||||
}
|
|
||||||
console.log('Default root:', defaultRoot);
|
|
||||||
console.log('Available roots:', rootsData.roots);
|
|
||||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
|
||||||
console.log(`Setting default root: ${defaultRoot}`);
|
|
||||||
modelRoot.value = defaultRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('modelRootLabel').textContent =
|
|
||||||
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
|
||||||
|
|
||||||
// Set autocomplete="off" on folderPath input
|
// Set autocomplete="off" on folderPath input
|
||||||
const folderPathInput = document.getElementById('folderPath');
|
const folderPathInput = document.getElementById('folderPath');
|
||||||
@@ -1035,6 +1008,7 @@ export class DownloadManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Setup model root change handler
|
// Setup model root change handler
|
||||||
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
modelRoot.addEventListener('change', async () => {
|
modelRoot.addEventListener('change', async () => {
|
||||||
await this.initializeFolderTree();
|
await this.initializeFolderTree();
|
||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
@@ -1049,6 +1023,116 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effective checkpoint/diffusion routing for the location step: the
|
||||||
|
* user's manual override when set, otherwise the auto classification.
|
||||||
|
*/
|
||||||
|
_getEffectiveIsDiffusionModel() {
|
||||||
|
return this._routingOverride ?? this._isDiffusionModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the root list for the effective routing group and repopulate
|
||||||
|
* the root dropdown, preselecting the matching default root and
|
||||||
|
* updating the root label.
|
||||||
|
*/
|
||||||
|
async _populateModelRoots() {
|
||||||
|
const isDiffusion = this._getEffectiveIsDiffusionModel();
|
||||||
|
|
||||||
|
let rootsData;
|
||||||
|
if (isDiffusion && this.apiClient.modelType === 'checkpoints') {
|
||||||
|
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
||||||
|
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
||||||
|
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
||||||
|
} else {
|
||||||
|
// An undecidable other sub_type (null) intentionally lands
|
||||||
|
// here: fetchModelRoots() lists all other roots so the user
|
||||||
|
// can pick manually.
|
||||||
|
rootsData = await this.apiClient.fetchModelRoots();
|
||||||
|
}
|
||||||
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
|
modelRoot.innerHTML = rootsData.roots.map(root =>
|
||||||
|
`<option value="${root}">${root}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
let defaultRoot;
|
||||||
|
let subtypeDisplay;
|
||||||
|
if (this.apiClient.modelType === 'other') {
|
||||||
|
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
||||||
|
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
||||||
|
subtypeDisplay = this._otherSubType
|
||||||
|
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
||||||
|
: this.apiClient.apiConfig.config.displayName;
|
||||||
|
} else {
|
||||||
|
const singularType = isDiffusion
|
||||||
|
? 'unet'
|
||||||
|
: this.apiClient.modelType.replace(/s$/, '');
|
||||||
|
const defaultRootKey = `default_${singularType}_root`;
|
||||||
|
defaultRoot = state.global.settings[defaultRootKey];
|
||||||
|
subtypeDisplay = isDiffusion ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||||
|
}
|
||||||
|
console.log('Default root:', defaultRoot);
|
||||||
|
console.log('Available roots:', rootsData.roots);
|
||||||
|
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||||
|
console.log(`Setting default root: ${defaultRoot}`);
|
||||||
|
modelRoot.value = defaultRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modelRootLabel').textContent =
|
||||||
|
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the checkpoint/diffusion override toggle on the checkpoints page
|
||||||
|
* (hidden elsewhere) and reflect the effective routing on its options.
|
||||||
|
*/
|
||||||
|
_syncRoutingOverrideToggle() {
|
||||||
|
const group = document.getElementById('routingOverrideGroup');
|
||||||
|
if (!group) return;
|
||||||
|
const isCheckpoints = this.apiClient.modelType === 'checkpoints';
|
||||||
|
group.style.display = isCheckpoints ? '' : 'none';
|
||||||
|
if (!isCheckpoints) return;
|
||||||
|
|
||||||
|
const effective = this._getEffectiveIsDiffusionModel();
|
||||||
|
group.querySelectorAll('.routing-override-option').forEach(btn => {
|
||||||
|
const active = (btn.dataset.routing === 'diffusion') === effective;
|
||||||
|
btn.classList.toggle('active', active);
|
||||||
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user switching the checkpoint/diffusion routing toggle.
|
||||||
|
* The override must never be silently ignored: the backend re-derives
|
||||||
|
* the routing group only under "use default path", so overriding turns
|
||||||
|
* that off for this session (the localStorage preference is left
|
||||||
|
* untouched) to guarantee the picked root is the actual destination.
|
||||||
|
*/
|
||||||
|
async handleRoutingOverrideChange(isDiffusion) {
|
||||||
|
if (this.apiClient.modelType !== 'checkpoints') return;
|
||||||
|
|
||||||
|
const overridden = isDiffusion !== this._isDiffusionModel;
|
||||||
|
this._routingOverride = overridden ? isDiffusion : null;
|
||||||
|
|
||||||
|
if (overridden && this.useDefaultPath) {
|
||||||
|
this.useDefaultPath = false;
|
||||||
|
const toggleInput = document.getElementById('useDefaultPath');
|
||||||
|
if (toggleInput) {
|
||||||
|
toggleInput.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._syncRoutingOverrideToggle();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._populateModelRoots();
|
||||||
|
await this.initializeFolderTree();
|
||||||
|
this.updatePathSelectionUI();
|
||||||
|
} catch (error) {
|
||||||
|
showToast('toast.downloads.loadError', { message: error.message }, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide whether this download routes to the diffusion model (unet)
|
* Decide whether this download routes to the diffusion model (unet)
|
||||||
* roots rather than the checkpoint roots. The backend owns the routing
|
* roots rather than the checkpoint roots. The backend owns the routing
|
||||||
@@ -2554,13 +2638,13 @@ export class DownloadManager {
|
|||||||
const modelRoot = document.getElementById('modelRoot').value;
|
const modelRoot = document.getElementById('modelRoot').value;
|
||||||
const config = this.apiClient.apiConfig.config;
|
const config = this.apiClient.apiConfig.config;
|
||||||
|
|
||||||
const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : config.displayName;
|
const subtypeDisplay = this._getEffectiveIsDiffusionModel() ? 'Diffusion Model' : config.displayName;
|
||||||
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||||
|
|
||||||
if (modelRoot) {
|
if (modelRoot) {
|
||||||
if (this.useDefaultPath) {
|
if (this.useDefaultPath) {
|
||||||
try {
|
try {
|
||||||
const singularType = this._isDiffusionModel
|
const singularType = this._getEffectiveIsDiffusionModel()
|
||||||
? 'unet'
|
? 'unet'
|
||||||
: this.apiClient.modelType.replace(/s$/, '');
|
: this.apiClient.modelType.replace(/s$/, '');
|
||||||
const templates = state.global?.settings?.download_path_templates;
|
const templates = state.global?.settings?.download_path_templates;
|
||||||
|
|||||||
@@ -80,6 +80,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Root Selection (always visible) -->
|
<!-- Model Root Selection (always visible) -->
|
||||||
|
<div class="input-group routing-override-group" id="routingOverrideGroup" style="display: none;"
|
||||||
|
title="{{ t('modals.download.routingOverride.tooltip') }}">
|
||||||
|
<label id="routingOverrideLabel">{{ t('modals.download.routingOverride.label') }}</label>
|
||||||
|
<div class="routing-override-toggle" role="group" aria-label="{{ t('modals.download.routingOverride.label') }}">
|
||||||
|
<button type="button" class="routing-override-option" data-routing="checkpoint">{{ t('checkpoints.modelTypes.checkpoint') }}</button>
|
||||||
|
<button type="button" class="routing-override-option" data-routing="diffusion">{{ t('checkpoints.modelTypes.diffusion_model') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="modelRoot" id="modelRootLabel">{{ t('modals.download.selectModelRoot') }}</label>
|
<label for="modelRoot" id="modelRootLabel">{{ t('modals.download.selectModelRoot') }}</label>
|
||||||
<select id="modelRoot"></select>
|
<select id="modelRoot"></select>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
DOWNLOAD_MANAGER_MODULE,
|
||||||
|
MODAL_MANAGER_MODULE,
|
||||||
|
UI_HELPERS_MODULE,
|
||||||
|
STATE_MODULE,
|
||||||
|
LOADING_MANAGER_MODULE,
|
||||||
|
API_FACTORY_MODULE,
|
||||||
|
STORAGE_HELPERS_MODULE,
|
||||||
|
FOLDER_TREE_MANAGER_MODULE,
|
||||||
|
I18N_HELPERS_MODULE,
|
||||||
|
SUMMARY_MODULE,
|
||||||
|
OTHER_MODELS_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname,
|
||||||
|
MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname,
|
||||||
|
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||||
|
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
|
||||||
|
LOADING_MANAGER_MODULE: new URL('../../../static/js/managers/LoadingManager.js', import.meta.url).pathname,
|
||||||
|
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
|
||||||
|
STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
|
||||||
|
FOLDER_TREE_MANAGER_MODULE: new URL('../../../static/js/components/FolderTreeManager.js', import.meta.url).pathname,
|
||||||
|
I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||||
|
SUMMARY_MODULE: new URL('../../../static/js/components/DownloadBatchSummaryModal.js', import.meta.url).pathname,
|
||||||
|
OTHER_MODELS_MODULE: new URL('../../../static/js/utils/otherModels.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(MODAL_MANAGER_MODULE, () => ({
|
||||||
|
modalManager: { showModal: vi.fn(), closeModal: vi.fn() },
|
||||||
|
}));
|
||||||
|
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||||
|
showToast: vi.fn(),
|
||||||
|
showActionToast: vi.fn(),
|
||||||
|
setupAutoNewlineOnPaste: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock(STATE_MODULE, () => ({
|
||||||
|
state: { global: { settings: {} }, loadingManager: {} },
|
||||||
|
}));
|
||||||
|
vi.mock(LOADING_MANAGER_MODULE, () => ({
|
||||||
|
LoadingManager: vi.fn(() => ({})),
|
||||||
|
}));
|
||||||
|
vi.mock(API_FACTORY_MODULE, () => ({
|
||||||
|
getModelApiClient: vi.fn(),
|
||||||
|
resetAndReload: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock(STORAGE_HELPERS_MODULE, () => ({
|
||||||
|
getStorageItem: vi.fn((_key, defaultValue) => defaultValue),
|
||||||
|
setStorageItem: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock(FOLDER_TREE_MANAGER_MODULE, () => ({
|
||||||
|
FolderTreeManager: vi.fn(() => ({})),
|
||||||
|
}));
|
||||||
|
vi.mock(I18N_HELPERS_MODULE, () => ({
|
||||||
|
translate: vi.fn((_key, _vars, fallback) => fallback ?? ''),
|
||||||
|
}));
|
||||||
|
vi.mock(SUMMARY_MODULE, () => ({
|
||||||
|
showDownloadBatchSummary: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock(OTHER_MODELS_MODULE, () => ({
|
||||||
|
enableOtherModels: vi.fn(),
|
||||||
|
openOtherModelsSettings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE);
|
||||||
|
const { state } = await import(STATE_MODULE);
|
||||||
|
const { setStorageItem } = await import(STORAGE_HELPERS_MODULE);
|
||||||
|
|
||||||
|
describe('DownloadManager checkpoint/diffusion routing override', () => {
|
||||||
|
let manager;
|
||||||
|
|
||||||
|
function setupDom() {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="routingOverrideGroup" style="display:none">
|
||||||
|
<button class="routing-override-option" data-routing="checkpoint"></button>
|
||||||
|
<button class="routing-override-option" data-routing="diffusion"></button>
|
||||||
|
</div>
|
||||||
|
<select id="modelRoot"></select>
|
||||||
|
<label id="modelRootLabel"></label>
|
||||||
|
<input id="folderPath" />
|
||||||
|
<input type="checkbox" id="useDefaultPath" />
|
||||||
|
<div id="manualPathSelection"></div>
|
||||||
|
<div id="targetPathDisplay"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createManager(modelType, displayName = 'Model') {
|
||||||
|
manager = new DownloadManager();
|
||||||
|
manager.apiClient = {
|
||||||
|
modelType,
|
||||||
|
apiConfig: { config: { displayName } },
|
||||||
|
fetchModelRoots: vi.fn(),
|
||||||
|
};
|
||||||
|
manager.selectedFile = null;
|
||||||
|
manager.selectedFiles = [];
|
||||||
|
manager.currentVersion = null;
|
||||||
|
manager.initializeFolderTree = vi.fn().mockResolvedValue();
|
||||||
|
manager.folderTreeManager = { init: vi.fn(), getSelectedPath: vi.fn(() => '') };
|
||||||
|
manager.loadDefaultPathSetting = vi.fn();
|
||||||
|
vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupDom();
|
||||||
|
state.global.settings = {};
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the toggle for diffusion-routed checkpoints and marks diffusion active', async () => {
|
||||||
|
createManager('checkpoints', 'Checkpoint');
|
||||||
|
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(true);
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/unet'],
|
||||||
|
});
|
||||||
|
state.global.settings.default_unet_root = '/models/unet';
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
const group = document.getElementById('routingOverrideGroup');
|
||||||
|
expect(group.style.display).not.toBe('none');
|
||||||
|
const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option');
|
||||||
|
expect(diffusionBtn.classList.contains('active')).toBe(true);
|
||||||
|
expect(diffusionBtn.getAttribute('aria-pressed')).toBe('true');
|
||||||
|
expect(checkpointBtn.classList.contains('active')).toBe(false);
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith('diffusion_model');
|
||||||
|
expect(document.getElementById('modelRoot').value).toBe('/models/unet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the toggle hidden on non-checkpoints pages', async () => {
|
||||||
|
createManager('loras', 'LoRA');
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/loras'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
expect(document.getElementById('routingOverrideGroup').style.display).toBe('none');
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-fetches diffusion roots and preselects the unet default when overriding to diffusion', async () => {
|
||||||
|
createManager('checkpoints', 'Checkpoint');
|
||||||
|
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||||
|
manager.apiClient.fetchModelRoots
|
||||||
|
.mockResolvedValueOnce({ success: true, roots: ['/models/checkpoints'] })
|
||||||
|
.mockResolvedValueOnce({ success: true, roots: ['/models/unet'] });
|
||||||
|
state.global.settings.default_unet_root = '/models/unet';
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
expect(document.getElementById('modelRoot').value).toBe('/models/checkpoints');
|
||||||
|
|
||||||
|
await manager.handleRoutingOverrideChange(true);
|
||||||
|
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenLastCalledWith('diffusion_model');
|
||||||
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
|
expect(Array.from(modelRoot.options).map(o => o.value)).toEqual(['/models/unet']);
|
||||||
|
expect(modelRoot.value).toBe('/models/unet');
|
||||||
|
const group = document.getElementById('routingOverrideGroup');
|
||||||
|
const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option');
|
||||||
|
expect(checkpointBtn.classList.contains('active')).toBe(false);
|
||||||
|
expect(diffusionBtn.classList.contains('active')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns off Use Default Path for the session without persisting when overriding', async () => {
|
||||||
|
createManager('checkpoints', 'Checkpoint');
|
||||||
|
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/checkpoints'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
manager.useDefaultPath = true;
|
||||||
|
document.getElementById('useDefaultPath').checked = true;
|
||||||
|
|
||||||
|
await manager.handleRoutingOverrideChange(true);
|
||||||
|
|
||||||
|
expect(manager.useDefaultPath).toBe(false);
|
||||||
|
expect(document.getElementById('useDefaultPath').checked).toBe(false);
|
||||||
|
expect(setStorageItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the override when switching back to the auto-classified side', async () => {
|
||||||
|
createManager('checkpoints', 'Checkpoint');
|
||||||
|
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/checkpoints'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
await manager.handleRoutingOverrideChange(true);
|
||||||
|
expect(manager._routingOverride).toBe(true);
|
||||||
|
|
||||||
|
manager.apiClient.fetchModelRoots.mockClear();
|
||||||
|
await manager.handleRoutingOverrideChange(false);
|
||||||
|
|
||||||
|
expect(manager._routingOverride).toBeNull();
|
||||||
|
// Back on the checkpoint group: no routing argument
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user