import { app } from "../../scripts/app.js"; // Bar chart with nodes icon const STATS_ICON = ` `; app.registerExtension({ name: "comfyui.nodes_stats", async setup() { const btn = document.createElement("button"); btn.innerHTML = STATS_ICON; btn.title = "Node Stats"; btn.className = "comfyui-button comfyui-menu-mobile-collapse"; btn.onclick = () => showStatsDialog(); btn.style.cssText = "display:flex;align-items:center;justify-content:center;padding:6px;cursor:pointer;"; if (app.menu?.settingsGroup?.element) { app.menu.settingsGroup.element.before(btn); } else { const menu = document.querySelector(".comfy-menu"); if (menu) { menu.append(btn); } } }, }); async function showStatsDialog() { let data; try { const resp = await fetch("/nodes-stats/packages"); if (!resp.ok) { alert("Failed to load node stats: HTTP " + resp.status); return; } data = await resp.json(); if (!Array.isArray(data)) { alert("Failed to load node stats: unexpected response format"); return; } } catch (e) { alert("Failed to load node stats: " + e.message); return; } // Remove existing dialog if any const existing = document.getElementById("nodes-stats-dialog"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "nodes-stats-dialog"; overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;"; overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); const dialog = document.createElement("div"); dialog.style.cssText = "background:#1e1e1e;color:#ddd;border-radius:8px;padding:24px;max-width:800px;width:90%;max-height:85vh;overflow-y:auto;font-family:monospace;font-size:13px;"; const custom = data.filter((p) => p.package !== "__builtin__"); const safeToRemove = custom.filter((p) => p.status === "safe_to_remove"); const considerRemoving = custom.filter((p) => p.status === "consider_removing"); const unusedNew = custom.filter((p) => p.status === "unused_new"); const used = custom.filter((p) => p.status === "used"); const uninstalled = custom.filter((p) => p.status === "uninstalled"); let html = `

Node Package Stats

`; html += `
${safeToRemove.length} safe to remove
${considerRemoving.length} consider removing
${unusedNew.length} unused <1 month
${used.length} used
`; if (safeToRemove.length > 0) { html += sectionHeader("Safe to Remove", "Unused for 2+ months", "#e44"); html += buildTable(safeToRemove, "safe_to_remove"); } if (considerRemoving.length > 0) { html += sectionHeader("Consider Removing", "Unused for 1-2 months", "#e90"); html += buildTable(considerRemoving, "consider_removing"); } if (unusedNew.length > 0) { html += sectionHeader("Recently Unused", "Unused for less than 1 month", "#68f"); html += buildTable(unusedNew, "unused_new"); } if (used.length > 0) { html += sectionHeader("Used", "", "#4a4"); html += buildTable(used, "used"); } if (uninstalled.length > 0) { html += sectionHeader("Uninstalled", "Previously tracked, no longer installed", "#555"); html += buildTable(uninstalled, "uninstalled"); } dialog.innerHTML = html; overlay.appendChild(dialog); document.body.appendChild(overlay); document .getElementById("nodes-stats-close") .addEventListener("click", () => overlay.remove()); // Toggle expandable rows dialog.querySelectorAll(".pkg-row").forEach((row) => { row.addEventListener("click", () => { const detail = row.nextElementSibling; if (detail && detail.classList.contains("pkg-detail")) { detail.style.display = detail.style.display === "none" ? "table-row" : "none"; const arrow = row.querySelector(".arrow"); if (arrow) arrow.textContent = detail.style.display === "none" ? "▶" : "▼"; } }); }); // Easter egg: click "used" badge 5 times to show podium let eggClicks = 0; let eggTimer = null; const usedBadge = document.getElementById("nodes-stats-used-badge"); if (usedBadge) { usedBadge.addEventListener("click", () => { eggClicks++; clearTimeout(eggTimer); eggTimer = setTimeout(() => (eggClicks = 0), 1500); if (eggClicks >= 5) { eggClicks = 0; const allNodes = custom .flatMap((p) => p.nodes.map((n) => ({ ...n, pkg: p.package }))) .sort((a, b) => b.count - a.count); showPodium(allNodes.slice(0, 3), overlay); } }); } } function sectionHeader(title, subtitle, color) { let html = `

${title}`; if (subtitle) html += ` — ${subtitle}`; html += `

`; return html; } const STATUS_COLORS = { safe_to_remove: { bg: "#2a1515", hover: "#3a2020" }, consider_removing: { bg: "#2a2215", hover: "#3a2e20" }, unused_new: { bg: "#1a1a25", hover: "#252530" }, used: { bg: "#151a15", hover: "#202a20" }, uninstalled: { bg: "#1a1a1a", hover: "#252525" }, }; function buildTable(packages, status) { const { bg: bgColor, hover: hoverColor } = STATUS_COLORS[status] || STATUS_COLORS.used; let html = ``; for (const pkg of packages) { const hasNodes = pkg.nodes && pkg.nodes.length > 0; const lastSeen = pkg.last_seen ? new Date(pkg.last_seen).toLocaleDateString() : "—"; html += ``; if (hasNodes) { html += ``; } } html += `
Package Nodes Used Executions Last Used
${hasNodes ? "▶" : " "} ${escapeHtml(pkg.package)} ${pkg.total_nodes} ${pkg.used_nodes}/${pkg.total_nodes} ${pkg.total_executions} ${lastSeen}
`; return html; } // Internal: builds celebratory overlay for top contributors function showPodium(top3, overlay) { const existing = document.getElementById("nodes-stats-podium"); if (existing) { existing.remove(); return; } const colors = ["#FFD700", "#C0C0C0", "#CD7F32"]; const heights = [160, 120, 90]; const order = [1, 0, 2]; // SVG characters: champion with cape, cool runner-up, happy bronze const characters = [ // Gold: flexing champion with crown and cape ` GOAT `, // Silver: sunglasses dude, arms crossed ` cool. `, // Bronze: happy little guy waving ` yay! `, ]; const podium = document.createElement("div"); podium.id = "nodes-stats-podium"; podium.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(ellipse at center,#1a1a2e 0%,#0a0a12 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;z-index:1;cursor:pointer;overflow:hidden;"; podium.addEventListener("click", () => podium.remove()); // Sparkle particles let sparkles = ""; for (let i = 0; i < 20; i++) { const x = Math.random() * 100; const y = Math.random() * 60; const d = (1 + Math.random() * 2).toFixed(1); const o = (0.3 + Math.random() * 0.7).toFixed(2); sparkles += `
`; } let html = ``; html += sparkles; // Trophy title html += `
#1
Hall of Fame
`; // Podium blocks html += `
`; for (const i of order) { const node = top3[i]; if (!node) continue; const isGold = i === 0; const w = isGold ? 170 : 140; const floatDelay = [0, 0.3, 0.6][i]; html += `
${characters[i]}
${escapeHtml(node.class_type)}
${escapeHtml(node.pkg)}
${i + 1}${["st","nd","rd"][i]}
${node.count.toLocaleString()}x
`; } html += `
`; html += `
click to dismiss
`; podium.innerHTML = html; overlay.querySelector("div").style.position = "relative"; overlay.querySelector("div").appendChild(podium); } function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; }