Kexin Wang commited on
Commit
04171ef
·
1 Parent(s): 9fcc92f

rm folder

Browse files
GAP_Large_project-fea-ui/.DS_Store DELETED
Binary file (6.15 kB)
 
GAP_Large_project-fea-ui/Dockerfile DELETED
@@ -1,24 +0,0 @@
1
- FROM python:3.10-slim
2
-
3
- WORKDIR /app
4
-
5
- # Install system dependencies for OpenCV
6
- RUN apt-get update && apt-get install -y \
7
- libglib2.0-0 \
8
- libsm6 \
9
- libxext6 \
10
- libxrender-dev \
11
- libgomp1 \
12
- libgthread-2.0-0 \
13
- libgl1 \
14
- libglib2.0-0 \
15
- && rm -rf /var/lib/apt/lists/*
16
-
17
- COPY requirements.txt .
18
- RUN pip install --no-cache-dir -r requirements.txt
19
-
20
- COPY . .
21
-
22
- EXPOSE 7860
23
-
24
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/README.md DELETED
@@ -1,9 +0,0 @@
1
- ---
2
- title: FOCUS GUARD
3
- colorFrom: green
4
- colorTo: red
5
- sdk: docker
6
- pinned: false
7
- ---
8
-
9
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/app.py DELETED
@@ -1,649 +0,0 @@
1
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
2
- from fastapi.staticfiles import StaticFiles
3
- from fastapi.responses import FileResponse
4
- from fastapi.middleware.cors import CORSMiddleware
5
- from pydantic import BaseModel
6
- from typing import Optional, List
7
- import base64
8
- import cv2
9
- import numpy as np
10
- import aiosqlite
11
- import json
12
- from datetime import datetime, timedelta
13
- import math
14
- import os
15
- from pathlib import Path
16
-
17
- # Initialize FastAPI app
18
- app = FastAPI(title="Focus Guard API")
19
-
20
- # Add CORS middleware
21
- app.add_middleware(
22
- CORSMiddleware,
23
- allow_origins=["*"],
24
- allow_credentials=True,
25
- allow_methods=["*"],
26
- allow_headers=["*"],
27
- )
28
-
29
- # Global variables
30
- model = None
31
- db_path = "focus_guard.db"
32
-
33
- # ================ DATABASE MODELS ================
34
-
35
- async def init_database():
36
- """Initialize SQLite database with required tables"""
37
- async with aiosqlite.connect(db_path) as db:
38
- # FocusSessions table
39
- await db.execute("""
40
- CREATE TABLE IF NOT EXISTS focus_sessions (
41
- id INTEGER PRIMARY KEY AUTOINCREMENT,
42
- start_time TIMESTAMP NOT NULL,
43
- end_time TIMESTAMP,
44
- duration_seconds INTEGER DEFAULT 0,
45
- focus_score REAL DEFAULT 0.0,
46
- total_frames INTEGER DEFAULT 0,
47
- focused_frames INTEGER DEFAULT 0,
48
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
49
- )
50
- """)
51
-
52
- # FocusEvents table
53
- await db.execute("""
54
- CREATE TABLE IF NOT EXISTS focus_events (
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- session_id INTEGER NOT NULL,
57
- timestamp TIMESTAMP NOT NULL,
58
- is_focused BOOLEAN NOT NULL,
59
- confidence REAL NOT NULL,
60
- detection_data TEXT,
61
- FOREIGN KEY (session_id) REFERENCES focus_sessions (id)
62
- )
63
- """)
64
-
65
- # UserSettings table
66
- await db.execute("""
67
- CREATE TABLE IF NOT EXISTS user_settings (
68
- id INTEGER PRIMARY KEY CHECK (id = 1),
69
- sensitivity INTEGER DEFAULT 6,
70
- notification_enabled BOOLEAN DEFAULT 1,
71
- notification_threshold INTEGER DEFAULT 30,
72
- frame_rate INTEGER DEFAULT 30,
73
- model_name TEXT DEFAULT 'yolov8n.pt'
74
- )
75
- """)
76
-
77
- # Insert default settings if not exists
78
- await db.execute("""
79
- INSERT OR IGNORE INTO user_settings (id, sensitivity, notification_enabled, notification_threshold, frame_rate, model_name)
80
- VALUES (1, 6, 1, 30, 30, 'yolov8n.pt')
81
- """)
82
-
83
- await db.commit()
84
-
85
- # ================ PYDANTIC MODELS ================
86
-
87
- class SessionCreate(BaseModel):
88
- pass
89
-
90
- class SessionEnd(BaseModel):
91
- session_id: int
92
-
93
- class SettingsUpdate(BaseModel):
94
- sensitivity: Optional[int] = None
95
- notification_enabled: Optional[bool] = None
96
- notification_threshold: Optional[int] = None
97
- frame_rate: Optional[int] = None
98
-
99
- # ================ YOLO MODEL LOADING ================
100
-
101
- def load_yolo_model():
102
- """Load YOLOv8 model with optimizations for CPU"""
103
- global model
104
- try:
105
- # Fix PyTorch 2.6+ weights_only issue
106
- # Set environment variable to allow loading YOLO weights
107
- os.environ['TORCH_LOAD_WEIGHTS_ONLY'] = '0'
108
-
109
- import torch
110
- if hasattr(torch.serialization, 'add_safe_globals'):
111
- # PyTorch 2.6+ compatibility - add required classes
112
- try:
113
- from ultralytics.nn.tasks import DetectionModel
114
- import torch.nn as nn
115
- torch.serialization.add_safe_globals([
116
- DetectionModel,
117
- nn.modules.container.Sequential,
118
- ])
119
- except Exception as e:
120
- print(f" Safe globals setup: {e}")
121
-
122
- from ultralytics import YOLO
123
-
124
- model_path = "models/yolov8n.pt"
125
-
126
- # Check if model file exists, if not use yolov8n (will download)
127
- if not os.path.exists(model_path):
128
- print(f"Model file {model_path} not found, downloading yolov8n.pt...")
129
- model_path = "yolov8n.pt" # This will trigger auto-download
130
-
131
- # Load model (ultralytics handles weights_only internally in newer versions)
132
- model = YOLO(model_path)
133
-
134
- # Optimize for CPU
135
- try:
136
- model.fuse() # Fuse Conv2d + BatchNorm layers
137
- print("[OK] Model layers fused for optimization")
138
- except Exception as e:
139
- print(f" Model fusion skipped: {e}")
140
-
141
- # Warm up model with dummy inference
142
- print("Warming up model...")
143
- dummy_img = np.zeros((416, 416, 3), dtype=np.uint8)
144
- model(dummy_img, imgsz=416, conf=0.4, iou=0.45, max_det=5, classes=[0], verbose=False)
145
-
146
- print("[OK] YOLOv8 model loaded and warmed up successfully")
147
- return True
148
- except Exception as e:
149
- print(f"[ERROR] Failed to load YOLOv8 model: {e}")
150
- print(" The app will run without detection features")
151
- import traceback
152
- traceback.print_exc()
153
- return False
154
-
155
- # ================ FOCUS DETECTION ALGORITHM ================
156
-
157
- def is_user_focused(detections, frame_shape, sensitivity=6):
158
- """
159
- Determine if user is focused based on YOLOv8 detections
160
-
161
- Simple logic: Detects person with confidence >= 80% (0.8)
162
-
163
- Args:
164
- detections: List of detection dictionaries
165
- frame_shape: Tuple of (height, width, channels)
166
- sensitivity: Integer 1-10, higher = stricter criteria (adjusts confidence threshold)
167
-
168
- Returns:
169
- Tuple of (is_focused: bool, confidence: float, metadata: dict)
170
- """
171
- # Filter person detections (class 0 in COCO dataset)
172
- persons = [d for d in detections if d.get('class') == 0]
173
-
174
- if not persons:
175
- return False, 0.0, {'reason': 'no_person', 'count': 0}
176
-
177
- # Find person with highest confidence
178
- best_person = max(persons, key=lambda x: x.get('confidence', 0))
179
- bbox = best_person['bbox'] # [x1, y1, x2, y2]
180
- conf = best_person['confidence']
181
-
182
- # Calculate confidence threshold based on sensitivity
183
- # sensitivity 6 (default) = 0.8 threshold
184
- # sensitivity 1 (lowest) = 0.5 threshold
185
- # sensitivity 10 (highest) = 0.9 threshold
186
- base_threshold = 0.8
187
- sensitivity_adjustment = (sensitivity - 6) * 0.02 # ±0.08 range
188
- confidence_threshold = base_threshold + sensitivity_adjustment
189
- confidence_threshold = max(0.5, min(0.95, confidence_threshold)) # Clamp to 0.5-0.95
190
-
191
- # Simple focus determination: confidence >= threshold
192
- is_focused = conf >= confidence_threshold
193
-
194
- # Optional: Check if person is somewhat centered (loose requirement)
195
- h, w = frame_shape[0], frame_shape[1]
196
- bbox_center_x = (bbox[0] + bbox[2]) / 2
197
- bbox_center_y = (bbox[1] + bbox[3]) / 2
198
-
199
- # Normalize to 0-1 range
200
- center_x_norm = bbox_center_x / w if w > 0 else 0.5
201
- center_y_norm = bbox_center_y / h if h > 0 else 0.5
202
-
203
- # Check if person is in frame (not at extreme edges)
204
- # Allow very loose centering: 20%-80% horizontal, 15%-85% vertical
205
- in_frame = (0.2 <= center_x_norm <= 0.8) and (0.15 <= center_y_norm <= 0.85)
206
-
207
- # Reduce focus score if person is at extreme edge
208
- position_factor = 1.0 if in_frame else 0.7
209
- final_score = conf * position_factor
210
-
211
- # Also reduce if multiple persons detected
212
- if len(persons) > 1:
213
- final_score *= 0.9
214
- reason = f"person_detected_multi_{len(persons)}"
215
- else:
216
- reason = "person_detected" if is_focused else "low_confidence"
217
-
218
- metadata = {
219
- 'bbox': bbox,
220
- 'detection_confidence': round(conf, 3),
221
- 'confidence_threshold': round(confidence_threshold, 3),
222
- 'center_position': [round(center_x_norm, 3), round(center_y_norm, 3)],
223
- 'in_frame': in_frame,
224
- 'person_count': len(persons),
225
- 'reason': reason
226
- }
227
-
228
- return is_focused and in_frame, final_score, metadata
229
-
230
- def parse_yolo_results(results):
231
- """Parse YOLOv8 results into a list of detections"""
232
- detections = []
233
-
234
- if results and len(results) > 0:
235
- result = results[0]
236
- boxes = result.boxes
237
-
238
- if boxes is not None and len(boxes) > 0:
239
- for box in boxes:
240
- # Get box coordinates
241
- xyxy = box.xyxy[0].cpu().numpy()
242
- conf = float(box.conf[0].cpu().numpy())
243
- cls = int(box.cls[0].cpu().numpy())
244
-
245
- detection = {
246
- 'bbox': [float(x) for x in xyxy],
247
- 'confidence': conf,
248
- 'class': cls,
249
- 'class_name': result.names[cls] if hasattr(result, 'names') else str(cls)
250
- }
251
- detections.append(detection)
252
-
253
- return detections
254
-
255
- # ================ DATABASE OPERATIONS ================
256
-
257
- async def create_session():
258
- """Create a new focus session"""
259
- async with aiosqlite.connect(db_path) as db:
260
- cursor = await db.execute(
261
- "INSERT INTO focus_sessions (start_time) VALUES (?)",
262
- (datetime.now().isoformat(),)
263
- )
264
- await db.commit()
265
- return cursor.lastrowid
266
-
267
- async def end_session(session_id: int):
268
- """End a focus session and calculate statistics"""
269
- async with aiosqlite.connect(db_path) as db:
270
- # Get session data
271
- cursor = await db.execute(
272
- "SELECT start_time, total_frames, focused_frames FROM focus_sessions WHERE id = ?",
273
- (session_id,)
274
- )
275
- row = await cursor.fetchone()
276
-
277
- if not row:
278
- return None
279
-
280
- start_time_str, total_frames, focused_frames = row
281
- start_time = datetime.fromisoformat(start_time_str)
282
- end_time = datetime.now()
283
- duration = (end_time - start_time).total_seconds()
284
-
285
- # Calculate focus score
286
- focus_score = focused_frames / total_frames if total_frames > 0 else 0.0
287
-
288
- # Update session
289
- await db.execute("""
290
- UPDATE focus_sessions
291
- SET end_time = ?, duration_seconds = ?, focus_score = ?
292
- WHERE id = ?
293
- """, (end_time.isoformat(), int(duration), focus_score, session_id))
294
-
295
- await db.commit()
296
-
297
- return {
298
- 'session_id': session_id,
299
- 'start_time': start_time_str,
300
- 'end_time': end_time.isoformat(),
301
- 'duration_seconds': int(duration),
302
- 'focus_score': round(focus_score, 3),
303
- 'total_frames': total_frames,
304
- 'focused_frames': focused_frames
305
- }
306
-
307
- async def store_focus_event(session_id: int, is_focused: bool, confidence: float, metadata: dict):
308
- """Store a focus detection event"""
309
- async with aiosqlite.connect(db_path) as db:
310
- await db.execute("""
311
- INSERT INTO focus_events (session_id, timestamp, is_focused, confidence, detection_data)
312
- VALUES (?, ?, ?, ?, ?)
313
- """, (session_id, datetime.now().isoformat(), is_focused, confidence, json.dumps(metadata)))
314
-
315
- # Update session frame counts
316
- await db.execute(f"""
317
- UPDATE focus_sessions
318
- SET total_frames = total_frames + 1,
319
- focused_frames = focused_frames + {1 if is_focused else 0}
320
- WHERE id = ?
321
- """, (session_id,))
322
-
323
- await db.commit()
324
-
325
- # ================ STARTUP/SHUTDOWN EVENTS ================
326
-
327
- @app.on_event("startup")
328
- async def startup_event():
329
- """Initialize database and load model on startup"""
330
- print(" Starting Focus Guard API...")
331
- await init_database()
332
- print("[OK] Database initialized")
333
- load_yolo_model()
334
-
335
- @app.on_event("shutdown")
336
- async def shutdown_event():
337
- """Cleanup on shutdown"""
338
- print(" Shutting down Focus Guard API...")
339
-
340
- # ================ STATIC FILES ================
341
-
342
- app.mount("/static", StaticFiles(directory="static"), name="static")
343
-
344
- @app.get("/")
345
- async def read_index():
346
- return FileResponse("static/index.html")
347
-
348
- # ================ WEBSOCKET ENDPOINT ================
349
-
350
- @app.websocket("/ws/video")
351
- async def websocket_endpoint(websocket: WebSocket):
352
- await websocket.accept()
353
- session_id = None
354
- frame_count = 0
355
- last_inference_time = 0
356
- min_inference_interval = 0.1 # Max 10 FPS server-side
357
-
358
- try:
359
- # Get user settings
360
- async with aiosqlite.connect(db_path) as db:
361
- cursor = await db.execute("SELECT sensitivity FROM user_settings WHERE id = 1")
362
- row = await cursor.fetchone()
363
- sensitivity = row[0] if row else 6
364
-
365
- while True:
366
- # Receive data from client
367
- data = await websocket.receive_json()
368
-
369
- if data['type'] == 'frame':
370
- from time import time
371
- current_time = time()
372
-
373
- # Rate limiting
374
- if current_time - last_inference_time < min_inference_interval:
375
- # Skip inference, just acknowledge
376
- await websocket.send_json({
377
- 'type': 'ack',
378
- 'frame_count': frame_count
379
- })
380
- continue
381
-
382
- last_inference_time = current_time
383
-
384
- try:
385
- # Decode base64 image
386
- img_data = base64.b64decode(data['image'])
387
- nparr = np.frombuffer(img_data, np.uint8)
388
- frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
389
-
390
- if frame is None:
391
- continue
392
-
393
- # Resize for faster inference
394
- frame = cv2.resize(frame, (640, 480))
395
-
396
- # YOLOv8 inference
397
- if model is not None:
398
- results = model(
399
- frame,
400
- imgsz=416,
401
- conf=0.4,
402
- iou=0.45,
403
- max_det=5,
404
- classes=[0], # Only person class
405
- verbose=False
406
- )
407
- detections = parse_yolo_results(results)
408
- else:
409
- # Fallback if model not loaded
410
- detections = []
411
-
412
- # Determine focus status
413
- is_focused, confidence, metadata = is_user_focused(
414
- detections, frame.shape, sensitivity
415
- )
416
-
417
- # Store event in database if session active
418
- if session_id:
419
- await store_focus_event(session_id, is_focused, confidence, metadata)
420
-
421
- # Send results back to client
422
- response = {
423
- 'type': 'detection',
424
- 'focused': is_focused,
425
- 'confidence': round(confidence, 3),
426
- 'detections': detections,
427
- 'frame_count': frame_count
428
- }
429
-
430
- await websocket.send_json(response)
431
- frame_count += 1
432
-
433
- except Exception as e:
434
- print(f"Error processing frame: {e}")
435
- await websocket.send_json({
436
- 'type': 'error',
437
- 'message': str(e)
438
- })
439
-
440
- elif data['type'] == 'start_session':
441
- session_id = await create_session()
442
- await websocket.send_json({
443
- 'type': 'session_started',
444
- 'session_id': session_id
445
- })
446
-
447
- elif data['type'] == 'end_session':
448
- if session_id:
449
- summary = await end_session(session_id)
450
- await websocket.send_json({
451
- 'type': 'session_ended',
452
- 'summary': summary
453
- })
454
- session_id = None
455
-
456
- except WebSocketDisconnect:
457
- if session_id:
458
- await end_session(session_id)
459
- print(f"WebSocket disconnected (session: {session_id})")
460
- except Exception as e:
461
- print(f"WebSocket error: {e}")
462
- if websocket.client_state.value == 1: # CONNECTED
463
- await websocket.close()
464
-
465
- # ================ REST API ENDPOINTS ================
466
-
467
- @app.post("/api/sessions/start")
468
- async def api_start_session():
469
- """Start a new focus session"""
470
- session_id = await create_session()
471
- return {"session_id": session_id}
472
-
473
- @app.post("/api/sessions/end")
474
- async def api_end_session(data: SessionEnd):
475
- """End a focus session"""
476
- summary = await end_session(data.session_id)
477
- if not summary:
478
- raise HTTPException(status_code=404, detail="Session not found")
479
- return summary
480
-
481
- @app.get("/api/sessions")
482
- async def get_sessions(filter: str = "all", limit: int = 50, offset: int = 0):
483
- """Get focus sessions with optional filtering"""
484
- async with aiosqlite.connect(db_path) as db:
485
- db.row_factory = aiosqlite.Row
486
-
487
- # Build query based on filter
488
- if filter == "today":
489
- date_filter = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
490
- query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
491
- params = (date_filter.isoformat(), limit, offset)
492
- elif filter == "week":
493
- date_filter = datetime.now() - timedelta(days=7)
494
- query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
495
- params = (date_filter.isoformat(), limit, offset)
496
- elif filter == "month":
497
- date_filter = datetime.now() - timedelta(days=30)
498
- query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
499
- params = (date_filter.isoformat(), limit, offset)
500
- else:
501
- query = "SELECT * FROM focus_sessions WHERE end_time IS NOT NULL ORDER BY start_time DESC LIMIT ? OFFSET ?"
502
- params = (limit, offset)
503
-
504
- cursor = await db.execute(query, params)
505
- rows = await cursor.fetchall()
506
-
507
- sessions = [dict(row) for row in rows]
508
- return sessions
509
-
510
- @app.get("/api/sessions/{session_id}")
511
- async def get_session(session_id: int):
512
- """Get detailed session information"""
513
- async with aiosqlite.connect(db_path) as db:
514
- db.row_factory = aiosqlite.Row
515
- cursor = await db.execute("SELECT * FROM focus_sessions WHERE id = ?", (session_id,))
516
- row = await cursor.fetchone()
517
-
518
- if not row:
519
- raise HTTPException(status_code=404, detail="Session not found")
520
-
521
- session = dict(row)
522
-
523
- # Get events
524
- cursor = await db.execute(
525
- "SELECT * FROM focus_events WHERE session_id = ? ORDER BY timestamp",
526
- (session_id,)
527
- )
528
- events = [dict(r) for r in await cursor.fetchall()]
529
- session['events'] = events
530
-
531
- return session
532
-
533
- @app.get("/api/settings")
534
- async def get_settings():
535
- """Get user settings"""
536
- async with aiosqlite.connect(db_path) as db:
537
- db.row_factory = aiosqlite.Row
538
- cursor = await db.execute("SELECT * FROM user_settings WHERE id = 1")
539
- row = await cursor.fetchone()
540
-
541
- if row:
542
- return dict(row)
543
- else:
544
- return {
545
- 'sensitivity': 6,
546
- 'notification_enabled': True,
547
- 'notification_threshold': 30,
548
- 'frame_rate': 30,
549
- 'model_name': 'yolov8n.pt'
550
- }
551
-
552
- @app.put("/api/settings")
553
- async def update_settings(settings: SettingsUpdate):
554
- """Update user settings"""
555
- async with aiosqlite.connect(db_path) as db:
556
- # First ensure the record exists
557
- cursor = await db.execute("SELECT id FROM user_settings WHERE id = 1")
558
- exists = await cursor.fetchone()
559
-
560
- if not exists:
561
- # Insert default record if it doesn't exist
562
- await db.execute("""
563
- INSERT INTO user_settings (id, sensitivity, notification_enabled, notification_threshold, frame_rate, model_name)
564
- VALUES (1, 6, 1, 30, 30, 'yolov8n.pt')
565
- """)
566
- await db.commit()
567
- print("[OK] Created default user_settings record")
568
-
569
- # Now update with provided values
570
- updates = []
571
- params = []
572
-
573
- if settings.sensitivity is not None:
574
- updates.append("sensitivity = ?")
575
- params.append(max(1, min(10, settings.sensitivity)))
576
-
577
- if settings.notification_enabled is not None:
578
- updates.append("notification_enabled = ?")
579
- params.append(settings.notification_enabled)
580
-
581
- if settings.notification_threshold is not None:
582
- updates.append("notification_threshold = ?")
583
- params.append(max(5, min(300, settings.notification_threshold)))
584
-
585
- if settings.frame_rate is not None:
586
- updates.append("frame_rate = ?")
587
- params.append(max(5, min(60, settings.frame_rate)))
588
-
589
- if updates:
590
- query = f"UPDATE user_settings SET {', '.join(updates)} WHERE id = 1"
591
- await db.execute(query, params)
592
- await db.commit()
593
- print(f"[OK] Settings updated: {settings.model_dump(exclude_none=True)}")
594
-
595
- return {"status": "success", "updated": len(updates) > 0}
596
-
597
- @app.get("/api/stats/summary")
598
- async def get_stats_summary():
599
- """Get overall statistics summary"""
600
- async with aiosqlite.connect(db_path) as db:
601
- # Total sessions
602
- cursor = await db.execute("SELECT COUNT(*) FROM focus_sessions WHERE end_time IS NOT NULL")
603
- total_sessions = (await cursor.fetchone())[0]
604
-
605
- # Total focus time
606
- cursor = await db.execute("SELECT SUM(duration_seconds) FROM focus_sessions WHERE end_time IS NOT NULL")
607
- total_focus_time = (await cursor.fetchone())[0] or 0
608
-
609
- # Average focus score
610
- cursor = await db.execute("SELECT AVG(focus_score) FROM focus_sessions WHERE end_time IS NOT NULL")
611
- avg_focus_score = (await cursor.fetchone())[0] or 0.0
612
-
613
- # Streak calculation (consecutive days with sessions)
614
- cursor = await db.execute("""
615
- SELECT DISTINCT DATE(start_time) as session_date
616
- FROM focus_sessions
617
- WHERE end_time IS NOT NULL
618
- ORDER BY session_date DESC
619
- """)
620
- dates = [row[0] for row in await cursor.fetchall()]
621
-
622
- streak_days = 0
623
- if dates:
624
- current_date = datetime.now().date()
625
- for i, date_str in enumerate(dates):
626
- session_date = datetime.fromisoformat(date_str).date()
627
- expected_date = current_date - timedelta(days=i)
628
- if session_date == expected_date:
629
- streak_days += 1
630
- else:
631
- break
632
-
633
- return {
634
- 'total_sessions': total_sessions,
635
- 'total_focus_time': int(total_focus_time),
636
- 'avg_focus_score': round(avg_focus_score, 3),
637
- 'streak_days': streak_days
638
- }
639
-
640
- # ================ HEALTH CHECK ================
641
-
642
- @app.get("/health")
643
- async def health_check():
644
- """Health check endpoint"""
645
- return {
646
- "status": "healthy",
647
- "model_loaded": model is not None,
648
- "database": os.path.exists(db_path)
649
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/assets/README.md DELETED
@@ -1,9 +0,0 @@
1
- ---
2
- title: FOCUS GUARD
3
- colorFrom: green
4
- colorTo: red
5
- sdk: docker
6
- pinned: false
7
- ---
8
-
9
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/focus_guard.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:fb042116505f0019d73e87a8262be3ef313b2b1f1471c3fd9bd1d61ee34d73ea
3
- size 1105920
 
 
 
 
GAP_Large_project-fea-ui/models/yolov8n.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:31e20dde3def09e2cf938c7be6fe23d9150bbbe503982af13345706515f2ef95
3
- size 6534387
 
 
 
 
GAP_Large_project-fea-ui/requirements.txt DELETED
@@ -1,12 +0,0 @@
1
- fastapi==0.104.1
2
- uvicorn[standard]==0.24.0
3
- websockets==12.0
4
- opencv-python-headless==4.8.1.78
5
- ultralytics==8.0.206
6
- numpy==1.24.3
7
- Pillow==10.1.0
8
- python-multipart==0.0.6
9
- aiosqlite==0.19.0
10
- pydantic==2.4.2
11
- torch==2.5.1
12
- torchvision==0.20.1
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/static/index.html DELETED
@@ -1,244 +0,0 @@
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/static/script.js DELETED
@@ -1,794 +0,0 @@
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');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/static/styles.css DELETED
@@ -1,635 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
GAP_Large_project-fea-ui/yolov8n.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:31e20dde3def09e2cf938c7be6fe23d9150bbbe503982af13345706515f2ef95
3
- size 6534387