Files
lennyzeltser-conversation-r…/examples/london-scam.html
T
lennyzeltser c437f04e0b Initial commit
2026-01-14 17:55:56 -05:00

1438 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>London Scam - Social Engineering Demo</title>
<style>
*, *::before, *::after {
box-sizing: border-box;
}
:root {
--bg-primary: #f6f7f9;
--bg-secondary: #ffffff;
--bg-chat: #ffffff;
--text-primary: #060426;
--text-secondary: #666666;
--text-muted: #666666;
--accent: #1a45bc;
--accent-light: #e0e7f8;
--user-bg: #e0f2fe;
--user-border: #7dd3fc;
--ai-bg: #f0fdf4;
--ai-border: #86efac;
--annotation-bg: transparent;
--annotation-border: #94a3b8;
--annotation-text: #64748b;
--transition-bg: #f3e8ff;
--transition-border: #c084fc;
--border-color: #e0e7f8;
--radius: 8px;
--radius-lg: 12px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-chat: #161b22;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-light: #1f2937;
--user-bg: #0c4a6e;
--user-border: #0284c7;
--ai-bg: #14532d;
--ai-border: #22c55e;
--annotation-bg: transparent;
--annotation-border: #475569;
--annotation-text: #94a3b8;
--transition-bg: #4c1d95;
--transition-border: #a855f7;
--border-color: #30363d;
}
}
/* Dark mode via data-theme attribute (for iframe sync with parent) */
:root[data-theme="dark"] {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-chat: #161b22;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-light: #1f2937;
--user-bg: #0c4a6e;
--user-border: #0284c7;
--ai-bg: #14532d;
--ai-border: #22c55e;
--annotation-bg: transparent;
--annotation-border: #475569;
--annotation-text: #94a3b8;
--transition-bg: #4c1d95;
--transition-border: #a855f7;
--border-color: #30363d;
}
:root[data-theme="dark"] .play-overlay {
background: rgba(0, 0, 0, 0.5);
}
:root[data-theme="dark"] .play-overlay-icon {
background: rgba(255, 255, 255, 0.9);
}
body {
margin: 0;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
background: var(--bg-primary);
color: var(--text-primary);
}
.demo-container {
max-width: 800px;
margin: 0 auto;
}
.demo-header {
margin-bottom: 16px;
padding-bottom: 12px;
}
.demo-header.hidden {
display: none;
}
.demo-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.demo-title-link {
color: inherit;
text-decoration: none;
}
.demo-title-link:hover {
text-decoration: underline;
}
.demo-description {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
.demo-description a {
color: var(--accent);
}
.chat-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.chat-container {
position: relative;
background: var(--bg-chat);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
padding: 16px;
flex: 1;
min-height: 350px;
max-height: var(--chat-max-height, 500px);
overflow-y: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
transition: opacity 0.3s ease;
}
.chat-container.fading {
opacity: 0;
}
/* Standalone mode: chat fills available space, controls stay visible */
body:not(.in-iframe) .demo-container {
height: calc(100vh - 32px);
display: flex;
flex-direction: column;
}
body:not(.in-iframe) .chat-container {
/* Fills remaining space after header/tabs/controls */
--chat-max-height: none;
flex: 1;
min-height: 200px;
}
/* Iframe mode: fill viewport, transparent background */
html.in-iframe,
body.in-iframe {
height: 100%;
background: transparent;
padding: 0;
margin: 0;
overflow: hidden;
}
body.in-iframe .demo-container {
height: 100%;
display: flex;
flex-direction: column;
background: transparent;
padding: 0;
}
body.in-iframe .tabs {
flex-shrink: 0;
}
body.in-iframe .chat-container {
flex: 1;
min-height: 0;
}
body.in-iframe .controls {
flex-shrink: 0;
margin-top: 12px;
padding-top: 8px;
padding-bottom: 8px;
}
.chat-messages {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 20px;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.25);
border-radius: var(--radius-lg);
cursor: pointer;
transition: opacity 0.3s ease;
z-index: 10;
}
.play-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.play-overlay-icon {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease;
}
.play-overlay:hover .play-overlay-icon {
transform: scale(1.05);
}
.play-overlay-icon svg {
margin-left: 4px;
}
@media (prefers-color-scheme: dark) {
.play-overlay {
background: rgba(0, 0, 0, 0.5);
}
.play-overlay-icon {
background: rgba(255, 255, 255, 0.9);
}
}
.message {
max-width: 85%;
opacity: 0;
transform: translateY(10px);
}
.message.visible {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.message.user {
align-self: flex-start;
}
.message.ai {
align-self: flex-end;
margin-right: 32px;
}
.message-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
color: var(--text-muted);
}
.message.ai .message-label {
text-align: right;
}
.message-content {
padding: 12px 16px;
border-radius: var(--radius);
border-left: 3px solid;
}
.message.user .message-content {
background: var(--user-bg);
border-color: var(--user-border);
}
.message.ai .message-content {
background: var(--ai-bg);
border-color: var(--ai-border);
}
.message-content pre {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px;
overflow-x: auto;
font-size: 12px;
margin: 8px 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-content code {
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
}
.annotation {
position: relative;
width: 100%;
margin: 20px 0;
padding: 0 0 0 16px;
font-size: 14px;
font-style: italic;
color: var(--annotation-text);
opacity: 0;
transform: translateY(10px);
}
.annotation::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--annotation-border);
border-radius: 2px;
}
.annotation.visible {
opacity: 1;
transform: translateY(0);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.annotation-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--annotation-border);
margin-bottom: 4px;
font-style: normal;
}
.annotation-content {
line-height: 1.5;
}
.transition {
position: relative;
width: 100%;
margin: 20px 0;
padding: 0 0 0 16px;
font-size: 14px;
font-weight: 500;
font-style: italic;
color: var(--transition-text, var(--text-muted));
opacity: 0;
transform: translateY(10px);
}
.transition::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--transition-border);
border-radius: 2px;
}
.transition.visible {
opacity: 1;
transform: translateY(0);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.scenario-ending {
text-align: center;
padding: 24px 20px;
margin-top: 24px;
color: var(--text-muted);
font-size: 13px;
opacity: 0;
transition: opacity 0.4s ease;
}
.scenario-ending.visible {
opacity: 1;
}
.scenario-ending .next-label {
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 11px;
margin-bottom: 4px;
}
.scenario-ending .next-title {
font-weight: 600;
font-size: 15px;
color: var(--text-primary);
}
.controls {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
flex-wrap: wrap;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
border-radius: var(--radius);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
background: var(--accent-light);
border-color: var(--accent);
}
.control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.control-btn.primary:hover:not(:disabled) {
opacity: 0.9;
}
.control-btn.icon-only {
padding: 8px 10px;
}
.speed-select {
padding: 8px 10px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
border-radius: var(--radius);
font-size: 13px;
cursor: pointer;
}
.progress {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
}
.timer-bar {
position: absolute;
bottom: 1px;
left: 1px;
height: 3px;
width: 0%;
background: var(--accent);
opacity: 0;
border-radius: 0 0 2px 10px;
z-index: 5;
transform-origin: left;
transition: opacity 0.2s ease;
pointer-events: none;
}
.timer-bar.active {
opacity: 0.6;
transition: width 0.1s linear, opacity 0.2s ease;
}
.timer-circle {
position: absolute;
top: 12px;
right: 12px;
width: 18px;
height: 18px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 5;
pointer-events: none;
}
.timer-circle.active {
opacity: 0.6;
}
.timer-circle circle {
fill: none;
stroke: var(--accent);
stroke-width: 2;
stroke-dasharray: 44;
stroke-dashoffset: 0;
transform: rotate(-90deg);
transform-origin: center;
}
.browser-error {
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
}
@media (prefers-reduced-motion: reduce) {
.message,
.annotation,
.transition {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
.play-overlay {
transition: none !important;
}
}
@media (max-width: 600px) {
body {
padding: 12px;
}
.demo-title {
font-size: 16px;
}
.demo-description {
font-size: 13px;
}
.tabs {
gap: 6px;
}
.tab {
padding: 10px 14px;
font-size: 12px;
min-height: 44px;
}
.message {
max-width: 95%;
}
.chat-container {
padding: 12px;
min-height: 250px;
}
body.in-iframe .chat-container {
max-height: 400px;
}
.controls {
gap: 8px;
padding-top: 12px;
}
.control-btn {
padding: 10px 14px;
font-size: 13px;
min-height: 44px;
}
.control-btn.icon-only {
padding: 10px 12px;
min-width: 44px;
min-height: 44px;
}
.speed-select {
padding: 10px 12px;
font-size: 13px;
min-height: 44px;
}
.play-overlay-icon {
width: 70px;
height: 70px;
}
.play-overlay-icon svg {
width: 28px;
height: 28px;
}
.message-content pre {
overflow-x: auto;
max-width: 100%;
}
}
/* Very small phones */
@media (max-width: 400px) {
body {
padding: 8px;
}
.tabs {
flex-direction: column;
gap: 4px;
}
.tab {
width: 100%;
text-align: center;
}
.controls {
flex-wrap: wrap;
justify-content: center;
}
.control-btn {
flex: 0 0 auto;
}
.control-btn.primary {
order: -1;
flex: 1 1 100%;
justify-content: center;
margin-bottom: 4px;
}
}
</style>
</head>
<body>
<div class="demo-container">
<header class="demo-header" id="demo-header">
<h1 class="demo-title"><a href="/bots-chatting-on-social-networks" class="demo-title-link">London Scam - Social Engineering Demo</a></h1>
<p class="demo-description">How attackers use compromised accounts to social engineer victims via chat</p>
</header>
<div class="chat-wrapper">
<div class="chat-container" id="chat-container" role="log" aria-live="polite">
<div class="chat-messages" id="chat-messages"></div>
</div>
<svg class="timer-circle" id="timer-element" aria-hidden="true" viewBox="0 0 18 18">
<circle cx="9" cy="9" r="7"/>
</svg>
<div class="play-overlay" id="play-overlay" role="button" aria-label="Play demo" tabindex="0">
<div class="play-overlay-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="#1a45bc">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</div>
</div>
</div>
<div class="controls">
<button class="control-btn primary" id="play-pause-btn" aria-label="Play animation">
<svg id="play-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
<svg id="pause-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="display:none">
<rect x="6" y="4" width="4" height="16"/>
<rect x="14" y="4" width="4" height="16"/>
</svg>
<span id="play-pause-text">Play</span>
</button>
<button class="control-btn" id="reset-btn" aria-label="Restart animation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>
Restart
</button>
<select class="speed-select" id="speed-select" aria-label="Animation speed">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
<option value="4">4x</option>
</select>
<span class="progress" id="progress"></span>
</div>
<noscript>
<div class="browser-error">
<p>This demo requires JavaScript to be enabled.</p>
</div>
</noscript>
</div>
<script>
(function() {
'use strict';
// Feature detection
var hasRequiredFeatures = (
typeof document.querySelector === 'function' &&
typeof document.querySelectorAll === 'function' &&
typeof window.addEventListener === 'function' &&
typeof Array.prototype.forEach === 'function'
);
if (!hasRequiredFeatures) {
var errorDiv = document.querySelector('.browser-error');
if (errorDiv) {
errorDiv.style.display = 'block';
}
return;
}
// Iframe detection - add class for CSS and hide header when embedded
var isInIframe = false;
try {
isInIframe = window.self !== window.top;
} catch (e) {
isInIframe = true;
}
if (isInIframe) {
document.documentElement.classList.add('in-iframe');
document.body.classList.add('in-iframe');
// Listen for theme changes from parent page
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'theme-change') {
var theme = event.data.theme;
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
});
}
var header = document.getElementById('demo-header');
if (isInIframe && header) {
header.classList.add('hidden');
}
// Configurable annotation label
var annotationLabel = "Security Note";
// Scenarios data
var scenarios = {
"main": {
title: "London Scam",
labels: { user: "Matt (compromised account)", ai: "Rakesh" },
steps: [{ type: "user", plainText: "hi. whats up?" }, { type: "ai", plainText: "Hi Matt. Everything OK?" }, { type: "annotation", plainText: "The attacker establishes rapport using the victim's friend's account. Notice how the response seems normal at first." }, { type: "user", plainText: "well, im really stuck here in london. i had to visit a resort here in london and i got robbed at the hotel im staying" }, { type: "annotation", plainText: "The scam begins. The attacker creates urgency with a crisis story. The 'London emergency' is one of the most common social engineering pretexts." }, { type: "ai", plainText: "Oh no! Are you OK? Did you call the police?" }, { type: "user", plainText: "yes i did, but they said it might take a while to get my stuff back. the problem is i need to pay my hotel bill and get a flight home" }, { type: "annotation", plainText: "The attacker builds on the crisis, establishing a clear financial need. They're setting up the ask." }, { type: "user", plainText: "i was wondering if you could help me out with some money? ill pay you back as soon as i get home" }, { type: "transition", plainText: "This is where the scam becomes explicit. The attacker asks for money." }, { type: "ai", plainText: "How much do you need? And how would I send it?" }, { type: "user", plainText: "about $1500 would cover the hotel and flight. you can send it through western union - its the fastest way" }, { type: "annotation", plainText: "Western Union is the preferred method because transfers are difficult to trace or reverse. This is a major red flag." }, { type: "ai", plainText: "That's a lot of money. Can't you call the embassy or your bank?" }, { type: "user", plainText: "i tried but they said it would take days. please, i really need your help. i promise ill pay you back" }, { type: "annotation", plainText: "The attacker increases pressure and appeals to friendship. Real friends would understand delays - scammers push urgency." }, { type: "transition", plainText: "Red flags in this conversation: urgency, money request, Western Union, resistance to alternatives" }]
}
};
var scenarioOrder = ["main"];
var autoAdvance = true;
var hasMultipleScenarios = false;
var useCircleTimer = true;
// Speed configuration
var speedConfig = {
minDelay: 3000,
maxDelay: 8000,
msPerWord: 200,
annotationMultiplier: 1.15,
upNextDelay: 2500
};
// Animation state
var currentScenario = scenarioOrder[0];
var currentStepIndex = 0;
var isPlaying = false;
var isPaused = false;
var animationTimeout = null;
var speed = 1;
var hasStarted = false;
// DOM elements
var chatMessages = document.getElementById('chat-messages');
var playPauseBtn = document.getElementById('play-pause-btn');
var playIcon = document.getElementById('play-icon');
var pauseIcon = document.getElementById('pause-icon');
var playPauseText = document.getElementById('play-pause-text');
var resetBtn = document.getElementById('reset-btn');
var speedSelect = document.getElementById('speed-select');
var progressEl = document.getElementById('progress');
var playOverlay = document.getElementById('play-overlay');
var timerElement = document.getElementById('timer-element');
var chatContainer = document.getElementById('chat-container');
var tabs = document.querySelectorAll('.tab');
// Timer animation
var timerAnimationFrame = null;
var timerStartTime = 0;
var timerDuration = 0;
// Reduced motion preference
var prefersReducedMotion = false;
try {
prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
} catch (e) {}
function getSteps() {
return scenarios[currentScenario].steps;
}
function getLabels() {
return scenarios[currentScenario].labels;
}
// Content-aware delay calculation
function calculateDelay(step) {
var wordCount = 0;
if (step.plainText) {
wordCount += step.plainText.split(/\s+/).length;
}
if (step.codeBlock) {
wordCount += Math.floor(step.codeBlock.split(/\s+/).length * 1.3);
}
if (step.footnote) {
wordCount += step.footnote.split(/\s+/).length;
}
var baseDelay = Math.max(speedConfig.minDelay, Math.min(speedConfig.maxDelay, wordCount * speedConfig.msPerWord));
if (step.type === 'annotation') {
baseDelay = Math.floor(baseDelay * speedConfig.annotationMultiplier);
}
return baseDelay / speed;
}
// Timer functions - supports bar (grows left to right) or circle (stroke countdown)
function startTimer(duration) {
timerDuration = duration;
timerStartTime = Date.now();
timerElement.classList.add('active');
if (useCircleTimer) {
// Circle: animate stroke-dashoffset from 0 to 44 (full circle = 44)
var circle = timerElement.querySelector('circle');
if (circle) circle.style.strokeDashoffset = '0';
} else {
// Bar: start at 0% width
timerElement.style.width = '0%';
}
function animate() {
var elapsed = Date.now() - timerStartTime;
var progress = Math.min(elapsed / timerDuration, 1);
if (useCircleTimer) {
// Circle: offset goes from 0 to 44 as time progresses
var circle = timerElement.querySelector('circle');
if (circle) circle.style.strokeDashoffset = (progress * 44) + '';
} else {
// Bar: width grows from 0% to 100%
timerElement.style.width = (progress * 100) + '%';
}
if (progress < 1 && isPlaying && !isPaused) {
timerAnimationFrame = requestAnimationFrame(animate);
}
}
if (typeof requestAnimationFrame === 'function') {
timerAnimationFrame = requestAnimationFrame(animate);
}
}
function stopTimer() {
if (timerAnimationFrame) {
cancelAnimationFrame(timerAnimationFrame);
timerAnimationFrame = null;
}
timerElement.classList.remove('active');
if (useCircleTimer) {
var circle = timerElement.querySelector('circle');
if (circle) circle.style.strokeDashoffset = '0';
} else {
timerElement.style.width = '0%';
}
}
function clearChatMessages() {
while (chatMessages.firstChild) {
chatMessages.removeChild(chatMessages.firstChild);
}
}
function smoothScrollToBottom() {
var container = chatMessages.parentElement;
if (typeof container.scrollTo === 'function') {
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
} else {
container.scrollTop = container.scrollHeight;
}
}
function createStepElement(step) {
var div = document.createElement('div');
var labels = getLabels();
if (step.type === 'annotation') {
div.className = 'annotation';
// Label
var label = document.createElement('div');
label.className = 'annotation-label';
label.textContent = annotationLabel;
div.appendChild(label);
// Content
var content = document.createElement('div');
content.className = 'annotation-content';
content.textContent = step.plainText;
div.appendChild(content);
return div;
}
if (step.type === 'transition') {
div.className = 'transition';
div.textContent = step.plainText;
return div;
}
// Message
div.className = 'message ' + step.type;
var label = document.createElement('div');
label.className = 'message-label';
label.textContent = labels[step.type] || step.type;
div.appendChild(label);
var contentWrapper = document.createElement('div');
contentWrapper.className = 'message-content';
if (step.plainText) {
var textNode = document.createElement('p');
var hasMoreContent = step.codeBlock || step.footnote;
textNode.style.margin = hasMoreContent ? '0 0 8px 0' : '0';
textNode.textContent = step.plainText;
contentWrapper.appendChild(textNode);
}
if (step.codeBlock) {
var pre = document.createElement('pre');
var code = document.createElement('code');
code.textContent = step.codeBlock;
pre.appendChild(code);
contentWrapper.appendChild(pre);
}
if (step.footnote) {
var footnote = document.createElement('p');
footnote.style.cssText = 'margin: 8px 0 0 0; font-style: italic; font-size: 12px; color: var(--text-secondary);';
footnote.textContent = step.footnote;
contentWrapper.appendChild(footnote);
}
div.appendChild(contentWrapper);
return div;
}
function updateTabStates() {
tabs.forEach(function(t) {
var isActive = t.getAttribute('data-scenario') === currentScenario;
if (isActive) {
t.classList.add('active');
t.setAttribute('aria-selected', 'true');
t.setAttribute('tabindex', '0');
} else {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
}
});
}
function updateButtonStates() {
var steps = getSteps();
var isComplete = currentStepIndex >= steps.length;
var isActivelyPlaying = isPlaying && !isPaused;
// Toggle play/pause button icon and text
if (isActivelyPlaying) {
playIcon.style.display = 'none';
pauseIcon.style.display = 'block';
playPauseText.textContent = 'Pause';
playPauseBtn.setAttribute('aria-label', 'Pause animation');
} else {
playIcon.style.display = 'block';
pauseIcon.style.display = 'none';
playPauseText.textContent = 'Play';
playPauseBtn.setAttribute('aria-label', 'Play animation');
}
playPauseBtn.disabled = isComplete && !isActivelyPlaying;
}
function updateProgress() {
var steps = getSteps();
progressEl.textContent = currentStepIndex + ' / ' + steps.length;
}
function showInitialPreview() {
var steps = getSteps();
if (steps.length > 0) {
var element = createStepElement(steps[0]);
element.classList.add('visible');
chatMessages.appendChild(element);
currentStepIndex = 0;
}
playOverlay.classList.remove('hidden');
hasStarted = false;
updateProgress();
updateButtonStates();
}
function switchToScenario(scenarioId, withFade) {
stopTimer();
if (animationTimeout) {
clearTimeout(animationTimeout);
animationTimeout = null;
}
function doSwitch() {
currentScenario = scenarioId;
currentStepIndex = 0;
isPlaying = false;
isPaused = false;
hasStarted = false;
updateTabStates();
clearChatMessages();
showInitialPreview();
if (withFade) {
// Fade back in
chatContainer.classList.remove('fading');
}
}
if (withFade) {
// Fade out first
chatContainer.classList.add('fading');
setTimeout(doSwitch, 300);
} else {
doSwitch();
}
}
function showTransitionMessage(nextScenarioTitle) {
var div = document.createElement('div');
div.className = 'transition';
div.textContent = 'Switching to: ' + nextScenarioTitle;
chatMessages.appendChild(div);
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(function() {
div.classList.add('visible');
smoothScrollToBottom();
});
} else {
div.classList.add('visible');
smoothScrollToBottom();
}
}
function advanceToNextScenario() {
var currentIndex = scenarioOrder.indexOf(currentScenario);
var nextIndex = (currentIndex + 1) % scenarioOrder.length;
var nextScenarioId = scenarioOrder[nextIndex];
var nextTitle = scenarios[nextScenarioId].title;
// Show "Up Next" indicator at end of current scenario
var upNextDiv = document.createElement('div');
upNextDiv.className = 'scenario-ending';
var labelDiv = document.createElement('div');
labelDiv.className = 'next-label';
labelDiv.textContent = 'Up Next';
var titleDiv = document.createElement('div');
titleDiv.className = 'next-title';
titleDiv.textContent = nextTitle;
upNextDiv.appendChild(labelDiv);
upNextDiv.appendChild(titleDiv);
chatMessages.appendChild(upNextDiv);
// Animate it in
requestAnimationFrame(function() {
upNextDiv.classList.add('visible');
smoothScrollToBottom();
});
// Wait for user to see it, then fade and switch
var upNextDelay = speedConfig.upNextDelay / speed;
startTimer(upNextDelay);
animationTimeout = setTimeout(function() {
stopTimer();
chatContainer.classList.add('fading');
setTimeout(function() {
currentScenario = nextScenarioId;
currentStepIndex = 0;
isPlaying = false;
isPaused = false;
hasStarted = false;
updateTabStates();
clearChatMessages();
// Skip showInitialPreview() during auto-advance to avoid overlay flash
// Keep overlay hidden and go directly to playing
playOverlay.classList.add('hidden');
chatContainer.classList.remove('fading');
// Auto-play immediately
play();
}, 300);
}, upNextDelay);
}
function showNextStep() {
stopTimer();
var steps = getSteps();
if (currentStepIndex >= steps.length) {
// Scenario complete
if (autoAdvance && hasMultipleScenarios) {
// Advance to next scenario - it handles its own "Up Next" timing
advanceToNextScenario();
} else {
isPlaying = false;
updateButtonStates();
}
return;
}
if (isPaused) return;
var step = steps[currentStepIndex];
var element = createStepElement(step);
chatMessages.appendChild(element);
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(function() {
element.classList.add('visible');
smoothScrollToBottom();
});
} else {
element.classList.add('visible');
smoothScrollToBottom();
}
currentStepIndex++;
updateProgress();
if (currentStepIndex < steps.length) {
var delay = calculateDelay(step);
startTimer(delay);
animationTimeout = setTimeout(showNextStep, delay);
} else {
// Check if we should auto-advance
if (autoAdvance && hasMultipleScenarios) {
// Advance to next scenario - it handles its own "Up Next" timing
advanceToNextScenario();
} else {
isPlaying = false;
updateButtonStates();
}
}
}
function play() {
if (!hasStarted) {
playOverlay.classList.add('hidden');
hasStarted = true;
isPlaying = true;
isPaused = false;
updateButtonStates();
var steps = getSteps();
var firstStep = steps[0];
var delay = calculateDelay(firstStep);
currentStepIndex = 1;
startTimer(delay);
animationTimeout = setTimeout(showNextStep, delay);
return;
}
if (isPaused) {
isPaused = false;
updateButtonStates();
showNextStep();
return;
}
if (isPlaying) return;
var steps = getSteps();
if (currentStepIndex >= steps.length) {
currentStepIndex = 0;
clearChatMessages();
var element = createStepElement(steps[0]);
element.classList.add('visible');
chatMessages.appendChild(element);
currentStepIndex = 1;
isPlaying = true;
isPaused = false;
updateButtonStates();
var delay = calculateDelay(steps[0]);
startTimer(delay);
animationTimeout = setTimeout(showNextStep, delay);
return;
}
isPlaying = true;
isPaused = false;
updateButtonStates();
showNextStep();
}
function pause() {
isPaused = true;
updateButtonStates();
stopTimer();
if (animationTimeout) {
clearTimeout(animationTimeout);
animationTimeout = null;
}
}
function reset() {
switchToScenario(currentScenario);
}
function showAllInstantly() {
var steps = getSteps();
clearChatMessages();
playOverlay.classList.add('hidden');
hasStarted = true;
steps.forEach(function(step) {
var element = createStepElement(step);
element.classList.add('visible');
chatMessages.appendChild(element);
});
currentStepIndex = steps.length;
isPlaying = false;
updateProgress();
updateButtonStates();
}
// Event listeners
playOverlay.addEventListener('click', function() {
if (prefersReducedMotion) {
showAllInstantly();
} else {
play();
}
});
playOverlay.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (prefersReducedMotion) {
showAllInstantly();
} else {
play();
}
}
});
// Tab switching logic (shared between click and keyboard)
function switchTab(tab) {
var scenarioId = tab.getAttribute('data-scenario');
if (scenarioId === currentScenario) return; // Already on this tab
// Remember if we were playing before switching
var wasPlaying = isPlaying && !isPaused;
// Stop any current animation
if (animationTimeout) {
clearTimeout(animationTimeout);
animationTimeout = null;
}
stopTimer();
// Fade out, switch, fade in
chatContainer.classList.add('fading');
setTimeout(function() {
currentScenario = scenarioId;
currentStepIndex = 0;
isPlaying = false;
isPaused = false;
hasStarted = false;
updateTabStates();
clearChatMessages();
// Keep overlay hidden to avoid flash
playOverlay.classList.add('hidden');
chatContainer.classList.remove('fading');
// If was playing, continue playing; otherwise stay paused
if (wasPlaying && !prefersReducedMotion) {
play();
} else if (prefersReducedMotion) {
showAllInstantly();
} else {
// Stay paused - show initial preview but keep overlay hidden
// User can click play button or canvas to start
showInitialPreview();
}
}, 300);
}
// Tab click handlers
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
switchTab(tab);
});
});
// Tab keyboard navigation (arrow keys per ARIA authoring practices)
tabs.forEach(function(tab, index) {
tab.addEventListener('keydown', function(e) {
var tabArray = Array.prototype.slice.call(tabs);
var currentIndex = tabArray.indexOf(tab);
var newIndex = currentIndex;
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
newIndex = currentIndex === 0 ? tabArray.length - 1 : currentIndex - 1;
e.preventDefault();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
newIndex = currentIndex === tabArray.length - 1 ? 0 : currentIndex + 1;
e.preventDefault();
} else if (e.key === 'Home') {
newIndex = 0;
e.preventDefault();
} else if (e.key === 'End') {
newIndex = tabArray.length - 1;
e.preventDefault();
}
if (newIndex !== currentIndex) {
tabArray[newIndex].focus();
switchTab(tabArray[newIndex]);
}
});
});
function togglePlayPause() {
if (isPlaying && !isPaused) {
pause();
} else {
play();
}
}
playPauseBtn.addEventListener('click', togglePlayPause);
resetBtn.addEventListener('click', reset);
speedSelect.addEventListener('change', function(e) {
speed = parseFloat(e.target.value);
});
// Handle page visibility changes (pause timer animation when tab is hidden)
if (typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', function() {
if (document.hidden && timerAnimationFrame) {
cancelAnimationFrame(timerAnimationFrame);
timerAnimationFrame = null;
}
});
}
// Initialize
updateTabStates();
showInitialPreview();
if (prefersReducedMotion) {
showAllInstantly();
}
})();
</script>
</body>
</html>