diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..5773f88 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +# Empty pack — only ships a frontend extension. +# ComfyUI auto-serves files under ./web/ when WEB_DIRECTORY is set. +WEB_DIRECTORY = "./web" +NODE_CLASS_MAPPINGS = {} +__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"] diff --git a/web/progressPanel.js b/web/progressPanel.js new file mode 100644 index 0000000..571d9fa --- /dev/null +++ b/web/progressPanel.js @@ -0,0 +1,321 @@ +// ComfyUI Progress Panel — shows live execution events in a toggleable side panel. +// Install: ComfyUI/custom_nodes/progress-panel/web/progressPanel.js +// Also create an empty __init__.py in progress-panel/ so ComfyUI loads the pack. + +import { app } from "../../scripts/app.js"; + +const MAX_LINES = 500; + +app.registerExtension({ + name: "local.toolbox", + + async setup() { + // ---- build DOM ---- + const panel = document.createElement("div"); + panel.id = "pp-panel"; + panel.innerHTML = ` +
+ Toolbox +
+ -- + + + + +
+
+
+
+
idle
+
+
+ `; + + const toggle = document.createElement("button"); + toggle.id = "pp-toggle"; + toggle.textContent = "🧰 Toolbox"; + toggle.title = "Toolbox — drag to move, click to open"; + + const style = document.createElement("style"); + style.textContent = ` + #pp-toggle { + position: fixed; left: 12px; top: 60px; z-index: 9998; + background: #2a2a2a; color: #ddd; border: 1px solid #444; + padding: 6px 12px; border-radius: 6px; font: 12px sans-serif; + cursor: grab; user-select: none; + } + #pp-toggle.dragging { cursor: grabbing; opacity: 0.8; } + #pp-toggle:hover { background: #3a3a3a; } + #pp-panel { + position: fixed; width: 460px; height: 360px; + background: #1a1a1a; border: 1px solid #444; border-radius: 8px; + display: none; flex-direction: column; z-index: 9999; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); font: 12px monospace; + color: #ddd; + } + #pp-panel.open { display: flex; } + #pp-header { + padding: 6px 10px; background: #252525; border-bottom: 1px solid #444; + display: flex; justify-content: space-between; align-items: center; + border-radius: 8px 8px 0 0; font-family: sans-serif; + } + #pp-header-btns { display: flex; gap: 4px; } + #pp-header-btns button { + background: #333; border: 1px solid #555; color: #ddd; + padding: 3px 8px; border-radius: 4px; cursor: pointer; + font: 11px sans-serif; + } + #pp-header-btns button:hover { background: #444; } + #pp-header-btns button:disabled { opacity: 0.4; cursor: not-allowed; } + #pp-vram { + font: 11px monospace; padding: 3px 6px; + background: #222; border: 1px solid #444; border-radius: 4px; + color: #7c7; min-width: 80px; text-align: center; + } + #pp-vram.warn { color: #e9a; } + #pp-vram.crit { color: #f66; } + #pp-bar { + position: relative; + height: 18px; background: #2a2a2a; overflow: hidden; + } + #pp-bar-fill { + height: 100%; width: 0%; background: linear-gradient(90deg, #4a9eff, #6ac); + transition: width 0.15s ease; + } + #pp-bar-text { + position: absolute; inset: 0; + display: flex; align-items: center; justify-content: center; + font: 11px sans-serif; color: #fff; + text-shadow: 0 0 3px rgba(0,0,0,0.9), 0 0 3px rgba(0,0,0,0.9); + pointer-events: none; + } + #pp-log { + flex: 1; overflow-y: auto; padding: 8px 12px; + white-space: pre-wrap; word-break: break-word; line-height: 1.5; + } + #pp-log .ts { color: #666; } + #pp-log .ok { color: #7c7; } + #pp-log .warn { color: #e9a; } + #pp-log .err { color: #f66; } + #pp-log .info { color: #9cf; } + `; + document.head.appendChild(style); + document.body.appendChild(panel); + document.body.appendChild(toggle); + + const logEl = panel.querySelector("#pp-log"); + const barEl = panel.querySelector("#pp-bar-fill"); + const barText = panel.querySelector("#pp-bar-text"); + + // ---- position restore ---- + const POS_KEY = "pp-toggle-pos"; + try { + const saved = JSON.parse(localStorage.getItem(POS_KEY) || "null"); + if (saved && typeof saved.x === "number" && typeof saved.y === "number") { + toggle.style.left = `${saved.x}px`; + toggle.style.top = `${saved.y}px`; + } + } catch {} + + const anchorPanel = () => { + // position panel just below the toggle button, keep on-screen + const r = toggle.getBoundingClientRect(); + const pw = 460, ph = 360, margin = 8; + let x = r.left; + let y = r.bottom + 6; + if (x + pw > window.innerWidth - margin) x = window.innerWidth - pw - margin; + if (y + ph > window.innerHeight - margin) y = r.top - ph - 6; + if (x < margin) x = margin; + if (y < margin) y = margin; + panel.style.left = `${x}px`; + panel.style.top = `${y}px`; + }; + + // ---- drag logic ---- + let dragging = false, moved = false, startX = 0, startY = 0, origX = 0, origY = 0; + toggle.addEventListener("pointerdown", (e) => { + dragging = true; moved = false; + startX = e.clientX; startY = e.clientY; + const r = toggle.getBoundingClientRect(); + origX = r.left; origY = r.top; + toggle.classList.add("dragging"); + toggle.setPointerCapture(e.pointerId); + }); + toggle.addEventListener("pointermove", (e) => { + if (!dragging) return; + const dx = e.clientX - startX, dy = e.clientY - startY; + if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; + const nx = Math.max(0, Math.min(window.innerWidth - toggle.offsetWidth, origX + dx)); + const ny = Math.max(0, Math.min(window.innerHeight - toggle.offsetHeight, origY + dy)); + toggle.style.left = `${nx}px`; + toggle.style.top = `${ny}px`; + toggle.style.right = "auto"; toggle.style.bottom = "auto"; + if (panel.classList.contains("open")) anchorPanel(); + }); + toggle.addEventListener("pointerup", (e) => { + if (!dragging) return; + dragging = false; + toggle.classList.remove("dragging"); + toggle.releasePointerCapture(e.pointerId); + const r = toggle.getBoundingClientRect(); + localStorage.setItem(POS_KEY, JSON.stringify({ x: r.left, y: r.top })); + if (!moved) { + panel.classList.toggle("open"); + if (panel.classList.contains("open")) { + anchorPanel(); + startVramPolling(); + } else { + stopVramPolling(); + } + } + }); + window.addEventListener("resize", () => { + if (panel.classList.contains("open")) anchorPanel(); + }); + + // ---- header buttons ---- + panel.querySelector("#pp-close").onclick = () => { + panel.classList.remove("open"); + stopVramPolling(); + }; + panel.querySelector("#pp-clear").onclick = () => { logEl.innerHTML = ""; }; + + panel.querySelector("#pp-free").onclick = async () => { + line("info", "→ freeing VRAM…"); + try { + const r = await fetch("/free", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ unload_models: true, free_memory: true }), + }); + line(r.ok ? "ok" : "err", r.ok ? "✓ VRAM freed" : `✗ /free returned ${r.status}`); + updateVram(); // refresh readout immediately + } catch (err) { + line("err", `✗ ${err.message}`); + } + }; + + // ---- VRAM polling ---- + const vramEl = panel.querySelector("#pp-vram"); + let vramLoggedShape = false; + const updateVram = async () => { + try { + const r = await fetch("/api/system_stats"); + if (!r.ok) { vramEl.textContent = "n/a"; return; } + const data = await r.json(); + const dev = data?.devices?.[0]; + if (!dev || typeof dev.vram_total !== "number" || typeof dev.vram_free !== "number") { + vramEl.textContent = "??"; + if (!vramLoggedShape) { + console.log("[toolbox] system_stats shape:", data); + vramLoggedShape = true; + } + return; + } + const total = dev.vram_total; + const used = total - dev.vram_free; + const pct = (used / total) * 100; + const gb = (n) => (n / (1024 ** 3)).toFixed(1); + vramEl.textContent = `${gb(used)}/${gb(total)} GB (${pct.toFixed(0)}%)`; + vramEl.classList.toggle("warn", pct >= 70 && pct < 90); + vramEl.classList.toggle("crit", pct >= 90); + } catch (err) { + vramEl.textContent = "err"; + } + }; + let vramTimer = null; + const startVramPolling = () => { + updateVram(); + if (vramTimer) clearInterval(vramTimer); + vramTimer = setInterval(updateVram, 2000); + }; + const stopVramPolling = () => { + if (vramTimer) { clearInterval(vramTimer); vramTimer = null; } + }; + + panel.querySelector("#pp-reboot").onclick = async () => { + if (!confirm("Restart ComfyUI? Current run will be lost.")) return; + line("warn", "→ requesting reboot…"); + try { + // ComfyUI-Manager exposes /manager/reboot + const r = await fetch("/manager/reboot", { method: "GET" }); + if (r.ok) { + line("ok", "✓ reboot requested — reload page when server is back"); + } else { + line("err", `✗ /manager/reboot returned ${r.status} (is ComfyUI-Manager installed?)`); + } + } catch (err) { + // server actually going down often causes a network error — that's the success case + line("warn", `server dropped connection (likely restarting): ${err.message}`); + } + }; + + const line = (cls, text) => { + const ts = new Date().toLocaleTimeString(); + const row = document.createElement("div"); + row.innerHTML = `[${ts}] ${text}`; + logEl.appendChild(row); + while (logEl.children.length > MAX_LINES) logEl.removeChild(logEl.firstChild); + logEl.scrollTop = logEl.scrollHeight; + }; + + // ---- hook into ComfyUI's api events ---- + const api = app.api; + let runStart = 0; + let currentNodeLabel = ""; + + api.addEventListener("execution_start", (e) => { + runStart = performance.now(); + barEl.style.width = "0%"; + barText.textContent = "starting…"; + line("info", `▶ Run started (prompt ${e.detail?.prompt_id ?? "?"})`); + }); + + api.addEventListener("executing", (e) => { + const nodeId = e.detail; + if (nodeId === null) { + const dur = ((performance.now() - runStart) / 1000).toFixed(1); + barEl.style.width = "100%"; + barText.textContent = `done in ${dur}s`; + currentNodeLabel = ""; + line("ok", `✓ Run finished in ${dur}s`); + } else { + const node = app.graph?.getNodeById?.(Number(nodeId)); + currentNodeLabel = node ? `${node.type} #${nodeId}` : `node #${nodeId}`; + barEl.style.width = "0%"; + barText.textContent = currentNodeLabel; + line("info", `→ ${currentNodeLabel}`); + } + }); + + api.addEventListener("progress", (e) => { + const { value, max } = e.detail || {}; + if (typeof value === "number" && typeof max === "number" && max > 0) { + const pct = (value / max) * 100; + barEl.style.width = `${pct}%`; + const prefix = currentNodeLabel ? `${currentNodeLabel} — ` : ""; + barText.textContent = `${prefix}${value}/${max} (${pct.toFixed(0)}%)`; + } + }); + + api.addEventListener("execution_cached", (e) => { + const n = e.detail?.nodes?.length ?? 0; + if (n) line("ts", `cached ${n} node${n === 1 ? "" : "s"}`); + }); + + api.addEventListener("execution_error", (e) => { + const d = e.detail || {}; + line("err", `✗ ${d.exception_type || "Error"}: ${d.exception_message || "unknown"}`); + if (d.node_type) line("err", ` in ${d.node_type} (#${d.node_id})`); + if (d.traceback) { + const tb = Array.isArray(d.traceback) ? d.traceback.join("") : d.traceback; + line("err", tb.split("\n").slice(-8).join("\n")); + } + panel.classList.add("open"); // auto-open on error + }); + + api.addEventListener("execution_interrupted", () => line("warn", "⊘ Interrupted")); + + line("ts", "Progress panel ready"); + }, +});