Kexin-251202 commited on
Commit
dd46120
·
verified ·
1 Parent(s): 04171ef

Upload 3 files

Browse files
Files changed (3) hide show
  1. static/index.html +244 -0
  2. static/script.js +794 -0
  3. 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
+ }