Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_scan_wizard.js
T

864 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Scan wizard UI and scan launch/polling coordination.
*/
import { translate } from './locales.js';
import { updateScanProgress, finishScanProgress, failScanProgress } from './scan_progress.js';
import { configureSidebarAction } from './sidebar_actions.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
const t = (key, params) => translate(key, params);
const SCAN_RADAR_ICON_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21" stroke-opacity="0.35"/><line x1="3" y1="12" x2="21" y2="12" stroke-opacity="0.35"/><line x1="12" y1="12" x2="18.5" y2="5.5" stroke-width="2"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>`;
export function setScanButtonState(btn, isScanning) {
if (!btn) return;
btn.innerHTML = SCAN_RADAR_ICON_SVG;
btn.classList.toggle('anomalous-radar-spinning', Boolean(isScanning));
btn.style.opacity = isScanning ? '0.85' : '1';
btn.style.animation = '';
configureSidebarAction(btn);
}
function setActiveScanButtonState(isScanning) {
setScanButtonState(document.getElementById('anomalous-scan-btn'), isScanning);
}
export function openScanWizard({ isGlobal = false, targetFiles = null } = {}) {
let wizard = document.getElementById('anomalous-wizard-modal');
if (wizard) document.body.removeChild(wizard);
wizard = document.createElement('div');
wizard.id = 'anomalous-wizard-modal';
wizard.style.position = 'fixed';
wizard.style.top = '0';
wizard.style.left = '0';
wizard.style.width = '100vw';
wizard.style.height = '100vh';
wizard.style.backgroundColor = 'rgba(0,0,0,0.6)';
wizard.style.zIndex = '999999';
wizard.style.display = 'flex';
wizard.style.justifyContent = 'center';
wizard.style.alignItems = 'center';
wizard.style.fontFamily = 'Roboto, "Segoe UI", sans-serif';
const content = document.createElement('div');
content.style.background = '#1E1E1E';
content.style.borderRadius = '12px';
content.style.padding = '32px';
content.style.width = '760px';
content.style.maxWidth = '95%';
content.style.maxHeight = '90vh';
content.style.overflowY = 'auto';
content.style.boxShadow = '0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12)';
content.style.color = '#fff';
content.style.position = 'relative';
// Top Header Area
const headerArea = document.createElement('div');
headerArea.style.display = 'flex';
headerArea.style.justifyContent = 'space-between';
headerArea.style.alignItems = 'center';
headerArea.style.marginBottom = '24px';
const title = document.createElement('h2');
if (targetFiles) {
title.textContent = t('sidebarSingleModelScan', { files: targetFiles });
} else {
title.textContent = t(isGlobal ? 'sidebarGlobalScanWizard' : 'sidebarScanWizardTitle');
}
title.style.margin = '0';
title.style.fontSize = '1.6em';
title.style.fontWeight = '500';
// Toolbar right
const topToolbar = document.createElement('div');
topToolbar.style.display = 'flex';
topToolbar.style.gap = '8px';
const createGhostBtn = (icon, textKey, onClick) => {
const btn = document.createElement('button');
btn.textContent = `${icon} ${t(textKey)}`;
btn.style.padding = '6px 12px';
btn.style.background = 'transparent';
btn.style.color = '#ccc';
btn.style.border = '1px solid rgba(255,255,255,0.1)';
btn.style.borderRadius = '6px';
btn.style.cursor = 'pointer';
btn.style.fontSize = '0.85em';
btn.style.transition = 'all 0.2s';
btn.onmouseover = () => { btn.style.background = 'rgba(255,255,255,0.08)'; btn.style.color = '#fff'; };
btn.onmouseout = () => { btn.style.background = 'transparent'; btn.style.color = '#ccc'; };
btn.onclick = onClick;
return btn;
};
const apiKeyBtn = createGhostBtn('🔑', 'sidebarApiKey', () => {
const newKey = prompt(t('sidebarApiKeyPrompt'), '');
if (newKey !== null) {
fetch('/anomalous/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: newKey.trim() })
}).then(res => res.json()).then(data => {
if (data.status !== 'ok') throw new Error(data.message || 'Save failed');
alert(t('sidebarKeySaved'));
}).catch(error => alert(t('sidebarSaveFailed') + error.message));
}
});
topToolbar.appendChild(apiKeyBtn);
headerArea.appendChild(title);
headerArea.appendChild(topToolbar);
content.appendChild(headerArea);
let scanMode = 'civitai';
let enableRename = true;
let enableAutoCheck = true;
let enableForceOverwrite = false;
let updateSections = () => { };
const formGroup = document.createElement('div');
formGroup.style.display = 'flex';
formGroup.style.flexDirection = 'column';
formGroup.style.gap = '24px';
const createChoiceCard = (id, icon, titleKey, descKey, isSelected) => {
const card = document.createElement('div');
card.style.flex = '1';
card.style.background = isSelected ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.04)';
card.style.border = `2px solid ${isSelected ? '#e5e7eb' : 'transparent'}`;
card.style.borderRadius = '8px';
card.style.padding = '16px';
card.style.cursor = 'pointer';
card.style.transition = 'all 0.2s';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.gap = '12px';
card.style.alignItems = 'flex-start';
card.onmouseover = () => {
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.08)';
};
card.onmouseout = () => {
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.04)';
};
const topRow = document.createElement('div');
topRow.style.display = 'flex';
topRow.style.alignItems = 'center';
topRow.style.gap = '12px';
const iconDiv = document.createElement('div');
iconDiv.innerText = icon;
iconDiv.style.fontSize = '1.6em';
const tTitle = document.createElement('div');
tTitle.textContent = t(titleKey);
tTitle.style.fontWeight = '500';
tTitle.style.fontSize = '1.05em';
tTitle.style.color = isSelected ? '#ffffff' : '#e5e7eb';
topRow.appendChild(iconDiv);
topRow.appendChild(tTitle);
const tDesc = document.createElement('div');
tDesc.textContent = t(descKey);
tDesc.style.fontSize = '0.85em';
tDesc.style.color = '#9ca3af';
tDesc.style.lineHeight = '1.5';
card.appendChild(topRow);
card.appendChild(tDesc);
return card;
};
let targetMode = 'all';
let selectedForScan = new Map();
if (!targetFiles) {
const section0 = document.createElement('div');
const sec0Title = document.createElement('div');
sec0Title.textContent = t('sidebarStep0');
sec0Title.style.fontWeight = '500';
sec0Title.style.color = '#e5e7eb';
sec0Title.style.marginBottom = '12px';
sec0Title.style.fontSize = '0.95em';
section0.appendChild(sec0Title);
const tContainer = document.createElement('div');
tContainer.style.display = 'flex';
tContainer.style.flexDirection = 'row';
tContainer.style.gap = '16px';
tContainer.style.marginBottom = '16px';
const customActionDiv = document.createElement('div');
customActionDiv.style.display = 'none';
customActionDiv.style.marginTop = '16px';
const openSelectorBtn = document.createElement('button');
openSelectorBtn.textContent = t('sidebarOpenSelector');
openSelectorBtn.style.cssText = 'width:100%;padding:12px;background:#e5e7eb;color:#111;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:1.05em;box-shadow:0 2px 4px rgba(0,0,0,0.2);';
const selectedCountSpan = document.createElement('div');
selectedCountSpan.style.cssText = 'text-align:center;color:#e5e7eb;margin-top:8px;font-size:0.9em;';
selectedCountSpan.textContent = t('sidebarSelectedZero');
const updateSelectedCount = () => {
let total = 0;
for (const set of selectedForScan.values()) total += set.size;
selectedCountSpan.textContent = t('sidebarSelectedCount', { count: total });
};
openSelectorBtn.onclick = () => {
this._openAdvancedModelSelector(selectedForScan, (newSelection) => {
selectedForScan = newSelection;
updateSelectedCount();
});
};
customActionDiv.appendChild(openSelectorBtn);
customActionDiv.appendChild(selectedCountSpan);
let tCard1, tCard2;
const updateTargetCards = () => {
if (tCard1 && tCard2) {
tContainer.removeChild(tCard1);
tContainer.removeChild(tCard2);
}
tCard1 = createChoiceCard('all', '📂', 'sidebarTargetGlobal', 'sidebarTargetGlobalDesc', targetMode === 'all');
tCard2 = createChoiceCard('custom', '☑️', 'sidebarTargetCustom', 'sidebarTargetCustomDesc', targetMode === 'custom');
tCard1.onclick = () => {
targetMode = 'all';
updateTargetCards();
customActionDiv.style.display = 'none';
};
tCard2.onclick = () => {
targetMode = 'custom';
updateTargetCards();
customActionDiv.style.display = 'block';
};
tContainer.appendChild(tCard1);
tContainer.appendChild(tCard2);
};
updateTargetCards();
section0.appendChild(tContainer);
section0.appendChild(customActionDiv);
formGroup.appendChild(section0);
}
// === Step 1: Fetch Data ===
const section1 = document.createElement('div');
const sec1Title = document.createElement('div');
sec1Title.textContent = t('sidebarStep1');
sec1Title.style.fontWeight = '500';
sec1Title.style.color = '#e5e7eb';
sec1Title.style.marginBottom = '12px';
sec1Title.style.fontSize = '0.95em';
section1.appendChild(sec1Title);
const cardsContainer = document.createElement('div');
cardsContainer.style.display = 'flex';
cardsContainer.style.flexDirection = 'row';
cardsContainer.style.gap = '16px';
let card1, card2;
const updateCards = () => {
if (card1 && card2) {
cardsContainer.removeChild(card1);
cardsContainer.removeChild(card2);
}
card1 = createChoiceCard('civitai', '🌍', 'sidebarOnline', 'sidebarOnlineDesc', scanMode === 'civitai');
card2 = createChoiceCard('offline', '🔌', 'sidebarOffline', 'sidebarOfflineDesc', scanMode === 'offline');
card1.onclick = () => { scanMode = 'civitai'; updateCards(); updateSections(); };
card2.onclick = () => { scanMode = 'offline'; updateCards(); updateSections(); };
cardsContainer.appendChild(card1);
cardsContainer.appendChild(card2);
};
updateCards();
section1.appendChild(cardsContainer);
formGroup.appendChild(section1);
// Material Switch Builder
const createMaterialSwitch = (initialState, onChange) => {
const track = document.createElement('div');
track.style.width = '36px';
track.style.height = '14px';
track.style.borderRadius = '7px';
track.style.background = initialState ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255,255,255,0.15)';
track.style.position = 'relative';
track.style.cursor = 'pointer';
track.style.transition = 'background 0.3s';
track.style.display = 'flex';
track.style.alignItems = 'center';
const thumb = document.createElement('div');
thumb.style.width = '20px';
thumb.style.height = '20px';
thumb.style.borderRadius = '50%';
thumb.style.background = initialState ? '#ffffff' : '#888888';
thumb.style.position = 'absolute';
thumb.style.left = initialState ? '16px' : '0px';
thumb.style.transition = 'left 0.3s, background 0.3s';
thumb.style.boxShadow = '0 1px 3px rgba(0,0,0,0.4)';
let state = initialState;
track.onclick = () => {
state = !state;
track.style.background = state ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255,255,255,0.15)';
thumb.style.background = state ? '#ffffff' : '#888888';
thumb.style.left = state ? '16px' : '0px';
onChange(state);
};
track.appendChild(thumb);
return track;
};
const createListRow = (icon, titleKey, descKey, actionEl) => {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '12px 0';
row.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'flex-start';
left.style.gap = '16px';
const iconEl = document.createElement('div');
if (typeof icon === 'string' && icon.startsWith('<')) {
iconEl.innerHTML = icon;
} else {
iconEl.innerText = icon;
}
iconEl.style.fontSize = '1.4em';
iconEl.style.lineHeight = '1.2';
iconEl.style.width = '24px';
iconEl.style.textAlign = 'center';
const textDiv = document.createElement('div');
const titleEl = document.createElement('div');
titleEl.textContent = t(titleKey);
titleEl.style.fontWeight = '500';
titleEl.style.fontSize = '1.0em';
titleEl.style.color = '#fff';
const d = document.createElement('div');
d.innerHTML = t(descKey);
d.style.fontSize = '0.85em';
d.style.color = '#aaa';
d.style.marginTop = '4px';
textDiv.appendChild(titleEl);
textDiv.appendChild(d);
left.appendChild(iconEl);
left.appendChild(textDiv);
row.appendChild(left);
if (actionEl) row.appendChild(actionEl);
return row;
};
// === Step 2: Normalize Naming ===
const section2 = document.createElement('div');
const sec2Title = document.createElement('div');
sec2Title.textContent = t('sidebarStep2');
sec2Title.style.fontWeight = '500';
sec2Title.style.color = '#e5e7eb';
sec2Title.style.marginBottom = '8px';
sec2Title.style.fontSize = '0.95em';
section2.appendChild(sec2Title);
const s2List = document.createElement('div');
let enableVirtualRename = true;
let enablePhysicalRename = false;
const dualChannelRow = document.createElement('div');
dualChannelRow.style.display = 'flex';
dualChannelRow.style.flexDirection = 'column';
dualChannelRow.style.gap = '14px';
dualChannelRow.style.padding = '0 16px 12px 16px';
dualChannelRow.style.marginLeft = '40px';
const updateDualChannelUI = () => {
dualChannelRow.style.opacity = enableRename ? '1' : '0.4';
dualChannelRow.style.pointerEvents = enableRename ? 'auto' : 'none';
};
const virtualSwitch = createMaterialSwitch(enableVirtualRename, (s) => enableVirtualRename = s);
let physicalProtectionNotice = null;
const physicalSwitch = createMaterialSwitch(enablePhysicalRename, (s) => {
enablePhysicalRename = s;
if (physicalProtectionNotice) {
physicalProtectionNotice.style.display = s ? 'block' : 'none';
}
});
const vContainer = document.createElement('div');
vContainer.style.display = 'flex';
vContainer.style.flexDirection = 'column';
const vRow = document.createElement('div');
vRow.style.display = 'flex';
vRow.style.alignItems = 'center';
vRow.style.gap = '8px';
vRow.innerHTML = `<span style="font-size:0.9em; color:#ddd;">✨ ${t('sidebarVirtualRename')}</span>`;
vRow.appendChild(virtualSwitch);
const vDesc = document.createElement('div');
vDesc.style.fontSize = '0.8em';
vDesc.style.color = '#888';
vDesc.style.marginTop = '4px';
vDesc.textContent = t('sidebarVirtualRenameDesc');
vContainer.appendChild(vRow);
vContainer.appendChild(vDesc);
const pContainer = document.createElement('div');
pContainer.style.display = 'flex';
pContainer.style.flexDirection = 'column';
const pRow = document.createElement('div');
pRow.style.display = 'flex';
pRow.style.alignItems = 'center';
pRow.style.gap = '8px';
pRow.innerHTML = `<span style="font-size:0.9em; color:#ddd; display:inline-flex; align-items:center; gap:6px;"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('sidebarPhysicalRename')}</span>`;
pRow.appendChild(physicalSwitch);
const pDesc = document.createElement('div');
pDesc.style.fontSize = '0.8em';
pDesc.style.color = '#888';
pDesc.style.marginTop = '4px';
pDesc.innerHTML = t('sidebarPhysicalRenameDesc');
pContainer.appendChild(pRow);
pContainer.appendChild(pDesc);
physicalProtectionNotice = document.createElement('div');
physicalProtectionNotice.style.display = enablePhysicalRename ? 'block' : 'none';
physicalProtectionNotice.style.marginTop = '8px';
physicalProtectionNotice.style.padding = '9px 11px';
physicalProtectionNotice.style.border = '1px solid rgba(251, 188, 4, 0.45)';
physicalProtectionNotice.style.borderRadius = '6px';
physicalProtectionNotice.style.background = 'rgba(251, 188, 4, 0.08)';
physicalProtectionNotice.style.color = '#fdd663';
physicalProtectionNotice.style.fontSize = '0.8em';
physicalProtectionNotice.style.lineHeight = '1.45';
physicalProtectionNotice.textContent = t('sidebarPhysicalRenameProtectedNotice');
pContainer.appendChild(physicalProtectionNotice);
dualChannelRow.appendChild(vContainer);
dualChannelRow.appendChild(pContainer);
const editSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
s2List.appendChild(createListRow(editSvg, 'sidebarNormalize', 'sidebarNormalizeDesc', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); })));
s2List.lastChild.style.borderBottom = 'none';
s2List.appendChild(dualChannelRow);
updateDualChannelUI();
section2.appendChild(s2List);
formGroup.appendChild(section2);
// === Step 3: Workflow Protection & Fix ===
const section3 = document.createElement('div');
const sec3Title = document.createElement('div');
sec3Title.textContent = t('sidebarStep3');
sec3Title.style.fontWeight = '500';
sec3Title.style.color = '#e5e7eb';
sec3Title.style.marginBottom = '8px';
sec3Title.style.fontSize = '0.95em';
section3.appendChild(sec3Title);
const s3List = document.createElement('div');
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
const cubeSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
const warnSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#f59e0b;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
const wandSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#8b5cf6;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
s3List.appendChild(createListRow(cubeSvg, 'sidebarProvenance', 'sidebarProvenanceDesc', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
s3List.appendChild(createListRow(warnSvg, 'sidebarOverwrite', 'sidebarOverwriteDesc', createMaterialSwitch(enableForceOverwrite, (s) => enableForceOverwrite = s)));
s3List.appendChild(createListRow(wandSvg, 'sidebarSmartFix', 'sidebarSmartFixDesc', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s)));
s3List.lastChild.style.borderBottom = 'none';
section3.appendChild(s3List);
formGroup.appendChild(section3);
content.appendChild(formGroup);
updateSections = () => {
if (scanMode === 'offline') {
section2.style.opacity = '0.3';
section2.style.pointerEvents = 'none';
section3.style.opacity = '0.3';
section3.style.pointerEvents = 'none';
} else {
section2.style.opacity = '1';
section2.style.pointerEvents = 'auto';
section3.style.opacity = '1';
section3.style.pointerEvents = 'auto';
}
};
updateSections();
// Execute Scan Logic
const doScan = async () => {
try {
const reqBody = {
offline_only: scanMode === 'offline',
skip_rename: !enableRename,
virtual_rename: enableRename ? enableVirtualRename : false,
physical_rename: enableRename ? enablePhysicalRename : false,
force_overwrite: enableForceOverwrite
};
if (targetMode === 'custom') {
let hasAny = false;
for (const set of selectedForScan.values()) {
if (set.size > 0) hasAny = true;
}
if (!hasAny) {
alert(t('sidebarNoModelsSelected'));
return;
}
document.body.removeChild(wizard);
const activeScanBtn = document.getElementById('anomalous-scan-btn');
if (activeScanBtn) {
setScanButtonState(activeScanBtn, true);
}
const customFolders = Array.from(selectedForScan.entries()).filter(([, files]) => files.size > 0);
let customFolderCurrent = 0;
updateScanProgress({
scanning: true,
phase: 'preparing',
folder_total: customFolders.length,
folder_current: 0,
});
// Sequential scan for multiple folders
for (const [folderKey, fileSet] of customFolders) {
customFolderCurrent += 1;
const parts = folderKey.split('|');
if (parts.length < 3) continue;
const type = parts[0], path_idx = parts[1], subfolder = parts.slice(2).join('|');
const params = new URLSearchParams({ type, path_idx, subfolder });
const targetUrl = '/anomalous/scan?' + params.toString();
const currentReqBody = { ...reqBody, target_files: Array.from(fileSet) };
try {
const res = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentReqBody)
});
const data = await res.json();
if (data.status === 'ok') {
// wait for this folder to finish scanning before starting next
await new Promise(resolve => {
const poll = setInterval(async () => {
try {
const statusUrl = '/anomalous/scan_status?' + params.toString();
const statusRes = await fetch(statusUrl);
const statusData = await statusRes.json();
updateScanProgress({
...statusData,
folder_total: customFolders.length,
folder_current: customFolderCurrent,
folder: subfolder,
});
if (!statusData.scanning) {
clearInterval(poll);
resolve();
}
} catch (err) {
clearInterval(poll);
resolve();
}
}, 2000);
});
} else {
console.error("Scan failed for folder: " + folderKey, data.message);
}
} catch(e) {
console.error("Request failed for folder: " + folderKey, e);
}
}
setActiveScanButtonState(false);
finishScanProgress();
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true);
}
this.loadModels();
return;
}
// Existing logic for 'all' mode or single file
let finalTargetFiles = targetFiles;
let currentReqBody = { ...reqBody };
let targetUrl = '';
if (isGlobal && targetMode !== 'custom') {
targetUrl = '/anomalous/scan_all';
} else {
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
if (finalTargetFiles) {
currentReqBody.target_files = finalTargetFiles.split(',');
}
targetUrl = '/anomalous/scan?' + params.toString();
}
const res = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentReqBody)
});
const data = await res.json();
if (data.status === 'ok') {
updateScanProgress({ scanning: true, phase: 'preparing', recovered: data.recovered });
setActiveScanButtonState(true);
// Start polling
const poll = setInterval(async () => {
try {
let statusUrl = '/anomalous/global_scan_status';
if (!isGlobal) {
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
statusUrl = '/anomalous/scan_status?' + params.toString();
}
const statusRes = await fetch(statusUrl);
const statusData = await statusRes.json();
updateScanProgress(statusData);
if (!statusData.scanning) {
clearInterval(poll);
if (statusData.interrupted) failScanProgress(t('scanProgressInterrupted'));
else finishScanProgress();
setActiveScanButtonState(false);
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true);
}
this.loadModels();
}
} catch (err) {
clearInterval(poll);
setActiveScanButtonState(false);
}
}, 2000);
document.body.removeChild(wizard);
return; // return early so we don't remove wizard again below
} else {
failScanProgress(t('sidebarScanFailed') + data.message);
alert(t('sidebarScanFailed') + data.message);
}
} catch (e) {
failScanProgress(String(e));
alert("Error: " + e);
}
document.body.removeChild(wizard);
};
const footer = document.createElement('div');
footer.style.marginTop = '32px';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '8px';
const closeBtn = document.createElement('button');
closeBtn.textContent = t('sidebarCancel');
closeBtn.style.padding = '8px 16px';
closeBtn.style.background = 'transparent';
closeBtn.style.color = '#9ca3af';
closeBtn.style.border = 'none';
closeBtn.style.borderRadius = '4px';
closeBtn.style.cursor = 'pointer';
closeBtn.style.fontSize = '0.95em';
closeBtn.style.fontWeight = '500';
closeBtn.style.textTransform = 'uppercase';
closeBtn.style.transition = 'background 0.2s';
closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(255, 255, 255, 0.08)';
closeBtn.onmouseout = () => closeBtn.style.background = 'transparent';
closeBtn.onclick = () => document.body.removeChild(wizard);
const startBtn = document.createElement('button');
startBtn.textContent = t('sidebarExecute');
startBtn.style.padding = '8px 24px';
startBtn.style.background = '#e5e7eb';
startBtn.style.color = '#111827';
startBtn.style.border = 'none';
startBtn.style.borderRadius = '4px';
startBtn.style.cursor = 'pointer';
startBtn.style.fontSize = '0.95em';
startBtn.style.fontWeight = '600';
startBtn.style.textTransform = 'uppercase';
startBtn.style.transition = 'background 0.2s, box-shadow 0.2s';
startBtn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
startBtn.onmouseover = () => {
startBtn.style.background = '#ffffff';
startBtn.style.boxShadow = '0 3px 8px rgba(0,0,0,0.4)';
};
startBtn.onmouseout = () => {
startBtn.style.background = '#e5e7eb';
startBtn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
};
startBtn.onclick = doScan;
footer.appendChild(closeBtn);
footer.appendChild(startBtn);
content.appendChild(footer);
wizard.appendChild(content);
document.body.appendChild(wizard);
}
async function formatScanCompletionToast(model) {
const isZh = window.anomalous_browser_lang === 'zh';
const fallbackName = model.name || model.filename;
try {
const findRes = await fetch('/anomalous/find_model?search=' + encodeURIComponent(model.filename));
if (findRes.ok) {
const updated = await findRes.json();
if (updated && updated.metadata) {
const isCivitai = Boolean(updated.metadata.id && updated.metadata.id !== -1 && updated.metadata.modelId !== -1);
const hasPreview = Boolean(updated.preview_url);
const baseModel = updated.metadata.baseModel;
if (isCivitai && hasPreview) {
return isZh ? `✓ 已从 Civitai 获取封面与模型信息!` : `✓ Civitai cover & metadata fetched!`;
}
if (isCivitai && !hasPreview) {
return isZh ? `✓ 已匹配到 Civitai 信息(线上未提供封面)` : `✓ Civitai metadata matched (no cover online)`;
}
if (!isCivitai) {
if (baseModel) {
return isZh
? `ℹ️ 非 Civitai 模型:已识别底模为 [${baseModel}](暂无封面,可点击编辑按钮手动添加)`
: `ℹ️ Non-Civitai model: inferred [${baseModel}] (no online cover)`;
}
return isZh
? `ℹ️ 未在 Civitai 匹配到此模型,已生成本地基础元数据`
: `ℹ️ No Civitai match found (local metadata created)`;
}
}
}
} catch {
// fallback to standard text
}
return isZh ? `✓ 模型 [${fallbackName}] 扫描完成!` : `✓ Model [${fallbackName}] scanned!`;
}
function pollDirectScanStatus(params, titleText, model, onComplete) {
const statusUrl = '/anomalous/scan_status?' + params.toString();
const poll = setInterval(async () => {
try {
const statusRes = await fetch(statusUrl);
const statusData = await statusRes.json();
updateScanProgress(statusData, titleText);
if (!statusData.scanning) {
clearInterval(poll);
if (statusData.interrupted) {
failScanProgress(t('scanProgressInterrupted'));
showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '扫描被中断' : 'Scan interrupted');
} else {
finishScanProgress();
const toastMsg = await formatScanCompletionToast(model);
showWorkbenchToast(toastMsg);
}
onComplete(true);
}
} catch (err) {
clearInterval(poll);
failScanProgress(String(err));
onComplete(false);
}
}, 1200);
return poll;
}
export async function triggerDirectModelScan(model, triggerBtn = null, browserInstance = null) {
const browser = browserInstance || this || {};
if (!model || !model.filename) return;
const modelLabel = model.name || model.filename;
const isZh = window.anomalous_browser_lang === 'zh';
const titleText = isZh ? `精准扫描: ${modelLabel}` : `Scanning: ${modelLabel}`;
if (triggerBtn) {
triggerBtn.classList.add('anomalous-radar-spinning');
if (triggerBtn.firstElementChild) {
triggerBtn.firstElementChild.style.animation = 'anomalous-radar-spin 1.2s linear infinite';
triggerBtn.firstElementChild.style.stroke = '#10b981';
}
triggerBtn.style.pointerEvents = 'none';
triggerBtn.style.opacity = '0.7';
}
setActiveScanButtonState(true);
updateScanProgress({ scanning: true, phase: 'preparing', total: 1, current: 0, filename: model.filename }, titleText);
const resetBtn = () => {
setActiveScanButtonState(false);
if (triggerBtn) {
triggerBtn.classList.remove('anomalous-radar-spinning');
if (triggerBtn.firstElementChild) {
triggerBtn.firstElementChild.style.animation = '';
triggerBtn.firstElementChild.style.stroke = '';
}
triggerBtn.style.pointerEvents = '';
triggerBtn.style.opacity = '';
}
};
try {
const params = new URLSearchParams({
type: browser.currentType || 'checkpoints',
path_idx: browser.currentPathIdx || 0,
subfolder: browser.currentSubfolder || '/',
});
const reqBody = {
target_files: [model.filename],
offline_only: false,
skip_rename: true,
virtual_rename: false,
physical_rename: false,
force_overwrite: false
};
const res = await fetch('/anomalous/scan?' + params.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqBody)
});
const data = await res.json();
if (data.status === 'ok') {
showWorkbenchToast(isZh ? `开始精准扫描: ${modelLabel}` : `Scanning model: ${modelLabel}`);
pollDirectScanStatus(params, titleText, model, () => {
resetBtn();
if (typeof browser.loadModels === 'function') {
browser.loadModels();
}
});
} else {
resetBtn();
const errMsg = data.message || (isZh ? '扫描启动失败' : 'Failed to start scan');
failScanProgress(errMsg);
showWorkbenchToast(errMsg);
}
} catch (e) {
resetBtn();
failScanProgress(String(e));
showWorkbenchToast(String(e));
}
}