From fed626685b0d34dea87cd3d19e21ea334c01c05d Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 21 Jun 2026 12:45:49 +0200 Subject: [PATCH] feat(workflow): workflow tab UI + auto-open on load --- js/nodes_stats.js | 105 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 17 deletions(-) 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 += ``; + for (const d of disabled) { + const t = trialByPkg[d.pkg]; + const note = t ? `on trial · ${t.days_remaining}d left` : ""; + html += ` + + + `; + } + html += `
${escapeHtml(d.type)}${escapeHtml(d.pkg)} ${note} + + +
`; + } + if (missing.length) { + html += sectionHeader("Missing", "Not installed — install via ComfyUI Manager", "#e44"); + html += ``; + for (const m of missing) { + html += ` + + + `; + } + html += `
${escapeHtml(m.type)}${m.pkg ? escapeHtml(m.pkg) : "unknown"} + ${m.pkg ? `` : "—"} +
`; + } + return html; +} + function sectionHeader(title, subtitle, color) { let html = `

${escapeHtml(title)}`; if (subtitle) html += ` — ${escapeHtml(subtitle)}`; @@ -515,6 +575,17 @@ function wireDisableButtons(dialog, managerInfo) { }); } +// Wire the Workflow tab's enable/install buttons. Handlers are filled in by the +// enable (Task 10) and install (Task 11) steps. +function wireWorkflowButtons(dialog) { + dialog.querySelectorAll(".ns-enable-temp-btn").forEach((b) => + b.addEventListener("click", (e) => { e.stopPropagation(); handleEnable(b.dataset.pkg, true, dialog); })); + dialog.querySelectorAll(".ns-enable-perm-btn").forEach((b) => + b.addEventListener("click", (e) => { e.stopPropagation(); handleEnable(b.dataset.pkg, false, dialog); })); + dialog.querySelectorAll(".ns-install-btn").forEach((b) => + b.addEventListener("click", (e) => { e.stopPropagation(); handleInstall(b.dataset.pkg, dialog); })); +} + async function handleDisable(pkgNames, dialog, managerInfo) { // Only act on packages Manager still reports as active (guards against // double-clicks and stale buttons after a partial batch).