(function () { "use strict"; // UI module extracted from original pqueue.js without logic changes 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 = { 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; }, 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; }, 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; }, 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; }, 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; }, 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; }, ensureAssets() { if (!document.getElementById("pqueue-style")) { const link = document.createElement("link"); link.id = "pqueue-style"; link.rel = "stylesheet"; link.href = "/extensions/ComfyUI-PersistentQueue/css/queue_style.css"; document.head.appendChild(link); } if (!document.querySelector("link[data-pqueue-icons]")) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = "/extensions/ComfyUI-PersistentQueue/lib/tabler-icons.min.css"; link.dataset.pqueueIcons = "true"; document.head.appendChild(link); } }, // Compute a robust numeric key for history ordering 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; } }, // Composite key: [ms, id] for strict total ordering historyKeyParts(row) { const ms = UI.historyKey(row) || 0; const id = Number(row?.id) || 0; return [ms, id]; }, // Find and cache the real scroll container (ComfyUI sidebar) 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; } }, // Choose the history card nearest the top of the viewport as the scroll anchor 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; } }, // Keep an anchor element visually stationary while mutating DOM 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; } }, render() { if (!state.container) return; UI.ensureAssets(); const prevDom = state.dom; let _prevScrollTop = 0; let _prevAnchorOffset = null; let _prevScroller = null; const findScrollContainer = (node) => { try { let el = node; while (el && el !== document.body && el !== document.documentElement) { const style = window.getComputedStyle(el); const oy = style.overflowY; if ((oy === 'auto' || oy === 'scroll') && el.scrollHeight > el.clientHeight) return el; el = el.parentElement; } return document.scrollingElement || document.documentElement || document.body; } catch (err) { return document.scrollingElement || document.documentElement || document.body; } }; try { _prevScroller = findScrollContainer(state.container); if (_prevScroller) { _prevScrollTop = _prevScroller.scrollTop || 0; const prevCard = (prevDom && prevDom.historyCard) ? prevDom.historyCard : state.container.querySelector('.pqueue-card'); if (prevCard) { const scRect = _prevScroller.getBoundingClientRect(); const cardRect = prevCard.getBoundingClientRect(); _prevAnchorOffset = cardRect.top - scRect.top; } } } catch (err) { /* noop */ } const dom = (state.dom = {}); state.container.innerHTML = ""; const root = UI.el("div", { class: "pqueue-root" }); dom.root = root; root.appendChild(UI.renderToolbar()); const sections = UI.el("div", { class: "pqueue-sections" }); const metrics = UI.renderMetrics(); if (metrics) sections.appendChild(metrics); sections.appendChild(UI.renderRunning()); sections.appendChild(UI.renderPending()); sections.appendChild(UI.renderHistory()); root.appendChild(sections); state.container.appendChild(root); dom.sections = sections; requestAnimationFrame(() => { try { const scroller = _prevScroller || findScrollContainer(state.container); if (!scroller) return; const newCard = (state.dom && state.dom.historyCard) ? state.dom.historyCard : state.container.querySelector('.pqueue-card'); if (_prevAnchorOffset != null && newCard) { const scRect2 = scroller.getBoundingClientRect(); const cardRect2 = newCard.getBoundingClientRect(); const newOffset = cardRect2.top - scRect2.top; scroller.scrollTop = Math.max(0, _prevScrollTop + (newOffset - _prevAnchorOffset)); } else { scroller.scrollTop = _prevScrollTop; } } catch (err) { /* noop */ } }); Events.bind(); UI.updateProgressBars(); UI.refreshFilter(); UI.updateSelectionUI(); UI.updateToolbarStatus(); }, renderToolbar() { const pauseBtn = UI.button({ id: "pqueue-toggle", icon: state.paused ? "ti ti-player-play-filled" : "ti ti-player-pause-filled", ariaLabel: state.paused ? "Resume queue" : "Pause queue", title: state.paused ? "Resume queue" : "Pause queue", variant: state.paused ? "success" : "warning", subtle: true, }); pauseBtn.addEventListener('click', Events.togglePause); const clearBtn = UI.button({ id: "pqueue-clear", icon: "ti ti-player-stop", ariaLabel: "Clear pending", title: "Clear pending jobs", variant: "danger", subtle: true, }); clearBtn.addEventListener('click', Events.clearPending); return UI.el("div", { class: "pqueue-toolbar" }, [ UI.el("div", { class: "pqueue-toolbar__row" }, [ UI.el("div", { class: "pqueue-toolbar__group" }, [pauseBtn, clearBtn]), ]), ]); }, renderMetrics() { const m = state.metrics; if (!m) return null; const tiles = [ UI.metricTile({ icon: "ti ti-stack-2", label: "In queue", value: String(m.queueCount), caption: `${m.runningCount} running • ${m.persistedCount} persisted`, variant: m.queueCount ? "primary" : "neutral", }), UI.metricTile({ icon: "ti ti-chart-pie", label: "Success rate", value: m.successRate != null ? `${Math.round(m.successRate * 100)}%` : "—", caption: m.failureCount ? `${m.failureCount} recent failure${m.failureCount === 1 ? "" : "s"}` : "No recent failures", variant: m.successRate != null && m.successRate >= 0.75 ? "success" : "neutral", }), UI.metricTile({ icon: "ti ti-clock-hour-3", label: "Avg duration", value: m.avgDuration ? Format.duration(m.avgDuration) : "—", caption: "Calculated from history", variant: "neutral", }), UI.metricTile({ icon: "ti ti-hourglass-high", label: "Est. total duration", value: m.estimatedTotalDuration ? Format.duration(m.estimatedTotalDuration) : "—", caption: [ m.estimatedRunningDuration ? `~${Format.duration(m.estimatedRunningDuration)} running` : null, m.estimatedPendingDuration ? `~${Format.duration(m.estimatedPendingDuration)} pending` : null, ].filter(Boolean).join(" • ") || (m.queueCount ? `${m.queueCount} pending item${m.queueCount === 1 ? "" : "s"}` : "No pending items"), variant: m.estimatedTotalDuration ? "neutral" : "neutral", tooltip: "Sum of per-workflow averages for running (remaining) and pending items. Per-workflow averages are computed from recent history; if unavailable, falls back to global average duration.", }), ]; const wrap = UI.el("div", { class: "pqueue-metrics" }, tiles); state.dom.metrics = wrap; return wrap; }, renderRunning() { const rows = []; if (!state.queue_running.length) { const placeholder = UI.el("div", { class: "pqueue-item pqueue-item--running", style: "display:flex;align-items:center;justify-content:center;text-align:center;" }, [ UI.el("span", { class: "pqueue-muted", text: "no running job" }) ]); rows.push(placeholder); } else { state.queue_running.forEach((item, index) => { const pid = item[1]; const fraction = Math.max(0, Math.min(1, Number(state.running_progress?.[pid]) || 0)); const workflowName = state.workflowNameCache.get(pid) || pid; const meta = UI.el("div", { class: "pqueue-item__meta" }, [ UI.icon("ti ti-loader-2", { spin: true }), UI.el("span", { class: "pqueue-chip pqueue-chip--primary", text: `#${index + 1}` }), UI.el("span", { class: "pqueue-row__label", text: workflowName }), UI.el("span", { class: "pqueue-progress__label", text: Format.percent(fraction) }), ]); const bar = UI.el("div", { class: "pqueue-progress-bar" }); bar.style.width = `${(fraction * 100).toFixed(1)}%`; const progress = UI.el("div", { class: "pqueue-progress" }, [UI.el("div", { class: "pqueue-progress__track" }, [bar])]); rows.push(UI.el("div", { class: "pqueue-item pqueue-item--running", "data-id": pid }, [meta, progress])); }); } const card = UI.card("Currently running", { icon: "ti ti-activity-heartbeat", subtitle: state.metrics.runningCount ? `${state.metrics.runningCount} active` : "", content: rows, classes: "pqueue-card--running", }); state.dom.runningCard = card; return card; }, renderPending() { const wrapper = UI.el("div", { class: "pqueue-table-wrapper" }); const list = UI.el("div", { class: "pqueue-list", id: "pqueue-pending" }); state.dom.pendingTable = list; const filter = UI.el("input", { id: "pqueue-filter", class: "pqueue-input", type: "search", placeholder: "Filter pending by prompt, status, error…", value: state.filters.pending, spellcheck: "false", }); state.dom.filterInput = filter; const listHeader = UI.el("div", { class: "pqueue-list__header" }, [ UI.el("label", { class: "pqueue-list__selectall" }, [ UI.el("input", { type: "checkbox", class: "pqueue-checkbox pqueue-select-all", title: "Select all visible" }), UI.el("span", { class: "pqueue-table__heading", text: "Select all" }) ]) ]); list.appendChild(listHeader); const rows = state.queue_pending.map(UI.pendingRow).filter(Boolean); if (!rows.length) { rows.push(UI.emptyState({ icon: "ti ti-stack-2", title: "Queue empty", description: "Enqueue prompts in ComfyUI to populate the persistent queue.", })); } rows.forEach((row) => list.appendChild(row)); wrapper.appendChild(list); const footerCount = UI.el("span", { class: "pqueue-muted" }); const footerUpdated = UI.el("span", { class: "pqueue-muted" }); state.dom.pendingCount = footerCount; state.dom.pendingUpdated = footerUpdated; const deleteSelected = UI.button({ id: "pqueue-delete-selected", icon: "ti ti-trash", variant: "danger", subtle: true, disabled: state.selectedPending.size === 0, badge: state.selectedPending.size || null, }); state.dom.deleteSelectedBtn = deleteSelected; const footer = UI.el("div", { class: "pqueue-table__footer" }, [ UI.el("div", { class: "pqueue-table__footer-left" }, [footerCount, footerUpdated]), UI.el("div", { class: "pqueue-table__footer-right" }, [deleteSelected]), ]); const card = UI.card("Queue", { icon: "ti ti-stack-front", subtitle: state.metrics.queueCount ? `${state.metrics.queueCount} pending` : null, actions: [filter], content: [wrapper, footer], }); state.dom.pendingCard = card; return card; }, pendingRow(item) { if (!Array.isArray(item)) return null; const pid = String(item[1] ?? ""); const db = state.dbIndex.get(pid) || {}; const selected = state.selectedPending.has(pid); const row = UI.el("article", { class: ["pqueue-row", selected ? "is-selected" : null], "data-id": pid, draggable: "true" }); const checkbox = UI.el("input", { type: "checkbox", class: "pqueue-checkbox pqueue-select" }); checkbox.checked = selected; const status = UI.statusBadge(db.status || "pending", { subtle: true }); const primaryLabel = UI.buildWorkflowLabel(pid, item, db); primaryLabel.contentEditable = "true"; primaryLabel.spellcheck = false; primaryLabel.dataset.id = pid; primaryLabel.dataset.original = primaryLabel.textContent || ""; primaryLabel.classList.add("pqueue-editable"); const finishEdit = async (node, save) => { try { const id = node?.dataset?.id; if (!id) return; const original = node.dataset.original || ""; const current = (node.textContent || "").trim(); if (!save) { node.textContent = original; return; } if (current === original || !current) return; await API.rename(id, current); state.workflowNameCache.set(id, current); setStatusMessage("Name updated"); } catch (err) { console.error("pqueue: rename failed", err); state.error = err?.message || "Failed to rename"; UI.updateToolbarStatus(); } finally { node.dataset.original = node.textContent || ""; } }; primaryLabel.addEventListener("keydown", async (e) => { e.stopPropagation(); const target = e.currentTarget; if (e.key === "Enter") { e.preventDefault(); await finishEdit(target, true); target.blur(); } else if (e.key === "Escape") { e.preventDefault(); await finishEdit(target, false); target.blur(); } }); primaryLabel.addEventListener("blur", async (e) => { await finishEdit(e.currentTarget, true); }); const line1 = UI.el("div", { class: "pqueue-pcard__line1" }, [ UI.el("div", { class: "pqueue-pcard__l1-left" }, [checkbox, primaryLabel]), UI.el("div", { class: "pqueue-pcard__l1-right" }, [UI.el("span", { class: "pqueue-muted", text: db.created_at ? Format.relative(db.created_at) : "—" })]) ]); const estimateSeconds = UI.estimateDuration(pid, item[2]); const estimateLabel = estimateSeconds ? `Est. ${Format.duration(estimateSeconds)}` : ""; const dateLabel = db.created_at ? new Date(db.created_at).toLocaleDateString() : ""; const timeLabel = db.created_at ? new Date(db.created_at).toLocaleTimeString() : ""; const deleteBtn = UI.button({ icon: "ti ti-trash", variant: "danger", subtle: true, title: "Delete" }); deleteBtn.dataset.action = "delete"; deleteBtn.dataset.id = pid; const moveTopBtn = UI.button({ icon: "ti ti-arrow-bar-to-up", variant: "ghost", subtle: true, title: "Move to top" }); moveTopBtn.dataset.action = "move-top"; moveTopBtn.dataset.id = pid; const moveBottomBtn = UI.button({ icon: "ti ti-arrow-bar-to-down", variant: "ghost", subtle: true, title: "Move to bottom" }); moveBottomBtn.dataset.action = "move-bottom"; moveBottomBtn.dataset.id = pid; const actionsWrap = UI.el("div", { class: "pqueue-actions-group" }, [deleteBtn, moveTopBtn, moveBottomBtn, UI.icon("ti ti-drag-drop", { size: "sm" })]); const line2 = UI.el("div", { class: "pqueue-pcard__line2" }, [ UI.el("span", { class: "pqueue-muted", text: estimateLabel }), UI.el("span", { class: "pqueue-muted", text: dateLabel }), UI.el("span", { class: "pqueue-muted", text: timeLabel }), actionsWrap, ]); const statusLine = UI.el("div", { class: "pqueue-pcard__status" }, [status]); if (db.error) { const err = UI.icon("ti ti-alert-triangle", { size: "sm" }); err.classList.add("pqueue-row__error"); err.title = db.error; statusLine.appendChild(err); } row.appendChild(line1); row.appendChild(line2); row.appendChild(statusLine); row.dataset.search = [state.workflowNameCache.get(pid) || "", db.status, db.error, db.created_at].filter(Boolean).join(" ").toLowerCase(); return row; }, renderHistory() { const grid = UI.el("div", { class: "pqueue-history" }); state.dom.historyGrid = grid; if (!state.history.length) { grid.appendChild(UI.emptyState({ icon: "ti ti-photo-off", title: "History empty", description: "Completed prompts will show their thumbnails here.", })); } else { state.history.forEach((row) => grid.appendChild(UI.historyCard(row))); } const sentinel = UI.historySentinel(); state.dom.historySentinel = sentinel; grid.appendChild(sentinel); const count = (state.historyTotal != null) ? state.historyTotal : state.metrics.historyCount; const base = (count != null) ? `${count} entries` : null; const range = UI.currentHistoryRangeLabel(); const subtitle = [base, range].filter(Boolean).join(' • '); const card = UI.card("History", { icon: "ti ti-clock-bolt", subtitle, actions: UI.historyFilters(), content: grid, }); state.dom.historyCard = card; state.dom.historySubtitle = card.querySelector('.pqueue-card__subtitle'); try { UI.ensureThumbObserver(); } catch (err) { /* noop */ } return card; }, updateAfterRefresh(paged) { try { UI.updateToolbarControls(); UI.updateMetrics(); UI.updateRunningSection(); UI.updatePendingSection(); UI.reconcileHistoryFromState(); UI.updateHistorySubtitle(); } catch (err) { try { UI.ensureHistoryObserver(); } catch (e) { /* noop */ } } }, reconcileHistoryFromState() { try { const grid = state.dom.historyGrid; const sentinel = state.dom.historySentinel; if (!grid || !sentinel) return; const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; UI.withStableAnchor(() => { try { Array.from(grid.querySelectorAll('.pqueue-empty')).forEach((n) => n.remove()); } catch (err) { /* noop */ } const existingIds = new Set(Array.from(grid.querySelectorAll('.pqueue-history-card')).map((n) => n.getAttribute('data-id'))); const ordered = Array.isArray(state.history) ? state.history.slice() : []; try { ordered.sort((a, b) => { 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) { /* noop */ } for (const row of ordered) { const id = row?.id != null ? String(row.id) : null; if (!id || existingIds.has(id)) continue; const card = UI.historyCard(row); const key = Number(card.getAttribute('data-key') || 0); const key2 = Number(card.getAttribute('data-key2') || 0); const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card')); let inserted = false; if (dir === 'desc') { for (const existing of existingCards) { const otherKey = Number(existing.getAttribute('data-key') || 0); const otherKey2 = Number(existing.getAttribute('data-key2') || 0); if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; } } if (!inserted) grid.insertBefore(card, sentinel); } else { for (let i = existingCards.length - 1; i >= 0; i--) { const existing = existingCards[i]; const otherKey = Number(existing.getAttribute('data-key') || 0); const otherKey2 = Number(existing.getAttribute('data-key2') || 0); if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; } } if (!inserted) { const firstCard = existingCards[0] || grid.querySelector('.pqueue-history-card'); grid.insertBefore(card, firstCard || sentinel); } } existingIds.add(id); } try { state.historyIds = new Set(Array.from(grid.querySelectorAll('.pqueue-history-card')).map((n) => Number(n.getAttribute('data-id'))).filter((v) => v != null)); } catch (err) { /* noop */ } }); try { UI.ensureThumbObserver(); } catch (err) { /* noop */ } } catch (err) { /* noop */ } }, updateToolbarControls() { try { const pauseBtn = document.getElementById('pqueue-toggle'); if (pauseBtn) { const isPaused = !!state.paused; pauseBtn.title = isPaused ? 'Resume queue' : 'Pause queue'; pauseBtn.setAttribute('aria-label', isPaused ? 'Resume queue' : 'Pause queue'); pauseBtn.classList.toggle('pqueue-button--success', isPaused); pauseBtn.classList.toggle('pqueue-button--warning', !isPaused); pauseBtn.innerHTML = ''; pauseBtn.appendChild(UI.icon(isPaused ? 'ti ti-player-play-filled' : 'ti ti-player-pause-filled')); pauseBtn.removeEventListener('click', Events.togglePause); pauseBtn.addEventListener('click', Events.togglePause); } } catch (err) { /* noop */ } }, updateMetrics() { try { const newMetrics = UI.renderMetrics(); if (!newMetrics) return; const old = state.dom.metrics; if (old && old.parentNode) { old.parentNode.replaceChild(newMetrics, old); state.dom.metrics = newMetrics; } } catch (err) { /* noop */ } }, updateRunningSection() { try { UI.withStableAnchor(() => { const card = state.dom.runningCard; if (!card) return; const body = card.querySelector('.pqueue-card__body'); if (!body) return; const rect = body.getBoundingClientRect(); const beforeHeight = Math.max(0, (rect && rect.height) || 0); body.innerHTML = ''; if (!state.queue_running.length) { if (beforeHeight) body.style.minHeight = `${beforeHeight}px`; const placeholder = UI.el('div', { class: 'pqueue-item pqueue-item--running', style: 'display:flex;align-items:center;justify-content:center;text-align:center;' }, [ UI.el('span', { class: 'pqueue-muted', text: 'no running job' }) ]); body.appendChild(placeholder); } else { try { body.style.minHeight = ''; } catch (err) { /* noop */ } state.queue_running.forEach((item, index) => { const pid = item[1]; const fraction = Math.max(0, Math.min(1, Number(state.running_progress?.[pid]) || 0)); const workflowName = state.workflowNameCache.get(pid) || pid; const meta = UI.el('div', { class: 'pqueue-item__meta' }, [ UI.icon('ti ti-loader-2', { spin: true }), UI.el('span', { class: 'pqueue-chip pqueue-chip--primary', text: `#${index + 1}` }), UI.el('span', { class: 'pqueue-row__label', text: workflowName }), UI.el('span', { class: 'pqueue-progress__label', text: Format.percent(fraction) }), ]); const bar = UI.el('div', { class: 'pqueue-progress-bar' }); bar.style.width = `${(fraction * 100).toFixed(1)}%`; const progress = UI.el('div', { class: 'pqueue-progress' }, [UI.el('div', { class: 'pqueue-progress__track' }, [bar])]); body.appendChild(UI.el('div', { class: 'pqueue-item pqueue-item--running', 'data-id': pid }, [meta, progress])); }); } const subtitle = card.querySelector('.pqueue-card__subtitle'); if (subtitle) subtitle.textContent = state.metrics.runningCount ? `${state.metrics.runningCount} active` : ''; }); } catch (err) { /* noop */ } }, updatePendingSection() { try { UI.withStableAnchor(() => { const card = state.dom.pendingCard; if (!card) return; const subtitle = card.querySelector('.pqueue-card__subtitle'); if (subtitle) subtitle.textContent = state.metrics.queueCount ? `${state.metrics.queueCount} pending` : ''; const table = state.dom.pendingTable; if (!table) return; Array.from(table.querySelectorAll('.pqueue-row[data-id]')).forEach((n) => n.remove()); const rows = state.queue_pending.map(UI.pendingRow).filter(Boolean); const header = table.querySelector('.pqueue-list__header'); const frag = document.createDocumentFragment(); rows.forEach((r) => frag.appendChild(r)); if (header && header.nextSibling) { table.insertBefore(frag, header.nextSibling); } else { table.appendChild(frag); } UI.refreshFilter(); UI.updateSelectionUI(); UI.updatePendingFooter(Array.from(table.querySelectorAll('.pqueue-row[data-id]')).filter((r) => r.style.display !== 'none').length); }); } catch (err) { /* noop */ } }, mergeHistoryFromRefresh(paged) { try { const grid = state.dom.historyGrid; const sentinel = state.dom.historySentinel; if (!grid || !sentinel) return; let list = Array.isArray(paged?.history) ? paged.history : []; try { const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; list = list.slice().sort((a, b) => { 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) { /* noop */ } if (!state.historyIds) state.historyIds = new Set(); UI.withStableAnchor(() => { try { Array.from(grid.querySelectorAll('.pqueue-empty')).forEach((n) => n.remove()); } catch (err) { /* noop */ } for (const row of list) { const id = row?.id; if (id == null || state.historyIds.has(id)) continue; state.historyIds.add(id); const card = UI.historyCard(row); const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; const key = Number(card.getAttribute('data-key') || 0); const key2 = Number(card.getAttribute('data-key2') || 0); const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card')); let inserted = false; if (dir === 'desc') { for (const existing of existingCards) { const otherKey = Number(existing.getAttribute('data-key') || 0); const otherKey2 = Number(existing.getAttribute('data-key2') || 0); if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; } } if (!inserted) grid.insertBefore(card, sentinel); } else { for (let i = existingCards.length - 1; i >= 0; i--) { const existing = existingCards[i]; const otherKey = Number(existing.getAttribute('data-key') || 0); const otherKey2 = Number(existing.getAttribute('data-key2') || 0); if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; } } if (!inserted) grid.insertBefore(card, sentinel); } if (Array.isArray(state.history)) { if (dir === 'desc') state.history.unshift(row); else state.history.push(row); } } }); if (typeof paged?.total === 'number') state.historyTotal = paged.total; state.historyPaging = state.historyPaging || { isLoading: false, hasMore: true, nextCursor: null, params: { sort_by: 'id', sort_dir: 'desc', limit: 60 } }; state.historyPaging.nextCursor = paged?.next_cursor || state.historyPaging.nextCursor; state.historyPaging.hasMore = !!paged?.has_more || state.historyPaging.hasMore; if (!state.historyPaging.hasMore && sentinel) sentinel.style.display = 'none'; try { UI.ensureThumbObserver(); } catch (err) { /* noop */ } } catch (err) { /* noop */ } }, currentHistoryRangeLabel() { try { const sDate = state.filters?.historySince || ""; const sTime = state.filters?.historySinceTime || ""; const uDate = state.filters?.historyUntil || ""; const uTime = state.filters?.historyUntilTime || ""; if (!sDate && !uDate) return ""; if (state.historyPreset === 'today') return 'Today'; if (state.historyPreset === '24h') return 'Last 24h'; if (state.historyPreset === '7d') return 'Last 7 days'; const compact = (d, t, end) => { if (!d) return ''; const parts = d.split('-').map((x) => parseInt(x, 10)); let hh = end ? 23 : 0, mm = end ? 59 : 0; if (t && /^\d{2}:\d{2}/.test(t)) { const tt = t.split(':').map((x) => parseInt(x, 10)); hh = tt[0] ?? hh; mm = tt[1] ?? mm; } const pad = (n) => String(n).padStart(2, '0'); return `${parts[0]}-${pad(parts[1])}-${pad(parts[2])} ${pad(hh)}:${pad(mm)}`; }; const left = compact(sDate, sTime, false); const right = compact(uDate, uTime, true); if (left && right) return `${left} – ${right}`; if (left) return `Since ${left}`; if (right) return `Until ${right}`; return ""; } catch (err) { return ""; } }, updateHistorySubtitle() { try { const el = state.dom.historySubtitle; if (!el) return; const count = (state.historyTotal != null) ? state.historyTotal : state.metrics?.historyCount; const base = (count != null) ? `${count} entries` : null; const range = UI.currentHistoryRangeLabel(); const text = [base, range].filter(Boolean).join(' • '); el.textContent = text; } catch (err) { /* noop */ } }, historyFilters() { const filtersBtn = UI.button({ icon: "ti ti-filter", variant: "ghost", subtle: true, title: "Filters" }); const sortToggle = UI.sortToggleButton(); filtersBtn.addEventListener('click', (e) => { e.stopPropagation(); Events.toggleFiltersPopover(e.currentTarget); }); return [filtersBtn, sortToggle, UI.clearFiltersIcon()]; }, clearFiltersIcon() { const btn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Clear filters" }); btn.addEventListener('click', () => Events.applyHistoryPreset('clear')); return btn; }, sortToggleButton() { const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down'; const title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)'; const btn = UI.button({ icon, variant: 'ghost', subtle: true, title }); btn.addEventListener('click', () => { const current = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; const next = current === 'asc' ? 'desc' : 'asc'; Events.setHistorySort(next); UI.updateSortToggle(); }); state.dom.sortToggleBtn = btn; return btn; }, updateSortToggle() { try { const btn = state.dom.sortToggleBtn; if (!btn) return; const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc'; const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down'; btn.innerHTML = ''; btn.appendChild(UI.icon(icon)); btn.title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)'; } catch (err) { /* noop */ } }, historySentinel() { const wrap = UI.el("div", { class: "pqueue-history-sentinel" }); const spinner = UI.icon("ti ti-loader-2", { size: "md", spin: true }); wrap.appendChild(spinner); return wrap; }, historyCard(row) { const attrs = { class: "pqueue-history-card", title: Format.tooltip(row) }; if (row && row.id != null) attrs["data-id"] = String(row.id); const ts = row && (row.completed_at || row.created_at); if (ts != null) attrs["data-ts"] = String(ts); const [ms, idPart] = UI.historyKeyParts(row); attrs["data-key"] = String(ms); attrs["data-key2"] = String(idPart); const card = UI.el("article", attrs); const header = UI.el("div", { class: "pqueue-history-card__header" }, [UI.statusBadge(row.status || "success"), UI.el("span", { class: "pqueue-history-card__time", text: row.completed_at ? Format.relative(row.completed_at) : Format.relative(row.created_at) })]); const fileLabel = UI.historyPrimaryFilename(row); const meta = UI.el("div", { class: "pqueue-history-card__meta" }, [UI.el("span", { class: "pqueue-code", text: fileLabel }), UI.el("span", { class: "pqueue-history-card__duration", text: Format.duration(Number(row.duration_seconds)) || "—" })]); const thumbs = UI.renderThumbs(row); card.appendChild(header); card.appendChild(meta); card.appendChild(thumbs); return card; }, renderThumbs(row) { const container = UI.el("div", { class: "pqueue-thumb-row" }); const placeholderMeta = UI.thumbPlaceholder(row); const ensurePlaceholder = () => { const wrap = UI.el("div", { class: ["pqueue-thumb-wrap", "pqueue-thumb-wrap-placeholder", placeholderMeta.variant ? `pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}` : null] }); wrap.appendChild(UI.thumbPlaceholderNode(placeholderMeta)); container.appendChild(wrap); }; const attachFallback = (wrap, img, images) => { img.addEventListener( "error", () => { wrap.classList.add("pqueue-thumb-wrap-placeholder"); if (placeholderMeta.variant) wrap.classList.add(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`); wrap.replaceChildren(UI.thumbPlaceholderNode(placeholderMeta)); if (!images.length) wrap.onclick = null; }, { once: true } ); }; if (row.id) { const galleryImages = UI.extractImages(row); const url = new URL(`/api/pqueue/history/thumb/${row.id}`, window.location.origin); url.searchParams.set("_", String(Date.now())); const wrap = UI.el("div", { class: "pqueue-thumb-wrap" }); const img = UI.el("img", { class: "pqueue-thumb", src: url.href, title: `history-${row.id}`, loading: "lazy", decoding: "async", fetchpriority: "low" }); attachFallback(wrap, img, galleryImages); wrap.appendChild(img); const count = UI.countImages(row); if (count > 1) wrap.appendChild(UI.el("div", { class: "pqueue-thumb-badge", text: `${count}` })); if (galleryImages.length) wrap.onclick = () => UI.openGallery(galleryImages, 0, row.prompt_id); container.appendChild(wrap); } if (!container.children.length) { const images = UI.extractImages(row); if (images.length) { const first = images[0]; const url = UI.buildPreviewUrl(first, row.prompt_id); const wrap = UI.el("div", { class: "pqueue-thumb-wrap" }); const img = UI.el("img", { class: "pqueue-thumb", src: url.href, title: first.filename, loading: "lazy", decoding: "async", fetchpriority: "low" }); attachFallback(wrap, img, images); wrap.appendChild(img); if (images.length > 1) wrap.appendChild(UI.el("div", { class: "pqueue-thumb-badge", text: `${images.length}` })); wrap.onclick = () => UI.openGallery(images, 0, row.prompt_id); container.appendChild(wrap); } } if (!container.children.length) ensurePlaceholder(); return container; }, thumbPlaceholder(row) { const status = String(row?.status ?? "").toLowerCase(); if (["interrupted", "cancelled", "canceled", "stopped"].includes(status)) { return { text: "Interrupted", icon: "ti ti-player-stop", variant: "interrupted" }; } if (["error", "failed", "failure"].includes(status)) { return { text: "Failed", icon: "ti ti-alert-triangle", variant: "failed" }; } return { text: "Preview unavailable", icon: "ti ti-photo-off", variant: "empty" }; }, thumbPlaceholderNode(meta) { return UI.el("div", { class: ["pqueue-thumb-placeholder", meta.variant ? `pqueue-thumb-placeholder--${meta.variant}` : null] }, [UI.icon(meta.icon), UI.el("span", { class: "pqueue-thumb-placeholder__text", text: meta.text })]); }, 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; }, updateProgressBars() { if (!state.dom.root) return; Object.entries(state.running_progress).forEach(([pid, frac]) => { const bar = state.dom.root.querySelector(`.pqueue-item[data-id="${pid}"] .pqueue-progress-bar`); if (bar) bar.style.width = `${(Number(frac) * 100).toFixed(1)}%`; }); }, ensureHistoryObserver() { try { if (!('IntersectionObserver' in window)) return; const grid = state.dom.historyGrid; const sentinel = state.dom.historySentinel; if (!grid || !sentinel) return; if (state.historyObserver) { state.historyObserver.disconnect(); state.historyObserver = null; } if (!state.historyPaging?.nextCursor && state.history?.length) { const last = state.history[state.history.length - 1]; state.historyPaging.nextCursor = { id: last?.id, value: last?.id }; state.historyPaging.hasMore = true; } const scroller = UI.getScrollContainer(); const rootIsAncestor = (el, ancestor) => { try { if (!ancestor || !(ancestor instanceof Element)) return false; return ancestor.contains(el); } catch (err) { return false; } }; const root = rootIsAncestor(sentinel, scroller) ? scroller : null; const obs = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; Events.loadMoreHistory(); }); }, { root: root, rootMargin: '360px 0px', threshold: 0 }); obs.observe(sentinel); state.historyObserver = obs; } catch (err) { /* noop */ } }, ensureThumbObserver() { try { if (!('IntersectionObserver' in window)) return; const grid = state.dom.historyGrid; if (!grid) return; if (state.thumbObserver) { try { state.thumbObserver.disconnect(); } catch (e) {} state.thumbObserver = null; } const scroller = UI.getScrollContainer(); const rootIsAncestor = (el, ancestor) => { try { return ancestor instanceof Element ? ancestor.contains(el) : false; } catch (err) { return false; } }; const root = rootIsAncestor(grid, scroller) ? scroller : null; const ric = window.requestIdleCallback || ((cb) => setTimeout(cb, 0)); const obs = new IntersectionObserver((entries) => { entries.forEach((entry) => { const img = entry.target; if (!(img instanceof HTMLImageElement)) return; if (entry.isIntersecting) { try { img.setAttribute('fetchpriority', 'high'); } catch (err) {} try { img.decoding = 'async'; } catch (err) {} try { ric(() => { try { img.decode().catch(() => {}); } catch (e) {} }); } catch (err) {} } else { try { img.setAttribute('fetchpriority', 'low'); } catch (err) {} } }); }, { root, rootMargin: '300px 0px', threshold: 0 }); const imgs = Array.from(grid.querySelectorAll('.pqueue-thumb')); imgs.forEach((img) => { try { obs.observe(img); } catch (err) {} }); state.thumbObserver = obs; } catch (err) { /* noop */ } }, buildFiltersPopover(anchor) { const since = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historySince || "", id: "pqueue-since-date" }); const sinceTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historySinceTime || "", id: "pqueue-since-time", step: "1" }); const until = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historyUntil || "", id: "pqueue-until-date" }); const untilTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historyUntilTime || "", id: "pqueue-until-time", step: "1" }); const onChange = () => Events.updateHistoryDateFilters(since.value, until.value, sinceTime.value, untilTime.value); [since, sinceTime, until, untilTime].forEach((el) => el.addEventListener("change", onChange)); state.dom.historySince = since; state.dom.historySinceTime = sinceTime; state.dom.historyUntil = until; state.dom.historyUntilTime = untilTime; const dates = UI.el("div", { class: "pqueue-popover__grid" }, [ UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Since date" }), since]), UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Since time" }), sinceTime]), UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Until date" }), until]), UI.el("label", { class: "pqueue-field" }, [UI.el("span", { class: "pqueue-field__label", text: "Until time" }), untilTime]), ]); const presets = UI.el("div", { class: "pqueue-popover__section pqueue-popover__presets" }, [ UI.button({ text: "Today", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("today") }), UI.button({ text: "Last 24h", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("24h") }), UI.button({ text: "Last 7d", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("7d") }), UI.button({ text: "Clear", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("clear") }), ]); const header = UI.el('div', { class: 'pqueue-popover__header pqueue-card__header' }, [ UI.el('div', { class: 'pqueue-popover__title', text: 'History filters' }), UI.el('div', { class: 'pqueue-popover__hint', text: 'Local timezone applied' }), UI.button({ icon: 'ti ti-x', variant: 'ghost', subtle: true, title: 'Close', onClick: () => { try { pop?.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {} } }) ]); const footer = UI.el('div', { class: 'pqueue-popover__footer' }, [ UI.el('div', { class: 'pqueue-popover__footer-left' }, [UI.el('span', { class: 'pqueue-muted', text: UI.currentHistoryRangeLabel() })]), UI.el('div', { class: 'pqueue-popover__footer-right' }, [ UI.button({ text: 'Clear', variant: 'ghost', subtle: true, size: 'sm', onClick: () => Events.applyHistoryPreset('clear') }), UI.button({ text: 'Done', variant: 'secondary', size: 'sm', onClick: () => { try { pop?.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {} } }) ]) ]); const content = UI.el('div', { class: 'pqueue-card__body pqueue-popover__content' }, [ UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Date range' })]), dates, UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Presets' })]), presets ]); const pop = UI.el('div', { class: ['pqueue-popover', 'pqueue-card'], role: 'dialog', 'aria-label': 'History filters' }, [ UI.el('div', { class: 'pqueue-popover__arrow' }), header, content, footer ]); pop.setAttribute('data-open', 'false'); const keyHandler = (e) => { if (e.key === 'Escape') { e.preventDefault(); try { pop.dispatchEvent(new CustomEvent('pqueue-close', { bubbles: true })); } catch (err) {} } else if (e.key === 'Tab') { const focusables = pop.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); const list = Array.from(focusables).filter((n) => !n.hasAttribute('disabled')); if (!list.length) return; const first = list[0]; const last = list[list.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } }; pop.addEventListener('keydown', keyHandler); try { const rect = anchor.getBoundingClientRect(); const maxLeft = window.innerWidth - 360; const left = Math.min(rect.left + window.scrollX, maxLeft); const top = rect.bottom + 8 + window.scrollY; pop.style.top = `${top}px`; pop.style.left = `${left}px`; const arrow = pop.querySelector('.pqueue-popover__arrow'); if (arrow) { const anchorCenter = rect.left + rect.width / 2 + window.scrollX; const popLeft = left; const offset = Math.max(14, Math.min((anchorCenter - popLeft), 346)); arrow.style.left = `${offset}px`; } } catch (err) {} window.setTimeout(() => since.focus(), 0); return pop; }, refreshFilter() { const table = state.dom.pendingTable; if (!table) return; const filterVal = (state.filters.pending || "").trim().toLowerCase(); const rows = Array.from(table.querySelectorAll(".pqueue-row[data-id]")); let visible = 0; rows.forEach((row) => { const match = !filterVal || (row.dataset.search || "").includes(filterVal); row.style.display = match ? "" : "none"; if (match) visible += 1; }); UI.updatePendingFooter(visible); }, updatePendingFooter(visibleCount) { if (state.dom.pendingCount) state.dom.pendingCount.textContent = `${visibleCount} row${visibleCount === 1 ? "" : "s"}`; if (state.dom.pendingUpdated) state.dom.pendingUpdated.textContent = state.lastUpdated ? `Updated ${Format.relative(state.lastUpdated)}` : "Awaiting update"; }, updateSelectionUI() { const table = state.dom.pendingTable; if (!table) return; const rows = Array.from(table.querySelectorAll(".pqueue-row[data-id]")); const visibleRows = rows.filter((row) => row.style.display !== "none"); visibleRows.forEach((row) => { const selected = state.selectedPending.has(row.dataset.id); row.classList.toggle("is-selected", selected); const checkbox = row.querySelector(".pqueue-select"); if (checkbox) checkbox.checked = selected; }); const visibleSelected = visibleRows.filter((row) => state.selectedPending.has(row.dataset.id)).length; const selectAll = table.querySelector(".pqueue-select-all"); if (selectAll) { if (!visibleRows.length) { selectAll.checked = false; selectAll.indeterminate = false; } else { selectAll.checked = visibleSelected === visibleRows.length; selectAll.indeterminate = visibleSelected > 0 && visibleSelected < visibleRows.length; } } const count = state.selectedPending.size || null; if (state.dom.bulkPriorityBtn) { state.dom.bulkPriorityBtn.disabled = !count; const badge = state.dom.bulkPriorityBtn.querySelector(".pqueue-button__badge"); if (badge) { if (count) badge.textContent = String(count); else badge.remove(); } else if (count) { state.dom.bulkPriorityBtn.appendChild(UI.el("span", { class: "pqueue-button__badge", text: String(count) })); } } if (state.dom.deleteSelectedBtn) { state.dom.deleteSelectedBtn.disabled = !count; const badge = state.dom.deleteSelectedBtn.querySelector(".pqueue-button__badge"); if (badge) { if (count) badge.textContent = String(count); else badge.remove(); } else if (count) { state.dom.deleteSelectedBtn.appendChild(UI.el("span", { class: "pqueue-button__badge", text: String(count) })); } } }, updateToolbarStatus() { const status = state.dom.status; if (!status) return; status.innerHTML = ""; if (state.error) { status.appendChild(UI.icon("ti ti-alert-circle", { size: "md" })); status.appendChild(UI.el("span", { class: "pqueue-toolbar__status-text pqueue-toolbar__status-text--error", text: state.error })); return; } if (state.statusMessage) { status.appendChild(UI.icon("ti ti-info-circle", { size: "md" })); status.appendChild(UI.el("span", { class: "pqueue-toolbar__status-text", text: state.statusMessage })); return; } if (state.isRefreshing) { status.appendChild(UI.icon("ti ti-loader-2", { size: "md", spin: true })); status.appendChild(UI.el("span", { class: "pqueue-toolbar__status-text", text: "Syncing…" })); return; } if (state.lastUpdated) { status.appendChild(UI.icon("ti ti-history", { size: "md" })); status.appendChild(UI.el("span", { class: "pqueue-toolbar__status-text", text: `Updated ${Format.relative(state.lastUpdated)}` })); } else { status.appendChild(UI.icon("ti ti-bolt", { size: "md" })); status.appendChild(UI.el("span", { class: "pqueue-toolbar__status-text", text: "Ready" })); } }, ensureGallery() { if (UI.galleryOverlay) return; const overlay = UI.el("div", { class: "pqueue-gallery-overlay" }); const box = UI.el("div", { class: "pqueue-gallery" }); const closeBtn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Close" }); closeBtn.addEventListener("click", UI.closeGallery); const img = UI.el("img", { class: "pqueue-gallery__img" }); const counter = UI.el("div", { class: "pqueue-gallery__counter" }); const prev = UI.button({ icon: "ti ti-chevron-left", variant: "ghost", subtle: true, title: "Previous" }); prev.addEventListener("click", () => UI.galleryShow(UI.galleryState.index - 1)); const next = UI.button({ icon: "ti ti-chevron-right", variant: "ghost", subtle: true, title: "Next" }); next.addEventListener("click", () => UI.galleryShow(UI.galleryState.index + 1)); box.appendChild(closeBtn); box.appendChild(img); box.appendChild(counter); box.appendChild(prev); box.appendChild(next); overlay.appendChild(box); overlay.addEventListener("click", (e) => { if (e.target === overlay) UI.closeGallery(); }); document.body.appendChild(overlay); UI.galleryOverlay = overlay; UI.galleryImg = img; UI.galleryCounter = counter; UI.galleryState = { images: [], index: 0, pid: null }; }, openGallery(images, startIndex = 0, pid = null) { UI.ensureGallery(); UI.galleryState.images = images || []; UI.galleryState.pid = pid; UI.galleryState.index = Math.max(0, Math.min(startIndex, UI.galleryState.images.length - 1)); UI.galleryOverlay.style.display = "flex"; UI.galleryShow(UI.galleryState.index); UI.bindGalleryKeys(true); }, closeGallery() { if (UI.galleryOverlay) UI.galleryOverlay.style.display = "none"; UI.bindGalleryKeys(false); }, galleryShow(index) { const images = UI.galleryState.images; if (!images.length) return; UI.galleryState.index = (index + images.length) % images.length; const desc = images[UI.galleryState.index]; UI.galleryImg.src = UI.buildPreviewUrl(desc, UI.galleryState.pid).href; UI.galleryCounter.textContent = `${UI.galleryState.index + 1} / ${images.length}`; }, bindGalleryKeys(enabled) { const handler = (e) => { if (!UI.galleryOverlay || UI.galleryOverlay.style.display !== "flex") return; if (e.key === "Escape") UI.closeGallery(); else if (e.key === "ArrowRight") UI.galleryShow(UI.galleryState.index + 1); else if (e.key === "ArrowLeft") UI.galleryShow(UI.galleryState.index - 1); }; if (enabled) { UI.galleryKeyHandler = handler; document.addEventListener("keydown", handler); } else if (UI.galleryKeyHandler) { document.removeEventListener("keydown", UI.galleryKeyHandler); UI.galleryKeyHandler = null; } }, 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; }, extractImages(row) { try { let outputs = row.outputs ?? {}; if (typeof outputs === "string") { try { outputs = JSON.parse(outputs); } catch (err) { outputs = {}; } } const images = []; Object.values(outputs).forEach((value) => { if (value && typeof value === "object") { const list = value.images ?? value.ui?.images ?? []; if (Array.isArray(list)) list.forEach((img) => pushImg(img)); } else if (Array.isArray(value)) { value.forEach((img) => pushImg(img)); } }); return images; function pushImg(img) { if (!img || typeof img !== "object") return; const filename = img.filename ?? img.name; if (!filename) return; images.push({ filename, type: img.type ?? "output", subfolder: img.subfolder ?? "" }); } } catch (err) { return []; } }, countImages(row) { try { return UI.extractImages(row).length; } catch (err) { return 0; } }, historyPrimaryFilename(row) { try { const images = UI.extractImages(row); if (images.length && images[0]?.filename) { return images[0].filename; } if (row.prompt_id) return String(row.prompt_id); return "Unnamed output"; } catch (err) { return row.prompt_id ? String(row.prompt_id) : "Unnamed output"; } }, 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"; }, 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 }); }, ensureWorkflowModal() { if (UI.workflowOverlay) return; const overlay = UI.el("div", { class: "pqueue-modal" }); const dialog = UI.el("div", { class: "pqueue-modal__dialog" }); const header = UI.el("header", { class: "pqueue-modal__header" }); const title = UI.el("h3", { class: "pqueue-modal__title" }); const subtitle = UI.el("div", { class: "pqueue-modal__subtitle" }); const copyBtn = UI.button({ icon: "ti ti-copy", text: "Copy JSON", variant: "ghost", subtle: true }); copyBtn.dataset.action = "workflow-copy"; const closeBtn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Close" }); closeBtn.addEventListener("click", UI.closeWorkflowModal); header.appendChild(title); header.appendChild(subtitle); header.appendChild(UI.el("div", { class: "pqueue-modal__actions" }, [copyBtn, closeBtn])); const body = UI.el("div", { class: "pqueue-modal__body" }); const code = UI.el("pre", { class: "pqueue-modal__code" }); body.appendChild(code); dialog.appendChild(header); dialog.appendChild(body); overlay.appendChild(dialog); overlay.addEventListener("click", (e) => { if (e.target === overlay) UI.closeWorkflowModal(); }); document.body.appendChild(overlay); UI.workflowOverlay = overlay; UI.workflowDialog = dialog; UI.workflowTitle = title; UI.workflowSubtitle = subtitle; UI.workflowCode = code; UI.workflowCopyBtn = copyBtn; UI.workflowCopyBtn.addEventListener("click", () => { if (UI.workflowCode?.textContent) copyText(UI.workflowCode.textContent); }); UI.workflowKeyHandler = (e) => { if (e.key === "Escape") UI.closeWorkflowModal(); }; document.addEventListener("keydown", UI.workflowKeyHandler); }, openWorkflowModal({ promptId, workflowText, sourceLabel }) { UI.ensureWorkflowModal(); UI.workflowOverlay.style.display = "flex"; UI.workflowTitle.textContent = `Workflow for ${promptId}`; UI.workflowSubtitle.textContent = sourceLabel ? `Source: ${sourceLabel}` : ""; UI.workflowCode.textContent = workflowText; }, closeWorkflowModal() { if (UI.workflowOverlay) UI.workflowOverlay.style.display = "none"; }, 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)]); }, mountFallback() { if (document.getElementById("pqueue-fallback-panel")) { state.container = document.querySelector("#pqueue-fallback-panel .pqueue-fallback__content"); return; } const fab = UI.el("button", { id: "pqueue-fab", class: "pqueue-fab", type: "button" }, [UI.icon("ti ti-history", { size: "md" }), UI.el("span", { class: "pqueue-fab__label", text: "Persistent Queue" })]); const panel = UI.el("div", { id: "pqueue-fallback-panel", class: "pqueue-fallback", "data-open": "false" }); const header = UI.el("header", { class: "pqueue-fallback__header" }, [UI.el("div", { class: "pqueue-fallback__title", text: "Persistent Queue" }), UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Close", onClick: () => panel.setAttribute("data-open", "false") })]); const content = UI.el("div", { class: "pqueue-fallback__content" }); panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); document.body.appendChild(fab); fab.addEventListener("click", () => { const open = panel.getAttribute("data-open") === "true"; panel.setAttribute("data-open", open ? "false" : "true"); if (!open) refresh({ skipIfBusy: true }); }); state.container = content; }, removeFallback() { document.getElementById("pqueue-fab")?.remove(); document.getElementById("pqueue-fallback-panel")?.remove(); }, }; window.PQueue = window.PQueue || {}; window.PQueue.UI = UI; window.UI = UI; })();