5927 lines
284 KiB
JavaScript
5927 lines
284 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
import { i18n } from './modules/locales.js';
|
|
|
|
// ============================================================================
|
|
// TABLE OF CONTENTS (TOC)
|
|
// 1. App Registration & Entry (Search for "app.registerExtension")
|
|
// 2. State & Class Constructor (Search for "class AnomalousBrowser")
|
|
// 3. UI - Sidebar (Search for "createDOM")
|
|
// 4. UI - Main Grid (Search for "renderGrid")
|
|
// 5. UI - Detail Panel (Search for "showDetail")
|
|
// 6. UI - Gallery Viewer (Search for "createGalleryViewer")
|
|
// 7. UI - Doctor Panel (Search for "createDoctorPanel")
|
|
// 8. Notebooks (Search for "Notebook")
|
|
// ============================================================================
|
|
|
|
let defaultLang = 'zh';
|
|
try {
|
|
let comfyDetected = false;
|
|
const aglLang = localStorage.getItem('Comfy.Settings.AIGODLIKE-COMFYUI-TRANSLATION.Language');
|
|
if (aglLang) {
|
|
defaultLang = aglLang.toLowerCase().includes('en') ? 'en' : 'zh';
|
|
comfyDetected = true;
|
|
} else {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && (key.toLowerCase().includes('lang') || key.toLowerCase().includes('locale'))) {
|
|
const val = localStorage.getItem(key);
|
|
if (typeof val === 'string') {
|
|
const lowerVal = val.toLowerCase();
|
|
if (lowerVal.includes('zh') || lowerVal.includes('chinese')) {
|
|
defaultLang = 'zh';
|
|
comfyDetected = true;
|
|
break;
|
|
} else if (lowerVal.includes('en') || lowerVal.includes('english')) {
|
|
defaultLang = 'en';
|
|
comfyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!comfyDetected && navigator.language && !navigator.language.toLowerCase().startsWith('zh')) {
|
|
defaultLang = 'en';
|
|
}
|
|
} catch (e) {
|
|
if (navigator.language && !navigator.language.toLowerCase().startsWith('zh')) {
|
|
defaultLang = 'en';
|
|
}
|
|
}
|
|
let currentLang = localStorage.getItem('anomalous_lang') || defaultLang;
|
|
window.anomalous_browser_lang = currentLang;
|
|
const t = (key) => i18n[currentLang][key] || key;
|
|
|
|
class AnomalousBrowser {
|
|
constructor() {
|
|
this.modal = null;
|
|
this.sidebar = null;
|
|
this.grid = null;
|
|
this.detailPanel = null;
|
|
this.currentType = 'loras';
|
|
this.currentPathIdx = 0;
|
|
this.currentSubfolder = '/';
|
|
this.foldersData = null;
|
|
this.expandedFolders = new Set(['/', 'checkpoints', 'loras', 'unet', 'diffusion_models']);
|
|
this.energySaving = localStorage.getItem('anomalous_energy_saving') === 'true';
|
|
this.createDOM();
|
|
}
|
|
|
|
createDOM() {
|
|
this.modal = document.createElement('div');
|
|
this.modal.id = 'anomalous-modal';
|
|
|
|
const container = document.createElement('div');
|
|
container.id = 'anomalous-container';
|
|
|
|
const updateLangClass = () => {
|
|
if (currentLang === 'en') container.classList.add('anomalous-lang-en');
|
|
else container.classList.remove('anomalous-lang-en');
|
|
};
|
|
updateLangClass();
|
|
|
|
const savedScale = localStorage.getItem('anomalous_ui_scale') || 1;
|
|
container.style.setProperty('--anomalous-scale', savedScale);
|
|
|
|
// Sidebar
|
|
this.sidebarWrapper = document.createElement('div');
|
|
this.sidebarWrapper.id = 'anomalous-sidebar-wrapper';
|
|
this.sidebarWrapper.style.position = 'relative';
|
|
|
|
const brandBar = document.createElement('div');
|
|
brandBar.style.padding = '15px 15px 10px 15px';
|
|
brandBar.style.display = 'flex';
|
|
brandBar.style.alignItems = 'center';
|
|
brandBar.style.justifyContent = 'space-between';
|
|
brandBar.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
|
|
|
const badge = document.createElement('div');
|
|
badge.style.background = 'linear-gradient(135deg, #444, #222)';
|
|
badge.style.color = '#ccc';
|
|
badge.style.fontSize = '0.7em';
|
|
badge.style.padding = '4px 8px';
|
|
badge.style.borderRadius = '6px';
|
|
badge.style.letterSpacing = '1px';
|
|
badge.style.border = '1px solid #555';
|
|
badge.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
|
|
badge.style.textTransform = 'uppercase';
|
|
badge.style.fontWeight = 'bold';
|
|
badge.innerHTML = 'Anomalous Browser';
|
|
|
|
const menuBtn = document.createElement('button');
|
|
menuBtn.innerHTML = '鈽?;
|
|
menuBtn.title = window.anomalous_browser_lang === 'zh' ? '鏀惰捣/灞曞紑渚ц竟鏍? : 'Toggle Sidebar';
|
|
menuBtn.style.background = 'transparent';
|
|
menuBtn.style.border = 'none';
|
|
menuBtn.style.color = '#ccc';
|
|
menuBtn.style.fontSize = '1.2em';
|
|
menuBtn.style.cursor = 'pointer';
|
|
menuBtn.onclick = () => {
|
|
if (container.classList.contains('anomalous-sidebar-closed')) {
|
|
container.classList.remove('anomalous-sidebar-closed');
|
|
localStorage.setItem('anomalous_user_sidebar_closed', 'false');
|
|
} else {
|
|
container.classList.add('anomalous-sidebar-closed');
|
|
localStorage.setItem('anomalous_user_sidebar_closed', 'true');
|
|
}
|
|
};
|
|
|
|
brandBar.appendChild(badge);
|
|
brandBar.appendChild(menuBtn);
|
|
|
|
this.sidebar = document.createElement('div');
|
|
this.sidebar.id = 'anomalous-sidebar';
|
|
|
|
this.sidebarActions = document.createElement('div');
|
|
this.sidebarActions.id = 'anomalous-sidebar-actions';
|
|
this.sidebarActions.style.padding = '10px 15px';
|
|
this.sidebarActions.style.display = 'flex';
|
|
this.sidebarActions.style.flexDirection = 'row';
|
|
this.sidebarActions.style.justifyContent = 'flex-start';
|
|
this.sidebarActions.style.alignItems = 'center';
|
|
this.sidebarActions.style.gap = '10px';
|
|
this.sidebarActions.style.borderTop = '1px solid rgba(255,255,255,0.05)';
|
|
this.sidebarActions.style.background = 'transparent';
|
|
this.sidebarActions.style.borderRadius = '0';
|
|
this.sidebarActions.style.width = '100%';
|
|
this.sidebarActions.style.boxSizing = 'border-box';
|
|
this.sidebarActions.style.margin = '0';
|
|
|
|
this.sidebarWrapper.appendChild(brandBar);
|
|
this.sidebarWrapper.appendChild(this.sidebar);
|
|
this.sidebarWrapper.appendChild(this.sidebarActions);
|
|
|
|
// Content Area
|
|
const content = document.createElement('div');
|
|
content.id = 'anomalous-content';
|
|
|
|
const header = document.createElement('div');
|
|
header.id = 'anomalous-header';
|
|
|
|
let isDragging = false;
|
|
let dragOffsetX = 0;
|
|
let dragOffsetY = 0;
|
|
|
|
const enforceBounds = (x, y) => {
|
|
let newX = x;
|
|
let newY = y;
|
|
if (newX + container.offsetWidth > window.innerWidth) newX = window.innerWidth - container.offsetWidth;
|
|
if (newY + container.offsetHeight > window.innerHeight) newY = window.innerHeight - container.offsetHeight;
|
|
if (newX < 0) newX = 0;
|
|
if (newY < 0) newY = 0;
|
|
return { x: newX, y: newY };
|
|
};
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.id === 'anomalous-close') return;
|
|
isDragging = true;
|
|
const rect = container.getBoundingClientRect();
|
|
dragOffsetX = e.clientX - rect.left;
|
|
dragOffsetY = e.clientY - rect.top;
|
|
e.preventDefault();
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
const pos = enforceBounds(e.clientX - dragOffsetX, e.clientY - dragOffsetY);
|
|
container.style.left = pos.x + 'px';
|
|
container.style.top = pos.y + 'px';
|
|
container.style.transform = 'none';
|
|
});
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
localStorage.setItem('anomalous_pos_x', container.style.left);
|
|
localStorage.setItem('anomalous_pos_y', container.style.top);
|
|
}
|
|
});
|
|
|
|
const savedX = localStorage.getItem('anomalous_pos_x');
|
|
const savedY = localStorage.getItem('anomalous_pos_y');
|
|
if (savedX && savedY) {
|
|
container.style.left = savedX;
|
|
container.style.top = savedY;
|
|
}
|
|
|
|
// Periodically enforce bounds to catch resize/zoom changes
|
|
setInterval(() => {
|
|
if (!isDragging && container.style.display !== 'none' && !container.classList.contains('anomalous-docked')) {
|
|
const rect = container.getBoundingClientRect();
|
|
const pos = enforceBounds(rect.left, rect.top);
|
|
if (pos.x !== rect.left || pos.y !== rect.top) {
|
|
if (container.style.left.endsWith('px') && container.style.top.endsWith('px')) {
|
|
container.style.left = pos.x + 'px';
|
|
container.style.top = pos.y + 'px';
|
|
}
|
|
}
|
|
}
|
|
}, 1000);
|
|
|
|
const spacer = document.createElement('div');
|
|
spacer.style.flex = '1 1 auto';
|
|
|
|
const leftGroup = document.createElement('div');
|
|
leftGroup.className = 'anomalous-header-group';
|
|
|
|
const rightGroup = document.createElement('div');
|
|
rightGroup.className = 'anomalous-header-group';
|
|
|
|
// We will define hideAllPanels as a class method instead of a local closure to make it globally accessible.
|
|
|
|
const showSidebar = () => {
|
|
container.classList.remove('anomalous-sidebar-closed');
|
|
};
|
|
|
|
const modelsBtn = document.createElement('button');
|
|
modelsBtn.id = 'anomalous-models-btn';
|
|
modelsBtn.innerHTML = `馃彔 <span class="anomalous-btn-text">${t('models')}</span>`;
|
|
modelsBtn.onclick = () => {
|
|
this.hideAllPanels();
|
|
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
|
|
container.classList.add('anomalous-sidebar-closed');
|
|
} else {
|
|
showSidebar();
|
|
}
|
|
menuBtn.disabled = false;
|
|
menuBtn.style.opacity = '1';
|
|
menuBtn.style.cursor = 'pointer';
|
|
this.grid.style.display = 'grid';
|
|
if (this.detailPanel.innerHTML !== '') {
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.currentDetailModel = null;
|
|
this.historyStack = [];
|
|
}
|
|
};
|
|
|
|
const galleryBtn = document.createElement('button');
|
|
galleryBtn.innerHTML = `馃柤锔?<span class="anomalous-btn-text">${t('gallery') || '鍥惧簱'}</span>`;
|
|
galleryBtn.onclick = () => {
|
|
this.hideAllPanels();
|
|
container.classList.add('anomalous-sidebar-closed');
|
|
menuBtn.disabled = true;
|
|
menuBtn.style.opacity = '0.3';
|
|
menuBtn.style.cursor = 'not-allowed';
|
|
this.galleryPanel.style.display = 'flex';
|
|
if (!this.galleryLoaded) {
|
|
this.loadGalleryImages(1, true);
|
|
}
|
|
};
|
|
|
|
const dockBtn = document.createElement('button');
|
|
dockBtn.innerHTML = '鈼?;
|
|
dockBtn.title = t('dockTitle');
|
|
dockBtn.onclick = () => {
|
|
container.classList.toggle('anomalous-docked');
|
|
if (container.classList.contains('anomalous-docked')) {
|
|
localStorage.setItem('anomalous_docked', 'true');
|
|
} else {
|
|
localStorage.setItem('anomalous_docked', 'false');
|
|
}
|
|
};
|
|
|
|
if (localStorage.getItem('anomalous_docked') === 'true') {
|
|
container.classList.add('anomalous-docked');
|
|
}
|
|
|
|
const helpBtn = document.createElement('button');
|
|
helpBtn.id = 'anomalous-help-btn';
|
|
helpBtn.title = t('helpTitle');
|
|
helpBtn.innerHTML = `鉂?<span class="anomalous-btn-text">${t('help')}</span>`;
|
|
helpBtn.onclick = () => this.showHelp();
|
|
|
|
const nbBtn = document.createElement('button');
|
|
nbBtn.id = 'anomalous-notebook-btn';
|
|
nbBtn.title = t('notebookTitle');
|
|
nbBtn.innerHTML = `馃搼 <span class="anomalous-btn-text">${t('notebooks')}</span>`;
|
|
|
|
const dBtn = document.getElementById('anomalous-doctor-btn');
|
|
if (dBtn) dBtn.title = window.anomalous_browser_lang === 'zh' ? '妯″瀷鍖荤敓' : 'Model Doctor';
|
|
const aBtn = document.getElementById('anomalous-assistant-btn');
|
|
if (aBtn) aBtn.title = window.anomalous_browser_lang === 'zh' ? '鑺傜偣鍔╂墜' : 'Node Assistant';
|
|
const iBtn = document.getElementById('anomalous-import-btn');
|
|
if (iBtn) iBtn.title = window.anomalous_browser_lang === 'zh' ? '馃摜 棰勬瀵煎叆宸ヤ綔娴? : '馃摜 Preflight Import';
|
|
const sBtn = document.getElementById('anomalous-settings-btn');
|
|
if (sBtn) sBtn.title = window.anomalous_browser_lang === 'zh' ? '璁剧疆涓績' : 'Settings Hub';
|
|
|
|
// Reset dynamic panels so they re-render in new language
|
|
if (window.anomalousBrowserInstance) {
|
|
const b = window.anomalousBrowserInstance;
|
|
if (b.doctorPanel) {
|
|
b.doctorPanel.innerHTML = '';
|
|
b.doctorPanelInitialized = false;
|
|
}
|
|
}
|
|
const impOverlay = document.getElementById('anomalous-import-overlay');
|
|
if (impOverlay && impOverlay.parentNode) {
|
|
impOverlay.parentNode.removeChild(impOverlay);
|
|
}
|
|
|
|
nbBtn.onclick = () => {
|
|
this.nbPanel.style.display = 'flex';
|
|
this.showNotebooks();
|
|
};
|
|
|
|
rightGroup.appendChild(modelsBtn);
|
|
rightGroup.appendChild(galleryBtn);
|
|
rightGroup.appendChild(nbBtn);
|
|
|
|
|
|
const apiKeyBtn = document.createElement('button');
|
|
apiKeyBtn.id = 'anomalous-api-btn';
|
|
apiKeyBtn.innerHTML = `<span class="anomalous-btn-text">${t('apiKeyConfig')}</span>`;
|
|
apiKeyBtn.onclick = async () => {
|
|
const current = localStorage.getItem('anomalous_api_key') || '';
|
|
const val = prompt(t('apiKeyPrompt'), current);
|
|
if (val !== null) {
|
|
localStorage.setItem('anomalous_api_key', val.trim());
|
|
try {
|
|
await fetch('/anomalous/save_config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ api_key: val.trim() })
|
|
});
|
|
} catch (e) { }
|
|
}
|
|
};
|
|
|
|
const scanBtn = document.createElement('button');
|
|
scanBtn.id = 'anomalous-scan-btn';
|
|
scanBtn.title = window.anomalous_browser_lang === 'zh' ? '馃攧 鎵弿鍚戝 (Scan Wizard)' : '馃攧 Scan Wizard';
|
|
scanBtn.innerHTML = `馃攧`;
|
|
scanBtn.style.background = 'transparent';
|
|
scanBtn.style.color = '#ccc';
|
|
scanBtn.style.border = 'none';
|
|
scanBtn.style.borderRadius = '6px';
|
|
scanBtn.style.padding = '6px';
|
|
scanBtn.style.fontSize = '1.1em';
|
|
scanBtn.style.cursor = 'pointer';
|
|
scanBtn.style.transition = 'all 0.2s ease';
|
|
scanBtn.onmouseover = () => { scanBtn.style.background = 'rgba(255,255,255,0.1)'; scanBtn.style.color = '#fff'; };
|
|
scanBtn.onmouseout = () => { scanBtn.style.background = 'transparent'; scanBtn.style.color = '#ccc'; };
|
|
|
|
let isCurrentlyScanning = false;
|
|
setInterval(async () => {
|
|
try {
|
|
let isScanning = false;
|
|
if (this.currentType) {
|
|
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx || 0, subfolder: this.currentSubfolder || '/' });
|
|
const resLocal = await fetch('/anomalous/scan_status?' + params.toString());
|
|
const dataLocal = await resLocal.json();
|
|
if (dataLocal.scanning) isScanning = true;
|
|
}
|
|
const resGlobal = await fetch('/anomalous/global_scan_status');
|
|
const dataGlobal = await resGlobal.json();
|
|
if (dataGlobal.scanning) isScanning = true;
|
|
|
|
if (isScanning && !isCurrentlyScanning) {
|
|
isCurrentlyScanning = true;
|
|
scanBtn.innerHTML = `鈴砢;
|
|
scanBtn.style.opacity = '0.7';
|
|
} else if (!isScanning && isCurrentlyScanning) {
|
|
isCurrentlyScanning = false;
|
|
scanBtn.innerHTML = `馃攧`;
|
|
scanBtn.style.opacity = '1';
|
|
this.loadModels();
|
|
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
|
|
alert(window.anomalous_browser_lang === 'zh' ? '鉁?鎵弿/閲嶅懡鍚嶅凡瀹屾垚锛佹暟鎹凡涓烘偍鏇存柊銆? : '鉁?Scan/Rename completed! Data updated.');
|
|
}
|
|
} catch (e) { }
|
|
}, 3000);
|
|
|
|
const createWizardModal = (isGlobal = false) => {
|
|
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');
|
|
title.innerText = window.anomalous_browser_lang === 'zh'
|
|
? (isGlobal ? '鍏ㄥ眬鎵弿鍚戝' : '鎵弿鍚戝')
|
|
: (isGlobal ? 'Global Scan Wizard' : 'Scan Wizard');
|
|
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, textZh, textEn, onClick) => {
|
|
const btn = document.createElement('button');
|
|
btn.innerHTML = `${icon} ${window.anomalous_browser_lang === 'zh' ? textZh : textEn}`;
|
|
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('馃攽', 'API 瀵嗛挜', 'API Key', () => {
|
|
const currentKey = localStorage.getItem('anomalous_civitai_api_key') || '';
|
|
const newKey = prompt(window.anomalous_browser_lang === 'zh' ? '璇疯緭鍏?Civitai API Key (鐣欑┖鍙栨秷):' : 'Enter Civitai API Key:', currentKey);
|
|
if (newKey !== null) {
|
|
localStorage.setItem('anomalous_civitai_api_key', newKey.trim());
|
|
alert(window.anomalous_browser_lang === 'zh' ? '鉁?瀵嗛挜淇濆瓨鎴愬姛' : '鉁?Key Saved');
|
|
}
|
|
});
|
|
|
|
topToolbar.appendChild(apiKeyBtn);
|
|
|
|
headerArea.appendChild(title);
|
|
headerArea.appendChild(topToolbar);
|
|
content.appendChild(headerArea);
|
|
|
|
let scanMode = 'civitai';
|
|
let enableRename = true;
|
|
let enableAutoCheck = true;
|
|
let updateSections = () => { };
|
|
|
|
const formGroup = document.createElement('div');
|
|
formGroup.style.display = 'flex';
|
|
formGroup.style.flexDirection = 'column';
|
|
formGroup.style.gap = '24px';
|
|
|
|
// === Step 1: Fetch Data ===
|
|
const section1 = document.createElement('div');
|
|
const sec1Title = document.createElement('div');
|
|
sec1Title.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 1. 鑾峰彇鏁版嵁' : 'Step 1. Fetch Data';
|
|
sec1Title.style.fontWeight = '500';
|
|
sec1Title.style.color = '#8AB4F8';
|
|
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';
|
|
|
|
const createChoiceCard = (id, icon, titleZh, titleEn, descZh, descEn, isSelected) => {
|
|
const card = document.createElement('div');
|
|
card.style.flex = '1';
|
|
card.style.background = isSelected ? 'rgba(138, 180, 248, 0.15)' : 'rgba(255, 255, 255, 0.05)';
|
|
card.style.border = `2px solid ${isSelected ? '#8AB4F8' : '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.05)';
|
|
};
|
|
|
|
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.innerText = window.anomalous_browser_lang === 'zh' ? titleZh : titleEn;
|
|
tTitle.style.fontWeight = '500';
|
|
tTitle.style.fontSize = '1.05em';
|
|
tTitle.style.color = isSelected ? '#8AB4F8' : '#fff';
|
|
|
|
topRow.appendChild(iconDiv);
|
|
topRow.appendChild(tTitle);
|
|
|
|
const tDesc = document.createElement('div');
|
|
tDesc.innerText = window.anomalous_browser_lang === 'zh' ? descZh : descEn;
|
|
tDesc.style.fontSize = '0.85em';
|
|
tDesc.style.color = '#bbb';
|
|
tDesc.style.lineHeight = '1.5';
|
|
|
|
card.appendChild(topRow);
|
|
card.appendChild(tDesc);
|
|
return card;
|
|
};
|
|
|
|
let card1, card2;
|
|
const updateCards = () => {
|
|
if (card1 && card2) {
|
|
cardsContainer.removeChild(card1);
|
|
cardsContainer.removeChild(card2);
|
|
}
|
|
card1 = createChoiceCard('civitai', '馃實', '鍦ㄧ嚎瀹屾暣鍖归厤', 'Online Full Matching', '鑷姩杩炴帴 Civitai 骞冲彴锛屾繁搴﹁幏鍙栨ā鍨嬪皝闈㈠浘銆佽Е鍙戣瘝 (Trigger Words)銆佷綔鑰呭娉ㄥ強璇︾粏鏍囩銆傛帹鑽愭棩甯镐娇鐢ㄣ€?, 'Connects to Civitai to fetch comprehensive model covers, trigger words, author notes, and tags. Recommended for daily use.', scanMode === 'civitai');
|
|
card2 = createChoiceCard('offline', '馃攲', '绂荤嚎鏋侀€熻鍙?, 'Offline Fast Read', '涓嶆秷鑰椾换浣曠綉缁溿€備粎閫氳繃鏈湴寮犻噺璁$畻鏋侀€熸彁鍙栨ā鍨嬪唴鐨勫熀纭€鍏冩暟鎹紝閫熷害鏋佸揩浣嗕俊鎭緝鍩虹銆?, 'No network required. Extremely fast extraction of basic metadata via local tensor computation, but info is limited.', 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(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)';
|
|
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 ? '#8AB4F8' : '#bdbdbd';
|
|
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(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)';
|
|
thumb.style.background = state ? '#8AB4F8' : '#bdbdbd';
|
|
thumb.style.left = state ? '16px' : '0px';
|
|
onChange(state);
|
|
};
|
|
track.appendChild(thumb);
|
|
return track;
|
|
};
|
|
|
|
const createListRow = (icon, titleZh, titleEn, descZh, descEn, 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');
|
|
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 t = document.createElement('div');
|
|
t.innerText = window.anomalous_browser_lang === 'zh' ? titleZh : titleEn;
|
|
t.style.fontWeight = '500';
|
|
t.style.fontSize = '1.0em';
|
|
t.style.color = '#fff';
|
|
|
|
const d = document.createElement('div');
|
|
d.innerHTML = window.anomalous_browser_lang === 'zh' ? descZh : descEn;
|
|
d.style.fontSize = '0.85em';
|
|
d.style.color = '#aaa';
|
|
d.style.marginTop = '4px';
|
|
|
|
textDiv.appendChild(t);
|
|
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.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 2. 瑙勮寖鍛藉悕' : 'Step 2. Normalize Naming';
|
|
sec2Title.style.fontWeight = '500';
|
|
sec2Title.style.color = '#8AB4F8';
|
|
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);
|
|
const physicalSwitch = createMaterialSwitch(enablePhysicalRename, (s) => enablePhysicalRename = s);
|
|
|
|
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;">鉁?${window.anomalous_browser_lang === 'zh' ? '铏氭嫙閲嶅懡鍚?(瀹夊叏鎺ㄨ崘)' : 'Virtual Rename (Safe)'}</span>`;
|
|
vRow.appendChild(virtualSwitch);
|
|
|
|
const vDesc = document.createElement('div');
|
|
vDesc.style.fontSize = '0.8em';
|
|
vDesc.style.color = '#888';
|
|
vDesc.style.marginTop = '4px';
|
|
vDesc.innerHTML = window.anomalous_browser_lang === 'zh' ? '浠呭皢鏍囧噯鍚嶇О娉ㄥ叆娴忚鍣ㄦ彃浠朵腑锛屼笉淇敼纭洏涓婄殑鐪熷疄鏂囦欢鍚嶏紝闅忔椂鍙棤鎹熻繕鍘熴€? : 'Inject standard names into the browser only, without modifying actual files on disk. Safely reversible.';
|
|
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;">馃捑 ${window.anomalous_browser_lang === 'zh' ? '鐗╃悊閲嶅懡鍚? : 'Physical Rename'}</span>`;
|
|
pRow.appendChild(physicalSwitch);
|
|
|
|
const pDesc = document.createElement('div');
|
|
pDesc.style.fontSize = '0.8em';
|
|
pDesc.style.color = '#888';
|
|
pDesc.style.marginTop = '4px';
|
|
pDesc.innerHTML = window.anomalous_browser_lang === 'zh' ? '鐪熷疄淇敼搴曞眰鐨?<code>.safetensors</code> 鍙婂叾鎵€鏈夐檮灞炴枃浠跺悕锛屽交搴曞憡鍒贡鐮佹枃浠跺悕銆? : 'Permanently rename the underlying <code>.safetensors</code> and associated files on disk.';
|
|
pContainer.appendChild(pRow);
|
|
pContainer.appendChild(pDesc);
|
|
|
|
dualChannelRow.appendChild(vContainer);
|
|
dualChannelRow.appendChild(pContainer);
|
|
|
|
s2List.appendChild(createListRow('馃摑', '鑷姩瑙勮寖鍛藉悕', 'Auto-Normalize Naming', '寮鸿揩鐥囩闊炽€傝嚜鍔ㄥ皢鏉備贡鏃犵珷鐨勬ā鍨嬫枃浠跺悕閲嶅懡鍚嶄负 <b>妯″瀷鍚峗鐗堟湰鍚?safetensors</b> 鐨勬爣鍑嗘牸寮忋€?br><span style="color:#8AB4F8; display:inline-block; margin-top:4px;">渚嬪锛?/span> <span style="color:#ff6b6b; text-decoration:line-through;">model_final(1).safetensors</span> 鉃?<span style="color:#28a745;">Beautiful_Mix_V1.0.safetensors</span>', 'Automatically rename messy model files to the official standard format: <b>ModelName_VersionName.safetensors</b>.<br><span style="color:#8AB4F8; display:inline-block; margin-top:4px;">e.g. </span> <span style="color:#ff6b6b; text-decoration:line-through;">model_final(1).safetensors</span> 鉃?<span style="color:#28a745;">Beautiful_Mix_V1.0.safetensors</span>', 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.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 3. 宸ヤ綔娴佷繚闅滀笌淇' : 'Step 3. Workflow Protection & Fix';
|
|
sec3Title.style.fontWeight = '500';
|
|
sec3Title.style.color = '#8AB4F8';
|
|
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';
|
|
s3List.appendChild(createListRow('馃摝', '妯″瀷婧簮缁戝畾', 'Model Provenance Binding', '鍏ㄥ眬鐢熸晥銆傛瘡娆$敓鎴愬浘鐗囨垨淇濆瓨宸ヤ綔娴佹椂锛屽皢妯″瀷鐨勭簿纭搱甯屽€奸殣寮忓啓鍏ュ埌鍥惧儚鍏冩暟鎹笌宸ヤ綔娴?JSON 涓紝闃叉鏈潵妯″瀷鏀瑰悕鎴栨崲鐜鍚庢姤閿欐壘涓嶅埌銆?, 'Global effect. Implicitly embed exact model hashes into generated images and saved workflows, ensuring missing models can always be auto-fixed.', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
|
|
s3List.appendChild(createListRow('馃獎', '鏅鸿兘淇褰撳墠宸ヤ綔娴?, 'Smart Fix Current Workflow', '鎵弿瀹屾垚鍚庯紝鑷姩灏濊瘯鐢ㄦ渶鏂版暟鎹慨澶嶅綋鍓嶇敾甯冧腑鎻愮ず缂哄け鐨勬姤閿欐ā鍨嬭妭鐐广€?, 'After scanning, automatically attempt to fix missing model errors on the current canvas.', 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
|
|
};
|
|
|
|
let targetUrl = '';
|
|
if (isGlobal) {
|
|
targetUrl = '/anomalous/scan_all';
|
|
} else {
|
|
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
|
|
targetUrl = '/anomalous/scan?' + params.toString();
|
|
}
|
|
|
|
const res = await fetch(targetUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(reqBody)
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.status === 'ok') {
|
|
// Close wizard modal immediately
|
|
document.body.removeChild(wizard);
|
|
|
|
// Set hourglass on sidebar button
|
|
if (typeof scanBtn !== 'undefined') {
|
|
scanBtn.innerHTML = `鈴砢;
|
|
scanBtn.style.animation = 'anomalous-spin 2s linear infinite';
|
|
}
|
|
|
|
// 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();
|
|
if (!statusData.scanning) {
|
|
clearInterval(poll);
|
|
if (typeof scanBtn !== 'undefined') {
|
|
scanBtn.innerHTML = `馃攧`;
|
|
scanBtn.style.animation = '';
|
|
}
|
|
|
|
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
|
|
window.anomalous_resolve_all_missing_nodes(true);
|
|
}
|
|
this.loadModels();
|
|
}
|
|
} catch (err) {
|
|
clearInterval(poll);
|
|
if (typeof scanBtn !== 'undefined') {
|
|
scanBtn.innerHTML = `馃攧`;
|
|
scanBtn.style.animation = '';
|
|
}
|
|
}
|
|
}, 2000);
|
|
|
|
return; // return early so we don't remove wizard again below
|
|
} else {
|
|
alert(window.anomalous_browser_lang === 'zh' ? '鎵弿澶辫触: ' + data.message : 'Scan failed: ' + data.message);
|
|
}
|
|
} catch (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.innerText = window.anomalous_browser_lang === 'zh' ? '鍙栨秷' : 'Cancel';
|
|
closeBtn.style.padding = '8px 16px';
|
|
closeBtn.style.background = 'transparent';
|
|
closeBtn.style.color = '#8AB4F8';
|
|
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(138, 180, 248, 0.08)';
|
|
closeBtn.onmouseout = () => closeBtn.style.background = 'transparent';
|
|
closeBtn.onclick = () => document.body.removeChild(wizard);
|
|
|
|
const startBtn = document.createElement('button');
|
|
startBtn.innerText = window.anomalous_browser_lang === 'zh' ? '鎵ц鎵弿' : 'Execute Scan';
|
|
startBtn.style.padding = '8px 24px';
|
|
startBtn.style.background = '#8AB4F8'; // Material Primary
|
|
startBtn.style.color = '#1E1E1E';
|
|
startBtn.style.border = 'none';
|
|
startBtn.style.borderRadius = '4px';
|
|
startBtn.style.cursor = 'pointer';
|
|
startBtn.style.fontSize = '0.95em';
|
|
startBtn.style.fontWeight = '500';
|
|
startBtn.style.textTransform = 'uppercase';
|
|
startBtn.style.transition = 'background 0.2s, box-shadow 0.2s';
|
|
startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)';
|
|
startBtn.onmouseover = () => {
|
|
startBtn.style.background = '#aecbf9';
|
|
startBtn.style.boxShadow = '0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12)';
|
|
};
|
|
startBtn.onmouseout = () => {
|
|
startBtn.style.background = '#8AB4F8';
|
|
startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)';
|
|
};
|
|
startBtn.onclick = doScan;
|
|
|
|
footer.appendChild(closeBtn);
|
|
footer.appendChild(startBtn);
|
|
content.appendChild(footer);
|
|
wizard.appendChild(content);
|
|
document.body.appendChild(wizard);
|
|
};
|
|
|
|
scanBtn.onclick = () => createWizardModal();
|
|
this.sidebarActions.appendChild(scanBtn);
|
|
|
|
const energyBtn = document.createElement('button');
|
|
energyBtn.id = 'anomalous-energy-btn';
|
|
energyBtn.title = t('togglePlayTitle');
|
|
const renderEnergyBtn = () => {
|
|
energyBtn.innerHTML = this.energySaving
|
|
? `<span class="anomalous-btn-text">${(window.anomalous_browser_lang === 'zh' ? '馃攱 瑙嗛鎮诞鎾斁 (鑺傝兘妯″紡)' : '馃攱 Eco Mode (Play video on hover)')}</span>`
|
|
: `<span class="anomalous-btn-text">${(window.anomalous_browser_lang === 'zh' ? '馃幀 鑷姩鎾斁瑙嗛灏侀潰 (楂樿兘鑰?' : '馃幀 AutoPlay Videos (High perf cost)')}</span>`;
|
|
};
|
|
renderEnergyBtn();
|
|
energyBtn.onclick = () => {
|
|
this.energySaving = !this.energySaving;
|
|
localStorage.setItem('anomalous_energy_saving', this.energySaving);
|
|
renderEnergyBtn();
|
|
this.loadModels();
|
|
};
|
|
|
|
const closeBtn = document.createElement('div');
|
|
closeBtn.id = 'anomalous-close';
|
|
closeBtn.innerHTML = '×';
|
|
closeBtn.onclick = () => this.close();
|
|
|
|
const cleanBtn = document.createElement('button');
|
|
cleanBtn.id = 'anomalous-clean-btn';
|
|
cleanBtn.title = t('cleanTitle');
|
|
cleanBtn.innerHTML = `<span class="anomalous-btn-text">${t('clean')}</span>`;
|
|
|
|
cleanBtn.onclick = async () => {
|
|
if (!confirm(t('cleanConfirm'))) return;
|
|
cleanBtn.innerHTML = `鈴?<span class="anomalous-btn-text">${t('cleaning')}</span>`;
|
|
cleanBtn.disabled = true;
|
|
try {
|
|
const res = await fetch('/anomalous/clean_civitai_info', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
alert(`${t('cleanDone')} ${data.count} ${t('files')}`);
|
|
} else {
|
|
alert(t('cleanFail') + data.message);
|
|
}
|
|
} catch (e) { alert(t('cleanErr') + e.message); }
|
|
cleanBtn.innerHTML = `<span class="anomalous-btn-text">${t('clean')}</span>`;
|
|
cleanBtn.disabled = false;
|
|
};
|
|
|
|
rightGroup.appendChild(dockBtn);
|
|
rightGroup.appendChild(closeBtn);
|
|
|
|
header.appendChild(leftGroup);
|
|
header.appendChild(spacer);
|
|
header.appendChild(rightGroup);
|
|
|
|
const settingsHubModal = document.createElement('div');
|
|
settingsHubModal.style.position = 'absolute';
|
|
settingsHubModal.style.bottom = '15px';
|
|
settingsHubModal.style.left = '100%';
|
|
settingsHubModal.style.marginLeft = '10px';
|
|
settingsHubModal.style.width = '260px';
|
|
settingsHubModal.style.background = 'var(--comfy-menu-bg, #2a2a2a)';
|
|
settingsHubModal.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
settingsHubModal.style.borderRadius = '12px';
|
|
settingsHubModal.style.padding = '10px';
|
|
settingsHubModal.style.display = 'none';
|
|
settingsHubModal.style.flexDirection = 'column';
|
|
settingsHubModal.style.gap = '4px';
|
|
settingsHubModal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.5)';
|
|
settingsHubModal.style.zIndex = '1000';
|
|
|
|
const langBtn = document.createElement('button');
|
|
langBtn.className = 'anomalous-lang-btn';
|
|
langBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃寪 Language: EN' : '馃寪 璇█: 涓枃';
|
|
langBtn.onclick = () => {
|
|
currentLang = window.anomalous_browser_lang === 'zh' ? 'en' : 'zh';
|
|
localStorage.setItem('anomalous_lang', currentLang);
|
|
window.anomalous_browser_lang = currentLang;
|
|
langBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃寪 Language: EN' : '馃寪 璇█: 涓枃';
|
|
updateLangClass();
|
|
modelsBtn.innerHTML = `馃彔 <span class="anomalous-btn-text">${t('models')}</span>`;
|
|
galleryBtn.innerHTML = `馃柤锔?<span class="anomalous-btn-text">${t('gallery')}</span>`;
|
|
scanBtn.title = t('scanTitle');
|
|
scanBtn.innerHTML = `馃攧`;
|
|
cleanBtn.title = t('cleanTitle');
|
|
cleanBtn.innerHTML = `<span class="anomalous-btn-text">${t('clean')}</span>`;
|
|
helpBtn.innerHTML = `鉂?<span class="anomalous-btn-text">${t('help')}</span>`;
|
|
nbBtn.title = t('notebookTitle');
|
|
nbBtn.innerHTML = `馃搼 <span class="anomalous-btn-text">${t('notebooks')}</span>`;
|
|
|
|
const dBtn = document.getElementById('anomalous-doctor-btn');
|
|
if (dBtn) dBtn.title = window.anomalous_browser_lang === 'zh' ? '妯″瀷鍖荤敓' : 'Model Doctor';
|
|
const aBtn = document.getElementById('anomalous-assistant-btn');
|
|
if (aBtn) aBtn.title = window.anomalous_browser_lang === 'zh' ? '鑺傜偣鍔╂墜' : 'Node Assistant';
|
|
const iBtn = document.getElementById('anomalous-import-btn');
|
|
if (iBtn) iBtn.title = window.anomalous_browser_lang === 'zh' ? '馃摜 棰勬瀵煎叆宸ヤ綔娴? : '馃摜 Preflight Import';
|
|
const sBtn = document.getElementById('anomalous-global-settings-btn');
|
|
if (sBtn) sBtn.title = window.anomalous_browser_lang === 'zh' ? '璁剧疆涓績' : 'Settings Hub';
|
|
|
|
// Reset dynamic panels so they re-render in new language
|
|
if (window.anomalousBrowserInstance) {
|
|
const b = window.anomalousBrowserInstance;
|
|
if (b.doctorPanel) {
|
|
b.doctorPanel.innerHTML = '';
|
|
b.doctorPanelInitialized = false;
|
|
}
|
|
}
|
|
const impOverlay = document.getElementById('anomalous-import-overlay');
|
|
if (impOverlay && impOverlay.parentNode) {
|
|
impOverlay.parentNode.removeChild(impOverlay);
|
|
}
|
|
|
|
apiKeyBtn.innerHTML = `<span class="anomalous-btn-text">${t('apiKeyConfig')}</span>`;
|
|
const globalScanBtnRef = document.getElementById('anomalous-global-scan-btn');
|
|
if (globalScanBtnRef) globalScanBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃實 涓€閿叏鐩樻瀬閫熸壂鎻?(涓嶄笅杞藉皝闈?涓嶆敼鍚?' : '馃實 Global Quick Scan (No rename/No media)';
|
|
const checkUnscannedBtnRef = document.getElementById('anomalous-check-unscanned-btn');
|
|
if (checkUnscannedBtnRef) checkUnscannedBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
const resetBtnRef = document.getElementById('anomalous-reset-btn');
|
|
if (resetBtnRef) resetBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攧 閲嶇疆鐣岄潰甯冨眬' : '馃攧 Reset Layout';
|
|
const scaleLabelRef = document.getElementById('anomalous-scale-label');
|
|
if (scaleLabelRef) scaleLabelRef.innerText = window.anomalous_browser_lang === 'zh' ? 'UI 缂╂斁' : 'UI Scale';
|
|
|
|
renderEnergyBtn();
|
|
this.renderSidebar();
|
|
this.loadModels();
|
|
if (this.detailPanel.style.display !== 'none' && this.currentDetailModel) {
|
|
this.showDetail(this.currentDetailModel);
|
|
}
|
|
if (this.nbEditor && this.nbEditor.innerHTML !== '') {
|
|
this.renderNotebookEditor();
|
|
this.refreshNotebooks();
|
|
}
|
|
};
|
|
|
|
const styleHubBtn = (btn) => {
|
|
btn.style.background = 'transparent';
|
|
btn.style.border = '1px solid rgba(255,255,255,0.05)';
|
|
btn.style.color = '#ccc';
|
|
btn.style.textAlign = 'left';
|
|
btn.style.padding = '8px 10px';
|
|
btn.style.borderRadius = '8px';
|
|
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'; };
|
|
};
|
|
|
|
styleHubBtn(cleanBtn);
|
|
styleHubBtn(energyBtn);
|
|
styleHubBtn(apiKeyBtn);
|
|
styleHubBtn(langBtn);
|
|
styleHubBtn(helpBtn);
|
|
|
|
const globalScanBtn = document.createElement('button');
|
|
globalScanBtn.id = 'anomalous-global-scan-btn';
|
|
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃實 涓€閿叏鐩樻瀬閫熸壂鎻?(涓嶄笅杞藉皝闈?涓嶆敼鍚?' : '馃實 Global Quick Scan (No rename/No media)';
|
|
styleHubBtn(globalScanBtn);
|
|
|
|
globalScanBtn.onclick = async () => {
|
|
if (!confirm(window.anomalous_browser_lang === 'zh' ? '鍗冲皢鎵ц鏋侀€熷叏鐩樻壂鎻忓苟鍚屾Hash锛屾鎿嶄綔涓嶆敼鍚嶄篃涓嶄笅杞藉皝闈紝鏄惁缁х画锛? : 'Start global quick scan to sync all hashes?')) return;
|
|
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鈴?鎵弿涓?..' : '鈴?Scanning...';
|
|
globalScanBtn.disabled = true;
|
|
try {
|
|
const res = await fetch('/anomalous/scan_all', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
use_local_metadata: localStorage.getItem('anomalous_local_metadata_scan') !== 'false'
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (data.status === 'ok') {
|
|
alert(window.anomalous_browser_lang === 'zh' ? '馃殌 鍏ㄥ眬鎵弿宸插悗鍙板惎鍔紒' : '馃殌 Global Scan started in background!');
|
|
const pollTimer = setInterval(async () => {
|
|
try {
|
|
const statusRes = await fetch('/anomalous/global_scan_status');
|
|
const statusData = await statusRes.json();
|
|
if (!statusData.scanning) {
|
|
clearInterval(pollTimer);
|
|
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鉁?鎵弿瀹屾垚' : '鉁?Scan Complete';
|
|
setTimeout(() => {
|
|
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃實 涓€閿叏鐩樻壂鎻?(涓嶄笅杞藉皝闈?涓嶆敼鍚?' : '馃實 Global Quick Scan (No rename/No media)';
|
|
globalScanBtn.disabled = false;
|
|
}, 3000);
|
|
}
|
|
} catch (e) { }
|
|
}, 3000);
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + data.message);
|
|
globalScanBtn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
globalScanBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const localParseToggleBtn = document.createElement('button');
|
|
localParseToggleBtn.id = 'anomalous-local-parse-toggle-btn';
|
|
const renderLocalParseToggleBtn = () => {
|
|
let isLocalEnabled = localStorage.getItem('anomalous_local_metadata_scan') !== 'false';
|
|
localParseToggleBtn.innerHTML = window.anomalous_browser_lang === 'zh'
|
|
? (isLocalEnabled ? '鈽?寮€鍚潪C绔欐ā鍨嬫湰鍦版壂鎻?(璇诲彇鍐呯疆鍙傛暟) [宸插紑鍚痌' : '鈽?寮€鍚潪C绔欐ā鍨嬫湰鍦版壂鎻?(璇诲彇鍐呯疆鍙傛暟) [宸插叧闂璢')
|
|
: (isLocalEnabled ? '鈽?Local metadata scan for non-Civitai [ON]' : '鈽?Local metadata scan for non-Civitai [OFF]');
|
|
};
|
|
renderLocalParseToggleBtn();
|
|
styleHubBtn(localParseToggleBtn);
|
|
localParseToggleBtn.onclick = () => {
|
|
let isLocalEnabled = localStorage.getItem('anomalous_local_metadata_scan') !== 'false';
|
|
localStorage.setItem('anomalous_local_metadata_scan', isLocalEnabled ? 'false' : 'true');
|
|
renderLocalParseToggleBtn();
|
|
};
|
|
|
|
|
|
const checkUnscannedBtn = document.createElement('button');
|
|
checkUnscannedBtn.id = 'anomalous-check-unscanned-btn';
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
styleHubBtn(checkUnscannedBtn);
|
|
checkUnscannedBtn.onclick = async () => {
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鈴?妫€鏌ヤ腑...' : '鈴?Checking...';
|
|
checkUnscannedBtn.disabled = true;
|
|
try {
|
|
const res = await fetch('/anomalous/all_hashes');
|
|
const data = await res.json();
|
|
const hashesObj = data.hashes ? data.hashes : data;
|
|
let hasUnscanned = false;
|
|
for (const key in hashesObj) {
|
|
if (hashesObj[key].hash === "") {
|
|
hasUnscanned = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasUnscanned) {
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鈿狅笍 鍙戠幇缂哄け锛屾鍦ㄨ嚜鍔ㄦ瀬閫熸壂鎻?..' : '鈿狅笍 Missing info found, Auto-Scanning...';
|
|
const scanRes = await fetch('/anomalous/scan_all', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
use_local_metadata: localStorage.getItem('anomalous_local_metadata_scan') !== 'false'
|
|
})
|
|
});
|
|
const scanData = await scanRes.json();
|
|
if (scanData.status === 'ok') {
|
|
const pollTimer = setInterval(async () => {
|
|
try {
|
|
const statusRes = await fetch('/anomalous/global_scan_status');
|
|
const statusData = await statusRes.json();
|
|
if (!statusData.scanning) {
|
|
clearInterval(pollTimer);
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鉁?琛ュ叏瀹屾垚' : '鉁?Info Complete';
|
|
setTimeout(() => {
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
checkUnscannedBtn.disabled = false;
|
|
}, 3000);
|
|
}
|
|
} catch (e) { }
|
|
}, 3000);
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + scanData.message);
|
|
checkUnscannedBtn.disabled = false;
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
}
|
|
} else {
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鉁?鎵€鏈夋ā鍨嬩俊鎭凡瀹屾暣' : '鉁?All Model Info is Complete';
|
|
setTimeout(() => {
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
checkUnscannedBtn.disabled = false;
|
|
}, 3000);
|
|
}
|
|
} catch (e) {
|
|
checkUnscannedBtn.disabled = false;
|
|
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攳 妫€鏌ュ苟鏋侀€熷綍鍏ョ己澶辨ā鍨嬩俊鎭? : '馃攳 Check & Auto-Scan Missing Info';
|
|
}
|
|
};
|
|
|
|
const scaleContainer = document.createElement('div');
|
|
scaleContainer.style.display = 'flex';
|
|
scaleContainer.style.alignItems = 'center';
|
|
scaleContainer.style.justifyContent = 'space-between';
|
|
scaleContainer.style.background = '#1a1a1a';
|
|
scaleContainer.style.padding = '8px 12px';
|
|
scaleContainer.style.borderRadius = '4px';
|
|
scaleContainer.style.border = '2px solid #555';
|
|
scaleContainer.style.marginBottom = '4px';
|
|
|
|
const scaleLabel = document.createElement('span');
|
|
scaleLabel.id = 'anomalous-scale-label';
|
|
scaleLabel.innerText = window.anomalous_browser_lang === 'zh' ? 'UI 缂╂斁' : 'UI Scale';
|
|
scaleLabel.style.color = '#ccc';
|
|
scaleLabel.style.fontSize = '0.9em';
|
|
|
|
let currentScale = parseFloat(savedScale);
|
|
|
|
const controlsWrapper = document.createElement('div');
|
|
controlsWrapper.style.display = 'flex';
|
|
controlsWrapper.style.alignItems = 'center';
|
|
controlsWrapper.style.gap = '8px';
|
|
|
|
const scaleVal = document.createElement('span');
|
|
scaleVal.innerText = `${Math.round(currentScale * 100)}%`;
|
|
scaleVal.style.color = '#fff';
|
|
scaleVal.style.fontSize = '0.9em';
|
|
scaleVal.style.minWidth = '45px';
|
|
scaleVal.style.textAlign = 'center';
|
|
|
|
const createScaleBtn = (text, delta) => {
|
|
const btn = document.createElement('button');
|
|
btn.innerText = text;
|
|
btn.style.background = '#333';
|
|
btn.style.color = '#fff';
|
|
btn.style.border = '1px solid #555';
|
|
btn.style.borderRadius = '4px';
|
|
btn.style.width = '24px';
|
|
btn.style.height = '24px';
|
|
btn.style.cursor = 'pointer';
|
|
btn.style.display = 'flex';
|
|
btn.style.alignItems = 'center';
|
|
btn.style.justifyContent = 'center';
|
|
btn.onmouseover = () => btn.style.background = '#444';
|
|
btn.onmouseout = () => btn.style.background = '#333';
|
|
btn.onclick = () => {
|
|
currentScale = Math.max(0.5, Math.min(1.5, currentScale + delta));
|
|
scaleVal.innerText = `${Math.round(currentScale * 100)}%`;
|
|
container.style.setProperty('--anomalous-scale', currentScale);
|
|
localStorage.setItem('anomalous_ui_scale', currentScale);
|
|
};
|
|
return btn;
|
|
};
|
|
|
|
const minusBtn = createScaleBtn('-', -0.1);
|
|
const plusBtn = createScaleBtn('+', 0.1);
|
|
|
|
controlsWrapper.appendChild(minusBtn);
|
|
controlsWrapper.appendChild(scaleVal);
|
|
controlsWrapper.appendChild(plusBtn);
|
|
|
|
scaleContainer.appendChild(scaleLabel);
|
|
scaleContainer.appendChild(controlsWrapper);
|
|
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.id = 'anomalous-reset-btn';
|
|
resetBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃攧 閲嶇疆鐣岄潰甯冨眬' : '馃攧 Reset Layout';
|
|
styleHubBtn(resetBtn);
|
|
resetBtn.onclick = () => {
|
|
if (confirm(window.anomalous_browser_lang === 'zh' ? '纭閲嶇疆绐楀彛浣嶇疆銆佺缉鏀惧拰鍋滈潬鐘舵€佸悧锛? : 'Reset window position, scale and dock state?')) {
|
|
localStorage.removeItem('anomalous_pos_x');
|
|
localStorage.removeItem('anomalous_pos_y');
|
|
localStorage.removeItem('anomalous_width');
|
|
localStorage.removeItem('anomalous_height');
|
|
localStorage.removeItem('anomalous_docked');
|
|
localStorage.removeItem('anomalous_ui_scale');
|
|
container.style.left = '5%';
|
|
container.style.top = '5%';
|
|
container.style.width = '90%';
|
|
container.style.height = '90%';
|
|
container.style.setProperty('--anomalous-scale', '1');
|
|
currentScale = 1;
|
|
scaleVal.innerText = '100%';
|
|
if (container.classList.contains('anomalous-docked')) {
|
|
container.classList.remove('anomalous-docked');
|
|
}
|
|
}
|
|
};
|
|
|
|
const hashToggleBtn = document.createElement('button');
|
|
hashToggleBtn.id = 'anomalous-hash-toggle-btn';
|
|
styleHubBtn(hashToggleBtn);
|
|
const updateHashToggleBtn = () => {
|
|
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
|
|
hashToggleBtn.innerHTML = window.anomalous_browser_lang === 'zh'
|
|
? (isInject ? '馃煝 娉ㄥ叆宸ヤ綔娴佸搱甯? : '鈿?涓嶆敞鍏ュ伐浣滄祦鍝堝笇')
|
|
: (isInject ? '馃煝 Inject Workflow Hash' : '鈿?Skip Workflow Hash');
|
|
};
|
|
updateHashToggleBtn();
|
|
hashToggleBtn.onclick = () => {
|
|
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
|
|
localStorage.setItem('anomalous_inject_hash', isInject ? 'false' : 'true');
|
|
updateHashToggleBtn();
|
|
};
|
|
|
|
// Many redundant buttons have been migrated to the Wizard!
|
|
|
|
settingsHubModal.appendChild(cleanBtn);
|
|
settingsHubModal.appendChild(energyBtn);
|
|
settingsHubModal.appendChild(scaleContainer);
|
|
settingsHubModal.appendChild(langBtn);
|
|
settingsHubModal.appendChild(helpBtn);
|
|
settingsHubModal.appendChild(resetBtn);
|
|
|
|
this.sidebarWrapper.appendChild(settingsHubModal);
|
|
|
|
const settingsBtn = document.createElement('button');
|
|
settingsBtn.id = 'anomalous-global-settings-btn';
|
|
settingsBtn.innerHTML = `鈿欙笍`;
|
|
settingsBtn.title = window.anomalous_browser_lang === 'zh' ? '璁剧疆涓績' : 'Settings Hub';
|
|
settingsBtn.style.background = 'transparent';
|
|
settingsBtn.style.color = '#ccc';
|
|
settingsBtn.style.border = 'none';
|
|
settingsBtn.style.borderRadius = '6px';
|
|
settingsBtn.style.padding = '6px';
|
|
settingsBtn.style.fontSize = '1.1em';
|
|
settingsBtn.style.marginLeft = 'auto';
|
|
settingsBtn.style.cursor = 'pointer';
|
|
settingsBtn.style.transition = 'all 0.2s ease';
|
|
settingsBtn.onmouseover = () => { settingsBtn.style.background = 'rgba(255,255,255,0.1)'; settingsBtn.style.color = '#fff'; };
|
|
settingsBtn.onmouseout = () => { settingsBtn.style.background = 'transparent'; settingsBtn.style.color = '#ccc'; };
|
|
const closeSettingsHub = (e) => {
|
|
if (settingsHubModal.style.display !== 'none' && !settingsHubModal.contains(e.target) && !settingsBtn.contains(e.target)) {
|
|
settingsHubModal.style.display = 'none';
|
|
settingsBtn.style.color = '#ccc';
|
|
document.removeEventListener('mousedown', closeSettingsHub);
|
|
}
|
|
};
|
|
|
|
settingsBtn.onclick = () => {
|
|
if (settingsHubModal.style.display === 'none') {
|
|
settingsHubModal.style.display = 'flex';
|
|
settingsBtn.style.color = '#fff';
|
|
// Delay adding the listener slightly to avoid triggering it on the same click
|
|
setTimeout(() => document.addEventListener('mousedown', closeSettingsHub), 10);
|
|
} else {
|
|
settingsHubModal.style.display = 'none';
|
|
settingsBtn.style.color = '#ccc';
|
|
document.removeEventListener('mousedown', closeSettingsHub);
|
|
}
|
|
};
|
|
|
|
const importBtn = document.createElement('button');
|
|
importBtn.id = 'anomalous-import-btn';
|
|
importBtn.title = t('importBtn');
|
|
importBtn.innerHTML = `馃摜`;
|
|
importBtn.style.background = 'transparent';
|
|
importBtn.style.color = '#ccc';
|
|
importBtn.style.border = 'none';
|
|
importBtn.style.borderRadius = '6px';
|
|
importBtn.style.padding = '6px';
|
|
importBtn.style.fontSize = '1.1em';
|
|
importBtn.style.cursor = 'pointer';
|
|
importBtn.style.transition = 'all 0.2s ease';
|
|
importBtn.onmouseover = () => { importBtn.style.background = 'rgba(255,255,255,0.1)'; importBtn.style.color = '#fff'; };
|
|
importBtn.onmouseout = () => { importBtn.style.background = 'transparent'; importBtn.style.color = '#ccc'; };
|
|
importBtn.onclick = () => {
|
|
if (window.AMB_WorkflowShare) {
|
|
window.AMB_WorkflowShare.showUnifiedModal();
|
|
} else {
|
|
alert(window.anomalous_browser_lang === 'zh' ? '妯″潡灏氭湭鍔犺浇锛岃绋嶇瓑鎴栧埛鏂伴〉闈紒' : 'Module not loaded, please refresh!');
|
|
}
|
|
};
|
|
|
|
|
|
const doctorBtn = document.createElement('button');
|
|
doctorBtn.id = 'anomalous-doctor-btn';
|
|
doctorBtn.title = window.anomalous_browser_lang === 'zh' ? '妯″瀷鍖荤敓' : 'Model Doctor';
|
|
doctorBtn.innerHTML = `馃┖`;
|
|
doctorBtn.style.background = 'transparent';
|
|
doctorBtn.style.color = '#ccc';
|
|
doctorBtn.style.border = 'none';
|
|
doctorBtn.style.borderRadius = '6px';
|
|
doctorBtn.style.padding = '6px';
|
|
doctorBtn.style.fontSize = '1.1em';
|
|
doctorBtn.style.cursor = 'pointer';
|
|
doctorBtn.style.transition = 'all 0.2s ease';
|
|
doctorBtn.onmouseover = () => { doctorBtn.style.background = 'rgba(255,255,255,0.1)'; doctorBtn.style.color = '#fff'; };
|
|
doctorBtn.onmouseout = () => { doctorBtn.style.background = 'transparent'; doctorBtn.style.color = '#ccc'; };
|
|
doctorBtn.onclick = async () => {
|
|
this.hideAllPanels();
|
|
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
|
|
container.classList.add('anomalous-sidebar-closed');
|
|
} else {
|
|
container.classList.remove('anomalous-sidebar-closed');
|
|
}
|
|
menuBtn.disabled = false;
|
|
menuBtn.style.opacity = '1';
|
|
menuBtn.style.cursor = 'pointer';
|
|
this.doctorPanel.style.display = 'flex';
|
|
if (!this.doctorPanelInitialized) {
|
|
this.initDoctorPanel();
|
|
}
|
|
// Trigger auto hash-resolve when opening Doctor
|
|
if (window.anomalous_resolve_all_missing_nodes) {
|
|
window.anomalous_resolve_all_missing_nodes(true, false).then(() => {
|
|
if (app.graph && app.graph.extra && app.graph.extra.anomalous_hashes) {
|
|
this.renderGlobalDashboard(app.graph.extra.anomalous_hashes);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const assistantBtn = document.createElement('button');
|
|
assistantBtn.id = 'anomalous-assistant-btn';
|
|
assistantBtn.title = window.anomalous_browser_lang === 'zh' ? '鑺傜偣鍔╂墜' : 'Node Assistant';
|
|
assistantBtn.innerHTML = `馃`;
|
|
assistantBtn.style.background = 'transparent';
|
|
assistantBtn.style.color = '#ccc';
|
|
assistantBtn.style.border = 'none';
|
|
assistantBtn.style.borderRadius = '6px';
|
|
assistantBtn.style.padding = '6px';
|
|
assistantBtn.style.fontSize = '1.1em';
|
|
assistantBtn.style.cursor = 'pointer';
|
|
assistantBtn.style.transition = 'all 0.2s ease';
|
|
assistantBtn.onmouseover = () => { assistantBtn.style.background = 'rgba(255,255,255,0.1)'; assistantBtn.style.color = '#fff'; };
|
|
assistantBtn.onmouseout = () => { assistantBtn.style.background = 'transparent'; assistantBtn.style.color = '#ccc'; };
|
|
assistantBtn.onclick = async () => {
|
|
this.hideAllPanels();
|
|
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
|
|
container.classList.add('anomalous-sidebar-closed');
|
|
} else {
|
|
container.classList.remove('anomalous-sidebar-closed');
|
|
}
|
|
menuBtn.disabled = false;
|
|
menuBtn.style.opacity = '1';
|
|
menuBtn.style.cursor = 'pointer';
|
|
this.assistantPanel.style.display = 'flex';
|
|
if (!this.assistantPanelInitialized) {
|
|
this.initAssistantPanel();
|
|
}
|
|
// Show current selected node immediately
|
|
if (Object.keys(app.canvas.selected_nodes || {}).length > 0) {
|
|
const firstSelected = Object.values(app.canvas.selected_nodes)[0];
|
|
this.diagnoseNode(firstSelected);
|
|
} else {
|
|
this.diagnoseNode(null);
|
|
}
|
|
};
|
|
|
|
this.sidebarActions.appendChild(doctorBtn);
|
|
this.sidebarActions.appendChild(assistantBtn);
|
|
this.sidebarActions.appendChild(importBtn);
|
|
this.sidebarActions.appendChild(settingsBtn);
|
|
|
|
this.grid = document.createElement('div');
|
|
this.grid.id = 'anomalous-grid';
|
|
|
|
this.detailPanel = document.createElement('div');
|
|
this.detailPanel.id = 'anomalous-detail';
|
|
this.detailPanel.style.display = 'none';
|
|
|
|
this.galleryPanel = document.createElement('div');
|
|
this.galleryPanel.id = 'anomalous-gallery-panel';
|
|
|
|
this.doctorPanel = document.createElement('div');
|
|
this.doctorPanel.id = 'anomalous-doctor-panel';
|
|
this.doctorPanel.style.display = 'none';
|
|
this.doctorPanel.style.flexDirection = 'column';
|
|
this.doctorPanel.style.flex = '1';
|
|
this.doctorPanel.style.overflowY = 'auto';
|
|
this.doctorPanel.style.boxSizing = 'border-box';
|
|
this.doctorPanelInitialized = false;
|
|
|
|
this.assistantPanel = document.createElement('div');
|
|
this.assistantPanel.id = 'anomalous-assistant-panel';
|
|
this.assistantPanel.style.display = 'none';
|
|
this.assistantPanel.style.flexDirection = 'column';
|
|
this.assistantPanel.style.flex = '1';
|
|
this.assistantPanel.style.overflowY = 'auto';
|
|
this.assistantPanel.style.boxSizing = 'border-box';
|
|
this.assistantPanelInitialized = false;
|
|
|
|
this.galleryGrid = document.createElement('div');
|
|
this.galleryGrid.className = 'anomalous-gallery-grid';
|
|
this.galleryPanel.appendChild(this.galleryGrid);
|
|
|
|
this.gallerySentinel = document.createElement('div');
|
|
this.gallerySentinel.className = 'anomalous-gallery-sentinel';
|
|
this.galleryGrid.appendChild(this.gallerySentinel);
|
|
|
|
this.galleryCurrentPage = 1;
|
|
this.galleryLoaded = false;
|
|
this.galleryLoading = false;
|
|
this.galleryHasMore = true;
|
|
|
|
this.galleryObserver = new IntersectionObserver((entries) => {
|
|
if (entries[0].isIntersecting && !this.galleryLoading && this.galleryHasMore) {
|
|
this.loadGalleryImages(this.galleryCurrentPage + 1);
|
|
}
|
|
}, { root: this.galleryGrid, rootMargin: '100px' });
|
|
this.galleryObserver.observe(this.gallerySentinel);
|
|
|
|
this.nbPanel = document.createElement('div');
|
|
this.nbPanel.className = 'anomalous-nb-modal';
|
|
this.nbPanel.style.display = 'none';
|
|
this.nbPanel.onclick = (e) => {
|
|
if (e.target === this.nbPanel) this.nbPanel.style.display = 'none';
|
|
};
|
|
|
|
content.appendChild(header);
|
|
content.appendChild(this.grid);
|
|
content.appendChild(this.detailPanel);
|
|
content.appendChild(this.galleryPanel);
|
|
content.appendChild(this.doctorPanel);
|
|
content.appendChild(this.assistantPanel);
|
|
|
|
container.appendChild(this.sidebarWrapper);
|
|
container.appendChild(content);
|
|
container.appendChild(this.nbPanel);
|
|
|
|
this.modal.appendChild(container);
|
|
|
|
// Resize handle
|
|
const resizeHandle = document.createElement('div');
|
|
resizeHandle.className = 'anomalous-resize-handle';
|
|
let isResizing = false;
|
|
resizeHandle.onmousedown = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
isResizing = true;
|
|
};
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
const rect = container.getBoundingClientRect();
|
|
let newWidth = e.clientX - rect.left;
|
|
let newHeight = e.clientY - rect.top;
|
|
if (newWidth < 600) newWidth = 600;
|
|
if (newHeight < 400) newHeight = 400;
|
|
container.style.width = newWidth + 'px';
|
|
container.style.height = newHeight + 'px';
|
|
});
|
|
window.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
localStorage.setItem('anomalous_width', container.style.width);
|
|
localStorage.setItem('anomalous_height', container.style.height);
|
|
}
|
|
});
|
|
const savedW = localStorage.getItem('anomalous_width');
|
|
const savedH = localStorage.getItem('anomalous_height');
|
|
if (savedW) container.style.width = savedW;
|
|
if (savedH) container.style.height = savedH;
|
|
|
|
container.appendChild(resizeHandle);
|
|
document.body.appendChild(this.modal);
|
|
}
|
|
|
|
|
|
showHelp() {
|
|
if (this.helpModal) {
|
|
this.helpModal.remove();
|
|
}
|
|
this.helpModal = document.createElement('div');
|
|
this.helpModal.style.position = 'absolute';
|
|
this.helpModal.style.top = '0';
|
|
this.helpModal.style.left = '0';
|
|
this.helpModal.style.width = '100%';
|
|
this.helpModal.style.height = '100%';
|
|
this.helpModal.style.background = 'rgba(0,0,0,0.85)';
|
|
this.helpModal.style.zIndex = '9999';
|
|
this.helpModal.style.display = 'flex';
|
|
this.helpModal.style.alignItems = 'center';
|
|
this.helpModal.style.justifyContent = 'center';
|
|
|
|
const box = document.createElement('div');
|
|
box.style.background = 'var(--bg-color, #222)';
|
|
box.style.border = '1px solid var(--border-color, #444)';
|
|
box.style.borderRadius = '8px';
|
|
box.style.width = '550px';
|
|
box.style.maxWidth = '90%';
|
|
box.style.boxShadow = '0 10px 40px rgba(0,0,0,0.8)';
|
|
box.style.display = 'flex';
|
|
box.style.flexDirection = 'column';
|
|
box.style.maxHeight = '90vh';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.padding = '15px 20px';
|
|
header.style.borderBottom = '1px solid #444';
|
|
header.style.background = '#333';
|
|
header.style.display = 'flex';
|
|
header.style.alignItems = 'center';
|
|
header.style.justifyContent = 'space-between';
|
|
|
|
const title = document.createElement('h2');
|
|
title.innerHTML = t('helpTitle');
|
|
title.style.margin = '0';
|
|
title.style.color = '#fff';
|
|
title.style.fontSize = '1.2em';
|
|
|
|
const closeX = document.createElement('div');
|
|
closeX.innerHTML = '×';
|
|
closeX.style.position = 'absolute';
|
|
closeX.style.top = '10px';
|
|
closeX.style.right = '15px';
|
|
closeX.style.fontSize = '1.8em';
|
|
closeX.style.cursor = 'pointer';
|
|
closeX.style.color = '#ff4444';
|
|
closeX.onclick = () => this.helpModal.remove();
|
|
|
|
header.appendChild(title);
|
|
header.appendChild(closeX);
|
|
|
|
const body = document.createElement('div');
|
|
body.style.padding = '20px';
|
|
body.innerHTML = t('helpContent');
|
|
body.style.overflowY = 'auto';
|
|
body.style.flex = '1';
|
|
|
|
const footer = document.createElement('div');
|
|
footer.style.padding = '15px';
|
|
footer.style.borderTop = '1px solid #444';
|
|
footer.style.display = 'flex';
|
|
footer.style.justifyContent = 'flex-end';
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerHTML = t('closeHelp');
|
|
closeBtn.style.padding = '8px 16px';
|
|
closeBtn.style.background = '#444';
|
|
closeBtn.style.color = '#fff';
|
|
closeBtn.style.border = 'none';
|
|
closeBtn.style.borderRadius = '4px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.onclick = () => this.helpModal.remove();
|
|
|
|
footer.appendChild(closeBtn);
|
|
|
|
box.appendChild(header);
|
|
box.appendChild(body);
|
|
box.appendChild(footer);
|
|
this.helpModal.appendChild(box);
|
|
|
|
document.getElementById('anomalous-container').appendChild(this.helpModal);
|
|
}
|
|
|
|
async loadFolders() {
|
|
try {
|
|
const res = await fetch('/anomalous/folders');
|
|
const data = await res.json();
|
|
this.foldersData = data.folders || [];
|
|
|
|
if (!this.firstLoadDone && this.foldersData.length > 0) {
|
|
this.firstLoadDone = true;
|
|
let found = false;
|
|
for (const typeGroup of this.foldersData) {
|
|
const sortedPaths = Object.keys(typeGroup.folders).sort();
|
|
for (const path of sortedPaths) {
|
|
if (typeGroup.folders[path].model_count > 0) {
|
|
this.currentType = typeGroup.type;
|
|
this.currentPathIdx = typeGroup.path_idx;
|
|
this.currentSubfolder = path;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
}
|
|
}
|
|
|
|
// Auto expand all
|
|
(this.foldersData || []).forEach(typeGroup => {
|
|
this.expandedFolders.add(typeGroup.type);
|
|
Object.keys(typeGroup.folders).forEach(path => {
|
|
this.expandedFolders.add(typeGroup.type + path);
|
|
});
|
|
});
|
|
|
|
this.renderSidebar();
|
|
this.loadModels();
|
|
} catch (e) { }
|
|
}
|
|
|
|
renderSidebar() {
|
|
this.sidebar.innerHTML = '';
|
|
|
|
const topBar = document.createElement('div');
|
|
topBar.style.display = 'flex';
|
|
topBar.style.justifyContent = 'space-between';
|
|
topBar.style.alignItems = 'center';
|
|
topBar.style.padding = '10px 15px 15px 15px';
|
|
|
|
const title = document.createElement('h3');
|
|
title.innerHTML = t('folders');
|
|
title.style.color = '#fff';
|
|
title.style.margin = '0';
|
|
|
|
const isAllCollapsed = this.expandedFolders.size === 0;
|
|
const collapseAllBtn = document.createElement('button');
|
|
collapseAllBtn.innerHTML = isAllCollapsed ? (window.anomalous_browser_lang === 'zh' ? '鉃?灞曞紑鍏ㄩ儴' : '鉃?Expand All') : (window.anomalous_browser_lang === 'zh' ? '鉃?鏀惰捣鍏ㄩ儴' : '鉃?Collapse All');
|
|
collapseAllBtn.style.padding = '4px 8px';
|
|
collapseAllBtn.style.background = '#444';
|
|
collapseAllBtn.style.color = '#fff';
|
|
collapseAllBtn.style.border = 'none';
|
|
collapseAllBtn.style.borderRadius = '4px';
|
|
collapseAllBtn.style.cursor = 'pointer';
|
|
collapseAllBtn.onclick = () => {
|
|
if (isAllCollapsed) {
|
|
(this.foldersData || []).forEach(typeGroup => {
|
|
this.expandedFolders.add(typeGroup.type);
|
|
Object.keys(typeGroup.folders).forEach(path => {
|
|
this.expandedFolders.add(typeGroup.type + path);
|
|
});
|
|
});
|
|
} else {
|
|
this.expandedFolders.clear();
|
|
}
|
|
this.renderSidebar();
|
|
};
|
|
|
|
topBar.appendChild(title);
|
|
topBar.appendChild(collapseAllBtn);
|
|
this.sidebar.appendChild(topBar);
|
|
|
|
const searchBox = document.createElement('div');
|
|
searchBox.style.padding = '0 15px 15px 15px';
|
|
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = window.anomalous_browser_lang === 'zh' ? '馃攳 鎼滅储妯″瀷...' : '馃攳 Search models...';
|
|
searchInput.style.width = '100%';
|
|
searchInput.style.padding = '8px 12px';
|
|
searchInput.style.borderRadius = '8px';
|
|
searchInput.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
searchInput.style.background = 'rgba(0,0,0,0.2)';
|
|
searchInput.style.color = '#fff';
|
|
searchInput.style.boxSizing = 'border-box';
|
|
searchInput.style.outline = 'none';
|
|
searchInput.style.transition = 'border-color 0.2s';
|
|
searchInput.onfocus = () => searchInput.style.border = '1px solid #007aff';
|
|
searchInput.onblur = () => searchInput.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
|
|
searchInput.oninput = (e) => {
|
|
if (this.currentDetailModel) {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel);
|
|
this.detailPanel.innerHTML = '';
|
|
this.currentDetailModel = null;
|
|
this.grid.style.display = 'grid';
|
|
}
|
|
const val = e.target.value.toLowerCase();
|
|
const cards = this.grid.querySelectorAll('.anomalous-card');
|
|
cards.forEach(card => {
|
|
const titleEl = card.querySelector('.anomalous-card-title');
|
|
if (!titleEl) return;
|
|
const titleText = titleEl.innerText.toLowerCase();
|
|
if (titleText.includes(val)) {
|
|
card.style.display = 'flex';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
};
|
|
|
|
searchBox.appendChild(searchInput);
|
|
this.sidebar.appendChild(searchBox);
|
|
|
|
(this.foldersData || []).forEach(typeGroup => {
|
|
const header = document.createElement('div');
|
|
header.className = 'anomalous-type-header';
|
|
header.style.display = 'flex';
|
|
header.style.justifyContent = 'space-between';
|
|
header.style.cursor = 'pointer';
|
|
|
|
const isTypeExpanded = this.expandedFolders.has(typeGroup.type);
|
|
header.innerHTML = `<span>${typeGroup.label}</span> <span>${isTypeExpanded ? '鈻? : '鈻?}</span>`;
|
|
|
|
header.onclick = () => {
|
|
if (isTypeExpanded) this.expandedFolders.delete(typeGroup.type);
|
|
else this.expandedFolders.add(typeGroup.type);
|
|
this.renderSidebar();
|
|
};
|
|
this.sidebar.appendChild(header);
|
|
|
|
if (!isTypeExpanded) return;
|
|
|
|
const sortedPaths = Object.keys(typeGroup.folders).sort();
|
|
|
|
sortedPaths.forEach(path => {
|
|
const info = typeGroup.folders[path];
|
|
const parts = path.split('/').filter(p => p);
|
|
const parentPath = parts.length > 1 ? '/' + parts.slice(0, -1).join('/') : '/';
|
|
|
|
if (path !== '/' && parentPath !== '/') {
|
|
let parentId = typeGroup.type + parentPath;
|
|
if (!this.expandedFolders.has(parentId)) return;
|
|
}
|
|
|
|
const hasChildren = sortedPaths.some(p => p !== path && p.startsWith(path === '/' ? '/' : path + '/'));
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'anomalous-folder-item';
|
|
|
|
const depth = path === '/' ? 0 : parts.length;
|
|
item.style.paddingLeft = (15 + depth * 15) + 'px';
|
|
|
|
const myId = typeGroup.type + path;
|
|
const isExpanded = this.expandedFolders.has(myId);
|
|
|
|
let toggleIcon = '';
|
|
if (hasChildren) {
|
|
toggleIcon = `<span class="anomalous-folder-toggle" style="margin-right: 8px; width: 12px; display: inline-block; font-size: 0.8em; color: #888;">${isExpanded ? '鈻? : '鈻?}</span>`;
|
|
} else {
|
|
toggleIcon = `<span style="margin-right: 8px; width: 12px; display: inline-block;"></span>`;
|
|
}
|
|
|
|
item.innerHTML = `${toggleIcon}<span class="anomalous-folder-name" style="color: #ddd;">${info.name}</span> <span style="opacity:0.4; font-size:0.8em; margin-left: 5px;">${info.model_count}</span>`;
|
|
|
|
if (this.currentType === typeGroup.type && this.currentPathIdx === typeGroup.path_idx && this.currentSubfolder === path) {
|
|
item.classList.add('active');
|
|
}
|
|
|
|
item.onclick = (e) => {
|
|
if (e.target.classList.contains('anomalous-folder-toggle')) {
|
|
if (isExpanded) this.expandedFolders.delete(myId);
|
|
else this.expandedFolders.add(myId);
|
|
this.renderSidebar();
|
|
return;
|
|
}
|
|
this.currentType = typeGroup.type;
|
|
this.currentPathIdx = typeGroup.path_idx;
|
|
this.currentSubfolder = path;
|
|
|
|
this.hideAllPanels();
|
|
this.grid.style.display = 'grid';
|
|
|
|
this.renderSidebar();
|
|
this.loadModels();
|
|
};
|
|
|
|
this.sidebar.appendChild(item);
|
|
});
|
|
});
|
|
}
|
|
|
|
async loadModels() {
|
|
try {
|
|
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
|
|
const res = await fetch('/anomalous/models?' + params.toString());
|
|
const data = await res.json();
|
|
|
|
if (window.anomalous_update_hash_cache && data.models) {
|
|
window.anomalous_update_hash_cache(data.models);
|
|
}
|
|
|
|
this.models = data.models || [];
|
|
const ts = Date.now();
|
|
this.models.forEach(m => {
|
|
if (m.preview_url) {
|
|
m.preview_url += (m.preview_url.includes('?') ? '&' : '?') + 't=' + ts;
|
|
}
|
|
});
|
|
|
|
this.grid.innerHTML = '';
|
|
|
|
if (!data.models || data.models.length === 0) {
|
|
this.grid.innerHTML = `<div style="color:white; padding:20px;">${t('noModels')}</div>`;
|
|
return;
|
|
}
|
|
|
|
data.models.forEach(model => {
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-card';
|
|
if (model.preview_url) {
|
|
const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVideo) {
|
|
const video = document.createElement('video');
|
|
video.src = model.preview_url;
|
|
video.muted = true; video.loop = true; video.playsInline = true;
|
|
if (this.energySaving) {
|
|
video.pause();
|
|
card.addEventListener('mouseenter', () => video.play().catch(e => { }));
|
|
card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; });
|
|
} else {
|
|
video.autoplay = true;
|
|
}
|
|
card.appendChild(video);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = model.preview_url;
|
|
card.appendChild(img);
|
|
}
|
|
} else {
|
|
const ph = document.createElement('div');
|
|
ph.className = 'anomalous-card-placeholder';
|
|
ph.innerHTML = `<div style="text-align:center;color:#666;margin-top:80px;">${t('noPreview')}</div><div style="font-size:0.8em;text-align:center;opacity:0.5;margin-top:5px">${t('clickScan')}</div>`;
|
|
card.appendChild(ph);
|
|
}
|
|
const title = document.createElement('div');
|
|
if (model.metadata && model.metadata.baseModel) {
|
|
const badge = document.createElement('div');
|
|
badge.innerText = model.metadata.baseModel;
|
|
badge.style.position = 'absolute';
|
|
badge.style.top = '6px';
|
|
badge.style.left = '6px';
|
|
badge.style.background = 'rgba(0,0,0,0.85)';
|
|
badge.style.color = '#00ffcc';
|
|
badge.style.padding = '3px 6px';
|
|
badge.style.borderRadius = '4px';
|
|
badge.style.fontSize = '0.75em';
|
|
badge.style.fontWeight = 'bold';
|
|
badge.style.border = '1px solid rgba(0,255,204,0.3)';
|
|
badge.style.pointerEvents = 'none';
|
|
badge.style.zIndex = '10';
|
|
card.appendChild(badge);
|
|
}
|
|
title.className = 'anomalous-card-title';
|
|
title.innerText = model.filename;
|
|
card.appendChild(title);
|
|
|
|
card.onclick = () => { this.historyStack = []; this.currentDetailModel = model; this.showDetail(model); };
|
|
|
|
const applyBtn = document.createElement('button');
|
|
applyBtn.innerHTML = '鉃?;
|
|
applyBtn.title = t('applyToCanvas');
|
|
applyBtn.style.position = 'absolute';
|
|
applyBtn.style.top = '6px';
|
|
applyBtn.style.right = '6px';
|
|
applyBtn.style.background = 'rgba(0,0,0,0.7)';
|
|
applyBtn.style.color = '#fff';
|
|
applyBtn.style.border = '1px solid rgba(255,255,255,0.2)';
|
|
applyBtn.style.borderRadius = '4px';
|
|
applyBtn.style.cursor = 'pointer';
|
|
applyBtn.style.padding = '4px 6px';
|
|
applyBtn.style.zIndex = '20';
|
|
applyBtn.style.fontSize = '1em';
|
|
applyBtn.style.display = 'none';
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.innerHTML = '鈿欙笍';
|
|
editBtn.title = window.anomalous_browser_lang === 'zh' ? '缂栬緫妯″瀷 (Edit)' : 'Edit Model';
|
|
editBtn.style.position = 'absolute';
|
|
editBtn.style.top = '6px';
|
|
editBtn.style.right = '40px';
|
|
editBtn.style.width = '26px';
|
|
editBtn.style.height = '26px';
|
|
editBtn.style.borderRadius = '50%';
|
|
editBtn.style.border = 'none';
|
|
editBtn.style.background = 'rgba(0,0,0,0.7)';
|
|
editBtn.style.color = '#fff';
|
|
editBtn.style.cursor = 'pointer';
|
|
editBtn.style.display = 'none';
|
|
editBtn.style.alignItems = 'center';
|
|
editBtn.style.justifyContent = 'center';
|
|
editBtn.style.fontSize = '14px';
|
|
editBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.showEditModal(model);
|
|
};
|
|
card.appendChild(editBtn);
|
|
|
|
card.addEventListener('mouseenter', () => {
|
|
applyBtn.style.display = 'block';
|
|
editBtn.style.display = 'flex';
|
|
});
|
|
card.addEventListener('mouseleave', () => {
|
|
applyBtn.style.display = 'none';
|
|
editBtn.style.display = 'none';
|
|
});
|
|
|
|
applyBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.applyModelToCanvas(this.currentType, this.currentSubfolder, model);
|
|
};
|
|
card.appendChild(applyBtn);
|
|
|
|
|
|
this.grid.appendChild(card);
|
|
});
|
|
} catch (e) { }
|
|
}
|
|
|
|
|
|
applyModelToCanvas(type, subfolder, model) {
|
|
const nodeTypeMap = {
|
|
'checkpoints': 'CheckpointLoaderSimple',
|
|
'loras': 'LoraLoader',
|
|
'unet': 'UNETLoader',
|
|
'diffusion_models': 'UNETLoader'
|
|
};
|
|
const nodeType = nodeTypeMap[type];
|
|
if (!nodeType) {
|
|
alert(window.anomalous_browser_lang === 'zh' ? '涓嶆敮鎸佽绫诲瀷妯″瀷鐨勮嚜鍔ㄥ簲鐢ㄣ€? : 'Unsupported model type for auto-apply.');
|
|
return;
|
|
}
|
|
|
|
const node = LiteGraph.createNode(nodeType);
|
|
if (!node) {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '鍒涘缓鑺傜偣澶辫触: ' : 'Failed to create node: ') + nodeType);
|
|
return;
|
|
}
|
|
|
|
if (app.canvas && app.canvas.graph_mouse) {
|
|
node.pos = [
|
|
app.canvas.graph_mouse[0] || (window.innerWidth / 2),
|
|
app.canvas.graph_mouse[1] || (window.innerHeight / 2)
|
|
];
|
|
} else {
|
|
node.pos = [window.innerWidth / 2, window.innerHeight / 2];
|
|
}
|
|
|
|
app.graph.add(node);
|
|
|
|
const sub = subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
const relPath = sub ? `${sub}/${model.filename}` : model.filename;
|
|
|
|
this.setWidgetValuePath(node, relPath);
|
|
|
|
this.close();
|
|
|
|
// 绮樺埌榧犳爣涓婄殑閫昏緫
|
|
let isSticking = true;
|
|
const stickHandler = (e) => {
|
|
if (!isSticking || !app.canvas) return;
|
|
const canvas = app.canvas;
|
|
|
|
// LiteGraph鍐呯疆浜嗗潗鏍囪浆鎹紝瀹冧細瀹岀編澶勭悊缂╂斁鍜屽亸绉诲甫鏉ョ殑鍧愭爣鍋忕Щ闂
|
|
let canvasX, canvasY;
|
|
if (canvas.convertEventToCanvasOffset) {
|
|
const pos = canvas.convertEventToCanvasOffset(e);
|
|
canvasX = pos[0];
|
|
canvasY = pos[1];
|
|
} else {
|
|
// 濡傛灉API涓嶅彲鐢紝浣跨敤鏍囧噯闄嶇骇璁$畻
|
|
const rect = canvas.canvas.getBoundingClientRect();
|
|
canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale;
|
|
canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale;
|
|
}
|
|
|
|
const w = (node.size && node.size[0]) ? node.size[0] : 200;
|
|
node.pos = [canvasX - w / 2, canvasY - 20];
|
|
canvas.setDirty(true, true);
|
|
};
|
|
const dropHandler = (e) => {
|
|
if (!isSticking) return;
|
|
isSticking = false;
|
|
window.removeEventListener('mousemove', stickHandler, true);
|
|
window.removeEventListener('pointerdown', dropHandler, true);
|
|
window.removeEventListener('mousedown', dropHandler, true);
|
|
window.removeEventListener('click', dropHandler, true);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
window.addEventListener('mousemove', stickHandler, true);
|
|
setTimeout(() => {
|
|
window.addEventListener('pointerdown', dropHandler, true);
|
|
window.addEventListener('mousedown', dropHandler, true);
|
|
window.addEventListener('click', dropHandler, true);
|
|
}, 100);
|
|
}
|
|
|
|
|
|
stopMediaInContainer(container) {
|
|
if (!container) return;
|
|
const mediaElements = container.querySelectorAll('video, audio');
|
|
mediaElements.forEach(media => {
|
|
media.pause();
|
|
media.removeAttribute('src');
|
|
media.load();
|
|
});
|
|
}
|
|
|
|
showDetail(model) {
|
|
if (this.isPickingModelForNode) {
|
|
const targetNode = this.isPickingModelForNode.node;
|
|
const targetWidget = this.isPickingModelForNode.widget;
|
|
|
|
const normVal = (model.filename).replace(/\\/g, '/');
|
|
let foundPath = model.filename;
|
|
if (targetWidget.options && targetWidget.options.values) {
|
|
const exactMatch = targetWidget.options.values.find(v => typeof v === 'string' && v.replace(/\\/g, '/').endsWith(normVal));
|
|
if (exactMatch) foundPath = exactMatch;
|
|
}
|
|
targetWidget.value = foundPath;
|
|
|
|
delete targetNode.color;
|
|
delete targetNode.bgcolor;
|
|
targetNode.has_errors = false;
|
|
if (targetWidget.callback) targetWidget.callback(targetWidget.value, app.canvas, targetNode, app.canvas.graph_mouse, null);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
|
|
this.isPickingModelForNode = null;
|
|
const banner = document.getElementById('anomalous-picker-banner');
|
|
if (banner) banner.remove();
|
|
|
|
if (this.grid) this.grid.style.display = 'none';
|
|
this.doctorPanel.style.display = 'flex';
|
|
this.diagnoseNode(targetNode);
|
|
return;
|
|
}
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
this.grid.style.display = 'none';
|
|
this.detailPanel.style.display = 'flex';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.width = '100%';
|
|
header.style.padding = '8px 15px';
|
|
header.style.background = 'var(--comfy-menu-bg, #333)';
|
|
header.style.borderBottom = '1px solid var(--border-color, #444)';
|
|
header.style.display = 'flex';
|
|
header.style.alignItems = 'center';
|
|
header.style.boxSizing = 'border-box';
|
|
|
|
const backBtn = document.createElement('button');
|
|
let isFromDoctor = false;
|
|
let isFromAssistant = false;
|
|
if (this.historyStack.length > 0) {
|
|
const lastHistory = this.historyStack[this.historyStack.length - 1];
|
|
if (lastHistory.type === 'doctor') {
|
|
isFromDoctor = true;
|
|
} else if (lastHistory.type === 'assistant') {
|
|
isFromAssistant = true;
|
|
}
|
|
}
|
|
|
|
if (isFromDoctor) {
|
|
backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '猬?杩斿洖鍖荤敓闈㈡澘' : '猬?Back to Doctor';
|
|
backBtn.style.background = '#8AB4F8';
|
|
backBtn.style.color = '#000';
|
|
} else if (isFromAssistant) {
|
|
backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '猬?杩斿洖鍔╂墜闈㈡澘' : '猬?Back to Assistant';
|
|
backBtn.style.background = '#8AB4F8';
|
|
backBtn.style.color = '#000';
|
|
} else {
|
|
backBtn.innerHTML = this.historyStack.length > 0 ? t('backToPrev') : t('back');
|
|
backBtn.style.background = '#444';
|
|
backBtn.style.color = '#fff';
|
|
}
|
|
backBtn.style.padding = '6px 12px';
|
|
backBtn.style.border = 'none';
|
|
backBtn.style.borderRadius = '4px';
|
|
backBtn.style.cursor = 'pointer';
|
|
backBtn.onclick = () => {
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
if (this.historyStack.length > 0) {
|
|
const prev = this.historyStack.pop();
|
|
if (prev.type === 'doctor') {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.doctorPanel.style.display = 'flex';
|
|
return;
|
|
}
|
|
if (prev.type === 'assistant') {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.assistantPanel.style.display = 'flex';
|
|
return;
|
|
}
|
|
this.currentType = prev.type;
|
|
this.currentPathIdx = prev.pathIdx;
|
|
this.currentSubfolder = prev.subfolder;
|
|
this.currentDetailModel = prev.model;
|
|
this.renderSidebar();
|
|
this.showDetail(prev.model);
|
|
} else {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.grid.style.display = 'grid';
|
|
}
|
|
};
|
|
|
|
const title = document.createElement('h2');
|
|
title.innerHTML = model.filename;
|
|
title.style.margin = '0 20px 0 20px';
|
|
title.style.color = '#fff';
|
|
title.style.fontSize = '1.2em';
|
|
// 寮哄埗鍗曡骞舵孩鍑烘樉绀虹渷鐣ュ彿
|
|
title.style.whiteSpace = 'nowrap';
|
|
title.style.overflow = 'hidden';
|
|
title.style.textOverflow = 'ellipsis';
|
|
title.style.flex = '1'; // 鎾戝紑鍓╀綑绌洪棿锛屾妸鍙充晶鎸夐挳鎸ゅ埌鏈€鍙宠竟
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.innerHTML = t('delModel');
|
|
delBtn.style.padding = '6px 12px';
|
|
delBtn.style.background = '#ff4444';
|
|
delBtn.style.color = '#fff';
|
|
delBtn.style.border = 'none';
|
|
delBtn.style.borderRadius = '4px';
|
|
delBtn.style.cursor = 'pointer';
|
|
delBtn.style.marginLeft = 'auto'; // push to the right
|
|
delBtn.style.whiteSpace = 'nowrap';
|
|
delBtn.onclick = async () => {
|
|
if (!confirm(`${t('delConfirm')} ${model.filename} ${t('delConfirm2')}`)) return;
|
|
try {
|
|
const res = await fetch('/anomalous/delete_model', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
alert(t('delSuccess') + data.deleted.join('\n') + t('delNote'));
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.grid.style.display = 'grid';
|
|
this.loadModels(); // refresh grid
|
|
} else {
|
|
alert(t('delFail') + data.message);
|
|
}
|
|
} catch (e) {
|
|
alert(t('delFail') + e.message);
|
|
}
|
|
};
|
|
|
|
const jumpBtn = document.createElement('button');
|
|
jumpBtn.innerHTML = '猬囷笍';
|
|
jumpBtn.title = t('jumpToBottom') || 'Jump to bottom';
|
|
jumpBtn.style.padding = '6px 12px';
|
|
jumpBtn.style.background = '#444';
|
|
jumpBtn.style.color = '#fff';
|
|
jumpBtn.style.border = 'none';
|
|
jumpBtn.style.borderRadius = '4px';
|
|
jumpBtn.style.cursor = 'pointer';
|
|
jumpBtn.style.marginLeft = '10px';
|
|
jumpBtn.onclick = () => {
|
|
// Find rightPanel which is created later, so we bind it dynamically
|
|
const rp = this.detailPanel.querySelector('.anomalous-split-right');
|
|
if (rp) rp.scrollTo({ top: rp.scrollHeight, behavior: 'smooth' });
|
|
};
|
|
|
|
header.appendChild(backBtn);
|
|
header.appendChild(title);
|
|
header.appendChild(delBtn);
|
|
header.appendChild(jumpBtn);
|
|
|
|
const applyDetailBtn = document.createElement('button');
|
|
applyDetailBtn.innerHTML = t('applyToCanvas') || 'Apply to Canvas';
|
|
applyDetailBtn.style.padding = '6px 12px';
|
|
applyDetailBtn.style.background = '#007bff';
|
|
applyDetailBtn.style.color = '#fff';
|
|
applyDetailBtn.style.border = 'none';
|
|
applyDetailBtn.style.borderRadius = '4px';
|
|
applyDetailBtn.style.cursor = 'pointer';
|
|
applyDetailBtn.style.marginLeft = '10px';
|
|
applyDetailBtn.style.fontWeight = 'bold';
|
|
applyDetailBtn.onclick = () => {
|
|
this.applyModelToCanvas(this.currentType, this.currentSubfolder, model);
|
|
};
|
|
header.appendChild(applyDetailBtn);
|
|
|
|
|
|
const splitContainer = document.createElement('div');
|
|
splitContainer.className = 'anomalous-split-container';
|
|
|
|
const leftPanel = document.createElement('div');
|
|
leftPanel.className = 'anomalous-split-left';
|
|
|
|
let isMediaRendered = null;
|
|
const renderMedia = (shouldRender) => {
|
|
if (shouldRender === isMediaRendered) return;
|
|
isMediaRendered = shouldRender;
|
|
leftPanel.innerHTML = '';
|
|
if (!shouldRender) return;
|
|
|
|
if (model.preview_url) {
|
|
const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVideo) {
|
|
const video = document.createElement('video');
|
|
video.src = model.preview_url;
|
|
video.controls = true;
|
|
video.autoplay = true;
|
|
video.loop = true;
|
|
video.style.width = '100%';
|
|
video.style.height = '100%';
|
|
video.style.objectFit = 'contain';
|
|
leftPanel.appendChild(video);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = model.preview_url;
|
|
img.style.width = '100%';
|
|
img.style.height = '100%';
|
|
img.style.objectFit = 'contain';
|
|
leftPanel.appendChild(img);
|
|
}
|
|
} else {
|
|
leftPanel.innerHTML = `<div style="color:#aaa; text-align:center; margin-top:50px;">${t('noPreview')}</div>`;
|
|
}
|
|
};
|
|
|
|
const containerEl = document.getElementById('anomalous-container');
|
|
this.currentDetailObserver = new ResizeObserver(entries => {
|
|
for (let entry of entries) {
|
|
renderMedia(entry.contentRect.width >= 750);
|
|
}
|
|
});
|
|
this.currentDetailObserver.observe(containerEl);
|
|
|
|
const resizer = document.createElement('div');
|
|
resizer.className = 'anomalous-resizer';
|
|
|
|
let isResizing = false;
|
|
resizer.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
document.body.style.cursor = 'col-resize';
|
|
e.preventDefault();
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
const containerRect = splitContainer.getBoundingClientRect();
|
|
let newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
if (newWidth < 20) newWidth = 20;
|
|
if (newWidth > 80) newWidth = 80;
|
|
leftPanel.style.width = newWidth + '%';
|
|
rightPanel.style.width = (100 - newWidth) + '%';
|
|
});
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.body.style.cursor = '';
|
|
}
|
|
});
|
|
|
|
const rightPanel = document.createElement('div');
|
|
rightPanel.className = 'anomalous-split-right';
|
|
rightPanel.style.display = 'flex';
|
|
rightPanel.style.flexDirection = 'column';
|
|
rightPanel.style.height = '100%';
|
|
rightPanel.style.boxSizing = 'border-box';
|
|
rightPanel.style.overflow = 'auto';
|
|
rightPanel.style.padding = '15px';
|
|
rightPanel.style.color = '#eee';
|
|
|
|
const m = model.metadata || {};
|
|
|
|
// 1. Top Bar (Title + Size + Model + Button) in a single compact row if possible
|
|
const topRow = document.createElement('div');
|
|
topRow.style.flexShrink = '0';
|
|
topRow.style.display = 'flex';
|
|
topRow.style.flexWrap = 'wrap';
|
|
topRow.style.alignItems = 'center';
|
|
topRow.style.gap = '10px';
|
|
topRow.style.paddingBottom = '10px';
|
|
topRow.style.marginBottom = '10px';
|
|
topRow.style.borderBottom = '1px solid #444';
|
|
|
|
const titleEl = document.createElement('h3');
|
|
titleEl.style.margin = '0';
|
|
titleEl.style.fontSize = '1.3em';
|
|
titleEl.style.marginRight = '10px';
|
|
titleEl.innerText = m.custom_name || m.name || model.filename;
|
|
if (m.custom_name) {
|
|
titleEl.style.color = '#88ff88';
|
|
}
|
|
topRow.appendChild(titleEl);
|
|
|
|
const metaSpan = document.createElement('span');
|
|
metaSpan.style.fontSize = '0.9em';
|
|
metaSpan.style.color = '#aaa';
|
|
metaSpan.innerHTML = `<strong>Size:</strong> ${model.size_mb} MB` + (m.baseModel ? ` <strong style="margin-left:10px;">Base:</strong> ${m.baseModel}` : '');
|
|
topRow.appendChild(metaSpan);
|
|
|
|
if (m.civitai_url) {
|
|
const cBtn = document.createElement('a');
|
|
cBtn.href = m.civitai_url;
|
|
cBtn.target = '_blank';
|
|
cBtn.innerHTML = '馃寪 Civitai';
|
|
cBtn.style.marginLeft = 'auto';
|
|
cBtn.style.padding = '4px 8px';
|
|
cBtn.style.background = '#1a73e8';
|
|
cBtn.style.color = '#fff';
|
|
cBtn.style.textDecoration = 'none';
|
|
cBtn.style.borderRadius = '4px';
|
|
cBtn.style.fontSize = '0.85em';
|
|
cBtn.style.fontWeight = 'bold';
|
|
topRow.appendChild(cBtn);
|
|
}
|
|
|
|
const editMetaBtn = document.createElement('button');
|
|
editMetaBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鈿欙笍 缂栬緫' : '鈿欙笍 Edit';
|
|
editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto';
|
|
editMetaBtn.style.padding = '4px 8px';
|
|
editMetaBtn.style.background = '#444';
|
|
editMetaBtn.style.color = '#fff';
|
|
editMetaBtn.style.border = 'none';
|
|
editMetaBtn.style.borderRadius = '4px';
|
|
editMetaBtn.style.fontSize = '0.85em';
|
|
editMetaBtn.style.fontWeight = 'bold';
|
|
editMetaBtn.style.cursor = 'pointer';
|
|
editMetaBtn.onclick = () => {
|
|
this.showEditModal(model);
|
|
};
|
|
topRow.appendChild(editMetaBtn);
|
|
|
|
rightPanel.appendChild(topRow);
|
|
|
|
// 1.5 Custom Notes Section (Google Material Card)
|
|
if (m.custom_notes) {
|
|
const notesCard = document.createElement('div');
|
|
notesCard.style.flexShrink = '0';
|
|
notesCard.style.marginBottom = '15px';
|
|
notesCard.style.padding = '12px 16px';
|
|
// Dark yellowish/khaki paper background for dark mode notebook feel
|
|
notesCard.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)';
|
|
notesCard.style.border = '1px solid #3c4043';
|
|
notesCard.style.borderLeft = '4px solid #a38d53';
|
|
notesCard.style.borderRadius = '4px 8px 8px 4px';
|
|
notesCard.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)';
|
|
notesCard.style.position = 'relative';
|
|
notesCard.style.fontFamily = 'Inter, Roboto, sans-serif';
|
|
// Faint notebook lines background
|
|
notesCard.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)';
|
|
notesCard.style.backgroundAttachment = 'local'; // ensures lines scroll with text
|
|
|
|
const notesHeader = document.createElement('div');
|
|
notesHeader.style.display = 'flex';
|
|
notesHeader.style.justifyContent = 'space-between';
|
|
notesHeader.style.alignItems = 'center';
|
|
notesHeader.style.marginBottom = '8px';
|
|
|
|
const notesTitle = document.createElement('div');
|
|
notesTitle.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃摀 涓撳睘澶囨敞 (Notes)' : '馃摀 Custom Notes';
|
|
notesTitle.style.color = '#a38d53';
|
|
notesTitle.style.fontWeight = '600';
|
|
notesTitle.style.fontSize = '0.85em';
|
|
notesTitle.style.letterSpacing = '0.5px';
|
|
|
|
const notesEditBtn = document.createElement('button');
|
|
notesEditBtn.innerHTML = '鉁忥笍';
|
|
notesEditBtn.title = window.anomalous_browser_lang === 'zh' ? '缂栬緫澶囨敞' : 'Edit Notes';
|
|
notesEditBtn.style.background = 'transparent';
|
|
notesEditBtn.style.border = 'none';
|
|
notesEditBtn.style.color = '#a38d53';
|
|
notesEditBtn.style.cursor = 'pointer';
|
|
notesEditBtn.style.padding = '2px';
|
|
notesEditBtn.style.fontSize = '1em';
|
|
notesEditBtn.style.borderRadius = '50%';
|
|
notesEditBtn.style.display = 'flex';
|
|
notesEditBtn.style.alignItems = 'center';
|
|
notesEditBtn.style.justifyContent = 'center';
|
|
notesEditBtn.style.opacity = '0.7';
|
|
notesEditBtn.onmouseover = () => notesEditBtn.style.opacity = '1';
|
|
notesEditBtn.onmouseout = () => notesEditBtn.style.opacity = '0.7';
|
|
notesEditBtn.onclick = () => {
|
|
this.showEditModal(model);
|
|
};
|
|
|
|
notesHeader.appendChild(notesTitle);
|
|
notesHeader.appendChild(notesEditBtn);
|
|
|
|
const notesContent = document.createElement('div');
|
|
notesContent.innerText = m.custom_notes;
|
|
notesContent.style.color = '#d1c9b4'; // Warm off-white
|
|
|
|
notesContent.style.fontSize = '0.95em';
|
|
notesContent.style.lineHeight = '24px'; // Matches the repeating gradient exactly
|
|
notesContent.style.whiteSpace = 'pre-wrap';
|
|
notesContent.style.fontFamily = '"Consolas", "Courier New", monospace'; // Handwriting / typewriter feel
|
|
// Removed text shadow for cleaner look
|
|
|
|
notesCard.appendChild(notesHeader);
|
|
notesCard.appendChild(notesContent);
|
|
|
|
rightPanel.appendChild(notesCard);
|
|
}
|
|
// 1.8 Generated Gallery Button
|
|
const galleryBtnCont = document.createElement('div');
|
|
galleryBtnCont.style.flexShrink = '0';
|
|
galleryBtnCont.style.marginBottom = '15px';
|
|
|
|
const galleryBtn = document.createElement('button');
|
|
galleryBtn.className = 'anomalous-nb-add-btn';
|
|
galleryBtn.style.width = '100%';
|
|
galleryBtn.style.display = 'flex';
|
|
galleryBtn.style.justifyContent = 'center';
|
|
galleryBtn.style.alignItems = 'center';
|
|
galleryBtn.style.gap = '8px';
|
|
galleryBtn.style.padding = '10px';
|
|
galleryBtn.style.background = '#2a2b2f';
|
|
galleryBtn.style.border = '1px solid #3c4043';
|
|
galleryBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃柤锔?鏌ョ湅鍘嗗彶鐢熸垚浣滃搧' : '馃柤锔?View Generated Gallery';
|
|
galleryBtn.onmouseover = () => { galleryBtn.style.background = '#3c4043'; galleryBtn.style.borderColor = '#8ab4f8'; };
|
|
galleryBtn.onmouseout = () => { galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.borderColor = '#3c4043'; };
|
|
|
|
galleryBtn.onclick = () => {
|
|
this.showGeneratedGallery(model);
|
|
};
|
|
galleryBtnCont.appendChild(galleryBtn);
|
|
rightPanel.appendChild(galleryBtnCont);
|
|
|
|
// 2. Trained Words Section
|
|
if (m.trainedWords && m.trainedWords.length > 0) {
|
|
const twCont = document.createElement('div');
|
|
twCont.style.flexShrink = '0';
|
|
twCont.style.marginBottom = '10px';
|
|
|
|
const twHeader = document.createElement('div');
|
|
twHeader.style.display = 'flex';
|
|
twHeader.style.alignItems = 'center';
|
|
twHeader.style.marginBottom = '5px';
|
|
|
|
const twLabel = document.createElement('strong');
|
|
twLabel.innerText = 'Trained Words:';
|
|
twHeader.appendChild(twLabel);
|
|
|
|
const copyAll = document.createElement('button');
|
|
copyAll.innerText = t('copyAll');
|
|
copyAll.style.marginLeft = '10px';
|
|
copyAll.style.padding = '2px 6px';
|
|
copyAll.style.background = '#444';
|
|
copyAll.style.color = '#fff';
|
|
copyAll.style.border = 'none';
|
|
copyAll.style.borderRadius = '3px';
|
|
copyAll.style.cursor = 'pointer';
|
|
copyAll.style.fontSize = '0.8em';
|
|
copyAll.onclick = () => {
|
|
const allWords = m.trainedWords.join(', ');
|
|
navigator.clipboard.writeText(allWords).then(() => {
|
|
const old = copyAll.innerText;
|
|
copyAll.innerText = t('copied');
|
|
setTimeout(() => { copyAll.innerText = old; }, 1500);
|
|
});
|
|
};
|
|
twHeader.appendChild(copyAll);
|
|
twCont.appendChild(twHeader);
|
|
|
|
const tagsCont = document.createElement('div');
|
|
tagsCont.style.display = 'flex';
|
|
tagsCont.style.flexWrap = 'wrap';
|
|
tagsCont.style.gap = '4px';
|
|
|
|
m.trainedWords.forEach(w => {
|
|
const tag = document.createElement('span');
|
|
tag.innerText = w;
|
|
tag.style.background = '#333';
|
|
tag.style.padding = '2px 6px';
|
|
tag.style.borderRadius = '4px';
|
|
tag.style.fontSize = '0.85em';
|
|
tag.style.cursor = 'pointer';
|
|
tag.style.border = '1px solid #555';
|
|
tag.title = t('clickToCopy') + w;
|
|
tag.onclick = () => {
|
|
navigator.clipboard.writeText(w).then(() => {
|
|
tag.style.background = '#28a745';
|
|
setTimeout(() => { tag.style.background = '#333'; }, 500);
|
|
});
|
|
};
|
|
tagsCont.appendChild(tag);
|
|
});
|
|
twCont.appendChild(tagsCont);
|
|
rightPanel.appendChild(twCont);
|
|
}
|
|
|
|
// 3. Description Section (Expands to fill remaining height)
|
|
if (m.description) {
|
|
const descCont = document.createElement('div');
|
|
descCont.style.flex = 'none';
|
|
descCont.style.display = 'flex';
|
|
descCont.style.flexDirection = 'column';
|
|
// important for flex scroll
|
|
|
|
const descLabel = document.createElement('strong');
|
|
descLabel.innerText = 'Description:';
|
|
descLabel.style.marginBottom = '5px';
|
|
descCont.appendChild(descLabel);
|
|
|
|
const descText = document.createElement('div');
|
|
descText.style.flex = 'none';
|
|
|
|
descText.style.background = '#222';
|
|
descText.style.padding = '10px';
|
|
descText.style.borderRadius = '6px';
|
|
descText.style.border = '1px solid #333';
|
|
descText.style.fontSize = '0.95em';
|
|
descText.style.lineHeight = '1.4';
|
|
descText.innerHTML = m.description;
|
|
descCont.appendChild(descText);
|
|
|
|
rightPanel.appendChild(descCont);
|
|
}
|
|
|
|
// 4. Notes Section
|
|
if (m.notes) {
|
|
const notesCont = document.createElement('div');
|
|
notesCont.style.flexShrink = '0';
|
|
notesCont.style.marginTop = '10px';
|
|
|
|
const notesLabel = document.createElement('strong');
|
|
notesLabel.innerText = 'Notes:';
|
|
notesCont.appendChild(notesLabel);
|
|
|
|
const notesText = document.createElement('div');
|
|
notesText.style.background = '#332b00';
|
|
notesText.style.padding = '8px';
|
|
notesText.style.borderRadius = '6px';
|
|
notesText.style.marginTop = '5px';
|
|
notesText.style.border = '1px solid #554400';
|
|
notesText.style.fontSize = '0.9em';
|
|
notesText.innerHTML = m.notes;
|
|
notesCont.appendChild(notesText);
|
|
|
|
rightPanel.appendChild(notesCont);
|
|
}
|
|
|
|
// --- Compatible Models Section ---
|
|
if (m.baseModel) {
|
|
const compSec = document.createElement('div');
|
|
compSec.className = 'anomalous-compatible-section';
|
|
|
|
const compTitle = document.createElement('div');
|
|
compTitle.className = 'anomalous-compatible-title';
|
|
compTitle.innerHTML = `${t('compatibleModels') || '馃敆 Compatible'} <span style="font-size:0.8em; opacity:0.6;">(${m.baseModel})</span>`;
|
|
|
|
const compList = document.createElement('div');
|
|
compList.className = 'anomalous-compatible-list';
|
|
compList.innerHTML = `<span style="color:#888;">${t('loadingCompatible') || 'Loading...'}</span>`;
|
|
|
|
compSec.appendChild(compTitle);
|
|
compSec.appendChild(compList);
|
|
rightPanel.appendChild(compSec);
|
|
|
|
const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras';
|
|
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(m.baseModel)}&target_type=${encodeURIComponent(targetType)}`)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
compList.innerHTML = '';
|
|
if (window.anomalous_update_hash_cache && d.models) {
|
|
window.anomalous_update_hash_cache(d.models);
|
|
}
|
|
if (!d.models || d.models.length === 0) {
|
|
compList.innerHTML = `<span style="color:#888;">No compatible models found.</span>`;
|
|
return;
|
|
}
|
|
d.models.forEach(m_comp => {
|
|
const mItem = document.createElement('div');
|
|
mItem.className = 'anomalous-compatible-item';
|
|
mItem.title = m_comp.filename;
|
|
|
|
let thumb = '';
|
|
if (m_comp.preview_url) {
|
|
const isVid = m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVid) thumb = `<video src="${m_comp.preview_url}" muted loop playsinline></video>`;
|
|
else thumb = `<img src="${m_comp.preview_url}" />`;
|
|
} else {
|
|
thumb = `<div style="width:30px; height:30px; background:#222; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; color:#555;">?</div>`;
|
|
}
|
|
|
|
mItem.innerHTML = `${thumb}<div class="anomalous-compatible-item-name">${m_comp.filename}</div>`;
|
|
|
|
if (m_comp.preview_url && (m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i))) {
|
|
mItem.onmouseenter = () => { const v = mItem.querySelector('video'); if (v) v.play().catch(e => { }); };
|
|
mItem.onmouseleave = () => { const v = mItem.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
|
|
}
|
|
|
|
mItem.onclick = () => {
|
|
this.historyStack.push({
|
|
type: this.currentType,
|
|
pathIdx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
model: this.currentDetailModel
|
|
});
|
|
|
|
this.currentType = m_comp.type;
|
|
this.currentPathIdx = m_comp.path_idx;
|
|
this.currentSubfolder = m_comp.subfolder;
|
|
this.currentDetailModel = m_comp;
|
|
|
|
this.renderSidebar();
|
|
this.showDetail(m_comp);
|
|
};
|
|
|
|
compList.appendChild(mItem);
|
|
});
|
|
})
|
|
.catch(e => {
|
|
compList.innerHTML = `<span style="color:#ff4444;">Failed to load.</span>`;
|
|
});
|
|
}
|
|
// ---------------------------------
|
|
|
|
splitContainer.appendChild(leftPanel);
|
|
splitContainer.appendChild(resizer);
|
|
splitContainer.appendChild(rightPanel);
|
|
|
|
this.detailPanel.appendChild(header);
|
|
this.detailPanel.appendChild(splitContainer);
|
|
}
|
|
|
|
show() {
|
|
this.modal.classList.add('visible');
|
|
if (!this.foldersData) {
|
|
this.loadFolders();
|
|
} else {
|
|
this.loadModels();
|
|
}
|
|
}
|
|
|
|
close() { this.modal.classList.remove('visible'); }
|
|
|
|
async showNotebooks() {
|
|
if (this.nbInitialized) {
|
|
this.refreshNotebooks(true);
|
|
return;
|
|
}
|
|
this.nbInitialized = true;
|
|
|
|
this.nbPanel.innerHTML = '';
|
|
|
|
const nbContainer = document.createElement('div');
|
|
nbContainer.className = 'anomalous-nb-container';
|
|
|
|
const nbHeader = document.createElement('div');
|
|
nbHeader.className = 'anomalous-nb-header';
|
|
nbHeader.innerHTML = `<h2>${t('notebookTitle')}</h2>`;
|
|
const closeNb = document.createElement('span');
|
|
closeNb.className = 'anomalous-nb-close';
|
|
closeNb.innerHTML = '×';
|
|
closeNb.onclick = () => { this.nbPanel.style.display = 'none'; };
|
|
nbHeader.appendChild(closeNb);
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'anomalous-nb-body';
|
|
|
|
// Sidebar for notebooks list
|
|
const sidebar = document.createElement('div');
|
|
sidebar.className = 'anomalous-nb-sidebar';
|
|
|
|
const nbList = document.createElement('div');
|
|
nbList.className = 'anomalous-nb-list';
|
|
|
|
const btnRow = document.createElement('div');
|
|
btnRow.style.padding = '10px';
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.gap = '5px';
|
|
|
|
const createBtn = document.createElement('button');
|
|
createBtn.innerHTML = `鉃?<span class="anomalous-nb-create-text">${t('createNotebook')}</span>`;
|
|
createBtn.className = 'anomalous-btn-primary';
|
|
|
|
const createInput = document.createElement('input');
|
|
createInput.type = 'text';
|
|
createInput.placeholder = t('newNotebookName');
|
|
createInput.style.display = 'none';
|
|
createInput.style.flex = '1';
|
|
createInput.style.padding = '4px';
|
|
createInput.style.background = '#222';
|
|
createInput.style.color = '#fff';
|
|
createInput.style.border = '1px solid #555';
|
|
createInput.style.borderRadius = '4px';
|
|
|
|
createBtn.onclick = () => {
|
|
if (createInput.style.display === 'none') {
|
|
createInput.style.display = 'block';
|
|
createBtn.innerHTML = '鉁?;
|
|
createInput.focus();
|
|
} else {
|
|
const name = createInput.value.trim();
|
|
if (name) {
|
|
this.currentNotebook = { filename: name + '.json', name: name, data: { baseModel: '', mainModel: null, loras: [], promptEn: '', promptZh: '' } };
|
|
this.saveCurrentNotebook();
|
|
this.renderNotebookEditor();
|
|
createInput.value = '';
|
|
}
|
|
createInput.style.display = 'none';
|
|
createBtn.innerHTML = `鉃?<span class="anomalous-nb-create-text">${t('createNotebook')}</span>`;
|
|
}
|
|
};
|
|
|
|
btnRow.appendChild(createInput);
|
|
btnRow.appendChild(createBtn);
|
|
sidebar.appendChild(btnRow);
|
|
sidebar.appendChild(nbList);
|
|
|
|
// Editor area
|
|
this.nbEditor = document.createElement('div');
|
|
this.nbEditor.className = 'anomalous-nb-editor';
|
|
|
|
body.appendChild(sidebar);
|
|
body.appendChild(this.nbEditor);
|
|
|
|
nbContainer.appendChild(nbHeader);
|
|
nbContainer.appendChild(body);
|
|
|
|
this.nbPanel.appendChild(nbContainer);
|
|
|
|
this.nbListEl = nbList;
|
|
this.refreshNotebooks(true);
|
|
}
|
|
|
|
async translateText(text) {
|
|
if (!text || !text.trim()) return "";
|
|
try {
|
|
const res = await fetch('/anomalous/translate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text: text, target_lang: window.anomalous_browser_lang === 'zh' ? 'zh-CN' : 'en' })
|
|
});
|
|
const data = await res.json();
|
|
return data.translated || text;
|
|
} catch (e) { return text; }
|
|
}
|
|
|
|
async loadGalleryImages(page = 1, reset = false) {
|
|
if (this.galleryLoading) return;
|
|
this.galleryLoading = true;
|
|
this.gallerySentinel.innerHTML = window.anomalous_browser_lang === 'zh' ? '鍔犺浇涓?..' : 'Loading...';
|
|
|
|
try {
|
|
const res = await fetch(`/anomalous/gallery_images?page=${page}&limit=50`);
|
|
const data = await res.json();
|
|
|
|
if (reset) {
|
|
// Clear existing cards
|
|
const cards = this.galleryGrid.querySelectorAll('.anomalous-gallery-card');
|
|
cards.forEach(c => c.remove());
|
|
this.galleryLoaded = true;
|
|
}
|
|
|
|
if (data.images && data.images.length > 0) {
|
|
data.images.forEach(imgData => {
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-gallery-card';
|
|
|
|
const q_sub = encodeURIComponent(imgData.subfolder);
|
|
const q_file = encodeURIComponent(imgData.filename);
|
|
const imgUrl = `/view?filename=${q_file}&subfolder=${q_sub}&type=output`;
|
|
|
|
const img = document.createElement('img');
|
|
img.src = imgUrl;
|
|
img.loading = 'lazy';
|
|
img.draggable = true;
|
|
|
|
// Drag and drop support for ComfyUI
|
|
img.addEventListener('dragstart', (e) => {
|
|
const fullUrl = new URL(imgUrl, window.location.href).href;
|
|
e.dataTransfer.setData('text/uri-list', fullUrl);
|
|
e.dataTransfer.setData('text/plain', fullUrl);
|
|
|
|
// Fix for Chromium failing to initiate drag for extremely large (Hires Fix) images
|
|
if (window.anomalousDragGhostImg) {
|
|
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
|
|
}
|
|
});
|
|
|
|
// Click to view
|
|
img.onclick = () => {
|
|
if (this.gallerySelectModel) {
|
|
const model = this.gallerySelectModel;
|
|
fetch('/anomalous/set_custom_cover', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
source_image: imgData.subfolder ? imgData.subfolder + '/' + imgData.filename : imgData.filename
|
|
})
|
|
}).then(res => res.json()).then(async data => {
|
|
if (data.status === 'success') {
|
|
const tempModel = this.gallerySelectModel;
|
|
this.gallerySelectModel = null;
|
|
const banner = document.getElementById('anomalous-gallery-select-banner');
|
|
if (banner) banner.style.display = 'none';
|
|
this.galleryPanel.style.display = 'none';
|
|
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
|
|
if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
this.detailPanel.style.display = 'flex';
|
|
if (updatedModel) this.showDetail(updatedModel);
|
|
} else {
|
|
this.grid.style.display = 'grid';
|
|
// Grid was already refreshed by loadModels
|
|
}
|
|
|
|
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + data.message);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
this.showGalleryViewer(imgUrl);
|
|
};
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.className = 'anomalous-gallery-delete';
|
|
delBtn.innerHTML = '馃棏锔?;
|
|
delBtn.title = window.anomalous_browser_lang === 'zh' ? '鍒犻櫎' : 'Delete';
|
|
|
|
delBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.style.position = 'absolute';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100%';
|
|
overlay.style.height = '100%';
|
|
overlay.style.background = 'rgba(0,0,0,0.85)';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.flexDirection = 'column';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.gap = '15px';
|
|
overlay.style.zIndex = '10';
|
|
|
|
const msg = document.createElement('div');
|
|
msg.innerHTML = '褰诲簳鍒犻櫎杩欏紶鍥剧墖锛?br><span style="font-size:0.8em;color:#aaa">Delete permanently?</span>';
|
|
msg.style.color = '#fff';
|
|
msg.style.fontWeight = 'bold';
|
|
msg.style.textAlign = 'center';
|
|
|
|
const btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.gap = '12px';
|
|
|
|
const confirmBtn = document.createElement('button');
|
|
confirmBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '馃棏锔?鍒犻櫎' : '馃棏锔?Delete';
|
|
confirmBtn.style.background = '#dc3545';
|
|
confirmBtn.style.color = '#fff';
|
|
confirmBtn.style.border = 'none';
|
|
confirmBtn.style.padding = '10px 16px';
|
|
confirmBtn.style.borderRadius = '6px';
|
|
confirmBtn.style.cursor = 'pointer';
|
|
confirmBtn.style.fontWeight = 'bold';
|
|
confirmBtn.style.transition = 'background 0.2s';
|
|
confirmBtn.onmouseover = () => confirmBtn.style.background = '#ff0000';
|
|
confirmBtn.onmouseout = () => confirmBtn.style.background = '#dc3545';
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鍙栨秷' : 'Cancel';
|
|
cancelBtn.style.background = '#444';
|
|
cancelBtn.style.color = '#fff';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.padding = '10px 16px';
|
|
cancelBtn.style.borderRadius = '6px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.style.transition = 'background 0.2s';
|
|
cancelBtn.onmouseover = () => cancelBtn.style.background = '#666';
|
|
cancelBtn.onmouseout = () => cancelBtn.style.background = '#444';
|
|
|
|
cancelBtn.onclick = (ce) => {
|
|
ce.stopPropagation();
|
|
overlay.remove();
|
|
};
|
|
|
|
confirmBtn.onclick = async (ce) => {
|
|
ce.stopPropagation();
|
|
confirmBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '鍒犻櫎涓?..' : 'Deleting...';
|
|
confirmBtn.disabled = true;
|
|
try {
|
|
const dr = await fetch('/anomalous/delete_gallery_image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename: imgData.filename, subfolder: imgData.subfolder })
|
|
});
|
|
const dd = await dr.json();
|
|
if (dd.status === 'success') {
|
|
card.remove();
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '鍒犻櫎澶辫触: ' : 'Delete failed: ') + dd.message);
|
|
overlay.remove();
|
|
}
|
|
} catch (err) {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + err);
|
|
overlay.remove();
|
|
}
|
|
};
|
|
|
|
btnRow.appendChild(cancelBtn);
|
|
btnRow.appendChild(confirmBtn);
|
|
overlay.appendChild(msg);
|
|
overlay.appendChild(btnRow);
|
|
|
|
card.appendChild(overlay);
|
|
};
|
|
|
|
card.appendChild(img);
|
|
card.appendChild(delBtn);
|
|
this.galleryGrid.insertBefore(card, this.gallerySentinel);
|
|
});
|
|
|
|
this.galleryCurrentPage = page;
|
|
this.galleryHasMore = page < data.pages;
|
|
|
|
if (!this.galleryHasMore) {
|
|
this.gallerySentinel.innerHTML = window.anomalous_browser_lang === 'zh' ? '娌℃湁鏇村鍥剧墖浜? : 'No more images';
|
|
} else {
|
|
this.gallerySentinel.innerHTML = window.anomalous_browser_lang === 'zh' ? '鍚戜笅婊氬姩鍔犺浇鏇村' : 'Scroll for more';
|
|
}
|
|
} else {
|
|
this.galleryHasMore = false;
|
|
this.gallerySentinel.innerHTML = reset ? '鍥惧簱涓虹┖ / Gallery is empty' : '娌℃湁鏇村鍥剧墖浜?/ No more images';
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load gallery images', e);
|
|
this.gallerySentinel.innerHTML = window.anomalous_browser_lang === 'zh' ? '鍔犺浇澶辫触' : 'Load failed';
|
|
}
|
|
|
|
this.galleryLoading = false;
|
|
}
|
|
|
|
|
|
showEditModal(model) {
|
|
const modal = document.createElement('div');
|
|
modal.style.position = 'fixed';
|
|
modal.style.top = '0';
|
|
modal.style.left = '0';
|
|
modal.style.width = '100vw';
|
|
modal.style.height = '100vh';
|
|
modal.style.background = 'rgba(0,0,0,0.6)';
|
|
modal.style.backdropFilter = 'blur(4px)';
|
|
modal.style.zIndex = '10000';
|
|
modal.style.display = 'flex';
|
|
modal.style.alignItems = 'center';
|
|
modal.style.justifyContent = 'center';
|
|
|
|
const content = document.createElement('div');
|
|
content.style.background = '#202124';
|
|
content.style.padding = '24px';
|
|
content.style.borderRadius = '12px';
|
|
content.style.width = '720px';
|
|
content.style.maxWidth = '90%';
|
|
content.style.border = '1px solid #3c4043';
|
|
content.style.boxShadow = '0 8px 32px rgba(0,0,0,0.5)';
|
|
content.style.display = 'flex';
|
|
content.style.flexDirection = 'row';
|
|
content.style.gap = '24px';
|
|
content.style.fontFamily = 'Inter, Roboto, sans-serif';
|
|
|
|
// --- LEFT COLUMN ---
|
|
const leftCol = document.createElement('div');
|
|
leftCol.style.width = '240px';
|
|
leftCol.style.flexShrink = '0';
|
|
leftCol.style.display = 'flex';
|
|
leftCol.style.flexDirection = 'column';
|
|
leftCol.style.gap = '12px';
|
|
|
|
const previewContainer = document.createElement('div');
|
|
previewContainer.style.width = '100%';
|
|
previewContainer.style.height = '320px';
|
|
previewContainer.style.background = '#303134';
|
|
previewContainer.style.borderRadius = '8px';
|
|
previewContainer.style.display = 'flex';
|
|
previewContainer.style.alignItems = 'center';
|
|
previewContainer.style.justifyContent = 'center';
|
|
previewContainer.style.overflow = 'hidden';
|
|
previewContainer.style.border = '1px solid #3c4043';
|
|
previewContainer.style.position = 'relative';
|
|
|
|
if (model.preview_url) {
|
|
const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVideo) {
|
|
const video = document.createElement('video');
|
|
video.src = model.preview_url;
|
|
video.controls = false;
|
|
video.autoplay = true;
|
|
video.loop = true;
|
|
video.muted = true;
|
|
video.style.width = '100%';
|
|
video.style.height = '100%';
|
|
video.style.objectFit = 'cover';
|
|
|
|
const muteBtn = document.createElement('div');
|
|
muteBtn.innerHTML = '馃攪';
|
|
muteBtn.style.position = 'absolute';
|
|
muteBtn.style.bottom = '8px';
|
|
muteBtn.style.right = '8px';
|
|
muteBtn.style.background = 'rgba(0,0,0,0.6)';
|
|
muteBtn.style.color = '#fff';
|
|
muteBtn.style.padding = '6px';
|
|
muteBtn.style.borderRadius = '50%';
|
|
muteBtn.style.cursor = 'pointer';
|
|
muteBtn.style.fontSize = '14px';
|
|
muteBtn.style.zIndex = '10';
|
|
muteBtn.title = window.anomalous_browser_lang === 'zh' ? '寮€鍚?鍏抽棴澹伴煶' : 'Toggle Sound';
|
|
muteBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
video.muted = !video.muted;
|
|
muteBtn.innerHTML = video.muted ? '馃攪' : '馃攰';
|
|
};
|
|
|
|
previewContainer.appendChild(video);
|
|
previewContainer.appendChild(muteBtn);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = model.preview_url;
|
|
img.style.width = '100%';
|
|
img.style.height = '100%';
|
|
img.style.objectFit = 'cover';
|
|
previewContainer.appendChild(img);
|
|
}
|
|
} else {
|
|
previewContainer.innerHTML = `<div style="color:#9aa0a6; font-size:0.9em; text-align:center;">${window.anomalous_browser_lang === 'zh' ? '鏆傛棤灏侀潰' : 'No Cover'}</div>`;
|
|
}
|
|
|
|
const coverRow = document.createElement('div');
|
|
coverRow.style.display = 'flex';
|
|
coverRow.style.flexDirection = 'column';
|
|
coverRow.style.gap = '8px';
|
|
|
|
const galleryBtn = document.createElement('button');
|
|
galleryBtn.innerHTML = '馃柤锔?' + (window.anomalous_browser_lang === 'zh' ? '浠庡巻鍙插浘搴撴寫閫? : 'Pick from Gallery');
|
|
galleryBtn.style.padding = '8px';
|
|
galleryBtn.style.background = '#303134';
|
|
galleryBtn.style.color = '#8ab4f8';
|
|
galleryBtn.style.border = '1px solid #5f6368';
|
|
galleryBtn.style.borderRadius = '6px';
|
|
galleryBtn.style.cursor = 'pointer';
|
|
galleryBtn.style.fontWeight = '500';
|
|
galleryBtn.style.fontSize = '0.9em';
|
|
galleryBtn.onmouseover = () => galleryBtn.style.background = '#3c4043';
|
|
galleryBtn.onmouseout = () => galleryBtn.style.background = '#303134';
|
|
galleryBtn.onclick = () => {
|
|
document.body.removeChild(modal);
|
|
this.showGallerySelectMode(model);
|
|
};
|
|
|
|
const localBtn = document.createElement('button');
|
|
localBtn.innerHTML = '馃搧 ' + (window.anomalous_browser_lang === 'zh' ? '浠庢湰鍦颁笂浼? : 'Upload Local');
|
|
localBtn.style.padding = '8px';
|
|
localBtn.style.background = '#303134';
|
|
localBtn.style.color = '#8ab4f8';
|
|
localBtn.style.border = '1px solid #5f6368';
|
|
localBtn.style.borderRadius = '6px';
|
|
localBtn.style.cursor = 'pointer';
|
|
localBtn.style.fontWeight = '500';
|
|
localBtn.style.fontSize = '0.9em';
|
|
localBtn.onmouseover = () => localBtn.style.background = '#3c4043';
|
|
localBtn.onmouseout = () => localBtn.style.background = '#303134';
|
|
localBtn.onclick = () => {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/*';
|
|
fileInput.onchange = async (e) => {
|
|
if (e.target.files && e.target.files.length > 0) {
|
|
const file = e.target.files[0];
|
|
const formData = new FormData();
|
|
formData.append('type', this.currentType);
|
|
formData.append('path_idx', this.currentPathIdx);
|
|
formData.append('subfolder', this.currentSubfolder);
|
|
formData.append('filename', model.filename);
|
|
formData.append('image', file);
|
|
try {
|
|
const res = await fetch('/anomalous/upload_custom_cover', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
if (updatedModel) this.showDetail(updatedModel);
|
|
}
|
|
document.body.removeChild(modal);
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + data.message);
|
|
}
|
|
} catch (err) {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '涓婁紶澶辫触: ' : 'Upload failed: ') + err);
|
|
}
|
|
}
|
|
};
|
|
fileInput.click();
|
|
};
|
|
|
|
coverRow.appendChild(galleryBtn);
|
|
coverRow.appendChild(localBtn);
|
|
|
|
leftCol.appendChild(previewContainer);
|
|
leftCol.appendChild(coverRow);
|
|
|
|
// --- RIGHT COLUMN ---
|
|
const rightCol = document.createElement('div');
|
|
rightCol.style.flex = '1';
|
|
rightCol.style.display = 'flex';
|
|
rightCol.style.flexDirection = 'column';
|
|
rightCol.style.gap = '15px';
|
|
|
|
const title = document.createElement('h2');
|
|
title.innerText = window.anomalous_browser_lang === 'zh' ? '缂栬緫妯″瀷淇℃伅' : 'Edit Model Info';
|
|
title.style.margin = '0';
|
|
title.style.color = '#e8eaed';
|
|
title.style.fontSize = '1.25em';
|
|
title.style.fontWeight = '500';
|
|
|
|
const filenameLabel = document.createElement('div');
|
|
filenameLabel.innerHTML = `<span style="color:#9aa0a6;">${window.anomalous_browser_lang === 'zh' ? '鏂囦欢:' : 'File:'}</span> ${model.filename}`;
|
|
filenameLabel.style.color = '#e8eaed';
|
|
filenameLabel.style.fontSize = '0.9em';
|
|
filenameLabel.style.wordBreak = 'break-all';
|
|
|
|
const inputStyle = `
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
background: #303134;
|
|
color: #e8eaed;
|
|
border: 1px solid #5f6368;
|
|
border-radius: 6px;
|
|
box-sizing: border-box;
|
|
outline: none;
|
|
font-size: 14px;
|
|
transition: border 0.2s;
|
|
`;
|
|
|
|
const nameInput = document.createElement('input');
|
|
nameInput.placeholder = window.anomalous_browser_lang === 'zh' ? '鑷畾涔夊悕绉?(鐣欑┖鍒欎娇鐢ㄥ師鍚?' : 'Custom Name (Leave empty to use original)';
|
|
nameInput.value = (model.metadata && model.metadata.custom_name) ? model.metadata.custom_name : '';
|
|
nameInput.style.cssText = inputStyle;
|
|
nameInput.onfocus = () => nameInput.style.borderColor = '#8ab4f8';
|
|
nameInput.onblur = () => nameInput.style.borderColor = '#5f6368';
|
|
|
|
const notesInput = document.createElement('textarea');
|
|
notesInput.placeholder = window.anomalous_browser_lang === 'zh' ? '馃摀 涓撳睘澶囨敞... (鏀寔澶氳)' : '馃摀 Custom Notes...';
|
|
notesInput.value = (model.metadata && model.metadata.custom_notes) ? model.metadata.custom_notes : '';
|
|
notesInput.style.cssText = inputStyle;
|
|
notesInput.style.flex = '1'; // fill remaining space
|
|
notesInput.style.minHeight = '150px';
|
|
notesInput.style.resize = 'vertical';
|
|
// Notebook styling override
|
|
notesInput.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)';
|
|
notesInput.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)';
|
|
notesInput.style.backgroundAttachment = 'local';
|
|
notesInput.style.border = '1px solid #3c4043';
|
|
notesInput.style.borderLeft = '4px solid #a38d53';
|
|
notesInput.style.borderRadius = '4px 8px 8px 4px';
|
|
notesInput.style.color = '#d1c9b4';
|
|
notesInput.style.fontFamily = '"Consolas", "Courier New", monospace';
|
|
notesInput.style.lineHeight = '24px';
|
|
// Removed text shadow for cleaner look
|
|
|
|
notesInput.onfocus = () => {
|
|
notesInput.style.boxShadow = '0 0 0 2px rgba(163, 141, 83, 0.2)';
|
|
notesInput.style.borderColor = '#a38d53';
|
|
};
|
|
notesInput.onblur = () => {
|
|
notesInput.style.boxShadow = 'none';
|
|
notesInput.style.borderColor = '#3c4043';
|
|
};
|
|
|
|
const physicalRow = document.createElement('div');
|
|
physicalRow.style.display = 'flex';
|
|
physicalRow.style.flexDirection = 'column';
|
|
physicalRow.style.gap = '4px';
|
|
|
|
const physicalCheckboxWrapper = document.createElement('div');
|
|
physicalCheckboxWrapper.style.display = 'flex';
|
|
physicalCheckboxWrapper.style.alignItems = 'center';
|
|
physicalCheckboxWrapper.style.gap = '8px';
|
|
|
|
const physicalCheckbox = document.createElement('input');
|
|
physicalCheckbox.type = 'checkbox';
|
|
physicalCheckbox.id = 'anomalous-physical-rename-checkbox';
|
|
physicalCheckbox.style.cursor = 'pointer';
|
|
|
|
const physicalLabel = document.createElement('label');
|
|
physicalLabel.htmlFor = 'anomalous-physical-rename-checkbox';
|
|
physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '鍚屾鐗╃悊閲嶅懡鍚? : 'Physical Rename on Disk';
|
|
physicalLabel.style.color = '#e8eaed';
|
|
physicalLabel.style.fontSize = '0.9em';
|
|
physicalLabel.style.cursor = 'pointer';
|
|
|
|
physicalCheckboxWrapper.appendChild(physicalCheckbox);
|
|
physicalCheckboxWrapper.appendChild(physicalLabel);
|
|
|
|
const physicalDesc = document.createElement('div');
|
|
physicalDesc.style.fontSize = '0.8em';
|
|
physicalDesc.style.color = '#9aa0a6';
|
|
physicalDesc.style.marginLeft = '22px';
|
|
physicalDesc.innerText = window.anomalous_browser_lang === 'zh' ? '鑻ヤ笉鍕鹃€夊垯浠呰櫄鎷熼噸鍛藉悕锛屽畨鍏ㄤ笉鐮村潖鍘熸枃浠躲€? : 'Virtual rename only if unchecked. Safe for originals.';
|
|
|
|
physicalRow.appendChild(physicalCheckboxWrapper);
|
|
physicalRow.appendChild(physicalDesc);
|
|
|
|
const actionRow = document.createElement('div');
|
|
actionRow.style.display = 'flex';
|
|
actionRow.style.justifyContent = 'space-between';
|
|
actionRow.style.marginTop = 'auto';
|
|
|
|
const leftActions = document.createElement('div');
|
|
leftActions.style.display = 'flex';
|
|
leftActions.style.gap = '10px';
|
|
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.innerText = window.anomalous_browser_lang === 'zh' ? '閲嶇疆璁剧疆' : 'Reset All';
|
|
resetBtn.style.padding = '8px 16px';
|
|
resetBtn.style.background = 'transparent';
|
|
resetBtn.style.color = '#f28b82';
|
|
resetBtn.style.border = '1px solid #f28b82';
|
|
resetBtn.style.borderRadius = '4px';
|
|
resetBtn.style.cursor = 'pointer';
|
|
resetBtn.style.fontWeight = '500';
|
|
resetBtn.onclick = async () => {
|
|
if (!confirm(window.anomalous_browser_lang === 'zh' ? '纭畾瑕侀噸缃妯″瀷鐨勬墍鏈夎嚜瀹氫箟淇℃伅锛堝悕绉般€佸皝闈€佸娉級鍚楋紵' : 'Reset all custom info for this model?')) return;
|
|
document.body.removeChild(modal);
|
|
try {
|
|
const res = await fetch('/anomalous/update_metadata', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
custom_name: '',
|
|
custom_notes: '',
|
|
reset_cover: true,
|
|
physical_rename: false
|
|
})
|
|
});
|
|
if (res.ok) {
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
if (updatedModel) {
|
|
this.showDetail(updatedModel);
|
|
} else {
|
|
this.grid.style.display = 'grid';
|
|
this.detailPanel.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
} catch (e) { console.error(e); }
|
|
};
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '鍙栨秷' : 'Cancel';
|
|
cancelBtn.style.padding = '8px 16px';
|
|
cancelBtn.style.background = 'transparent';
|
|
cancelBtn.style.color = '#8ab4f8';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.borderRadius = '4px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.style.fontWeight = '500';
|
|
cancelBtn.onclick = () => document.body.removeChild(modal);
|
|
|
|
leftActions.appendChild(resetBtn);
|
|
leftActions.appendChild(cancelBtn);
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '淇濆瓨鏇存敼' : 'Save Changes';
|
|
saveBtn.style.padding = '8px 24px';
|
|
saveBtn.style.background = '#8ab4f8';
|
|
saveBtn.style.color = '#202124';
|
|
saveBtn.style.border = 'none';
|
|
saveBtn.style.borderRadius = '4px';
|
|
saveBtn.style.cursor = 'pointer';
|
|
saveBtn.style.fontWeight = '600';
|
|
saveBtn.onclick = () => {
|
|
const newName = nameInput.value.trim();
|
|
const newNotes = notesInput.value.trim();
|
|
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '淇濆瓨涓?..' : 'Saving...';
|
|
saveBtn.disabled = true;
|
|
|
|
fetch('/anomalous/update_metadata', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
custom_name: newName,
|
|
custom_notes: newNotes,
|
|
physical_rename: physicalCheckbox.checked
|
|
})
|
|
}).then(res => res.json()).then(data => {
|
|
document.body.removeChild(modal);
|
|
if (data.status === 'success') {
|
|
if (!model.metadata) model.metadata = {};
|
|
model.metadata.custom_name = newName;
|
|
model.metadata.custom_notes = newNotes;
|
|
if (data.new_filename && physicalCheckbox.checked) {
|
|
model.filename = data.new_filename;
|
|
}
|
|
this.loadModels();
|
|
if (this.currentDetailModel && (this.currentDetailModel.filename === model.filename || (data.new_filename && this.currentDetailModel.filename === data.new_filename))) {
|
|
this.showDetail(model);
|
|
}
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + data.message);
|
|
}
|
|
}).catch(e => {
|
|
document.body.removeChild(modal);
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + e);
|
|
});
|
|
};
|
|
|
|
actionRow.appendChild(leftActions);
|
|
actionRow.appendChild(saveBtn);
|
|
|
|
rightCol.appendChild(title);
|
|
rightCol.appendChild(filenameLabel);
|
|
rightCol.appendChild(nameInput);
|
|
rightCol.appendChild(notesInput);
|
|
rightCol.appendChild(physicalRow);
|
|
rightCol.appendChild(actionRow);
|
|
|
|
content.appendChild(leftCol);
|
|
content.appendChild(rightCol);
|
|
modal.appendChild(content);
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
|
|
async showGeneratedGallery(model) {
|
|
let overlay = document.getElementById('anomalous-generated-gallery-overlay');
|
|
if (!overlay) {
|
|
overlay = document.createElement('div');
|
|
overlay.id = 'anomalous-generated-gallery-overlay';
|
|
overlay.style.position = 'absolute';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100%';
|
|
overlay.style.height = '100%';
|
|
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
|
overlay.style.zIndex = '999999';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
|
|
const modalBox = document.createElement('div');
|
|
modalBox.id = 'anomalous-generated-gallery-modal';
|
|
modalBox.style.width = '95%';
|
|
modalBox.style.maxHeight = '95%';
|
|
modalBox.style.backgroundColor = 'var(--comfy-menu-bg, #222)';
|
|
modalBox.style.borderRadius = '12px';
|
|
modalBox.style.display = 'flex';
|
|
modalBox.style.flexDirection = 'column';
|
|
modalBox.style.overflow = 'hidden';
|
|
modalBox.style.boxShadow = '0 10px 40px rgba(0, 0, 0, 0.8)';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.padding = '15px 25px';
|
|
header.style.background = '#333';
|
|
header.style.display = 'flex';
|
|
header.style.justifyContent = 'space-between';
|
|
header.style.alignItems = 'center';
|
|
header.style.borderBottom = '1px solid #444';
|
|
|
|
const title = document.createElement('h2');
|
|
title.id = 'anomalous-generated-gallery-title';
|
|
title.style.margin = '0';
|
|
title.style.color = '#fff';
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerHTML = '✖ ' + (window.anomalous_browser_lang === 'zh' ? '鍏抽棴鍥惧簱' : 'Close Gallery');
|
|
closeBtn.style.padding = '8px 15px';
|
|
closeBtn.style.background = '#dc3545';
|
|
closeBtn.style.color = '#fff';
|
|
closeBtn.style.border = 'none';
|
|
closeBtn.style.borderRadius = '5px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.style.fontWeight = 'bold';
|
|
closeBtn.onmouseover = () => closeBtn.style.background = '#c82333';
|
|
closeBtn.onmouseout = () => closeBtn.style.background = '#dc3545';
|
|
closeBtn.onclick = () => {
|
|
overlay.style.display = 'none';
|
|
};
|
|
|
|
header.appendChild(title);
|
|
header.appendChild(closeBtn);
|
|
modalBox.appendChild(header);
|
|
|
|
const contentCont = document.createElement('div');
|
|
contentCont.id = 'anomalous-generated-gallery-content';
|
|
contentCont.style.flex = '1';
|
|
contentCont.style.overflowY = 'auto';
|
|
contentCont.style.padding = '20px';
|
|
contentCont.style.display = 'grid';
|
|
contentCont.style.gridTemplateColumns = 'repeat(auto-fill, minmax(220px, 1fr))';
|
|
contentCont.style.gap = '25px';
|
|
contentCont.style.rowGap = '40px';
|
|
contentCont.style.alignContent = 'start';
|
|
modalBox.appendChild(contentCont);
|
|
|
|
overlay.appendChild(modalBox);
|
|
document.getElementById('anomalous-container').appendChild(overlay);
|
|
}
|
|
|
|
const title = document.getElementById('anomalous-generated-gallery-title');
|
|
title.innerText = (window.anomalous_browser_lang === 'zh' ? '鍘嗗彶鐢熸垚鍥惧簱: ' : 'Generated History: ') + (model.name || model.filename);
|
|
|
|
const contentCont = document.getElementById('anomalous-generated-gallery-content');
|
|
contentCont.innerHTML = '';
|
|
|
|
const loading = document.createElement('div');
|
|
loading.innerText = window.anomalous_browser_lang === 'zh' ? '鍔犺浇涓紝姝e湪鎵弿鍥剧墖鍏冩暟鎹?..' : 'Loading, scanning metadata...';
|
|
loading.style.textAlign = 'center';
|
|
loading.style.gridColumn = '1 / -1';
|
|
loading.style.padding = '50px';
|
|
loading.style.color = '#aaa';
|
|
contentCont.appendChild(loading);
|
|
|
|
overlay.style.display = 'flex';
|
|
|
|
try {
|
|
const res = await fetch('/anomalous/model_images?model_name=' + encodeURIComponent(model.filename) + '&t=' + Date.now());
|
|
const data = await res.json();
|
|
contentCont.innerHTML = '';
|
|
|
|
if (!data.images || data.images.length === 0) {
|
|
const emptyMsg = document.createElement('div');
|
|
emptyMsg.innerText = window.anomalous_browser_lang === 'zh' ? '娌℃湁鎵惧埌浣跨敤姝ゆā鍨嬬敓鎴愮殑鍘嗗彶鍥剧墖銆? : 'No images found generated by this model.';
|
|
emptyMsg.style.textAlign = 'center';
|
|
emptyMsg.style.gridColumn = '1 / -1';
|
|
emptyMsg.style.padding = '50px';
|
|
emptyMsg.style.color = '#888';
|
|
contentCont.appendChild(emptyMsg);
|
|
return;
|
|
}
|
|
|
|
data.images.forEach(img => {
|
|
const imgCont = document.createElement('div');
|
|
imgCont.className = 'anomalous-card';
|
|
imgCont.style.cursor = 'pointer';
|
|
|
|
const el = document.createElement('img');
|
|
el.src = img.url || img; // Support both just in case
|
|
el.loading = 'lazy';
|
|
el.draggable = true;
|
|
|
|
el.addEventListener('dragstart', (e) => {
|
|
const fullUrl = new URL(el.src, window.location.href).href;
|
|
e.dataTransfer.setData('text/uri-list', fullUrl);
|
|
e.dataTransfer.setData('text/plain', fullUrl);
|
|
if (window.anomalousDragGhostImg) {
|
|
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
|
|
}
|
|
});
|
|
|
|
let source_image = "";
|
|
let filenameText = "";
|
|
if (img.url) {
|
|
try {
|
|
const urlParams = new URLSearchParams(img.url.split('?')[1]);
|
|
filenameText = urlParams.get('filename') || '';
|
|
const sub = urlParams.get('subfolder') || '';
|
|
source_image = sub ? sub + '/' + filenameText : filenameText;
|
|
} catch (e) { }
|
|
} else {
|
|
filenameText = img.split('/').pop().split('?')[0];
|
|
source_image = filenameText;
|
|
}
|
|
|
|
const titleDiv = document.createElement('div');
|
|
titleDiv.className = 'anomalous-card-title';
|
|
titleDiv.innerText = filenameText;
|
|
|
|
const setCoverBtn = document.createElement('button');
|
|
setCoverBtn.innerText = window.anomalous_browser_lang === 'zh' ? '璁句负灏侀潰' : 'Set Cover';
|
|
setCoverBtn.style.position = 'absolute';
|
|
setCoverBtn.style.bottom = '40px';
|
|
setCoverBtn.style.right = '5px';
|
|
setCoverBtn.style.background = 'rgba(40, 167, 69, 0.85)';
|
|
setCoverBtn.style.color = '#fff';
|
|
setCoverBtn.style.border = '1px solid rgba(255,255,255,0.3)';
|
|
setCoverBtn.style.borderRadius = '4px';
|
|
setCoverBtn.style.padding = '4px 8px';
|
|
setCoverBtn.style.cursor = 'pointer';
|
|
setCoverBtn.style.zIndex = '10';
|
|
setCoverBtn.style.fontSize = '12px';
|
|
|
|
setCoverBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
if (!confirm(window.anomalous_browser_lang === 'zh' ? '纭畾灏嗘鍥剧墖璁句负灏侀潰鍚楋紵' : 'Set this image as cover?')) return;
|
|
|
|
fetch('/anomalous/set_custom_cover', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
source_image: source_image
|
|
})
|
|
}).then(res => res.json()).then(async result => {
|
|
if (result.status === 'success') {
|
|
alert(window.anomalous_browser_lang === 'zh' ? '璁剧疆鎴愬姛锛? : 'Cover set successfully!');
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
if (updatedModel && this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
this.showDetail(updatedModel);
|
|
}
|
|
} else {
|
|
alert((window.anomalous_browser_lang === 'zh' ? '閿欒: ' : 'Error: ') + result.message);
|
|
}
|
|
}).catch(err => {
|
|
alert('Error: ' + err.message);
|
|
});
|
|
};
|
|
|
|
imgCont.onclick = () => {
|
|
this.showGalleryViewer(img.url || img);
|
|
};
|
|
|
|
imgCont.appendChild(el);
|
|
imgCont.appendChild(setCoverBtn);
|
|
imgCont.appendChild(titleDiv);
|
|
contentCont.appendChild(imgCont);
|
|
});
|
|
} catch (e) {
|
|
contentCont.innerHTML = '<div style="color:red; text-align:center; grid-column: 1/-1; padding: 50px;">Error loading images</div>';
|
|
}
|
|
}
|
|
|
|
showGallerySelectMode(model) {
|
|
this.gallerySelectModel = model;
|
|
this.grid.style.display = 'none';
|
|
this.detailPanel.style.display = 'none';
|
|
this.galleryPanel.style.display = 'flex';
|
|
let banner = document.getElementById('anomalous-gallery-select-banner');
|
|
if (!banner) {
|
|
banner = document.createElement('div');
|
|
banner.id = 'anomalous-gallery-select-banner';
|
|
banner.style.background = '#28a745';
|
|
banner.style.color = '#fff';
|
|
banner.style.padding = '10px';
|
|
banner.style.textAlign = 'center';
|
|
banner.style.fontWeight = 'bold';
|
|
banner.style.position = 'sticky';
|
|
banner.style.top = '0';
|
|
banner.style.zIndex = '1000';
|
|
this.galleryPanel.insertBefore(banner, this.galleryPanel.firstChild);
|
|
}
|
|
banner.style.display = 'block';
|
|
banner.innerHTML = window.anomalous_browser_lang === 'zh'
|
|
? `姝e湪涓烘ā鍨?<span style="color:#ff0;">${model.filename}</span> 閫夋嫨灏侀潰銆傝鐐瑰嚮涓嬫柟鐨勫浘鐗囥€?button id="anomalous-cancel-select" style="margin-left:15px;color:#000;background:#fff;border:none;padding:2px 8px;border-radius:4px;cursor:pointer;">鍙栨秷</button>`
|
|
: `Selecting cover for <span style="color:#ff0;">${model.filename}</span>. Click an image below.<button id="anomalous-cancel-select" style="margin-left:15px;color:#000;background:#fff;border:none;padding:2px 8px;border-radius:4px;cursor:pointer;">Cancel</button>`;
|
|
|
|
document.getElementById('anomalous-cancel-select').onclick = () => {
|
|
const tempModel = this.gallerySelectModel;
|
|
this.gallerySelectModel = null;
|
|
banner.style.display = 'none';
|
|
this.galleryPanel.style.display = 'none';
|
|
if (this.currentDetailModel) {
|
|
this.detailPanel.style.display = 'flex';
|
|
} else {
|
|
this.grid.style.display = 'grid';
|
|
}
|
|
if (tempModel) {
|
|
this.showEditModal(tempModel);
|
|
}
|
|
};
|
|
|
|
if (this.galleryImages.length === 0) {
|
|
this.loadGalleryImages(1, true);
|
|
}
|
|
}
|
|
|
|
showGalleryViewer(src) {
|
|
let viewer = document.getElementById('anomalous-gallery-viewer');
|
|
if (!viewer) {
|
|
viewer = document.createElement('div');
|
|
viewer.id = 'anomalous-gallery-viewer';
|
|
viewer.className = 'anomalous-gallery-viewer';
|
|
|
|
const closeBtn = document.createElement('div');
|
|
closeBtn.className = 'anomalous-gallery-viewer-close';
|
|
closeBtn.innerHTML = '×';
|
|
|
|
const img = document.createElement('img');
|
|
img.id = 'anomalous-gallery-viewer-img';
|
|
|
|
viewer.appendChild(img);
|
|
viewer.appendChild(closeBtn);
|
|
|
|
let scale = 1;
|
|
let translateX = 0;
|
|
let translateY = 0;
|
|
let isDragging = false;
|
|
let startX = 0, startY = 0;
|
|
|
|
const resetImgTransform = () => {
|
|
scale = 1; translateX = 0; translateY = 0;
|
|
img.style.transform = `translate(0px, 0px) scale(1)`;
|
|
img.style.cursor = 'grab';
|
|
};
|
|
|
|
closeBtn.onclick = () => {
|
|
viewer.style.display = 'none';
|
|
resetImgTransform();
|
|
};
|
|
|
|
viewer.onclick = (e) => {
|
|
if (e.target === viewer) {
|
|
viewer.style.display = 'none';
|
|
resetImgTransform();
|
|
}
|
|
};
|
|
|
|
viewer.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
const zoomFactor = 0.1;
|
|
if (e.deltaY < 0) scale += zoomFactor;
|
|
else scale = Math.max(0.1, scale - zoomFactor);
|
|
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
});
|
|
|
|
img.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
startX = e.clientX - translateX;
|
|
startY = e.clientY - translateY;
|
|
img.style.cursor = 'grabbing';
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
translateX = e.clientX - startX;
|
|
translateY = e.clientY - startY;
|
|
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
});
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
img.style.cursor = 'grab';
|
|
});
|
|
|
|
document.body.appendChild(viewer);
|
|
}
|
|
|
|
const img = document.getElementById('anomalous-gallery-viewer-img');
|
|
img.src = src;
|
|
|
|
// Reset scale and translation when opening a new image
|
|
img.style.transform = `translate(0px, 0px) scale(1)`;
|
|
img.style.cursor = 'grab';
|
|
|
|
viewer.style.display = 'flex';
|
|
}
|
|
|
|
async refreshNotebooks(autoOpenFirst = false) {
|
|
try {
|
|
const res = await fetch('/anomalous/notebooks');
|
|
const data = await res.json();
|
|
this.nbListEl.innerHTML = '';
|
|
|
|
if (data.notebooks && data.notebooks.length > 0) {
|
|
if (autoOpenFirst) {
|
|
if (!this.currentNotebook) {
|
|
this.currentNotebook = data.notebooks[0];
|
|
}
|
|
if (this.currentNotebook) {
|
|
this.renderNotebookEditor();
|
|
}
|
|
}
|
|
|
|
data.notebooks.forEach(nb => {
|
|
const item = document.createElement('div');
|
|
item.className = 'anomalous-nb-item';
|
|
if (this.currentNotebook && this.currentNotebook.filename === nb.filename) {
|
|
item.classList.add('active');
|
|
}
|
|
item.innerHTML = `<span class="anomalous-nb-item-icon">馃搫 </span><span class="anomalous-nb-item-text">${nb.name}</span>`;
|
|
item.onclick = () => {
|
|
this.currentNotebook = nb;
|
|
this.renderNotebookEditor();
|
|
this.refreshNotebooks();
|
|
};
|
|
this.nbListEl.appendChild(item);
|
|
});
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
async saveCurrentNotebook() {
|
|
if (!this.currentNotebook) return;
|
|
try {
|
|
await fetch('/anomalous/save_notebook', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(this.currentNotebook)
|
|
});
|
|
this.refreshNotebooks();
|
|
} catch (e) { }
|
|
}
|
|
|
|
async deleteCurrentNotebook(skipConfirm = false) {
|
|
if (!this.currentNotebook) return;
|
|
if (!skipConfirm && !confirm(t('deleteNotebook') + ' ?')) return;
|
|
try {
|
|
await fetch('/anomalous/delete_notebook', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename: this.currentNotebook.filename })
|
|
});
|
|
this.currentNotebook = null;
|
|
this.nbEditor.innerHTML = '';
|
|
this.refreshNotebooks();
|
|
} catch (e) { }
|
|
}
|
|
|
|
renderNotebookEditor() {
|
|
this.nbEditor.innerHTML = '';
|
|
if (!this.currentNotebook) return;
|
|
|
|
const data = this.currentNotebook.data || {};
|
|
if (!data.loras) data.loras = [];
|
|
|
|
// Toolbar
|
|
const tb = document.createElement('div');
|
|
tb.className = 'anomalous-nb-toolbar';
|
|
|
|
const titleArea = document.createElement('h3');
|
|
titleArea.innerHTML = this.currentNotebook.name;
|
|
titleArea.style.margin = '0';
|
|
|
|
const rightBtns = document.createElement('div');
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.innerHTML = t('saveNotebook');
|
|
saveBtn.className = 'anomalous-btn-primary';
|
|
saveBtn.onclick = async () => {
|
|
const orig = saveBtn.innerHTML;
|
|
saveBtn.innerHTML = '鈴?..';
|
|
await this.saveCurrentNotebook();
|
|
saveBtn.innerHTML = '鉁?;
|
|
saveBtn.style.background = '#2e8b57';
|
|
setTimeout(() => {
|
|
saveBtn.innerHTML = orig;
|
|
saveBtn.style.background = '';
|
|
}, 1500);
|
|
};
|
|
|
|
let delTimer = null;
|
|
const delContainer = document.createElement('span');
|
|
delContainer.style.display = 'inline-flex';
|
|
delContainer.style.alignItems = 'center';
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.innerHTML = t('deleteNotebook');
|
|
delBtn.className = 'anomalous-btn-danger';
|
|
|
|
const cancelDelBtn = document.createElement('button');
|
|
cancelDelBtn.innerHTML = '鉁?;
|
|
cancelDelBtn.className = 'anomalous-btn-danger';
|
|
cancelDelBtn.style.display = 'none';
|
|
cancelDelBtn.style.background = '#555';
|
|
cancelDelBtn.style.marginLeft = '2px';
|
|
cancelDelBtn.style.padding = '6px 8px';
|
|
|
|
delContainer.appendChild(delBtn);
|
|
delContainer.appendChild(cancelDelBtn);
|
|
|
|
const resetDel = () => {
|
|
clearTimeout(delTimer);
|
|
delBtn.innerHTML = t('deleteNotebook');
|
|
delBtn.style.background = '';
|
|
cancelDelBtn.style.display = 'none';
|
|
};
|
|
|
|
delBtn.onclick = () => {
|
|
if (delBtn.innerHTML === t('deleteNotebook')) {
|
|
delBtn.innerHTML = t('delSure');
|
|
delBtn.style.background = '#800';
|
|
cancelDelBtn.style.display = 'block';
|
|
delTimer = setTimeout(resetDel, 4000);
|
|
} else {
|
|
resetDel();
|
|
this.deleteCurrentNotebook(true);
|
|
}
|
|
};
|
|
|
|
cancelDelBtn.onclick = resetDel;
|
|
|
|
const sendBtn = document.createElement('button');
|
|
sendBtn.innerHTML = t('sendToCanvas');
|
|
sendBtn.className = 'anomalous-btn-success';
|
|
sendBtn.onclick = () => this.sendNotebookToCanvas();
|
|
|
|
rightBtns.appendChild(saveBtn);
|
|
rightBtns.appendChild(sendBtn);
|
|
rightBtns.appendChild(delContainer);
|
|
|
|
tb.appendChild(titleArea);
|
|
tb.appendChild(rightBtns);
|
|
|
|
// Settings / Models
|
|
const modelSection = document.createElement('div');
|
|
modelSection.className = 'anomalous-nb-section';
|
|
|
|
// Base Model
|
|
const baseRow = document.createElement('div');
|
|
baseRow.className = 'anomalous-nb-row';
|
|
baseRow.innerHTML = `<strong>${t('baseModel')}</strong>`;
|
|
const baseSelect = document.createElement('select');
|
|
baseSelect.className = 'anomalous-nb-select';
|
|
const buildSelect = (bases) => {
|
|
baseSelect.innerHTML = '';
|
|
bases.forEach(b => {
|
|
const opt = document.createElement('option');
|
|
opt.value = b; opt.text = b;
|
|
if (data.baseModel === b) opt.selected = true;
|
|
baseSelect.appendChild(opt);
|
|
});
|
|
if (!data.baseModel && bases.length > 0) data.baseModel = bases[0];
|
|
};
|
|
|
|
if (this.baseModelsCache) {
|
|
buildSelect(this.baseModelsCache);
|
|
} else {
|
|
const tempBases = ['SD 1.5', 'SD 2.1', 'SDXL', 'SD 3.0', 'SD 3.5', 'Flux.1', 'Pony', 'HunyuanVideo', 'LTX-Video', 'OmniGen'];
|
|
buildSelect(tempBases);
|
|
fetch('/anomalous/base_models').then(r => r.json()).then(d => {
|
|
if (d.base_models && d.base_models.length > 0) {
|
|
this.baseModelsCache = d.base_models;
|
|
buildSelect(this.baseModelsCache);
|
|
}
|
|
}).catch(e => { });
|
|
}
|
|
if (!data.baseModel) data.baseModel = 'SDXL';
|
|
baseSelect.onchange = () => {
|
|
data.baseModel = baseSelect.value;
|
|
data.mainModel = null;
|
|
data.loras = [];
|
|
this.saveCurrentNotebook();
|
|
this.renderNotebookEditor();
|
|
};
|
|
baseRow.appendChild(baseSelect);
|
|
|
|
// Main Model (Card Selection)
|
|
const mainBox = document.createElement('div');
|
|
mainBox.className = 'anomalous-nb-gallery-box';
|
|
const mainRow = document.createElement('div');
|
|
mainRow.className = 'anomalous-nb-row';
|
|
mainRow.innerHTML = `<strong>${t('mainModel')}</strong>`;
|
|
|
|
const mainGallery = document.createElement('div');
|
|
mainGallery.className = 'anomalous-nb-gallery-wrap';
|
|
|
|
mainBox.appendChild(mainRow);
|
|
mainBox.appendChild(mainGallery);
|
|
|
|
// Loras (Card Selection)
|
|
const loraBox = document.createElement('div');
|
|
loraBox.className = 'anomalous-nb-gallery-box';
|
|
const loraRow = document.createElement('div');
|
|
loraRow.className = 'anomalous-nb-row';
|
|
loraRow.innerHTML = `<strong>Loras</strong>`;
|
|
|
|
const loraGallery = document.createElement('div');
|
|
loraGallery.className = 'anomalous-nb-gallery-wrap';
|
|
|
|
loraBox.appendChild(loraRow);
|
|
loraBox.appendChild(loraGallery);
|
|
|
|
modelSection.appendChild(baseRow);
|
|
modelSection.appendChild(mainBox);
|
|
modelSection.appendChild(loraBox);
|
|
|
|
// Prompt Section
|
|
const promptSec = document.createElement('div');
|
|
promptSec.className = 'anomalous-nb-section';
|
|
|
|
// Toolbar
|
|
const pToolbar = document.createElement('div');
|
|
pToolbar.className = 'anomalous-nb-prompt-toolbar';
|
|
|
|
const langSelect = document.createElement('select');
|
|
langSelect.className = 'anomalous-nb-select';
|
|
const langs = [
|
|
{ v: 'zh-CN', l: '馃嚚馃嚦 涓枃 (zh-CN)' }, { v: 'en', l: '馃嚞馃嚙 English (en)' },
|
|
{ v: 'ja', l: '馃嚡馃嚨 鏃ユ湰璇?(ja)' }, { v: 'ko', l: '馃嚢馃嚪 頃滉淡鞏?(ko)' },
|
|
{ v: 'fr', l: '馃嚝馃嚪 Fran莽ais (fr)' }, { v: 'de', l: '馃嚛馃嚜 Deutsch (de)' },
|
|
{ v: 'es', l: '馃嚜馃嚫 Espa帽ol (es)' }, { v: 'ru', l: '馃嚪馃嚭 袪褍褋褋泻懈泄 (ru)' }
|
|
];
|
|
langs.forEach(lg => {
|
|
const opt = document.createElement('option');
|
|
opt.value = lg.v; opt.text = lg.l;
|
|
if ((data.targetLang || 'zh-CN') === lg.v) opt.selected = true;
|
|
langSelect.appendChild(opt);
|
|
});
|
|
langSelect.onchange = () => {
|
|
data.targetLang = langSelect.value;
|
|
data.translations = {}; // Clear translation cache on lang change
|
|
this.saveCurrentNotebook();
|
|
updateVisualTags();
|
|
};
|
|
|
|
const findInput = document.createElement('input');
|
|
findInput.className = 'anomalous-nb-select';
|
|
findInput.placeholder = t('findPlaceholder');
|
|
findInput.style.flex = '1';
|
|
|
|
const replaceInput = document.createElement('input');
|
|
replaceInput.className = 'anomalous-nb-select';
|
|
replaceInput.placeholder = t('replacePlaceholder');
|
|
replaceInput.style.flex = '1';
|
|
|
|
const replaceBtn = document.createElement('button');
|
|
replaceBtn.className = 'anomalous-btn-primary';
|
|
replaceBtn.innerHTML = t('replaceAll');
|
|
|
|
pToolbar.appendChild(langSelect);
|
|
pToolbar.appendChild(findInput);
|
|
pToolbar.appendChild(replaceInput);
|
|
pToolbar.appendChild(replaceBtn);
|
|
|
|
// Raw Input Toggle
|
|
const toggleRow = document.createElement('div');
|
|
toggleRow.style.display = 'flex';
|
|
toggleRow.style.justifyContent = 'space-between';
|
|
toggleRow.style.marginBottom = '5px';
|
|
toggleRow.innerHTML = `<strong>Prompt Editor</strong>`;
|
|
const rawBtn = document.createElement('button');
|
|
rawBtn.className = 'anomalous-btn-primary';
|
|
rawBtn.innerHTML = t('editRaw');
|
|
toggleRow.appendChild(rawBtn);
|
|
|
|
// Raw Textarea
|
|
const rawArea = document.createElement('textarea');
|
|
rawArea.className = 'anomalous-nb-textarea';
|
|
rawArea.value = data.promptEn || '';
|
|
rawArea.style.display = 'none';
|
|
rawArea.style.height = '150px';
|
|
|
|
// Visual Dual Pane
|
|
const dualPane = document.createElement('div');
|
|
dualPane.className = 'anomalous-nb-dual-pane';
|
|
|
|
if (!data.translations) data.translations = {};
|
|
|
|
const updateVisualTags = () => {
|
|
dualPane.innerHTML = '';
|
|
const txt = rawArea.value;
|
|
data.promptEn = txt;
|
|
this.saveCurrentNotebook();
|
|
if (!txt.trim()) return;
|
|
|
|
const tags = txt.split(',').map(s => s.trim()).filter(s => s);
|
|
tags.forEach((tag, idx) => {
|
|
const tagRow = document.createElement('div');
|
|
tagRow.className = 'anomalous-nb-tag-row';
|
|
|
|
const tagL = document.createElement('div');
|
|
tagL.className = 'anomalous-nb-visual-tag';
|
|
tagL.style.flex = '1';
|
|
tagL.style.justifyContent = 'space-between';
|
|
const txtL = document.createElement('span');
|
|
txtL.innerText = tag;
|
|
const copyL = document.createElement('span');
|
|
copyL.className = 'anomalous-nb-copy-btn';
|
|
copyL.innerHTML = '馃搵';
|
|
copyL.onclick = (e) => {
|
|
e.stopPropagation();
|
|
navigator.clipboard.writeText(tag).then(() => { copyL.innerHTML = '鉁?; setTimeout(() => copyL.innerHTML = '馃搵', 1000); });
|
|
};
|
|
tagL.appendChild(txtL);
|
|
tagL.appendChild(copyL);
|
|
|
|
const tagR = document.createElement('div');
|
|
tagR.className = 'anomalous-nb-visual-tag';
|
|
tagR.style.flex = '1';
|
|
tagR.style.justifyContent = 'space-between';
|
|
const transTxt = data.translations[tag] ? data.translations[tag] : '...';
|
|
const txtR = document.createElement('span');
|
|
txtR.innerText = transTxt;
|
|
const copyR = document.createElement('span');
|
|
copyR.className = 'anomalous-nb-copy-btn';
|
|
copyR.innerHTML = '馃搵';
|
|
copyR.onclick = (e) => {
|
|
e.stopPropagation();
|
|
navigator.clipboard.writeText(txtR.innerText).then(() => { copyR.innerHTML = '鉁?; setTimeout(() => copyR.innerHTML = '馃搵', 1000); });
|
|
};
|
|
tagR.appendChild(txtR);
|
|
tagR.appendChild(copyR);
|
|
|
|
tagL.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); };
|
|
tagL.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); };
|
|
tagR.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); };
|
|
tagR.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); };
|
|
|
|
tagL.onclick = () => {
|
|
const inp = document.createElement('input');
|
|
inp.value = tag; inp.className = 'anomalous-nb-tag-edit';
|
|
tagL.innerHTML = ''; tagL.appendChild(inp); inp.focus();
|
|
const finish = () => {
|
|
tags[idx] = inp.value.trim();
|
|
rawArea.value = tags.join(', ');
|
|
updateVisualTags();
|
|
};
|
|
inp.onblur = finish;
|
|
inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); };
|
|
};
|
|
|
|
tagR.onclick = () => {
|
|
const inp = document.createElement('input');
|
|
inp.value = data.translations[tag] || ''; inp.className = 'anomalous-nb-tag-edit';
|
|
tagR.innerHTML = ''; tagR.appendChild(inp); inp.focus();
|
|
const finish = () => {
|
|
data.translations[tag] = inp.value.trim();
|
|
this.saveCurrentNotebook();
|
|
updateVisualTags();
|
|
};
|
|
inp.onblur = finish;
|
|
inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); };
|
|
};
|
|
|
|
tagRow.appendChild(tagL);
|
|
tagRow.appendChild(tagR);
|
|
dualPane.appendChild(tagRow);
|
|
|
|
if (!data.translations[tag]) {
|
|
fetch('/anomalous/translate', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text: tag, target_lang: data.targetLang || 'zh-CN' })
|
|
}).then(r => r.json()).then(d => {
|
|
if (d.translated) {
|
|
data.translations[tag] = d.translated;
|
|
txtR.innerText = d.translated;
|
|
this.saveCurrentNotebook();
|
|
}
|
|
}).catch(() => { });
|
|
}
|
|
});
|
|
};
|
|
|
|
rawBtn.onclick = () => {
|
|
if (rawArea.style.display === 'none') {
|
|
rawArea.style.display = 'block';
|
|
dualPane.style.display = 'none';
|
|
rawBtn.innerHTML = '馃憗锔?Done Editing';
|
|
pToolbar.style.display = 'none';
|
|
} else {
|
|
rawArea.style.display = 'none';
|
|
dualPane.style.display = 'flex';
|
|
rawBtn.innerHTML = '馃摑 Edit Raw / Paste';
|
|
pToolbar.style.display = 'flex';
|
|
updateVisualTags();
|
|
}
|
|
};
|
|
|
|
replaceBtn.onclick = () => {
|
|
const findStr = findInput.value;
|
|
const repStr = replaceInput.value;
|
|
if (!findStr) return;
|
|
const newTxt = rawArea.value.split(findStr).join(repStr);
|
|
rawArea.value = newTxt;
|
|
updateVisualTags();
|
|
};
|
|
|
|
rawArea.oninput = () => {
|
|
clearTimeout(this.pTimeout);
|
|
this.pTimeout = setTimeout(() => { data.promptEn = rawArea.value; this.saveCurrentNotebook(); }, 500);
|
|
};
|
|
|
|
updateVisualTags();
|
|
|
|
promptSec.appendChild(pToolbar);
|
|
promptSec.appendChild(toggleRow);
|
|
promptSec.appendChild(rawArea);
|
|
promptSec.appendChild(dualPane);
|
|
|
|
this.nbEditor.appendChild(tb);
|
|
this.nbEditor.appendChild(modelSection);
|
|
this.nbEditor.appendChild(promptSec);
|
|
|
|
// Fetch compatible models and fill galleries
|
|
this.fillNotebookGalleries(data.baseModel, mainGallery, loraGallery, data);
|
|
}
|
|
|
|
fillNotebookGalleries(baseModel, mainGallery, loraGallery, data) {
|
|
if (!baseModel) return;
|
|
|
|
const buildThumbHtml = (m) => {
|
|
let thumb = '';
|
|
if (m.preview_url) {
|
|
const isVid = m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVid) thumb = `<video src="${m.preview_url}" muted loop playsinline></video>`;
|
|
else thumb = `<img src="${m.preview_url}" />`;
|
|
} else {
|
|
thumb = `<div style="width:30px; height:30px; background:#222; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; color:#555;">?</div>`;
|
|
}
|
|
return thumb;
|
|
};
|
|
|
|
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=checkpoints,unet,diffusion_models`)
|
|
.then(r => r.json()).then(d => {
|
|
const buildMainDOM = (models) => {
|
|
mainGallery.innerHTML = '';
|
|
if (!models || !models.length) {
|
|
mainGallery.innerHTML = '<span style="color:#666;">No compatible main models found.</span>';
|
|
} else {
|
|
models.forEach(m => {
|
|
const isSelected = (data.mainModel && data.mainModel.filename === m.filename);
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-nb-minicheck ' + (isSelected ? 'selected' : '');
|
|
card.innerHTML = `${buildThumbHtml(m)}<div class="anomalous-nb-minicheck-name" title="${m.filename}">${m.filename}</div>`;
|
|
|
|
if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) {
|
|
card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(e => { }); };
|
|
card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
|
|
}
|
|
|
|
card.onclick = () => {
|
|
data.mainModel = m;
|
|
this.saveCurrentNotebook();
|
|
buildMainDOM(models); // re-render just the main gallery
|
|
};
|
|
mainGallery.appendChild(card);
|
|
});
|
|
}
|
|
};
|
|
buildMainDOM(d.models || []);
|
|
});
|
|
|
|
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=loras`)
|
|
.then(r => r.json()).then(d => {
|
|
const buildLoraDOM = (models) => {
|
|
loraGallery.innerHTML = '';
|
|
if (!models || !models.length) {
|
|
loraGallery.innerHTML = '<span style="color:#666;">No compatible Loras found.</span>';
|
|
} else {
|
|
models.forEach(m => {
|
|
const loraIndex = data.loras.findIndex(l => l.filename === m.filename);
|
|
const isSelected = loraIndex !== -1;
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-nb-minilora ' + (isSelected ? 'selected' : '');
|
|
card.style.position = 'relative'; // for badge positioning
|
|
|
|
let badgeHtml = '';
|
|
if (isSelected) {
|
|
badgeHtml = `<div style="position:absolute; top:-5px; right:-5px; background:#00ffcc; color:#000; border-radius:50%; width:20px; height:20px; font-size:12px; display:flex; align-items:center; justify-content:center; font-weight:bold; z-index:10; box-shadow: 0 2px 4px rgba(0,0,0,0.5);">${loraIndex + 1}</div>`;
|
|
}
|
|
|
|
card.innerHTML = `${badgeHtml}${buildThumbHtml(m)}<div class="anomalous-nb-minilora-name" title="${m.filename}">${m.filename}</div>`;
|
|
|
|
if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) {
|
|
card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(e => { }); };
|
|
card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
|
|
}
|
|
|
|
card.onclick = () => {
|
|
if (isSelected) {
|
|
data.loras = data.loras.filter(l => l.filename !== m.filename);
|
|
} else {
|
|
data.loras.push(m);
|
|
}
|
|
this.saveCurrentNotebook();
|
|
buildLoraDOM(models); // re-render just the lora gallery
|
|
};
|
|
loraGallery.appendChild(card);
|
|
});
|
|
}
|
|
};
|
|
buildLoraDOM(d.models || []);
|
|
});
|
|
}
|
|
|
|
sendNotebookToCanvas() {
|
|
if (!this.currentNotebook) return;
|
|
const data = this.currentNotebook.data || {};
|
|
if (!data.mainModel) {
|
|
alert(window.anomalous_browser_lang === 'zh' ? "璇峰厛閫夋嫨涓€涓富妯″瀷銆? : "Please select a Main Model first.");
|
|
return;
|
|
}
|
|
|
|
const groupNodes = [];
|
|
const isUnet = data.mainModel.type === 'unet' || data.mainModel.type === 'diffusion_models';
|
|
|
|
const ckptNode = LiteGraph.createNode(isUnet ? "UNETLoader" : "CheckpointLoaderSimple");
|
|
app.graph.add(ckptNode);
|
|
groupNodes.push({ node: ckptNode, relX: 0, relY: 0 });
|
|
|
|
const sub = data.mainModel.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
const relPath = sub ? `${sub}/${data.mainModel.filename}` : data.mainModel.filename;
|
|
this.setWidgetValuePath(ckptNode, relPath);
|
|
|
|
let lastNode = ckptNode;
|
|
let lastModelSlot = isUnet ? 0 : 0;
|
|
let lastClipSlot = isUnet ? null : 1;
|
|
|
|
let relX = 350;
|
|
let relY = 0;
|
|
|
|
data.loras.forEach((lora, idx) => {
|
|
const loraNode = LiteGraph.createNode("LoraLoader");
|
|
app.graph.add(loraNode);
|
|
groupNodes.push({ node: loraNode, relX: relX, relY: relY });
|
|
|
|
const lsub = lora.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
const lrelPath = lsub ? `${lsub}/${lora.filename}` : lora.filename;
|
|
this.setWidgetValuePath(loraNode, lrelPath);
|
|
|
|
lastNode.connect(lastModelSlot, loraNode, 0);
|
|
if (lastClipSlot !== null) lastNode.connect(lastClipSlot, loraNode, 1);
|
|
|
|
lastNode = loraNode;
|
|
lastModelSlot = 0;
|
|
lastClipSlot = 1;
|
|
relX += 350;
|
|
});
|
|
|
|
if (data.promptEn) {
|
|
const posNode = LiteGraph.createNode("CLIPTextEncode");
|
|
posNode.title = "CLIP Text Encode (Positive)";
|
|
app.graph.add(posNode);
|
|
groupNodes.push({ node: posNode, relX: relX, relY: 0 });
|
|
|
|
if (posNode.widgets && posNode.widgets.length > 0) {
|
|
const tw = posNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
|
|
if (tw) tw.value = data.promptEn;
|
|
}
|
|
if (lastClipSlot !== null) {
|
|
lastNode.connect(lastClipSlot, posNode, 0);
|
|
}
|
|
|
|
const negNode = LiteGraph.createNode("CLIPTextEncode");
|
|
negNode.title = "CLIP Text Encode (Negative)";
|
|
app.graph.add(negNode);
|
|
groupNodes.push({ node: negNode, relX: relX, relY: 250 });
|
|
|
|
if (negNode.widgets && negNode.widgets.length > 0) {
|
|
const tw = negNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
|
|
if (tw) tw.value = "text, watermark, ugly, bad anatomy";
|
|
}
|
|
if (lastClipSlot !== null) {
|
|
lastNode.connect(lastClipSlot, negNode, 0);
|
|
}
|
|
}
|
|
|
|
this.nbPanel.style.display = 'none';
|
|
this.close();
|
|
|
|
// Magnetic Sticking Logic
|
|
let isSticking = true;
|
|
const stickHandler = (e) => {
|
|
if (!isSticking || !app.canvas) return;
|
|
const canvas = app.canvas;
|
|
|
|
let canvasX, canvasY;
|
|
if (canvas.convertEventToCanvasOffset) {
|
|
const pos = canvas.convertEventToCanvasOffset(e);
|
|
canvasX = pos[0];
|
|
canvasY = pos[1];
|
|
} else {
|
|
const rect = canvas.canvas.getBoundingClientRect();
|
|
canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale;
|
|
canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale;
|
|
}
|
|
|
|
groupNodes.forEach(item => {
|
|
const w = (item.node.size && item.node.size[0]) ? item.node.size[0] : 200;
|
|
item.node.pos = [canvasX - (w / 2) + item.relX, canvasY - 20 + item.relY];
|
|
});
|
|
canvas.setDirty(true, true);
|
|
};
|
|
|
|
const dropHandler = (e) => {
|
|
if (!isSticking) return;
|
|
isSticking = false;
|
|
window.removeEventListener('mousemove', stickHandler, true);
|
|
window.removeEventListener('pointerdown', dropHandler, true);
|
|
window.removeEventListener('mousedown', dropHandler, true);
|
|
window.removeEventListener('click', dropHandler, true);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
window.addEventListener('mousemove', stickHandler, true);
|
|
setTimeout(() => {
|
|
window.addEventListener('pointerdown', dropHandler, true);
|
|
window.addEventListener('mousedown', dropHandler, true);
|
|
window.addEventListener('click', dropHandler, true);
|
|
}, 100);
|
|
}
|
|
setWidgetValuePath(node, relPath) {
|
|
if (!node.widgets || node.widgets.length === 0) return;
|
|
const w = node.widgets.find(wg => wg.type === 'combo');
|
|
const targetWidget = w || node.widgets[0];
|
|
if (!targetWidget) return;
|
|
|
|
if (targetWidget.options && targetWidget.options.values) {
|
|
const normalizedTarget = relPath.replace(/\\/g, '/');
|
|
const match = targetWidget.options.values.find(v => {
|
|
return String(v).replace(/\\/g, '/') === normalizedTarget;
|
|
});
|
|
if (match) {
|
|
targetWidget.value = match;
|
|
return;
|
|
}
|
|
}
|
|
targetWidget.value = relPath;
|
|
}
|
|
|
|
hideAllPanels() {
|
|
this.grid.style.display = 'none';
|
|
this.detailPanel.style.display = 'none';
|
|
if (this.galleryPanel) this.galleryPanel.style.display = 'none';
|
|
if (this.nbPanel) this.nbPanel.style.display = 'none';
|
|
if (this.doctorPanel) this.doctorPanel.style.display = 'none';
|
|
if (this.assistantPanel) this.assistantPanel.style.display = 'none';
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
}
|
|
|
|
initDoctorPanel() {
|
|
this.doctorPanelInitialized = true;
|
|
this.doctorPanel.innerHTML = '';
|
|
this.doctorPanel.style.padding = '0'; // Use full bleed
|
|
this.doctorPanel.style.boxSizing = 'border-box';
|
|
this.doctorPanel.style.overflow = 'hidden';
|
|
|
|
// Beautiful dark gradient header
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'padding: 24px 28px; background: linear-gradient(180deg, rgba(20,20,25,1) 0%, rgba(20,20,25,0) 100%); display:flex; flex-direction:column; gap:16px; flex-shrink:0; border-bottom: 1px solid rgba(255,255,255,0.05);';
|
|
|
|
const topRow = document.createElement('div');
|
|
topRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;';
|
|
|
|
const titleEl = document.createElement('div');
|
|
titleEl.style.cssText = 'display:flex;align-items:center;gap:12px;';
|
|
titleEl.innerHTML = `<span style="font-size:24px; filter: drop-shadow(0 0 8px rgba(0,255,204,0.3));">馃┖</span><span style="font-size:18px;font-weight:600;color:#fff;font-family:Inter, sans-serif; letter-spacing: 0.5px;">${window.anomalous_browser_lang === 'zh' ? '鍏ㄥ眬浣撴涓績' : 'Global Health Center'}</span>`;
|
|
|
|
// Header control group on the right side (contains toggle & close button)
|
|
const controlGroup = document.createElement('div');
|
|
controlGroup.style.cssText = 'display:flex;align-items:center;gap:12px;';
|
|
|
|
// Auto Scan Toggle inside Doctor Panel
|
|
const autoScanToggle = document.createElement('div');
|
|
autoScanToggle.style.cssText = 'display:flex;align-items:center;gap:8px;background:rgba(255,255,255,0.05);padding:6px 12px;border-radius:6px;cursor:pointer;transition:all 0.2s;';
|
|
|
|
const renderAutoScanToggle = () => {
|
|
let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true';
|
|
autoScanToggle.innerHTML = window.anomalous_browser_lang === 'zh'
|
|
? (isAutoEnabled
|
|
? '<span style="font-size:16px;">馃泿锔?/span><span style="font-size:12px;color:#00ffcc;font-weight:500;">ComfyUI 鍚姩鏃惰嚜鍔ㄦ娴嬪苟鏇挎崲缂哄け妯″瀷 [宸插紑鍚痌</span>'
|
|
: '<span style="font-size:16px;opacity:0.5;">馃敃</span><span style="font-size:12px;color:#aaa;">ComfyUI 鍚姩鏃惰嚜鍔ㄦ娴嬪苟鏇挎崲缂哄け妯″瀷 [榛樿鍏抽棴]</span>')
|
|
: (isAutoEnabled
|
|
? '<span style="font-size:16px;">馃泿锔?/span><span style="font-size:12px;color:#00ffcc;font-weight:500;">Auto-Detect on ComfyUI Startup [ON]</span>'
|
|
: '<span style="font-size:16px;opacity:0.5;">馃敃</span><span style="font-size:12px;color:#aaa;">Auto-Detect on ComfyUI Startup [OFF]</span>');
|
|
};
|
|
renderAutoScanToggle();
|
|
|
|
autoScanToggle.onmouseover = () => { autoScanToggle.style.background = 'rgba(255,255,255,0.1)'; };
|
|
autoScanToggle.onmouseout = () => { autoScanToggle.style.background = 'rgba(255,255,255,0.05)'; };
|
|
autoScanToggle.onclick = () => {
|
|
let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true';
|
|
localStorage.setItem('anomalous_auto_scan_enabled', isAutoEnabled ? 'false' : 'true');
|
|
renderAutoScanToggle();
|
|
};
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerHTML = '鉁?;
|
|
closeBtn.style.cssText = 'background:transparent;border:none;color:rgba(255,255,255,0.4);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:4px;transition:all 0.2s;';
|
|
closeBtn.onmouseover = () => { closeBtn.style.background = 'rgba(255,255,255,0.1)'; closeBtn.style.color = '#fff'; };
|
|
closeBtn.onmouseout = () => { closeBtn.style.background = 'transparent'; closeBtn.style.color = 'rgba(255,255,255,0.4)'; };
|
|
closeBtn.onclick = () => { this.doctorPanel.style.display = 'none'; if (this.grid) this.grid.style.display = 'grid'; };
|
|
|
|
controlGroup.appendChild(autoScanToggle);
|
|
controlGroup.appendChild(closeBtn);
|
|
|
|
topRow.appendChild(titleEl);
|
|
topRow.appendChild(controlGroup);
|
|
header.appendChild(topRow);
|
|
|
|
// Stats row placeholder (populated by renderGlobalDashboard)
|
|
const statsRow = document.createElement('div');
|
|
statsRow.id = 'anomalous-doctor-stats-row';
|
|
statsRow.style.cssText = 'display:flex; gap:12px; align-items:center;';
|
|
|
|
header.appendChild(statsRow);
|
|
|
|
this.doctorPanel.appendChild(header);
|
|
|
|
// Node list container (takes up remaining space)
|
|
const nodeListContainer = document.createElement('div');
|
|
nodeListContainer.id = 'anomalous-doctor-node-list';
|
|
nodeListContainer.style.cssText = 'display:flex;flex-direction:column;gap:12px;overflow-y:auto;flex:1; padding: 20px 28px; background: rgba(0,0,0,0.2);';
|
|
this.doctorPanel.appendChild(nodeListContainer);
|
|
|
|
// Initial render
|
|
this.renderGlobalDashboard();
|
|
}
|
|
diagnoseNode(node) {
|
|
// This method serves the Node Assistant panel only
|
|
if (!this.assistantPanelInitialized) {
|
|
this.initAssistantPanel();
|
|
}
|
|
const placeholder = document.getElementById('anomalous-assistant-placeholder');
|
|
const nodeContent = document.getElementById('anomalous-assistant-node-content');
|
|
if (!placeholder || !nodeContent) return;
|
|
|
|
if (!node) {
|
|
placeholder.innerHTML = `<div style="font-size:48px;">馃</div><div style="text-align:center;">${window.anomalous_browser_lang === 'zh' ? '璇峰湪鐢诲竷涓?strong style="color:#aaa">鐐瑰嚮閫変腑浠绘剰鑺傜偣</strong>' : 'Please <strong style="color:#aaa">select any node</strong> in the canvas'}</div>`;
|
|
placeholder.style.display = 'flex';
|
|
nodeContent.style.display = 'none';
|
|
nodeContent.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const modelWidgets = [];
|
|
if (node.widgets) {
|
|
for (const w of node.widgets) {
|
|
if (w.type === 'combo' && typeof w.value === 'string') {
|
|
if (w.value.match(/\.(safetensors|ckpt|pt|bin|pth)$/i)) modelWidgets.push(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (modelWidgets.length === 0) {
|
|
placeholder.innerHTML = `<div style="font-size:36px;">鈿狅笍</div><div style="text-align:center;">${window.anomalous_browser_lang === 'zh' ? '璇ヨ妭鐐规病鏈夊彈鏀寔鐨勬ā鍨嬪弬鏁? : 'No supported model parameter on this node'}</div><div style="font-size:12px;color:#555;margin-top:4px;">${node.type || ''}</div>`;
|
|
placeholder.style.display = 'flex';
|
|
nodeContent.style.display = 'none';
|
|
nodeContent.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
placeholder.style.display = 'none';
|
|
nodeContent.style.display = 'flex';
|
|
nodeContent.innerHTML = '';
|
|
|
|
const titleBar = document.createElement('div');
|
|
titleBar.style.cssText = 'padding:14px 20px 12px;border-bottom:1px solid rgba(255,255,255,0.07);display:flex;align-items:center;gap:10px;flex-shrink:0;';
|
|
titleBar.innerHTML = `<span style="font-size:18px;">馃</span><span style="font-weight:bold;color:#fff;font-size:14px;">${node.title || node.type || 'Node'}</span><span style="font-size:11px;color:#555;margin-left:auto;">${node.type || ''}</span>`;
|
|
nodeContent.appendChild(titleBar);
|
|
|
|
for (const w of modelWidgets) {
|
|
this.renderAssistantModelCard(node, w, nodeContent);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
renderGlobalDashboard() {
|
|
const content = document.getElementById('anomalous-doctor-node-list');
|
|
const statsRow = document.getElementById('anomalous-doctor-stats-row');
|
|
if (!content || !statsRow) return;
|
|
content.innerHTML = '';
|
|
statsRow.innerHTML = '';
|
|
|
|
if (this.doctorPanel) this.doctorPanel.currentDiagnosedNode = 'global';
|
|
|
|
let nodes = [];
|
|
if (app.graph && app.graph.computeExecutionOrder) {
|
|
nodes = app.graph.computeExecutionOrder(false, true);
|
|
} else if (app.graph && app.graph._nodes) {
|
|
nodes = app.graph._nodes;
|
|
}
|
|
|
|
let total = 0, healthy = 0, missing = 0;
|
|
let missingNodesData = [];
|
|
|
|
// Collect data
|
|
for (const node of nodes) {
|
|
if (!node.widgets) continue;
|
|
for (const w of node.widgets) {
|
|
if (w.type === 'combo' && typeof w.value === 'string' && w.value.match(/\.(safetensors|ckpt|pt|bin|pth)$/i)) {
|
|
total++;
|
|
const val = w.value;
|
|
let isHealthy = false;
|
|
let exactMatch = null;
|
|
if (w.options && w.options.values && w.options.values.includes(val)) {
|
|
isHealthy = true;
|
|
} else if (w.options && w.options.values) {
|
|
const normVal = val.replace(/\\/g, '/');
|
|
exactMatch = w.options.values.find(v => typeof v === 'string' && v.replace(/\\/g, '/') === normVal);
|
|
if (exactMatch) isHealthy = true;
|
|
}
|
|
if (isHealthy) healthy++; else missing++;
|
|
|
|
missingNodesData.push({ node, w, val, isHealthy, exactMatch });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render Stats Badges
|
|
const createBadge = (label, count, color, bg) => {
|
|
return `<div style="display:flex;align-items:center;gap:8px;padding:8px 16px;border-radius:20px;background:${bg};border:1px solid ${color}33;">
|
|
<span style="color:${color};font-size:13px;font-weight:600;">${label}</span>
|
|
<span style="color:#fff;font-size:14px;font-weight:bold;">${count}</span>
|
|
</div>`;
|
|
};
|
|
const zh = window.anomalous_browser_lang === 'zh';
|
|
statsRow.innerHTML = `
|
|
${createBadge(zh?'鎬绘ā鍨?:'Total', total, '#aaa', 'rgba(255,255,255,0.05)')}
|
|
${createBadge(zh?'馃煝 鍋ュ悍':'馃煝 Healthy', healthy, '#28a745', 'rgba(40, 167, 69, 0.1)')}
|
|
${createBadge(zh?'馃敶 缂哄け':'馃敶 Missing', missing, '#ff6b6b', 'rgba(220, 53, 69, 0.1)')}
|
|
`;
|
|
|
|
if (total === 0) {
|
|
content.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:rgba(255,255,255,0.3);font-size:14px;">
|
|
<div style="font-size:48px;margin-bottom:16px;">馃懟</div>
|
|
${zh ? '褰撳墠宸ヤ綔娴佹病鏈変换浣曟ā鍨嬭妭鐐? : 'No models found in this workflow'}
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
// Render List
|
|
for (const data of missingNodesData) {
|
|
const { node, w, val, isHealthy, exactMatch } = data;
|
|
|
|
const item = document.createElement('div');
|
|
item.style.cssText = `display:flex; flex-direction:column; padding:16px 20px; background:rgba(255,255,255,0.02); border-radius:12px; border:1px solid rgba(255,255,255,0.04); transition:all 0.2s; position:relative; overflow:hidden; flex-shrink:0;`;
|
|
item.onmouseover = () => item.style.background = 'rgba(255,255,255,0.04)';
|
|
item.onmouseout = () => item.style.background = 'rgba(255,255,255,0.02)';
|
|
|
|
// Accent bar
|
|
const accent = document.createElement('div');
|
|
accent.style.cssText = `position:absolute; left:0; top:0; bottom:0; width:4px; background:${isHealthy ? '#28a745' : '#ff6b6b'};`;
|
|
item.appendChild(accent);
|
|
|
|
const top = document.createElement('div');
|
|
top.style.cssText = 'display:flex; justify-content:space-between; align-items:flex-start; margin-left:8px;';
|
|
|
|
const left = document.createElement('div');
|
|
left.style.cssText = 'display:flex; flex-direction:column; gap:6px;';
|
|
|
|
const nodeTitle = document.createElement('div');
|
|
nodeTitle.innerHTML = `<span style="color:rgba(255,255,255,0.4);font-size:12px;">#${node.id}</span> <span style="color:#aaa;font-size:12px;font-weight:600;">${node.title || node.type}</span>`;
|
|
|
|
const fileText = document.createElement('div');
|
|
fileText.innerText = val.split(/[\\/]/).pop();
|
|
fileText.style.cssText = 'color:#fff; font-size:15px; font-weight:600; word-break:break-all; font-family:Inter, sans-serif;';
|
|
|
|
left.appendChild(nodeTitle);
|
|
left.appendChild(fileText);
|
|
|
|
const right = document.createElement('div');
|
|
right.style.cssText = 'display:flex; align-items:center; gap:12px;';
|
|
|
|
if (isHealthy && exactMatch && exactMatch !== val) {
|
|
right.innerHTML = `<div style="text-align:right;"><div style="color:#ffc107;font-size:13px;font-weight:bold;">馃煛 ${zh?'鑷姩閲嶅畾鍚?:'Auto-Redirected'}</div><div style="color:rgba(255,255,255,0.4);font-size:11px;margin-top:4px;">${exactMatch.split(/[\/]/).pop()}</div></div>`;
|
|
} else if (isHealthy) {
|
|
right.innerHTML = `<div style="color:#28a745;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(40,167,69,0.1);border-radius:20px;">馃煝 ${zh?'姝e父':'Ready'}</div>`;
|
|
} else {
|
|
right.innerHTML = `<div style="color:#ff6b6b;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(220,53,69,0.1);border-radius:20px;">馃敶 ${zh?'涓㈠け':'Missing'}</div>`;
|
|
}
|
|
|
|
top.appendChild(left);
|
|
top.appendChild(right);
|
|
item.appendChild(top);
|
|
|
|
const actionRow = document.createElement('div');
|
|
actionRow.style.cssText = 'display:flex; gap:10px; margin-top:16px; margin-left:8px;';
|
|
|
|
const civitaiBtn = document.createElement('button');
|
|
civitaiBtn.innerHTML = zh ? '馃寪 C绔? : '馃寪 Civitai';
|
|
civitaiBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
|
|
civitaiBtn.onmouseover = () => civitaiBtn.style.background = 'rgba(255,255,255,0.2)';
|
|
civitaiBtn.onmouseout = () => civitaiBtn.style.background = 'rgba(255,255,255,0.1)';
|
|
civitaiBtn.onclick = () => {
|
|
const searchName = val.split(/[/\\]/).pop().replace('.safetensors', '').replace('.ckpt', '').replace('.pt', '');
|
|
const url = `https://civitai.com/search/models?sortBy=models_v9&query=${encodeURIComponent(searchName)}`;
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
if (!isHealthy) {
|
|
const deepScanBtn = document.createElement('button');
|
|
deepScanBtn.innerHTML = zh ? '馃攳 娣卞害鍝堝笇鎵弿' : '馃攳 Deep Hash Scan';
|
|
deepScanBtn.style.cssText = 'padding:8px 16px; background:#1a73e8; color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
|
|
deepScanBtn.onmouseover = () => deepScanBtn.style.background = '#1557b0';
|
|
deepScanBtn.onmouseout = () => deepScanBtn.style.background = '#1a73e8';
|
|
deepScanBtn.onclick = async () => {
|
|
deepScanBtn.innerText = zh ? '鈴?姝e湪鍚姩鎵弿寮曟搸...' : '鈴?Starting scan engine...';
|
|
deepScanBtn.disabled = true;
|
|
deepScanBtn.style.opacity = '0.7';
|
|
|
|
try {
|
|
const r = await fetch('/anomalous/scan_missing_models', { method: 'POST' });
|
|
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
|
|
const rData = await r.json();
|
|
if (rData.status === 'error' && rData.message !== 'Scan already in progress') {
|
|
throw new Error(rData.message);
|
|
}
|
|
|
|
const checkInterval = setInterval(async () => {
|
|
try {
|
|
const statusRes = await fetch('/anomalous/global_scan_status');
|
|
const statusData = await statusRes.json();
|
|
|
|
if (statusData.scanning) {
|
|
let filename = statusData.filename || '';
|
|
if (filename.length > 20) filename = filename.substring(0, 10) + '...' + filename.substring(filename.length - 7);
|
|
deepScanBtn.innerText = zh
|
|
? `鈴?鎵弿涓?(${statusData.current}/${statusData.total}) ${filename}`
|
|
: `鈴?Scanning (${statusData.current}/${statusData.total}) ${filename}`;
|
|
} else {
|
|
clearInterval(checkInterval);
|
|
if (statusData.error) {
|
|
alert(zh ? '鉂?鎵弿杩囩▼涓彂鐢熼敊璇? ' + statusData.error : '鉂?Scan error: ' + statusData.error);
|
|
}
|
|
|
|
deepScanBtn.innerText = zh ? '鈴?姝e湪鍖归厤骞舵浛鎹㈤绾㈣妭鐐?..' : '鈴?Matching and resolving red nodes...';
|
|
|
|
if (window.anomalous_reload_hashes) {
|
|
await window.anomalous_reload_hashes();
|
|
}
|
|
|
|
if (window.anomalous_resolve_all_missing_nodes) {
|
|
await window.anomalous_resolve_all_missing_nodes(true, false);
|
|
}
|
|
|
|
let stillMissing = false;
|
|
for (let i = 0; i < node.widgets.length; i++) {
|
|
const wi = node.widgets[i];
|
|
if (wi.name === w.name && wi.options && wi.options.values && !wi.options.values.includes(wi.value)) {
|
|
stillMissing = true;
|
|
}
|
|
}
|
|
|
|
const scanInfo = zh
|
|
? `鍏辨繁搴︽壂鎻忎簡 ${statusData.total} 涓己澶变俊鎭殑妯″瀷銆俙
|
|
: `Deep scanned ${statusData.total} models with missing info.`;
|
|
|
|
if (stillMissing) {
|
|
alert(zh ? `鉂?鎵弿缁撴潫锛屾湰鍦版湭鍖归厤鍒版ā鍨嬨€俓n\n${scanInfo}\n\n杩欒鏄庢ā鍨嬪彲鑳界湡鐨勪笉鍦ㄦ偍鐨勭‖鐩橀噷锛屾垨鑰呮偍鍒犻櫎浜嗗師鏈褰曠潃鍝堝笇鐨勬簮鏂囦欢銆俓n璇风偣鍑诲崱鐗囦笂鐨勩€愷煂?C绔欍€戝幓浜戠涓嬭浇銆俙 : `鉂?Scan finished, but no local match found.\n\n${scanInfo}\n\nThis means the model is truly missing from your disk, or the original source file with hash was deleted.\nPlease click [馃寪 Civitai] to download it from the cloud.`);
|
|
} else {
|
|
alert(zh ? `鉁?娣卞害鎵弿鎴愬姛锛佸凡鑷姩淇璇ヨ妭鐐癸紒\n\n${scanInfo}` : `鉁?Deep Scan successful! Node auto-healed!\n\n${scanInfo}`);
|
|
}
|
|
|
|
this.renderGlobalDashboard();
|
|
deepScanBtn.innerHTML = zh ? '馃攳 娣卞害鍝堝笇鎵弿' : '馃攳 Deep Hash Scan';
|
|
deepScanBtn.disabled = false;
|
|
deepScanBtn.style.opacity = '1';
|
|
}
|
|
} catch (err) {
|
|
// Ignore poll errors
|
|
}
|
|
}, 500);
|
|
|
|
} catch(e) {
|
|
alert(zh ? '鉂?鎵弿鍑洪敊: ' + e.message : '鉂?Scan Error: ' + e.message);
|
|
deepScanBtn.innerHTML = zh ? '馃攳 娣卞害鍝堝笇鎵弿' : '馃攳 Deep Hash Scan';
|
|
deepScanBtn.disabled = false;
|
|
deepScanBtn.style.opacity = '1';
|
|
}
|
|
};
|
|
|
|
const manualBtn = document.createElement('button');
|
|
manualBtn.innerHTML = zh ? '馃攢 鎵嬪姩鏇挎崲' : '馃攢 Manual Replace';
|
|
manualBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
|
|
manualBtn.onmouseover = () => manualBtn.style.background = 'rgba(255,255,255,0.2)';
|
|
manualBtn.onmouseout = () => manualBtn.style.background = 'rgba(255,255,255,0.1)';
|
|
manualBtn.onclick = () => {
|
|
this._openGalleryReplacer(node, w);
|
|
};
|
|
|
|
actionRow.appendChild(deepScanBtn);
|
|
actionRow.appendChild(manualBtn);
|
|
|
|
const pathText = document.createElement('div');
|
|
pathText.innerText = `${zh?'鍘熻矾寰?:'Original'}: ${val}`;
|
|
pathText.style.cssText = 'margin-top:12px; margin-left:8px; color:rgba(255,255,255,0.3); font-size:11px; font-family:monospace; word-break:break-all;';
|
|
item.appendChild(pathText);
|
|
}
|
|
|
|
actionRow.appendChild(civitaiBtn);
|
|
item.appendChild(actionRow);
|
|
|
|
content.appendChild(item);
|
|
}
|
|
}
|
|
|
|
initAssistantPanel() {
|
|
this.assistantPanelInitialized = true;
|
|
this.assistantPanel.innerHTML = '';
|
|
this.assistantPanel.style.padding = '0';
|
|
this.assistantPanel.style.overflow = 'hidden';
|
|
|
|
if (!this._assistantPanelHooked) {
|
|
this._assistantPanelHooked = true;
|
|
const self = this;
|
|
const originalOnSelected = app.canvas.onNodeSelected;
|
|
app.canvas.onNodeSelected = function (node) {
|
|
if (originalOnSelected) originalOnSelected.apply(this, arguments);
|
|
if (self.assistantPanel && self.assistantPanel.style.display !== 'none') {
|
|
self.diagnoseNode(node);
|
|
}
|
|
if (self.doctorPanel && self.doctorPanel.style.display !== 'none') {
|
|
self.diagnoseNodeForDoctor(node);
|
|
}
|
|
};
|
|
const originalOnDeselected = app.canvas.onNodeDeselected;
|
|
app.canvas.onNodeDeselected = function (node) {
|
|
if (originalOnDeselected) originalOnDeselected.apply(this, arguments);
|
|
if (self.assistantPanel && self.assistantPanel.style.display !== 'none') {
|
|
const stillSelected = Object.values(app.canvas.selected_nodes || {});
|
|
if (stillSelected.length > 0) self.diagnoseNode(stillSelected[0]);
|
|
else self.diagnoseNode(null);
|
|
}
|
|
if (self.doctorPanel && self.doctorPanel.style.display !== 'none') {
|
|
const stillSelected = Object.values(app.canvas.selected_nodes || {});
|
|
if (stillSelected.length > 0) self.diagnoseNodeForDoctor(stillSelected[0]);
|
|
else self.diagnoseNodeForDoctor(null);
|
|
}
|
|
};
|
|
}
|
|
|
|
const placeholder = document.createElement('div');
|
|
placeholder.id = 'anomalous-assistant-placeholder';
|
|
placeholder.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:#666;font-size:15px;gap:12px;padding:40px;text-align:center;';
|
|
placeholder.innerHTML = `<div style="font-size:48px;">馃</div><div>${window.anomalous_browser_lang === 'zh' ? '璇峰湪鐢诲竷涓?strong style="color:#aaa">鐐瑰嚮閫変腑浠绘剰鑺傜偣</strong>' : 'Please <strong style="color:#aaa">select any node</strong> in the canvas'}</div>`;
|
|
this.assistantPanel.appendChild(placeholder);
|
|
|
|
const nodeContent = document.createElement('div');
|
|
nodeContent.id = 'anomalous-assistant-node-content';
|
|
nodeContent.style.cssText = 'display:none;flex-direction:column;flex:1;overflow-y:auto;';
|
|
this.assistantPanel.appendChild(nodeContent);
|
|
}
|
|
|
|
renderAssistantModelCard(node, w, container) {
|
|
const val = w.value;
|
|
const filename = val.split(/[\\/]/).pop();
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.style.cssText = 'padding:16px 20px;border-bottom:1px solid rgba(255,255,255,0.05);display:flex;flex-direction:column;gap:14px;';
|
|
|
|
// Preview image
|
|
const previewBox = document.createElement('div');
|
|
previewBox.style.cssText = 'width:100%;aspect-ratio:1.5;max-height:280px;background:#0c0c0e;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-shrink:0;';
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '鍔犺浇棰勮...' : 'Loading preview...'}</span>`;
|
|
wrapper.appendChild(previewBox);
|
|
|
|
// Name and path
|
|
const nameEl = document.createElement('div');
|
|
nameEl.style.cssText = 'color:#fff;font-weight:bold;font-size:14px;word-break:break-all;';
|
|
nameEl.innerText = filename;
|
|
const pathEl = document.createElement('div');
|
|
pathEl.style.cssText = 'color:#555;font-size:11px;word-break:break-all;margin-top:-10px;';
|
|
pathEl.innerText = val;
|
|
wrapper.appendChild(nameEl);
|
|
wrapper.appendChild(pathEl);
|
|
|
|
// Metadata zone (populated async)
|
|
const metaZone = document.createElement('div');
|
|
metaZone.style.cssText = 'display:flex;flex-direction:column;gap:10px;';
|
|
wrapper.appendChild(metaZone);
|
|
|
|
// Gallery replacer button
|
|
const actionRow = document.createElement('div');
|
|
actionRow.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;';
|
|
const browseBtn = document.createElement('button');
|
|
browseBtn.innerText = window.anomalous_browser_lang === 'zh' ? '馃攢 鏇存崲妯″瀷' : '馃攢 Change Model';
|
|
browseBtn.style.cssText = 'flex:1;padding:9px 12px;background:rgba(255,193,7,0.85);color:#000;border:none;border-radius:6px;cursor:pointer;font-weight:bold;font-size:13px;transition:filter 0.2s;';
|
|
browseBtn.onmouseover = () => browseBtn.style.filter = 'brightness(1.1)';
|
|
browseBtn.onmouseout = () => browseBtn.style.filter = 'brightness(1)';
|
|
browseBtn.onclick = () => this._openGalleryReplacer(node, w);
|
|
|
|
const profileBtn = document.createElement('button');
|
|
profileBtn.innerText = window.anomalous_browser_lang === 'zh' ? '馃摉 鏌ョ湅妗f' : '馃摉 View Profile';
|
|
profileBtn.style.cssText = 'padding:9px 12px;background:rgba(138,180,248,0.1);color:#8AB4F8;border:1px solid rgba(138,180,248,0.3);border-radius:6px;cursor:pointer;font-size:13px;transition:filter 0.2s;';
|
|
profileBtn.onmouseover = () => profileBtn.style.filter = 'brightness(1.2)';
|
|
profileBtn.onmouseout = () => profileBtn.style.filter = 'brightness(1)';
|
|
|
|
actionRow.appendChild(browseBtn);
|
|
actionRow.appendChild(profileBtn);
|
|
wrapper.appendChild(actionRow);
|
|
container.appendChild(wrapper);
|
|
|
|
// Async: load preview + metadata
|
|
fetch(`/anomalous/find_model?search=${encodeURIComponent(val.replace(/\\/g, '/'))}`)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
// Preview
|
|
if (d.status === 'success' && d.model && d.model.preview_url) {
|
|
previewBox.innerHTML = '';
|
|
const pu = d.model.preview_url;
|
|
const isVid = /\.mp4(?:&|$)/i.test(pu) || /\.webm(?:&|$)/i.test(pu);
|
|
if (isVid) {
|
|
previewBox.innerHTML = `<video src="${pu}" muted loop autoplay playsinline style="width:100%;height:100%;object-fit:contain;"></video>`;
|
|
} else {
|
|
previewBox.innerHTML = `<img src="${pu}" style="width:100%;height:100%;object-fit:contain;" />`;
|
|
}
|
|
} else {
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '鏆傛棤棰勮鍥? : 'No preview'}</span>`;
|
|
}
|
|
|
|
// Profile button links to detail
|
|
if (d.status === 'success' && d.model) {
|
|
profileBtn.onclick = () => {
|
|
this.assistantPanel.style.display = 'none';
|
|
this.currentType = d.type;
|
|
this.currentPathIdx = d.path_idx;
|
|
this.currentSubfolder = d.subfolder;
|
|
this.historyStack = this.historyStack || [];
|
|
this.historyStack.push({ type: 'assistant' });
|
|
this.showDetail(d.model);
|
|
if (this.foldersData) this.renderSidebar();
|
|
};
|
|
}
|
|
|
|
// Metadata
|
|
if (d.status === 'success' && d.model && d.model.metadata) {
|
|
const meta = d.model.metadata;
|
|
|
|
// Type + base model badges
|
|
if (d.type || meta.baseModel) {
|
|
const badgeRow = document.createElement('div');
|
|
badgeRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;';
|
|
if (d.type) {
|
|
const b = document.createElement('span');
|
|
b.style.cssText = 'background:rgba(138,180,248,0.15);color:#8AB4F8;padding:3px 8px;border-radius:4px;font-size:11px;';
|
|
b.innerText = d.type; badgeRow.appendChild(b);
|
|
}
|
|
if (meta.baseModel) {
|
|
const b = document.createElement('span');
|
|
b.style.cssText = 'background:rgba(0,255,204,0.1);color:#00ffcc;padding:3px 8px;border-radius:4px;font-size:11px;';
|
|
b.innerText = meta.baseModel; badgeRow.appendChild(b);
|
|
}
|
|
metaZone.appendChild(badgeRow);
|
|
}
|
|
|
|
// Trigger words
|
|
const triggers = meta.trainedWords || meta.trigger_words || meta.trained_words;
|
|
if (triggers && triggers.length > 0) {
|
|
const trigSection = document.createElement('div');
|
|
trigSection.style.cssText = 'background:rgba(255,255,255,0.04);border-radius:6px;padding:10px 12px;';
|
|
const trigTitle = document.createElement('div');
|
|
trigTitle.style.cssText = 'color:#aaa;font-size:11px;margin-bottom:8px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
|
|
trigTitle.innerText = window.anomalous_browser_lang === 'zh' ? '瑙﹀彂璇? : 'Trigger Words';
|
|
const tagList = document.createElement('div');
|
|
tagList.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;';
|
|
const words = Array.isArray(triggers) ? triggers : [triggers];
|
|
words.forEach(word => {
|
|
const tag = document.createElement('span');
|
|
tag.style.cssText = 'background:rgba(255,193,7,0.12);color:#ffc107;padding:3px 8px;border-radius:4px;font-size:12px;cursor:pointer;';
|
|
tag.innerText = word;
|
|
tag.title = window.anomalous_browser_lang === 'zh' ? '鐐瑰嚮澶嶅埗' : 'Click to copy';
|
|
tag.onclick = () => {
|
|
navigator.clipboard.writeText(word).then(() => { const o = tag.innerText; tag.innerText = '鉁?; setTimeout(() => tag.innerText = o, 1000); });
|
|
};
|
|
tagList.appendChild(tag);
|
|
});
|
|
const copyAll = document.createElement('button');
|
|
copyAll.style.cssText = 'background:transparent;border:1px solid #444;color:#888;border-radius:4px;padding:3px 8px;font-size:11px;cursor:pointer;margin-top:4px;';
|
|
copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '馃搵 鍏ㄩ儴澶嶅埗' : '馃搵 Copy All';
|
|
copyAll.onclick = () => {
|
|
navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.innerText = '鉁?; setTimeout(() => copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '馃搵 鍏ㄩ儴澶嶅埗' : '馃搵 Copy All', 1500); });
|
|
};
|
|
trigSection.appendChild(trigTitle);
|
|
trigSection.appendChild(tagList);
|
|
trigSection.appendChild(copyAll);
|
|
metaZone.appendChild(trigSection);
|
|
}
|
|
|
|
// Custom notes (parchment)
|
|
const textNotes = meta.custom_notes || meta.notes;
|
|
if (textNotes) {
|
|
const notesCard = document.createElement('div');
|
|
notesCard.style.cssText = 'background:linear-gradient(135deg,#262522 0%,#202124 100%);border:1px solid #3c4043;border-left:4px solid #a38d53;border-radius:4px 8px 8px 4px;padding:12px 14px;';
|
|
const notesTitle = document.createElement('div');
|
|
notesTitle.style.cssText = 'color:#a38d53;font-size:11px;font-weight:bold;margin-bottom:6px;';
|
|
notesTitle.innerText = window.anomalous_browser_lang === 'zh' ? '馃摑 鎴戠殑澶囨敞' : '馃摑 My Notes';
|
|
const notesText = document.createElement('div');
|
|
notesText.style.cssText = 'color:#d4c4a0;font-size:13px;line-height:1.6;white-space:pre-wrap;';
|
|
notesText.innerText = textNotes;
|
|
notesCard.appendChild(notesTitle);
|
|
notesCard.appendChild(notesText);
|
|
metaZone.appendChild(notesCard);
|
|
}
|
|
}
|
|
|
|
// History gallery 鈥?always load if we can resolve the filename
|
|
const resolvedFilename = (d.status === 'success' && d.model) ? (d.model.filename || filename) : filename;
|
|
this._loadAssistantHistory(resolvedFilename, metaZone, d.status === 'success' ? d.model : null);
|
|
}).catch(() => {
|
|
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '鏃犳硶鍔犺浇棰勮' : 'Failed to load preview'}</span>`;
|
|
|
|
// Still try to load history gallery by filename
|
|
this._loadAssistantHistory(filename, metaZone, null);
|
|
});
|
|
}
|
|
|
|
_loadAssistantHistory(filename, container, model) {
|
|
fetch('/anomalous/model_images?model_name=' + encodeURIComponent(filename) + '&t=' + Date.now())
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const images = data.images || [];
|
|
if (images.length === 0) return;
|
|
|
|
const section = document.createElement('div');
|
|
section.style.cssText = 'display:flex;flex-direction:column;gap:8px;';
|
|
|
|
// Section header with count + full gallery button
|
|
const sectionHeader = document.createElement('div');
|
|
sectionHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;';
|
|
const sectionTitle = document.createElement('div');
|
|
sectionTitle.style.cssText = 'color:#aaa;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
|
|
sectionTitle.innerText = window.anomalous_browser_lang === 'zh' ? `馃柤锔?鍘嗗彶鐢熸垚鍥?(${images.length})` : `馃柤锔?History (${images.length})`;
|
|
sectionHeader.appendChild(sectionTitle);
|
|
|
|
// "View all" button if model is available
|
|
if (model) {
|
|
const viewAllBtn = document.createElement('button');
|
|
viewAllBtn.innerText = window.anomalous_browser_lang === 'zh' ? '鏌ョ湅鍏ㄩ儴 鈫? : 'View All 鈫?;
|
|
viewAllBtn.style.cssText = 'background:transparent;border:1px solid rgba(138,180,248,0.3);color:#8AB4F8;font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.2s;';
|
|
viewAllBtn.onmouseover = () => { viewAllBtn.style.background = 'rgba(138,180,248,0.1)'; };
|
|
viewAllBtn.onmouseout = () => { viewAllBtn.style.background = 'transparent'; };
|
|
viewAllBtn.onclick = () => this.showGeneratedGallery(model);
|
|
sectionHeader.appendChild(viewAllBtn);
|
|
}
|
|
section.appendChild(sectionHeader);
|
|
|
|
const grid = document.createElement('div');
|
|
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:6px;';
|
|
images.slice(0, 16).forEach(img => {
|
|
const card = document.createElement('div');
|
|
card.style.cssText = 'border-radius:6px;overflow:hidden;aspect-ratio:1;background:#111;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;';
|
|
card.onmouseover = () => { card.style.transform = 'scale(1.05)'; card.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; };
|
|
card.onmouseout = () => { card.style.transform = 'scale(1)'; card.style.boxShadow = 'none'; };
|
|
const imgEl = document.createElement('img');
|
|
imgEl.src = img.url || img;
|
|
imgEl.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
|
imgEl.loading = 'lazy';
|
|
card.appendChild(imgEl);
|
|
if (img.workflow) {
|
|
card.title = window.anomalous_browser_lang === 'zh' ? '鐐瑰嚮鎭㈠姝ゅ伐浣滄祦' : 'Click to restore workflow';
|
|
card.onclick = () => {
|
|
try {
|
|
const wf = typeof img.workflow === 'string' ? JSON.parse(img.workflow) : img.workflow;
|
|
if (app && app.loadGraphData) app.loadGraphData(wf);
|
|
} catch (e) { }
|
|
};
|
|
} else if (model) {
|
|
card.title = window.anomalous_browser_lang === 'zh' ? '鐐瑰嚮鏌ョ湅瀹屾暣鍥惧簱' : 'Click to view full gallery';
|
|
card.onclick = () => this.showGeneratedGallery(model);
|
|
}
|
|
grid.appendChild(card);
|
|
});
|
|
section.appendChild(grid);
|
|
container.appendChild(section);
|
|
}).catch(() => { });
|
|
}
|
|
|
|
_openGalleryReplacer(node, w) {
|
|
const modal = document.createElement('div');
|
|
modal.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.88);z-index:999999;display:flex;flex-direction:column;padding:40px;box-sizing:border-box;overflow-y:auto;';
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerHTML = '鉁?;
|
|
closeBtn.style.cssText = 'position:absolute;top:20px;right:20px;background:transparent;border:none;color:#fff;font-size:24px;cursor:pointer;';
|
|
closeBtn.onclick = () => modal.remove();
|
|
modal.appendChild(closeBtn);
|
|
const title = document.createElement('h2');
|
|
title.style.cssText = 'color:#fff;margin:0 0 20px 0;font-size:20px;';
|
|
title.innerText = window.anomalous_browser_lang === 'zh' ? '馃柤锔?閫夋嫨瑕佹浛鎹㈢殑妯″瀷' : '馃柤锔?Select Replacement Model';
|
|
modal.appendChild(title);
|
|
const grid = document.createElement('div');
|
|
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:15px;';
|
|
modal.appendChild(grid);
|
|
document.body.appendChild(modal);
|
|
|
|
const validPaths = (w.options && w.options.values) ? w.options.values.filter(v => typeof v === 'string') : [];
|
|
if (!validPaths.length) { modal.remove(); return; }
|
|
|
|
fetch('/anomalous/resolve_paths_to_previews', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ paths: validPaths })
|
|
}).then(r => r.json()).then(data => {
|
|
const previews = data.previews || {};
|
|
for (const path of validPaths) {
|
|
const card = document.createElement('div');
|
|
card.style.cssText = 'background:#222;border-radius:8px;overflow:hidden;cursor:pointer;display:flex;flex-direction:column;border:1px solid #444;transition:transform 0.2s,box-shadow 0.2s;';
|
|
card.onmouseover = () => { card.style.transform = 'scale(1.05)'; card.style.boxShadow = '0 6px 20px rgba(0,0,0,0.5)'; };
|
|
card.onmouseout = () => { card.style.transform = 'scale(1)'; card.style.boxShadow = 'none'; };
|
|
const imgBox = document.createElement('div');
|
|
imgBox.style.cssText = 'height:150px;background:#111;background-size:cover;background-position:center;display:flex;align-items:center;justify-content:center;font-size:30px;';
|
|
if (previews[path]) { imgBox.style.backgroundImage = `url("${previews[path]}")`; imgBox.innerText = ''; } else { imgBox.innerText = '馃柤锔?; }
|
|
const nameDiv = document.createElement('div');
|
|
nameDiv.style.cssText = 'padding:8px;font-size:12px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
nameDiv.innerText = path.split(/[\\/]/).pop();
|
|
nameDiv.title = path;
|
|
card.appendChild(imgBox);
|
|
card.appendChild(nameDiv);
|
|
grid.appendChild(card);
|
|
card.onclick = () => {
|
|
w.value = path;
|
|
delete node.color; delete node.bgcolor; node.has_errors = false;
|
|
if (w.callback) w.callback(w.value, app.canvas, node, app.canvas.graph_mouse, null);
|
|
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
|
this.diagnoseNode(node);
|
|
modal.remove();
|
|
};
|
|
}
|
|
}).catch(e => console.error('Gallery replacer failed', e));
|
|
}
|
|
|
|
runGlobalDoctorScan() {
|
|
const content = document.getElementById('anomalous-doctor-node-list');
|
|
const inst = document.getElementById('anomalous-doctor-instructions');
|
|
if (inst) inst.style.display = 'none';
|
|
if (content) content.innerHTML = '';
|
|
|
|
let totalNodes = 0;
|
|
let missingNodes = 0;
|
|
|
|
for (const node of app.graph._nodes) {
|
|
if (node.widgets) {
|
|
for (let w of node.widgets) {
|
|
const val = w.value;
|
|
if (typeof val === 'string' && (val.endsWith('.safetensors') || val.endsWith('.ckpt') || val.endsWith('.pt') || val.endsWith('.sft') || val.endsWith('.bin'))) {
|
|
totalNodes++;
|
|
let isHealthy = false;
|
|
if (w.options && w.options.values && w.options.values.includes(val)) isHealthy = true;
|
|
if (!isHealthy) {
|
|
missingNodes++;
|
|
const nodeTitle = document.createElement('div');
|
|
nodeTitle.innerText = `${window.anomalous_browser_lang === 'zh' ? '鑺傜偣' : 'Node'}: ${node.title || node.type}`;
|
|
nodeTitle.style.color = '#8AB4F8';
|
|
nodeTitle.style.fontWeight = 'bold';
|
|
nodeTitle.style.marginTop = '10px';
|
|
content.appendChild(nodeTitle);
|
|
content.appendChild(this.renderDoctorState(node, w));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (missingNodes === 0) {
|
|
content.innerHTML = `<div style="color:#28a745; text-align:center; padding:20px; font-size:16px;">${window.anomalous_browser_lang === 'zh' ? '馃帀 澶浜嗭紒褰撳墠宸ヤ綔娴佷腑鎵€鏈夋ā鍨嬪潎鍦ㄦ湰鍦板氨缁€? : '馃帀 Awesome! All models in this workflow are healthy.'}</div>`;
|
|
}
|
|
}
|
|
|
|
showPreflightImportModal() {
|
|
let overlay = document.getElementById('anomalous-import-overlay');
|
|
if (!overlay) {
|
|
overlay = document.createElement('div');
|
|
overlay.id = 'anomalous-import-overlay';
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.backgroundColor = 'rgba(0,0,0,0.85)';
|
|
overlay.style.zIndex = '9999999';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.alignItems = 'center';
|
|
|
|
const modal = document.createElement('div');
|
|
modal.style.width = '600px';
|
|
modal.style.maxWidth = '90vw';
|
|
modal.style.background = '#222';
|
|
modal.style.borderRadius = '12px';
|
|
modal.style.padding = '20px';
|
|
modal.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
modal.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)';
|
|
modal.style.display = 'flex';
|
|
modal.style.flexDirection = 'column';
|
|
modal.style.gap = '15px';
|
|
|
|
const header = document.createElement('h2');
|
|
header.style.margin = '0';
|
|
header.style.color = '#fff';
|
|
header.innerText = window.anomalous_browser_lang === 'zh' ? '馃摜 瀵煎叆宸ヤ綔娴?(棰勬娴峰叧)' : '馃摜 Pre-flight Import Workflow';
|
|
modal.appendChild(header);
|
|
|
|
const desc = document.createElement('div');
|
|
desc.style.color = '#aaa';
|
|
desc.style.fontSize = '14px';
|
|
desc.innerText = window.anomalous_browser_lang === 'zh' ? '璇峰湪姝ゅ绮樿创宸ヤ綔娴佺殑 JSON 浠g爜锛堟湭鏉ュ皢鏀寔鍒嗕韩鐮侊級銆傛垜浠皢涓烘偍鎻愬彇骞舵牎楠屾墍鏈夋ā鍨嬩緷璧栥€? : 'Paste the workflow JSON here. We will extract and validate all model dependencies before loading.';
|
|
modal.appendChild(desc);
|
|
|
|
const textarea = document.createElement('textarea');
|
|
textarea.id = 'anomalous-import-textarea';
|
|
textarea.style.width = '100%';
|
|
textarea.style.height = '150px';
|
|
textarea.style.background = '#111';
|
|
textarea.style.color = '#fff';
|
|
textarea.style.border = '1px solid #444';
|
|
textarea.style.borderRadius = '6px';
|
|
textarea.style.padding = '10px';
|
|
textarea.style.fontFamily = 'monospace';
|
|
textarea.style.resize = 'vertical';
|
|
textarea.style.boxSizing = 'border-box';
|
|
textarea.placeholder = '{"last_node_id": ...}';
|
|
modal.appendChild(textarea);
|
|
|
|
const resultArea = document.createElement('div');
|
|
resultArea.id = 'anomalous-import-result';
|
|
resultArea.style.maxHeight = '300px';
|
|
resultArea.style.overflowY = 'auto';
|
|
resultArea.style.display = 'none';
|
|
resultArea.style.flexDirection = 'column';
|
|
resultArea.style.gap = '10px';
|
|
resultArea.style.background = 'rgba(0,0,0,0.2)';
|
|
resultArea.style.padding = '15px';
|
|
resultArea.style.borderRadius = '8px';
|
|
modal.appendChild(resultArea);
|
|
|
|
const btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.justifyContent = 'flex-end';
|
|
btnRow.style.gap = '10px';
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerText = window.anomalous_browser_lang === 'zh' ? '鍙栨秷' : 'Cancel';
|
|
closeBtn.style.padding = '8px 16px';
|
|
closeBtn.style.background = 'transparent';
|
|
closeBtn.style.color = '#ccc';
|
|
closeBtn.style.border = '1px solid #555';
|
|
closeBtn.style.borderRadius = '6px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.onclick = () => { overlay.style.display = 'none'; };
|
|
|
|
const analyzeBtn = document.createElement('button');
|
|
analyzeBtn.id = 'anomalous-import-analyze-btn';
|
|
analyzeBtn.innerText = window.anomalous_browser_lang === 'zh' ? '馃攳 瑙f瀽骞堕妫€' : '馃攳 Analyze & Pre-flight';
|
|
analyzeBtn.style.padding = '8px 16px';
|
|
analyzeBtn.style.background = '#1a73e8';
|
|
analyzeBtn.style.color = '#fff';
|
|
analyzeBtn.style.border = 'none';
|
|
analyzeBtn.style.borderRadius = '6px';
|
|
analyzeBtn.style.cursor = 'pointer';
|
|
|
|
const loadBtn = document.createElement('button');
|
|
loadBtn.id = 'anomalous-import-load-btn';
|
|
loadBtn.innerText = window.anomalous_browser_lang === 'zh' ? '馃殌 寮哄埗杞藉叆鐢诲竷' : '馃殌 Force Load to Canvas';
|
|
loadBtn.style.padding = '8px 16px';
|
|
loadBtn.style.background = '#28a745';
|
|
loadBtn.style.color = '#fff';
|
|
loadBtn.style.border = 'none';
|
|
loadBtn.style.borderRadius = '6px';
|
|
loadBtn.style.cursor = 'pointer';
|
|
loadBtn.style.display = 'none';
|
|
|
|
let parsedGraphData = null;
|
|
|
|
analyzeBtn.onclick = async () => {
|
|
try {
|
|
const jsonStr = textarea.value.trim();
|
|
if (!jsonStr) return;
|
|
parsedGraphData = JSON.parse(jsonStr);
|
|
|
|
try {
|
|
const res = await fetch('/anomalous/all_hashes');
|
|
const hData = await res.json();
|
|
const hObj = hData.hashes ? hData.hashes : hData;
|
|
Object.assign(window.anomalous_hash_cache, hObj);
|
|
} catch (e) { }
|
|
|
|
const models = [];
|
|
if (parsedGraphData.nodes) {
|
|
for (const node of parsedGraphData.nodes) {
|
|
if (node.widgets_values) {
|
|
for (const v of node.widgets_values) {
|
|
if (typeof v === 'string' && (v.endsWith('.safetensors') || v.endsWith('.ckpt') || v.endsWith('.pt') || v.endsWith('.sft') || v.endsWith('.bin'))) {
|
|
models.push({ nodeType: node.type, value: v });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
textarea.style.display = 'none';
|
|
resultArea.style.display = 'flex';
|
|
analyzeBtn.style.display = 'none';
|
|
loadBtn.style.display = 'block';
|
|
|
|
if (models.length === 0) {
|
|
resultArea.innerHTML = `<div style="color:#28a745;">${window.anomalous_browser_lang === 'zh' ? '鉁?鏈娴嬪埌浠讳綍妯″瀷渚濊禆銆? : '鉁?No model dependencies detected.'}</div>`;
|
|
return;
|
|
}
|
|
|
|
resultArea.innerHTML = `<div style="color:#fff; font-weight:bold; margin-bottom:10px;">${window.anomalous_browser_lang === 'zh' ? `鎵弿鍒?${models.length} 涓ā鍨嬭祫婧愶細` : `Detected ${models.length} model resources:`}</div>`;
|
|
|
|
for (const m of models) {
|
|
const val = m.value;
|
|
const parts = val.split(/[\\/]/);
|
|
const basename = parts[parts.length - 1];
|
|
|
|
let isHealthy = false;
|
|
let cacheHit = window.anomalous_hash_cache[basename] || window.anomalous_hash_cache[val];
|
|
|
|
const fetchRes = await fetch(`/anomalous/resolve_hash?hash=unknown&size=&filename=${encodeURIComponent(val)}`);
|
|
const fetchData = await fetchRes.json();
|
|
if (fetchData.found) {
|
|
isHealthy = true;
|
|
} else if (cacheHit) {
|
|
const resolveRes = await fetch(`/anomalous/resolve_hash?hash=${encodeURIComponent(cacheHit.hash || cacheHit)}&size=`);
|
|
const resolveData = await resolveRes.json();
|
|
if (resolveData.found) isHealthy = true;
|
|
}
|
|
|
|
const item = document.createElement('div');
|
|
item.style.display = 'flex';
|
|
item.style.alignItems = 'center';
|
|
item.style.justifyContent = 'space-between';
|
|
item.style.padding = '8px';
|
|
item.style.background = 'rgba(255,255,255,0.05)';
|
|
item.style.borderRadius = '4px';
|
|
|
|
const left = document.createElement('div');
|
|
left.style.display = 'flex';
|
|
left.style.alignItems = 'center';
|
|
left.style.gap = '8px';
|
|
|
|
const icon = document.createElement('span');
|
|
icon.innerText = isHealthy ? '鉁? : '鈿狅笍';
|
|
|
|
const text = document.createElement('span');
|
|
text.innerText = `[${m.nodeType}] ${basename}`;
|
|
text.style.color = isHealthy ? '#ccc' : '#ff6b6b';
|
|
text.style.fontSize = '14px';
|
|
|
|
left.appendChild(icon);
|
|
left.appendChild(text);
|
|
item.appendChild(left);
|
|
|
|
if (!isHealthy) {
|
|
const right = document.createElement('a');
|
|
right.href = `https://civitai.com/search/models?sortBy=models_v9&query=${encodeURIComponent(basename.replace('.safetensors', '').replace('.ckpt', ''))}`;
|
|
right.target = '_blank';
|
|
right.innerText = window.anomalous_browser_lang === 'zh' ? '鍘?C绔欎笅杞? : 'Download';
|
|
right.style.color = '#1a73e8';
|
|
right.style.fontSize = '12px';
|
|
right.style.textDecoration = 'none';
|
|
item.appendChild(right);
|
|
} else {
|
|
const right = document.createElement('span');
|
|
right.innerText = window.anomalous_browser_lang === 'zh' ? '宸插氨缁? : 'Ready';
|
|
right.style.color = '#28a745';
|
|
right.style.fontSize = '12px';
|
|
item.appendChild(right);
|
|
}
|
|
|
|
resultArea.appendChild(item);
|
|
}
|
|
|
|
} catch (e) {
|
|
alert('Invalid JSON! ' + e.message);
|
|
}
|
|
};
|
|
|
|
loadBtn.onclick = () => {
|
|
if (parsedGraphData && app) {
|
|
app.loadGraphData(parsedGraphData);
|
|
overlay.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
btnRow.appendChild(closeBtn);
|
|
btnRow.appendChild(analyzeBtn);
|
|
btnRow.appendChild(loadBtn);
|
|
modal.appendChild(btnRow);
|
|
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
const textarea = document.getElementById('anomalous-import-textarea');
|
|
const resultArea = document.getElementById('anomalous-import-result');
|
|
const analyzeBtn = document.getElementById('anomalous-import-analyze-btn');
|
|
const loadBtn = document.getElementById('anomalous-import-load-btn');
|
|
|
|
if (textarea) {
|
|
textarea.value = '';
|
|
textarea.style.display = 'block';
|
|
}
|
|
if (resultArea) {
|
|
resultArea.style.display = 'none';
|
|
resultArea.innerHTML = '';
|
|
}
|
|
if (analyzeBtn) analyzeBtn.style.display = 'block';
|
|
if (loadBtn) loadBtn.style.display = 'none';
|
|
|
|
overlay.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: 'Anomalous.ModelBrowser',
|
|
async setup() {
|
|
const cssUrl = '/extensions/Anomalous_Model_Browser/styles.css?v=' + Date.now();
|
|
if (!document.querySelector(`link[href^="/extensions/Anomalous_Model_Browser/styles.css"]`)) {
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.type = "text/css";
|
|
link.href = cssUrl;
|
|
document.head.appendChild(link);
|
|
}
|
|
if (!localStorage.getItem('anomalous_lang')) {
|
|
try {
|
|
if (app && app.ui && app.ui.settings) {
|
|
const locale = app.ui.settings.getSettingValue('Comfy.Locale') || app.ui.settings.getSettingValue('Comfy.Locale.Language');
|
|
if (locale) {
|
|
currentLang = locale.toLowerCase().includes('en') ? 'en' : 'zh';
|
|
window.anomalous_browser_lang = currentLang;
|
|
}
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
const browser = new AnomalousBrowser();
|
|
window.anomalousBrowserInstance = browser;
|
|
const btn = document.createElement('button');
|
|
btn.id = 'anomalous-trigger-btn';
|
|
btn.innerHTML = '馃摝';
|
|
btn.title = window.anomalous_browser_lang === 'zh' ? '鎵撳紑 Anomalous 妯″瀷娴忚鍣? : 'Open Anomalous Model Browser';
|
|
|
|
let isDragging = false;
|
|
let startX, startY, initialX, initialY;
|
|
|
|
btn.addEventListener('mousedown', (e) => {
|
|
isDragging = true;
|
|
startX = e.clientX; startY = e.clientY;
|
|
const rect = btn.getBoundingClientRect();
|
|
initialX = rect.left; initialY = rect.top;
|
|
btn.style.transition = 'none';
|
|
});
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
e.preventDefault();
|
|
let newX = initialX + (e.clientX - startX);
|
|
let newY = initialY + (e.clientY - startY);
|
|
if (newX < 0) newX = 0;
|
|
if (newY < 0) newY = 0;
|
|
if (newX > window.innerWidth - 60) newX = window.innerWidth - 60;
|
|
if (newY > window.innerHeight - 60) newY = window.innerHeight - 60;
|
|
btn.style.left = newX + 'px';
|
|
btn.style.top = newY + 'px';
|
|
btn.style.right = 'auto'; btn.style.bottom = 'auto';
|
|
});
|
|
window.addEventListener('mouseup', (e) => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
btn.style.transition = 'transform 0.15s, box-shadow 0.15s';
|
|
localStorage.setItem('anomalous_btn_x', btn.style.left);
|
|
localStorage.setItem('anomalous_btn_y', btn.style.top);
|
|
if (Math.abs(e.clientX - startX) < 5 && Math.abs(e.clientY - startY) < 5) browser.show();
|
|
}
|
|
});
|
|
|
|
const savedX = localStorage.getItem('anomalous_btn_x');
|
|
const savedY = localStorage.getItem('anomalous_btn_y');
|
|
|
|
const updateBtnBounds = () => {
|
|
let numX = parseInt(btn.style.left || savedX);
|
|
let numY = parseInt(btn.style.top || savedY);
|
|
if (isNaN(numX)) numX = window.innerWidth - 90;
|
|
if (isNaN(numY)) numY = window.innerHeight - 90;
|
|
|
|
if (numX < 0) numX = 0;
|
|
if (numY < 0) numY = 0;
|
|
if (numX > window.innerWidth - 60) numX = window.innerWidth - 60;
|
|
if (numY > window.innerHeight - 60) numY = window.innerHeight - 60;
|
|
btn.style.left = numX + 'px';
|
|
btn.style.top = numY + 'px';
|
|
};
|
|
|
|
if (savedX && savedY && savedX !== 'NaN' && savedY !== 'NaN') {
|
|
btn.style.right = 'auto';
|
|
btn.style.bottom = 'auto';
|
|
btn.style.left = savedX;
|
|
btn.style.top = savedY;
|
|
}
|
|
|
|
// Always trigger an update slightly after load to ensure it's in bounds
|
|
setTimeout(updateBtnBounds, 200);
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (btn.style.left) updateBtnBounds();
|
|
});
|
|
|
|
document.body.appendChild(btn);
|
|
|
|
// Pre-create a lightweight, translucent, and aesthetic drag ghost image for huge Hires Fix images
|
|
window.anomalousDragGhostImg = new Image();
|
|
window.anomalousDragGhostImg.src = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='76' height='76' x='2' y='2' fill='%231a1a1a' fill-opacity='0.6' rx='16' stroke='%2300ffcc' stroke-width='2'/><text x='40' y='50' font-family='sans-serif' font-size='32' font-weight='bold' fill='%2300ffcc' text-anchor='middle'>W</text></svg>";
|
|
}
|
|
});
|
|
|
|
|
|
// --- INJECTED WORKFLOW SHARE MODULE ---
|
|
// Workflow Share and Preview Modal for Anomalous_Model_Browser
|
|
|
|
const AMB_WorkflowShare = {
|
|
// ----------------------------------------------------------------------
|
|
// 1. Data Compression & Base64 Utils
|
|
// ----------------------------------------------------------------------
|
|
strToU8(str) {
|
|
return new TextEncoder().encode(str);
|
|
},
|
|
u8ToStr(u8) {
|
|
return new TextDecoder().decode(u8);
|
|
},
|
|
u8ToBase64(u8) {
|
|
let binary = '';
|
|
const len = u8.byteLength;
|
|
for (let i = 0; i < len; i++) {
|
|
binary += String.fromCharCode(u8[i]);
|
|
}
|
|
return window.btoa(binary);
|
|
},
|
|
base64ToU8(b64) {
|
|
const binary = window.atob(b64);
|
|
const len = binary.length;
|
|
const u8 = new Uint8Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
u8[i] = binary.charCodeAt(i);
|
|
}
|
|
return u8;
|
|
},
|
|
async compress(str) {
|
|
const stream = new Blob([this.strToU8(str)]).stream();
|
|
const compressedStream = stream.pipeThrough(new CompressionStream('deflate-raw'));
|
|
const response = new Response(compressedStream);
|
|
const blob = await response.blob();
|
|
const buffer = await blob.arrayBuffer();
|
|
return new Uint8Array(buffer);
|
|
},
|
|
async decompress(u8) {
|
|
const stream = new Blob([u8]).stream();
|
|
const decompressedStream = stream.pipeThrough(new DecompressionStream('deflate-raw'));
|
|
const response = new Response(decompressedStream);
|
|
const blob = await response.blob();
|
|
const buffer = await blob.arrayBuffer();
|
|
return this.u8ToStr(new Uint8Array(buffer));
|
|
},
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 2. Skeleton Generation (Strip visual / default data)
|
|
// ----------------------------------------------------------------------
|
|
skeletonize(workflowJson) {
|
|
const wf = JSON.parse(JSON.stringify(workflowJson)); // deep copy
|
|
|
|
// ComfyUI workflow JSON format has "nodes" array
|
|
if (wf.nodes && Array.isArray(wf.nodes)) {
|
|
wf.nodes.forEach(node => {
|
|
// Delete layout coords and styles
|
|
delete node.pos;
|
|
delete node.size;
|
|
delete node.color;
|
|
delete node.bgcolor;
|
|
delete node.shape;
|
|
delete node.flags;
|
|
// Delete empty properties
|
|
if (node.properties && Object.keys(node.properties).length === 0) {
|
|
delete node.properties;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove view metadata
|
|
if (wf.extra) {
|
|
delete wf.extra.ds; // scale/offset
|
|
}
|
|
|
|
return wf;
|
|
},
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 3. Auto-Layout Algorithm
|
|
// ----------------------------------------------------------------------
|
|
autoLayout(workflowJson) {
|
|
if (!workflowJson.nodes || !Array.isArray(workflowJson.nodes)) return workflowJson;
|
|
|
|
const nodes = workflowJson.nodes;
|
|
|
|
// 1. Build adjacency list and in-degrees
|
|
const adj = new Map();
|
|
const inDegree = new Map();
|
|
|
|
nodes.forEach(n => {
|
|
adj.set(n.id, []);
|
|
if (!inDegree.has(n.id)) inDegree.set(n.id, 0);
|
|
});
|
|
|
|
// Check links
|
|
if (workflowJson.links) {
|
|
workflowJson.links.forEach(link => {
|
|
if (!link) return;
|
|
const fromId = link[1];
|
|
const toId = link[3];
|
|
if (adj.has(fromId) && adj.has(toId)) {
|
|
adj.get(fromId).push(toId);
|
|
inDegree.set(toId, inDegree.get(toId) + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2. Topological sort with depth levels
|
|
const depthMap = new Map(); // id -> depth
|
|
const queue = [];
|
|
|
|
nodes.forEach(n => {
|
|
if (inDegree.get(n.id) === 0) {
|
|
queue.push(n.id);
|
|
depthMap.set(n.id, 0);
|
|
}
|
|
});
|
|
|
|
while (queue.length > 0) {
|
|
const curr = queue.shift();
|
|
const currDepth = depthMap.get(curr);
|
|
|
|
const neighbors = adj.get(curr);
|
|
if (neighbors) {
|
|
neighbors.forEach(nxt => {
|
|
// Reduce in-degree
|
|
const ind = inDegree.get(nxt) - 1;
|
|
inDegree.set(nxt, ind);
|
|
|
|
// Update depth to be max(existing depth, currDepth + 1)
|
|
const existingDepth = depthMap.get(nxt) || 0;
|
|
depthMap.set(nxt, Math.max(existingDepth, currDepth + 1));
|
|
|
|
if (ind === 0) {
|
|
queue.push(nxt);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle cycles (nodes not reached)
|
|
nodes.forEach(n => {
|
|
if (!depthMap.has(n.id)) {
|
|
depthMap.set(n.id, 0);
|
|
}
|
|
});
|
|
|
|
// 3. Assign X, Y coordinates
|
|
const nodesByDepth = {};
|
|
nodes.forEach(n => {
|
|
const d = depthMap.get(n.id);
|
|
if (!nodesByDepth[d]) nodesByDepth[d] = [];
|
|
nodesByDepth[d].push(n);
|
|
});
|
|
|
|
// Spacing constants
|
|
const X_SPACING = 400;
|
|
const Y_SPACING = 300;
|
|
|
|
Object.keys(nodesByDepth).forEach(d => {
|
|
const levelNodes = nodesByDepth[d];
|
|
const depth = parseInt(d);
|
|
levelNodes.forEach((n, idx) => {
|
|
// Approximate size
|
|
n.pos = [
|
|
depth * X_SPACING,
|
|
idx * Y_SPACING
|
|
];
|
|
});
|
|
});
|
|
|
|
return workflowJson;
|
|
},
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 4. Encode / Decode
|
|
// ----------------------------------------------------------------------
|
|
async encodeShareCode(workflowJson, isSkeleton) {
|
|
let targetJson = workflowJson;
|
|
if (isSkeleton) {
|
|
targetJson = this.skeletonize(workflowJson);
|
|
}
|
|
|
|
const jsonStr = JSON.stringify(targetJson);
|
|
const compressedU8 = await this.compress(jsonStr);
|
|
const base64Str = this.u8ToBase64(compressedU8);
|
|
|
|
const prefix = isSkeleton ? 'AMB1-' : 'AMB0-';
|
|
return prefix + base64Str;
|
|
},
|
|
|
|
async decodeShareCode(shareCode) {
|
|
if (!shareCode.startsWith('AMB0-') && !shareCode.startsWith('AMB1-')) {
|
|
throw new Error('Invalid Share Code Format.');
|
|
}
|
|
|
|
const isSkeleton = shareCode.startsWith('AMB1-');
|
|
const base64Str = shareCode.substring(5);
|
|
|
|
const compressedU8 = this.base64ToU8(base64Str);
|
|
const jsonStr = await this.decompress(compressedU8);
|
|
|
|
let workflowJson = JSON.parse(jsonStr);
|
|
|
|
if (isSkeleton) {
|
|
workflowJson = this.autoLayout(workflowJson);
|
|
}
|
|
|
|
return workflowJson;
|
|
},
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 5. UI Modals
|
|
// ----------------------------------------------------------------------
|
|
showExportModal() {
|
|
const lang = window.anomalous_browser_lang || 'en';
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'amb-export-modal';
|
|
overlay.style.cssText = `
|
|
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: rgba(0,0,0,0.6); backdrop-filter: blur(5px);
|
|
z-index: 999999; display: flex; justify-content: center; align-items: center;
|
|
font-family: Arial, sans-serif;
|
|
`;
|
|
|
|
const content = document.createElement('div');
|
|
content.style.cssText = `
|
|
background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px;
|
|
width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
display: flex; flex-direction: column; gap: 20px;
|
|
`;
|
|
|
|
const title = document.createElement('h2');
|
|
title.style.margin = '0';
|
|
title.textContent = lang === 'zh' ? '馃摝 瀵煎嚭宸ヤ綔娴佸垎浜爜' : '馃摝 Export Workflow Share Code';
|
|
|
|
const typeSelectContainer = document.createElement('div');
|
|
typeSelectContainer.innerHTML = `
|
|
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
|
|
<input type="radio" name="amb-share-type" value="skeleton" checked />
|
|
${lang === 'zh' ? '绮剧畝鐗?(鎺ㄨ崘 B绔欒瘎璁哄尯, 浣撶Н灏? 涓㈠け鍧愭爣)' : 'Skeleton (Compact, best for comments, loses coords)'}
|
|
</label>
|
|
<label style="display: block; cursor: pointer;">
|
|
<input type="radio" name="amb-share-type" value="full" />
|
|
${lang === 'zh' ? '瀹屾暣鐗?(淇濈暀鎵€鏈夊潗鏍囦笌棰滆壊, 杈冮暱)' : 'Full (Keeps coords and colors, longer string)'}
|
|
</label>
|
|
`;
|
|
|
|
const textArea = document.createElement('textarea');
|
|
textArea.style.cssText = `
|
|
width: 100%; height: 150px; background: #1e1e1f; color: #eee;
|
|
border: 1px solid #444; border-radius: 6px; padding: 10px;
|
|
font-family: monospace; font-size: 12px; resize: none; box-sizing: border-box;
|
|
`;
|
|
textArea.readOnly = true;
|
|
|
|
const btnGroup = document.createElement('div');
|
|
btnGroup.style.cssText = `display: flex; gap: 10px; justify-content: flex-end;`;
|
|
|
|
const generateBtn = document.createElement('button');
|
|
generateBtn.textContent = lang === 'zh' ? '鐢熸垚鍒嗕韩鐮? : 'Generate';
|
|
generateBtn.style.cssText = `padding: 8px 16px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer;`;
|
|
|
|
const copyBtn = document.createElement('button');
|
|
copyBtn.textContent = lang === 'zh' ? '澶嶅埗鍒板壀璐存澘' : 'Copy';
|
|
copyBtn.style.cssText = `padding: 8px 16px; background: #5cb85c; color: #fff; border: none; border-radius: 6px; cursor: pointer; display: none;`;
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = lang === 'zh' ? '鍏抽棴' : 'Close';
|
|
closeBtn.style.cssText = `padding: 8px 16px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer;`;
|
|
|
|
closeBtn.onclick = () => overlay.remove();
|
|
|
|
generateBtn.onclick = async () => {
|
|
const isSkeleton = document.querySelector('input[name="amb-share-type"]:checked').value === 'skeleton';
|
|
|
|
// Get current workflow from app graph
|
|
const p = await app.graphToPrompt();
|
|
const workflowJson = p.workflow;
|
|
|
|
try {
|
|
const code = await AMB_WorkflowShare.encodeShareCode(workflowJson, isSkeleton);
|
|
textArea.value = code;
|
|
copyBtn.style.display = 'block';
|
|
} catch (err) {
|
|
textArea.value = 'Error generating code: ' + err.message;
|
|
}
|
|
};
|
|
|
|
copyBtn.onclick = () => {
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
alert(lang === 'zh' ? '澶嶅埗鎴愬姛锛? : 'Copied successfully!');
|
|
};
|
|
|
|
btnGroup.appendChild(generateBtn);
|
|
btnGroup.appendChild(copyBtn);
|
|
btnGroup.appendChild(closeBtn);
|
|
|
|
content.appendChild(title);
|
|
content.appendChild(typeSelectContainer);
|
|
content.appendChild(textArea);
|
|
content.appendChild(btnGroup);
|
|
overlay.appendChild(content);
|
|
|
|
document.body.appendChild(overlay);
|
|
},
|
|
|
|
showImportModal() {
|
|
const lang = window.anomalous_browser_lang || 'en';
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'amb-import-modal';
|
|
overlay.style.cssText = `
|
|
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: rgba(0,0,0,0.6); backdrop-filter: blur(5px);
|
|
z-index: 999999; display: flex; justify-content: center; align-items: center;
|
|
font-family: Arial, sans-serif;
|
|
`;
|
|
|
|
const content = document.createElement('div');
|
|
content.style.cssText = `
|
|
background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px;
|
|
width: 600px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
display: flex; flex-direction: column; gap: 20px;
|
|
`;
|
|
|
|
const title = document.createElement('h2');
|
|
title.style.margin = '0';
|
|
title.textContent = lang === 'zh' ? '馃摜 瀵煎叆宸ヤ綔娴佸垎浜爜' : '馃摜 Import Workflow Share Code';
|
|
|
|
const inputArea = document.createElement('textarea');
|
|
inputArea.placeholder = lang === 'zh' ? '绮樿创 AMB0- 鎴?AMB1- 寮€澶寸殑鍒嗕韩鐮?..' : 'Paste AMB0- or AMB1- share code here...';
|
|
inputArea.style.cssText = `
|
|
width: 100%; height: 100px; background: #1e1e1f; color: #eee;
|
|
border: 1px solid #444; border-radius: 6px; padding: 10px;
|
|
font-family: monospace; font-size: 12px; resize: none; box-sizing: border-box;
|
|
`;
|
|
|
|
const previewArea = document.createElement('div');
|
|
previewArea.style.cssText = `
|
|
min-height: 80px; max-height: 300px; overflow-y: auto;
|
|
background: #1e1e1f; border-radius: 6px; padding: 10px; display: none;
|
|
font-size: 14px; color: #ccc;
|
|
`;
|
|
|
|
const btnGroup = document.createElement('div');
|
|
btnGroup.style.cssText = `display: flex; gap: 10px; justify-content: flex-end;`;
|
|
|
|
const previewBtn = document.createElement('button');
|
|
previewBtn.textContent = lang === 'zh' ? '棰勮鍒嗘瀽' : 'Preview';
|
|
previewBtn.style.cssText = `padding: 8px 16px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer;`;
|
|
|
|
const loadBtn = document.createElement('button');
|
|
loadBtn.textContent = lang === 'zh' ? '鍔犺浇宸ヤ綔娴? : 'Load Workflow';
|
|
loadBtn.style.cssText = `padding: 8px 16px; background: #e07a5f; color: #fff; border: none; border-radius: 6px; cursor: pointer; display: none;`;
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = lang === 'zh' ? '鍙栨秷' : 'Cancel';
|
|
closeBtn.style.cssText = `padding: 8px 16px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer;`;
|
|
|
|
closeBtn.onclick = () => overlay.remove();
|
|
|
|
let pendingWorkflow = null;
|
|
|
|
previewBtn.onclick = async () => {
|
|
const code = inputArea.value.trim();
|
|
if (!code) return;
|
|
try {
|
|
pendingWorkflow = await AMB_WorkflowShare.decodeShareCode(code);
|
|
|
|
// Analyze
|
|
const nodesCount = pendingWorkflow.nodes ? pendingWorkflow.nodes.length : 0;
|
|
|
|
// Get required extensions / node types
|
|
const requiredNodes = new Set();
|
|
if (pendingWorkflow.nodes) {
|
|
pendingWorkflow.nodes.forEach(n => requiredNodes.add(n.type));
|
|
}
|
|
|
|
// Check if they exist in LiteGraph
|
|
const missingNodes = [];
|
|
requiredNodes.forEach(t => {
|
|
if (window.LiteGraph && !window.LiteGraph.registered_node_types[t]) {
|
|
missingNodes.push(t);
|
|
}
|
|
});
|
|
|
|
let html = `<strong>${lang === 'zh' ? '宸ヤ綔娴佹瑙? : 'Workflow Overview'}</strong><br>`;
|
|
html += `${lang === 'zh' ? '鎬昏妭鐐规暟' : 'Total Nodes'}: ${nodesCount}<br><br>`;
|
|
|
|
if (missingNodes.length > 0) {
|
|
html += `<span style="color: #ff6b6b;">鈿狅笍 ${lang === 'zh' ? '缂哄け浠ヤ笅鑺傜偣 (瀵煎叆鍚庡皢鏄剧ず绾㈡)' : 'Missing Node Types (will be red on canvas)'}:</span><br>`;
|
|
html += `<ul style="margin-top:5px; color:#ff9f43; font-size:12px;">`;
|
|
missingNodes.forEach(m => {
|
|
html += `<li>${m}</li>`;
|
|
});
|
|
html += `</ul>`;
|
|
} else {
|
|
html += `<span style="color: #5cb85c;">鉁?${lang === 'zh' ? '鎵€鏈夋墍闇€鑺傜偣宸插湪褰撳墠鐜瀹夎锛? : 'All required nodes are installed!'}</span>`;
|
|
}
|
|
|
|
previewArea.innerHTML = html;
|
|
previewArea.style.display = 'block';
|
|
loadBtn.style.display = 'block';
|
|
|
|
} catch (err) {
|
|
previewArea.style.display = 'block';
|
|
previewArea.innerHTML = `<span style="color:#ff6b6b;">${lang === 'zh' ? '瑙f瀽澶辫触' : 'Decode Failed'}: ${err.message}</span>`;
|
|
}
|
|
};
|
|
|
|
loadBtn.onclick = () => {
|
|
if (pendingWorkflow) {
|
|
app.loadGraphData(pendingWorkflow);
|
|
overlay.remove();
|
|
}
|
|
};
|
|
|
|
btnGroup.appendChild(previewBtn);
|
|
btnGroup.appendChild(loadBtn);
|
|
btnGroup.appendChild(closeBtn);
|
|
|
|
content.appendChild(title);
|
|
content.appendChild(inputArea);
|
|
content.appendChild(previewArea);
|
|
content.appendChild(btnGroup);
|
|
overlay.appendChild(content);
|
|
|
|
document.body.appendChild(overlay);
|
|
} showUnifiedModal() {
|
|
const lang = window.anomalous_browser_lang || 'en';
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: rgba(0,0,0,0.6); backdrop-filter: blur(5px);
|
|
z-index: 999999; display: flex; justify-content: center; align-items: center;
|
|
font-family: Arial, sans-serif;
|
|
`;
|
|
|
|
const content = document.createElement('div');
|
|
content.style.cssText = `
|
|
background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px;
|
|
width: 400px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
display: flex; flex-direction: column; gap: 20px; text-align: center;
|
|
`;
|
|
|
|
const title = document.createElement('h2');
|
|
title.style.margin = '0';
|
|
title.textContent = lang === 'zh' ? '馃攧 宸ヤ綔娴佸垎浜笌瀵煎叆' : '馃攧 Workflow Share & Import';
|
|
|
|
const exportBtn = document.createElement('button');
|
|
exportBtn.textContent = lang === 'zh' ? '馃摛 瀵煎嚭褰撳墠宸ヤ綔娴佷负鍒嗕韩鐮? : '馃摛 Export Workflow to Share Code';
|
|
exportBtn.style.cssText = `padding: 12px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;`;
|
|
exportBtn.onclick = () => { overlay.remove(); this.showExportModal(); };
|
|
|
|
const importBtn = document.createElement('button');
|
|
importBtn.textContent = lang === 'zh' ? '馃摜 浠庡垎浜爜瀵煎叆宸ヤ綔娴? : '馃摜 Import Workflow from Share Code';
|
|
importBtn.style.cssText = `padding: 12px; background: #e07a5f; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;`;
|
|
importBtn.onclick = () => { overlay.remove(); this.showImportModal(); };
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = lang === 'zh' ? '鍏抽棴' : 'Close';
|
|
closeBtn.style.cssText = `padding: 8px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; margin-top: 10px;`;
|
|
closeBtn.onclick = () => overlay.remove();
|
|
|
|
content.appendChild(title);
|
|
content.appendChild(exportBtn);
|
|
content.appendChild(importBtn);
|
|
content.appendChild(closeBtn);
|
|
overlay.appendChild(content);
|
|
|
|
document.body.appendChild(overlay);
|
|
}
|
|
};
|
|
|
|
window.AMB_WorkflowShare = AMB_WorkflowShare;
|
|
|