805 lines
22 KiB
HTML
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"> </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"> </div>
|
|
<div class="tap-count" id="tapCount"> </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>
|