From e50ea96f9dd7d325dc017bb6e6f37b79f6fa36bc Mon Sep 17 00:00:00 2001 From: Mehdi Chaid Date: Fri, 19 Jun 2026 14:22:49 -0400 Subject: [PATCH] feat: Generation Data overlay layout + ADetailer A/B toggle (#324, #328) - Widen the overlay and switch the combo view to a 33/67 left/right split. - Increase the preview/composite image size to 384px. - Make combo-axis card grids wrap instead of horizontal-scroll. - Add ADetailer/Upscale settings as a new 'Generation settings' section. - Store ADetailer A/B outputs per combo key in queue.js; add an A/B toggle below the image dimensions to switch between regular and ADetailer outputs in the overlay (#328). --- web/queue.js | 7 ++ web/widgets/flake-generate.js | 22 +++++- web/widgets/generation-data.js | 136 ++++++++++++++++++++++++++++----- 3 files changed, 145 insertions(+), 20 deletions(-) diff --git a/web/queue.js b/web/queue.js index 6f792b5..c6193f1 100644 --- a/web/queue.js +++ b/web/queue.js @@ -221,6 +221,8 @@ function setupBatchTracking(combinations, comboKeys) { if (idx < 0) return; const comboKey = (comboKeys && comboKeys[idx]) ?? ""; const images = output.flake_images; + const adImages = output.flake_images_ad; + const isAB = Array.isArray(output.adetailer_ab) && output.adetailer_ab.length > 0; if (!Array.isArray(images) || images.length === 0) return; const candidates = nodeId ? [app.graph.getNodeById(nodeId)].filter(n => n && n.type === "FlakeGenerate") @@ -229,6 +231,11 @@ function setupBatchTracking(combinations, comboKeys) { fg.properties = fg.properties || {}; fg.properties._images_by_combo = fg.properties._images_by_combo || {}; fg.properties._images_by_combo[comboKey] = images[0]; + if (isAB && Array.isArray(adImages) && adImages.length > 0) { + fg.properties._images_by_combo_ad = fg.properties._images_by_combo_ad || {}; + fg.properties._images_by_combo_ad[comboKey] = adImages[0]; + fg.properties._adetailer_ab = true; + } } } diff --git a/web/widgets/flake-generate.js b/web/widgets/flake-generate.js index 4e64cc8..7ea950b 100644 --- a/web/widgets/flake-generate.js +++ b/web/widgets/flake-generate.js @@ -98,7 +98,18 @@ export function setupFlakeGenerateWidget(node) { if (link) startNode = graph.getNodeById(link.origin_id); } const model = startNode ? buildModel(startNode) : { axes: [], fixed: { presetName: null, stackFlakes: [] } }; - openGenerationDataOverlay(model, node.properties._images_by_combo || {}); + const adetailer = findWidget("adetailer")?.value || "Off"; + openGenerationDataOverlay(model, node.properties._images_by_combo || {}, { + lastImagesByComboAd: node.properties._images_by_combo_ad || {}, + adetailerAB: !!node.properties._adetailer_ab, + adetailer, + adetailerDenoise: findWidget("adetailer_denoise")?.value, + adetailerSteps: findWidget("adetailer_steps")?.value, + adetailerBbox: findWidget("adetailer_bbox")?.value, + upscale: !!findWidget("upscale")?.value, + upscaleModel: findWidget("upscale_model")?.value, + upscaleFactor: findWidget("upscale_factor")?.value, + }); }); topRow.appendChild(genDataBtn); container.appendChild(topRow); @@ -198,6 +209,15 @@ export function setupFlakeGenerateWidget(node) { const img = output.flake_images[0]; showImage(img); node.properties._last_image = img; + // Store ADetailer A/B outputs when present (#328). + const isAB = Array.isArray(output.adetailer_ab) && output.adetailer_ab.length > 0; + if (isAB && Array.isArray(output.flake_images_ad) && output.flake_images_ad.length > 0) { + node.properties._last_image_ad = output.flake_images_ad[0]; + node.properties._adetailer_ab = true; + } else { + node.properties._last_image_ad = null; + node.properties._adetailer_ab = false; + } // Per-combo storage is populated from queue.js via the `executed` // websocket event (which carries the correct prompt_id), so we no // longer write _images_by_combo here — doing so raced when the diff --git a/web/widgets/generation-data.js b/web/widgets/generation-data.js index 591894c..77e911b 100644 --- a/web/widgets/generation-data.js +++ b/web/widgets/generation-data.js @@ -355,11 +355,21 @@ export function combinationKeyFor(model, selIdx) { } // ── Overlay UI ───────────────────────────────────────────────────────────── -export function openGenerationDataOverlay(model, lastImagesByCombo) { +export function openGenerationDataOverlay(model, lastImagesByCombo, opts = {}) { + const { + lastImagesByComboAd = {}, + adetailerAB = false, + adetailer = "Off", + adetailerDenoise, + adetailerSteps, + adetailerBbox, + upscale = false, + upscaleModel, + upscaleFactor, + } = opts; let { content, footer, close } = openOverlay(); - // Cap width at 70% of previous (#229 — reduce overlay width by 30%): the - // previous cap was min(1400px, 95vw); new cap is min(980px, 66vw). - css(content.parentElement, content.parentElement.style.cssText + "width:auto;max-width:min(980px,66vw);min-width:0;"); + // Wider overlay (#324): give the right-hand image + data panel more room. + css(content.parentElement, content.parentElement.style.cssText + "width:auto;max-width:min(1200px,85vw);min-width:0;"); const header = document.createElement("div"); css(header, "display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #333;"); @@ -393,12 +403,12 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { css(split, "display:flex;gap:8px;align-items:flex-start;"); content.appendChild(split); - // Left half: combo axis grids + combo-specific fields below + // Left third: combo axis grids + combo-specific fields below const left = document.createElement("div"); - css(left, "flex:1;display:flex;flex-direction:column;gap:12px;min-width:0;max-height:60vh;overflow:auto;"); - // Right half: composite image + common data sections + css(left, "flex:0 0 33%;display:flex;flex-direction:column;gap:12px;min-width:0;max-height:65vh;overflow:auto;"); + // Right two-thirds: composite image + common data sections const right = document.createElement("div"); - css(right, "flex:1;display:flex;flex-direction:column;gap:10px;min-width:0;max-height:60vh;overflow:auto;"); + css(right, "flex:1;display:flex;flex-direction:column;gap:10px;min-width:0;max-height:65vh;overflow:auto;"); split.appendChild(left); split.appendChild(right); @@ -420,7 +430,7 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { section.appendChild(lbl); const scroll = document.createElement("div"); - css(scroll, "display:flex;gap:6px;overflow-x:auto;padding-bottom:4px;"); + css(scroll, "display:flex;flex-wrap:wrap;gap:6px;padding-bottom:4px;"); const axisCards = []; axis.items.forEach((item, ii) => { const card = document.createElement("div"); @@ -570,18 +580,21 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { const compositeImg = document.createElement("img"); // Aspect ratio is set per-combination in refreshRight (#231); start at // a 1:1 placeholder while data loads. - css(compositeImg, "width:256px;height:256px;object-fit:cover;border-radius:6px;border:1px solid #333;background:#1a1a1a;"); + css(compositeImg, "width:384px;height:384px;object-fit:cover;border-radius:6px;border:1px solid #333;background:#1a1a1a;"); const compositeLabel = document.createElement("div"); css(compositeLabel, "font-size:11px;color:#888;text-align:center;"); const compositeDimensions = document.createElement("div"); css(compositeDimensions, "font-size:10px;color:#666;text-align:center;"); + const adToggleWrap = document.createElement("div"); + css(adToggleWrap, "display:flex;align-items:center;gap:8px;font-size:12px;color:#ddd;"); compositeWrap.appendChild(compositeImg); compositeWrap.appendChild(compositeLabel); compositeWrap.appendChild(compositeDimensions); + compositeWrap.appendChild(adToggleWrap); right.appendChild(compositeWrap); function applyAspectRatio(w, h) { if (!w || !h) return; - const MAX = 256; + const MAX = 384; const ratio = w / h; let cw, ch; if (ratio >= 1) { cw = MAX; ch = Math.round(MAX / ratio); } @@ -632,7 +645,9 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { applyAspectRatio(data.width, data.height); // Composite (or generated output if we have one for this combination) - const generated = lastImagesByCombo && lastImagesByCombo[key]; + const generated = showAd + ? (lastImagesByComboAd && lastImagesByComboAd[key]) + : (lastImagesByCombo && lastImagesByCombo[key]); if (generated) { compositeImg.src = `/view?filename=${encodeURIComponent(generated.filename)}&type=${generated.type || "output"}&subfolder=${encodeURIComponent(generated.subfolder || "")}`; const sub = (generated.subfolder || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""); @@ -650,8 +665,9 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { const s1 = section("Model", data.modelRows); const s2 = section("LoRAs", data.commonLoraRows); const s3 = section("Prompts", data.commonPromptRows); - for (const s of [s1, s2, s3]) if (s) dataWrap.appendChild(s); - if (!s1 && !s2 && !s3) { + const s4 = section("Generation settings", extraInfoRows()); + for (const s of [s1, s2, s3, s4]) if (s) dataWrap.appendChild(s); + if (!s1 && !s2 && !s3 && !s4) { const empty = document.createElement("div"); css(empty, "font-size:12px;color:#555;text-align:center;padding:20px;"); empty.textContent = "No data for this combination"; @@ -667,6 +683,45 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { if (cs2) leftDataWrap.appendChild(cs2); } + // ADetailer A/B toggle (#328): only visible when this run produced both + // regular and ADetailer outputs. It switches the previewed image. + let showAd = false; + let adToggle = null; + if (adetailerAB) { + adToggle = document.createElement("label"); + css(adToggle, "display:flex;align-items:center;gap:6px;cursor:pointer;"); + const chk = document.createElement("input"); + chk.type = "checkbox"; + chk.checked = false; + css(chk, "cursor:pointer;margin:0;"); + const span = document.createElement("span"); + span.textContent = "Show ADetailer output"; + adToggle.appendChild(chk); + adToggle.appendChild(span); + chk.addEventListener("change", () => { + showAd = chk.checked; + refreshRight(); + }); + adToggleWrap.appendChild(adToggle); + } + + // Build extra info rows for ADetailer/Upscale settings (#324). + function extraInfoRows() { + const rows = []; + if (adetailer !== "Off") { + rows.push(["ADetailer", adetailer]); + if (adetailerDenoise != null) rows.push(["ADetailer denoise", String(adetailerDenoise)]); + if (adetailerSteps != null) rows.push(["ADetailer steps", String(adetailerSteps)]); + if (adetailerBbox) rows.push(["ADetailer bbox", adetailerBbox]); + } + if (upscale) { + rows.push(["Upscale", "On"]); + if (upscaleModel) rows.push(["Upscale model", upscaleModel]); + if (upscaleFactor != null) rows.push(["Upscale factor", String(upscaleFactor)]); + } + return rows; + } + refreshRight(); } else { // No combo nodes: single centered panel @@ -677,18 +732,21 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { const compositeWrap = document.createElement("div"); css(compositeWrap, "display:flex;flex-direction:column;align-items:center;gap:4px;"); const compositeImg = document.createElement("img"); - css(compositeImg, "width:256px;height:256px;object-fit:cover;border-radius:6px;border:1px solid #333;background:#1a1a1a;"); + css(compositeImg, "width:384px;height:384px;object-fit:cover;border-radius:6px;border:1px solid #333;background:#1a1a1a;"); const compositeLabel = document.createElement("div"); css(compositeLabel, "font-size:11px;color:#888;text-align:center;"); const compositeDimensions = document.createElement("div"); css(compositeDimensions, "font-size:10px;color:#666;text-align:center;"); + const adToggleWrap = document.createElement("div"); + css(adToggleWrap, "display:flex;align-items:center;gap:8px;font-size:12px;color:#ddd;"); compositeWrap.appendChild(compositeImg); compositeWrap.appendChild(compositeLabel); compositeWrap.appendChild(compositeDimensions); + compositeWrap.appendChild(adToggleWrap); singleWrap.appendChild(compositeWrap); function applyAspectRatioSingle(w, h) { if (!w || !h) return; - const MAX = 256; + const MAX = 384; const ratio = w / h; let cw, ch; if (ratio >= 1) { cw = MAX; ch = Math.round(MAX / ratio); } @@ -727,6 +785,43 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { return wrap; } + // ADetailer A/B toggle (#328) for the single-panel path. + let showAd = false; + if (adetailerAB) { + const adToggle = document.createElement("label"); + css(adToggle, "display:flex;align-items:center;gap:6px;cursor:pointer;"); + const chk = document.createElement("input"); + chk.type = "checkbox"; + chk.checked = false; + css(chk, "cursor:pointer;margin:0;"); + const span = document.createElement("span"); + span.textContent = "Show ADetailer output"; + adToggle.appendChild(chk); + adToggle.appendChild(span); + chk.addEventListener("change", () => { + showAd = chk.checked; + refreshRight(); + }); + adToggleWrap.appendChild(adToggle); + } + + // Extra info rows for ADetailer/Upscale settings (#324). + function extraInfoRows() { + const rows = []; + if (adetailer !== "Off") { + rows.push(["ADetailer", adetailer]); + if (adetailerDenoise != null) rows.push(["ADetailer denoise", String(adetailerDenoise)]); + if (adetailerSteps != null) rows.push(["ADetailer steps", String(adetailerSteps)]); + if (adetailerBbox) rows.push(["ADetailer bbox", adetailerBbox]); + } + if (upscale) { + rows.push(["Upscale", "On"]); + if (upscaleModel) rows.push(["Upscale model", upscaleModel]); + if (upscaleFactor != null) rows.push(["Upscale factor", String(upscaleFactor)]); + } + return rows; + } + let refreshToken = 0; async function refreshRight() { const token = ++refreshToken; @@ -744,7 +839,9 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { ? "" : combinationKeyFor(model, model.axes.map(() => 0)); applyAspectRatioSingle(data.width, data.height); - const generated = lastImagesByCombo && (lastImagesByCombo[key] ?? lastImagesByCombo[""]); + const generated = showAd + ? (lastImagesByComboAd && (lastImagesByComboAd[key] ?? lastImagesByComboAd[""])) + : (lastImagesByCombo && (lastImagesByCombo[key] ?? lastImagesByCombo[""])); if (generated) { compositeImg.src = `/view?filename=${encodeURIComponent(generated.filename)}&type=${generated.type || "output"}&subfolder=${encodeURIComponent(generated.subfolder || "")}`; const sub = (generated.subfolder || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""); @@ -761,8 +858,9 @@ export function openGenerationDataOverlay(model, lastImagesByCombo) { const s1 = section("Model", data.modelRows); const s2 = section("LoRAs", [...data.commonLoraRows, ...data.comboLoraRows]); const s3 = section("Prompts", [...data.commonPromptRows, ...data.comboPromptRows]); - for (const s of [s1, s2, s3]) if (s) dataWrap.appendChild(s); - if (!s1 && !s2 && !s3) { + const s4 = section("Generation settings", extraInfoRows()); + for (const s of [s1, s2, s3, s4]) if (s) dataWrap.appendChild(s); + if (!s1 && !s2 && !s3 && !s4) { const empty = document.createElement("div"); css(empty, "font-size:12px;color:#555;text-align:center;padding:20px;"); empty.textContent = "No data for this combination";