(function () { "use strict"; const state = (window.PQueue && window.PQueue.state) || {}; const API = (window.PQueue && window.PQueue.API) || window.API; const Icons = (window.PQueue && window.PQueue.Icons) || window.Icons; const Format = (window.PQueue && window.PQueue.Format) || window.Format; const Events = (window.PQueue && window.PQueue.Events) || window.Events; const setStatusMessage = (window.PQueue && window.PQueue.setStatusMessage) || window.setStatusMessage; const copyText = (window.PQueue && window.PQueue.copyText) || window.copyText; const UI = window.PQueue?.UI || (window.PQueue = (window.PQueue || {}), window.PQueue.UI = {}, window.PQueue.UI); UI.el = function el(tag, attrs = {}, children = []) { const el = document.createElement(tag); Object.entries(attrs || {}).forEach(([key, value]) => { if (value === null || value === undefined) return; if (key === "class") { el.className = Array.isArray(value) ? value.filter(Boolean).join(" ") : String(value); } else if (key === "text") { el.textContent = value; } else { el.setAttribute(key, value); } }); (Array.isArray(children) ? children : [children]).forEach((child) => { if (child instanceof Node) el.appendChild(child); else if (child !== null && child !== undefined) el.appendChild(document.createTextNode(String(child))); }); return el; }; UI.icon = function icon(name, { spin = false, size } = {}) { const classes = ["pqueue-icon"]; if (spin) classes.push("pqueue-icon--spin"); if (size) classes.push(`pqueue-icon--${size}`); const el = UI.el("span", { class: classes }); const svg = Icons.resolve(name); if (svg) el.appendChild(svg); else if (name) el.classList.add(...String(name).split(/\s+/)); return el; }; UI.button = function button({ id, text, icon, variant, subtle, size, badge, title, disabled, onClick, ariaLabel }) { const classes = ["pqueue-button"]; if (variant) classes.push(`pqueue-button--${variant}`); if (subtle) classes.push("pqueue-button--subtle"); if (size) classes.push(`pqueue-button--${size}`); const attrs = { class: classes, type: "button" }; if (id) attrs.id = id; if (title) attrs.title = title; if (ariaLabel) attrs["aria-label"] = ariaLabel; const btn = UI.el("button", attrs); if (disabled) btn.disabled = true; if (icon) btn.appendChild(UI.icon(icon)); if (text) btn.appendChild(UI.el("span", { class: "pqueue-button__label", text })); if (badge != null) { btn.appendChild(UI.el("span", { class: "pqueue-button__badge", text: String(badge) })); } if (typeof onClick === "function") btn.addEventListener("click", onClick); return btn; }; UI.statusBadge = function statusBadge(status, { subtle } = {}) { const normalized = String(status || "").toLowerCase(); const meta = { success: { icon: "ti ti-circle-check", label: "Success", variant: "success" }, completed: { icon: "ti ti-circle-check", label: "Completed", variant: "success" }, running: { icon: "ti ti-player-play", label: "Running", variant: "info" }, executing: { icon: "ti ti-player-play", label: "Executing", variant: "info" }, failed: { icon: "ti ti-alert-triangle", label: "Failed", variant: "danger" }, error: { icon: "ti ti-alert-triangle", label: "Error", variant: "danger" }, interrupted: { icon: "ti ti-player-stop", label: "Interrupted", variant: "warning" }, cancelled: { icon: "ti ti-player-stop", label: "Cancelled", variant: "warning" }, canceled: { icon: "ti ti-player-stop", label: "Cancelled", variant: "warning" }, pending: { icon: "ti ti-clock-hour-3", label: "Pending", variant: "neutral" }, queued: { icon: "ti ti-clock-hour-3", label: "Queued", variant: "neutral" }, }[normalized] || { icon: "ti ti-circle-dashed", label: Format.statusLabel(status || "Unknown"), variant: "neutral" }; const classes = ["pqueue-status", `pqueue-status--${meta.variant}`]; if (subtle) classes.push("pqueue-status--subtle"); const badge = UI.el("span", { class: classes }); badge.appendChild(UI.icon(meta.icon, { size: "sm" })); badge.appendChild(UI.el("span", { class: "pqueue-status__label", text: meta.label })); return badge; }; UI.emptyState = function emptyState({ icon, title, description, action } = {}) { const box = UI.el("div", { class: "pqueue-empty" }); if (icon) box.appendChild(UI.icon(icon, { size: "lg" })); if (title) box.appendChild(UI.el("h4", { class: "pqueue-empty__title", text: title })); if (description) box.appendChild(UI.el("p", { class: "pqueue-empty__desc", text: description })); if (action) box.appendChild(action); return box; }; UI.metricTile = function metricTile({ icon, label, value, caption, variant, tooltip }) { const card = UI.el("article", { class: ["pqueue-metric", variant ? `pqueue-metric--${variant}` : null] }); const labelEl = UI.el("span", { class: "pqueue-metric__label", text: label }); const headerChildren = [UI.icon(icon, { size: "md" }), labelEl]; if (tooltip) { card.setAttribute("title", tooltip); } const header = UI.el("div", { class: "pqueue-metric__header" }, headerChildren); card.appendChild(header); card.appendChild(UI.el("div", { class: "pqueue-metric__value", text: value })); if (caption) card.appendChild(UI.el("div", { class: "pqueue-metric__caption", text: caption })); return card; }; UI.ensureAssets = function ensureAssets() { const ver = (window.PQueue && window.PQueue.cssVersion) || (window.PQueue && (window.PQueue.cssVersion = String(Date.now()))) || String(Date.now()); const baseHref = "/extensions/ComfyUI-Persistent-Queue/css/queue_style.css"; const desiredHref = baseHref + "?v=" + ver; const existing = document.getElementById("pqueue-style"); if (!existing) { const link = document.createElement("link"); link.id = "pqueue-style"; link.rel = "stylesheet"; link.href = desiredHref; document.head.appendChild(link); } else { try { if (existing.href && !existing.href.endsWith(ver)) existing.href = desiredHref; } catch (err) { /* noop */ } } if (!document.querySelector("link[data-pqueue-icons]")) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = "/extensions/ComfyUI-Persistent-Queue/lib/tabler-icons.min.css"; link.dataset.pqueueIcons = "true"; document.head.appendChild(link); } }; UI.historyKey = function historyKey(row) { try { const ts = row && (row.completed_at || row.created_at); if (typeof ts === 'number' && Number.isFinite(ts)) { return ts < 1e12 ? Math.floor(ts * 1000) : Math.floor(ts); } if (typeof ts === 'string') { const s = ts.trim(); if (/^\d+$/.test(s)) { const n = parseInt(s, 10); if (Number.isFinite(n)) return n < 1e12 ? n * 1000 : n; } let ms = Date.parse(s); if (!Number.isFinite(ms)) { ms = Date.parse(s.replace(' ', 'T')); } if (Number.isFinite(ms)) return ms; } const id = Number(row?.id); return Number.isFinite(id) ? id : 0; } catch (err) { const id = Number(row?.id); return Number.isFinite(id) ? id : 0; } }; UI.historyKeyParts = function historyKeyParts(row) { const ms = UI.historyKey(row) || 0; const id = Number(row?.id) || 0; return [ms, id]; }; UI.compareHistoryRows = function compareHistoryRows(a, b, dir = 'desc') { try { const [ams, aid] = UI.historyKeyParts(a); const [bms, bid] = UI.historyKeyParts(b); if (ams !== bms) return dir === 'desc' ? (bms - ams) : (ams - bms); return dir === 'desc' ? (bid - aid) : (aid - bid); } catch (err) { return 0; } }; UI.getScrollContainer = function getScrollContainer() { try { if (state?.dom?.scrollContainer && document.body.contains(state.dom.scrollContainer)) return state.dom.scrollContainer; const explicit = document.querySelector('.sidebar-content-container'); if (explicit) { state.dom.scrollContainer = explicit; return explicit; } let el = state.container; while (el && el !== document.body && el !== document.documentElement) { try { const style = window.getComputedStyle(el); const oy = style.overflowY; if ((oy === 'auto' || oy === 'scroll') && el.scrollHeight > el.clientHeight) { state.dom.scrollContainer = el; return el; } el = el.parentElement; } catch (err) { break; } } const fallback = document.scrollingElement || document.documentElement || document.body; state.dom.scrollContainer = fallback; return fallback; } catch (err) { return document.scrollingElement || document.documentElement || document.body; } }; UI.getVisibleHistoryAnchor = function getVisibleHistoryAnchor() { try { const scroller = UI.getScrollContainer(); const grid = state.dom.historyGrid; if (!scroller || !grid) return null; const scRect = scroller.getBoundingClientRect(); const cards = Array.from(grid.querySelectorAll('.pqueue-history-card')); if (!cards.length) return null; let best = null; let bestDelta = Infinity; for (const el of cards) { const top = el.getBoundingClientRect().top; const delta = top - scRect.top; if (delta >= -1 && delta < bestDelta) { bestDelta = delta; best = el; } } if (best) return best; for (let i = cards.length - 1; i >= 0; i--) { const el = cards[i]; if (el.getBoundingClientRect().top < scRect.top) return el; } return cards[0]; } catch (err) { return null; } }; UI.withStableAnchor = function withStableAnchor(mutator) { try { state.anchorLockDepth = (state.anchorLockDepth || 0) + 1; if (state.anchorLockDepth === 1) { const scroller = UI.getScrollContainer(); const anchor = UI.getVisibleHistoryAnchor() || state.dom.historyCard || state.dom.root; if (scroller && anchor) { const scRect = scroller.getBoundingClientRect(); const aRect = anchor.getBoundingClientRect(); state.anchorSnapshot = { scroller, anchor, before: aRect.top - scRect.top }; } else { state.anchorSnapshot = null; } } mutator && mutator(); window.requestAnimationFrame(() => { try { state.anchorLockDepth = Math.max(0, (state.anchorLockDepth || 1) - 1); if (state.anchorLockDepth === 0 && state.anchorSnapshot && state.anchorSnapshot.scroller && document.body.contains(state.anchorSnapshot.anchor)) { const { scroller, anchor, before } = state.anchorSnapshot; const scRect2 = scroller.getBoundingClientRect(); const aRect2 = anchor.getBoundingClientRect(); const delta = (aRect2.top - scRect2.top) - before; if (delta) scroller.scrollTop = Math.max(0, scroller.scrollTop + delta); } } catch (err) { /* noop */ } }); } catch (err) { try { mutator && mutator(); } catch (e) { /* noop */ } state.anchorLockDepth = 0; state.anchorSnapshot = null; } }; UI.estimateDuration = function estimateDuration(promptId, workflow) { let key = state.workflowCache.get(promptId); if (!key && workflow !== undefined) { try { key = typeof workflow === "string" ? workflow : JSON.stringify(workflow); if (key) state.workflowCache.set(promptId, key); } catch (err) { /* ignore */ } } if (key && state.durationByWorkflow.has(key)) return state.durationByWorkflow.get(key); return state.metrics.avgDuration; }; UI.card = function card(title, { icon, actions, subtitle, content, classes } = {}) { const head = []; const titleWrap = UI.el("div", { class: "pqueue-card__title-wrap" }); if (icon) titleWrap.appendChild(UI.icon(icon, { size: "md" })); titleWrap.appendChild(UI.el("h3", { class: "pqueue-card__title", text: title })); if (subtitle) titleWrap.appendChild(UI.el("span", { class: "pqueue-card__subtitle", text: subtitle })); head.push(titleWrap); if (actions && actions.length) head.push(UI.el("div", { class: "pqueue-card__actions" }, actions)); const body = Array.isArray(content) ? content : [content]; return UI.el("section", { class: ["pqueue-card", classes].filter(Boolean) }, [UI.el("header", { class: "pqueue-card__header" }, head), UI.el("div", { class: "pqueue-card__body" }, body)]); }; UI.buildPreviewUrl = function buildPreviewUrl(imgDesc, pid) { const url = new URL("/api/pqueue/preview", window.location.origin); url.searchParams.set("filename", imgDesc.filename); url.searchParams.set("type", imgDesc.type); if (imgDesc.subfolder) url.searchParams.set("subfolder", imgDesc.subfolder); url.searchParams.set("preview", "webp;50"); if (pid) url.searchParams.set("pid", pid); return url; }; UI.getActiveWorkflowName = function getActiveWorkflowName() { try { const app = window.app; const activeTab = app?.tabManager?.getActive?.() || app?.tabManager?.activeTab || app?.workspace?.activeTab; const fromTab = activeTab?.title || activeTab?.label || activeTab?.name; const fromProject = (typeof app?.projectManager?.getCurrentProject === 'function' ? app.projectManager.getCurrentProject()?.name : null) || app?.projectManager?.currentProject?.name; const g = app?.graph; const fromGraph = (g?.extra && g.extra.workflow && g.extra.workflow.name) || (g?.workflow && g.workflow.name) || g?.title; let raw = fromTab || fromProject || fromGraph; // DOM fallbacks: prefer exact selectors from current ComfyUI frontend if (!raw) { try { const sel = [ // Breadcrumb active item label '#comfyui-body-top .subgraph-breadcrumb .active-breadcrumb-item .p-breadcrumb-item-label', '.subgraph-breadcrumb .active-breadcrumb-item .p-breadcrumb-item-label', '.subgraph-breadcrumb .p-breadcrumb-item .p-breadcrumb-item-label', // Workflow tabs: active togglebutton carries checked/pressed states '#comfyui-body-top .workflow-tabs .p-togglebutton-checked .workflow-label', '.workflow-tabs .p-togglebutton-checked .workflow-label', '#comfyui-body-top .workflow-tabs [data-p-checked="true"] .workflow-label', '.workflow-tabs [data-p-checked="true"] .workflow-label', '#comfyui-body-top .workflow-tabs .p-togglebutton[aria-pressed="true"] .workflow-label', '.workflow-tabs .p-togglebutton[aria-pressed="true"] .workflow-label', ]; for (const s of sel) { const n = document.querySelector(s); if (n && typeof n.textContent === 'string' && n.textContent.trim()) { raw = n.textContent; break; } } } catch (err) {} } // Broader generic fallbacks as a last resort if (!raw) { try { const candidates = []; candidates.push(document.querySelector('.p-tabview-nav li.p-highlight .p-tabview-title')); candidates.push(document.querySelector('[role="tab"][aria-selected="true"]')); candidates.push(document.querySelector('.workflow-tab.is-active .workflow-label')); candidates.push(document.querySelector('.workflow-tab[aria-selected="true"] .workflow-label')); candidates.push(document.querySelector('[data-test-id="workflow-tab"],[data-testid="workflow-tab"]')); const node = candidates.find((n) => n && typeof n.textContent === 'string'); raw = node ? node.textContent : null; } catch (err) {} } if (!raw) return null; const name = String(raw).replace(/^[*•]+\s*/, '').trim(); return name || null; } catch (err) { return null; } }; UI.deriveWorkflowLabel = function deriveWorkflowLabel(item, dbRow) { try { const prompt = item?.[2]; if (prompt && typeof prompt === "object") { const name = prompt?.workflow?.name || prompt?.workflow?.title || prompt?.name; if (typeof name === "string" && name.trim()) return name.trim(); } if (dbRow?.workflow) { try { const parsed = JSON.parse(dbRow.workflow); const name = parsed?.workflow?.name || parsed?.workflow?.title || parsed?.name; if (typeof name === "string" && name.trim()) return name.trim(); } catch (err) { /* ignore */ } } } catch (err) { /* ignore */ } return "Untitled workflow"; }; UI.buildWorkflowLabel = function buildWorkflowLabel(pid, item, dbRow) { let label = state.workflowNameCache.get(pid); if (!label) { label = UI.deriveWorkflowLabel(item, dbRow); state.workflowNameCache.set(pid, label); } return UI.el("span", { class: "pqueue-row__label", text: label }); }; window.PQueue = window.PQueue || {}; window.PQueue.UI = UI; window.UI = UI; })();