From eb6126b40492f5197539c06714f08c6feb9f1aef Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 27 Mar 2026 08:37:09 -0600 Subject: [PATCH] feat: add remix UI to comparison board Per-variant element selectors (Layout, Colors, Typography, Spacing) with radio buttons in a grid. Remix button collects selections into a remixSpec object and sends via the same HTTP POST feedback mechanism. Enabled only when at least one element is selected. Board shows regenerating spinner while agent generates the hybrid variant. Co-Authored-By: Claude Opus 4.6 (1M context) --- design/src/compare.ts | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/design/src/compare.ts b/design/src/compare.ts index bededfe9..5c5f9568 100644 --- a/design/src/compare.ts +++ b/design/src/compare.ts @@ -243,6 +243,43 @@ export function generateCompareHtml(images: string[]): string { /* Hidden result elements for agent polling */ #status, #feedback-result { display: none; } + /* Remix section */ + .remix-bar { + background: #fafafa; + padding: 16px 24px; + border-top: 1px solid #e5e5e5; + } + .remix-bar .inner { max-width: 1200px; margin: 0 auto; } + .remix-bar h3 { font-size: 14px; font-weight: 600; margin-bottom: 10px; } + .remix-grid { + display: grid; + grid-template-columns: auto repeat(${images.length}, 1fr); + gap: 8px 16px; + align-items: center; + font-size: 13px; + } + .remix-grid .remix-header { font-weight: 600; text-align: center; } + .remix-grid .remix-label { color: #666; } + .remix-grid label { + display: flex; + justify-content: center; + cursor: pointer; + } + .remix-grid input[type="radio"] { accent-color: #000; } + .remix-btn { + margin-top: 12px; + padding: 8px 18px; + background: #000; + color: #fff; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + } + .remix-btn:hover { background: #333; } + .remix-btn:disabled { background: #ccc; cursor: not-allowed; } + /* Skeleton loading state */ .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); @@ -289,6 +326,21 @@ export function generateCompareHtml(images: string[]): string { +
+
+

Remix — mix elements from different variants

+
+
+ ${images.map((_, i) => `
${variantLabels[i]}
`).join("")} + ${["Layout", "Colors", "Typography", "Spacing"].map(element => ` +
${element}
+ ${images.map((_, i) => ``).join("")} + `).join("")} +
+ +
+
+
@@ -317,6 +369,37 @@ export function generateCompareHtml(images: string[]): string { }); }); + // Remix radio buttons — enable remix button when at least one element is selected + document.querySelectorAll('.remix-grid input[type="radio"]').forEach(function(radio) { + radio.addEventListener('change', function() { + var anySelected = document.querySelector('.remix-grid input[type="radio"]:checked'); + document.getElementById('remix-btn').disabled = !anySelected; + }); + }); + + // Remix button + document.getElementById('remix-btn').addEventListener('click', function() { + var remixSpec = {}; + ['layout', 'colors', 'typography', 'spacing'].forEach(function(element) { + var selected = document.querySelector('input[name="remix-' + element + '"]:checked'); + if (selected) remixSpec[element] = selected.value; + }); + if (Object.keys(remixSpec).length === 0) return; + var feedback = collectFeedback(); + feedback.regenerated = true; + feedback.regenerateAction = 'remix'; + feedback.remixSpec = remixSpec; + document.getElementById('feedback-result').textContent = JSON.stringify(feedback); + document.getElementById('status').textContent = 'regenerate'; + postFeedback(feedback).then(function(result) { + if (result && result.received) { + showRegeneratingState(); + } else if (window.__GSTACK_SERVER_URL) { + showPostFailure(feedback); + } + }); + }); + // Regenerate chiclets (toggle active) document.querySelectorAll('.regen-chiclet').forEach(chiclet => { chiclet.addEventListener('click', () => {