feat(workflow): workflow tab UI + auto-open on load
This commit is contained in:
+88
-17
@@ -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>` : "—"}
|
||||||
|
</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).
|
||||||
|
|||||||
Reference in New Issue
Block a user