feat(dashboard): preview badge, Pass filter, superseded previews, full-pass button

PREVIEW badge on cards and a modal note, a Pass filter row (hidden until previews exist),
previews superseded by a full image (config_fp/base_fp match) hidden by default, a preview bar
with 'Full pass for survivors (N)'. The button asks web/dashboard.js (postMessage) to switch
Preview pass off in the matching Config Builder and call app.queuePrompt; a marked stub
covers a future run API; fallback is an instructions toast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jason Hoku
2026-11-04 00:09:26 -08:00
co-authored by Claude Fable 5.1
parent ffb352fb92
commit 148922ee2f
9 changed files with 304 additions and 11 deletions
+2
View File
@@ -227,6 +227,7 @@ If you'd rather build the graph yourself:
* **Unified Settings Panel:** Gear icon (⚙️) opens a consolidated settings panel with Session management, Grid Display controls, Export Favorites, and Scan External Directory — replacing the old separate SESSION button.
* **Scan External Directory:** Load image sessions from any folder (including other ComfyUI instances) into the dashboard for viewing and filtering. Reads PNG metadata for full config display.
* **Export Favorites:** Copy all favorited images to a dedicated export folder with optional CivitAI metadata packing for sharing.
* **Preview Pass support:** preview images carry an amber `PREVIEW` badge, a **Pass** filter row (Preview / Full, shown once previews exist) separates them, and a preview whose cell now has a full image is hidden by default (isolate `Preview` in the Pass row to see them). A bar under the header counts previews and offers **Full pass for survivors (N)**, which switches Preview Pass off in the matching Config Builder and queues the workflow (falls back to instructions if the ComfyUI frontend does not allow it).
### ⚡ The "Revise & Generate" Workflow
* **One-Click Revision:** Click "REVISE" on any image to open a detail view.
@@ -840,6 +841,7 @@ Both live in the **Run Settings** section of the Config Builder and default to t
* **Bottom Right:** Generation time in seconds.
* **Top Left (on hover):** Red ✕ button to reject/hide image.
* **Top Right (on hover):** Gold ⭐ button to favorite image, and green "REVISE" button to open studio view.
* **Bottom Center:** amber `PREVIEW · N st` badge on preview-pass images (hover for the full-quality size/steps it stands in for).
### JSON Bars (Bottom — Horizontal Layout)
* **Green Bar (Left — Accepted):** Contains optimized JSON of all currently visible images. Click to select all, then copy-paste back into the `configs_json` widget to refine your batch.
+84 -4
View File
@@ -157,6 +157,7 @@ function getFilterKey() {
[...filters.cfg].sort().join(','),
[...filters.upscaleMethod].sort().join(','),
[...filters.mediaType].sort().join(','),
[...(filters.passType || [])].sort().join(',') + '#' + supersededFps.size,
searchFilters.map(f => `${f.type}:${f.term}`).join('|'),
(typeof logicFilters !== 'undefined' ? logicFilters.map(f => `${f.field}${f.op}${f.value}`).join('|') : ''),
(typeof quickFilters !== 'undefined' ? quickFilters.map(f => `${f.type}:${f.value}`).join('|') : '')
@@ -335,6 +336,63 @@ function runMultiSort(list) {
// --- 4. PIPELINE & FILTER LOGIC ---
// Preview pass support -------------------------------------------------------
// A preview is "superseded" once a full (non-preview) item exists for the same cell fingerprint.
// Superseded previews are hidden by default; isolating the Pass filter on "Preview" (without "Full") shows them.
function isSupersededPreview(d) {
if (!d.preview || !d.preview_of || !supersededFps.has(d.preview_of)) return false;
const pf = filters.passType;
if (pf && pf.has('Preview') && !pf.has('Full')) return false;
return true;
}
// newItems omitted: rebuild the whole set from activeData. With newItems: add only their fingerprints and
// return true when that superseded a preview that is currently displayed (caller then re-runs the pipeline).
function refreshSupersededFps(newItems) {
if (!newItems) {
const s = new Set();
for (let i = 0; i < activeData.length; i++) {
const d = activeData[i];
if (d.preview) continue;
if (d.config_fp) s.add(d.config_fp);
if (d.base_fp) s.add(d.base_fp);
}
supersededFps = s;
return false;
}
let grew = false;
for (let i = 0; i < newItems.length; i++) {
const d = newItems[i];
if (d.preview) continue;
if (d.config_fp && !supersededFps.has(d.config_fp)) { supersededFps.add(d.config_fp); grew = true; }
if (d.base_fp && !supersededFps.has(d.base_fp)) { supersededFps.add(d.base_fp); grew = true; }
}
if (!grew) return false;
return processedData.some(p => p.preview && supersededFps.has(p.preview_of));
}
// Shows "N previews | M awaiting a full pass" + the full-pass button when the session holds previews.
function updatePreviewBar() {
const bar = document.getElementById('preview-bar');
if (!bar) return;
let previews = 0, survivors = 0;
for (let i = 0; i < activeData.length; i++) {
const d = activeData[i];
if (!d.preview) continue;
previews++;
if (!d.rejected && !supersededFps.has(d.preview_of)) survivors++;
}
if (previews === 0) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
const txt = document.getElementById('preview-bar-text');
if (txt) txt.textContent = `${previews} preview image${previews === 1 ? '' : 's'} | ${survivors} not rejected and awaiting a full pass`;
const btn = document.getElementById('full-pass-btn');
if (btn) {
btn.textContent = `\u25B6 Full pass for survivors (${survivors})`;
btn.disabled = survivors === 0;
}
}
function incrementalFilter(newItems) {
const hasButtonFilters = filters.sampler.size > 0 ||
filters.scheduler.size > 0 ||
@@ -348,13 +406,15 @@ function incrementalFilter(newItems) {
filters.steps.size > 0 ||
filters.cfg.size > 0 ||
filters.upscaleMethod.size > 0 ||
filters.mediaType.size > 0;
filters.mediaType.size > 0 ||
(filters.passType && filters.passType.size > 0);
return newItems.filter(d => {
// Apply top-level filters (Favorites/Non-Favorited/Rejected visibility)
if (d.favorited && !topFilters.showFavorites) return false;
if (!d.favorited && !d.rejected && !topFilters.showNonFavorited) return false;
if (d.rejected && !topFilters.showRejected) return false;
if (d.preview && isSupersededPreview(d)) return false;
// Apply button filters
if (hasButtonFilters) {
@@ -395,6 +455,9 @@ function incrementalFilter(newItems) {
const mt = d.media_type || 'image';
if (!filters.mediaType.has(mt)) return false;
}
if (filters.passType && filters.passType.size > 0) {
if (!filters.passType.has(d.preview ? 'Preview' : 'Full')) return false;
}
}
// Apply search filters
@@ -427,6 +490,7 @@ let pipelineAbortId = 0;
function executePipeline() {
const startTime = performance.now();
refreshSupersededFps();
const currentFilterKey = getFilterKey();
const filtersChanged = currentFilterKey !== lastFilterKey;
@@ -449,7 +513,8 @@ function executePipeline() {
filters.steps.size > 0 ||
filters.cfg.size > 0 ||
filters.upscaleMethod.size > 0 ||
filters.mediaType.size > 0;
filters.mediaType.size > 0 ||
(filters.passType && filters.passType.size > 0);
const hasSearchFilters = typeof matchesSearchFilters === 'function' && searchFilters.length > 0;
@@ -458,6 +523,7 @@ function executePipeline() {
if (d.favorited && !topFilters.showFavorites) return false;
if (!d.favorited && !d.rejected && !topFilters.showNonFavorited) return false;
if (d.rejected && !topFilters.showRejected) return false;
if (d.preview && isSupersededPreview(d)) return false;
// Apply button filters (skip entirely if none are active)
if (hasButtonFilters) {
@@ -495,6 +561,9 @@ function executePipeline() {
const mt = d.media_type || 'image';
if (!filters.mediaType.has(mt)) return false;
}
if (filters.passType && filters.passType.size > 0) {
if (!filters.passType.has(d.preview ? 'Preview' : 'Full')) return false;
}
}
if (hasSearchFilters && !matchesSearchFilters(d)) return false;
@@ -523,6 +592,7 @@ function executePipeline() {
const elapsed = (performance.now() - startTime).toFixed(1);
console.log(`[Pipeline] Processed ${processedData.length} items in ${elapsed}ms`);
updateJSONs(processedData);
updatePreviewBar();
if (filtersChanged) {
if(typeof renderDOM === 'function') renderDOM();
@@ -542,6 +612,15 @@ function processNewData(newItems) {
// (activeData already contains the new items via fullManifest.items reference)
refreshIndices();
// A full image landing for a cell supersedes its preview: if any preview on screen just became
// superseded, re-run the whole pipeline (it rebuilds the superseded set and hides them).
if (refreshSupersededFps(newItems)) {
updateFiltersForNewData(newItems);
updateDataPipeline();
return;
}
updatePreviewBar();
// Recompute label global values when data changes (new items may change what's unique)
// Then refresh ALL existing card labels since what's "unique" may have changed
if (typeof computeLabelGlobalValues === 'function' && labelMode && labelMode.enabled) {
@@ -647,7 +726,7 @@ function loadSortPreference() {
function updateFiltersForNewData(newItems) {
// Track which filter categories actually got new unique values
let changed = false;
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed'];
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'passType'];
// Build a set of new values for each key in a single pass over newItems
for (let i = 0; i < newItems.length; i++) {
@@ -659,9 +738,10 @@ function updateFiltersForNewData(newItems) {
else if (key === 'positive') val = d.positive || meta.positive || "";
else if (key === 'negative') val = d.negative || meta.negative || "";
else if (key === 'size') val = `${d.width}x${d.height}`;
else if (key === 'passType') val = d.preview ? 'Preview' : 'Full';
else val = d[key];
if (val !== undefined && val !== null && !filters[key].has(val)) {
if (val !== undefined && val !== null && !(filters[key] && filters[key].has(val))) {
changed = true;
// Don't break - we need to continue checking all items/keys
// so that initFilters gets a complete picture
+6 -1
View File
@@ -32,9 +32,14 @@ var filters = filters || {
steps: new Set(),
cfg: new Set(),
upscaleMethod: new Set(),
mediaType: new Set()
mediaType: new Set(),
passType: new Set() // 'Preview' | 'Full'
};
// config_fp / base_fp of every non-preview item: a preview whose preview_of is in here has been superseded
// by a full render and is hidden by default (see isSupersededPreview in logic_pipeline.js).
var supersededFps = supersededFps || new Set();
// Top-level filter toggles (true = show, false = hide)
var topFilters = topFilters || {
showFavorites: true, // Default ON
+27 -4
View File
@@ -558,7 +558,7 @@ function initFilters() {
if (!activeData || activeData.length === 0) return;
// Ensure all filter Sets exist (add new ones if missing)
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod', 'mediaType'];
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod', 'mediaType', 'passType'];
filterKeys.forEach(key => {
if (!filters.hasOwnProperty(key) || !(filters[key] instanceof Set)) {
filters[key] = new Set();
@@ -587,12 +587,19 @@ function initFilters() {
return shortModel ? `${mode} + ${shortModel}` : mode || 'Upscaled';
}
if (key === 'mediaType') return d.media_type || 'image';
if (key === 'passType') return d.preview ? 'Preview' : 'Full';
return d[key];
}))].sort();
const container = document.getElementById('filter-' + key);
if (!container) return;
// The Pass row only matters once the session holds preview images
if (key === 'passType') {
const passRow = container.closest('.filter-row');
if (passRow) passRow.style.display = unique.length > 1 ? '' : 'none';
}
const cacheKey = unique.join(',');
if (filterButtonCache[key] === cacheKey) {
return;
@@ -699,7 +706,7 @@ function initFilters() {
function resetAllFilters() {
if (!activeData || activeData.length === 0) return;
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod', 'mediaType'];
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod', 'mediaType', 'passType'];
// Re-populate every filter Set with all unique values from activeData
filterKeys.forEach(key => {
@@ -722,6 +729,7 @@ function resetAllFilters() {
val = shortModel ? `${mode} + ${shortModel}` : mode || 'Upscaled';
}
} else if (key === 'mediaType') val = d.media_type || 'image';
else if (key === 'passType') val = d.preview ? 'Preview' : 'Full';
else val = d[key];
if (val !== undefined && val !== null) filters[key].add(val);
});
@@ -1332,6 +1340,9 @@ function createCard(d) {
const reviseBtn = isVideo ? '' : `<button class="revise-btn" onclick="event.stopPropagation(); openM(${d.id})">REVISE</button>`;
const upscaleBtn = isVideo ? '' : `<button class="upscale-btn" onclick="openUpscaleModal(${d.id})" title="Upscale this image">\u2B06</button>`;
const videoBadge = isVideo ? '<div class="video-badge">\u25B6 VIDEO</div>' : '';
const previewBadge = d.preview
? `<div class="preview-badge" title="Preview of ${d.full_width}x${d.full_height} @ ${d.full_steps} steps">PREVIEW \u00B7 ${d.steps} st</div>`
: '';
// FIXED LAYOUT: Star top-right, Revise below it, time bottom-right, index bottom-left
card.innerHTML = `
@@ -1342,6 +1353,7 @@ function createCard(d) {
${reviseBtn}
${upscaleBtn}
${videoBadge}
${previewBadge}
<div class="time-tag">${d.duration}s</div>
<div class="index-tag">#${totalIndex}</div>
${buildLabelOverlay(d)}
@@ -1386,6 +1398,15 @@ function openM(id) {
const d = activeData.find(x => x.id === id);
if (!d) return;
document.getElementById('m-img').src = d.file;
const previewNote = document.getElementById('m-preview-note');
if (previewNote) {
if (d.preview) {
previewNote.textContent = `Preview of ${d.full_width}x${d.full_height} @ ${d.full_steps} steps (this image: ${d.width}x${d.height} @ ${d.steps} steps)`;
previewNote.style.display = 'block';
} else {
previewNote.style.display = 'none';
}
}
console.log(d)
// Populate read-only info fields
const modelEl = document.getElementById('f-model');
@@ -1999,15 +2020,16 @@ const _qfTypeLabels = {
cfg: 'CFG',
upscaleMethod: 'Upscale',
mediaType: 'Media',
passType: 'Pass',
positive: 'Positive',
negative: 'Negative'
};
// All types that Quick Filter supports (type-menu order)
const _qfAllTypes = ['lora', 'lora_strength', 'cfg', 'steps', 'sampler', 'scheduler', 'denoise', 'size', 'model', 'seed', 'upscaleMethod', 'mediaType', 'positive', 'negative'];
const _qfAllTypes = ['lora', 'lora_strength', 'cfg', 'steps', 'sampler', 'scheduler', 'denoise', 'size', 'model', 'seed', 'upscaleMethod', 'mediaType', 'passType', 'positive', 'negative'];
// Types shown in the expanded facet panel (omit prompt types for brevity)
const _qfFacetableTypes = ['sampler', 'scheduler', 'lora', 'lora_strength', 'model', 'denoise', 'cfg', 'steps', 'size', 'seed', 'upscaleMethod', 'mediaType'];
const _qfFacetableTypes = ['sampler', 'scheduler', 'lora', 'lora_strength', 'model', 'denoise', 'cfg', 'steps', 'size', 'seed', 'upscaleMethod', 'mediaType', 'passType'];
/**
* Extract a facet value (or array of values) from an item for a given type.
@@ -2046,6 +2068,7 @@ function _qfExtractItemValue(item, type) {
return shortModel ? `${mode} + ${shortModel}` : mode || 'Upscaled';
}
if (type === 'mediaType') return item.media_type || 'image';
if (type === 'passType') return item.preview ? 'Preview' : 'Full';
const v = item[type];
if (v === undefined || v === null) return null;
return String(v);
+69 -1
View File
@@ -976,4 +976,72 @@ async function scanDirectory() {
}
}
setupKeyReloadFullscreen()
setupKeyReloadFullscreen()
// ---------------------------------------------------------------------------
// FULL PASS FOR SURVIVORS (preview-pass workflow)
// Strategy (a): ask the parent ComfyUI page (web/dashboard.js) to flip the matching Config Builder to
// "Preview pass off", then queue the prompt. Needs a parent that understands 'uscg_run_full_pass'.
// Strategy (b): STUB for a future run API (see _uscgTryRunApi). Fallback: instructions toast.
// ---------------------------------------------------------------------------
function _uscgToast(msg, ok) {
var el = document.createElement('div');
el.textContent = msg;
el.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);max-width:80%;z-index:99999;' +
'padding:10px 16px;border-radius:6px;font-size:12px;color:#fff;box-shadow:0 4px 16px rgba(0,0,0,0.5);' +
'background:' + (ok ? '#1f5a2e' : '#5a3a14') + ';border:1px solid ' + (ok ? '#3c9a55' : '#a8741f') + ';';
document.body.appendChild(el);
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 9000);
}
// Strategy (b) stub: wire the run API here once it exists. Detection: window.USCG_RUN_API (set by the host
// page) or a '/config_tester/run' route. Resolves to {ok, message} when it handled the request, else null.
async function _uscgTryRunApi(session) {
try {
if (window.USCG_RUN_API && typeof window.USCG_RUN_API.run === 'function') {
// TODO(merge with the run-API task): confirm the real signature; overrides below are the contract.
var res = await window.USCG_RUN_API.run({
session_name: session,
settings_overrides: { preview_pass: { enabled: false }, skip_rejected_previews: true }
});
return { ok: true, message: (res && res.message) || 'Full pass queued.' };
}
var probe = await fetch('/config_tester/run', { method: 'GET' }).catch(function () { return null; });
if (probe && probe.status !== 404) {
// TODO(merge with the run-API task): POST /config_tester/run with the same overrides, then return
// {ok, message}. Intentionally not implemented here (this task does not depend on it).
}
} catch (e) { /* fall through to the instructions toast */ }
return null;
}
function _uscgAskParentFullPass(session) {
return new Promise(function (resolve) {
if (!window.parent || window.parent === window) { resolve(null); return; }
var done = false;
var onMsg = function (ev) {
if (!ev.data || ev.data.type !== 'uscg_run_full_pass_result') return;
done = true;
window.removeEventListener('message', onMsg);
resolve({ ok: !!ev.data.ok, message: ev.data.message || '' });
};
window.addEventListener('message', onMsg);
window.parent.postMessage({ type: 'uscg_run_full_pass', session_name: session }, '*');
setTimeout(function () {
if (done) return;
window.removeEventListener('message', onMsg);
resolve(null); // parent too old to answer
}, 4000);
});
}
async function runFullPassForSurvivors() {
var session = (document.getElementById('session-input') && document.getElementById('session-input').value) ||
(fullManifest && fullManifest.session_name) || '';
var manual = 'In the Config Builder open Run Settings, untick "Preview Pass" and queue the workflow again. ' +
'Only cells whose preview was not rejected will render at full quality.';
var res = await _uscgAskParentFullPass(session);
if (!res) res = await _uscgTryRunApi(session);
if (res && res.ok) { _uscgToast(res.message || 'Full pass queued.', true); return; }
_uscgToast((res && res.message ? res.message + ' ' : '') + manual, false);
}
+20
View File
@@ -593,6 +593,26 @@ body {
text-shadow: 0 0 4px rgba(0,0,0,0.8);
}
.preview-badge {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
background: rgba(160, 110, 20, 0.92);
color: #fff;
padding: 3px 7px;
border-radius: 4px;
font-weight: bold;
font-size: 10px;
white-space: nowrap;
pointer-events: none;
z-index: 5;
border: 1px solid rgba(255, 255, 255, 0.2);
text-shadow: 0 0 4px rgba(0,0,0,0.8);
}
#full-pass-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.card video {
width: 100%;
height: 100%;
+14 -1
View File
@@ -449,6 +449,15 @@
</div>
<!-- PREVIEW PASS BAR (shown only when the session holds preview images) -->
<div id="preview-bar" style="display: none; align-items: center; gap: 10px; padding: 4px 12px; background: #2a2418; border-bottom: 1px solid #443a22; font-size: 11px; color: #cdb26a;">
<span style="font-weight: bold;">PREVIEW PASS</span>
<span id="preview-bar-text" style="flex: 1;">--</span>
<button id="full-pass-btn" onclick="runFullPassForSurvivors()"
title="Render the survivors at full quality: turns Preview pass off in the Config Builder and queues the workflow"
style="background: #3a2f12; border: 1px solid #8a7330; color: #ffd76a; padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 11px;">&#9654; Full pass for survivors</button>
</div>
<div id="filters-popup" class="filters-popup" style="display: none;">
<div class="filters-popup-header">
<h3>Filters & Info</h3>
@@ -627,6 +636,10 @@
<span class="filter-label">Type</span>
<div id="filter-mediaType" class="filter-buttons-container"></div>
</div>
<div class="filter-row" style="display: none;">
<span class="filter-label">Pass</span>
<div id="filter-passType" class="filter-buttons-container"></div>
</div>
</div>
</div>
@@ -696,7 +709,7 @@
<span style="font-size: 9px; color: #444; margin-left: 8px;">← → Space navigate &nbsp;|&nbsp; 1-9 favorite</span>
</div>
<div class="modal-main">
<div class="preview"><img id="m-img" src=""></div>
<div class="preview"><img id="m-img" src=""><div id="m-preview-note" style="display: none; font-size: 11px; color: #cdb26a; padding: 4px 8px; text-align: center;"></div></div>
<div class="controls">
<h2 style="margin:0 0 10px 0; color:#fff;">Revise & Generate</h2>
<button class="modal-action-btn copy-comfy-btn" onclick="copyConfigsAsComfyNodes(window.currentModalId)"
+43
View File
@@ -0,0 +1,43 @@
// Run: node tests/superseded_previews.test.js (dashboard superseded-preview logic, no browser needed)
const fs = require('fs');
const src = fs.readFileSync(require('path').join(__dirname, '..', 'resources', 'logic_pipeline.js'), 'utf8');
function grab(name) {
const i = src.indexOf('function ' + name + '(');
let depth = 0, j = src.indexOf('{', i);
for (let k = j; k < src.length; k++) {
if (src[k] === '{') depth++;
if (src[k] === '}') { depth--; if (depth === 0) return src.slice(i, k + 1); }
}
}
global.document = { getElementById: () => null };
let code = 'var supersededFps = new Set(); var activeData = []; var processedData = []; var filters = { passType: new Set(["Preview","Full"]) };\n' +
grab('isSupersededPreview') + '\n' + grab('refreshSupersededFps') + '\n' + grab('updatePreviewBar') + '\n' +
'return {isSupersededPreview, refreshSupersededFps, state: () => ({supersededFps, filters, setActive: (a, p) => { activeData = a; processedData = p; }})};';
const api = new Function(code)();
const assert = require('assert');
const prev = { id: 1, preview: true, preview_of: 'fp:a' };
const prev2 = { id: 2, preview: true, preview_of: 'fp:b' };
const full = { id: 3, config_fp: 'fp:a' };
const up = { id: 4, config_fp: 'fp:x', base_fp: 'fp:b' };
const st = api.state();
st.setActive([prev, prev2], [prev, prev2]);
assert.strictEqual(api.refreshSupersededFps(), false);
assert.strictEqual(api.isSupersededPreview(prev), false);
// incremental: full item lands for fp:a while preview is displayed
assert.strictEqual(api.refreshSupersededFps([full]), true);
assert.strictEqual(api.isSupersededPreview(prev), true);
assert.strictEqual(api.isSupersededPreview(prev2), false);
// upscaled item supersedes through base_fp
assert.strictEqual(api.refreshSupersededFps([up]), true);
assert.strictEqual(api.isSupersededPreview(prev2), true);
// no growth -> false
assert.strictEqual(api.refreshSupersededFps([full]), false);
// isolating Preview-only shows superseded previews
st.filters.passType.delete('Full');
assert.strictEqual(api.isSupersededPreview(prev), false);
st.filters.passType.add('Full');
// full rebuild
st.setActive([prev, prev2, full, up], []);
api.refreshSupersededFps();
assert.strictEqual(api.state().supersededFps.size, 3);
console.log('superseded logic ok');
+39
View File
@@ -158,6 +158,45 @@ app.registerExtension({
return;
}
// Full pass for survivors (preview-pass workflow): flip the matching Config Builder to
// "Preview pass off" and queue the prompt. Feature-detected; the dashboard falls back to an
// instructions toast when this does not answer (or app.queuePrompt is unavailable).
if (event.data?.type === 'uscg_run_full_pass') {
const reply = (ok, message) => {
try { event.source?.postMessage({ type: 'uscg_run_full_pass_result', ok, message }, '*'); } catch (e) { /* ignore */ }
};
try {
const san = (s) => String(s || '').replace(/[^\w\-]/g, '');
const wanted = san(event.data.session_name);
const builders = (app.graph?._nodes || []).filter(n =>
n.type === "UltimateConfigBuilder" && n.state && san(n.state.session_name) === wanted);
if (builders.length !== 1) {
reply(false, builders.length === 0
? `No Config Builder in this workflow uses session "${wanted}".`
: `${builders.length} Config Builders use session "${wanted}".`);
return;
}
if (typeof app.queuePrompt !== 'function') {
reply(false, 'This ComfyUI frontend does not expose app.queuePrompt.');
return;
}
const b = builders[0];
if (!b.state.preview_pass || typeof b.state.preview_pass !== 'object') {
b.state.preview_pass = { enabled: false, steps: 8, scale: 0.5, cfg_override: null };
}
b.state.preview_pass.enabled = false;
b.state.skip_rejected_previews = true;
b.saveState?.();
b.renderUI?.();
Promise.resolve(app.queuePrompt(0, 1)).then(
() => reply(true, 'Queued the full pass (Preview pass switched off in the Config Builder).'),
(err) => reply(false, 'Queueing failed: ' + (err?.message || err)));
} catch (e) {
reply(false, 'Could not queue the full pass: ' + (e?.message || e));
}
return;
}
if (event.data?.type === 'toggle_fullscreen') {
const nodeId = parseInt(event.data.node_id);
const graphNode = app.graph.getNodeById(nodeId);