feat(workflow): workflow tab UI + auto-open on load

This commit is contained in:
2026-06-21 12:45:49 +02:00
parent 743741afc6
commit fed626685b
+88 -17
View File
@@ -69,24 +69,32 @@ function unresolvedNodeTypes() {
return [...types]; return [...types];
} }
// Latest workflow scan, shared so showStatsDialog can render the Workflow tab.
let _lastWorkflowScan = { disabled: [], missing: [] };
async function onWorkflowLoaded() { async function onWorkflowLoaded() {
const unresolved = unresolvedNodeTypes(); const types = unresolvedNodeTypes();
if (unresolved.length) console.log("[Node Stats] unresolved:", unresolved); _lastWorkflowScan = await classifyUnresolved(types);
if (_lastWorkflowScan.disabled.length || _lastWorkflowScan.missing.length) {
showStatsDialog("workflow"); // auto-open on the Workflow tab
}
} }
async function showStatsDialog() { async function showStatsDialog(initialTab = "nodes") {
let data, modelData, managerInfo; let data, modelData, managerInfo, trials = [];
try { try {
const [pkgResp, modelResp, mgr] = await Promise.all([ const [pkgResp, modelResp, mgr, trialsResp] = await Promise.all([
fetch("/nodes-stats/packages"), fetch("/nodes-stats/packages"),
fetch("/nodes-stats/models"), fetch("/nodes-stats/models"),
fetchManagerInfo(), fetchManagerInfo(),
fetch("/nodes-stats/trials").catch(() => null),
]); ]);
if (!pkgResp.ok) { alert("Failed to load node stats: HTTP " + pkgResp.status); return; } 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; } if (!modelResp.ok) { alert("Failed to load model stats: HTTP " + modelResp.status); return; }
data = await pkgResp.json(); data = await pkgResp.json();
modelData = await modelResp.json(); modelData = await modelResp.json();
managerInfo = mgr; managerInfo = mgr;
if (trialsResp && trialsResp.ok) { try { trials = await trialsResp.json(); } catch { trials = []; } }
if (!Array.isArray(data) || !Array.isArray(modelData)) { if (!Array.isArray(data) || !Array.isArray(modelData)) {
alert("Failed to load stats: unexpected response format"); alert("Failed to load stats: unexpected response format");
return; 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;"> style="background:none;border:none;border-bottom:2px solid transparent;color:#888;padding:8px 18px;cursor:pointer;font-family:monospace;font-size:13px;">
Models Models
</button> </button>
<button id="ns-tab-workflow"
style="background:none;border:none;border-bottom:2px solid transparent;color:#888;padding:8px 18px;cursor:pointer;font-family:monospace;font-size:13px;">
Workflow
</button>
</div>`; </div>`;
// Nodes tab content // Nodes tab content
@@ -144,25 +156,29 @@ async function showStatsDialog() {
html += buildModelsTabContent(modelData); html += buildModelsTabContent(modelData);
html += `</div>`; html += `</div>`;
// Workflow tab content (missing / disabled nodes in the loaded workflow)
html += `<div id="ns-content-workflow" style="display:none;">`;
html += buildWorkflowTabContent(_lastWorkflowScan, trials);
html += `</div>`;
dialog.innerHTML = html; dialog.innerHTML = html;
overlay.appendChild(dialog); overlay.appendChild(dialog);
document.body.appendChild(overlay); document.body.appendChild(overlay);
// Tab switch — local function, no window pollution // Tab switch — local function, no window pollution
const TABS = ["nodes", "models", "workflow"];
function switchTab(tab) { function switchTab(tab) {
dialog.querySelector("#ns-content-nodes").style.display = tab === "nodes" ? "" : "none"; for (const t of TABS) {
dialog.querySelector("#ns-content-models").style.display = tab === "models" ? "" : "none"; dialog.querySelector(`#ns-content-${t}`).style.display = t === tab ? "" : "none";
const nodeBtn = dialog.querySelector("#ns-tab-nodes"); const b = dialog.querySelector(`#ns-tab-${t}`);
const modelBtn = dialog.querySelector("#ns-tab-models"); b.style.borderBottomColor = t === tab ? "#4a4" : "transparent";
nodeBtn.style.borderBottomColor = tab === "nodes" ? "#4a4" : "transparent"; b.style.color = t === tab ? "#4a4" : "#888";
nodeBtn.style.color = tab === "nodes" ? "#4a4" : "#888"; b.style.fontWeight = t === tab ? "bold" : "normal";
nodeBtn.style.fontWeight = tab === "nodes" ? "bold" : "normal"; }
modelBtn.style.borderBottomColor = tab === "models" ? "#4a4" : "transparent"; }
modelBtn.style.color = tab === "models" ? "#4a4" : "#888"; for (const t of TABS) {
modelBtn.style.fontWeight = tab === "models" ? "bold" : "normal"; 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()); dialog.querySelector("#nodes-stats-close").addEventListener("click", () => overlay.remove());
@@ -181,6 +197,9 @@ async function showStatsDialog() {
}); });
wireDisableButtons(dialog, managerInfo); wireDisableButtons(dialog, managerInfo);
wireWorkflowButtons(dialog);
switchTab(TABS.includes(initialTab) ? initialTab : "nodes");
// Easter egg: click "used" badge 5 times to show podium // Easter egg: click "used" badge 5 times to show podium
let eggClicks = 0; let eggClicks = 0;
@@ -342,6 +361,47 @@ function buildModelTable(models) {
return html; 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 `<p style="color:#666;">No missing or disabled nodes in the current workflow.</p>`;
}
if (disabled.length) {
html += sectionHeader("Disabled", "Installed but disabled — re-enable to use", "#e90");
html += `<table style="width:100%;border-collapse:collapse;margin-bottom:12px;"><tbody>`;
for (const d of disabled) {
const t = trialByPkg[d.pkg];
const note = t ? `<span style="color:#6a6;font-size:11px;">on trial · ${t.days_remaining}d left</span>` : "";
html += `<tr class="ns-row-consider_removing" style="border-bottom:1px solid #222;">
<td style="padding:6px 8px;color:#fff;">${escapeHtml(d.type)}</td>
<td style="padding:6px 8px;color:#888;">${escapeHtml(d.pkg)} ${note}</td>
<td style="padding:6px 8px;text-align:right;white-space:nowrap;">
<button class="ns-btn ns-enable-temp-btn" data-pkg="${escapeAttr(d.pkg)}">Enable 7d</button>
<button class="ns-btn ns-enable-perm-btn" data-pkg="${escapeAttr(d.pkg)}" style="margin-left:6px;">Enable</button>
</td></tr>`;
}
html += `</tbody></table>`;
}
if (missing.length) {
html += sectionHeader("Missing", "Not installed — install via ComfyUI Manager", "#e44");
html += `<table style="width:100%;border-collapse:collapse;margin-bottom:12px;"><tbody>`;
for (const m of missing) {
html += `<tr class="ns-row-safe_to_remove" style="border-bottom:1px solid #222;">
<td style="padding:6px 8px;color:#fff;">${escapeHtml(m.type)}</td>
<td style="padding:6px 8px;color:#888;">${m.pkg ? escapeHtml(m.pkg) : "unknown"}</td>
<td style="padding:6px 8px;text-align:right;">
${m.pkg ? `<button class="ns-btn ns-install-btn" data-pkg="${escapeAttr(m.pkg)}">Install</button>` : "&mdash;"}
</td></tr>`;
}
html += `</tbody></table>`;
}
return html;
}
function sectionHeader(title, subtitle, color) { function sectionHeader(title, subtitle, color) {
let html = `<h3 style="color:${color};margin:16px 0 8px;font-size:14px;">${escapeHtml(title)}`; let html = `<h3 style="color:${color};margin:16px 0 8px;font-size:14px;">${escapeHtml(title)}`;
if (subtitle) html += ` <span style="color:#666;font-size:12px;font-weight:normal;">— ${escapeHtml(subtitle)}</span>`; if (subtitle) html += ` <span style="color:#666;font-size:12px;font-weight:normal;">— ${escapeHtml(subtitle)}</span>`;
@@ -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) { async function handleDisable(pkgNames, dialog, managerInfo) {
// Only act on packages Manager still reports as active (guards against // Only act on packages Manager still reports as active (guards against
// double-clicks and stale buttons after a partial batch). // double-clicks and stale buttons after a partial batch).