diff --git a/js/nodes_stats.js b/js/nodes_stats.js index 15a47de..5c49636 100644 --- a/js/nodes_stats.js +++ b/js/nodes_stats.js @@ -69,24 +69,32 @@ function unresolvedNodeTypes() { return [...types]; } +// Latest workflow scan, shared so showStatsDialog can render the Workflow tab. +let _lastWorkflowScan = { disabled: [], missing: [] }; + async function onWorkflowLoaded() { - const unresolved = unresolvedNodeTypes(); - if (unresolved.length) console.log("[Node Stats] unresolved:", unresolved); + const types = unresolvedNodeTypes(); + _lastWorkflowScan = await classifyUnresolved(types); + if (_lastWorkflowScan.disabled.length || _lastWorkflowScan.missing.length) { + showStatsDialog("workflow"); // auto-open on the Workflow tab + } } -async function showStatsDialog() { - let data, modelData, managerInfo; +async function showStatsDialog(initialTab = "nodes") { + let data, modelData, managerInfo, trials = []; try { - const [pkgResp, modelResp, mgr] = await Promise.all([ + const [pkgResp, modelResp, mgr, trialsResp] = await Promise.all([ fetch("/nodes-stats/packages"), fetch("/nodes-stats/models"), fetchManagerInfo(), + fetch("/nodes-stats/trials").catch(() => null), ]); if (!pkgResp.ok) { alert("Failed to load node stats: HTTP " + pkgResp.status); return; } if (!modelResp.ok) { alert("Failed to load model stats: HTTP " + modelResp.status); return; } data = await pkgResp.json(); modelData = await modelResp.json(); managerInfo = mgr; + if (trialsResp && trialsResp.ok) { try { trials = await trialsResp.json(); } catch { trials = []; } } if (!Array.isArray(data) || !Array.isArray(modelData)) { alert("Failed to load stats: unexpected response format"); return; @@ -132,6 +140,10 @@ async function showStatsDialog() { style="background:none;border:none;border-bottom:2px solid transparent;color:#888;padding:8px 18px;cursor:pointer;font-family:monospace;font-size:13px;"> Models + `; // Nodes tab content @@ -144,25 +156,29 @@ async function showStatsDialog() { html += buildModelsTabContent(modelData); html += ``; + // Workflow tab content (missing / disabled nodes in the loaded workflow) + html += `
`; + dialog.innerHTML = html; overlay.appendChild(dialog); document.body.appendChild(overlay); // Tab switch — local function, no window pollution + const TABS = ["nodes", "models", "workflow"]; function switchTab(tab) { - dialog.querySelector("#ns-content-nodes").style.display = tab === "nodes" ? "" : "none"; - dialog.querySelector("#ns-content-models").style.display = tab === "models" ? "" : "none"; - const nodeBtn = dialog.querySelector("#ns-tab-nodes"); - const modelBtn = dialog.querySelector("#ns-tab-models"); - nodeBtn.style.borderBottomColor = tab === "nodes" ? "#4a4" : "transparent"; - nodeBtn.style.color = tab === "nodes" ? "#4a4" : "#888"; - nodeBtn.style.fontWeight = tab === "nodes" ? "bold" : "normal"; - modelBtn.style.borderBottomColor = tab === "models" ? "#4a4" : "transparent"; - modelBtn.style.color = tab === "models" ? "#4a4" : "#888"; - modelBtn.style.fontWeight = tab === "models" ? "bold" : "normal"; + for (const t of TABS) { + dialog.querySelector(`#ns-content-${t}`).style.display = t === tab ? "" : "none"; + const b = dialog.querySelector(`#ns-tab-${t}`); + b.style.borderBottomColor = t === tab ? "#4a4" : "transparent"; + b.style.color = t === tab ? "#4a4" : "#888"; + b.style.fontWeight = t === tab ? "bold" : "normal"; + } + } + for (const t of TABS) { + dialog.querySelector(`#ns-tab-${t}`).addEventListener("click", () => switchTab(t)); } - dialog.querySelector("#ns-tab-nodes").addEventListener("click", () => switchTab("nodes")); - dialog.querySelector("#ns-tab-models").addEventListener("click", () => switchTab("models")); dialog.querySelector("#nodes-stats-close").addEventListener("click", () => overlay.remove()); @@ -181,6 +197,9 @@ async function showStatsDialog() { }); wireDisableButtons(dialog, managerInfo); + wireWorkflowButtons(dialog); + + switchTab(TABS.includes(initialTab) ? initialTab : "nodes"); // Easter egg: click "used" badge 5 times to show podium let eggClicks = 0; @@ -342,6 +361,47 @@ function buildModelTable(models) { return html; } +// Render the Workflow tab from a classification result. `disabled` entries get +// re-enable actions (temporary trial or permanent); `missing` entries get an +// Install button that defers to ComfyUI Manager. +function buildWorkflowTabContent({ disabled, missing }, trials) { + const trialByPkg = Object.fromEntries((trials || []).map((t) => [t.package, t])); + let html = ""; + if (!disabled.length && !missing.length) { + return `No missing or disabled nodes in the current workflow.
`; + } + if (disabled.length) { + html += sectionHeader("Disabled", "Installed but disabled — re-enable to use", "#e90"); + html += `| ${escapeHtml(d.type)} | +${escapeHtml(d.pkg)} ${note} | ++ + + |
| ${escapeHtml(m.type)} | +${m.pkg ? escapeHtml(m.pkg) : "unknown"} | ++ ${m.pkg ? `` : "—"} + |