feat: dual-mode feedback + post-submit lifecycle in comparison board

When __GSTACK_SERVER_URL is set (injected by $D serve), the board POSTs
feedback to the server instead of only writing to hidden DOM elements.
After submit: disables all inputs, shows "Return to your coding agent."
After regenerate: shows spinner, polls /api/progress, auto-refreshes on
ready. On POST failure: shows copyable JSON fallback. On progress timeout
(5 min): shows error with /design-shotgun prompt. DOM fallback preserved
for headed browser mode and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-27 08:14:03 -06:00
parent 41cf56617a
commit 095df5b063
+107 -5
View File
@@ -346,22 +346,124 @@ export function generateCompareHtml(images: string[]): string {
submitRegenerate(detail);
});
function postFeedback(feedback) {
if (!window.__GSTACK_SERVER_URL) return Promise.resolve(null);
return fetch(window.__GSTACK_SERVER_URL + '/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedback),
}).then(function(r) { return r.json(); }).catch(function() { return null; });
}
function disableAllInputs() {
document.querySelectorAll('input, button, textarea, .star, .regen-chiclet').forEach(function(el) {
el.disabled = true;
el.style.pointerEvents = 'none';
el.style.opacity = '0.5';
});
}
function showPostSubmitState() {
disableAllInputs();
document.querySelector('.regenerate-bar').style.display = 'none';
document.getElementById('submit-btn').style.display = 'none';
document.getElementById('success-msg').style.display = 'block';
document.getElementById('success-msg').innerHTML =
'Feedback received! Return to your coding agent.' +
'<br><small style="color:#666;margin-top:8px;display:block;">Want to make more changes? Run <code>/design-shotgun</code> again.</small>';
}
function showRegeneratingState() {
disableAllInputs();
document.querySelector('.variants').innerHTML =
'<div style="text-align:center;padding:80px 24px;color:#666;">' +
'<div style="font-size:24px;margin-bottom:12px;">Generating new designs...</div>' +
'<div class="skeleton" style="width:60px;height:60px;border-radius:50%;margin:0 auto;"></div>' +
'</div>';
document.querySelector('.regenerate-bar').style.display = 'none';
document.querySelector('.submit-bar').style.display = 'none';
document.querySelector('.overall-section').style.display = 'none';
startProgressPolling();
}
function startProgressPolling() {
if (!window.__GSTACK_SERVER_URL) return;
var pollCount = 0;
var maxPolls = 150; // 5 min at 2s intervals
var pollInterval = setInterval(function() {
pollCount++;
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
document.querySelector('.variants').innerHTML =
'<div style="text-align:center;padding:80px 24px;color:#666;">' +
'<div style="font-size:18px;margin-bottom:8px;">Something went wrong.</div>' +
'<div>Run <code>/design-shotgun</code> again in your coding agent.</div>' +
'</div>';
return;
}
fetch(window.__GSTACK_SERVER_URL + '/api/progress')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'serving') {
clearInterval(pollInterval);
window.location.reload();
}
})
.catch(function() {
// Server gone, stop polling
clearInterval(pollInterval);
document.querySelector('.variants').innerHTML =
'<div style="text-align:center;padding:80px 24px;color:#666;">' +
'<div style="font-size:18px;margin-bottom:8px;">Connection lost.</div>' +
'<div>Run <code>/design-shotgun</code> again in your coding agent.</div>' +
'</div>';
});
}, 2000);
}
function showPostFailure(feedback) {
disableAllInputs();
var json = JSON.stringify(feedback, null, 2);
document.getElementById('success-msg').style.display = 'block';
document.getElementById('success-msg').innerHTML =
'<div style="color:#c00;margin-bottom:8px;">Connection lost. Copy your feedback below and paste it in your coding agent:</div>' +
'<pre style="text-align:left;background:#f5f5f5;padding:12px;border-radius:4px;font-size:12px;overflow-x:auto;cursor:pointer;" onclick="navigator.clipboard.writeText(this.textContent)">' +
json.replace(/</g, '&lt;') + '</pre>' +
'<small style="color:#666;">Click to copy</small>';
}
function submitRegenerate(detail) {
const feedback = collectFeedback();
var feedback = collectFeedback();
feedback.regenerated = true;
feedback.regenerateAction = detail;
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);
}
});
}
// Submit button
document.getElementById('submit-btn').addEventListener('click', () => {
const feedback = collectFeedback();
document.getElementById('submit-btn').addEventListener('click', function() {
var feedback = collectFeedback();
feedback.regenerated = false;
document.getElementById('feedback-result').textContent = JSON.stringify(feedback);
document.getElementById('status').textContent = 'submitted';
document.getElementById('submit-btn').disabled = true;
document.getElementById('success-msg').style.display = 'block';
postFeedback(feedback).then(function(result) {
if (result && result.received) {
showPostSubmitState();
} else if (window.__GSTACK_SERVER_URL) {
showPostFailure(feedback);
} else {
// DOM-only mode (legacy / test)
document.getElementById('submit-btn').disabled = true;
document.getElementById('success-msg').style.display = 'block';
}
});
});
function collectFeedback() {