import { showDetail, showEditModal, _openAdvancedModelSelector, setWidgetValuePath } from './modules/ui_detail.js'; import { loadModels, applyModelToCanvas, stopMediaInContainer } from './modules/ui_grid.js'; import { createDOM, renderSidebar, loadFolders, showHelp, hideAllPanels, openFolderManager } from './modules/ui_sidebar.js'; import { loadGalleryImages, showGeneratedGallery, showGallerySelectMode, showGalleryViewer } from './modules/ui_gallery.js'; import { showNotebooks, refreshNotebooks, saveCurrentNotebook, deleteCurrentNotebook, renderNotebookEditor, fillNotebookGalleries, sendNotebookToCanvas } from './modules/ui_notebooks.js'; import { initDoctorPanel, diagnoseNode, renderGlobalDashboard, initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, _openGalleryReplacer, openLoraInsertionPicker, runGlobalDoctorScan } from './modules/ui_doctor.js'; 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.cardThumbnailMode = localStorage.getItem('anomalous_card_thumbnail_mode') === 'original' ? 'original' : 'balanced'; this.createDOM(); } // [EXTRACTED] createDOM // [EXTRACTED] showHelp // [EXTRACTED] loadFolders // [EXTRACTED] renderSidebar // [EXTRACTED] loadModels // [EXTRACTED] applyModelToCanvas // [EXTRACTED] stopMediaInContainer // [EXTRACTED] showDetail show() { if (this._idleReleaseTimer) { clearTimeout(this._idleReleaseTimer); this._idleReleaseTimer = null; } this.modal.classList.add('visible'); if (!this.foldersData) { this.loadFolders(); } else { this.loadModels(); } } close() { this.modal.classList.remove('visible'); if (this._modelLoadController) this._modelLoadController.abort(); if (this._modelMediaObserver) this._modelMediaObserver.disconnect(); this.modal.querySelectorAll('video, audio').forEach(media => media.pause()); this.stopMediaInContainer(this.grid); if (this._idleReleaseTimer) clearTimeout(this._idleReleaseTimer); this._idleReleaseTimer = setTimeout(() => { if (this.modal.classList.contains('visible')) return; this.stopMediaInContainer(this.grid); this.grid.replaceChildren(); this.models = []; }, 90000); } // [EXTRACTED] showNotebooks 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; } } // [EXTRACTED] loadGalleryImages // [EXTRACTED] showEditModal // [EXTRACTED] showGeneratedGallery // [EXTRACTED] showGallerySelectMode // [EXTRACTED] showGalleryViewer // [EXTRACTED] refreshNotebooks // [EXTRACTED] saveCurrentNotebook // [EXTRACTED] deleteCurrentNotebook // [EXTRACTED] renderNotebookEditor // [EXTRACTED] fillNotebookGalleries // [EXTRACTED] sendNotebookToCanvas // [EXTRACTED] setWidgetValuePath // [EXTRACTED] hideAllPanels // [Doctor & Assistant Panel methods extracted to ui_doctor.js] 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 代码(未来将支持分享码)。我们将为您提取并校验所有模型依赖。' : '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' ? '🔍 解析并预检' : '🔍 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 = `
${window.anomalous_browser_lang === 'zh' ? '✅ 未检测到任何模型依赖。' : '✅ No model dependencies detected.'}
`; return; } resultArea.innerHTML = `
${window.anomalous_browser_lang === 'zh' ? `扫描到 ${models.length} 个模型资源:` : `Detected ${models.length} model resources:`}
`; 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[val] || window.anomalous_hash_cache[basename]; 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'); const searchHash = cacheHit ? (typeof cacheHit === 'string' ? cacheHit : cacheHit.hash) : null; const searchStr = searchHash || basename.replace('.safetensors', '').replace('.ckpt', '').replace('.pt', '').replace('.sft', ''); right.href = `https://civitai.com/search/models?sortBy=models_v9&query=${encodeURIComponent(searchStr)}`; 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'; } // [EXTRACTED] _openAdvancedModelSelector } // --- Extracted Module Bindings --- AnomalousBrowser.prototype.initDoctorPanel = initDoctorPanel; AnomalousBrowser.prototype.diagnoseNode = diagnoseNode; AnomalousBrowser.prototype.renderGlobalDashboard = renderGlobalDashboard; AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel; AnomalousBrowser.prototype.renderAssistantModelCard = renderAssistantModelCard; AnomalousBrowser.prototype._loadAssistantHistory = _loadAssistantHistory; AnomalousBrowser.prototype._openGalleryReplacer = _openGalleryReplacer; AnomalousBrowser.prototype.openLoraInsertionPicker = openLoraInsertionPicker; AnomalousBrowser.prototype.runGlobalDoctorScan = runGlobalDoctorScan; AnomalousBrowser.prototype.showNotebooks = showNotebooks; AnomalousBrowser.prototype.refreshNotebooks = refreshNotebooks; AnomalousBrowser.prototype.saveCurrentNotebook = saveCurrentNotebook; AnomalousBrowser.prototype.deleteCurrentNotebook = deleteCurrentNotebook; AnomalousBrowser.prototype.renderNotebookEditor = renderNotebookEditor; AnomalousBrowser.prototype.fillNotebookGalleries = fillNotebookGalleries; AnomalousBrowser.prototype.sendNotebookToCanvas = sendNotebookToCanvas; AnomalousBrowser.prototype.loadGalleryImages = loadGalleryImages; AnomalousBrowser.prototype.showGeneratedGallery = showGeneratedGallery; AnomalousBrowser.prototype.showGallerySelectMode = showGallerySelectMode; AnomalousBrowser.prototype.showGalleryViewer = showGalleryViewer; AnomalousBrowser.prototype.createDOM = createDOM; AnomalousBrowser.prototype.openFolderManager = openFolderManager; AnomalousBrowser.prototype.renderSidebar = renderSidebar; AnomalousBrowser.prototype.loadFolders = loadFolders; AnomalousBrowser.prototype.showHelp = showHelp; AnomalousBrowser.prototype.hideAllPanels = hideAllPanels; AnomalousBrowser.prototype.loadModels = loadModels; AnomalousBrowser.prototype.applyModelToCanvas = applyModelToCanvas; AnomalousBrowser.prototype.stopMediaInContainer = stopMediaInContainer; AnomalousBrowser.prototype.showDetail = showDetail; AnomalousBrowser.prototype.showEditModal = showEditModal; AnomalousBrowser.prototype._openAdvancedModelSelector = _openAdvancedModelSelector; AnomalousBrowser.prototype.setWidgetValuePath = setWidgetValuePath; 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,W"; } }); // --- 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 // ---------------------------------------------------------------------- showToast(message, color) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 30px; right: 30px; background: #2a2a2b; color: ${color || '#fff'}; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); font-family: Arial, sans-serif; font-size: 14px; z-index: 9999999; opacity: 0; transition: opacity 0.3s ease; border-left: 4px solid ${color || '#fff'}; `; document.body.appendChild(toast); setTimeout(() => toast.style.opacity = '1', 10); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000); }, 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 = ` `; 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'); AMB_WorkflowShare.showToast(lang === 'zh' ? '✅ 复制成功!' : '✅ Copied successfully!', '#5cb85c'); }; 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 btnGroup = document.createElement('div'); btnGroup.style.cssText = `display: flex; gap: 10px; justify-content: flex-end;`; const loadBtn = document.createElement('button'); loadBtn.textContent = lang === 'zh' ? '导入并加载' : 'Import & Load'; loadBtn.style.cssText = `padding: 8px 16px; background: #e07a5f; color: #fff; border: none; border-radius: 6px; cursor: pointer;`; 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(); loadBtn.onclick = async () => { const code = inputArea.value.trim(); if (!code) { AMB_WorkflowShare.showToast(lang === 'zh' ? '❌ 分享码为空' : '❌ Share code is empty', '#ff6b6b'); return; } try { const pendingWorkflow = await AMB_WorkflowShare.decodeShareCode(code); app.loadGraphData(pendingWorkflow); overlay.remove(); const nodesCount = pendingWorkflow.nodes ? pendingWorkflow.nodes.length : 0; AMB_WorkflowShare.showToast(lang === 'zh' ? `✅ 成功导入 ${nodesCount} 个节点配置` : `✅ Successfully imported ${nodesCount} nodes`, '#5cb85c'); // Auto close the main browser panel const mainCloseBtn = document.getElementById('anomalous-close'); if (mainCloseBtn) mainCloseBtn.click(); } catch (err) { AMB_WorkflowShare.showToast(lang === 'zh' ? `❌ 解析失败: ${err.message}` : `❌ Decode Failed: ${err.message}`, '#ff6b6b'); } }; btnGroup.appendChild(loadBtn); btnGroup.appendChild(closeBtn); content.appendChild(title); content.appendChild(inputArea); 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;