mirror of
https://github.com/lennyzeltser/conversation-replay
synced 2026-06-21 13:55:53 +00:00
1512 lines
44 KiB
HTML
1512 lines
44 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI-Assisted IR Report Writing</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);
|
|
}
|
|
|
|
|
|
/* Tabs - browser-style integrated with canvas */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
margin-left: 12px;
|
|
margin-bottom: -1px;
|
|
flex-wrap: wrap;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
border: 1px solid transparent;
|
|
border-bottom: none;
|
|
background: transparent;
|
|
color: var(--tab-inactive-color, var(--text-muted));
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
opacity: 0.75;
|
|
}
|
|
|
|
.tab:hover {
|
|
opacity: 0.9;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab.active {
|
|
background: var(--bg-chat);
|
|
color: var(--text-primary);
|
|
border-color: var(--border-color);
|
|
opacity: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: var(--bg-chat);
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.tab {
|
|
padding: 10px 14px;
|
|
font-size: 12px;
|
|
flex: 1;
|
|
text-align: center;
|
|
min-width: 0;
|
|
}
|
|
}
|
|
|
|
|
|
.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">AI-Assisted IR Report Writing</h1>
|
|
<p class="demo-description">Using AI tools to create, review, and improve incident response documentation</p>
|
|
</header>
|
|
|
|
|
|
<nav class="tabs" role="tablist">
|
|
<button class="tab active" role="tab" data-scenario="create" aria-selected="true" tabindex="0">IR Report Creation</button>
|
|
<button class="tab" role="tab" data-scenario="review" aria-selected="false" tabindex="-1">IR Report Review</button>
|
|
<button class="tab" role="tab" data-scenario="coach" aria-selected="false" tabindex="-1">Writing Coach</button>
|
|
</nav>
|
|
|
|
<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 = "Behind the Scenes";
|
|
|
|
// Scenarios data
|
|
var scenarios = {
|
|
"create": {
|
|
title: "IR Report Creation",
|
|
labels: { user: "Security Analyst", ai: "AI Assistant" },
|
|
steps: [{ type: "user", plainText: "I need to create an IR report for a BEC incident. The CFO received a fraudulent wire transfer request that appeared to be from the CEO." }, { type: "ai", plainText: "I can help you draft that IR report. Let me ask a few questions first to ensure we capture all the relevant details." }, { type: "annotation", plainText: "The AI uses a structured approach to gather incident details systematically." }, { type: "ai", plainText: "When was the incident discovered, and what was the initial indicator that something was wrong?" }, { type: "user", plainText: "Discovered yesterday at 2pm when the CFO called the CEO to confirm the transfer. The CEO had no knowledge of the request." }, { type: "ai", plainText: "Good catch by the CFO. What was the requested transfer amount and where was it supposed to go?" }, { type: "user", plainText: "$47,500 to an account at First National Bank, routing number ending in 4521." }, { type: "ai", plainText: "Based on what you've shared, here's a draft executive summary:", codeBlock: "EXECUTIVE SUMMARY\n\nOn [date], a Business Email Compromise (BEC) attempt\ntargeted our organization. An attacker impersonating\nthe CEO requested a $47,500 wire transfer from the CFO.\n\nThe attack was detected before funds were transferred\nwhen the CFO verbally confirmed the request with the CEO.\n\nImmediate actions: Email headers analyzed, account\nsecurity reviewed, finance team alerted to new protocols.\n" }, { type: "transition", plainText: "The analyst continues building the full report..." }]
|
|
},
|
|
"review": {
|
|
title: "IR Report Review",
|
|
labels: { user: "Security Analyst", ai: "AI Assistant" },
|
|
steps: [{ type: "user", plainText: "Can you review this section of my IR report? I want to make sure it's clear for executives.", codeBlock: "The threat actor utilized sophisticated social engineering\ntechniques leveraging compromised email infrastructure to\nperpetrate a financial fraud attempt via BEC methodology.\n" }, { type: "annotation", plainText: "The AI identifies jargon and passive voice that could confuse non-technical readers." }, { type: "ai", plainText: "This section could be clearer for executives. Here are specific suggestions:" }, { type: "ai", plainText: "1. 'sophisticated social engineering techniques' - What specifically made it sophisticated? Was it personalized?", footnote: "Executives appreciate specifics over vague adjectives" }, { type: "ai", plainText: "2. 'leveraging compromised email infrastructure' - Whose email was compromised? The CEO's? An external vendor's?" }, { type: "ai", plainText: "3. 'perpetrate a financial fraud attempt via BEC methodology' - This is passive and jargon-heavy. Try: 'The attacker posed as our CEO to request a fraudulent wire transfer.'" }, { type: "user", plainText: "Good points. Let me revise that paragraph." }, { type: "user", plainText: "Revised version:", codeBlock: "An attacker gained access to the CEO's email account\nand sent a convincing wire transfer request to the CFO.\nThe email matched the CEO's writing style and referenced\nan actual pending acquisition, showing the attacker had\nresearched our company.\n" }, { type: "ai", plainText: "Much better. This version is specific, active voice, and tells executives exactly what happened without requiring security background knowledge." }]
|
|
},
|
|
"coach": {
|
|
title: "Writing Coach",
|
|
labels: { user: "Security Analyst", ai: "Writing Coach" },
|
|
steps: [{ type: "user", plainText: "I struggle with executive summaries. They either end up too technical or too vague. Any tips?" }, { type: "ai", plainText: "Executive summaries for IR reports should answer four questions in plain language:" }, { type: "ai", plainText: "1. WHAT happened? (One sentence describing the incident)" }, { type: "ai", plainText: "2. WHAT's the impact? (Business terms: financial, operational, reputational)" }, { type: "ai", plainText: "3. WHAT did we do? (Immediate response actions)" }, { type: "ai", plainText: "4. WHAT's next? (Recommended follow-up actions)" }, { type: "annotation", plainText: "This framework helps analysts structure their thinking and ensures coverage of key points." }, { type: "user", plainText: "That's helpful. What about tone? My manager says my reports sound too alarmist." }, { type: "ai", plainText: "Good question. Compare these two approaches:", codeBlock: "ALARMIST:\n\"A catastrophic security breach nearly resulted in\nmassive financial losses. Urgent action is required\nimmediately to prevent future attacks.\"\n\nMEASURED:\n\"A BEC attempt was detected and blocked before any\nfunds transferred. We recommend three process\nimprovements to reduce future risk.\"\n" }, { type: "ai", plainText: "The measured version states facts, acknowledges what went right, and focuses on actionable improvements rather than fear." }, { type: "user", plainText: "I see the difference. Facts over fear, and focus on what we're doing about it." }, { type: "ai", plainText: "Exactly. Executives want to know the situation is under control and what resources you need. Alarmist language undermines confidence in the security team." }]
|
|
}
|
|
};
|
|
var scenarioOrder = ["create","review","coach"];
|
|
var autoAdvance = true;
|
|
var hasMultipleScenarios = true;
|
|
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> |