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:
co-authored by
Claude Fable 5.1
parent
ffb352fb92
commit
148922ee2f
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;">▶ 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 | 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)"
|
||||
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user