Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_scan_wizard.js
T
DemonGatanjieuandClaude Opus 5.5 c8689d56e4 fix: offline scans stay offline, workshop snap dock, 400 on malformed requests
From an outside audit (same change as 32a3719 on feature/audio-engines):
- The scan wizard's offline mode still sent the greyed-out rename and
  overwrite options, and scraper.py fetched Civitai model pages and preview
  media for models whose .info held a Civitai id. Offline now implies no
  media and no model page; the folder scan and scan_all refuse a missing or
  non-object body instead of defaulting to an online scan.
- The Prompt Workshop's snap dock referenced leftSearch from the source deck
  module (ReferenceError on click); the deck now exposes focusSearch().
- translate, delete_model, delete_gallery_image and the image-material
  routes answer malformed bodies with 400 instead of 500.
- Removed six unused imports in ui_doctor.js and 16 duplicate locale keys
  per language (the later, effective entry is kept, so nothing changes on
  screen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:44:04 +08:00

921 lines
39 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 { app } from '../../../scripts/app.js';
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) {
if (typeof wizard.cleanupModal === 'function') wizard.cleanupModal();
else if (wizard.parentNode) wizard.parentNode.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 closeWizard = () => {
document.removeEventListener('keydown', handleKeydown);
if (wizard && wizard.parentNode) {
wizard.parentNode.removeChild(wizard);
}
};
wizard.cleanupModal = closeWizard;
const handleKeydown = (e) => {
if (e.key === 'Escape') closeWizard();
};
document.addEventListener('keydown', handleKeydown);
wizard.onclick = (e) => {
if (e.target === wizard) closeWizard();
};
const content = document.createElement('div');
content.style.background = '#1E1E1E';
content.style.borderRadius = '12px';
content.style.padding = '28px 32px 20px 32px';
content.style.width = '760px';
content.style.maxWidth = '95%';
content.style.maxHeight = '85vh';
content.style.display = 'flex';
content.style.flexDirection = 'column';
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';
formGroup.style.overflowY = 'auto';
formGroup.style.flex = '1';
formGroup.style.minHeight = '0';
formGroup.style.paddingRight = '6px';
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 {
// Offline greys out renaming and overwriting, so none of them may be sent.
const offline = scanMode === 'offline';
const reqBody = {
offline_only: offline,
skip_rename: offline || !enableRename,
virtual_rename: !offline && enableRename && enableVirtualRename,
physical_rename: !offline && enableRename && enablePhysicalRename,
force_overwrite: !offline && 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;
}
closeWizard();
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();
try {
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
} catch (e) {
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
}
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);
try {
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
} catch (e) {
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
}
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true);
}
this.loadModels();
}
} catch (err) {
clearInterval(poll);
setActiveScanButtonState(false);
}
}, 2000);
closeWizard();
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);
}
closeWizard();
};
const footer = document.createElement('div');
footer.style.marginTop = '16px';
footer.style.paddingTop = '16px';
footer.style.borderTop = '1px solid rgba(255,255,255,0.08)';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '8px';
footer.style.flexShrink = '0';
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 = closeWizard;
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);
}
export 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();
const target = updated?.model || updated || model;
if (target && target.metadata) {
const meta = target.metadata;
const isCivitai = Boolean(
(meta.id && meta.id !== -1) ||
(meta.modelId && meta.modelId !== -1) ||
(meta.model_id && meta.model_id !== -1) ||
(meta.version_id && meta.version_id !== -1) ||
meta.civitai_url
);
const hasPreview = Boolean(target.preview_url);
const baseModel = meta.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 base model [${baseModel}]`;
}
return isZh
? `ℹ️ 未在 Civitai 匹配到此模型`
: `ℹ️ No Civitai match found for this model`;
}
}
}
} 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 {
const toastMsg = await formatScanCompletionToast(model);
finishScanProgress(toastMsg);
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, async () => {
resetBtn();
if (typeof browser.loadModels === 'function') {
browser.loadModels();
}
try {
if (app?.refreshComboInNodes) await app.refreshComboInNodes();
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
} catch (e) {
console.warn('[AMB] Error reloading hashes or combo nodes:', e);
}
});
} else {
resetBtn();
const errMsg = data.message || (isZh ? '扫描启动失败' : 'Failed to start scan');
failScanProgress(errMsg);
showWorkbenchToast(errMsg);
}
} catch (e) {
resetBtn();
failScanProgress(String(e));
showWorkbenchToast(String(e));
}
}