/** * BANGTRIXTOOLKIT - AMD Monitor Floating Widget * Real-time AMD GPU monitoring overlay for ComfyUI * Cross-platform: Windows (ADL) / Linux (ROCm) */ (function() { "use strict"; // ===== CONFIGURATION ===== const CONFIG = { wsUrl: `ws://${window.location.host}/ws/amd_monitor`, updateInterval: 1000, storageKey: 'bangtrix_amd_monitor_config', defaultPosition: { top: '60px', right: '20px' } }; // ===== STATE ===== let ws = null; let reconnectTimer = null; let reconnectAttempts = 0; const MAX_RECONNECT = 5; // Widget state let widget = null; let isMinimized = false; let isVisible = true; let isDragging = false; let dragOffset = { x: 0, y: 0 }; // ===== INITIALIZATION ===== function init() { if (document.getElementById('bangtrix-amd-monitor')) { console.log('AMD Monitor: Already initialized'); return; } console.log('AMD Monitor: Initializing floating widget...'); createWidget(); setupEventListeners(); loadConfig(); connectWebSocket(); } // ===== WIDGET CREATION ===== function createWidget() { widget = document.createElement('div'); widget.id = 'bangtrix-amd-monitor'; widget.className = 'amd-monitor-widget'; widget.innerHTML = `
🔴 AMD Monitor
GPU Load --
VRAM -- / -- GB
Temperature --°C
Fan Speed --%
`; injectStyles(); document.body.appendChild(widget); setupDrag(); return widget; } function injectStyles() { const style = document.createElement('style'); style.id = 'bangtrix-amd-monitor-styles'; style.textContent = ` #bangtrix-amd-monitor { position: fixed; top: 60px; right: 20px; width: 240px; background: rgba(18, 18, 24, 0.92); border: 1px solid rgba(196, 48, 43, 0.3); border-radius: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; color: #e0e0e0; z-index: 9999; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); backdrop-filter: blur(8px); user-select: none; transition: opacity 0.2s, transform 0.2s; } #bangtrix-amd-monitor.hidden { display: none; } #bangtrix-amd-monitor.minimized .amd-body { display: none; } .amd-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: linear-gradient(135deg, rgba(196, 48, 43, 0.2) 0%, rgba(139, 32, 29, 0.1) 100%); border-radius: 10px 10px 0 0; cursor: move; border-bottom: 1px solid rgba(255,255,255,0.05); } .amd-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 12px; color: #fff; } .amd-icon { font-size: 14px; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } .amd-controls { display: flex; gap: 4px; } .amd-btn { background: rgba(255,255,255,0.1); border: none; color: #ccc; width: 22px; height: 22px; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: all 0.15s; } .amd-btn:hover { background: rgba(255,255,255,0.2); color: #fff; } .amd-btn-close:hover { background: rgba(255, 80, 80, 0.3); color: #fff; } .amd-body { padding: 12px 14px; } .amd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; } .amd-stat { display: flex; flex-direction: column; gap: 4px; } .amd-label { color: #888; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; } .amd-value { font-weight: 600; font-size: 13px; color: #00e676; transition: color 0.2s; } .amd-value.warning { color: #ffaa00; } .amd-value.critical { color: #ff4444; } .amd-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; margin-top: 2px; } .amd-fill { height: 100%; background: linear-gradient(90deg, #00e676, #00c853); border-radius: 2px; transition: width 0.3s ease; width: 0%; } .amd-fill.amd-temp { background: linear-gradient(90deg, #00e676, #ffaa00, #ff4444); } .amd-footer { display: flex; justify-content: space-between; margin-top: 14px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); font-size: 10px; color: #666; } .amd-status { transition: color 0.2s; } .amd-status.connected { color: #00e676; } .amd-status.disconnected { color: #ff4444; } .amd-status.warning { color: #ffaa00; } .amd-method { color: #888; font-family: monospace; } .amd-header:active { cursor: grabbing; } @media (max-width: 768px) { #bangtrix-amd-monitor { width: 200px; font-size: 10px; } .amd-grid { grid-template-columns: 1fr; } } `; document.head.appendChild(style); } // ===== DRAG FUNCTIONALITY ===== function setupDrag() { const header = widget.querySelector('.amd-header'); header.addEventListener('mousedown', (e) => { if (e.target.closest('.amd-btn')) return; isDragging = true; const rect = widget.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; widget.style.transition = 'none'; document.body.style.cursor = 'grabbing'; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); }); function onDrag(e) { if (!isDragging) return; const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; const maxX = window.innerWidth - widget.offsetWidth + 20; const maxY = window.innerHeight - widget.offsetHeight + 20; widget.style.left = Math.max(10, Math.min(x, maxX)) + 'px'; widget.style.top = Math.max(10, Math.min(y, maxY)) + 'px'; widget.style.right = 'auto'; } function stopDrag() { isDragging = false; widget.style.transition = ''; document.body.style.cursor = ''; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); saveConfig(); } } // ===== EVENT LISTENERS ===== function setupEventListeners() { widget.querySelector('.amd-btn-minimize').addEventListener('click', () => { isMinimized = !isMinimized; widget.classList.toggle('minimized', isMinimized); widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : '−'; saveConfig(); }); widget.querySelector('.amd-btn-close').addEventListener('click', () => { isVisible = !isVisible; widget.classList.toggle('hidden', !isVisible); saveConfig(); }); widget.querySelector('.amd-btn-settings').addEventListener('click', () => { showToast('Settings coming soon!'); }); } // ===== CONFIGURATION PERSISTENCE ===== function loadConfig() { try { const saved = localStorage.getItem(CONFIG.storageKey); if (saved) { const config = JSON.parse(saved); if (config.position) { if (config.position.left) { widget.style.left = config.position.left; widget.style.right = 'auto'; } if (config.position.top) { widget.style.top = config.position.top; } } if (config.minimized !== undefined) { isMinimized = config.minimized; widget.classList.toggle('minimized', isMinimized); widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : '−'; } if (config.visible !== undefined) { isVisible = config.visible; widget.classList.toggle('hidden', !isVisible); } } } catch (e) { console.warn('AMD Monitor: Failed to load config:', e); } } function saveConfig() { try { const config = { position: { top: widget.style.top, left: widget.style.left }, minimized: isMinimized, visible: isVisible }; localStorage.setItem(CONFIG.storageKey, JSON.stringify(config)); } catch (e) { console.warn('AMD Monitor: Failed to save config:', e); } } // ===== WEBSOCKET CONNECTION ===== function connectWebSocket() { if (ws?.readyState === WebSocket.OPEN) return; console.log('AMD Monitor: Connecting to', CONFIG.wsUrl); updateStatus('Connecting...', 'warning'); ws = new WebSocket(CONFIG.wsUrl); ws.onopen = () => { console.log('AMD Monitor: Connected'); updateStatus('Connected', 'connected'); updateMethod('Live'); reconnectAttempts = 0; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'amd_stats') { updateDisplay(data); } } catch (e) { console.error('AMD Monitor: Parse error:', e); } }; ws.onclose = () => { console.log('AMD Monitor: Disconnected'); updateStatus('Disconnected', 'disconnected'); updateMethod('--'); if (reconnectAttempts < MAX_RECONNECT) { reconnectAttempts++; const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 8000); console.log(`AMD Monitor: Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); reconnectTimer = setTimeout(connectWebSocket, delay); } }; ws.onerror = (error) => { console.error('AMD Monitor: WebSocket error:', error); updateStatus('Connection error', 'disconnected'); }; } // ===== DISPLAY UPDATES ===== function updateDisplay(data) { if (!data.is_available) { setStat('amd-gpu-util', 'N/A'); setStat('amd-vram', 'N/A'); setStat('amd-temp', 'N/A'); setStat('amd-fan', 'N/A'); setBar('amd-gpu-bar', 0); setBar('amd-vram-bar', 0); setBar('amd-temp-bar', 0); setBar('amd-fan-bar', 0); updateStatus(data.error || 'AMD backend unavailable', 'warning'); updateMethod(data.method || '--'); return; } const gpuUtil = data.gpu_utilization || 0; setStat('amd-gpu-util', `${gpuUtil.toFixed(1)}%`, gpuUtil); setBar('amd-gpu-bar', gpuUtil); const vramUsedGB = (data.vram_used_mb || 0) / 1024; const vramTotalGB = (data.vram_total_mb || 0) / 1024; const vramPct = data.vram_usage_pct || (vramTotalGB > 0 ? (vramUsedGB / vramTotalGB * 100) : 0); setStat('amd-vram', `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)} GB`, vramPct); setBar('amd-vram-bar', vramPct); const temp = data.temperature || 0; setStat('amd-temp', `${temp.toFixed(1)}°C`, temp); setBar('amd-temp-bar', Math.min(temp, 100)); const fan = data.fan_speed || 0; setStat('amd-fan', `${fan}%`, fan); setBar('amd-fan-bar', fan); updateStatus(`GPU ${data.gpu_id}`, 'connected'); updateMethod(data.method || 'unknown'); } function setStat(elementId, value, numericValue = null) { const el = document.getElementById(elementId); if (!el) return; el.textContent = value; el.classList.remove('warning', 'critical'); if (numericValue !== null) { if (elementId === 'amd-temp' && numericValue > 80) { el.classList.add('critical'); } else if (elementId === 'amd-temp' && numericValue > 65) { el.classList.add('warning'); } else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 90) { el.classList.add('critical'); } else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 75) { el.classList.add('warning'); } } } function setBar(elementId, percent) { const el = document.getElementById(elementId); if (!el) return; el.style.width = `${Math.min(100, Math.max(0, percent))}%`; } function updateStatus(message, className) { const el = document.getElementById('amd-status'); if (!el) return; el.textContent = message; el.className = `amd-status ${className}`; const icon = widget.querySelector('.amd-icon'); if (icon) { icon.textContent = className === 'connected' ? '🟢' : className === 'warning' ? '🟡' : '🔴'; } } function updateMethod(method) { const el = document.getElementById('amd-method'); if (!el) return; el.textContent = method ? `[${method}]` : ''; } // ===== UTILITIES ===== function showToast(message, duration = 2000) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: rgba(30, 30, 40, 0.95); color: #fff; padding: 10px 16px; border-radius: 6px; font-size: 12px; z-index: 10001; border: 1px solid rgba(196, 48, 43, 0.5); animation: slideIn 0.2s ease; `; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.2s ease'; setTimeout(() => toast.remove(), 200); }, duration); } if (!document.getElementById('amd-monitor-animations')) { const animStyle = document.createElement('style'); animStyle.id = 'amd-monitor-animations'; animStyle.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(animStyle); } // ===== START ===== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('beforeunload', () => { if (ws) ws.close(); if (reconnectTimer) clearTimeout(reconnectTimer); }); })();