← back to Wallco Ai
T3: regenerate-THIS-variant button on /luxe-curator.html
05d4109c8c5fea66af154bb7dcc0045bb10d8a3c · 2026-05-25 03:03:16 -0700 · Steve Abrams
POST /api/luxe-curator/regen {root_id, variant} — recovers motif +
ground from the prior queue entry for that (root_id, variant) pair,
spawns scripts/gen-luxe.js --curator-mode --variant=X --texture-cat=Y,
waits for the new luxe row, appends a regen entry to
data/luxe-curator-queue.jsonl, returns the new_id.
GET /api/luxe-curator now dedups variants per (root_id, variant) with
latest-wins (was first-wins on root_id only). New regen entries
naturally bubble up to the surface on next refresh.
UI: hover-revealed ↻ button in the top-right corner of every variant
tile. Click → optimistic "regenerating…" overlay → on response the tile
image swaps in-place with a cache-bust querystring. Failure shows
toast + restores the original. Click-bubbling guarded so the regen
button never accidentally triggers the parent tile's pick handler.
Validated:
- Bad input → 400
- No prior entry → 404
- Real regen on root #10310 variant A → #41841 → #42023 (34s wall —
composition gate retried 2× before passing, normal variance for
locked-variant runs where MAX=3)
Files touched
M public/luxe-curator.htmlM server.js
Diff
commit 05d4109c8c5fea66af154bb7dcc0045bb10d8a3c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 25 03:03:16 2026 -0700
T3: regenerate-THIS-variant button on /luxe-curator.html
POST /api/luxe-curator/regen {root_id, variant} — recovers motif +
ground from the prior queue entry for that (root_id, variant) pair,
spawns scripts/gen-luxe.js --curator-mode --variant=X --texture-cat=Y,
waits for the new luxe row, appends a regen entry to
data/luxe-curator-queue.jsonl, returns the new_id.
GET /api/luxe-curator now dedups variants per (root_id, variant) with
latest-wins (was first-wins on root_id only). New regen entries
naturally bubble up to the surface on next refresh.
UI: hover-revealed ↻ button in the top-right corner of every variant
tile. Click → optimistic "regenerating…" overlay → on response the tile
image swaps in-place with a cache-bust querystring. Failure shows
toast + restores the original. Click-bubbling guarded so the regen
button never accidentally triggers the parent tile's pick handler.
Validated:
- Bad input → 400
- No prior entry → 404
- Real regen on root #10310 variant A → #41841 → #42023 (34s wall —
composition gate retried 2× before passing, normal variance for
locked-variant runs where MAX=3)
---
public/luxe-curator.html | 70 ++++++++++++++++++++++++++++++++++++++++
server.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 151 insertions(+), 3 deletions(-)
diff --git a/public/luxe-curator.html b/public/luxe-curator.html
index 41a74b0..878d02b 100644
--- a/public/luxe-curator.html
+++ b/public/luxe-curator.html
@@ -46,6 +46,23 @@
letter-spacing:0.05em; background:rgba(255,255,255,0.92); color:#1a1a1a;
border-radius:2px; font-weight:600;
}
+ .row .tile .regen-btn {
+ position:absolute; top:6px; right:6px; width:24px; height:24px;
+ border-radius:50%; background:rgba(255,255,255,0.92); border:none;
+ color:#1a1a1a; font-size:14px; cursor:pointer; display:flex;
+ align-items:center; justify-content:center; opacity:0;
+ transition:opacity .15s, transform .15s; line-height:1; padding:0;
+ }
+ .row .tile:hover .regen-btn { opacity:1; }
+ .row .tile .regen-btn:hover { background:#fff; transform:scale(1.1); }
+ .row .tile .regen-btn:disabled { cursor:wait; }
+ .row .tile.regenerating { position:relative; }
+ .row .tile.regenerating::after {
+ content:'⟳ regenerating…'; position:absolute; inset:0;
+ background:rgba(255,255,255,0.86); display:flex; align-items:center; justify-content:center;
+ font:600 11px var(--sans); color:var(--root); letter-spacing:.04em;
+ text-transform:uppercase; z-index:5;
+ }
.row .meta {
grid-column:1/-1; display:flex; justify-content:space-between; align-items:center;
font-size:11.5px; color:var(--muted); padding:4px 4px 0;
@@ -113,6 +130,7 @@ function render() {
<img loading="lazy" src="/designs/img/by-id/${v.new_id}" alt="${v.variant}">
<span class="label ${v.variant}">Variant ${v.variant}</span>
<span class="ground">${v.ground}</span>
+ <button class="regen-btn" data-regen="${v.variant}" data-row="${i}" title="Regenerate this variant (~10s, ~$0.04)">↻</button>
</div>
`).join('');
return `
@@ -133,6 +151,8 @@ function render() {
// Wire clicks
main.querySelectorAll('[data-pick]').forEach(el => {
el.addEventListener('click', (e) => {
+ // Regen button click bubbles up — let the regen handler claim it instead
+ if (e.target.matches('[data-regen]')) return;
const kind = el.dataset.pick;
const row = parseInt(el.dataset.row, 10);
if (kind === 'variant') {
@@ -142,6 +162,56 @@ function render() {
}
});
});
+ // Regen buttons
+ main.querySelectorAll('[data-regen]').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const row = parseInt(btn.dataset.row, 10);
+ const variant = btn.dataset.regen;
+ regenVariant(row, variant);
+ });
+ });
+}
+
+async function regenVariant(rowIdx, variant) {
+ const p = DATA[rowIdx];
+ if (!p) return;
+ // Mark the tile as regenerating
+ const tile = document.querySelector(`#row-${rowIdx} [data-variant="${variant}"]`);
+ if (tile) {
+ tile.classList.add('regenerating');
+ tile.querySelector('.regen-btn')?.setAttribute('disabled', 'true');
+ }
+ toast(`Regenerating ${variant} for root #${p.root_id} (~10s)`);
+ try {
+ const r = await fetch('/api/luxe-curator/regen', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ root_id: p.root_id, variant }),
+ });
+ const j = await r.json();
+ if (!j.ok) throw new Error(j.error || 'regen failed');
+ // Swap the image to the new luxe id without a full reload — update DATA + re-render this row
+ const vIdx = p.variants.findIndex(v => v.variant === variant);
+ if (vIdx >= 0) {
+ p.variants[vIdx].new_id = j.new_id;
+ p.variants[vIdx].duration_ms = j.duration_ms;
+ }
+ // Re-render just this row
+ if (tile) {
+ const img = tile.querySelector('img');
+ if (img) img.src = `/designs/img/by-id/${j.new_id}?t=${Date.now()}`;
+ tile.dataset.luxeId = String(j.new_id);
+ tile.classList.remove('regenerating');
+ tile.querySelector('.regen-btn')?.removeAttribute('disabled');
+ }
+ toast(`Variant ${variant} regenerated → #${j.new_id} (${(j.duration_ms/1000).toFixed(1)}s)`);
+ } catch (e) {
+ if (tile) {
+ tile.classList.remove('regenerating');
+ tile.querySelector('.regen-btn')?.removeAttribute('disabled');
+ }
+ toast('Regen failed: ' + e.message);
+ }
}
async function pickVariant(rowIdx, luxeId, variant) {
diff --git a/server.js b/server.js
index 5452e31..13f7fe0 100644
--- a/server.js
+++ b/server.js
@@ -15984,14 +15984,21 @@ app.get('/api/luxe-curator', (req, res) => {
try {
const logF = path.join(__dirname, 'data', 'luxe-curator-queue.jsonl');
if (!fs.existsSync(logF)) return res.json({ count: 0, items: [], decided: 0 });
+ // Build a (root_id → variant → latest-entry) map so regen entries
+ // replace prior ones on the same (root_id, variant) key.
const byRoot = new Map();
for (const line of fs.readFileSync(logF, 'utf8').split('\n')) {
if (!line.trim()) continue;
try {
const r = JSON.parse(line);
if (!r.ok || !r.new_id) continue;
- if (!byRoot.has(r.root_id)) byRoot.set(r.root_id, { root_id: r.root_id, motif: r.motif, variants: [] });
- byRoot.get(r.root_id).variants.push({ variant: r.variant, new_id: r.new_id, ground: r.ground, duration_ms: r.duration_ms });
+ if (!byRoot.has(r.root_id)) byRoot.set(r.root_id, { root_id: r.root_id, motif: r.motif, variantMap: new Map() });
+ // Overwrite previous entry for this variant — latest wins
+ byRoot.get(r.root_id).variantMap.set(r.variant, { variant: r.variant, new_id: r.new_id, ground: r.ground, duration_ms: r.duration_ms });
+ // Keep the longest motif we've seen — regen entries may have a truncated one
+ if (r.motif && r.motif.length > (byRoot.get(r.root_id).motif || '').length) {
+ byRoot.get(r.root_id).motif = r.motif;
+ }
} catch {}
}
let decided = new Set();
@@ -16001,7 +16008,11 @@ app.get('/api/luxe-curator', (req, res) => {
} catch { /* table may not exist yet */ }
const pending = [...byRoot.values()]
.filter(r => !decided.has(r.root_id))
- .map(r => ({ ...r, variants: r.variants.sort((a, b) => a.variant.localeCompare(b.variant)) }))
+ .map(r => ({
+ root_id: r.root_id,
+ motif: r.motif,
+ variants: [...r.variantMap.values()].sort((a, b) => a.variant.localeCompare(b.variant)),
+ }))
.sort((a, b) => a.root_id - b.root_id);
res.json({ count: pending.length, decided: decided.size, items: pending });
} catch (e) {
@@ -16009,6 +16020,73 @@ app.get('/api/luxe-curator', (req, res) => {
}
});
+// POST /api/luxe-curator/regen — re-run gen-luxe.js for a specific
+// (root_id, variant) cell. Looks up the prior entry to recover the
+// motif + ground, spawns gen-luxe in curator-mode, waits for the new
+// luxe row, appends a new entry to luxe-curator-queue.jsonl, and
+// returns the new luxe_id. The /api/luxe-curator endpoint dedups on
+// (root_id, variant) with latest-wins so the UI surfaces the new image
+// on next refresh.
+//
+// Body: { root_id, variant: 'A'|'B'|'C'|'D'|'E' }
+// Returns: { ok, new_id, duration_ms } or { ok: false, error }
+app.post('/api/luxe-curator/regen', express.json({ limit: '4kb' }), async (req, res) => {
+ const rootId = parseInt(req.body?.root_id, 10);
+ const variant = String(req.body?.variant || '').toUpperCase();
+ if (!Number.isFinite(rootId) || !['A','B','C','D','E'].includes(variant)) {
+ return res.status(400).json({ ok: false, error: 'bad input' });
+ }
+ try {
+ // Find motif + ground for this (root_id, variant) from the queue log
+ const logF = path.join(__dirname, 'data', 'luxe-curator-queue.jsonl');
+ if (!fs.existsSync(logF)) return res.status(404).json({ ok: false, error: 'no queue log' });
+ let motif = '', ground = '';
+ for (const line of fs.readFileSync(logF, 'utf8').split('\n')) {
+ if (!line.trim()) continue;
+ try {
+ const r = JSON.parse(line);
+ if (r.root_id === rootId && r.variant === variant && r.motif) {
+ motif = r.motif; ground = r.ground || '';
+ }
+ } catch {}
+ }
+ if (!motif) return res.status(404).json({ ok: false, error: 'no prior entry for this (root,variant)' });
+ // Spawn gen-luxe.js
+ const { spawn } = require('child_process');
+ const args = [
+ path.join(__dirname, 'scripts', 'gen-luxe.js'),
+ String(rootId), motif,
+ `--variant=${variant}`,
+ `--texture-cat=${ground || 'silk'}`,
+ `--curator-mode`,
+ ];
+ const t0 = Date.now();
+ const child = spawn('node', args, { cwd: __dirname, env: process.env });
+ let stdout = '', stderr = '';
+ child.stdout.on('data', d => { stdout += d; });
+ child.stderr.on('data', d => { stderr += d; });
+ const code = await new Promise(resolve => child.on('close', resolve));
+ const duration_ms = Date.now() - t0;
+ const m = stdout.match(/\[gen-luxe\] PG id: (\d+)/);
+ const newId = m ? parseInt(m[1], 10) : null;
+ if (code !== 0 || !newId) {
+ return res.status(500).json({ ok: false, error: 'gen-luxe failed', code, stderr: stderr.slice(-300) });
+ }
+ // Append regen entry to queue log so dedup picks it up
+ fs.appendFileSync(logF, JSON.stringify({
+ ts: new Date().toISOString(),
+ regen: true,
+ root_id: rootId,
+ variant, ground,
+ motif: motif.slice(0, 200),
+ ok: true,
+ new_id: newId,
+ duration_ms,
+ }) + '\n');
+ res.json({ ok: true, new_id: newId, duration_ms });
+ } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
// POST /api/luxe-curator/pick — record Steve's per-root decision.
// Body: { root_id, picked_variant: 'A'|'C'|'E'|'NONE', picked_luxe_id?: <id> }
// picked_variant='NONE' means "keep the root, reject all variants".
← e7bc7e1 T2: subject-mismatch scan + gallery — flags dropped-subject
·
back to Wallco Ai
·
T4: keyboard R = regen focused row; visual focus ring; K-key c2332ef →