638 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GooseFactory — Side-by-Side Arena</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #111;
--card: #1a1a2e;
--text: #e0e0e0;
--muted: #777;
--blue: #4488FF;
--orange: #FF8844;
--gold: #FFD700;
--radius: 14px;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* ── VS Header ── */
.vs-header {
text-align: center;
padding: 16px 16px 12px;
position: relative;
}
.vs-header .review-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--muted);
margin-bottom: 4px;
}
.vs-header .review-title {
font-size: 16px;
font-weight: 700;
}
/* ── Arena Grid ── */
.arena {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0;
padding: 0 12px;
flex: 1;
min-height: 0;
}
.contender {
border-radius: var(--radius);
padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
cursor: pointer;
position: relative;
-webkit-tap-highlight-color: transparent;
}
.contender:hover { transform: scale(1.01); }
.contender-a {
border: 2px solid color-mix(in srgb, var(--blue) 50%, #333);
background: color-mix(in srgb, var(--blue) 5%, var(--card));
margin-right: -4px;
}
.contender-b {
border: 2px solid color-mix(in srgb, var(--orange) 50%, #333);
background: color-mix(in srgb, var(--orange) 5%, var(--card));
margin-left: -4px;
}
.contender .c-badge {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.contender-a .c-badge { color: var(--blue); }
.contender-b .c-badge { color: var(--orange); }
.contender .c-name {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
line-height: 1.3;
}
.contender .c-preview {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
flex: 1;
overflow-y: auto;
max-height: 200px;
}
.contender .c-preview::-webkit-scrollbar { width: 4px; }
.contender .c-preview::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
.contender .c-metrics {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.c-metric {
font-size: 10px;
background: rgba(255,255,255,0.06);
padding: 3px 8px;
border-radius: 6px;
color: var(--muted);
}
/* Winner / Loser states */
.contender.winner {
border-color: var(--gold);
box-shadow: 0 0 30px color-mix(in srgb, var(--gold) 25%, transparent);
transform: scale(1.02);
animation: winner-glow 1.2s ease-in-out;
}
.contender.loser {
opacity: 0.3;
transform: scale(0.97);
filter: grayscale(60%);
pointer-events: none;
}
@keyframes winner-glow {
0% { box-shadow: 0 0 0 transparent; }
50% { box-shadow: 0 0 50px color-mix(in srgb, var(--gold) 40%, transparent); }
100% { box-shadow: 0 0 30px color-mix(in srgb, var(--gold) 25%, transparent); }
}
/* ── VS Badge ── */
.vs-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
z-index: 10;
}
.vs-circle {
width: 44px;
height: 44px;
border-radius: 50%;
background: #222;
border: 2px solid #444;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 900;
color: #fff;
text-shadow: 0 0 8px rgba(255,255,255,0.3);
animation: vs-pulse 2s ease-in-out infinite;
}
@keyframes vs-pulse {
0%, 100% { box-shadow: 0 0 8px rgba(255,255,255,0.1); }
50% { box-shadow: 0 0 16px rgba(255,255,255,0.2); }
}
.vs-circle.decided {
animation: none;
background: var(--gold);
border-color: var(--gold);
color: #000;
}
/* ── Winner Selection Row ── */
.verdict-section {
padding: 16px 12px;
}
.verdict-label {
text-align: center;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--muted);
margin-bottom: 12px;
}
.winner-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.win-btn {
padding: 12px 24px;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
border: 2px solid;
background: transparent;
transition: all 0.25s;
min-height: 48px;
-webkit-tap-highlight-color: transparent;
}
.win-btn:active { transform: scale(0.96); }
.a-btn { border-color: var(--blue); color: var(--blue); }
.a-btn:hover { background: color-mix(in srgb, var(--blue) 15%, transparent); }
.a-btn.selected { background: var(--blue); color: #fff; }
.tie-btn { border-color: #666; color: #999; font-size: 13px; }
.tie-btn:hover { background: rgba(255,255,255,0.05); }
.tie-btn.selected { background: #666; color: #fff; }
.b-btn { border-color: var(--orange); color: var(--orange); }
.b-btn:hover { background: color-mix(in srgb, var(--orange) 15%, transparent); }
.b-btn.selected { background: var(--orange); color: #fff; }
/* ── Why Input ── */
.why-section {
padding: 0 12px 12px;
opacity: 0;
transform: translateY(10px);
transition: all 0.35s;
pointer-events: none;
}
.why-section.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.why-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
margin-bottom: 10px;
}
.why-chip {
padding: 7px 14px;
border-radius: 18px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid #444;
background: var(--card);
color: var(--text);
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
}
.why-chip:hover { border-color: #888; }
.why-chip.active { border-color: var(--gold); color: var(--gold); background: color-mix(in srgb, var(--gold) 10%, var(--card)); }
.why-input {
width: 100%;
background: var(--card);
border: 1.5px solid #333;
border-radius: 10px;
color: var(--text);
font-family: inherit;
font-size: 13px;
padding: 10px 14px;
resize: none;
outline: none;
transition: border-color 0.25s;
}
.why-input:focus { border-color: #555; }
.why-input::placeholder { color: #555; }
/* ── Submit ── */
.submit-row {
padding: 12px;
opacity: 0;
transform: translateY(10px);
transition: all 0.35s 0.1s;
pointer-events: none;
}
.submit-row.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.submit-btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
background: linear-gradient(135deg, #b8860b, #daa520);
color: #fff;
transition: all 0.25s;
}
.submit-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.4); }
.submit-btn:active { transform: scale(0.98); }
/* ── Dimension Breakdown Toggle ── */
.dimension-toggle {
text-align: center;
padding: 8px 12px 4px;
}
.dim-toggle-btn {
font-size: 11px;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
padding: 4px 12px;
border-radius: 12px;
transition: color 0.2s;
}
.dim-toggle-btn:hover { color: var(--text); }
.dim-breakdown {
padding: 0 12px 8px;
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.dim-breakdown.open { max-height: 400px; }
.dim-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 12px;
}
.dim-name { flex: 1; color: var(--muted); }
.dim-pick {
display: flex;
gap: 4px;
}
.dim-pick-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 1.5px solid #444;
background: transparent;
font-size: 10px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
color: var(--muted);
}
.dim-pick-btn:hover { border-color: #888; }
.dim-pick-btn.a-pick.active { background: var(--blue); border-color: var(--blue); color: #fff; }
.dim-pick-btn.tie-pick.active { background: #666; border-color: #666; color: #fff; }
.dim-pick-btn.b-pick.active { background: var(--orange); border-color: var(--orange); color: #fff; }
/* ── Done ── */
.done-overlay {
position: fixed; inset: 0; background: var(--bg);
display: flex; flex-direction: column; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.5s; z-index: 100;
}
.done-overlay.visible { opacity: 1; pointer-events: auto; }
.done-overlay .done-emoji { font-size: 56px; margin-bottom: 12px; }
.done-overlay .done-text { font-size: 18px; color: var(--muted); }
</style>
</head>
<body>
<div class="vs-header">
<div class="review-label">Compare & Choose</div>
<div class="review-title" id="reviewTitle">Which approach is better?</div>
</div>
<div class="arena" id="arena">
<div class="contender contender-a" id="contenderA" tabindex="0">
<div class="c-badge">🔵 Option A</div>
<div class="c-name" id="nameA">Approach A</div>
<div class="c-preview" id="previewA">Loading content…</div>
<div class="c-metrics" id="metricsA"></div>
</div>
<div class="vs-badge">
<div class="vs-circle" id="vsCircle">VS</div>
</div>
<div class="contender contender-b" id="contenderB" tabindex="0">
<div class="c-badge">🟠 Option B</div>
<div class="c-name" id="nameB">Approach B</div>
<div class="c-preview" id="previewB">Loading content…</div>
<div class="c-metrics" id="metricsB"></div>
</div>
</div>
<div class="dimension-toggle">
<button class="dim-toggle-btn" id="dimToggle">▾ Per-dimension breakdown</button>
</div>
<div class="dim-breakdown" id="dimBreakdown"></div>
<div class="verdict-section">
<div class="verdict-label">And the winner is…</div>
<div class="winner-buttons">
<button class="win-btn a-btn" data-winner="A" id="btnA">🔵 A</button>
<button class="win-btn tie-btn" data-winner="tie" id="btnTie">🤝 Tie</button>
<button class="win-btn b-btn" data-winner="B" id="btnB">🟠 B</button>
</div>
</div>
<div class="why-section" id="whySection">
<div class="why-chips" id="whyChips">
<button class="why-chip" data-why="Cleaner">Cleaner</button>
<button class="why-chip" data-why="Faster">Faster</button>
<button class="why-chip" data-why="Simpler">Simpler</button>
<button class="why-chip" data-why="More Complete">More Complete</button>
<button class="why-chip" data-why="Better DX">Better DX</button>
<button class="why-chip" data-why="Better Error Handling">Error Handling</button>
</div>
<textarea class="why-input" id="whyInput" placeholder="Why did this one win? (optional)" rows="2" maxlength="200"></textarea>
</div>
<div class="submit-row" id="submitRow">
<button class="submit-btn" id="submitBtn">Submit Verdict</button>
</div>
<div class="done-overlay" id="doneOverlay">
<div class="done-emoji">🏆</div>
<div class="done-text" id="doneText">Verdict submitted</div>
</div>
<script>
(function() {
'use strict';
const ctx = window.__FACTORY_CONTEXT__ || {};
const modalType = 'side-by-side';
const modalVersion = '1.0.0';
const startTime = Date.now();
function post(msg) { try { window.parent.postMessage(msg, '*'); } catch(e) {} }
post({ type: 'factory_modal_ready', modalType, version: modalVersion });
// ── Populate content ──
const itemA = (ctx.items && ctx.items[0]) || { name: 'Token Bucket Rate Limiter', preview: 'Classic token bucket algorithm. Refills at fixed rate. Simple to implement, predictable behavior. O(1) per request. Works well for steady traffic patterns.' };
const itemB = (ctx.items && ctx.items[1]) || { name: 'Sliding Window Counter', preview: 'Sliding window approach with sub-window counters. More accurate near window boundaries. Slightly more complex but handles bursty traffic better.' };
document.getElementById('nameA').textContent = itemA.name || 'Option A';
document.getElementById('nameB').textContent = itemB.name || 'Option B';
document.getElementById('previewA').textContent = itemA.preview || itemA.content || '';
document.getElementById('previewB').textContent = itemB.preview || itemB.content || '';
document.getElementById('reviewTitle').textContent = ctx.itemName || 'Which approach is better?';
// ── Hover / attention tracking ──
let hoverTimeA = 0;
let hoverTimeB = 0;
let hoverStartA = null;
let hoverStartB = null;
document.getElementById('contenderA').addEventListener('mouseenter', () => { hoverStartA = Date.now(); });
document.getElementById('contenderA').addEventListener('mouseleave', () => {
if (hoverStartA) { hoverTimeA += Date.now() - hoverStartA; hoverStartA = null; }
});
document.getElementById('contenderB').addEventListener('mouseenter', () => { hoverStartB = Date.now(); });
document.getElementById('contenderB').addEventListener('mouseleave', () => {
if (hoverStartB) { hoverTimeB += Date.now() - hoverStartB; hoverStartB = null; }
});
// ── Dimension breakdown ──
const DEFAULT_DIMS = [
{ id: 'code_quality', name: 'Code Quality' },
{ id: 'performance', name: 'Performance' },
{ id: 'error_handling', name: 'Error Handling' },
{ id: 'documentation', name: 'Documentation' },
{ id: 'creativity', name: 'Creativity' },
];
const dims = (ctx.dimensions && ctx.dimensions.length > 0) ? ctx.dimensions : DEFAULT_DIMS;
const dimPicks = {};
const dimBreakdown = document.getElementById('dimBreakdown');
dims.forEach(dim => {
const row = document.createElement('div');
row.className = 'dim-row';
row.innerHTML = `
<div class="dim-name">${dim.name}</div>
<div class="dim-pick">
<button class="dim-pick-btn a-pick" data-dim="${dim.id}" data-val="A">A</button>
<button class="dim-pick-btn tie-pick" data-dim="${dim.id}" data-val="tie">=</button>
<button class="dim-pick-btn b-pick" data-dim="${dim.id}" data-val="B">B</button>
</div>
`;
dimBreakdown.appendChild(row);
});
document.getElementById('dimToggle').addEventListener('click', () => {
dimBreakdown.classList.toggle('open');
document.getElementById('dimToggle').textContent = dimBreakdown.classList.contains('open')
? '▴ Per-dimension breakdown'
: '▾ Per-dimension breakdown';
});
dimBreakdown.addEventListener('click', (e) => {
const btn = e.target.closest('.dim-pick-btn');
if (!btn) return;
const dimId = btn.dataset.dim;
const val = btn.dataset.val;
dimPicks[dimId] = val;
// Update visuals for this row
btn.closest('.dim-pick').querySelectorAll('.dim-pick-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
// ── State ──
let selectedWinner = null;
let selectedWhys = new Set();
let interactions = 0;
let firstInteraction = null;
function track() { interactions++; if (!firstInteraction) firstInteraction = Date.now(); }
// ── Click contender as shortcut ──
document.getElementById('contenderA').addEventListener('click', () => pickWinner('A'));
document.getElementById('contenderB').addEventListener('click', () => pickWinner('B'));
// ── Winner buttons ──
document.querySelectorAll('.win-btn').forEach(btn => {
btn.addEventListener('click', () => pickWinner(btn.dataset.winner));
});
function pickWinner(winner) {
track();
selectedWinner = winner;
// Visual states
document.querySelectorAll('.win-btn').forEach(b => b.classList.remove('selected'));
const btnMap = { A: 'btnA', B: 'btnB', tie: 'btnTie' };
document.getElementById(btnMap[winner]).classList.add('selected');
const cA = document.getElementById('contenderA');
const cB = document.getElementById('contenderB');
cA.classList.remove('winner', 'loser');
cB.classList.remove('winner', 'loser');
if (winner === 'A') {
cA.classList.add('winner');
cB.classList.add('loser');
} else if (winner === 'B') {
cB.classList.add('winner');
cA.classList.add('loser');
}
document.getElementById('vsCircle').textContent = winner === 'tie' ? '🤝' : '🏆';
document.getElementById('vsCircle').classList.add('decided');
// Show why section
document.getElementById('whySection').classList.add('visible');
document.getElementById('submitRow').classList.add('visible');
}
// ── Why chips ──
document.querySelectorAll('.why-chip').forEach(chip => {
chip.addEventListener('click', () => {
track();
const w = chip.dataset.why;
if (selectedWhys.has(w)) { selectedWhys.delete(w); chip.classList.remove('active'); }
else { selectedWhys.add(w); chip.classList.add('active'); }
});
});
// ── Submit ──
document.getElementById('submitBtn').addEventListener('click', () => {
if (!selectedWinner) return;
track();
// Finalize hover times
if (hoverStartA) { hoverTimeA += Date.now() - hoverStartA; }
if (hoverStartB) { hoverTimeB += Date.now() - hoverStartB; }
const whyText = document.getElementById('whyInput').value.trim();
const perDimension = Object.keys(dimPicks).length > 0
? Object.entries(dimPicks).map(([dim, winner]) => ({
dimension: dim,
winner: winner === 'tie' ? 'tie' : winner,
}))
: undefined;
const prefMap = { A: 'A', B: 'B', tie: 'neither' };
const payload = {
type: 'factory_modal_response',
modalType,
modalVersion,
pipelineId: ctx.pipelineId || 'unknown',
itemId: ctx.itemId || 'unknown',
sessionId: ctx.sessionId || 'sess_' + Date.now(),
timestamp: new Date().toISOString(),
responseTimeMs: Date.now() - startTime,
feedback: {
comparison: {
preferred: prefMap[selectedWinner],
reason: whyText || undefined,
preferenceStrength: selectedWhys.size >= 3 ? 'strong' : selectedWhys.size >= 1 ? 'moderate' : 'slight',
winningFactors: Array.from(selectedWhys),
perDimension,
timeOnA_ms: Math.round(hoverTimeA),
timeOnB_ms: Math.round(hoverTimeB),
},
},
meta: {
timeToFirstInteractionMs: firstInteraction ? firstInteraction - startTime : null,
timeToDecisionMs: Date.now() - startTime,
totalInteractions: interactions,
fieldsModified: ['winner', ...(whyText ? ['whyText'] : []), ...(selectedWhys.size > 0 ? ['whyChips'] : [])],
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
viewportSize: { width: window.innerWidth, height: window.innerHeight },
hoverJourney: [
{ target: 'contenderA', durationMs: Math.round(hoverTimeA) },
{ target: 'contenderB', durationMs: Math.round(hoverTimeB) },
],
},
};
post(payload);
const winnerLabel = selectedWinner === 'A' ? 'Option A wins!' : selectedWinner === 'B' ? 'Option B wins!' : "It's a tie!";
document.getElementById('doneText').textContent = winnerLabel;
document.getElementById('doneOverlay').classList.add('visible');
setTimeout(() => {
post({ type: 'factory_modal_close', reason: 'completed' });
}, 1200);
});
})();
</script>
</body>
</html>