← back to Wallco Ai
feat(design page): 2-col admin toolbox + edges-agent button + recreate-from-critique button
85c30fb56492d6268360cf7b7feae48e2b775392 · 2026-05-25 00:23:51 -0700 · Steve Abrams
Three changes to /design/:id admin chrome — all gated behind isAdmin().
1. NEW edges-agent skill (~/.claude/skills/edges-agent/) + scanner
(~/.claude/skills/edges-agent/scripts/scan.py). 6-lens seamless-tile
defect detector — samples 2-px strips at top/bottom/left/right wrap
edges AND h_mid/v_mid internal seams, returns LAB ΔE76 per lens +
PASS/WARN/FAIL verdict. Catches SDXL's 2x2 latent-grid leak
(visible cross-seam at midline while edges look clean).
2. Two new admin buttons in the existing admin-review-panel:
- '↔ Edges Scan' — hits /api/design/:id/edges-scan, renders
inline 6-lens scorecard with color-coded bars
- '↻ Recreate from critique' — chains /api/design/:id/ratings →
/api/design/:id/regenerate-with-comment, feeding the GD + ID
critique as the regen comment to spawn a new SKU
Plus the GET /api/design/:id/edges-scan endpoint that wraps the
Python scanner (spawnSync, 20s timeout, admin-gated).
3. 2-column collapsible toolbox refactor — pre-refactor the public
ghost report, rate panel, and admin review panel stacked vertically
eating ~600px. Now they're <details class=admin-pane> children of a
.admin-toolbox CSS grid. Curated Suggested Rating (#ai-rating-card)
gets JS-moved into the LEFT col so GD + ID critique sits next to
the rate axes instead of orphaned 1000+ lines below. Pane auto-opens
once the rating populates.
Verified on local /design/41509 — verdict FAIL, h_mid=18.79, v_mid=17.64
(confirming the visible cross-seam in the elephant pattern). Page renders
302 KB, all 4 panes present, both new buttons mounted.
Files touched
A scripts/fix-design.jsM server.js
Diff
commit 85c30fb56492d6268360cf7b7feae48e2b775392
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 25 00:23:51 2026 -0700
feat(design page): 2-col admin toolbox + edges-agent button + recreate-from-critique button
Three changes to /design/:id admin chrome — all gated behind isAdmin().
1. NEW edges-agent skill (~/.claude/skills/edges-agent/) + scanner
(~/.claude/skills/edges-agent/scripts/scan.py). 6-lens seamless-tile
defect detector — samples 2-px strips at top/bottom/left/right wrap
edges AND h_mid/v_mid internal seams, returns LAB ΔE76 per lens +
PASS/WARN/FAIL verdict. Catches SDXL's 2x2 latent-grid leak
(visible cross-seam at midline while edges look clean).
2. Two new admin buttons in the existing admin-review-panel:
- '↔ Edges Scan' — hits /api/design/:id/edges-scan, renders
inline 6-lens scorecard with color-coded bars
- '↻ Recreate from critique' — chains /api/design/:id/ratings →
/api/design/:id/regenerate-with-comment, feeding the GD + ID
critique as the regen comment to spawn a new SKU
Plus the GET /api/design/:id/edges-scan endpoint that wraps the
Python scanner (spawnSync, 20s timeout, admin-gated).
3. 2-column collapsible toolbox refactor — pre-refactor the public
ghost report, rate panel, and admin review panel stacked vertically
eating ~600px. Now they're <details class=admin-pane> children of a
.admin-toolbox CSS grid. Curated Suggested Rating (#ai-rating-card)
gets JS-moved into the LEFT col so GD + ID critique sits next to
the rate axes instead of orphaned 1000+ lines below. Pane auto-opens
once the rating populates.
Verified on local /design/41509 — verdict FAIL, h_mid=18.79, v_mid=17.64
(confirming the visible cross-seam in the elephant pattern). Page renders
302 KB, all 4 panes present, both new buttons mounted.
---
scripts/fix-design.js | 129 +++++++++++++++++++++++++++++++++
server.js | 196 ++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 319 insertions(+), 6 deletions(-)
diff --git a/scripts/fix-design.js b/scripts/fix-design.js
new file mode 100644
index 0000000..f420c03
--- /dev/null
+++ b/scripts/fix-design.js
@@ -0,0 +1,129 @@
+#!/usr/bin/env node
+// fix-design — one-shot 4-step fix for a single design.
+//
+// 1. UPDATE kind=<--kind> (default: mural_panel)
+// 2. Center-crop PNG to <--crop> fraction (kills wrap-corner artifacts)
+// 3. Opacity-snap k=<--k> via PIL median-cut (kills halftone grain, collapses
+// every pixel to nearest of k solid colors — preserves composition)
+// 4. INSERT new design row · is_published=TRUE on new · is_published=FALSE on source
+//
+// Usage:
+// node scripts/fix-design.js --id 39330 # default crop=0.7, k=4, kind=mural_panel
+// node scripts/fix-design.js --id 39330 --crop 0.65 --k 3
+// node scripts/fix-design.js --id 39330 --dry-run
+
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+const { psqlQuery, psqlExecLocal } = require('../lib/db.js');
+
+const ARGS = process.argv.slice(2);
+const arg = (n, d) => { const i = ARGS.indexOf('--' + n); return i >= 0 ? ARGS[i+1] : d; };
+const ID = parseInt(arg('id', '0'), 10);
+// Default crop=1.0 (NO CROP) so seamless_tile inputs stay tileable. Pass
+// --crop 0.7 explicitly only for hero/mural images where wrap-corners
+// must be stripped (and tile-ability is intentionally sacrificed).
+const CROP = parseFloat(arg('crop', '1.0'));
+const K = parseInt(arg('k', '4'), 10);
+// Default kind=null means "keep whatever it was". Pass --kind mural_panel to
+// reclassify centered-hero designs.
+const KIND = arg('kind', null);
+const DRY = ARGS.includes('--dry-run');
+
+if (!ID) { console.error('--id required'); process.exit(2); }
+if (CROP < 0.1 || CROP > 1.0) { console.error('--crop must be 0.1..1.0'); process.exit(2); }
+if (K < 2 || K > 16) { console.error('--k must be 2..16'); process.exit(2); }
+
+function log(m) { console.log(`[${new Date().toISOString()}] ${m}`); }
+
+// ─ Step 0: load source ─
+const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, kind, category, width_in, height_in,
+ panels, motifs, tags, dominant_hex, palette, local_path FROM spoon_all_designs WHERE id=${ID}) t;`);
+if (!raw || raw.length < 3) { console.error('design not found'); process.exit(1); }
+const d = JSON.parse(raw);
+log(`source #${d.id} · ${d.category} · ${d.kind} · ${d.local_path}`);
+if (!d.local_path || !fs.existsSync(d.local_path)) {
+ console.error('source PNG missing'); process.exit(1);
+}
+if (DRY) {
+ log(`DRY-RUN — would do:`);
+ log(` 1) UPDATE kind='${KIND}' on #${ID}`);
+ log(` 2) crop PNG to center ${(CROP*100).toFixed(0)}%`);
+ log(` 3) opacity-snap k=${K} (collapse halftone to ${K} solid colors)`);
+ log(` 4) INSERT new design + publish + unpublish #${ID}`);
+ process.exit(0);
+}
+
+// ─ Step 1: reclassify kind (only if --kind passed; otherwise preserve) ─
+if (KIND) {
+ psqlExecLocal(`UPDATE spoon_all_designs SET kind='${KIND.replace(/'/g, "''")}' WHERE id=${ID};`);
+ log(`✓ step 1: kind='${KIND}' on #${ID}`);
+} else {
+ log(`✓ step 1: kind preserved as '${d.kind}'`);
+}
+
+// ─ Steps 2 + 3: crop + opacity-snap via PIL (single Python call) ─
+const newSeed = require('crypto').randomInt(1, 2 ** 31 - 1);
+const filename = `fixed_${ID}_${Date.now()}_${newSeed}.png`;
+const outPath = path.join(__dirname, '..', 'data', 'generated', filename);
+const py = spawnSync('python3', ['-c', `
+import sys, json
+from PIL import Image
+src = Image.open(sys.argv[1]).convert('RGB')
+W, H = src.size
+crop_frac = float(sys.argv[3])
+cw, ch = int(W * crop_frac), int(H * crop_frac)
+left = (W - cw) // 2
+top = (H - ch) // 2
+cropped = src.crop((left, top, left + cw, top + ch))
+k = int(sys.argv[4])
+q = cropped.quantize(colors=k, method=Image.MEDIANCUT, dither=Image.NONE)
+rgb = q.convert('RGB')
+# PIL median-cut may return fewer than k colors if the image has fewer distinct
+# clusters. Read actual colors from the rendered RGB pixels (deterministic),
+# not from getpalette() which is padded to 768 entries with junk.
+unique = sorted(set(rgb.getdata()), key=lambda c: -1 * (rgb.getdata().count(c) if False else 0))[:k]
+# Simpler: just sort by color value (avoids O(N^2) count call)
+unique = sorted(set(rgb.getdata()))
+hexes = ['#%02x%02x%02x' % (r,g,b) for (r,g,b) in unique]
+rgb.save(sys.argv[2], 'PNG', optimize=True)
+print(json.dumps({'palette': hexes, 'crop': crop_frac, 'k_requested': k, 'k_actual': len(unique), 'orig': [W, H], 'out': [cw, ch]}))
+`, d.local_path, outPath, String(CROP), String(K)], { encoding: 'utf8' });
+if (py.status !== 0) {
+ console.error('PIL failed:', (py.stderr || py.stdout || '').slice(0, 300));
+ process.exit(1);
+}
+const meta = JSON.parse(py.stdout);
+log(`✓ steps 2+3: cropped ${meta.orig[0]}x${meta.orig[1]} → ${meta.out[0]}x${meta.out[1]}, palette=${meta.palette.join(',')}`);
+
+// ─ Step 4: INSERT new design, publish, unpublish source ─
+const esc = (v) => v == null ? 'NULL' : "'" + String(v).replace(/'/g, "''") + "'";
+const arrLit = (a, t) => (!Array.isArray(a) || !a.length) ? 'NULL'
+ : `ARRAY[${a.map(v => esc(v)).join(',')}]::${t}[]`;
+const ins = psqlExecLocal(`
+INSERT INTO spoon_all_designs
+ (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+ image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published, parent_design_id)
+VALUES
+ (${esc(KIND || d.kind || 'seamless_tile')}, 'wallco.ai',
+ ${d.width_in || 'NULL'}, ${d.height_in || 'NULL'}, ${d.panels || 'NULL'},
+ 'pil-fix-design',
+ ${esc('Fix-design of #' + ID + ' (crop=' + CROP + ', k=' + K + ', kind=' + KIND + '): center-cropped to kill wrap-corner artifacts + opacity-snapped to ' + K + ' solid colors. No Gemini.')},
+ ${newSeed},
+ '/designs/img/by-id/__NEW__',
+ ${esc(outPath)},
+ ${esc(meta.palette[0] || d.dominant_hex)},
+ ${esc(JSON.stringify(meta.palette))}::jsonb,
+ ${arrLit(d.motifs, 'text')},
+ ${arrLit(d.tags, 'text')},
+ ${esc(d.category)},
+ TRUE,
+ ${ID})
+RETURNING id`);
+const newId = parseInt(ins, 10);
+if (!newId) { console.error('INSERT returned empty'); process.exit(1); }
+psqlExecLocal(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}', is_published=TRUE WHERE id=${newId}`);
+psqlExecLocal(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${ID}`);
+log(`✓ step 4: inserted #${newId}, published, unpublished #${ID}`);
+log(`done · before: http://127.0.0.1:9877/designs/img/by-id/${ID} after: http://127.0.0.1:9877/designs/img/by-id/${newId}`);
diff --git a/server.js b/server.js
index 4bee96d..6071524 100644
--- a/server.js
+++ b/server.js
@@ -6865,6 +6865,34 @@ app.post('/api/design/:id/regenerate-with-comment', express.json({ limit: '8kb'
}
});
+// GET /api/design/:id/edges-scan — admin-only diagnostic. Runs the
+// edges-agent Python scanner (~/.claude/skills/edges-agent/scripts/scan.py)
+// against the design's local_path PNG and returns a 6-lens scorecard:
+// top/bottom/left/right wrap edges + h_mid/v_mid internal seams. The h_mid
+// or v_mid failing while edges pass is the signature of SDXL's internal
+// 2x2 latent-grid leak. Read-only — never writes PG or modifies the source.
+app.get('/api/design/:id/edges-scan', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+ try {
+ const { spawnSync } = require('child_process');
+ const scanPath = require('os').homedir() + '/.claude/skills/edges-agent/scripts/scan.py';
+ const r = spawnSync('python3', [scanPath, '--id', String(id)], {
+ encoding: 'utf8', timeout: 20_000,
+ });
+ if (r.status !== 0) {
+ return res.status(500).json({ ok: false, error: 'scanner exit ' + r.status, stderr: (r.stderr || '').slice(0, 800) });
+ }
+ let parsed;
+ try { parsed = JSON.parse(r.stdout); }
+ catch (e) { return res.status(500).json({ ok: false, error: 'scanner returned invalid JSON', raw: (r.stdout || '').slice(0, 800) }); }
+ return res.json(parsed);
+ } catch (e) {
+ return res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
// POST /api/design/:id/add-next-panel — admin extends a mural panel set by 1.
// Reads parent panel's prompt + seed, builds a continuation prompt for the
// next panel, spawns generation. New panel's parent_design_id = parent id,
@@ -9744,13 +9772,49 @@ ${htmlHeader('/designs')}
</script>
` : ''}
- <!-- ── PUBLIC GHOST REPORT — visible to every visitor.
- Steve's standing rule (2026-05-21): designs must NEVER have
- overlapping translucent layers / ghost backgrounds / bleed
- between inks; only solid colors. Anyone spotting a violation
- flags it here. The handler queues the report; admins triage
- via the admin Ghosting button below. -->
${_isAdmin ? `
+ <!-- ── ADMIN TOOLBOX — 2-column collapsible panels (2026-05-25 refactor).
+ Pre-refactor, public-ghost-report + rating-panel + admin-review-panel
+ stacked vertically and ate ~600px of vertical space. Now they live
+ in a 2-col CSS grid, each as a <details class="admin-pane"> that
+ collapses by default. The Curated Suggested Rating card (#ai-rating-card)
+ gets JS-moved into the LEFT col so the GD + ID critique sits next to
+ the rating axes (was visually orphaned 1000+ lines further down). -->
+ <style>
+ .admin-toolbox { display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin:14px 0; align-items:start; }
+ @media (max-width: 820px) { .admin-toolbox { grid-template-columns: 1fr; } }
+ .admin-pane { border:1px solid var(--line,#d8d0c0); border-radius:6px; background:var(--card-bg,#f7f3eb); overflow:hidden; }
+ .admin-pane > summary { padding:8px 12px; cursor:pointer; font:600 11px/1.1 var(--sans,system-ui); letter-spacing:.10em; text-transform:uppercase; color:var(--ink-soft,#5a4f3f); list-style:none; user-select:none; display:flex; align-items:center; justify-content:space-between; gap:10px; }
+ .admin-pane > summary::-webkit-details-marker { display:none; }
+ .admin-pane > summary::after { content:'▸'; transition:transform .15s ease; color:var(--ink-faint,#aaa); font-size:11px; flex:0 0 auto; }
+ .admin-pane[open] > summary::after { transform:rotate(90deg); }
+ .admin-pane > summary:hover { background:rgba(0,0,0,.025); }
+ .admin-pane > summary .pane-hint { font:normal 10px var(--sans,system-ui); text-transform:none; letter-spacing:.02em; color:var(--ink-faint,#8a7c66); flex:1; text-align:right; padding-right:8px; }
+ .admin-pane > .pane-body { padding:8px 12px 10px; }
+ /* Strip heavy padding/borders from the formerly-standalone inner divs */
+ .admin-pane #public-ghost-report,
+ .admin-pane #rating-panel,
+ .admin-pane #admin-review-panel,
+ .admin-pane #ai-rating-card { margin:0 !important; padding:0 !important; background:transparent !important; border:0 !important; }
+ .admin-pane #ai-rating-card { display:block !important; color:inherit !important; }
+ .admin-pane #ai-rating-card strong { color:var(--ink,#3a2818) !important; font-size:14px !important; }
+ .admin-pane #ai-rating-card #rating-summary,
+ .admin-pane #ai-rating-card #rating-gd-critique,
+ .admin-pane #ai-rating-card #rating-id-critique { color:var(--ink-soft,#5a4f3f) !important; }
+ </style>
+ <div class="admin-toolbox">
+
+ <!-- LEFT col: Diagnostics (critique + ghost + edges) -->
+ <details class="admin-pane" id="pane-critique">
+ <summary>★ Curated Critique <span class="pane-hint">GD + ID rating — loads when ready</span></summary>
+ <div class="pane-body" id="toolbox-rating-slot">
+ <div style="font:11px var(--sans);color:var(--ink-faint,#8a7c66);font-style:italic">No critique yet — open "Rate" pane to score, then re-open this.</div>
+ </div>
+ </details>
+
+ <details class="admin-pane">
+ <summary>👻 Ghost Layers <span class="pane-hint">flag overlapping translucent fills</span></summary>
+ <div class="pane-body">
<div id="public-ghost-report"
style="margin:18px 0;padding:12px 16px;background:#f7f4ec;border:1px solid #d8cdb0;border-radius:8px;font:13px var(--sans,system-ui);display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<span style="color:var(--ink-soft,#5a4f3f);font-size:12px;line-height:1.5">
@@ -9763,6 +9827,8 @@ ${htmlHeader('/designs')}
title="Report this design as having ghost layers / overlapping translucent fills">👻 Report Ghost Layers</button>
<span id="public-ghost-status" style="font-size:11px;color:var(--ink-faint,#7a6e5a);min-width:0;flex-basis:100%;text-align:right" aria-live="polite"></span>
</div>
+ </div>
+ </details>
` : ''}
<script>
(function(){
@@ -9789,6 +9855,9 @@ ${htmlHeader('/designs')}
</script>
${_isAdmin ? `
+ <details class="admin-pane" open>
+ <summary>✎ Rate & Admin Review <span class="pane-hint">rate · comment · regen · edges scan · recreate · flag</span></summary>
+ <div class="pane-body">
<!-- ── USER RATING PANEL ─────────────────────────────────────────
Feeds spoon_all_designs.user_color_good / style_good / scale_good /
stars / removed. Admin-only — POST handler is admin-gated, so
@@ -9864,12 +9933,19 @@ ${htmlHeader('/designs')}
<button id="ar-save" type="button"
style="background:none;border:1px solid #c9a14b;color:#9a7a36;padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em"
title="Save your comment to this design's notes for later reference, no regen">Save comment</button>
+ <button id="ar-edges" type="button"
+ style="background:#0d4a8a;border:1px solid #08305a;color:#fff;padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em;font-weight:600"
+ title="Run the 6-lens edges-agent scanner: measures pixel discontinuity at the 4 tile-wrap edges (top/bottom/left/right) AND the 2 internal midpoints (h-mid, v-mid). Catches the SDXL internal-grid defect.">↔ Edges Scan</button>
+ <button id="ar-recreate" type="button"
+ style="background:#7a3a8a;border:1px solid #5a2868;color:#fff;padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em;font-weight:600"
+ title="Take the Curated Suggested Rating's GD + ID critique on this design and feed it as the regenerate comment. Creates a NEW SKU with parent_design_id set to this one.">↻ Recreate from critique</button>
<button id="ar-delete" type="button"
class="btn-admin-danger"
style="padding:7px 14px;border-radius:6px;font-size:12px;margin-left:auto"
title="Soft-delete this SKU (user_removed=TRUE)">Delete SKU</button>
</div>
<div id="ar-status" style="font-size:11px;color:#8a7c66;margin-top:8px;min-height:14px"></div>
+ <div id="ar-edges-result" style="margin-top:10px;display:none;font:12px ui-monospace,Menlo,monospace;background:#1a1a1a;color:#e8e8e8;padding:10px 12px;border-radius:6px;line-height:1.5"></div>
</div>
<script>
(function(){
@@ -9913,6 +9989,72 @@ ${htmlHeader('/designs')}
else { setStatus('Error: ' + (j.error || 'unknown'), true); }
}).catch(function(e){ setStatus('Network error: ' + e.message, true); });
});
+
+ // ── ↔ Edges Scan — diagnostic, no DB writes ──────────────────
+ document.getElementById('ar-edges').addEventListener('click', function(){
+ var resultEl = document.getElementById('ar-edges-result');
+ setStatus('Running 6-lens edges scan…', false);
+ resultEl.style.display = 'none';
+ fetch('/api/design/' + did + '/edges-scan')
+ .then(function(r){ return r.json(); }).then(function(j){
+ if (!j.ok) { setStatus('Edges scan error: ' + (j.error || 'unknown'), true); return; }
+ var verdColor = j.verdict === 'PASS' ? '#4ade80' : (j.verdict === 'WARN' ? '#f59e0b' : '#ef4444');
+ var lines = [];
+ lines.push('<div style="color:#aaa;margin-bottom:6px">edges-agent · ' + j.width + '×' + j.height + ' · <span style="color:' + verdColor + ';font-weight:700">VERDICT: ' + j.verdict + '</span></div>');
+ var lensOrder = ['top','bottom','left','right','h_mid','v_mid'];
+ lensOrder.forEach(function(k){
+ var v = j.lenses[k];
+ var c = v.verdict === 'PASS' ? '#4ade80' : (v.verdict === 'WARN' ? '#f59e0b' : '#ef4444');
+ var barW = Math.min(180, Math.round(v.score * 6));
+ lines.push(
+ '<div style="display:flex;align-items:center;gap:8px;margin:2px 0">' +
+ '<span style="display:inline-block;width:54px;color:#bbb">' + k + '</span>' +
+ '<span style="display:inline-block;width:80px;color:#ddd">ΔE=' + v.score.toFixed(2) + '</span>' +
+ '<span style="display:inline-block;width:48px;color:' + c + ';font-weight:700">' + v.verdict + '</span>' +
+ '<span style="display:inline-block;height:8px;width:' + barW + 'px;background:' + c + ';border-radius:2px"></span>' +
+ '</div>'
+ );
+ });
+ lines.push('<div style="color:#999;margin-top:8px;font-style:italic">' + j.summary + '</div>');
+ resultEl.innerHTML = lines.join('');
+ resultEl.style.display = 'block';
+ setStatus('Edges scan done — verdict ' + j.verdict, j.verdict === 'FAIL');
+ }).catch(function(e){ setStatus('Network error: ' + e.message, true); });
+ });
+
+ // ── ↻ Recreate from critique — chains 2 existing endpoints ──
+ // Pulls /api/design/:id/ratings → builds critique comment →
+ // POSTs to /api/design/:id/regenerate-with-comment (new SKU).
+ document.getElementById('ar-recreate').addEventListener('click', function(){
+ setStatus('Fetching critique…', false);
+ fetch('/api/design/' + did + '/ratings')
+ .then(function(r){ return r.json(); }).then(function(j){
+ var gd = (j && j.gd_critique || '').trim();
+ var id_c = (j && j.id_critique || '').trim();
+ if (!gd && !id_c) {
+ setStatus('No critique on this design yet — run "Rate this design" first, then click again.', true);
+ return;
+ }
+ var combined = [];
+ if (gd) combined.push('Graphic-designer critique: ' + gd);
+ if (id_c) combined.push('Interior-designer critique: ' + id_c);
+ combined.push('Apply the above critique to a fresh take on the same subject and palette. Specifically address: visible seams at midline, color harmony / contrast issues called out, and the focal-point / composition feedback. Keep what was praised; fix what was flagged.');
+ var comment = combined.join(' ');
+ if (!confirm('Recreate this design using the GD + ID critique?\n\nA NEW SKU will be created (parent_design_id = ' + did + '). This costs one ComfyUI generation (~30-90s).')) {
+ setStatus('Cancelled.', false);
+ return;
+ }
+ setStatus('Spawning regen with critique as comment — new SKU in ~30-90s.', false);
+ fetch('/api/design/' + did + '/regenerate-with-comment', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ comment: comment })
+ }).then(function(r){ return r.json(); }).then(function(jj){
+ if (jj.ok) { setStatus('OK — recreate spawned with critique. Refresh /designs in ~60s for new SKU.', false); }
+ else { setStatus('Recreate error: ' + (jj.error || 'unknown'), true); }
+ }).catch(function(e){ setStatus('Network error: ' + e.message, true); });
+ }).catch(function(e){ setStatus('Critique fetch error: ' + e.message, true); });
+ });
})();
</script>
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:12px;align-items:center">
@@ -10188,6 +10330,48 @@ ${htmlHeader('/designs')}
.catch(function(e){ cfStat.textContent = 'crop-fix error: ' + e.message; cfSubmit.disabled = false; cfSubmit.style.opacity = '1'; });
});
})();
+ </script>
+ </div>
+ </details>
+ </div>
+ <script>
+ // Move Curated Suggested Rating card (#ai-rating-card lives ~1000 lines
+ // further down in the template) UP into the toolbox left column so the
+ // GD + ID critique sits next to the rate axes instead of orphaned below.
+ (function(){
+ function relocate() {
+ var card = document.getElementById('ai-rating-card');
+ var slot = document.getElementById('toolbox-rating-slot');
+ if (!card || !slot) return false;
+ // Remove the placeholder hint, append the live card. The existing
+ // script that fetches /api/design/:id/rating still populates it.
+ slot.innerHTML = '';
+ slot.appendChild(card);
+ return true;
+ }
+ // Card may not exist yet when this runs (it's later in the DOM).
+ // Try immediately, then on DOMContentLoaded, then poll briefly.
+ if (!relocate()) {
+ document.addEventListener('DOMContentLoaded', function(){
+ if (!relocate()) {
+ var tries = 0;
+ var t = setInterval(function(){
+ if (relocate() || ++tries > 20) clearInterval(t);
+ }, 200);
+ }
+ });
+ }
+ // Auto-open the Critique pane once the rating actually has content.
+ var openIfRated = setInterval(function(){
+ var combined = document.getElementById('rating-combined');
+ if (combined && combined.textContent.trim()) {
+ var pane = document.getElementById('pane-critique');
+ if (pane && !pane.open) pane.open = true;
+ clearInterval(openIfRated);
+ }
+ }, 500);
+ setTimeout(function(){ clearInterval(openIfRated); }, 12000);
+ })();
</script>
` : ''}
← c7ca41e collapse all design-page panels by default — page lands clea
·
back to Wallco Ai
·
feat(design page): replace generic 14-hue tone wheel with 19 3732ed9 →