← back to Wallco Ai
feat(design page): move Curated Rating into admin drawer + add 'Run rating now' button
bcbd37906a0f01387dada9b57746fc071907ecc4 · 2026-05-25 00:44:52 -0700 · Steve Abrams
Steve: 'move to the right Curated Suggested Rating … still need button to
activate'.
- #ai-rating-card gains class js-admin-drawer-card; admin-sheet mover
now grabs ALL .js-admin-drawer-card elements (plus their sibling
inline-wire-up <script>) and appends them to the right drawer body,
not just the tone-on-tone .recolor-wheel.
- New 'Run rating now' button calls POST /api/design/:id/ai-rate (which
spawns scripts/rate_design.js detached), then polls
GET /api/design/:id/rating every 4s for up to 3 min, rendering as soon
as the row populates.
- Card is now display:block by default (was display:none until a rating
existed) so empty-state shows the button. render() returns false on
empty payload so polling continues correctly.
- Mover runs at DOMContentLoaded + 150ms so the existing
#toolbox-rating-slot relocator runs first; we then steal the card via
appendChild (which moves, not copies).
Files touched
Diff
commit bcbd37906a0f01387dada9b57746fc071907ecc4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 25 00:44:52 2026 -0700
feat(design page): move Curated Rating into admin drawer + add 'Run rating now' button
Steve: 'move to the right Curated Suggested Rating … still need button to
activate'.
- #ai-rating-card gains class js-admin-drawer-card; admin-sheet mover
now grabs ALL .js-admin-drawer-card elements (plus their sibling
inline-wire-up <script>) and appends them to the right drawer body,
not just the tone-on-tone .recolor-wheel.
- New 'Run rating now' button calls POST /api/design/:id/ai-rate (which
spawns scripts/rate_design.js detached), then polls
GET /api/design/:id/rating every 4s for up to 3 min, rendering as soon
as the row populates.
- Card is now display:block by default (was display:none until a rating
existed) so empty-state shows the button. render() returns false on
empty payload so polling continues correctly.
- Mover runs at DOMContentLoaded + 150ms so the existing
#toolbox-rating-slot relocator runs first; we then steal the card via
appendChild (which moves, not copies).
---
server.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 85 insertions(+), 27 deletions(-)
diff --git a/server.js b/server.js
index af18d70..00a876b 100644
--- a/server.js
+++ b/server.js
@@ -8931,9 +8931,15 @@ app.get('/design/:id', (req, res) => {
// parent_design_id. Smart-fix / regenerate-reverse / recolor each create a
// new id and unpublish the source — without this redirect, /design/<old-id>
// still renders the broken source PNG. Transitive: A → B → C resolves to C.
- // Cycle-guarded (cap at 5 hops, seen-set). Bypass with ?asis=1 (or already
- // looking at the head — no redirect to self). Query string preserved.
- if (!req.query.asis) {
+ // Cycle-guarded (cap at 5 hops, seen-set). Query string preserved.
+ // BYPASS:
+ // - ?asis=1 query param (explicit)
+ // - admin request (Steve typed the specific id; he wants to act on it,
+ // not be bounced to a descendant). 2026-05-25 — fixing "Crop & Fix
+ // looked broken" UX where every fix created a child and the admin
+ // kept redirecting away from the source. Public users still get the
+ // latest-and-best behavior.
+ if (!req.query.asis && !isAdmin(req)) {
try {
let cursor = id;
const seen = new Set([cursor]);
@@ -9185,18 +9191,29 @@ ${htmlHeader('/designs')}
btn.addEventListener('click', () => sh.classList.contains('open') ? close() : open());
document.addEventListener('click', (e) => { if (e.target && e.target.matches('[data-close-admin]')) close(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
- // After DOM settles, MOVE the tone-on-tone block into this sheet.
- // The block has an in-line script that wires its own click handlers,
- // so we move the whole .recolor-wheel + its sibling <script> together.
- document.addEventListener('DOMContentLoaded', () => {
- const tone = document.querySelector('.recolor-wheel');
- const sib = tone && tone.nextElementSibling && tone.nextElementSibling.tagName === 'SCRIPT' ? tone.nextElementSibling : null;
+ // After DOM settles, MOVE admin blocks into this sheet.
+ // We move two classes of element:
+ // .recolor-wheel — the tone-on-tone fashion palette block
+ // .js-admin-drawer-card — any admin block that opted-in by class
+ // For each, the immediately following <script> (its wire-up) is
+ // moved with it so the inline handlers keep working. setTimeout
+ // 150ms delay lets the existing #toolbox-rating-slot relocator
+ // (which appends #ai-rating-card to the left-column toolbox) run
+ // first; we then steal the card from whatever parent it ended up
+ // in via appendChild (which moves rather than copies).
+ function moveBlock(el, body) {
+ if (!el || !body) return;
+ const sib = el.nextElementSibling && el.nextElementSibling.tagName === 'SCRIPT' ? el.nextElementSibling : null;
+ body.appendChild(el);
+ if (sib) body.appendChild(sib);
+ }
+ function moveAll() {
const body = document.getElementById('admin-sheet-body');
- if (tone && body) {
- body.appendChild(tone);
- if (sib) body.appendChild(sib);
- }
- });
+ if (!body) return;
+ moveBlock(document.querySelector('.recolor-wheel'), body);
+ document.querySelectorAll('.js-admin-drawer-card').forEach(el => moveBlock(el, body));
+ }
+ document.addEventListener('DOMContentLoaded', () => setTimeout(moveAll, 150));
})();
</script>
` : ''}
@@ -11399,9 +11416,11 @@ ${(() => {
})();
</script>
- <!-- Curated Suggested Rating (graphic designer + interior designer) -->
- <div id="ai-rating-card" style="display:none;margin:14px 0;padding:14px 16px;background:#1a1816;color:#e8e2d6;border-radius:8px;border:1px solid #2a2a2a;font-size:13px">
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+ <!-- Curated Suggested Rating (graphic designer + interior designer).
+ Has class js-admin-drawer-card so the admin-sheet mover relocates
+ it into the right drawer at DOMContentLoaded. -->
+ <div id="ai-rating-card" class="js-admin-drawer-card" style="display:block;margin:14px 0;padding:14px 16px;background:#1a1816;color:#e8e2d6;border-radius:8px;border:1px solid #2a2a2a;font-size:13px">
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px">
<strong style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:16px;color:#d2b15c">Curated Suggested Rating</strong>
<span id="rating-combined" style="font-size:18px;color:#d2b15c"></span>
</div>
@@ -11410,22 +11429,61 @@ ${(() => {
<div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Graphic designer</div><div id="rating-gd-score" style="font-size:14px;color:#fff"></div><div id="rating-gd-critique" style="color:#aaa;margin-top:4px"></div></div>
<div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Interior designer</div><div id="rating-id-score" style="font-size:14px;color:#fff"></div><div id="rating-id-critique" style="color:#aaa;margin-top:4px"></div></div>
</div>
+ <!-- Trigger / re-run button — calls POST /api/design/:id/ai-rate
+ which spawns scripts/rate_design.js in the background. Then we
+ poll GET /api/design/:id/rating until the row is populated. -->
+ <div style="margin-top:12px;padding-top:10px;border-top:1px solid #2a2a2a;display:flex;align-items:center;gap:10px">
+ <button id="rate-run-btn" type="button"
+ style="padding:8px 14px;background:#d2b15c;color:#1a1816;border:0;border-radius:4px;font:600 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer">
+ ★ Run rating now
+ </button>
+ <span id="rate-run-status" style="font:11px var(--sans);color:#888"></span>
+ </div>
</div>
<script>
- (async function(){
- try {
- var r = await fetch('/api/design/${design.id}/rating');
- if (!r.ok) return;
- var j = await r.json();
- if (!j.combined_score) return;
- document.getElementById('ai-rating-card').style.display = 'block';
- document.getElementById('rating-combined').textContent = '★ ' + j.combined_score + ' / 5';
- document.getElementById('rating-summary').textContent = j.rating_summary || '';
+ (function(){
+ var DID = ${design.id};
+ function render(j) {
+ if (!j) return false;
+ var has = !!(j.combined_score || j.gd_score || j.id_score);
+ if (!has) return false;
+ document.getElementById('rating-combined').textContent = j.combined_score ? '★ ' + j.combined_score + ' / 5' : '';
+ document.getElementById('rating-summary').textContent = j.rating_summary || '';
document.getElementById('rating-gd-score').textContent = (j.gd_score || '?') + ' / 5';
document.getElementById('rating-gd-critique').textContent = j.gd_critique || '';
document.getElementById('rating-id-score').textContent = (j.id_score || '?') + ' / 5';
document.getElementById('rating-id-critique').textContent = j.id_critique || '';
- } catch(e) {}
+ return true;
+ }
+ async function fetchOnce() {
+ try { var r = await fetch('/api/design/' + DID + '/rating'); if (!r.ok) return null; return await r.json(); }
+ catch (e) { return null; }
+ }
+ // Initial load — populate if a rating already exists.
+ fetchOnce().then(render);
+ // "Run rating now" — kick the AI rater, poll for the row.
+ var btn = document.getElementById('rate-run-btn');
+ var stat = document.getElementById('rate-run-status');
+ if (btn) btn.addEventListener('click', async function(){
+ btn.disabled = true; btn.style.opacity = '0.5';
+ stat.textContent = 'Queued…';
+ try {
+ var r = await fetch('/api/design/' + DID + '/ai-rate', { method: 'POST' });
+ var j = await r.json();
+ if (!j.ok) throw new Error(j.error || 'queue failed');
+ stat.textContent = 'Rating… (this takes ~25-60s)';
+ // Poll every 4s for up to 3 minutes.
+ var start = Date.now();
+ var poll = setInterval(async function(){
+ var got = await fetchOnce();
+ if (render(got)) { clearInterval(poll); stat.textContent = '✓ Done'; btn.disabled = false; btn.style.opacity = '1'; }
+ else if (Date.now() - start > 180000) { clearInterval(poll); stat.textContent = '⚠ Timed out — try again'; btn.disabled = false; btn.style.opacity = '1'; }
+ }, 4000);
+ } catch (e) {
+ stat.textContent = '✗ ' + e.message;
+ btn.disabled = false; btn.style.opacity = '1';
+ }
+ });
})();
</script>
← 9b4e075 stack both hamburgers in upper-right corner of design image
·
back to Wallco Ai
·
design page admin sheet: add ⎋ Logout button next to close c6011e8 →