Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- static/index.html +244 -0
- static/script.js +794 -0
- static/styles.css +635 -0
static/index.html
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Focus Guard</title>
|
| 6 |
+
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap" rel="stylesheet">
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css?v=2">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<h1>Focus Guard</h1>
|
| 11 |
+
|
| 12 |
+
<!-- Top Menu -->
|
| 13 |
+
<nav id="top-menu">
|
| 14 |
+
<button id="menu-start" class="menu-btn">Start Focus</button>
|
| 15 |
+
<div class="separator"></div>
|
| 16 |
+
<button id="menu-achievement" class="menu-btn">My Achievement</button>
|
| 17 |
+
<div class="separator"></div>
|
| 18 |
+
<button id="menu-records" class="menu-btn">My Records</button>
|
| 19 |
+
<div class="separator"></div>
|
| 20 |
+
<button id="menu-customise" class="menu-btn">Customise</button>
|
| 21 |
+
<div class="separator"></div>
|
| 22 |
+
<button id="menu-help" class="menu-btn">Help</button>
|
| 23 |
+
</nav>
|
| 24 |
+
<!-- Page A -->
|
| 25 |
+
<main id="page-a" class="page">
|
| 26 |
+
<h1>Focus Guard</h1>
|
| 27 |
+
<p>Your productivity monitor assistant.</p>
|
| 28 |
+
<button id="start-button" class="btn-main">Start</button>
|
| 29 |
+
</main>
|
| 30 |
+
<!-- Page B -->
|
| 31 |
+
<main id="page-b" class="page hidden">
|
| 32 |
+
<!-- 1. Camera / Display Area -->
|
| 33 |
+
<section id="display-area">
|
| 34 |
+
<p id="display-placeholder">Nothing</p>
|
| 35 |
+
</section>
|
| 36 |
+
|
| 37 |
+
<!-- 2. Timeline Area -->
|
| 38 |
+
<section id="timeline-area">
|
| 39 |
+
<div class="timeline-label">Timeline</div>
|
| 40 |
+
<div id="timeline-visuals"></div>
|
| 41 |
+
<div id="timeline-line"></div>
|
| 42 |
+
</section>
|
| 43 |
+
|
| 44 |
+
<!-- 3. Control Buttons -->
|
| 45 |
+
<section id="control-panel">
|
| 46 |
+
<button id="btn-cam-start" class="action-btn green">Start</button>
|
| 47 |
+
<button id="btn-floating" class="action-btn yellow">Floating Window</button>
|
| 48 |
+
<button id="btn-models" class="action-btn blue">Models</button>
|
| 49 |
+
<button id="btn-cam-stop" class="action-btn red">Stop</button>
|
| 50 |
+
</section>
|
| 51 |
+
|
| 52 |
+
<!-- 4. Frame Control -->
|
| 53 |
+
<section id="frame-control">
|
| 54 |
+
<label for="frame-slider">Frame</label>
|
| 55 |
+
<input type="range" id="frame-slider" min="1" max="60" value="30">
|
| 56 |
+
<input type="number" id="frame-input" value="30">
|
| 57 |
+
</section>
|
| 58 |
+
</main>
|
| 59 |
+
|
| 60 |
+
<!-- Page C - My Achievement -->
|
| 61 |
+
<main id="page-c" class="page hidden">
|
| 62 |
+
<h1>My Achievement</h1>
|
| 63 |
+
|
| 64 |
+
<div class="stats-grid">
|
| 65 |
+
<div class="stat-card">
|
| 66 |
+
<div class="stat-number" id="total-sessions">0</div>
|
| 67 |
+
<div class="stat-label">Total Sessions</div>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="stat-card">
|
| 70 |
+
<div class="stat-number" id="total-hours">0h</div>
|
| 71 |
+
<div class="stat-label">Total Focus Time</div>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="stat-card">
|
| 74 |
+
<div class="stat-number" id="avg-focus">0%</div>
|
| 75 |
+
<div class="stat-label">Average Focus</div>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="stat-card">
|
| 78 |
+
<div class="stat-number" id="current-streak">0</div>
|
| 79 |
+
<div class="stat-label">Day Streak</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div class="achievements-section">
|
| 84 |
+
<h2>Badges</h2>
|
| 85 |
+
<div id="badges-container" class="badges-grid"></div>
|
| 86 |
+
</div>
|
| 87 |
+
</main>
|
| 88 |
+
|
| 89 |
+
<!-- Page D - My Records -->
|
| 90 |
+
<main id="page-d" class="page hidden">
|
| 91 |
+
<h1>My Records</h1>
|
| 92 |
+
|
| 93 |
+
<div class="records-controls">
|
| 94 |
+
<button id="filter-today" class="filter-btn active">Today</button>
|
| 95 |
+
<button id="filter-week" class="filter-btn">This Week</button>
|
| 96 |
+
<button id="filter-month" class="filter-btn">This Month</button>
|
| 97 |
+
<button id="filter-all" class="filter-btn">All Time</button>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div class="chart-container">
|
| 101 |
+
<canvas id="focus-chart"></canvas>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<div class="sessions-list">
|
| 105 |
+
<h2>Recent Sessions</h2>
|
| 106 |
+
<table id="sessions-table">
|
| 107 |
+
<thead>
|
| 108 |
+
<tr>
|
| 109 |
+
<th>Date</th>
|
| 110 |
+
<th>Duration</th>
|
| 111 |
+
<th>Focus Score</th>
|
| 112 |
+
<th>Action</th>
|
| 113 |
+
</tr>
|
| 114 |
+
</thead>
|
| 115 |
+
<tbody id="sessions-tbody"></tbody>
|
| 116 |
+
</table>
|
| 117 |
+
</div>
|
| 118 |
+
</main>
|
| 119 |
+
|
| 120 |
+
<!-- Page E - Customise -->
|
| 121 |
+
<main id="page-e" class="page hidden">
|
| 122 |
+
<h1>Customise</h1>
|
| 123 |
+
|
| 124 |
+
<div class="settings-container">
|
| 125 |
+
<div class="setting-group">
|
| 126 |
+
<h2>Detection Settings</h2>
|
| 127 |
+
|
| 128 |
+
<div class="setting-item">
|
| 129 |
+
<label for="sensitivity-slider">Detection Sensitivity</label>
|
| 130 |
+
<div class="slider-group">
|
| 131 |
+
<input type="range" id="sensitivity-slider" min="1" max="10" value="6">
|
| 132 |
+
<span id="sensitivity-value">6</span>
|
| 133 |
+
</div>
|
| 134 |
+
<p class="setting-description">Higher values require stricter focus criteria</p>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<div class="setting-item">
|
| 138 |
+
<label for="default-framerate">Default Frame Rate</label>
|
| 139 |
+
<div class="slider-group">
|
| 140 |
+
<input type="range" id="default-framerate" min="5" max="60" value="30">
|
| 141 |
+
<span id="framerate-value">30</span> FPS
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<div class="setting-group">
|
| 147 |
+
<h2>Notifications</h2>
|
| 148 |
+
|
| 149 |
+
<div class="setting-item">
|
| 150 |
+
<label>
|
| 151 |
+
<input type="checkbox" id="enable-notifications" checked>
|
| 152 |
+
Enable distraction notifications
|
| 153 |
+
</label>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<div class="setting-item">
|
| 157 |
+
<label for="notification-threshold">Alert after (seconds)</label>
|
| 158 |
+
<input type="number" id="notification-threshold" value="30" min="5" max="300">
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div class="setting-group">
|
| 163 |
+
<h2>Data Management</h2>
|
| 164 |
+
|
| 165 |
+
<button id="export-data" class="action-btn blue">Export Data</button>
|
| 166 |
+
<button id="clear-history" class="action-btn red">Clear History</button>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<button id="save-settings" class="btn-main">Save Settings</button>
|
| 170 |
+
</div>
|
| 171 |
+
</main>
|
| 172 |
+
|
| 173 |
+
<!-- Page F - Help -->
|
| 174 |
+
<main id="page-f" class="page hidden">
|
| 175 |
+
<h1>Help</h1>
|
| 176 |
+
|
| 177 |
+
<div class="help-container">
|
| 178 |
+
<section class="help-section">
|
| 179 |
+
<h2>How to Use Focus Guard</h2>
|
| 180 |
+
<ol>
|
| 181 |
+
<li>Click "Start" or navigate to "Start Focus" in the menu</li>
|
| 182 |
+
<li>Allow camera access when prompted</li>
|
| 183 |
+
<li>Click the green "Start" button to begin monitoring</li>
|
| 184 |
+
<li>Position yourself in front of the camera</li>
|
| 185 |
+
<li>The system will track your focus in real-time</li>
|
| 186 |
+
<li>Click "Stop" when you're done to save the session</li>
|
| 187 |
+
</ol>
|
| 188 |
+
</section>
|
| 189 |
+
|
| 190 |
+
<section class="help-section">
|
| 191 |
+
<h2>What is "Focused"?</h2>
|
| 192 |
+
<p>The system considers you focused when:</p>
|
| 193 |
+
<ul>
|
| 194 |
+
<li>You are clearly visible in the camera frame</li>
|
| 195 |
+
<li>You are centered in the view</li>
|
| 196 |
+
<li>Your face is directed toward the screen</li>
|
| 197 |
+
<li>No other people are detected in the frame</li>
|
| 198 |
+
</ul>
|
| 199 |
+
</section>
|
| 200 |
+
|
| 201 |
+
<section class="help-section">
|
| 202 |
+
<h2>Adjusting Settings</h2>
|
| 203 |
+
<p><strong>Frame Rate:</strong> Lower values reduce CPU usage but update less frequently. Recommended: 15-30 FPS.</p>
|
| 204 |
+
<p><strong>Sensitivity:</strong> Higher values require stricter focus criteria. Adjust based on your setup.</p>
|
| 205 |
+
</section>
|
| 206 |
+
|
| 207 |
+
<section class="help-section">
|
| 208 |
+
<h2>Privacy & Data</h2>
|
| 209 |
+
<p>All video processing happens in real-time. No video frames are stored - only detection metadata (focus status, timestamps) is saved in your local database.</p>
|
| 210 |
+
</section>
|
| 211 |
+
|
| 212 |
+
<section class="help-section">
|
| 213 |
+
<h2>FAQ</h2>
|
| 214 |
+
<details>
|
| 215 |
+
<summary>Why is my focus score low?</summary>
|
| 216 |
+
<p>Ensure good lighting, center yourself in the camera frame, and adjust sensitivity settings in the Customise page.</p>
|
| 217 |
+
</details>
|
| 218 |
+
<details>
|
| 219 |
+
<summary>Can I use this without a camera?</summary>
|
| 220 |
+
<p>No, camera access is required for focus detection.</p>
|
| 221 |
+
</details>
|
| 222 |
+
<details>
|
| 223 |
+
<summary>Does this work on mobile?</summary>
|
| 224 |
+
<p>The app works on mobile browsers but performance may vary due to processing requirements.</p>
|
| 225 |
+
</details>
|
| 226 |
+
<details>
|
| 227 |
+
<summary>Is my data private?</summary>
|
| 228 |
+
<p>Yes! All processing happens locally. Video frames are analyzed in real-time and never stored. Only metadata is saved.</p>
|
| 229 |
+
</details>
|
| 230 |
+
</section>
|
| 231 |
+
|
| 232 |
+
<section class="help-section">
|
| 233 |
+
<h2>Technical Info</h2>
|
| 234 |
+
<p><strong>Model:</strong> YOLOv8n (Nano)</p>
|
| 235 |
+
<p><strong>Detection:</strong> Real-time person detection with pose analysis</p>
|
| 236 |
+
<p><strong>Storage:</strong> SQLite local database</p>
|
| 237 |
+
<p><strong>Framework:</strong> FastAPI + Native JavaScript</p>
|
| 238 |
+
</section>
|
| 239 |
+
</div>
|
| 240 |
+
</main>
|
| 241 |
+
|
| 242 |
+
<script src="/static/script.js?v=2"></script>
|
| 243 |
+
</body>
|
| 244 |
+
</html>
|
static/script.js
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ================ PAGE NAVIGATION ================
|
| 2 |
+
|
| 3 |
+
const pages = {
|
| 4 |
+
'page-a': document.getElementById('page-a'),
|
| 5 |
+
'page-b': document.getElementById('page-b'),
|
| 6 |
+
'page-c': document.getElementById('page-c'), // Achievement
|
| 7 |
+
'page-d': document.getElementById('page-d'), // Records
|
| 8 |
+
'page-e': document.getElementById('page-e'), // Customise
|
| 9 |
+
'page-f': document.getElementById('page-f') // Help
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
function showPage(pageId) {
|
| 13 |
+
for (const key in pages) {
|
| 14 |
+
pages[key].classList.add('hidden');
|
| 15 |
+
}
|
| 16 |
+
if (pages[pageId]) {
|
| 17 |
+
pages[pageId].classList.remove('hidden');
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// ================ VIDEO MANAGER CLASS ================
|
| 22 |
+
|
| 23 |
+
class VideoManager {
|
| 24 |
+
constructor() {
|
| 25 |
+
this.videoElement = null;
|
| 26 |
+
this.canvasElement = null;
|
| 27 |
+
this.ctx = null;
|
| 28 |
+
this.captureCanvas = null;
|
| 29 |
+
this.captureCtx = null;
|
| 30 |
+
this.stream = null;
|
| 31 |
+
this.ws = null;
|
| 32 |
+
this.isStreaming = false;
|
| 33 |
+
this.sessionId = null;
|
| 34 |
+
this.frameRate = 30;
|
| 35 |
+
this.frameInterval = null;
|
| 36 |
+
this.renderLoopId = null;
|
| 37 |
+
|
| 38 |
+
// Status smoothing for stable display
|
| 39 |
+
this.currentStatus = false; // Default: not focused
|
| 40 |
+
this.previousStatus = false; // Track previous status to detect changes
|
| 41 |
+
this.statusBuffer = []; // Buffer for last N frames
|
| 42 |
+
this.bufferSize = 5; // Number of frames to average (smaller = more responsive)
|
| 43 |
+
|
| 44 |
+
// Latest detection data for rendering
|
| 45 |
+
this.latestDetectionData = null;
|
| 46 |
+
this.lastConfidence = 0;
|
| 47 |
+
this.detectionHoldMs = 30;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
async initCamera() {
|
| 51 |
+
try {
|
| 52 |
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
| 53 |
+
video: {
|
| 54 |
+
width: { ideal: 640 },
|
| 55 |
+
height: { ideal: 480 },
|
| 56 |
+
facingMode: 'user'
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
this.videoElement = document.createElement('video');
|
| 61 |
+
this.videoElement.srcObject = this.stream;
|
| 62 |
+
this.videoElement.autoplay = true;
|
| 63 |
+
this.videoElement.playsInline = true;
|
| 64 |
+
|
| 65 |
+
this.canvasElement = document.createElement('canvas');
|
| 66 |
+
this.canvasElement.width = 640;
|
| 67 |
+
this.canvasElement.height = 480;
|
| 68 |
+
this.ctx = this.canvasElement.getContext('2d');
|
| 69 |
+
|
| 70 |
+
this.captureCanvas = document.createElement('canvas');
|
| 71 |
+
this.captureCanvas.width = 640;
|
| 72 |
+
this.captureCanvas.height = 480;
|
| 73 |
+
this.captureCtx = this.captureCanvas.getContext('2d');
|
| 74 |
+
|
| 75 |
+
const displayArea = document.getElementById('display-area');
|
| 76 |
+
displayArea.innerHTML = '';
|
| 77 |
+
displayArea.appendChild(this.canvasElement);
|
| 78 |
+
|
| 79 |
+
await this.videoElement.play();
|
| 80 |
+
this.startRenderLoop();
|
| 81 |
+
|
| 82 |
+
return true;
|
| 83 |
+
} catch (error) {
|
| 84 |
+
console.error('Camera init error:', error);
|
| 85 |
+
throw error;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
connectWebSocket() {
|
| 90 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 91 |
+
const wsUrl = `${protocol}//${window.location.host}/ws/video`;
|
| 92 |
+
|
| 93 |
+
this.ws = new WebSocket(wsUrl);
|
| 94 |
+
|
| 95 |
+
this.ws.onopen = () => {
|
| 96 |
+
console.log('WebSocket connected');
|
| 97 |
+
this.startSession();
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
this.ws.onmessage = (event) => {
|
| 101 |
+
const data = JSON.parse(event.data);
|
| 102 |
+
this.handleServerMessage(data);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
this.ws.onerror = (error) => {
|
| 106 |
+
console.error('WebSocket error:', error);
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
this.ws.onclose = () => {
|
| 110 |
+
console.log('WebSocket closed');
|
| 111 |
+
};
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
startStreaming() {
|
| 115 |
+
this.isStreaming = true;
|
| 116 |
+
this.connectWebSocket();
|
| 117 |
+
|
| 118 |
+
this.frameInterval = setInterval(() => {
|
| 119 |
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
| 120 |
+
this.captureAndSendFrame();
|
| 121 |
+
}
|
| 122 |
+
}, 1000 / this.frameRate);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
captureAndSendFrame() {
|
| 126 |
+
if (!this.videoElement || !this.captureCanvas || !this.captureCtx) return;
|
| 127 |
+
|
| 128 |
+
this.captureCtx.drawImage(this.videoElement, 0, 0, 640, 480);
|
| 129 |
+
|
| 130 |
+
const imageData = this.captureCanvas.toDataURL('image/jpeg', 0.8);
|
| 131 |
+
const base64Data = imageData.split(',')[1];
|
| 132 |
+
|
| 133 |
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
| 134 |
+
this.ws.send(JSON.stringify({
|
| 135 |
+
type: 'frame',
|
| 136 |
+
image: base64Data
|
| 137 |
+
}));
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
handleServerMessage(data) {
|
| 142 |
+
switch (data.type) {
|
| 143 |
+
case 'detection':
|
| 144 |
+
// Update status with smoothing
|
| 145 |
+
this.updateStatus(data.focused);
|
| 146 |
+
|
| 147 |
+
// Render with smoothed status
|
| 148 |
+
this.renderDetections(data);
|
| 149 |
+
|
| 150 |
+
// Update timeline and notifications with smoothed status
|
| 151 |
+
timeline.addEvent(this.currentStatus);
|
| 152 |
+
checkDistraction(this.currentStatus);
|
| 153 |
+
break;
|
| 154 |
+
case 'session_started':
|
| 155 |
+
this.sessionId = data.session_id;
|
| 156 |
+
console.log('Session started:', this.sessionId);
|
| 157 |
+
break;
|
| 158 |
+
case 'session_ended':
|
| 159 |
+
console.log('Session ended:', data.summary);
|
| 160 |
+
showSessionSummary(data.summary);
|
| 161 |
+
break;
|
| 162 |
+
case 'ack':
|
| 163 |
+
// Frame acknowledged but not processed
|
| 164 |
+
break;
|
| 165 |
+
case 'error':
|
| 166 |
+
console.error('Server error:', data.message);
|
| 167 |
+
break;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
updateStatus(newFocused) {
|
| 172 |
+
// Add to buffer
|
| 173 |
+
this.statusBuffer.push(newFocused);
|
| 174 |
+
|
| 175 |
+
// Keep buffer size limited
|
| 176 |
+
if (this.statusBuffer.length > this.bufferSize) {
|
| 177 |
+
this.statusBuffer.shift();
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Don't update status until buffer is full (prevents initial flickering)
|
| 181 |
+
if (this.statusBuffer.length < this.bufferSize) {
|
| 182 |
+
return false; // Status hasn't changed
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Calculate majority vote with moderate thresholds for better responsiveness
|
| 186 |
+
const focusedCount = this.statusBuffer.filter(f => f).length;
|
| 187 |
+
const focusedRatio = focusedCount / this.statusBuffer.length;
|
| 188 |
+
|
| 189 |
+
// Store previous status
|
| 190 |
+
this.previousStatus = this.currentStatus;
|
| 191 |
+
|
| 192 |
+
// Moderate thresholds: quicker to change, still avoids rapid flipping
|
| 193 |
+
// For 8 frames: need 6+ focused to become FOCUSED, or 6+ not focused to become NOT FOCUSED
|
| 194 |
+
if (focusedRatio >= 0.75) {
|
| 195 |
+
this.currentStatus = true;
|
| 196 |
+
} else if (focusedRatio <= 0.25) {
|
| 197 |
+
this.currentStatus = false;
|
| 198 |
+
}
|
| 199 |
+
// Between 0.25-0.75: keep current status (hysteresis to avoid jitter)
|
| 200 |
+
|
| 201 |
+
// Log only when status actually changes
|
| 202 |
+
const statusChanged = this.currentStatus !== this.previousStatus;
|
| 203 |
+
if (statusChanged) {
|
| 204 |
+
console.log(`Status changed: ${this.previousStatus ? 'FOCUSED' : 'NOT FOCUSED'} -> ${this.currentStatus ? 'FOCUSED' : 'NOT FOCUSED'} (ratio: ${focusedRatio.toFixed(2)})`);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Return whether status changed
|
| 208 |
+
return statusChanged;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
renderDetections(data) {
|
| 212 |
+
this.latestDetectionData = {
|
| 213 |
+
detections: data.detections || [],
|
| 214 |
+
confidence: data.confidence || 0,
|
| 215 |
+
focused: data.focused,
|
| 216 |
+
timestamp: performance.now()
|
| 217 |
+
};
|
| 218 |
+
this.lastConfidence = data.confidence || 0;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
startRenderLoop() {
|
| 222 |
+
if (this.renderLoopId) return;
|
| 223 |
+
|
| 224 |
+
const render = () => {
|
| 225 |
+
if (this.videoElement && this.ctx) {
|
| 226 |
+
this.ctx.drawImage(this.videoElement, 0, 0, 640, 480);
|
| 227 |
+
|
| 228 |
+
const now = performance.now();
|
| 229 |
+
const latest = this.latestDetectionData;
|
| 230 |
+
const hasFresh = latest && (now - latest.timestamp) <= this.detectionHoldMs;
|
| 231 |
+
|
| 232 |
+
// Draw detection boxes using last known data (prevents flicker)
|
| 233 |
+
if (hasFresh && latest.detections.length > 0) {
|
| 234 |
+
latest.detections.forEach(det => {
|
| 235 |
+
const [x1, y1, x2, y2] = det.bbox;
|
| 236 |
+
this.ctx.strokeStyle = this.currentStatus ? '#00FF00' : '#FF0000';
|
| 237 |
+
this.ctx.lineWidth = 3;
|
| 238 |
+
this.ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
| 239 |
+
|
| 240 |
+
this.ctx.fillStyle = this.currentStatus ? '#00FF00' : '#FF0000';
|
| 241 |
+
this.ctx.font = '16px Nunito';
|
| 242 |
+
const label = `${det.class_name} ${(det.confidence * 100).toFixed(1)}%`;
|
| 243 |
+
this.ctx.fillText(label, x1, y1 - 5);
|
| 244 |
+
});
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
const statusText = this.currentStatus ? 'FOCUSED' : 'NOT FOCUSED';
|
| 248 |
+
this.ctx.fillStyle = this.currentStatus ? '#00FF00' : '#FF0000';
|
| 249 |
+
this.ctx.font = 'bold 24px Nunito';
|
| 250 |
+
this.ctx.fillText(statusText, 10, 30);
|
| 251 |
+
|
| 252 |
+
this.ctx.font = '16px Nunito';
|
| 253 |
+
this.ctx.fillText(`Confidence: ${(this.lastConfidence * 100).toFixed(1)}%`, 10, 55);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
this.renderLoopId = requestAnimationFrame(render);
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
+
this.renderLoopId = requestAnimationFrame(render);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
stopRenderLoop() {
|
| 263 |
+
if (this.renderLoopId) {
|
| 264 |
+
cancelAnimationFrame(this.renderLoopId);
|
| 265 |
+
this.renderLoopId = null;
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
startSession() {
|
| 270 |
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
| 271 |
+
this.ws.send(JSON.stringify({ type: 'start_session' }));
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
stopStreaming() {
|
| 276 |
+
this.isStreaming = false;
|
| 277 |
+
this.stopRenderLoop();
|
| 278 |
+
|
| 279 |
+
if (this.frameInterval) {
|
| 280 |
+
clearInterval(this.frameInterval);
|
| 281 |
+
this.frameInterval = null;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
if (this.ws) {
|
| 285 |
+
this.ws.send(JSON.stringify({ type: 'end_session' }));
|
| 286 |
+
this.ws.close();
|
| 287 |
+
this.ws = null;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
if (this.stream) {
|
| 291 |
+
this.stream.getTracks().forEach(track => track.stop());
|
| 292 |
+
this.stream = null;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if (this.canvasElement && this.ctx) {
|
| 296 |
+
this.ctx.clearRect(0, 0, 640, 480);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
// Reset status
|
| 300 |
+
this.currentStatus = false;
|
| 301 |
+
this.statusBuffer = [];
|
| 302 |
+
this.latestDetectionData = null;
|
| 303 |
+
this.lastConfidence = 0;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
setFrameRate(rate) {
|
| 307 |
+
this.frameRate = Math.max(1, Math.min(60, rate));
|
| 308 |
+
|
| 309 |
+
if (this.isStreaming && this.frameInterval) {
|
| 310 |
+
clearInterval(this.frameInterval);
|
| 311 |
+
this.frameInterval = setInterval(() => {
|
| 312 |
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
| 313 |
+
this.captureAndSendFrame();
|
| 314 |
+
}
|
| 315 |
+
}, 1000 / this.frameRate);
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// ================ TIMELINE MANAGER CLASS ================
|
| 321 |
+
|
| 322 |
+
class TimelineManager {
|
| 323 |
+
constructor(maxEvents = 60) {
|
| 324 |
+
this.events = [];
|
| 325 |
+
this.maxEvents = maxEvents;
|
| 326 |
+
this.container = document.getElementById('timeline-visuals');
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
addEvent(isFocused) {
|
| 330 |
+
const timestamp = Date.now();
|
| 331 |
+
this.events.push({ timestamp, isFocused });
|
| 332 |
+
|
| 333 |
+
if (this.events.length > this.maxEvents) {
|
| 334 |
+
this.events.shift();
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
this.render();
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
render() {
|
| 341 |
+
if (!this.container) return;
|
| 342 |
+
|
| 343 |
+
this.container.innerHTML = '';
|
| 344 |
+
|
| 345 |
+
this.events.forEach((event, index) => {
|
| 346 |
+
const block = document.createElement('div');
|
| 347 |
+
block.className = 'timeline-block';
|
| 348 |
+
block.style.backgroundColor = event.isFocused ? '#00FF00' : '#FF0000';
|
| 349 |
+
block.style.width = '10px';
|
| 350 |
+
block.style.height = '20px';
|
| 351 |
+
block.style.display = 'inline-block';
|
| 352 |
+
block.style.marginRight = '2px';
|
| 353 |
+
block.title = event.isFocused ? 'Focused' : 'Distracted';
|
| 354 |
+
this.container.appendChild(block);
|
| 355 |
+
});
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
clear() {
|
| 359 |
+
this.events = [];
|
| 360 |
+
this.render();
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// ================ NOTIFICATION SYSTEM ================
|
| 365 |
+
|
| 366 |
+
let distractionStartTime = null;
|
| 367 |
+
let notificationTimeout = null;
|
| 368 |
+
let currentSettings = null;
|
| 369 |
+
|
| 370 |
+
async function loadCurrentSettings() {
|
| 371 |
+
try {
|
| 372 |
+
const response = await fetch('/api/settings');
|
| 373 |
+
currentSettings = await response.json();
|
| 374 |
+
} catch (error) {
|
| 375 |
+
console.error('Failed to load settings:', error);
|
| 376 |
+
currentSettings = {
|
| 377 |
+
notification_enabled: true,
|
| 378 |
+
notification_threshold: 30
|
| 379 |
+
};
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
function checkDistraction(isFocused) {
|
| 384 |
+
if (!currentSettings || !currentSettings.notification_enabled) return;
|
| 385 |
+
|
| 386 |
+
if (!isFocused) {
|
| 387 |
+
if (!distractionStartTime) {
|
| 388 |
+
distractionStartTime = Date.now();
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
const distractionDuration = (Date.now() - distractionStartTime) / 1000;
|
| 392 |
+
|
| 393 |
+
if (distractionDuration >= currentSettings.notification_threshold && !notificationTimeout) {
|
| 394 |
+
sendNotification('Focus Guard Alert', 'You seem distracted. Time to refocus!');
|
| 395 |
+
notificationTimeout = setTimeout(() => {
|
| 396 |
+
notificationTimeout = null;
|
| 397 |
+
}, 60000);
|
| 398 |
+
}
|
| 399 |
+
} else {
|
| 400 |
+
distractionStartTime = null;
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
async function sendNotification(title, body) {
|
| 405 |
+
if ('Notification' in window) {
|
| 406 |
+
if (Notification.permission === 'granted') {
|
| 407 |
+
new Notification(title, { body });
|
| 408 |
+
} else if (Notification.permission !== 'denied') {
|
| 409 |
+
const permission = await Notification.requestPermission();
|
| 410 |
+
if (permission === 'granted') {
|
| 411 |
+
new Notification(title, { body });
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// ================ SESSION SUMMARY MODAL ================
|
| 418 |
+
|
| 419 |
+
function showSessionSummary(summary) {
|
| 420 |
+
const modal = document.createElement('div');
|
| 421 |
+
modal.className = 'modal-overlay';
|
| 422 |
+
modal.innerHTML = `
|
| 423 |
+
<div class="modal-content">
|
| 424 |
+
<h2>Session Complete!</h2>
|
| 425 |
+
<div class="summary-stats">
|
| 426 |
+
<div class="summary-item">
|
| 427 |
+
<span class="summary-label">Duration:</span>
|
| 428 |
+
<span class="summary-value">${formatDuration(summary.duration_seconds)}</span>
|
| 429 |
+
</div>
|
| 430 |
+
<div class="summary-item">
|
| 431 |
+
<span class="summary-label">Focus Score:</span>
|
| 432 |
+
<span class="summary-value">${(summary.focus_score * 100).toFixed(1)}%</span>
|
| 433 |
+
</div>
|
| 434 |
+
<div class="summary-item">
|
| 435 |
+
<span class="summary-label">Total Frames:</span>
|
| 436 |
+
<span class="summary-value">${summary.total_frames}</span>
|
| 437 |
+
</div>
|
| 438 |
+
<div class="summary-item">
|
| 439 |
+
<span class="summary-label">Focused Frames:</span>
|
| 440 |
+
<span class="summary-value">${summary.focused_frames}</span>
|
| 441 |
+
</div>
|
| 442 |
+
</div>
|
| 443 |
+
<button class="btn-main" onclick="closeModal()">Close</button>
|
| 444 |
+
</div>
|
| 445 |
+
`;
|
| 446 |
+
|
| 447 |
+
document.body.appendChild(modal);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function closeModal() {
|
| 451 |
+
const modal = document.querySelector('.modal-overlay');
|
| 452 |
+
if (modal) {
|
| 453 |
+
modal.remove();
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
function formatDuration(seconds) {
|
| 458 |
+
const hours = Math.floor(seconds / 3600);
|
| 459 |
+
const minutes = Math.floor((seconds % 3600) / 60);
|
| 460 |
+
const secs = seconds % 60;
|
| 461 |
+
|
| 462 |
+
if (hours > 0) {
|
| 463 |
+
return `${hours}h ${minutes}m ${secs}s`;
|
| 464 |
+
} else if (minutes > 0) {
|
| 465 |
+
return `${minutes}m ${secs}s`;
|
| 466 |
+
} else {
|
| 467 |
+
return `${secs}s`;
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
// ================ GLOBAL INSTANCES ================
|
| 472 |
+
|
| 473 |
+
const videoManager = new VideoManager();
|
| 474 |
+
const timeline = new TimelineManager();
|
| 475 |
+
|
| 476 |
+
// ================ EVENT LISTENERS ================
|
| 477 |
+
|
| 478 |
+
// Page navigation
|
| 479 |
+
document.getElementById('menu-start').addEventListener('click', () => showPage('page-b'));
|
| 480 |
+
document.getElementById('menu-achievement').addEventListener('click', () => {
|
| 481 |
+
showPage('page-c');
|
| 482 |
+
loadAchievements();
|
| 483 |
+
});
|
| 484 |
+
document.getElementById('menu-records').addEventListener('click', () => {
|
| 485 |
+
showPage('page-d');
|
| 486 |
+
loadRecords('today');
|
| 487 |
+
});
|
| 488 |
+
document.getElementById('menu-customise').addEventListener('click', () => {
|
| 489 |
+
showPage('page-e');
|
| 490 |
+
loadSettings();
|
| 491 |
+
});
|
| 492 |
+
document.getElementById('menu-help').addEventListener('click', () => showPage('page-f'));
|
| 493 |
+
|
| 494 |
+
document.getElementById('start-button').addEventListener('click', () => showPage('page-b'));
|
| 495 |
+
|
| 496 |
+
// Page B controls
|
| 497 |
+
document.getElementById('btn-cam-start').addEventListener('click', async () => {
|
| 498 |
+
try {
|
| 499 |
+
await videoManager.initCamera();
|
| 500 |
+
videoManager.startStreaming();
|
| 501 |
+
timeline.clear();
|
| 502 |
+
await loadCurrentSettings();
|
| 503 |
+
} catch (error) {
|
| 504 |
+
console.error('Failed to start camera:', error);
|
| 505 |
+
alert('Camera access denied. Please allow camera permissions and ensure you are using HTTPS or localhost.');
|
| 506 |
+
}
|
| 507 |
+
});
|
| 508 |
+
|
| 509 |
+
document.getElementById('btn-cam-stop').addEventListener('click', () => {
|
| 510 |
+
videoManager.stopStreaming();
|
| 511 |
+
});
|
| 512 |
+
|
| 513 |
+
document.getElementById('btn-floating').addEventListener('click', () => {
|
| 514 |
+
alert('Floating window feature coming soon!');
|
| 515 |
+
});
|
| 516 |
+
|
| 517 |
+
document.getElementById('btn-models').addEventListener('click', () => {
|
| 518 |
+
alert('Model selection feature coming soon!');
|
| 519 |
+
});
|
| 520 |
+
|
| 521 |
+
// Frame control
|
| 522 |
+
const frameSlider = document.getElementById('frame-slider');
|
| 523 |
+
const frameInput = document.getElementById('frame-input');
|
| 524 |
+
|
| 525 |
+
frameSlider.addEventListener('input', (e) => {
|
| 526 |
+
const rate = parseInt(e.target.value);
|
| 527 |
+
frameInput.value = rate;
|
| 528 |
+
videoManager.setFrameRate(rate);
|
| 529 |
+
});
|
| 530 |
+
|
| 531 |
+
frameInput.addEventListener('input', (e) => {
|
| 532 |
+
const rate = parseInt(e.target.value);
|
| 533 |
+
frameSlider.value = rate;
|
| 534 |
+
videoManager.setFrameRate(rate);
|
| 535 |
+
});
|
| 536 |
+
|
| 537 |
+
// ================ ACHIEVEMENT PAGE ================
|
| 538 |
+
|
| 539 |
+
async function loadAchievements() {
|
| 540 |
+
try {
|
| 541 |
+
const response = await fetch('/api/stats/summary');
|
| 542 |
+
const stats = await response.json();
|
| 543 |
+
|
| 544 |
+
document.getElementById('total-sessions').textContent = stats.total_sessions;
|
| 545 |
+
document.getElementById('total-hours').textContent =
|
| 546 |
+
(stats.total_focus_time / 3600).toFixed(1) + 'h';
|
| 547 |
+
document.getElementById('avg-focus').textContent =
|
| 548 |
+
(stats.avg_focus_score * 100).toFixed(1) + '%';
|
| 549 |
+
document.getElementById('current-streak').textContent = stats.streak_days;
|
| 550 |
+
|
| 551 |
+
loadBadges(stats);
|
| 552 |
+
} catch (error) {
|
| 553 |
+
console.error('Failed to load achievements:', error);
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
function loadBadges(stats) {
|
| 558 |
+
const badges = [
|
| 559 |
+
{ name: 'First Session', condition: stats.total_sessions >= 1, icon: '' },
|
| 560 |
+
{ name: '10 Sessions', condition: stats.total_sessions >= 10, icon: '' },
|
| 561 |
+
{ name: '50 Sessions', condition: stats.total_sessions >= 50, icon: '' },
|
| 562 |
+
{ name: '10 Hour Focus', condition: stats.total_focus_time >= 36000, icon: '' },
|
| 563 |
+
{ name: '7 Day Streak', condition: stats.streak_days >= 7, icon: '' },
|
| 564 |
+
{ name: '90% Avg Focus', condition: stats.avg_focus_score >= 0.9, icon: '' }
|
| 565 |
+
];
|
| 566 |
+
|
| 567 |
+
const container = document.getElementById('badges-container');
|
| 568 |
+
container.innerHTML = '';
|
| 569 |
+
|
| 570 |
+
badges.forEach(badge => {
|
| 571 |
+
const badgeEl = document.createElement('div');
|
| 572 |
+
badgeEl.className = 'badge ' + (badge.condition ? 'earned' : 'locked');
|
| 573 |
+
badgeEl.innerHTML = `
|
| 574 |
+
<div class="badge-icon">${badge.icon}</div>
|
| 575 |
+
<div class="badge-name">${badge.name}</div>
|
| 576 |
+
`;
|
| 577 |
+
container.appendChild(badgeEl);
|
| 578 |
+
});
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
// ================ RECORDS PAGE ================
|
| 582 |
+
|
| 583 |
+
async function loadRecords(filter = 'today') {
|
| 584 |
+
try {
|
| 585 |
+
const response = await fetch(`/api/sessions?filter=${filter}`);
|
| 586 |
+
const sessions = await response.json();
|
| 587 |
+
|
| 588 |
+
renderSessionsTable(sessions);
|
| 589 |
+
renderChart(sessions);
|
| 590 |
+
} catch (error) {
|
| 591 |
+
console.error('Failed to load records:', error);
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function renderSessionsTable(sessions) {
|
| 596 |
+
const tbody = document.getElementById('sessions-tbody');
|
| 597 |
+
tbody.innerHTML = '';
|
| 598 |
+
|
| 599 |
+
sessions.forEach(session => {
|
| 600 |
+
const row = document.createElement('tr');
|
| 601 |
+
const date = new Date(session.start_time).toLocaleString();
|
| 602 |
+
const duration = formatDuration(session.duration_seconds);
|
| 603 |
+
const score = (session.focus_score * 100).toFixed(1) + '%';
|
| 604 |
+
|
| 605 |
+
row.innerHTML = `
|
| 606 |
+
<td>${date}</td>
|
| 607 |
+
<td>${duration}</td>
|
| 608 |
+
<td>${score}</td>
|
| 609 |
+
<td><button class="btn-view" onclick="viewSessionDetails(${session.id})">View</button></td>
|
| 610 |
+
`;
|
| 611 |
+
tbody.appendChild(row);
|
| 612 |
+
});
|
| 613 |
+
|
| 614 |
+
if (sessions.length === 0) {
|
| 615 |
+
const row = document.createElement('tr');
|
| 616 |
+
row.innerHTML = '<td colspan="4" style="text-align: center;">No sessions found</td>';
|
| 617 |
+
tbody.appendChild(row);
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
function renderChart(sessions) {
|
| 622 |
+
const canvas = document.getElementById('focus-chart');
|
| 623 |
+
const ctx = canvas.getContext('2d');
|
| 624 |
+
|
| 625 |
+
canvas.width = 800;
|
| 626 |
+
canvas.height = 300;
|
| 627 |
+
|
| 628 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 629 |
+
|
| 630 |
+
if (sessions.length === 0) {
|
| 631 |
+
ctx.fillStyle = '#888';
|
| 632 |
+
ctx.font = '20px Nunito';
|
| 633 |
+
ctx.fillText('No data available', canvas.width / 2 - 80, canvas.height / 2);
|
| 634 |
+
return;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
const barWidth = Math.min((canvas.width - 40) / sessions.length - 10, 80);
|
| 638 |
+
const maxScore = 1.0;
|
| 639 |
+
|
| 640 |
+
sessions.forEach((session, index) => {
|
| 641 |
+
const x = index * (barWidth + 10) + 20;
|
| 642 |
+
const barHeight = (session.focus_score / maxScore) * (canvas.height - 60);
|
| 643 |
+
const y = canvas.height - barHeight - 30;
|
| 644 |
+
|
| 645 |
+
ctx.fillStyle = session.focus_score > 0.7 ? '#28a745' :
|
| 646 |
+
session.focus_score > 0.4 ? '#ffc107' : '#dc3545';
|
| 647 |
+
ctx.fillRect(x, y, barWidth, barHeight);
|
| 648 |
+
|
| 649 |
+
ctx.fillStyle = '#333';
|
| 650 |
+
ctx.font = '12px Nunito';
|
| 651 |
+
const scoreText = (session.focus_score * 100).toFixed(0) + '%';
|
| 652 |
+
ctx.fillText(scoreText, x + barWidth / 2 - 15, y - 5);
|
| 653 |
+
});
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
function viewSessionDetails(sessionId) {
|
| 657 |
+
alert(`Session details for ID ${sessionId} - Feature coming soon!`);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
// Filter buttons
|
| 661 |
+
document.getElementById('filter-today').addEventListener('click', () => {
|
| 662 |
+
setActiveFilter('filter-today');
|
| 663 |
+
loadRecords('today');
|
| 664 |
+
});
|
| 665 |
+
|
| 666 |
+
document.getElementById('filter-week').addEventListener('click', () => {
|
| 667 |
+
setActiveFilter('filter-week');
|
| 668 |
+
loadRecords('week');
|
| 669 |
+
});
|
| 670 |
+
|
| 671 |
+
document.getElementById('filter-month').addEventListener('click', () => {
|
| 672 |
+
setActiveFilter('filter-month');
|
| 673 |
+
loadRecords('month');
|
| 674 |
+
});
|
| 675 |
+
|
| 676 |
+
document.getElementById('filter-all').addEventListener('click', () => {
|
| 677 |
+
setActiveFilter('filter-all');
|
| 678 |
+
loadRecords('all');
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
function setActiveFilter(activeId) {
|
| 682 |
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
| 683 |
+
btn.classList.remove('active');
|
| 684 |
+
});
|
| 685 |
+
document.getElementById(activeId).classList.add('active');
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
// ================ SETTINGS PAGE ================
|
| 689 |
+
|
| 690 |
+
async function loadSettings() {
|
| 691 |
+
try {
|
| 692 |
+
const response = await fetch('/api/settings');
|
| 693 |
+
if (!response.ok) {
|
| 694 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
const settings = await response.json();
|
| 698 |
+
console.log('Loaded settings:', settings);
|
| 699 |
+
|
| 700 |
+
// Apply settings with fallback to defaults
|
| 701 |
+
document.getElementById('sensitivity-slider').value = settings.sensitivity || 6;
|
| 702 |
+
document.getElementById('sensitivity-value').textContent = settings.sensitivity || 6;
|
| 703 |
+
document.getElementById('default-framerate').value = settings.frame_rate || 30;
|
| 704 |
+
document.getElementById('framerate-value').textContent = settings.frame_rate || 30;
|
| 705 |
+
document.getElementById('enable-notifications').checked = settings.notification_enabled !== false;
|
| 706 |
+
document.getElementById('notification-threshold').value = settings.notification_threshold || 30;
|
| 707 |
+
} catch (error) {
|
| 708 |
+
console.error('Failed to load settings:', error);
|
| 709 |
+
alert('Failed to load settings: ' + error.message);
|
| 710 |
+
}
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
async function saveSettings() {
|
| 714 |
+
const settings = {
|
| 715 |
+
sensitivity: parseInt(document.getElementById('sensitivity-slider').value),
|
| 716 |
+
frame_rate: parseInt(document.getElementById('default-framerate').value),
|
| 717 |
+
notification_enabled: document.getElementById('enable-notifications').checked,
|
| 718 |
+
notification_threshold: parseInt(document.getElementById('notification-threshold').value)
|
| 719 |
+
};
|
| 720 |
+
|
| 721 |
+
console.log('Saving settings:', settings);
|
| 722 |
+
|
| 723 |
+
try {
|
| 724 |
+
const response = await fetch('/api/settings', {
|
| 725 |
+
method: 'PUT',
|
| 726 |
+
headers: { 'Content-Type': 'application/json' },
|
| 727 |
+
body: JSON.stringify(settings)
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
if (response.ok) {
|
| 731 |
+
const result = await response.json();
|
| 732 |
+
console.log('Settings saved:', result);
|
| 733 |
+
alert('Settings saved successfully!');
|
| 734 |
+
await loadCurrentSettings();
|
| 735 |
+
} else {
|
| 736 |
+
const error = await response.text();
|
| 737 |
+
console.error('Save failed with status:', response.status, error);
|
| 738 |
+
alert(`Failed to save settings: ${response.status} ${response.statusText}`);
|
| 739 |
+
}
|
| 740 |
+
} catch (error) {
|
| 741 |
+
console.error('Failed to save settings:', error);
|
| 742 |
+
alert('Failed to save settings: ' + error.message);
|
| 743 |
+
}
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
// Settings UI handlers
|
| 747 |
+
document.getElementById('sensitivity-slider').addEventListener('input', (e) => {
|
| 748 |
+
document.getElementById('sensitivity-value').textContent = e.target.value;
|
| 749 |
+
});
|
| 750 |
+
|
| 751 |
+
document.getElementById('default-framerate').addEventListener('input', (e) => {
|
| 752 |
+
document.getElementById('framerate-value').textContent = e.target.value;
|
| 753 |
+
});
|
| 754 |
+
|
| 755 |
+
document.getElementById('save-settings').addEventListener('click', saveSettings);
|
| 756 |
+
|
| 757 |
+
document.getElementById('export-data').addEventListener('click', async () => {
|
| 758 |
+
try {
|
| 759 |
+
const response = await fetch('/api/sessions?filter=all');
|
| 760 |
+
const sessions = await response.json();
|
| 761 |
+
|
| 762 |
+
const dataStr = JSON.stringify(sessions, null, 2);
|
| 763 |
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
| 764 |
+
const url = URL.createObjectURL(dataBlob);
|
| 765 |
+
|
| 766 |
+
const link = document.createElement('a');
|
| 767 |
+
link.href = url;
|
| 768 |
+
link.download = `focus-guard-data-${new Date().toISOString().split('T')[0]}.json`;
|
| 769 |
+
link.click();
|
| 770 |
+
|
| 771 |
+
URL.revokeObjectURL(url);
|
| 772 |
+
} catch (error) {
|
| 773 |
+
console.error('Failed to export data:', error);
|
| 774 |
+
alert('Failed to export data');
|
| 775 |
+
}
|
| 776 |
+
});
|
| 777 |
+
|
| 778 |
+
document.getElementById('clear-history').addEventListener('click', async () => {
|
| 779 |
+
if (confirm('Are you sure you want to clear all history? This cannot be undone.')) {
|
| 780 |
+
alert('Clear history feature requires backend implementation');
|
| 781 |
+
}
|
| 782 |
+
});
|
| 783 |
+
|
| 784 |
+
// ================ INITIALIZATION ================
|
| 785 |
+
|
| 786 |
+
// Request notification permission on load
|
| 787 |
+
if ('Notification' in window && Notification.permission === 'default') {
|
| 788 |
+
Notification.requestPermission();
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
// Load settings on startup
|
| 792 |
+
loadCurrentSettings();
|
| 793 |
+
|
| 794 |
+
console.log(' Focus Guard initialized');
|
static/styles.css
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* GLOBAL STYLES */
|
| 2 |
+
body {
|
| 3 |
+
margin: 0;
|
| 4 |
+
font-family: 'Nunito', sans-serif; /* Rounded font */
|
| 5 |
+
background-color: #f9f9f9;
|
| 6 |
+
height: 100vh;
|
| 7 |
+
overflow-x: hidden;
|
| 8 |
+
overflow-y: auto;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.hidden {
|
| 12 |
+
display: none !important;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* TOP MENU */
|
| 16 |
+
#top-menu {
|
| 17 |
+
height: 60px;
|
| 18 |
+
background-color: white;
|
| 19 |
+
display: flex;
|
| 20 |
+
align-items: center;
|
| 21 |
+
justify-content: center; /* Center buttons horizontally */
|
| 22 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 23 |
+
position: fixed;
|
| 24 |
+
top: 0;
|
| 25 |
+
width: 100%;
|
| 26 |
+
z-index: 1000;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.menu-btn {
|
| 30 |
+
background: none;
|
| 31 |
+
border: none;
|
| 32 |
+
font-family: 'Nunito', sans-serif;
|
| 33 |
+
font-size: 16px;
|
| 34 |
+
color: #333;
|
| 35 |
+
padding: 10px 20px;
|
| 36 |
+
cursor: pointer;
|
| 37 |
+
transition: background-color 0.2s;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.menu-btn:hover {
|
| 41 |
+
background-color: #f0f0f0;
|
| 42 |
+
border-radius: 4px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.separator {
|
| 46 |
+
width: 1px;
|
| 47 |
+
height: 20px;
|
| 48 |
+
background-color: #555; /* Dark gray separator */
|
| 49 |
+
margin: 0 5px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* PAGE CONTAINER */
|
| 53 |
+
.page {
|
| 54 |
+
min-height: calc(100vh - 60px);
|
| 55 |
+
width: 100%;
|
| 56 |
+
padding-top: 60px; /* Space for fixed menu */
|
| 57 |
+
padding-bottom: 40px; /* Space at bottom for scrolling */
|
| 58 |
+
box-sizing: border-box;
|
| 59 |
+
display: flex;
|
| 60 |
+
flex-direction: column;
|
| 61 |
+
align-items: center;
|
| 62 |
+
overflow-y: auto; /* Enable vertical scrolling */
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* PAGE A SPECIFIC */
|
| 66 |
+
#page-a {
|
| 67 |
+
justify-content: center; /* Center vertically */
|
| 68 |
+
margin-top: -40px; /* Slight offset to look optical centered */
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
#page-a h1 {
|
| 72 |
+
font-size: 80px;
|
| 73 |
+
margin: 0 0 10px 0;
|
| 74 |
+
color: #000;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
#page-a p {
|
| 78 |
+
color: #666;
|
| 79 |
+
font-size: 20px;
|
| 80 |
+
margin-bottom: 40px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.btn-main {
|
| 84 |
+
background-color: #007BFF; /* Blue */
|
| 85 |
+
color: white;
|
| 86 |
+
border: none;
|
| 87 |
+
padding: 15px 50px;
|
| 88 |
+
font-size: 20px;
|
| 89 |
+
font-family: 'Nunito', sans-serif;
|
| 90 |
+
border-radius: 30px; /* Fully rounded corners */
|
| 91 |
+
cursor: pointer;
|
| 92 |
+
transition: transform 0.2s ease;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.btn-main:hover {
|
| 96 |
+
transform: scale(1.1); /* Zoom effect */
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* PAGE B SPECIFIC */
|
| 100 |
+
#page-b {
|
| 101 |
+
justify-content: space-evenly; /* Distribute vertical space */
|
| 102 |
+
padding-bottom: 20px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* 1. Display Area */
|
| 106 |
+
#display-area {
|
| 107 |
+
width: 60%;
|
| 108 |
+
height: 50%; /* Takes up half the page height */
|
| 109 |
+
border: 2px solid #ddd;
|
| 110 |
+
border-radius: 12px;
|
| 111 |
+
background-color: #fff;
|
| 112 |
+
display: flex;
|
| 113 |
+
align-items: center;
|
| 114 |
+
justify-content: center;
|
| 115 |
+
color: #555;
|
| 116 |
+
font-size: 24px;
|
| 117 |
+
position: relative;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* 2. Timeline Area */
|
| 121 |
+
#timeline-area {
|
| 122 |
+
width: 60%;
|
| 123 |
+
height: 80px;
|
| 124 |
+
position: relative;
|
| 125 |
+
display: flex;
|
| 126 |
+
flex-direction: column;
|
| 127 |
+
justify-content: flex-end;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.timeline-label {
|
| 131 |
+
position: absolute;
|
| 132 |
+
top: 0;
|
| 133 |
+
left: 0;
|
| 134 |
+
color: #888;
|
| 135 |
+
font-size: 14px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#timeline-line {
|
| 139 |
+
width: 100%;
|
| 140 |
+
height: 2px;
|
| 141 |
+
background-color: #87CEEB; /* Light blue */
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* 3. Control Panel */
|
| 145 |
+
#control-panel {
|
| 146 |
+
display: flex;
|
| 147 |
+
gap: 20px;
|
| 148 |
+
width: 60%;
|
| 149 |
+
justify-content: space-between;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.action-btn {
|
| 153 |
+
flex: 1; /* Evenly distributed width */
|
| 154 |
+
padding: 12px 0;
|
| 155 |
+
border: none;
|
| 156 |
+
border-radius: 12px;
|
| 157 |
+
font-size: 16px;
|
| 158 |
+
font-family: 'Nunito', sans-serif;
|
| 159 |
+
font-weight: 700;
|
| 160 |
+
cursor: pointer;
|
| 161 |
+
color: white;
|
| 162 |
+
transition: opacity 0.2s;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.action-btn:hover {
|
| 166 |
+
opacity: 0.9;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.action-btn.green { background-color: #28a745; }
|
| 170 |
+
.action-btn.yellow { background-color: #ffc107; color: #333; }
|
| 171 |
+
.action-btn.blue { background-color: #17a2b8; }
|
| 172 |
+
.action-btn.red { background-color: #dc3545; }
|
| 173 |
+
|
| 174 |
+
/* 4. Frame Control */
|
| 175 |
+
#frame-control {
|
| 176 |
+
display: flex;
|
| 177 |
+
align-items: center;
|
| 178 |
+
gap: 15px;
|
| 179 |
+
color: #333;
|
| 180 |
+
font-weight: bold;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
#frame-slider {
|
| 184 |
+
width: 200px;
|
| 185 |
+
cursor: pointer;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
#frame-input {
|
| 189 |
+
width: 50px;
|
| 190 |
+
padding: 5px;
|
| 191 |
+
border: 1px solid #ccc;
|
| 192 |
+
border-radius: 5px;
|
| 193 |
+
text-align: center;
|
| 194 |
+
font-family: 'Nunito', sans-serif;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* ================ ACHIEVEMENT PAGE ================ */
|
| 198 |
+
|
| 199 |
+
.stats-grid {
|
| 200 |
+
display: grid;
|
| 201 |
+
grid-template-columns: repeat(4, 1fr);
|
| 202 |
+
gap: 20px;
|
| 203 |
+
width: 80%;
|
| 204 |
+
margin: 40px auto;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.stat-card {
|
| 208 |
+
background: white;
|
| 209 |
+
padding: 30px;
|
| 210 |
+
border-radius: 12px;
|
| 211 |
+
text-align: center;
|
| 212 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.stat-number {
|
| 216 |
+
font-size: 48px;
|
| 217 |
+
font-weight: bold;
|
| 218 |
+
color: #007BFF;
|
| 219 |
+
margin-bottom: 10px;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.stat-label {
|
| 223 |
+
font-size: 16px;
|
| 224 |
+
color: #666;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.achievements-section {
|
| 228 |
+
width: 80%;
|
| 229 |
+
margin: 0 auto;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.achievements-section h2 {
|
| 233 |
+
color: #333;
|
| 234 |
+
margin-bottom: 20px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.badges-grid {
|
| 238 |
+
display: grid;
|
| 239 |
+
grid-template-columns: repeat(3, 1fr);
|
| 240 |
+
gap: 20px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.badge {
|
| 244 |
+
background: white;
|
| 245 |
+
padding: 30px 20px;
|
| 246 |
+
border-radius: 12px;
|
| 247 |
+
text-align: center;
|
| 248 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 249 |
+
transition: transform 0.2s;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.badge:hover {
|
| 253 |
+
transform: translateY(-5px);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.badge.locked {
|
| 257 |
+
opacity: 0.4;
|
| 258 |
+
filter: grayscale(100%);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.badge-icon {
|
| 262 |
+
font-size: 64px;
|
| 263 |
+
margin-bottom: 15px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.badge-name {
|
| 267 |
+
font-size: 16px;
|
| 268 |
+
font-weight: bold;
|
| 269 |
+
color: #333;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* ================ RECORDS PAGE ================ */
|
| 273 |
+
|
| 274 |
+
.records-controls {
|
| 275 |
+
display: flex;
|
| 276 |
+
gap: 10px;
|
| 277 |
+
margin: 20px auto;
|
| 278 |
+
width: 80%;
|
| 279 |
+
justify-content: center;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.filter-btn {
|
| 283 |
+
padding: 10px 20px;
|
| 284 |
+
border: 2px solid #007BFF;
|
| 285 |
+
background: white;
|
| 286 |
+
color: #007BFF;
|
| 287 |
+
border-radius: 8px;
|
| 288 |
+
cursor: pointer;
|
| 289 |
+
font-family: 'Nunito', sans-serif;
|
| 290 |
+
font-weight: 600;
|
| 291 |
+
transition: all 0.2s;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.filter-btn:hover {
|
| 295 |
+
background: #e7f3ff;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.filter-btn.active {
|
| 299 |
+
background: #007BFF;
|
| 300 |
+
color: white;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.chart-container {
|
| 304 |
+
width: 80%;
|
| 305 |
+
background: white;
|
| 306 |
+
padding: 30px;
|
| 307 |
+
border-radius: 12px;
|
| 308 |
+
margin: 20px auto;
|
| 309 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
#focus-chart {
|
| 313 |
+
display: block;
|
| 314 |
+
margin: 0 auto;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.sessions-list {
|
| 318 |
+
width: 80%;
|
| 319 |
+
margin: 20px auto;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.sessions-list h2 {
|
| 323 |
+
color: #333;
|
| 324 |
+
margin-bottom: 15px;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
#sessions-table {
|
| 328 |
+
width: 100%;
|
| 329 |
+
background: white;
|
| 330 |
+
border-collapse: collapse;
|
| 331 |
+
border-radius: 12px;
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
#sessions-table th {
|
| 337 |
+
background: #007BFF;
|
| 338 |
+
color: white;
|
| 339 |
+
padding: 15px;
|
| 340 |
+
text-align: left;
|
| 341 |
+
font-weight: 600;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
#sessions-table td {
|
| 345 |
+
padding: 12px 15px;
|
| 346 |
+
border-bottom: 1px solid #eee;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
#sessions-table tr:last-child td {
|
| 350 |
+
border-bottom: none;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
#sessions-table tbody tr:hover {
|
| 354 |
+
background: #f8f9fa;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.btn-view {
|
| 358 |
+
padding: 6px 12px;
|
| 359 |
+
background: #007BFF;
|
| 360 |
+
color: white;
|
| 361 |
+
border: none;
|
| 362 |
+
border-radius: 5px;
|
| 363 |
+
cursor: pointer;
|
| 364 |
+
font-family: 'Nunito', sans-serif;
|
| 365 |
+
transition: background 0.2s;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.btn-view:hover {
|
| 369 |
+
background: #0056b3;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/* ================ SETTINGS PAGE ================ */
|
| 373 |
+
|
| 374 |
+
.settings-container {
|
| 375 |
+
width: 60%;
|
| 376 |
+
max-width: 800px;
|
| 377 |
+
margin: 20px auto;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.setting-group {
|
| 381 |
+
background: white;
|
| 382 |
+
padding: 30px;
|
| 383 |
+
border-radius: 12px;
|
| 384 |
+
margin-bottom: 20px;
|
| 385 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.setting-group h2 {
|
| 389 |
+
margin-top: 0;
|
| 390 |
+
color: #333;
|
| 391 |
+
font-size: 20px;
|
| 392 |
+
margin-bottom: 20px;
|
| 393 |
+
border-bottom: 2px solid #007BFF;
|
| 394 |
+
padding-bottom: 10px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.setting-item {
|
| 398 |
+
margin-bottom: 25px;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.setting-item:last-child {
|
| 402 |
+
margin-bottom: 0;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.setting-item label {
|
| 406 |
+
display: block;
|
| 407 |
+
margin-bottom: 8px;
|
| 408 |
+
color: #333;
|
| 409 |
+
font-weight: 600;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.slider-group {
|
| 413 |
+
display: flex;
|
| 414 |
+
align-items: center;
|
| 415 |
+
gap: 15px;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.slider-group input[type="range"] {
|
| 419 |
+
flex: 1;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.slider-group span {
|
| 423 |
+
min-width: 40px;
|
| 424 |
+
text-align: center;
|
| 425 |
+
font-weight: bold;
|
| 426 |
+
color: #007BFF;
|
| 427 |
+
font-size: 18px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.setting-description {
|
| 431 |
+
font-size: 14px;
|
| 432 |
+
color: #666;
|
| 433 |
+
margin-top: 5px;
|
| 434 |
+
font-style: italic;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
input[type="checkbox"] {
|
| 438 |
+
margin-right: 10px;
|
| 439 |
+
cursor: pointer;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
input[type="number"] {
|
| 443 |
+
width: 100px;
|
| 444 |
+
padding: 8px;
|
| 445 |
+
border: 1px solid #ccc;
|
| 446 |
+
border-radius: 5px;
|
| 447 |
+
font-family: 'Nunito', sans-serif;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.setting-group .action-btn {
|
| 451 |
+
margin-right: 10px;
|
| 452 |
+
margin-top: 10px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
#save-settings {
|
| 456 |
+
display: block;
|
| 457 |
+
margin: 20px auto;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
/* ================ HELP PAGE ================ */
|
| 461 |
+
|
| 462 |
+
.help-container {
|
| 463 |
+
width: 70%;
|
| 464 |
+
max-width: 900px;
|
| 465 |
+
margin: 20px auto;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.help-section {
|
| 469 |
+
background: white;
|
| 470 |
+
padding: 30px;
|
| 471 |
+
border-radius: 12px;
|
| 472 |
+
margin-bottom: 20px;
|
| 473 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.help-section h2 {
|
| 477 |
+
color: #007BFF;
|
| 478 |
+
margin-top: 0;
|
| 479 |
+
margin-bottom: 15px;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.help-section ol,
|
| 483 |
+
.help-section ul {
|
| 484 |
+
line-height: 1.8;
|
| 485 |
+
color: #333;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.help-section p {
|
| 489 |
+
line-height: 1.6;
|
| 490 |
+
color: #333;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
details {
|
| 494 |
+
margin: 15px 0;
|
| 495 |
+
cursor: pointer;
|
| 496 |
+
padding: 10px;
|
| 497 |
+
background: #f8f9fa;
|
| 498 |
+
border-radius: 5px;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
summary {
|
| 502 |
+
font-weight: bold;
|
| 503 |
+
padding: 5px;
|
| 504 |
+
color: #007BFF;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
details[open] summary {
|
| 508 |
+
margin-bottom: 10px;
|
| 509 |
+
border-bottom: 1px solid #ddd;
|
| 510 |
+
padding-bottom: 10px;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
details p {
|
| 514 |
+
margin: 10px 0 0 0;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
/* ================ SESSION SUMMARY MODAL ================ */
|
| 518 |
+
|
| 519 |
+
.modal-overlay {
|
| 520 |
+
position: fixed;
|
| 521 |
+
top: 0;
|
| 522 |
+
left: 0;
|
| 523 |
+
width: 100%;
|
| 524 |
+
height: 100%;
|
| 525 |
+
background: rgba(0, 0, 0, 0.7);
|
| 526 |
+
display: flex;
|
| 527 |
+
align-items: center;
|
| 528 |
+
justify-content: center;
|
| 529 |
+
z-index: 2000;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.modal-content {
|
| 533 |
+
background: white;
|
| 534 |
+
padding: 40px;
|
| 535 |
+
border-radius: 16px;
|
| 536 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
| 537 |
+
max-width: 500px;
|
| 538 |
+
width: 90%;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.modal-content h2 {
|
| 542 |
+
margin-top: 0;
|
| 543 |
+
color: #333;
|
| 544 |
+
text-align: center;
|
| 545 |
+
margin-bottom: 30px;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.summary-stats {
|
| 549 |
+
margin-bottom: 30px;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.summary-item {
|
| 553 |
+
display: flex;
|
| 554 |
+
justify-content: space-between;
|
| 555 |
+
padding: 15px 0;
|
| 556 |
+
border-bottom: 1px solid #eee;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.summary-item:last-child {
|
| 560 |
+
border-bottom: none;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.summary-label {
|
| 564 |
+
font-weight: 600;
|
| 565 |
+
color: #666;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.summary-value {
|
| 569 |
+
font-weight: bold;
|
| 570 |
+
color: #007BFF;
|
| 571 |
+
font-size: 18px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.modal-content .btn-main {
|
| 575 |
+
display: block;
|
| 576 |
+
margin: 0 auto;
|
| 577 |
+
padding: 12px 40px;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
/* ================ TIMELINE BLOCKS ================ */
|
| 581 |
+
|
| 582 |
+
.timeline-block {
|
| 583 |
+
transition: opacity 0.2s;
|
| 584 |
+
border-radius: 2px;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.timeline-block:hover {
|
| 588 |
+
opacity: 0.7;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
/* ================ RESPONSIVE DESIGN ================ */
|
| 592 |
+
|
| 593 |
+
@media (max-width: 1200px) {
|
| 594 |
+
.stats-grid {
|
| 595 |
+
grid-template-columns: repeat(2, 1fr);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.badges-grid {
|
| 599 |
+
grid-template-columns: repeat(2, 1fr);
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
@media (max-width: 768px) {
|
| 604 |
+
.stats-grid,
|
| 605 |
+
.badges-grid {
|
| 606 |
+
grid-template-columns: 1fr;
|
| 607 |
+
width: 90%;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.settings-container,
|
| 611 |
+
.help-container,
|
| 612 |
+
.chart-container,
|
| 613 |
+
.sessions-list,
|
| 614 |
+
.records-controls {
|
| 615 |
+
width: 90%;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
#control-panel {
|
| 619 |
+
width: 90%;
|
| 620 |
+
flex-wrap: wrap;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
#display-area {
|
| 624 |
+
width: 90%;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
#timeline-area {
|
| 628 |
+
width: 90%;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
#frame-control {
|
| 632 |
+
width: 90%;
|
| 633 |
+
flex-direction: column;
|
| 634 |
+
}
|
| 635 |
+
}
|