805 lines
22 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 — Applause Meter</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #111;
--card: #1a1a2e;
--text: #e0e0e0;
--muted: #888;
--radius: 14px;
--meter-empty: #222;
--level-1: #4488FF;
--level-2: #44FF88;
--level-3: #FFD700;
--level-4: #FF4444;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px 16px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
/* ── Summary Card ── */
.summary-card {
background: var(--card);
border-radius: var(--radius);
padding: 14px 20px;
width: 100%;
max-width: 440px;
margin-bottom: 20px;
border: 1px solid #2a2a3e;
text-align: center;
}
.summary-card .label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--muted);
margin-bottom: 4px;
}
.summary-card h3 {
font-size: 15px;
font-weight: 700;
}
/* ── Title ── */
.title {
font-size: 22px;
font-weight: 800;
text-align: center;
margin-bottom: 4px;
letter-spacing: 1px;
}
.subtitle {
font-size: 13px;
color: var(--muted);
text-align: center;
margin-bottom: 20px;
}
/* ── Level Display ── */
.level-display {
font-size: 18px;
font-weight: 800;
text-align: center;
margin-bottom: 12px;
min-height: 28px;
transition: all 0.3s;
letter-spacing: 1px;
}
/* ── Meter ── */
.meter-container {
width: 100%;
max-width: 440px;
margin-bottom: 8px;
}
.meter-bar {
height: 36px;
background: var(--meter-empty);
border-radius: 18px;
overflow: hidden;
position: relative;
border: 2px solid #333;
}
.meter-fill {
height: 100%;
width: 0%;
border-radius: 16px;
transition: width 0.08s linear;
position: relative;
background: linear-gradient(90deg, var(--level-1), var(--level-2), var(--level-3), var(--level-4));
background-size: 400% 100%;
}
.meter-fill::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 20px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3));
border-radius: 0 16px 16px 0;
}
.meter-glow {
position: absolute;
top: -4px;
right: -2px;
bottom: -4px;
width: 12px;
border-radius: 50%;
background: white;
box-shadow: 0 0 12px white, 0 0 24px white;
opacity: 0;
transition: opacity 0.2s;
}
.meter-labels {
display: flex;
justify-content: space-between;
font-size: 10px;
color: #555;
margin-top: 4px;
padding: 0 4px;
}
/* ── Clap Zone ── */
.clap-zone {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
}
.clap-btn {
width: 120px;
height: 120px;
border-radius: 50%;
background: radial-gradient(circle at 40% 40%, #2a2a4e, #1a1a2e);
border: 3px solid #444;
cursor: pointer;
font-size: 56px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.08s, border-color 0.2s, box-shadow 0.2s;
position: relative;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.clap-btn:hover {
border-color: #666;
box-shadow: 0 0 20px rgba(255,255,255,0.1);
}
.clap-btn:active, .clap-btn.tapped {
transform: scale(1.15);
}
.clap-btn.inactive {
opacity: 0.3;
pointer-events: none;
border-color: #333;
}
.clap-btn.waiting {
animation: waiting-pulse 1.5s ease-in-out infinite;
}
@keyframes waiting-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255,215,0,0); border-color: #444; }
50% { box-shadow: 0 0 20px rgba(255,215,0,0.3); border-color: #FFD700; }
}
.timer-display {
font-size: 28px;
font-weight: 800;
font-variant-numeric: tabular-nums;
margin-top: 10px;
min-height: 36px;
transition: color 0.3s;
}
.timer-display.urgent { color: var(--level-4); }
.tap-count {
font-size: 14px;
color: var(--muted);
margin-top: 4px;
}
/* ── Start Prompt ── */
.start-prompt {
text-align: center;
margin-top: 12px;
}
.start-btn {
padding: 14px 32px;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, #FFD700, #FF8C00);
color: #000;
font-size: 16px;
font-weight: 800;
cursor: pointer;
transition: all 0.2s;
letter-spacing: 1px;
}
.start-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 20px rgba(255,215,0,0.3); }
.start-btn:active { transform: scale(0.97); }
/* ── Reason Chips ── */
.reason-section {
width: 100%;
max-width: 440px;
margin-top: 20px;
opacity: 0;
transform: translateY(16px);
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
pointer-events: none;
}
.reason-section.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.reason-label {
font-size: 12px;
color: var(--muted);
margin-bottom: 10px;
text-align: center;
}
.reason-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.chip {
padding: 8px 18px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 2px solid #444;
background: var(--card);
color: var(--text);
transition: all 0.25s;
-webkit-tap-highlight-color: transparent;
}
.chip:hover { border-color: #666; background: #222; }
.chip.active {
border-color: #FFD700;
background: rgba(255,215,0,0.12);
color: #FFD700;
}
/* ── Submit ── */
.submit-row {
width: 100%;
max-width: 440px;
margin-top: 16px;
opacity: 0;
transform: translateY(12px);
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) 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, #FFD700, #FF8C00);
color: #000;
transition: all 0.25s;
}
.submit-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255,215,0,0.3); }
.submit-btn:active { transform: scale(0.98); }
/* ── Confetti canvas ── */
#confettiCanvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 90;
}
/* ── Shaking ── */
.shaking-light {
animation: shake-light 0.15s infinite;
}
.shaking-heavy {
animation: shake-heavy 0.1s infinite;
}
@keyframes shake-light {
0%, 100% { transform: translate(0, 0); }
25% { transform: translate(-2px, 1px); }
50% { transform: translate(2px, -1px); }
75% { transform: translate(-1px, -1px); }
}
@keyframes shake-heavy {
0%, 100% { transform: translate(0, 0); }
25% { transform: translate(-4px, 2px); }
50% { transform: translate(4px, -3px); }
75% { transform: translate(-3px, 3px); }
}
/* ── Screen flash ── */
.flash-overlay {
position: fixed;
inset: 0;
background: white;
opacity: 0;
pointer-events: none;
z-index: 80;
transition: opacity 0.05s;
}
.flash-overlay.flash { opacity: 0.15; }
/* ── Done Overlay ── */
.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.4s;
z-index: 100;
}
.done-overlay.visible { opacity: 1; pointer-events: auto; }
.done-overlay .done-emoji { font-size: 64px; margin-bottom: 12px; }
.done-overlay .done-text { font-size: 18px; color: var(--muted); }
.done-overlay .done-sub { font-size: 14px; color: #555; margin-top: 4px; }
/* ── Clap Ripple ── */
.clap-ripple {
position: absolute;
width: 120px;
height: 120px;
border-radius: 50%;
border: 2px solid rgba(255,215,0,0.6);
animation: ripple-out 0.5s ease-out forwards;
pointer-events: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@keyframes ripple-out {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
100% { transform: translate(-50%, -50%) scale(2); opacity: 0; }
}
</style>
</head>
<body>
<div class="summary-card">
<div class="label">Applauding</div>
<h3 id="itemName">Loading…</h3>
</div>
<div class="title">👏 APPLAUSE METER 👏</div>
<div class="subtitle" id="subtitle">How much applause does this deserve?</div>
<div class="level-display" id="levelDisplay">&nbsp;</div>
<div class="meter-container">
<div class="meter-bar">
<div class="meter-fill" id="meterFill">
<div class="meter-glow" id="meterGlow"></div>
</div>
</div>
<div class="meter-labels">
<span>Golf clap</span>
<span>Nice</span>
<span>Standing O!</span>
<span>THUNDER</span>
</div>
</div>
<div class="clap-zone" id="clapZone">
<button class="clap-btn waiting" id="clapBtn">👏</button>
<div class="timer-display" id="timerDisplay">&nbsp;</div>
<div class="tap-count" id="tapCount">&nbsp;</div>
</div>
<div class="start-prompt" id="startPrompt">
<button class="start-btn" id="startBtn">TAP TO START! 🎬</button>
</div>
<div class="reason-section" id="reasonSection">
<div class="reason-label">What deserves the applause?</div>
<div class="reason-chips">
<button class="chip" data-reason="Clever">✨ Clever</button>
<button class="chip" data-reason="Clean">🧹 Clean</button>
<button class="chip" data-reason="Fast">⚡ Fast</button>
<button class="chip" data-reason="Thorough">📋 Thorough</button>
<button class="chip" data-reason="Surprising">🎁 Surprising</button>
</div>
</div>
<div class="submit-row" id="submitRow">
<button class="submit-btn" id="submitBtn">Submit Applause 👏</button>
</div>
<canvas id="confettiCanvas"></canvas>
<div class="flash-overlay" id="flashOverlay"></div>
<div class="done-overlay" id="doneOverlay">
<div class="done-emoji" id="doneEmoji">👏</div>
<div class="done-text" id="doneText">Applause recorded!</div>
<div class="done-sub" id="doneSub"></div>
</div>
<script>
(function() {
'use strict';
const ctx = window.__FACTORY_CONTEXT__ || {};
const modalType = 'applause-meter';
const modalVersion = '1.0.0';
const startTime = Date.now();
let interactionCount = 0;
let firstInteractionTime = null;
function trackInteraction() {
interactionCount++;
if (!firstInteractionTime) firstInteractionTime = Date.now();
}
function post(msg) { try { window.parent.postMessage(msg, '*'); } catch(e) {} }
// Populate
document.getElementById('itemName').textContent = ctx.itemName || ctx.pipelineName || 'MCP Server Output';
// ── State ──
const DURATION = 5000; // 5 seconds
const MAX_TAPS_FOR_FULL = 40; // ~8 taps/sec for max
let phase = 'idle'; // idle | active | done
let tapCount = 0;
let tapTimestamps = [];
let meterPct = 0;
let timerStart = 0;
let timerInterval = null;
let decayInterval = null;
let selectedReasons = new Set();
const clapBtn = document.getElementById('clapBtn');
const meterFill = document.getElementById('meterFill');
const meterGlow = document.getElementById('meterGlow');
const timerDisplay = document.getElementById('timerDisplay');
const tapCountEl = document.getElementById('tapCount');
const levelDisplay = document.getElementById('levelDisplay');
const startPrompt = document.getElementById('startPrompt');
const subtitle = document.getElementById('subtitle');
const flashOverlay = document.getElementById('flashOverlay');
// ── Meter Update ──
function updateMeter() {
const pct = Math.min(100, meterPct);
meterFill.style.width = pct + '%';
// Gradient position shifts with fill
meterFill.style.backgroundPosition = `${100 - pct}% 0`;
// Glow at leading edge
meterGlow.style.opacity = pct > 5 ? '1' : '0';
// Level display
let level, color;
if (pct < 25) {
level = '🏌️ Polite Golf Clap';
color = 'var(--level-1)';
} else if (pct < 50) {
level = '👏 Nice Applause!';
color = 'var(--level-2)';
} else if (pct < 75) {
level = '🎉 Standing Ovation!';
color = 'var(--level-3)';
} else {
level = '🌩️ THUNDEROUS APPLAUSE!';
color = 'var(--level-4)';
}
levelDisplay.textContent = level;
levelDisplay.style.color = color;
// Shaking
document.body.classList.remove('shaking-light', 'shaking-heavy');
if (pct >= 75) {
document.body.classList.add('shaking-heavy');
} else if (pct >= 50) {
document.body.classList.add('shaking-light');
}
// Flash at high levels
if (pct >= 90) {
flashOverlay.classList.add('flash');
setTimeout(() => flashOverlay.classList.remove('flash'), 60);
}
}
// ── Confetti ──
const confettiCanvas = document.getElementById('confettiCanvas');
const confCtx = confettiCanvas.getContext('2d');
let confettiPieces = [];
let confettiRunning = false;
function resizeConfetti() {
confettiCanvas.width = window.innerWidth;
confettiCanvas.height = window.innerHeight;
}
resizeConfetti();
window.addEventListener('resize', resizeConfetti);
function spawnConfetti(count) {
const colors = ['#FFD700', '#FF4444', '#44FF88', '#4488FF', '#FF88FF', '#FF8C00', '#00FFFF'];
for (let i = 0; i < count; i++) {
confettiPieces.push({
x: Math.random() * confettiCanvas.width,
y: -10 - Math.random() * 40,
w: Math.random() * 8 + 4,
h: Math.random() * 6 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
vx: (Math.random() - 0.5) * 4,
vy: Math.random() * 3 + 2,
rot: Math.random() * 360,
rotSpeed: (Math.random() - 0.5) * 12,
life: 1,
});
}
if (!confettiRunning) {
confettiRunning = true;
animateConfetti();
}
}
function animateConfetti() {
confCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
confettiPieces = confettiPieces.filter(p => p.life > 0);
confettiPieces.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08;
p.rot += p.rotSpeed;
p.life -= 0.005;
confCtx.save();
confCtx.translate(p.x, p.y);
confCtx.rotate(p.rot * Math.PI / 180);
confCtx.globalAlpha = Math.max(0, p.life);
confCtx.fillStyle = p.color;
confCtx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
confCtx.restore();
});
if (confettiPieces.length > 0) {
requestAnimationFrame(animateConfetti);
} else {
confettiRunning = false;
}
}
// ── Clap Ripple ──
function spawnRipple() {
const ripple = document.createElement('div');
ripple.className = 'clap-ripple';
clapBtn.parentElement.style.position = 'relative';
const rect = clapBtn.getBoundingClientRect();
const zone = document.getElementById('clapZone');
const zoneRect = zone.getBoundingClientRect();
ripple.style.top = (rect.top - zoneRect.top + rect.height / 2) + 'px';
ripple.style.left = (rect.left - zoneRect.left + rect.width / 2) + 'px';
zone.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
}
// ── Start ──
document.getElementById('startBtn').addEventListener('click', () => {
trackInteraction();
startClapping();
});
function startClapping() {
phase = 'active';
timerStart = Date.now();
tapCount = 0;
tapTimestamps = [];
meterPct = 0;
startPrompt.style.display = 'none';
subtitle.textContent = 'TAP TAP TAP! 🔥';
clapBtn.classList.remove('waiting', 'inactive');
timerDisplay.textContent = '5.0s';
tapCountEl.textContent = '0 claps';
// Start timer
timerInterval = setInterval(() => {
const elapsed = Date.now() - timerStart;
const remaining = Math.max(0, (DURATION - elapsed) / 1000);
timerDisplay.textContent = remaining.toFixed(1) + 's';
if (remaining <= 1.5) timerDisplay.classList.add('urgent');
if (elapsed >= DURATION) {
endClapping();
}
}, 50);
// Decay: meter slowly loses if not tapping
decayInterval = setInterval(() => {
if (phase === 'active') {
meterPct = Math.max(0, meterPct - 0.3);
updateMeter();
}
}, 100);
}
// ── Clap Handler ──
function handleClap(e) {
if (phase !== 'active') return;
e.preventDefault();
trackInteraction();
tapCount++;
tapTimestamps.push(Date.now() - timerStart);
// Fill amount: diminishing returns at high counts
const fillPerTap = Math.max(1.5, (100 / MAX_TAPS_FOR_FULL) * (1 + (tapCount < 10 ? 0.3 : 0)));
meterPct = Math.min(100, meterPct + fillPerTap);
tapCountEl.textContent = tapCount + ' claps';
updateMeter();
// Visual feedback
clapBtn.classList.add('tapped');
setTimeout(() => clapBtn.classList.remove('tapped'), 80);
spawnRipple();
// Confetti at high levels
if (meterPct >= 75) {
spawnConfetti(3);
}
if (meterPct >= 95) {
spawnConfetti(5);
}
}
clapBtn.addEventListener('mousedown', handleClap);
clapBtn.addEventListener('touchstart', handleClap, { passive: false });
// Also allow starting by first clap
clapBtn.addEventListener('click', () => {
if (phase === 'idle') {
trackInteraction();
startClapping();
}
});
// ── End Clapping ──
function endClapping() {
phase = 'done';
clearInterval(timerInterval);
clearInterval(decayInterval);
document.body.classList.remove('shaking-light', 'shaking-heavy');
clapBtn.classList.add('inactive');
timerDisplay.textContent = 'TIME!';
timerDisplay.classList.remove('urgent');
subtitle.textContent = 'Final score locked in!';
// Lock final meter
updateMeter();
// Final confetti burst if thunderous
if (meterPct >= 75) {
spawnConfetti(40);
flashOverlay.classList.add('flash');
setTimeout(() => flashOverlay.classList.remove('flash'), 150);
}
// Show reason chips
setTimeout(() => {
document.getElementById('reasonSection').classList.add('visible');
document.getElementById('submitRow').classList.add('visible');
}, 600);
}
// ── Reason Chips ──
document.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
trackInteraction();
const reason = chip.dataset.reason;
if (selectedReasons.has(reason)) {
selectedReasons.delete(reason);
chip.classList.remove('active');
} else {
selectedReasons.add(reason);
chip.classList.add('active');
}
});
});
// ── Submit ──
document.getElementById('submitBtn').addEventListener('click', () => {
trackInteraction();
const responseTimeMs = Date.now() - startTime;
// Calculate taps per second curve
const tpsWindows = [];
for (let t = 0; t < DURATION; t += 1000) {
const tapsInWindow = tapTimestamps.filter(ts => ts >= t && ts < t + 1000).length;
tpsWindows.push(tapsInWindow);
}
const finalPct = Math.round(meterPct);
let applauseLevel;
if (finalPct < 25) applauseLevel = 'polite';
else if (finalPct < 50) applauseLevel = 'nice';
else if (finalPct < 75) applauseLevel = 'standing_ovation';
else applauseLevel = 'thunderous';
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,
feedback: {
tapCount,
tapsPerSecond: tpsWindows,
finalMeterPct: finalPct,
applauseLevel,
applauseReasons: Array.from(selectedReasons),
},
meta: {
timeToFirstInteractionMs: firstInteractionTime ? firstInteractionTime - startTime : null,
timeToDecisionMs: responseTimeMs,
totalInteractions: interactionCount,
tapTimestamps,
peakTapsPerSecond: Math.max(...tpsWindows),
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
viewportSize: { width: window.innerWidth, height: window.innerHeight },
responseTimeMs,
},
};
post(payload);
const levelEmojis = {
polite: '🏌️',
nice: '👏',
standing_ovation: '🎉',
thunderous: '🌩️',
};
setTimeout(() => {
document.getElementById('doneEmoji').textContent = levelEmojis[applauseLevel];
document.getElementById('doneText').textContent =
applauseLevel === 'thunderous' ? 'THUNDEROUS APPLAUSE!' :
applauseLevel === 'standing_ovation' ? 'Standing Ovation!' :
applauseLevel === 'nice' ? 'Nice Applause!' : 'Polite Golf Clap';
document.getElementById('doneSub').textContent = `${tapCount} claps · ${finalPct}% meter`;
document.getElementById('doneOverlay').classList.add('visible');
setTimeout(() => {
post({ type: 'factory_modal_close', reason: 'completed' });
}, 1200);
}, 400);
});
// ── Init ──
post({ type: 'factory_modal_ready', modalType, version: modalVersion });
})();
</script>
</body>
</html>