Spaces:
Sleeping
Sleeping
Merge hf-final_v2/main (HF Space final_v2) into hugging_face_final
Browse files- .coverage +0 -0
- checkpoints/L2CSNet_gaze360.pkl +3 -0
- checkpoints/hybrid_focus_config.json +3 -7
- src/App.css +2047 -1992
- src/App.jsx +125 -91
- src/components/FocusPageLocal.jsx +1040 -944
- src/components/Help.jsx +44 -15
- src/components/Home.jsx +22 -18
- src/components/Records.jsx +757 -645
- src/src/App.css +2047 -0
- src/src/App.jsx +125 -0
- ui/pipeline.py +8 -5
.coverage
DELETED
|
Binary file (86 kB)
|
|
|
checkpoints/L2CSNet_gaze360.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8a7f3480d868dd48261e1d59f915b0ef0bb33ea12ea00938fb2168f212080665
|
| 3 |
+
size 95849977
|
checkpoints/hybrid_focus_config.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
| 1 |
{
|
| 2 |
-
"use_xgb":
|
| 3 |
"w_mlp": 0.3,
|
| 4 |
-
"w_xgb": 0.3,
|
| 5 |
"w_geo": 0.7,
|
| 6 |
-
"threshold": 0.
|
| 7 |
"use_yawn_veto": true,
|
| 8 |
"geo_face_weight": 0.7,
|
| 9 |
"geo_eye_weight": 0.3,
|
| 10 |
-
"mar_yawn_threshold": 0.55
|
| 11 |
-
"metric": "f1",
|
| 12 |
-
"combiner": "logistic",
|
| 13 |
-
"combiner_path": "hybrid_combiner.joblib"
|
| 14 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"use_xgb": false,
|
| 3 |
"w_mlp": 0.3,
|
|
|
|
| 4 |
"w_geo": 0.7,
|
| 5 |
+
"threshold": 0.35,
|
| 6 |
"use_yawn_veto": true,
|
| 7 |
"geo_face_weight": 0.7,
|
| 8 |
"geo_eye_weight": 0.3,
|
| 9 |
+
"mar_yawn_threshold": 0.55
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
src/App.css
CHANGED
|
@@ -1,1992 +1,2047 @@
|
|
| 1 |
-
/* =========================================
|
| 2 |
-
1. REACT layout setting
|
| 3 |
-
========================================= */
|
| 4 |
-
html, body, #root {
|
| 5 |
-
width: 100%;
|
| 6 |
-
height: 100%;
|
| 7 |
-
margin: 0;
|
| 8 |
-
padding: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
.app-container {
|
| 12 |
-
width: 100%;
|
| 13 |
-
min-height: 100vh; /* screen height */
|
| 14 |
-
display: flex;
|
| 15 |
-
flex-direction: column;
|
| 16 |
-
background-color: #f9f9f9;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
/* =========================================
|
| 20 |
-
2. original layout
|
| 21 |
-
========================================= */
|
| 22 |
-
|
| 23 |
-
/* GLOBAL STYLES */
|
| 24 |
-
body {
|
| 25 |
-
font-family: 'Nunito', sans-serif;
|
| 26 |
-
background-color: #f9f9f9;
|
| 27 |
-
overflow-x: hidden;
|
| 28 |
-
overflow-y: auto;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
/* dynamic class name */
|
| 32 |
-
.hidden {
|
| 33 |
-
display: none !important;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
/* TOP MENU */
|
| 37 |
-
#top-menu {
|
| 38 |
-
height: 60px;
|
| 39 |
-
background-color: white;
|
| 40 |
-
display: flex;
|
| 41 |
-
align-items: center;
|
| 42 |
-
justify-content:
|
| 43 |
-
gap: 0;
|
| 44 |
-
padding: 0 16px 0 20px;
|
| 45 |
-
box-sizing: border-box;
|
| 46 |
-
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 47 |
-
position: fixed;
|
| 48 |
-
top: 0;
|
| 49 |
-
left: 0;
|
| 50 |
-
right: 0;
|
| 51 |
-
width: 100%;
|
| 52 |
-
z-index: 1000;
|
| 53 |
-
overflow-x: auto;
|
| 54 |
-
overflow-y: hidden;
|
| 55 |
-
white-space: nowrap;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
.top-menu-links {
|
| 59 |
-
flex: 1;
|
| 60 |
-
display: flex;
|
| 61 |
-
align-items: center;
|
| 62 |
-
justify-content: center;
|
| 63 |
-
flex-wrap: wrap;
|
| 64 |
-
gap: 0;
|
| 65 |
-
min-width: 0;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.menu-btn {
|
| 69 |
-
background: none;
|
| 70 |
-
border: none;
|
| 71 |
-
font-family: 'Nunito', sans-serif;
|
| 72 |
-
font-size: 16px;
|
| 73 |
-
color: #333;
|
| 74 |
-
padding: 10px 20px;
|
| 75 |
-
cursor: pointer;
|
| 76 |
-
transition: background-color 0.2s;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
.menu-btn:hover {
|
| 80 |
-
background-color: #f0f0f0;
|
| 81 |
-
border-radius: 4px;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
/* active for React */
|
| 85 |
-
.menu-btn.active {
|
| 86 |
-
font-weight: bold;
|
| 87 |
-
color: #007BFF;
|
| 88 |
-
background-color: #eef7ff;
|
| 89 |
-
border-radius: 4px;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
.separator {
|
| 93 |
-
width: 1px;
|
| 94 |
-
height: 20px;
|
| 95 |
-
background-color: #555; /* Dark gray separator */
|
| 96 |
-
margin: 0 5px;
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
/* PAGE CONTAINER */
|
| 100 |
-
.page {
|
| 101 |
-
/* content under menu */
|
| 102 |
-
min-height: calc(100vh - 60px);
|
| 103 |
-
width: 100%;
|
| 104 |
-
padding-top: 60px; /* Space for fixed menu */
|
| 105 |
-
padding-bottom: 40px; /* Space at bottom for scrolling */
|
| 106 |
-
box-sizing: border-box;
|
| 107 |
-
display: flex;
|
| 108 |
-
flex-direction: column;
|
| 109 |
-
align-items: center;
|
| 110 |
-
overflow-y: auto;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
/* Ensure page titles are black */
|
| 114 |
-
.page h1 {
|
| 115 |
-
color: #000 !important;
|
| 116 |
-
background: transparent !important;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.page-title {
|
| 120 |
-
color: #000 !important;
|
| 121 |
-
background: transparent !important;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/* PAGE A SPECIFIC */
|
| 125 |
-
#page-a {
|
| 126 |
-
justify-content: center; /* Center vertically */
|
| 127 |
-
/* Fine-tune this margin if the Home screen sits slightly too low. */
|
| 128 |
-
margin-top: -40px;
|
| 129 |
-
flex: 1; /* Fill the remaining height so vertical centering still works. */
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
#page-a h1 {
|
| 133 |
-
font-size: 80px;
|
| 134 |
-
margin: 0 0 10px 0;
|
| 135 |
-
color: #000;
|
| 136 |
-
text-align: center; /* Keep the heading centered. */
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
#page-a p {
|
| 140 |
-
color: #666;
|
| 141 |
-
font-size: 20px;
|
| 142 |
-
margin-bottom: 40px;
|
| 143 |
-
text-align: center;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.btn-main {
|
| 147 |
-
background-color: #007BFF; /* Blue */
|
| 148 |
-
color: white;
|
| 149 |
-
border: none;
|
| 150 |
-
padding: 15px 50px;
|
| 151 |
-
font-size: 20px;
|
| 152 |
-
font-family: 'Nunito', sans-serif;
|
| 153 |
-
border-radius: 30px; /* Fully rounded corners */
|
| 154 |
-
cursor: pointer;
|
| 155 |
-
transition: transform 0.2s ease;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.btn-main:hover {
|
| 159 |
-
transform: scale(1.1); /* Zoom effect */
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
/* PAGE B SPECIFIC */
|
| 163 |
-
#page-b {
|
| 164 |
-
justify-content: space-evenly; /* Distribute vertical space */
|
| 165 |
-
padding-bottom: 20px;
|
| 166 |
-
min-height: calc(100vh - 60px); /* Ensure the page still fills the viewport. */
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
/* 1. Display Area */
|
| 170 |
-
#display-area {
|
| 171 |
-
width: 60%;
|
| 172 |
-
height: 50vh; /* Use viewport height to scale more consistently across screens. */
|
| 173 |
-
min-height: 300px;
|
| 174 |
-
border: 2px solid #ddd;
|
| 175 |
-
border-radius: 12px;
|
| 176 |
-
background-color: #fff;
|
| 177 |
-
display: flex;
|
| 178 |
-
align-items: center;
|
| 179 |
-
justify-content: center;
|
| 180 |
-
color: #555;
|
| 181 |
-
font-size: 24px;
|
| 182 |
-
position: relative;
|
| 183 |
-
/* Keep video content centered without overflowing the frame. */
|
| 184 |
-
overflow: hidden;
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
.focus-display-shell {
|
| 188 |
-
background: #101010;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
.focus-flow-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
.focus-flow-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
background: #
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
background: #
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
}
|
| 395 |
-
|
| 396 |
-
.focus-
|
| 397 |
-
background:
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
font-size:
|
| 429 |
-
font-weight: 800;
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
box-
|
| 454 |
-
}
|
| 455 |
-
|
| 456 |
-
.focus-
|
| 457 |
-
position: absolute;
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
border-radius:
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
.model-card-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
color: #007BFF;
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
.model-card-
|
| 587 |
-
margin: 0 0
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
}
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
.
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
font-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
}
|
| 674 |
-
|
| 675 |
-
.
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
color: #
|
| 691 |
-
|
| 692 |
-
}
|
| 693 |
-
|
| 694 |
-
.
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
}
|
| 730 |
-
|
| 731 |
-
.
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
}
|
| 774 |
-
|
| 775 |
-
.
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
.
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
gap: 20px;
|
| 826 |
-
width:
|
| 827 |
-
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
.
|
| 831 |
-
|
| 832 |
-
padding:
|
| 833 |
-
border
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
.
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
}
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
padding:
|
| 869 |
-
border
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
}
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
}
|
| 904 |
-
|
| 905 |
-
.
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
}
|
| 920 |
-
|
| 921 |
-
.
|
| 922 |
-
background:
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
}
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
}
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
}
|
| 1078 |
-
|
| 1079 |
-
.records-detail-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
color: #
|
| 1112 |
-
font-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
}
|
| 1153 |
-
|
| 1154 |
-
.records-detail-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
font-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
background: #
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
}
|
| 1291 |
-
|
| 1292 |
-
.records-detail-
|
| 1293 |
-
background: #
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
|
| 1317 |
-
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
.
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
}
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
.help-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
margin
|
| 1471 |
-
|
| 1472 |
-
|
| 1473 |
-
|
| 1474 |
-
|
| 1475 |
-
|
| 1476 |
-
|
| 1477 |
-
|
| 1478 |
-
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
|
| 1505 |
-
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
| 1535 |
-
|
| 1536 |
-
|
| 1537 |
-
}
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
margin
|
| 1541 |
-
|
| 1542 |
-
|
| 1543 |
-
|
| 1544 |
-
|
| 1545 |
-
|
| 1546 |
-
|
| 1547 |
-
|
| 1548 |
-
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
|
| 1555 |
-
|
| 1556 |
-
|
| 1557 |
-
}
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
.modal-
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
|
| 1572 |
-
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
| 1576 |
-
}
|
| 1577 |
-
|
| 1578 |
-
.
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
|
| 1585 |
-
|
| 1586 |
-
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
|
| 1590 |
-
|
| 1591 |
-
|
| 1592 |
-
}
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
|
| 1601 |
-
|
| 1602 |
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
| 1635 |
-
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
-
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
| 1653 |
-
|
| 1654 |
-
}
|
| 1655 |
-
|
| 1656 |
-
.
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
.
|
| 1661 |
-
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
| 1666 |
-
|
| 1667 |
-
|
| 1668 |
-
|
| 1669 |
-
|
| 1670 |
-
|
| 1671 |
-
|
| 1672 |
-
|
| 1673 |
-
|
| 1674 |
-
|
| 1675 |
-
|
| 1676 |
-
|
| 1677 |
-
|
| 1678 |
-
|
| 1679 |
-
|
| 1680 |
-
|
| 1681 |
-
|
| 1682 |
-
.
|
| 1683 |
-
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
|
| 1687 |
-
|
| 1688 |
-
|
| 1689 |
-
|
| 1690 |
-
|
| 1691 |
-
}
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
| 1700 |
-
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
| 1708 |
-
|
| 1709 |
-
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
-
|
| 1713 |
-
|
| 1714 |
-
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
|
| 1720 |
-
|
| 1721 |
-
.
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
.
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
}
|
| 1735 |
-
|
| 1736 |
-
.
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
}
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
}
|
| 1746 |
-
|
| 1747 |
-
/* =================
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
| 1762 |
-
|
| 1763 |
-
|
| 1764 |
-
|
| 1765 |
-
|
| 1766 |
-
|
| 1767 |
-
|
| 1768 |
-
.
|
| 1769 |
-
|
| 1770 |
-
|
| 1771 |
-
/*
|
| 1772 |
-
|
| 1773 |
-
|
| 1774 |
-
|
| 1775 |
-
|
| 1776 |
-
|
| 1777 |
-
|
| 1778 |
-
|
| 1779 |
-
.
|
| 1780 |
-
|
| 1781 |
-
|
| 1782 |
-
border-
|
| 1783 |
-
|
| 1784 |
-
|
| 1785 |
-
|
| 1786 |
-
|
| 1787 |
-
|
| 1788 |
-
|
| 1789 |
-
|
| 1790 |
-
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
|
| 1794 |
-
|
| 1795 |
-
|
| 1796 |
-
|
| 1797 |
-
|
| 1798 |
-
|
| 1799 |
-
|
| 1800 |
-
|
| 1801 |
-
|
| 1802 |
-
|
| 1803 |
-
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
|
| 1808 |
-
|
| 1809 |
-
|
| 1810 |
-
|
| 1811 |
-
}
|
| 1812 |
-
|
| 1813 |
-
|
| 1814 |
-
|
| 1815 |
-
|
| 1816 |
-
|
| 1817 |
-
|
| 1818 |
-
|
| 1819 |
-
|
| 1820 |
-
|
| 1821 |
-
|
| 1822 |
-
|
| 1823 |
-
.
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
|
| 1827 |
-
|
| 1828 |
-
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
|
| 1834 |
-
|
| 1835 |
-
|
| 1836 |
-
|
| 1837 |
-
|
| 1838 |
-
|
| 1839 |
-
|
| 1840 |
-
|
| 1841 |
-
|
| 1842 |
-
|
| 1843 |
-
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 1848 |
-
}
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
|
| 1852 |
-
|
| 1853 |
-
|
| 1854 |
-
|
| 1855 |
-
|
| 1856 |
-
|
| 1857 |
-
|
| 1858 |
-
|
| 1859 |
-
|
| 1860 |
-
|
| 1861 |
-
|
| 1862 |
-
|
| 1863 |
-
|
| 1864 |
-
|
| 1865 |
-
|
| 1866 |
-
|
| 1867 |
-
|
| 1868 |
-
|
| 1869 |
-
|
| 1870 |
-
|
| 1871 |
-
|
| 1872 |
-
|
| 1873 |
-
|
| 1874 |
-
|
| 1875 |
-
|
| 1876 |
-
|
| 1877 |
-
|
| 1878 |
-
|
| 1879 |
-
|
| 1880 |
-
|
| 1881 |
-
|
| 1882 |
-
font-
|
| 1883 |
-
|
| 1884 |
-
|
| 1885 |
-
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
| 1893 |
-
.cal-
|
| 1894 |
-
|
| 1895 |
-
|
| 1896 |
-
|
| 1897 |
-
|
| 1898 |
-
|
| 1899 |
-
|
| 1900 |
-
|
| 1901 |
-
|
| 1902 |
-
|
| 1903 |
-
|
| 1904 |
-
|
| 1905 |
-
|
| 1906 |
-
|
| 1907 |
-
|
| 1908 |
-
|
| 1909 |
-
|
| 1910 |
-
|
| 1911 |
-
|
| 1912 |
-
|
| 1913 |
-
|
| 1914 |
-
|
| 1915 |
-
|
| 1916 |
-
|
| 1917 |
-
|
| 1918 |
-
|
| 1919 |
-
|
| 1920 |
-
|
| 1921 |
-
|
| 1922 |
-
|
| 1923 |
-
|
| 1924 |
-
|
| 1925 |
-
|
| 1926 |
-
|
| 1927 |
-
|
| 1928 |
-
|
| 1929 |
-
|
| 1930 |
-
|
| 1931 |
-
|
| 1932 |
-
|
| 1933 |
-
|
| 1934 |
-
|
| 1935 |
-
font-
|
| 1936 |
-
|
| 1937 |
-
|
| 1938 |
-
|
| 1939 |
-
.
|
| 1940 |
-
|
| 1941 |
-
|
| 1942 |
-
|
| 1943 |
-
|
| 1944 |
-
|
| 1945 |
-
|
| 1946 |
-
|
| 1947 |
-
|
| 1948 |
-
|
| 1949 |
-
|
| 1950 |
-
|
| 1951 |
-
|
| 1952 |
-
|
| 1953 |
-
|
| 1954 |
-
|
| 1955 |
-
|
| 1956 |
-
|
| 1957 |
-
|
| 1958 |
-
|
| 1959 |
-
|
| 1960 |
-
|
| 1961 |
-
|
| 1962 |
-
|
| 1963 |
-
|
| 1964 |
-
|
| 1965 |
-
}
|
| 1966 |
-
|
| 1967 |
-
|
| 1968 |
-
|
| 1969 |
-
|
| 1970 |
-
|
| 1971 |
-
|
| 1972 |
-
|
| 1973 |
-
|
| 1974 |
-
|
| 1975 |
-
|
| 1976 |
-
|
| 1977 |
-
|
| 1978 |
-
|
| 1979 |
-
|
| 1980 |
-
|
| 1981 |
-
|
| 1982 |
-
|
| 1983 |
-
|
| 1984 |
-
|
| 1985 |
-
|
| 1986 |
-
|
| 1987 |
-
|
| 1988 |
-
|
| 1989 |
-
|
| 1990 |
-
|
| 1991 |
-
|
| 1992 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =========================================
|
| 2 |
+
1. REACT layout setting
|
| 3 |
+
========================================= */
|
| 4 |
+
html, body, #root {
|
| 5 |
+
width: 100%;
|
| 6 |
+
height: 100%;
|
| 7 |
+
margin: 0;
|
| 8 |
+
padding: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.app-container {
|
| 12 |
+
width: 100%;
|
| 13 |
+
min-height: 100vh; /* screen height */
|
| 14 |
+
display: flex;
|
| 15 |
+
flex-direction: column;
|
| 16 |
+
background-color: #f9f9f9;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* =========================================
|
| 20 |
+
2. original layout
|
| 21 |
+
========================================= */
|
| 22 |
+
|
| 23 |
+
/* GLOBAL STYLES */
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Nunito', sans-serif;
|
| 26 |
+
background-color: #f9f9f9;
|
| 27 |
+
overflow-x: hidden;
|
| 28 |
+
overflow-y: auto;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* dynamic class name */
|
| 32 |
+
.hidden {
|
| 33 |
+
display: none !important;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* TOP MENU */
|
| 37 |
+
#top-menu {
|
| 38 |
+
height: 60px;
|
| 39 |
+
background-color: white;
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
justify-content: center;
|
| 43 |
+
gap: 0;
|
| 44 |
+
padding: 0 16px 0 20px;
|
| 45 |
+
box-sizing: border-box;
|
| 46 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 47 |
+
position: fixed;
|
| 48 |
+
top: 0;
|
| 49 |
+
left: 0;
|
| 50 |
+
right: 0;
|
| 51 |
+
width: 100%;
|
| 52 |
+
z-index: 1000;
|
| 53 |
+
overflow-x: auto;
|
| 54 |
+
overflow-y: hidden;
|
| 55 |
+
white-space: nowrap;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.top-menu-links {
|
| 59 |
+
flex: 1;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
flex-wrap: wrap;
|
| 64 |
+
gap: 0;
|
| 65 |
+
min-width: 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.menu-btn {
|
| 69 |
+
background: none;
|
| 70 |
+
border: none;
|
| 71 |
+
font-family: 'Nunito', sans-serif;
|
| 72 |
+
font-size: 16px;
|
| 73 |
+
color: #333;
|
| 74 |
+
padding: 10px 20px;
|
| 75 |
+
cursor: pointer;
|
| 76 |
+
transition: background-color 0.2s;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.menu-btn:hover {
|
| 80 |
+
background-color: #f0f0f0;
|
| 81 |
+
border-radius: 4px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* active for React */
|
| 85 |
+
.menu-btn.active {
|
| 86 |
+
font-weight: bold;
|
| 87 |
+
color: #007BFF;
|
| 88 |
+
background-color: #eef7ff;
|
| 89 |
+
border-radius: 4px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.separator {
|
| 93 |
+
width: 1px;
|
| 94 |
+
height: 20px;
|
| 95 |
+
background-color: #555; /* Dark gray separator */
|
| 96 |
+
margin: 0 5px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* PAGE CONTAINER */
|
| 100 |
+
.page {
|
| 101 |
+
/* content under menu */
|
| 102 |
+
min-height: calc(100vh - 60px);
|
| 103 |
+
width: 100%;
|
| 104 |
+
padding-top: 60px; /* Space for fixed menu */
|
| 105 |
+
padding-bottom: 40px; /* Space at bottom for scrolling */
|
| 106 |
+
box-sizing: border-box;
|
| 107 |
+
display: flex;
|
| 108 |
+
flex-direction: column;
|
| 109 |
+
align-items: center;
|
| 110 |
+
overflow-y: auto;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Ensure page titles are black */
|
| 114 |
+
.page h1 {
|
| 115 |
+
color: #000 !important;
|
| 116 |
+
background: transparent !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.page-title {
|
| 120 |
+
color: #000 !important;
|
| 121 |
+
background: transparent !important;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* PAGE A SPECIFIC */
|
| 125 |
+
#page-a {
|
| 126 |
+
justify-content: center; /* Center vertically */
|
| 127 |
+
/* Fine-tune this margin if the Home screen sits slightly too low. */
|
| 128 |
+
margin-top: -40px;
|
| 129 |
+
flex: 1; /* Fill the remaining height so vertical centering still works. */
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
#page-a h1 {
|
| 133 |
+
font-size: 80px;
|
| 134 |
+
margin: 0 0 10px 0;
|
| 135 |
+
color: #000;
|
| 136 |
+
text-align: center; /* Keep the heading centered. */
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
#page-a p {
|
| 140 |
+
color: #666;
|
| 141 |
+
font-size: 20px;
|
| 142 |
+
margin-bottom: 40px;
|
| 143 |
+
text-align: center;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn-main {
|
| 147 |
+
background-color: #007BFF; /* Blue */
|
| 148 |
+
color: white;
|
| 149 |
+
border: none;
|
| 150 |
+
padding: 15px 50px;
|
| 151 |
+
font-size: 20px;
|
| 152 |
+
font-family: 'Nunito', sans-serif;
|
| 153 |
+
border-radius: 30px; /* Fully rounded corners */
|
| 154 |
+
cursor: pointer;
|
| 155 |
+
transition: transform 0.2s ease;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.btn-main:hover {
|
| 159 |
+
transform: scale(1.1); /* Zoom effect */
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* PAGE B SPECIFIC */
|
| 163 |
+
#page-b {
|
| 164 |
+
justify-content: space-evenly; /* Distribute vertical space */
|
| 165 |
+
padding-bottom: 20px;
|
| 166 |
+
min-height: calc(100vh - 60px); /* Ensure the page still fills the viewport. */
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* 1. Display Area */
|
| 170 |
+
#display-area {
|
| 171 |
+
width: 60%;
|
| 172 |
+
height: 50vh; /* Use viewport height to scale more consistently across screens. */
|
| 173 |
+
min-height: 300px;
|
| 174 |
+
border: 2px solid #ddd;
|
| 175 |
+
border-radius: 12px;
|
| 176 |
+
background-color: #fff;
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
color: #555;
|
| 181 |
+
font-size: 24px;
|
| 182 |
+
position: relative;
|
| 183 |
+
/* Keep video content centered without overflowing the frame. */
|
| 184 |
+
overflow: hidden;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.focus-display-shell {
|
| 188 |
+
background: #101010;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
@keyframes fadeInOverlay {
|
| 192 |
+
from { opacity: 0; }
|
| 193 |
+
to { opacity: 1; }
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
@keyframes slideUpCard {
|
| 197 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 198 |
+
to { opacity: 1; transform: translateY(0); }
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
.focus-flow-overlay {
|
| 203 |
+
position: fixed;
|
| 204 |
+
top: 76px;
|
| 205 |
+
right: 20px;
|
| 206 |
+
bottom: 20px;
|
| 207 |
+
left: 20px;
|
| 208 |
+
display: flex;
|
| 209 |
+
align-items: center;
|
| 210 |
+
justify-content: center;
|
| 211 |
+
padding: 0;
|
| 212 |
+
background: rgba(17, 31, 52, 0.18);
|
| 213 |
+
backdrop-filter: blur(10px);
|
| 214 |
+
z-index: 900;
|
| 215 |
+
animation: fadeInOverlay 0.3s ease-out forwards;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.focus-flow-card {
|
| 219 |
+
width: min(1040px, 100%);
|
| 220 |
+
background: #fff;
|
| 221 |
+
border-radius: 24px;
|
| 222 |
+
padding: 30px 34px;
|
| 223 |
+
box-shadow: 0 28px 80px rgba(14, 44, 88, 0.18);
|
| 224 |
+
border: 1px solid rgba(0, 123, 255, 0.12);
|
| 225 |
+
box-sizing: border-box;
|
| 226 |
+
animation: slideUpCard 0.4s ease-out forwards;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.focus-flow-header {
|
| 230 |
+
display: flex;
|
| 231 |
+
align-items: center;
|
| 232 |
+
justify-content: space-between;
|
| 233 |
+
gap: 24px;
|
| 234 |
+
margin-bottom: 18px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.focus-flow-eyebrow {
|
| 238 |
+
display: inline-block;
|
| 239 |
+
padding: 6px 12px;
|
| 240 |
+
border-radius: 999px;
|
| 241 |
+
background: #e7f3ff;
|
| 242 |
+
color: #007BFF;
|
| 243 |
+
font-size: 0.82rem;
|
| 244 |
+
font-weight: 800;
|
| 245 |
+
letter-spacing: 0.04em;
|
| 246 |
+
text-transform: uppercase;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.focus-flow-header h2 {
|
| 250 |
+
margin: 14px 0 0;
|
| 251 |
+
color: #333;
|
| 252 |
+
font-size: clamp(1.8rem, 2.5vw, 2.5rem);
|
| 253 |
+
line-height: 1.1;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.focus-flow-icon {
|
| 257 |
+
flex: 0 0 auto;
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
justify-content: center;
|
| 261 |
+
width: 116px;
|
| 262 |
+
height: 116px;
|
| 263 |
+
border-radius: 24px;
|
| 264 |
+
background: linear-gradient(180deg, #f4f9ff 0%, #edf5ff 100%);
|
| 265 |
+
border: 1px solid rgba(0, 123, 255, 0.12);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.focus-flow-lead {
|
| 269 |
+
margin: 0 0 20px;
|
| 270 |
+
color: #4a4a4a;
|
| 271 |
+
font-size: 1rem;
|
| 272 |
+
line-height: 1.6;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.focus-flow-grid {
|
| 276 |
+
display: grid;
|
| 277 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 278 |
+
gap: 16px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.focus-flow-panel {
|
| 282 |
+
background: #f8fbff;
|
| 283 |
+
border: 1px solid #d9eaff;
|
| 284 |
+
border-radius: 14px;
|
| 285 |
+
padding: 18px;
|
| 286 |
+
}
|
| 287 |
+
.focus-flow-grid .focus-flow-panel:nth-child(4) {
|
| 288 |
+
grid-column: span 3 !important;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.focus-flow-panel h3,
|
| 292 |
+
.focus-flow-step-copy h3 {
|
| 293 |
+
margin: 0 0 8px;
|
| 294 |
+
color: #333;
|
| 295 |
+
font-size: 1rem;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.focus-flow-panel p,
|
| 299 |
+
.focus-flow-step-copy p {
|
| 300 |
+
margin: 0;
|
| 301 |
+
color: #5e6670;
|
| 302 |
+
font-size: 0.95rem;
|
| 303 |
+
line-height: 1.6;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.focus-flow-steps {
|
| 307 |
+
display: grid;
|
| 308 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 309 |
+
gap: 14px;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.focus-flow-step {
|
| 313 |
+
display: flex;
|
| 314 |
+
align-items: flex-start;
|
| 315 |
+
gap: 14px;
|
| 316 |
+
background: #f8fbff;
|
| 317 |
+
border: 1px solid #d9eaff;
|
| 318 |
+
border-radius: 14px;
|
| 319 |
+
padding: 16px 18px;
|
| 320 |
+
min-height: 100px;
|
| 321 |
+
box-sizing: border-box;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.focus-flow-step-number {
|
| 325 |
+
flex: 0 0 auto;
|
| 326 |
+
display: inline-flex;
|
| 327 |
+
align-items: center;
|
| 328 |
+
justify-content: center;
|
| 329 |
+
width: 34px;
|
| 330 |
+
height: 34px;
|
| 331 |
+
border-radius: 50%;
|
| 332 |
+
background: #007BFF;
|
| 333 |
+
color: #fff;
|
| 334 |
+
font-size: 0.95rem;
|
| 335 |
+
font-weight: 800;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.focus-flow-step-copy {
|
| 339 |
+
min-width: 0;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.focus-flow-footer {
|
| 343 |
+
display: flex;
|
| 344 |
+
align-items: center;
|
| 345 |
+
justify-content: space-between;
|
| 346 |
+
gap: 16px;
|
| 347 |
+
margin-top: 20px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.focus-flow-note {
|
| 351 |
+
color: #667281;
|
| 352 |
+
font-size: 0.94rem;
|
| 353 |
+
line-height: 1.6;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.focus-flow-glasses-note {
|
| 357 |
+
background: #fffbea;
|
| 358 |
+
border: 1px solid #f5c518;
|
| 359 |
+
border-radius: 10px;
|
| 360 |
+
padding: 12px 16px;
|
| 361 |
+
font-size: 0.9rem;
|
| 362 |
+
color: #5a4a00;
|
| 363 |
+
line-height: 1.55;
|
| 364 |
+
margin-top: 4px;
|
| 365 |
+
margin-bottom: 4px;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.focus-flow-panel-warn {
|
| 369 |
+
border-left: 3px solid #f5a623;
|
| 370 |
+
background: #fff9f0;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.eye-gaze-modal-checkbox {
|
| 374 |
+
display: flex;
|
| 375 |
+
align-items: center;
|
| 376 |
+
gap: 8px;
|
| 377 |
+
margin-top: 16px;
|
| 378 |
+
font-size: 0.9rem;
|
| 379 |
+
color: #667281;
|
| 380 |
+
cursor: pointer;
|
| 381 |
+
user-select: none;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.focus-flow-button,
|
| 385 |
+
.focus-flow-secondary {
|
| 386 |
+
border: none;
|
| 387 |
+
border-radius: 999px;
|
| 388 |
+
padding: 13px 24px;
|
| 389 |
+
font-family: 'Nunito', sans-serif;
|
| 390 |
+
font-size: 0.98rem;
|
| 391 |
+
font-weight: 800;
|
| 392 |
+
cursor: pointer;
|
| 393 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.focus-flow-button {
|
| 397 |
+
background: #007BFF;
|
| 398 |
+
color: #fff;
|
| 399 |
+
box-shadow: 0 12px 24px rgba(0, 123, 255, 0.18);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.focus-flow-button:hover {
|
| 403 |
+
background: #0069d9;
|
| 404 |
+
border-color: transparent;
|
| 405 |
+
transform: translateY(-1px);
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.focus-flow-secondary {
|
| 409 |
+
background: #eef3f8;
|
| 410 |
+
color: #4b5a6b;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.focus-flow-secondary:hover {
|
| 414 |
+
background: #e2eaf3;
|
| 415 |
+
border-color: transparent;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.focus-state-pill {
|
| 419 |
+
position: absolute;
|
| 420 |
+
top: 18px;
|
| 421 |
+
left: 18px;
|
| 422 |
+
display: inline-flex;
|
| 423 |
+
align-items: center;
|
| 424 |
+
gap: 10px;
|
| 425 |
+
padding: 10px 16px;
|
| 426 |
+
border-radius: 999px;
|
| 427 |
+
color: #fff;
|
| 428 |
+
font-size: 0.88rem;
|
| 429 |
+
font-weight: 800;
|
| 430 |
+
letter-spacing: 0.04em;
|
| 431 |
+
text-transform: uppercase;
|
| 432 |
+
z-index: 2;
|
| 433 |
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.focus-state-pill.pending {
|
| 437 |
+
background: rgba(87, 96, 111, 0.92);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.focus-state-pill.focused {
|
| 441 |
+
background: rgba(33, 163, 102, 0.94);
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.focus-state-pill.not-focused {
|
| 445 |
+
background: rgba(215, 68, 68, 0.94);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.focus-state-dot {
|
| 449 |
+
width: 10px;
|
| 450 |
+
height: 10px;
|
| 451 |
+
border-radius: 50%;
|
| 452 |
+
background: currentColor;
|
| 453 |
+
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.16);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.focus-idle-overlay {
|
| 457 |
+
position: absolute;
|
| 458 |
+
inset: 0;
|
| 459 |
+
display: flex;
|
| 460 |
+
flex-direction: column;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
gap: 10px;
|
| 464 |
+
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.72));
|
| 465 |
+
color: #fff;
|
| 466 |
+
text-align: center;
|
| 467 |
+
z-index: 1;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.focus-idle-overlay p {
|
| 471 |
+
margin: 0;
|
| 472 |
+
font-size: 1.6rem;
|
| 473 |
+
font-weight: 800;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.focus-idle-overlay span {
|
| 477 |
+
max-width: 420px;
|
| 478 |
+
color: rgba(255, 255, 255, 0.82);
|
| 479 |
+
font-size: 0.98rem;
|
| 480 |
+
line-height: 1.5;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.focus-inline-error {
|
| 484 |
+
margin-top: 18px;
|
| 485 |
+
padding: 12px 16px;
|
| 486 |
+
max-width: 620px;
|
| 487 |
+
border-radius: 12px;
|
| 488 |
+
background: #fff1ee;
|
| 489 |
+
color: #b54028;
|
| 490 |
+
font-size: 0.95rem;
|
| 491 |
+
font-weight: 700;
|
| 492 |
+
box-shadow: 0 10px 20px rgba(181, 64, 40, 0.08);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.focus-inline-error-standalone {
|
| 496 |
+
width: 60%;
|
| 497 |
+
box-sizing: border-box;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.focus-debug-panel {
|
| 501 |
+
position: absolute;
|
| 502 |
+
top: 10px;
|
| 503 |
+
right: 10px;
|
| 504 |
+
background: rgba(0,0,0,0.7);
|
| 505 |
+
color: white;
|
| 506 |
+
padding: 10px;
|
| 507 |
+
border-radius: 5px;
|
| 508 |
+
font-size: 12px;
|
| 509 |
+
font-family: monospace;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.focus-model-strip {
|
| 513 |
+
display: flex;
|
| 514 |
+
align-items: center;
|
| 515 |
+
justify-content: center;
|
| 516 |
+
flex-wrap: wrap;
|
| 517 |
+
gap: 8px;
|
| 518 |
+
padding: 10px 16px;
|
| 519 |
+
background: #fff;
|
| 520 |
+
border: 1px solid #e0e0e0;
|
| 521 |
+
border-radius: 12px;
|
| 522 |
+
margin: 10px auto;
|
| 523 |
+
max-width: 700px;
|
| 524 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* --- Model info card --- */
|
| 528 |
+
.model-card {
|
| 529 |
+
width: 60%;
|
| 530 |
+
margin: 14px auto 0;
|
| 531 |
+
background: #fff;
|
| 532 |
+
border: 1px solid #e0e0e0;
|
| 533 |
+
border-radius: 14px;
|
| 534 |
+
padding: 18px 22px 14px;
|
| 535 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
| 536 |
+
animation: cardFadeIn 0.25s ease;
|
| 537 |
+
box-sizing: border-box;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.model-card-details {
|
| 541 |
+
display: grid;
|
| 542 |
+
grid-template-columns: repeat(3, 1fr);
|
| 543 |
+
gap: 12px;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
@keyframes cardFadeIn {
|
| 547 |
+
from { opacity: 0; transform: translateY(4px); }
|
| 548 |
+
to { opacity: 1; transform: translateY(0); }
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.model-card-header {
|
| 552 |
+
display: flex;
|
| 553 |
+
align-items: center;
|
| 554 |
+
gap: 10px;
|
| 555 |
+
margin-bottom: 4px;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.model-card-title {
|
| 559 |
+
margin: 0;
|
| 560 |
+
font-size: 1.05rem;
|
| 561 |
+
color: #1a1a2e;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.model-card-badge {
|
| 565 |
+
padding: 3px 10px;
|
| 566 |
+
border-radius: 999px;
|
| 567 |
+
background: #e7f3ff;
|
| 568 |
+
color: #007BFF;
|
| 569 |
+
font-size: 0.7rem;
|
| 570 |
+
font-weight: 800;
|
| 571 |
+
letter-spacing: 0.04em;
|
| 572 |
+
text-transform: uppercase;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.model-card-badge-baseline {
|
| 576 |
+
padding: 3px 10px;
|
| 577 |
+
border-radius: 999px;
|
| 578 |
+
background: #fff3e0;
|
| 579 |
+
color: #e67e22;
|
| 580 |
+
font-size: 0.7rem;
|
| 581 |
+
font-weight: 800;
|
| 582 |
+
letter-spacing: 0.04em;
|
| 583 |
+
text-transform: uppercase;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.model-card-tagline {
|
| 587 |
+
margin: 0 0 12px;
|
| 588 |
+
color: #667281;
|
| 589 |
+
font-size: 0.85rem;
|
| 590 |
+
line-height: 1.4;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.model-card-metrics {
|
| 594 |
+
display: grid;
|
| 595 |
+
grid-template-columns: repeat(4, 1fr);
|
| 596 |
+
gap: 8px;
|
| 597 |
+
margin-bottom: 14px;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.model-card-metric {
|
| 601 |
+
text-align: center;
|
| 602 |
+
padding: 8px 4px;
|
| 603 |
+
background: #f8fbff;
|
| 604 |
+
border: 1px solid #e8f0fe;
|
| 605 |
+
border-radius: 10px;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.model-card-metric-value {
|
| 609 |
+
display: block;
|
| 610 |
+
font-size: 1.1rem;
|
| 611 |
+
font-weight: 800;
|
| 612 |
+
color: #007BFF;
|
| 613 |
+
line-height: 1.2;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.model-card-metric-label {
|
| 617 |
+
display: block;
|
| 618 |
+
font-size: 0.65rem;
|
| 619 |
+
color: #8899aa;
|
| 620 |
+
font-weight: 700;
|
| 621 |
+
text-transform: uppercase;
|
| 622 |
+
letter-spacing: 0.04em;
|
| 623 |
+
margin-top: 2px;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.model-card-section {
|
| 627 |
+
margin-bottom: 8px;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.model-card-section h4 {
|
| 631 |
+
margin: 0 0 2px;
|
| 632 |
+
font-size: 0.78rem;
|
| 633 |
+
color: #555;
|
| 634 |
+
font-weight: 800;
|
| 635 |
+
text-transform: uppercase;
|
| 636 |
+
letter-spacing: 0.03em;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.model-card-section p {
|
| 640 |
+
margin: 0;
|
| 641 |
+
font-size: 0.82rem;
|
| 642 |
+
color: #4a4a4a;
|
| 643 |
+
line-height: 1.5;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.model-card-eval {
|
| 647 |
+
margin-top: 10px;
|
| 648 |
+
padding: 6px 10px;
|
| 649 |
+
background: #f5f7fa;
|
| 650 |
+
border-radius: 8px;
|
| 651 |
+
font-size: 0.72rem;
|
| 652 |
+
color: #7a8a9a;
|
| 653 |
+
font-weight: 600;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
@media (max-width: 768px) {
|
| 657 |
+
.model-card {
|
| 658 |
+
width: 90%;
|
| 659 |
+
}
|
| 660 |
+
.model-card-metrics {
|
| 661 |
+
grid-template-columns: repeat(2, 1fr);
|
| 662 |
+
}
|
| 663 |
+
.model-card-details {
|
| 664 |
+
grid-template-columns: 1fr;
|
| 665 |
+
}
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.focus-model-label {
|
| 669 |
+
color: #666;
|
| 670 |
+
font-size: 13px;
|
| 671 |
+
font-weight: 700;
|
| 672 |
+
margin-right: 4px;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.focus-model-button {
|
| 676 |
+
padding: 6px 16px;
|
| 677 |
+
border-radius: 16px;
|
| 678 |
+
border: 1px solid #d0d0d0;
|
| 679 |
+
background: #f5f5f5;
|
| 680 |
+
color: #555;
|
| 681 |
+
font-size: 12px;
|
| 682 |
+
font-weight: 600;
|
| 683 |
+
text-transform: uppercase;
|
| 684 |
+
cursor: pointer;
|
| 685 |
+
transition: all 0.2s;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.focus-model-button:hover {
|
| 689 |
+
border-color: #007BFF;
|
| 690 |
+
color: #007BFF;
|
| 691 |
+
background: #f0f7ff;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.focus-model-button.active {
|
| 695 |
+
border: 2px solid #007BFF;
|
| 696 |
+
background: #007BFF;
|
| 697 |
+
color: #fff;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.focus-model-sep {
|
| 701 |
+
width: 1px;
|
| 702 |
+
height: 24px;
|
| 703 |
+
background: #d0d0d0;
|
| 704 |
+
margin: 0 4px;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.eye-gaze-toggle {
|
| 708 |
+
display: inline-flex;
|
| 709 |
+
align-items: center;
|
| 710 |
+
gap: 6px;
|
| 711 |
+
padding: 6px 14px;
|
| 712 |
+
border-radius: 16px;
|
| 713 |
+
font-size: 12px;
|
| 714 |
+
font-weight: 700;
|
| 715 |
+
cursor: pointer;
|
| 716 |
+
transition: all 0.25s ease;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.eye-gaze-toggle.off {
|
| 720 |
+
border: 1px solid #d0d0d0;
|
| 721 |
+
background: #f5f5f5;
|
| 722 |
+
color: #888;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.eye-gaze-toggle.off:hover {
|
| 726 |
+
border-color: #007BFF;
|
| 727 |
+
color: #007BFF;
|
| 728 |
+
background: #f0f7ff;
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
.eye-gaze-toggle.on {
|
| 732 |
+
border: 2px solid #007BFF;
|
| 733 |
+
background: #007BFF;
|
| 734 |
+
color: #fff;
|
| 735 |
+
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.25);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
.eye-gaze-toggle.on:hover {
|
| 739 |
+
background: #0069d9;
|
| 740 |
+
border-color: #0069d9;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.eye-gaze-icon {
|
| 744 |
+
flex-shrink: 0;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.focus-model-button.recalibrate {
|
| 748 |
+
border: 1px solid #007BFF;
|
| 749 |
+
background: transparent;
|
| 750 |
+
color: #007BFF;
|
| 751 |
+
font-weight: 600;
|
| 752 |
+
font-size: 11px;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.focus-model-button.recalibrate:hover {
|
| 756 |
+
background: #f0f7ff;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.focus-system-stats {
|
| 760 |
+
display: flex;
|
| 761 |
+
align-items: center;
|
| 762 |
+
justify-content: center;
|
| 763 |
+
gap: 12px;
|
| 764 |
+
padding: 4px 16px;
|
| 765 |
+
margin: 4px auto;
|
| 766 |
+
max-width: 400px;
|
| 767 |
+
font-size: 12px;
|
| 768 |
+
color: #888;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.focus-system-stats strong {
|
| 772 |
+
color: #555;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.focus-system-stats-sep {
|
| 776 |
+
width: 1px;
|
| 777 |
+
height: 12px;
|
| 778 |
+
background: #ccc;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
#display-area video {
|
| 782 |
+
width: 100%;
|
| 783 |
+
height: 100%;
|
| 784 |
+
object-fit: cover; /* Behaves similarly to background-size: cover. */
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
/* 2. Timeline Area */
|
| 788 |
+
#timeline-area {
|
| 789 |
+
width: 60%;
|
| 790 |
+
height: 80px;
|
| 791 |
+
position: relative;
|
| 792 |
+
display: flex;
|
| 793 |
+
flex-direction: column;
|
| 794 |
+
justify-content: flex-end;
|
| 795 |
+
align-self: center;
|
| 796 |
+
margin: 0 auto;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
#timeline-visuals {
|
| 800 |
+
display: flex;
|
| 801 |
+
justify-content: center;
|
| 802 |
+
flex-wrap: wrap;
|
| 803 |
+
align-items: flex-end;
|
| 804 |
+
gap: 2px;
|
| 805 |
+
width: 100%;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.timeline-label {
|
| 809 |
+
position: absolute;
|
| 810 |
+
top: 0;
|
| 811 |
+
left: 0;
|
| 812 |
+
color: #888;
|
| 813 |
+
font-size: 14px;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
#timeline-line {
|
| 817 |
+
width: 100%;
|
| 818 |
+
height: 2px;
|
| 819 |
+
background-color: #87CEEB; /* Light blue */
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
/* 3. Control Panel */
|
| 823 |
+
#control-panel {
|
| 824 |
+
display: flex;
|
| 825 |
+
gap: 20px;
|
| 826 |
+
width: 60%;
|
| 827 |
+
justify-content: space-between;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.action-btn {
|
| 831 |
+
flex: 1; /* Evenly distributed width */
|
| 832 |
+
padding: 12px 0;
|
| 833 |
+
border: none;
|
| 834 |
+
border-radius: 12px;
|
| 835 |
+
font-size: 16px;
|
| 836 |
+
font-family: 'Nunito', sans-serif;
|
| 837 |
+
font-weight: 700;
|
| 838 |
+
cursor: pointer;
|
| 839 |
+
color: white;
|
| 840 |
+
transition: opacity 0.2s;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.action-btn:hover {
|
| 844 |
+
opacity: 0.9;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.action-btn.green { background-color: #28a745; }
|
| 848 |
+
.action-btn.blue { background-color: #007BFF; }
|
| 849 |
+
.action-btn.orange { background-color: #e67e22; }
|
| 850 |
+
.action-btn.red { background-color: #dc3545; }
|
| 851 |
+
|
| 852 |
+
/* 4. Frame Control */
|
| 853 |
+
#frame-control {
|
| 854 |
+
display: flex;
|
| 855 |
+
align-items: center;
|
| 856 |
+
gap: 15px;
|
| 857 |
+
color: #333;
|
| 858 |
+
font-weight: bold;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
#frame-slider {
|
| 862 |
+
width: 200px;
|
| 863 |
+
cursor: pointer;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
#frame-input {
|
| 867 |
+
width: 50px;
|
| 868 |
+
padding: 5px;
|
| 869 |
+
border: 1px solid #ccc;
|
| 870 |
+
border-radius: 5px;
|
| 871 |
+
text-align: center;
|
| 872 |
+
font-family: 'Nunito', sans-serif;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
/* ================ ACHIEVEMENT PAGE ================ */
|
| 876 |
+
|
| 877 |
+
.stats-grid {
|
| 878 |
+
display: grid;
|
| 879 |
+
grid-template-columns: repeat(4, 1fr);
|
| 880 |
+
gap: 20px;
|
| 881 |
+
width: 80%;
|
| 882 |
+
margin: 40px auto;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.stat-card {
|
| 886 |
+
background: white;
|
| 887 |
+
padding: 30px;
|
| 888 |
+
border-radius: 12px;
|
| 889 |
+
text-align: center;
|
| 890 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.stat-number {
|
| 894 |
+
font-size: 48px;
|
| 895 |
+
font-weight: bold;
|
| 896 |
+
color: #007BFF;
|
| 897 |
+
margin-bottom: 10px;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
.stat-label {
|
| 901 |
+
font-size: 16px;
|
| 902 |
+
color: #666;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
.achievements-section {
|
| 906 |
+
width: 80%;
|
| 907 |
+
margin: 0 auto;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.achievements-section h2 {
|
| 911 |
+
color: #333;
|
| 912 |
+
margin-bottom: 20px;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.badges-grid {
|
| 916 |
+
display: grid;
|
| 917 |
+
grid-template-columns: repeat(3, 1fr);
|
| 918 |
+
gap: 20px;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
.badge {
|
| 922 |
+
background: white;
|
| 923 |
+
padding: 30px 20px;
|
| 924 |
+
border-radius: 12px;
|
| 925 |
+
text-align: center;
|
| 926 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 927 |
+
transition: transform 0.2s;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.badge:hover {
|
| 931 |
+
transform: translateY(-5px);
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.badge.locked {
|
| 935 |
+
opacity: 0.4;
|
| 936 |
+
filter: grayscale(100%);
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
.badge-icon {
|
| 940 |
+
font-size: 64px;
|
| 941 |
+
margin-bottom: 15px;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.badge-name {
|
| 945 |
+
font-size: 16px;
|
| 946 |
+
font-weight: bold;
|
| 947 |
+
color: #333;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
/* ================ RECORDS PAGE ================ */
|
| 951 |
+
|
| 952 |
+
.records-controls {
|
| 953 |
+
display: flex;
|
| 954 |
+
gap: 10px;
|
| 955 |
+
margin: 20px auto;
|
| 956 |
+
width: 80%;
|
| 957 |
+
justify-content: center;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.filter-btn {
|
| 961 |
+
padding: 10px 20px;
|
| 962 |
+
border: 2px solid #007BFF;
|
| 963 |
+
background: white;
|
| 964 |
+
color: #007BFF;
|
| 965 |
+
border-radius: 8px;
|
| 966 |
+
cursor: pointer;
|
| 967 |
+
font-family: 'Nunito', sans-serif;
|
| 968 |
+
font-weight: 600;
|
| 969 |
+
transition: all 0.2s;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.filter-btn:hover {
|
| 973 |
+
background: #e7f3ff;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
.filter-btn.active {
|
| 977 |
+
background: #007BFF;
|
| 978 |
+
color: white;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
.chart-container {
|
| 982 |
+
width: 80%;
|
| 983 |
+
background: white;
|
| 984 |
+
padding: 30px;
|
| 985 |
+
border-radius: 12px;
|
| 986 |
+
margin: 20px auto;
|
| 987 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
#focus-chart {
|
| 991 |
+
display: block;
|
| 992 |
+
margin: 0 auto;
|
| 993 |
+
/* Make sure the chart scales within its container. */
|
| 994 |
+
max-width: 100%;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
.sessions-list {
|
| 998 |
+
width: 80%;
|
| 999 |
+
margin: 20px auto;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.sessions-list h2 {
|
| 1003 |
+
color: #333;
|
| 1004 |
+
margin-bottom: 15px;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
#sessions-table {
|
| 1008 |
+
width: 100%;
|
| 1009 |
+
background: white;
|
| 1010 |
+
border-collapse: collapse;
|
| 1011 |
+
border-radius: 12px;
|
| 1012 |
+
overflow: hidden;
|
| 1013 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
#sessions-table th {
|
| 1017 |
+
background: #007BFF;
|
| 1018 |
+
color: white;
|
| 1019 |
+
padding: 15px;
|
| 1020 |
+
text-align: left;
|
| 1021 |
+
font-weight: 600;
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
#sessions-table td {
|
| 1025 |
+
padding: 12px 15px;
|
| 1026 |
+
border-bottom: 1px solid #eee;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
#sessions-table tr:last-child td {
|
| 1030 |
+
border-bottom: none;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
#sessions-table tbody tr:hover {
|
| 1034 |
+
background: #f8f9fa;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.btn-view {
|
| 1038 |
+
padding: 6px 18px;
|
| 1039 |
+
background: #007BFF;
|
| 1040 |
+
color: white;
|
| 1041 |
+
border: none;
|
| 1042 |
+
border-radius: 999px;
|
| 1043 |
+
cursor: pointer;
|
| 1044 |
+
font-family: 'Nunito', sans-serif;
|
| 1045 |
+
font-size: 12px;
|
| 1046 |
+
font-weight: 700;
|
| 1047 |
+
transition: background 0.2s;
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
.btn-view:hover {
|
| 1051 |
+
background: #0056b3;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.records-detail-modal {
|
| 1055 |
+
width: min(960px, 92vw);
|
| 1056 |
+
max-width: 960px;
|
| 1057 |
+
max-height: 86vh;
|
| 1058 |
+
overflow-y: auto;
|
| 1059 |
+
padding: 30px;
|
| 1060 |
+
box-sizing: border-box;
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
.records-detail-header {
|
| 1064 |
+
display: flex;
|
| 1065 |
+
justify-content: space-between;
|
| 1066 |
+
align-items: flex-start;
|
| 1067 |
+
gap: 20px;
|
| 1068 |
+
margin-bottom: 24px;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
.records-detail-kicker {
|
| 1072 |
+
color: #007BFF;
|
| 1073 |
+
font-size: 12px;
|
| 1074 |
+
font-weight: 800;
|
| 1075 |
+
letter-spacing: 0.08em;
|
| 1076 |
+
text-transform: uppercase;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.records-detail-header h2 {
|
| 1080 |
+
margin: 10px 0 8px;
|
| 1081 |
+
color: #333;
|
| 1082 |
+
text-align: left;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
.records-detail-subtitle {
|
| 1086 |
+
margin: 0;
|
| 1087 |
+
color: #667281;
|
| 1088 |
+
line-height: 1.6;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
.records-detail-close {
|
| 1092 |
+
border: 1px solid #d6e6fa;
|
| 1093 |
+
background: #f4f9ff;
|
| 1094 |
+
color: #3569a8;
|
| 1095 |
+
border-radius: 999px;
|
| 1096 |
+
padding: 10px 18px;
|
| 1097 |
+
font-family: 'Nunito', sans-serif;
|
| 1098 |
+
font-weight: 700;
|
| 1099 |
+
cursor: pointer;
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
.records-detail-close:hover {
|
| 1103 |
+
border-color: #bfd9f7;
|
| 1104 |
+
background: #e9f4ff;
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.records-detail-feedback {
|
| 1108 |
+
padding: 18px 20px;
|
| 1109 |
+
border-radius: 14px;
|
| 1110 |
+
background: #f7f9fc;
|
| 1111 |
+
color: #516173;
|
| 1112 |
+
font-weight: 700;
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
.records-detail-feedback-error {
|
| 1116 |
+
background: #fff1ee;
|
| 1117 |
+
color: #b54028;
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
.records-detail-summary {
|
| 1121 |
+
display: grid;
|
| 1122 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 1123 |
+
gap: 14px;
|
| 1124 |
+
margin-bottom: 18px;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
.records-detail-stat {
|
| 1128 |
+
padding: 18px;
|
| 1129 |
+
border-radius: 14px;
|
| 1130 |
+
background: #f8fbff;
|
| 1131 |
+
border: 1px solid #d9eaff;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.records-detail-stat.excellent {
|
| 1135 |
+
background: #eef9f0;
|
| 1136 |
+
border-color: #cdebd3;
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
.records-detail-stat.good {
|
| 1140 |
+
background: #fff9eb;
|
| 1141 |
+
border-color: #f8e3a8;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.records-detail-stat.fair {
|
| 1145 |
+
background: #fff4eb;
|
| 1146 |
+
border-color: #ffd6af;
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
.records-detail-stat.low {
|
| 1150 |
+
background: #fff0f0;
|
| 1151 |
+
border-color: #f3c7c7;
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
.records-detail-stat-label {
|
| 1155 |
+
display: block;
|
| 1156 |
+
margin-bottom: 8px;
|
| 1157 |
+
color: #667281;
|
| 1158 |
+
font-size: 13px;
|
| 1159 |
+
font-weight: 700;
|
| 1160 |
+
text-transform: uppercase;
|
| 1161 |
+
letter-spacing: 0.04em;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
.records-detail-stat-value {
|
| 1165 |
+
display: block;
|
| 1166 |
+
color: #1f2d3d;
|
| 1167 |
+
font-size: 28px;
|
| 1168 |
+
line-height: 1.1;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
.records-detail-grid {
|
| 1172 |
+
display: grid;
|
| 1173 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1174 |
+
gap: 16px;
|
| 1175 |
+
margin-bottom: 16px;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.records-detail-card {
|
| 1179 |
+
background: white;
|
| 1180 |
+
border: 1px solid #e8eef5;
|
| 1181 |
+
border-radius: 16px;
|
| 1182 |
+
padding: 20px;
|
| 1183 |
+
box-shadow: 0 8px 24px rgba(20, 44, 74, 0.06);
|
| 1184 |
+
margin-bottom: 16px;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
.records-detail-card:last-child {
|
| 1188 |
+
margin-bottom: 0;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
.records-detail-card h3 {
|
| 1192 |
+
margin: 0 0 16px;
|
| 1193 |
+
color: #333;
|
| 1194 |
+
font-size: 18px;
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
.records-detail-list {
|
| 1198 |
+
display: grid;
|
| 1199 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1200 |
+
gap: 14px 18px;
|
| 1201 |
+
}
|
| 1202 |
+
|
| 1203 |
+
.records-detail-item {
|
| 1204 |
+
display: flex;
|
| 1205 |
+
flex-direction: column;
|
| 1206 |
+
gap: 6px;
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.records-detail-item-label {
|
| 1210 |
+
color: #7a8795;
|
| 1211 |
+
font-size: 12px;
|
| 1212 |
+
font-weight: 700;
|
| 1213 |
+
text-transform: uppercase;
|
| 1214 |
+
letter-spacing: 0.05em;
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
.records-detail-item-value {
|
| 1218 |
+
color: #263445;
|
| 1219 |
+
font-size: 15px;
|
| 1220 |
+
font-weight: 700;
|
| 1221 |
+
line-height: 1.5;
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
.records-detail-section-head {
|
| 1225 |
+
display: flex;
|
| 1226 |
+
align-items: center;
|
| 1227 |
+
justify-content: space-between;
|
| 1228 |
+
gap: 12px;
|
| 1229 |
+
margin-bottom: 16px;
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
.records-detail-section-head span {
|
| 1233 |
+
color: #7a8795;
|
| 1234 |
+
font-size: 13px;
|
| 1235 |
+
font-weight: 700;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
.records-detail-timeline {
|
| 1239 |
+
display: grid;
|
| 1240 |
+
grid-template-columns: repeat(auto-fit, minmax(10px, 1fr));
|
| 1241 |
+
gap: 5px;
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
.records-detail-segment {
|
| 1245 |
+
height: 48px;
|
| 1246 |
+
border-radius: 999px;
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
.records-detail-segment.focused {
|
| 1250 |
+
background: linear-gradient(180deg, #3ab86a 0%, #23a057 100%);
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
.records-detail-segment.mixed {
|
| 1254 |
+
background: linear-gradient(180deg, #f1b447 0%, #df9a1e 100%);
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.records-detail-segment.distracted {
|
| 1258 |
+
background: linear-gradient(180deg, #ec7d7d 0%, #d9534f 100%);
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.records-detail-legend {
|
| 1262 |
+
display: flex;
|
| 1263 |
+
flex-wrap: wrap;
|
| 1264 |
+
gap: 16px;
|
| 1265 |
+
margin-top: 14px;
|
| 1266 |
+
color: #667281;
|
| 1267 |
+
font-size: 13px;
|
| 1268 |
+
font-weight: 700;
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
.records-detail-legend span {
|
| 1272 |
+
display: inline-flex;
|
| 1273 |
+
align-items: center;
|
| 1274 |
+
gap: 8px;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
.records-detail-dot {
|
| 1278 |
+
width: 10px;
|
| 1279 |
+
height: 10px;
|
| 1280 |
+
border-radius: 50%;
|
| 1281 |
+
display: inline-block;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
.records-detail-dot.focused {
|
| 1285 |
+
background: #23a057;
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
.records-detail-dot.mixed {
|
| 1289 |
+
background: #df9a1e;
|
| 1290 |
+
}
|
| 1291 |
+
|
| 1292 |
+
.records-detail-dot.distracted {
|
| 1293 |
+
background: #d9534f;
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
.records-detail-events {
|
| 1297 |
+
display: grid;
|
| 1298 |
+
gap: 10px;
|
| 1299 |
+
max-height: 280px;
|
| 1300 |
+
overflow-y: auto;
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
.records-detail-event {
|
| 1304 |
+
display: grid;
|
| 1305 |
+
grid-template-columns: auto 1fr auto;
|
| 1306 |
+
align-items: center;
|
| 1307 |
+
gap: 12px;
|
| 1308 |
+
padding: 12px 14px;
|
| 1309 |
+
background: #f8fbff;
|
| 1310 |
+
border: 1px solid #e1edf9;
|
| 1311 |
+
border-radius: 14px;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
.records-detail-event-time {
|
| 1315 |
+
min-width: 52px;
|
| 1316 |
+
color: #3569a8;
|
| 1317 |
+
font-size: 13px;
|
| 1318 |
+
font-weight: 800;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
.records-detail-event-copy {
|
| 1322 |
+
min-width: 0;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.records-detail-event-status {
|
| 1326 |
+
color: #243345;
|
| 1327 |
+
font-size: 14px;
|
| 1328 |
+
font-weight: 800;
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
.records-detail-event-meta {
|
| 1332 |
+
margin-top: 4px;
|
| 1333 |
+
color: #6f7d8c;
|
| 1334 |
+
font-size: 12px;
|
| 1335 |
+
line-height: 1.5;
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
.records-detail-event-badge {
|
| 1339 |
+
padding: 7px 12px;
|
| 1340 |
+
border-radius: 999px;
|
| 1341 |
+
font-size: 11px;
|
| 1342 |
+
font-weight: 800;
|
| 1343 |
+
letter-spacing: 0.04em;
|
| 1344 |
+
text-transform: uppercase;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
.records-detail-event-badge.focused {
|
| 1348 |
+
background: #eaf8ef;
|
| 1349 |
+
color: #1f8a4c;
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
+
.records-detail-event-badge.distracted {
|
| 1353 |
+
background: #fff1f1;
|
| 1354 |
+
color: #c24c49;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
.records-detail-empty {
|
| 1358 |
+
padding: 16px 18px;
|
| 1359 |
+
border-radius: 14px;
|
| 1360 |
+
background: #f7f9fc;
|
| 1361 |
+
color: #708090;
|
| 1362 |
+
font-weight: 700;
|
| 1363 |
+
}
|
| 1364 |
+
|
| 1365 |
+
/* ================ SETTINGS PAGE ================ */
|
| 1366 |
+
|
| 1367 |
+
.settings-container {
|
| 1368 |
+
width: 60%;
|
| 1369 |
+
max-width: 800px;
|
| 1370 |
+
margin: 20px auto;
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
.setting-group {
|
| 1374 |
+
background: white;
|
| 1375 |
+
padding: 30px;
|
| 1376 |
+
border-radius: 12px;
|
| 1377 |
+
margin-bottom: 20px;
|
| 1378 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.setting-group h2 {
|
| 1382 |
+
margin-top: 0;
|
| 1383 |
+
color: #333;
|
| 1384 |
+
font-size: 20px;
|
| 1385 |
+
margin-bottom: 20px;
|
| 1386 |
+
border-bottom: 2px solid #007BFF;
|
| 1387 |
+
padding-bottom: 10px;
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
.setting-item {
|
| 1391 |
+
margin-bottom: 25px;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
.setting-item:last-child {
|
| 1395 |
+
margin-bottom: 0;
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
.setting-item label {
|
| 1399 |
+
display: block;
|
| 1400 |
+
margin-bottom: 8px;
|
| 1401 |
+
color: #333;
|
| 1402 |
+
font-weight: 600;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
.slider-group {
|
| 1406 |
+
display: flex;
|
| 1407 |
+
align-items: center;
|
| 1408 |
+
gap: 15px;
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
.slider-group input[type="range"] {
|
| 1412 |
+
flex: 1;
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
.slider-group span {
|
| 1416 |
+
min-width: 40px;
|
| 1417 |
+
text-align: center;
|
| 1418 |
+
font-weight: bold;
|
| 1419 |
+
color: #007BFF;
|
| 1420 |
+
font-size: 18px;
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
.setting-description {
|
| 1424 |
+
font-size: 14px;
|
| 1425 |
+
color: #666;
|
| 1426 |
+
margin-top: 5px;
|
| 1427 |
+
font-style: italic;
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
input[type="checkbox"] {
|
| 1431 |
+
margin-right: 10px;
|
| 1432 |
+
cursor: pointer;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
input[type="number"] {
|
| 1436 |
+
width: 100px;
|
| 1437 |
+
padding: 8px;
|
| 1438 |
+
border: 1px solid #ccc;
|
| 1439 |
+
border-radius: 5px;
|
| 1440 |
+
font-family: 'Nunito', sans-serif;
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
/* Center the settings buttons and give them more width. */
|
| 1444 |
+
.setting-group .action-btn {
|
| 1445 |
+
display: inline-block; /* Allow buttons to sit side by side. */
|
| 1446 |
+
width: 48%; /* Roughly half-width each, with a small gutter. */
|
| 1447 |
+
margin: 15px 1%; /* Vertical spacing plus horizontal separation. */
|
| 1448 |
+
text-align: center; /* Center the label text. */
|
| 1449 |
+
box-sizing: border-box; /* Prevent borders from forcing an early wrap. */
|
| 1450 |
+
}
|
| 1451 |
+
|
| 1452 |
+
#save-settings {
|
| 1453 |
+
display: block;
|
| 1454 |
+
margin: 20px auto;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
/* ================ HELP PAGE ================ */
|
| 1458 |
+
|
| 1459 |
+
.help-container {
|
| 1460 |
+
width: 70%;
|
| 1461 |
+
max-width: 900px;
|
| 1462 |
+
margin: 20px auto;
|
| 1463 |
+
}
|
| 1464 |
+
|
| 1465 |
+
/* Fake ad block (Help page) */
|
| 1466 |
+
.fake-ad {
|
| 1467 |
+
position: relative;
|
| 1468 |
+
display: block;
|
| 1469 |
+
width: min(600px, 90%);
|
| 1470 |
+
margin: 10px auto 30px auto;
|
| 1471 |
+
border: 1px solid #e5e5e5;
|
| 1472 |
+
border-radius: 12px;
|
| 1473 |
+
overflow: hidden;
|
| 1474 |
+
background: #fff;
|
| 1475 |
+
text-decoration: none;
|
| 1476 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
| 1477 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
.fake-ad:hover {
|
| 1481 |
+
transform: translateY(-2px);
|
| 1482 |
+
box-shadow: 0 12px 30px rgba(0,0,0,0.16);
|
| 1483 |
+
}
|
| 1484 |
+
|
| 1485 |
+
.fake-ad img {
|
| 1486 |
+
display: block;
|
| 1487 |
+
width: 100%;
|
| 1488 |
+
height: auto;
|
| 1489 |
+
}
|
| 1490 |
+
|
| 1491 |
+
.fake-ad-badge {
|
| 1492 |
+
position: absolute;
|
| 1493 |
+
top: 12px;
|
| 1494 |
+
left: 12px;
|
| 1495 |
+
background: rgba(0,0,0,0.75);
|
| 1496 |
+
color: #fff;
|
| 1497 |
+
font-size: 12px;
|
| 1498 |
+
padding: 4px 8px;
|
| 1499 |
+
border-radius: 6px;
|
| 1500 |
+
letter-spacing: 0.5px;
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
.fake-ad-cta {
|
| 1504 |
+
position: absolute;
|
| 1505 |
+
right: 12px;
|
| 1506 |
+
bottom: 12px;
|
| 1507 |
+
background: #111;
|
| 1508 |
+
color: #fff;
|
| 1509 |
+
font-size: 14px;
|
| 1510 |
+
padding: 8px 12px;
|
| 1511 |
+
border-radius: 8px;
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
.help-section {
|
| 1515 |
+
background: white;
|
| 1516 |
+
padding: 30px;
|
| 1517 |
+
border-radius: 12px;
|
| 1518 |
+
margin-bottom: 20px;
|
| 1519 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.help-section h2 {
|
| 1523 |
+
color: #007BFF;
|
| 1524 |
+
margin-top: 0;
|
| 1525 |
+
margin-bottom: 15px;
|
| 1526 |
+
}
|
| 1527 |
+
|
| 1528 |
+
.help-section ol,
|
| 1529 |
+
.help-section ul {
|
| 1530 |
+
line-height: 1.8;
|
| 1531 |
+
color: #333;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.help-section p {
|
| 1535 |
+
line-height: 1.6;
|
| 1536 |
+
color: #333;
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
details {
|
| 1540 |
+
margin: 15px 0;
|
| 1541 |
+
cursor: pointer;
|
| 1542 |
+
padding: 10px;
|
| 1543 |
+
background: #f8f9fa;
|
| 1544 |
+
border-radius: 5px;
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
summary {
|
| 1548 |
+
font-weight: bold;
|
| 1549 |
+
padding: 5px;
|
| 1550 |
+
color: #007BFF;
|
| 1551 |
+
}
|
| 1552 |
+
|
| 1553 |
+
details[open] summary {
|
| 1554 |
+
margin-bottom: 10px;
|
| 1555 |
+
border-bottom: 1px solid #ddd;
|
| 1556 |
+
padding-bottom: 10px;
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
details p {
|
| 1560 |
+
margin: 10px 0 0 0;
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
/* ================ SESSION SUMMARY MODAL ================ */
|
| 1564 |
+
/* These modal styles can be reused for future overlays. */
|
| 1565 |
+
.modal-overlay {
|
| 1566 |
+
position: fixed;
|
| 1567 |
+
top: 0;
|
| 1568 |
+
left: 0;
|
| 1569 |
+
width: 100%;
|
| 1570 |
+
height: 100%;
|
| 1571 |
+
background: rgba(0, 0, 0, 0.7);
|
| 1572 |
+
display: flex;
|
| 1573 |
+
align-items: center;
|
| 1574 |
+
justify-content: center;
|
| 1575 |
+
z-index: 2000;
|
| 1576 |
+
}
|
| 1577 |
+
|
| 1578 |
+
.modal-content {
|
| 1579 |
+
background: white;
|
| 1580 |
+
padding: 40px;
|
| 1581 |
+
border-radius: 16px;
|
| 1582 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
| 1583 |
+
max-width: 500px;
|
| 1584 |
+
width: 90%;
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
.modal-content h2 {
|
| 1588 |
+
margin-top: 0;
|
| 1589 |
+
color: #333;
|
| 1590 |
+
text-align: center;
|
| 1591 |
+
margin-bottom: 30px;
|
| 1592 |
+
}
|
| 1593 |
+
|
| 1594 |
+
.summary-stats {
|
| 1595 |
+
margin-bottom: 30px;
|
| 1596 |
+
}
|
| 1597 |
+
|
| 1598 |
+
.summary-item {
|
| 1599 |
+
display: flex;
|
| 1600 |
+
justify-content: space-between;
|
| 1601 |
+
padding: 15px 0;
|
| 1602 |
+
border-bottom: 1px solid #eee;
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
.summary-item:last-child {
|
| 1606 |
+
border-bottom: none;
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
.summary-label {
|
| 1610 |
+
font-weight: 600;
|
| 1611 |
+
color: #666;
|
| 1612 |
+
}
|
| 1613 |
+
|
| 1614 |
+
.summary-value {
|
| 1615 |
+
font-weight: bold;
|
| 1616 |
+
color: #007BFF;
|
| 1617 |
+
font-size: 18px;
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
.modal-content .btn-main {
|
| 1621 |
+
display: block;
|
| 1622 |
+
margin: 0 auto;
|
| 1623 |
+
padding: 12px 40px;
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
/* ================ TIMELINE BLOCKS ================ */
|
| 1627 |
+
|
| 1628 |
+
.timeline-block {
|
| 1629 |
+
transition: opacity 0.2s;
|
| 1630 |
+
border-radius: 2px;
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
.timeline-block:hover {
|
| 1634 |
+
opacity: 0.7;
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
/* ================ RESPONSIVE DESIGN ================ */
|
| 1638 |
+
|
| 1639 |
+
@media (max-width: 1200px) {
|
| 1640 |
+
.stats-grid {
|
| 1641 |
+
grid-template-columns: repeat(2, 1fr);
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
.badges-grid {
|
| 1645 |
+
grid-template-columns: repeat(2, 1fr);
|
| 1646 |
+
}
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
@media (max-width: 768px) {
|
| 1650 |
+
.stats-grid,
|
| 1651 |
+
.badges-grid {
|
| 1652 |
+
grid-template-columns: 1fr;
|
| 1653 |
+
width: 90%;
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
.settings-container,
|
| 1657 |
+
.help-container,
|
| 1658 |
+
.chart-container,
|
| 1659 |
+
.sessions-list,
|
| 1660 |
+
.records-controls {
|
| 1661 |
+
width: 90%;
|
| 1662 |
+
}
|
| 1663 |
+
|
| 1664 |
+
#control-panel {
|
| 1665 |
+
width: 90%;
|
| 1666 |
+
flex-wrap: wrap;
|
| 1667 |
+
}
|
| 1668 |
+
|
| 1669 |
+
#display-area {
|
| 1670 |
+
width: 90%;
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
#timeline-area {
|
| 1674 |
+
width: 90%;
|
| 1675 |
+
}
|
| 1676 |
+
|
| 1677 |
+
#frame-control {
|
| 1678 |
+
width: 90%;
|
| 1679 |
+
flex-direction: column;
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
+
.focus-inline-error-standalone {
|
| 1683 |
+
width: 90%;
|
| 1684 |
+
}
|
| 1685 |
+
|
| 1686 |
+
.focus-flow-overlay {
|
| 1687 |
+
top: 70px;
|
| 1688 |
+
right: 10px;
|
| 1689 |
+
bottom: 10px;
|
| 1690 |
+
left: 10px;
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
.focus-flow-card {
|
| 1694 |
+
padding: 22px 20px;
|
| 1695 |
+
}
|
| 1696 |
+
|
| 1697 |
+
.focus-flow-header {
|
| 1698 |
+
flex-direction: column;
|
| 1699 |
+
align-items: flex-start;
|
| 1700 |
+
}
|
| 1701 |
+
|
| 1702 |
+
.focus-flow-icon {
|
| 1703 |
+
width: 92px;
|
| 1704 |
+
height: 92px;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
.focus-flow-grid {
|
| 1708 |
+
grid-template-columns: 1fr;
|
| 1709 |
+
}
|
| 1710 |
+
|
| 1711 |
+
.focus-flow-steps {
|
| 1712 |
+
grid-template-columns: 1fr;
|
| 1713 |
+
}
|
| 1714 |
+
|
| 1715 |
+
.focus-flow-footer {
|
| 1716 |
+
flex-direction: column;
|
| 1717 |
+
align-items: stretch;
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
.focus-flow-button,
|
| 1721 |
+
.focus-flow-secondary {
|
| 1722 |
+
width: 100%;
|
| 1723 |
+
}
|
| 1724 |
+
|
| 1725 |
+
.records-detail-modal {
|
| 1726 |
+
width: 94vw;
|
| 1727 |
+
padding: 22px 18px;
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
.records-detail-header,
|
| 1731 |
+
.records-detail-section-head {
|
| 1732 |
+
flex-direction: column;
|
| 1733 |
+
align-items: flex-start;
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
.records-detail-summary,
|
| 1737 |
+
.records-detail-grid,
|
| 1738 |
+
.records-detail-list {
|
| 1739 |
+
grid-template-columns: 1fr;
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
.records-detail-event {
|
| 1743 |
+
grid-template-columns: 1fr;
|
| 1744 |
+
align-items: flex-start;
|
| 1745 |
+
}
|
| 1746 |
+
}
|
| 1747 |
+
/* =========================================
|
| 1748 |
+
SESSION RESULT OVERLAY
|
| 1749 |
+
========================================= */
|
| 1750 |
+
|
| 1751 |
+
.session-result-overlay {
|
| 1752 |
+
position: absolute;
|
| 1753 |
+
top: 0;
|
| 1754 |
+
left: 0;
|
| 1755 |
+
width: 100%;
|
| 1756 |
+
height: 100%;
|
| 1757 |
+
background-color: rgba(0, 0, 0, 0.85); /* Dark semi-transparent backdrop. */
|
| 1758 |
+
display: flex;
|
| 1759 |
+
flex-direction: column;
|
| 1760 |
+
justify-content: center;
|
| 1761 |
+
align-items: center;
|
| 1762 |
+
color: white;
|
| 1763 |
+
z-index: 10;
|
| 1764 |
+
animation: fadeIn 0.5s ease;
|
| 1765 |
+
backdrop-filter: blur(5px); /* Optional background blur. */
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
.session-result-overlay h3 {
|
| 1769 |
+
font-size: 32px;
|
| 1770 |
+
margin-bottom: 30px;
|
| 1771 |
+
color: #4cd137; /* Green title accent. */
|
| 1772 |
+
text-transform: uppercase;
|
| 1773 |
+
letter-spacing: 2px;
|
| 1774 |
+
}
|
| 1775 |
+
|
| 1776 |
+
.session-result-overlay .result-item {
|
| 1777 |
+
display: flex;
|
| 1778 |
+
justify-content: space-between;
|
| 1779 |
+
width: 200px; /* Keep the stat row compact. */
|
| 1780 |
+
margin-bottom: 15px;
|
| 1781 |
+
font-size: 20px;
|
| 1782 |
+
border-bottom: 1px solid rgba(255,255,255,0.2);
|
| 1783 |
+
padding-bottom: 5px;
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
.session-result-overlay .label {
|
| 1787 |
+
color: #ccc;
|
| 1788 |
+
font-weight: normal;
|
| 1789 |
+
}
|
| 1790 |
+
|
| 1791 |
+
.session-result-overlay .value {
|
| 1792 |
+
color: #fff;
|
| 1793 |
+
font-weight: bold;
|
| 1794 |
+
font-family: 'Courier New', monospace; /* Give the values a data-like look. */
|
| 1795 |
+
}
|
| 1796 |
+
|
| 1797 |
+
@keyframes fadeIn {
|
| 1798 |
+
from { opacity: 0; transform: scale(0.95); }
|
| 1799 |
+
to { opacity: 1; transform: scale(1); }
|
| 1800 |
+
}
|
| 1801 |
+
|
| 1802 |
+
/* ================= Welcome modal styles ================= */
|
| 1803 |
+
.welcome-modal-overlay {
|
| 1804 |
+
position: fixed;
|
| 1805 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 1806 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 1807 |
+
display: flex;
|
| 1808 |
+
justify-content: center;
|
| 1809 |
+
align-items: center;
|
| 1810 |
+
z-index: 9999;
|
| 1811 |
+
}
|
| 1812 |
+
|
| 1813 |
+
.welcome-modal {
|
| 1814 |
+
background-color: #1e1e24;
|
| 1815 |
+
padding: 40px;
|
| 1816 |
+
border-radius: 15px;
|
| 1817 |
+
text-align: center;
|
| 1818 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 1819 |
+
border: 1px solid #333;
|
| 1820 |
+
}
|
| 1821 |
+
|
| 1822 |
+
.welcome-modal h2 { margin-top: 0; color: #fff; }
|
| 1823 |
+
.welcome-modal p { margin-bottom: 30px; color: #ccc; }
|
| 1824 |
+
.welcome-buttons { display: flex; gap: 20px; justify-content: center; }
|
| 1825 |
+
|
| 1826 |
+
/* ================= Top-left avatar styles ================= */
|
| 1827 |
+
.avatar-container {
|
| 1828 |
+
position: absolute;
|
| 1829 |
+
left: 20px;
|
| 1830 |
+
cursor: pointer;
|
| 1831 |
+
z-index: 1;
|
| 1832 |
+
}
|
| 1833 |
+
|
| 1834 |
+
.avatar-circle {
|
| 1835 |
+
width: 40px;
|
| 1836 |
+
height: 40px;
|
| 1837 |
+
border-radius: 50%;
|
| 1838 |
+
display: flex;
|
| 1839 |
+
justify-content: center;
|
| 1840 |
+
align-items: center;
|
| 1841 |
+
font-weight: bold;
|
| 1842 |
+
font-size: 1.2rem;
|
| 1843 |
+
color: white;
|
| 1844 |
+
transition: all 0.3s ease;
|
| 1845 |
+
border: 2px solid transparent;
|
| 1846 |
+
}
|
| 1847 |
+
|
| 1848 |
+
.avatar-circle.user { background-color: #555; }
|
| 1849 |
+
.avatar-circle.admin { background-color: #ffaa00; border-color: #fff; box-shadow: 0 0 10px rgba(255, 170, 0, 0.5); }
|
| 1850 |
+
|
| 1851 |
+
/* ================ CALIBRATION OVERLAY ================ */
|
| 1852 |
+
|
| 1853 |
+
.cal-overlay {
|
| 1854 |
+
position: fixed;
|
| 1855 |
+
top: 0;
|
| 1856 |
+
left: 0;
|
| 1857 |
+
width: 100vw;
|
| 1858 |
+
height: 100vh;
|
| 1859 |
+
background: rgba(8, 15, 28, 0.94);
|
| 1860 |
+
backdrop-filter: blur(6px);
|
| 1861 |
+
z-index: 10000;
|
| 1862 |
+
display: flex;
|
| 1863 |
+
align-items: center;
|
| 1864 |
+
justify-content: center;
|
| 1865 |
+
font-family: 'Nunito', sans-serif;
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
/* ---- header / instructions ---- */
|
| 1869 |
+
.cal-header {
|
| 1870 |
+
position: absolute;
|
| 1871 |
+
top: 36px;
|
| 1872 |
+
left: 50%;
|
| 1873 |
+
transform: translateX(-50%);
|
| 1874 |
+
text-align: center;
|
| 1875 |
+
pointer-events: none;
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
.cal-eyebrow {
|
| 1879 |
+
display: inline-block;
|
| 1880 |
+
padding: 6px 14px;
|
| 1881 |
+
border-radius: 999px;
|
| 1882 |
+
font-size: 0.82rem;
|
| 1883 |
+
font-weight: 800;
|
| 1884 |
+
letter-spacing: 0.04em;
|
| 1885 |
+
text-transform: uppercase;
|
| 1886 |
+
}
|
| 1887 |
+
|
| 1888 |
+
.cal-eyebrow-collect {
|
| 1889 |
+
background: rgba(40, 167, 69, 0.18);
|
| 1890 |
+
color: #5ee882;
|
| 1891 |
+
}
|
| 1892 |
+
|
| 1893 |
+
.cal-eyebrow-verify {
|
| 1894 |
+
background: rgba(0, 123, 255, 0.18);
|
| 1895 |
+
color: #6bb8ff;
|
| 1896 |
+
}
|
| 1897 |
+
|
| 1898 |
+
.cal-instruction {
|
| 1899 |
+
margin: 10px 0 0;
|
| 1900 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1901 |
+
font-size: 0.95rem;
|
| 1902 |
+
line-height: 1.5;
|
| 1903 |
+
}
|
| 1904 |
+
|
| 1905 |
+
/* ---- target dot + ring ---- */
|
| 1906 |
+
.cal-target {
|
| 1907 |
+
position: absolute;
|
| 1908 |
+
transform: translate(-50%, -50%);
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
.cal-ring {
|
| 1912 |
+
position: absolute;
|
| 1913 |
+
left: -30px;
|
| 1914 |
+
top: -30px;
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
.cal-dot {
|
| 1918 |
+
width: 20px;
|
| 1919 |
+
height: 20px;
|
| 1920 |
+
border-radius: 50%;
|
| 1921 |
+
transition: box-shadow 0.3s ease;
|
| 1922 |
+
}
|
| 1923 |
+
|
| 1924 |
+
/* ---- cancel button (matches focus-flow-secondary) ---- */
|
| 1925 |
+
.cal-cancel {
|
| 1926 |
+
position: absolute;
|
| 1927 |
+
bottom: 40px;
|
| 1928 |
+
left: 50%;
|
| 1929 |
+
transform: translateX(-50%);
|
| 1930 |
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
| 1931 |
+
border-radius: 999px;
|
| 1932 |
+
padding: 12px 28px;
|
| 1933 |
+
background: rgba(255, 255, 255, 0.08);
|
| 1934 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1935 |
+
font-family: 'Nunito', sans-serif;
|
| 1936 |
+
font-size: 0.95rem;
|
| 1937 |
+
font-weight: 700;
|
| 1938 |
+
cursor: pointer;
|
| 1939 |
+
transition: background 0.2s ease, border-color 0.2s ease;
|
| 1940 |
+
}
|
| 1941 |
+
|
| 1942 |
+
.cal-cancel:hover {
|
| 1943 |
+
background: rgba(255, 255, 255, 0.14);
|
| 1944 |
+
border-color: rgba(255, 255, 255, 0.4);
|
| 1945 |
+
}
|
| 1946 |
+
|
| 1947 |
+
/* ---- done card (matches focus-flow-card style) ---- */
|
| 1948 |
+
.cal-done-card {
|
| 1949 |
+
text-align: center;
|
| 1950 |
+
padding: 36px 44px;
|
| 1951 |
+
border-radius: 20px;
|
| 1952 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 1953 |
+
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.4);
|
| 1954 |
+
animation: fadeIn 0.4s ease;
|
| 1955 |
+
}
|
| 1956 |
+
|
| 1957 |
+
.cal-done-success {
|
| 1958 |
+
background: linear-gradient(168deg, rgba(40, 167, 69, 0.15) 0%, rgba(20, 30, 48, 0.95) 60%);
|
| 1959 |
+
border-color: rgba(40, 167, 69, 0.3);
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
.cal-done-fail {
|
| 1963 |
+
background: linear-gradient(168deg, rgba(220, 53, 69, 0.15) 0%, rgba(20, 30, 48, 0.95) 60%);
|
| 1964 |
+
border-color: rgba(220, 53, 69, 0.3);
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
.cal-done-eyebrow {
|
| 1968 |
+
display: inline-block;
|
| 1969 |
+
padding: 6px 14px;
|
| 1970 |
+
border-radius: 999px;
|
| 1971 |
+
font-size: 0.78rem;
|
| 1972 |
+
font-weight: 800;
|
| 1973 |
+
letter-spacing: 0.06em;
|
| 1974 |
+
text-transform: uppercase;
|
| 1975 |
+
margin-bottom: 14px;
|
| 1976 |
+
}
|
| 1977 |
+
|
| 1978 |
+
.cal-done-success .cal-done-eyebrow {
|
| 1979 |
+
background: rgba(40, 167, 69, 0.2);
|
| 1980 |
+
color: #5ee882;
|
| 1981 |
+
}
|
| 1982 |
+
|
| 1983 |
+
.cal-done-fail .cal-done-eyebrow {
|
| 1984 |
+
background: rgba(220, 53, 69, 0.2);
|
| 1985 |
+
color: #f87171;
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
.cal-done-title {
|
| 1989 |
+
margin: 0 0 8px;
|
| 1990 |
+
font-size: 1.6rem;
|
| 1991 |
+
color: #fff;
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.cal-done-subtitle {
|
| 1995 |
+
margin: 0;
|
| 1996 |
+
color: rgba(255, 255, 255, 0.6);
|
| 1997 |
+
font-size: 0.95rem;
|
| 1998 |
+
line-height: 1.5;
|
| 1999 |
+
}
|
| 2000 |
+
/* ================= Home page 2x2 responsive button grid ================= */
|
| 2001 |
+
.home-button-grid {
|
| 2002 |
+
display: flex;
|
| 2003 |
+
justify-content: center;
|
| 2004 |
+
width: 100%;
|
| 2005 |
+
max-width: 360px;
|
| 2006 |
+
margin: 40px auto 0;
|
| 2007 |
+
}
|
| 2008 |
+
|
| 2009 |
+
.home-button-grid .btn-main {
|
| 2010 |
+
width: 100%;
|
| 2011 |
+
height: 60px; /* Keep all tiles at the same height. */
|
| 2012 |
+
margin: 0; /* Remove default outer spacing. */
|
| 2013 |
+
padding: 10px;
|
| 2014 |
+
font-size: 1rem;
|
| 2015 |
+
display: flex;
|
| 2016 |
+
justify-content: center;
|
| 2017 |
+
align-items: center;
|
| 2018 |
+
text-align: center;
|
| 2019 |
+
box-sizing: border-box; /* Prevent padding and borders from breaking the grid. */
|
| 2020 |
+
}
|
| 2021 |
+
|
| 2022 |
+
/* Mobile-only scaling for screens below 600px. */
|
| 2023 |
+
@media (max-width: 600px) {
|
| 2024 |
+
#top-menu {
|
| 2025 |
+
justify-content: flex-start;
|
| 2026 |
+
padding: 0 12px 0 68px;
|
| 2027 |
+
}
|
| 2028 |
+
|
| 2029 |
+
.menu-btn {
|
| 2030 |
+
padding: 10px 14px;
|
| 2031 |
+
font-size: 0.92rem;
|
| 2032 |
+
}
|
| 2033 |
+
|
| 2034 |
+
.separator {
|
| 2035 |
+
margin: 0 2px;
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
.home-button-grid {
|
| 2039 |
+
gap: 15px;
|
| 2040 |
+
max-width: 90%;
|
| 2041 |
+
}
|
| 2042 |
+
|
| 2043 |
+
.home-button-grid .btn-main {
|
| 2044 |
+
height: 50px;
|
| 2045 |
+
font-size: 0.85rem;
|
| 2046 |
+
}
|
| 2047 |
+
}
|
src/App.jsx
CHANGED
|
@@ -1,91 +1,125 @@
|
|
| 1 |
-
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
-
import './App.css';
|
| 3 |
-
import { VideoManagerLocal } from './utils/VideoManagerLocal';
|
| 4 |
-
|
| 5 |
-
import Home from './components/Home';
|
| 6 |
-
import FocusPageLocal from './components/FocusPageLocal';
|
| 7 |
-
import Achievement from './components/Achievement';
|
| 8 |
-
import Records from './components/Records';
|
| 9 |
-
import
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
const
|
| 14 |
-
const
|
| 15 |
-
const [
|
| 16 |
-
const [
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import './App.css';
|
| 3 |
+
import { VideoManagerLocal } from './utils/VideoManagerLocal';
|
| 4 |
+
|
| 5 |
+
import Home from './components/Home';
|
| 6 |
+
import FocusPageLocal from './components/FocusPageLocal';
|
| 7 |
+
import Achievement from './components/Achievement';
|
| 8 |
+
import Records from './components/Records';
|
| 9 |
+
import Help from './components/Help';
|
| 10 |
+
|
| 11 |
+
function App() {
|
| 12 |
+
const [activeTab, setActiveTab] = useState('home');
|
| 13 |
+
const videoManagerRef = useRef(null);
|
| 14 |
+
const [isSessionActive, setIsSessionActive] = useState(false);
|
| 15 |
+
const [sessionResult, setSessionResult] = useState(null);
|
| 16 |
+
const [isTutorialActive, setIsTutorialActive] = useState(false);
|
| 17 |
+
const [hasSeenTutorial, setHasSeenTutorial] = useState(false);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
fetch('/api/history', { method: 'DELETE' })
|
| 21 |
+
.then(() => {
|
| 22 |
+
const backup = localStorage.getItem('focus_magic_backup');
|
| 23 |
+
if (backup) {
|
| 24 |
+
try {
|
| 25 |
+
const sessions = JSON.parse(backup);
|
| 26 |
+
fetch('/api/import', {
|
| 27 |
+
method: 'POST',
|
| 28 |
+
headers: { 'Content-Type': 'application/json' },
|
| 29 |
+
body: JSON.stringify(sessions)
|
| 30 |
+
});
|
| 31 |
+
} catch (err) {
|
| 32 |
+
console.error(err);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
})
|
| 36 |
+
.catch(err => console.error(err));
|
| 37 |
+
|
| 38 |
+
const callbacks = {
|
| 39 |
+
onSessionStart: () => {
|
| 40 |
+
setIsSessionActive(true);
|
| 41 |
+
setSessionResult(null);
|
| 42 |
+
},
|
| 43 |
+
onSessionEnd: (summary) => {
|
| 44 |
+
setIsSessionActive(false);
|
| 45 |
+
if (summary) setSessionResult(summary);
|
| 46 |
+
|
| 47 |
+
fetch('/api/sessions?filter=all')
|
| 48 |
+
.then(res => res.json())
|
| 49 |
+
.then(data => {
|
| 50 |
+
if (data && Array.isArray(data)) {
|
| 51 |
+
localStorage.setItem('focus_magic_backup', JSON.stringify(data));
|
| 52 |
+
}
|
| 53 |
+
})
|
| 54 |
+
.catch(err => console.error(err));
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
videoManagerRef.current = new VideoManagerLocal(callbacks);
|
| 58 |
+
|
| 59 |
+
return () => {
|
| 60 |
+
if (videoManagerRef.current) videoManagerRef.current.stopStreaming();
|
| 61 |
+
};
|
| 62 |
+
}, []);
|
| 63 |
+
|
| 64 |
+
const handleStartFocus = () => {
|
| 65 |
+
if (!hasSeenTutorial) {
|
| 66 |
+
setIsTutorialActive(true);
|
| 67 |
+
}
|
| 68 |
+
setActiveTab('focus');
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const handleStartTutorial = () => {
|
| 72 |
+
setIsTutorialActive(true);
|
| 73 |
+
setActiveTab('focus');
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
return (
|
| 77 |
+
<div className="app-container">
|
| 78 |
+
<nav id="top-menu">
|
| 79 |
+
<button
|
| 80 |
+
className={`menu-btn ${activeTab === 'home' ? 'active' : ''}`}
|
| 81 |
+
onClick={() => setActiveTab('home')}
|
| 82 |
+
>
|
| 83 |
+
Home
|
| 84 |
+
</button>
|
| 85 |
+
<div className="separator"></div>
|
| 86 |
+
|
| 87 |
+
<button className={`menu-btn ${activeTab === 'focus' ? 'active' : ''}`} onClick={handleStartFocus}>
|
| 88 |
+
Start Focus {isSessionActive && <span style={{ marginLeft: '8px', color: '#00FF00' }}>●</span>}
|
| 89 |
+
</button>
|
| 90 |
+
<div className="separator"></div>
|
| 91 |
+
|
| 92 |
+
<button className={`menu-btn ${activeTab === 'achievement' ? 'active' : ''}`} onClick={() => setActiveTab('achievement')}>
|
| 93 |
+
My Achievement
|
| 94 |
+
</button>
|
| 95 |
+
<div className="separator"></div>
|
| 96 |
+
|
| 97 |
+
<button className={`menu-btn ${activeTab === 'records' ? 'active' : ''}`} onClick={() => setActiveTab('records')}>
|
| 98 |
+
My Records
|
| 99 |
+
</button>
|
| 100 |
+
<div className="separator"></div>
|
| 101 |
+
|
| 102 |
+
<button className={`menu-btn ${activeTab === 'help' ? 'active' : ''}`} onClick={() => setActiveTab('help')}>
|
| 103 |
+
Help
|
| 104 |
+
</button>
|
| 105 |
+
</nav>
|
| 106 |
+
|
| 107 |
+
{activeTab === 'home' && <Home onStartFocus={handleStartFocus} onStartTutorial={handleStartTutorial} />}
|
| 108 |
+
|
| 109 |
+
<FocusPageLocal
|
| 110 |
+
videoManager={videoManagerRef.current}
|
| 111 |
+
sessionResult={sessionResult}
|
| 112 |
+
setSessionResult={setSessionResult}
|
| 113 |
+
isActive={activeTab === 'focus'}
|
| 114 |
+
isTutorialActive={isTutorialActive}
|
| 115 |
+
setIsTutorialActive={setIsTutorialActive}
|
| 116 |
+
setHasSeenTutorial={setHasSeenTutorial}
|
| 117 |
+
/>
|
| 118 |
+
{activeTab === 'achievement' && <Achievement />}
|
| 119 |
+
{activeTab === 'records' && <Records />}
|
| 120 |
+
{activeTab === 'help' && <Help />}
|
| 121 |
+
</div>
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export default App;
|
src/components/FocusPageLocal.jsx
CHANGED
|
@@ -1,944 +1,1040 @@
|
|
| 1 |
-
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
-
import CalibrationOverlay from './CalibrationOverlay';
|
| 3 |
-
|
| 4 |
-
const FLOW_STEPS = {
|
| 5 |
-
intro: 'intro',
|
| 6 |
-
permission: 'permission',
|
| 7 |
-
ready: 'ready'
|
| 8 |
-
};
|
| 9 |
-
|
| 10 |
-
const FOCUS_STATES = {
|
| 11 |
-
pending: 'pending',
|
| 12 |
-
focused: 'focused',
|
| 13 |
-
notFocused: 'not-focused'
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
function HelloIcon() {
|
| 17 |
-
return (
|
| 18 |
-
<svg width="96" height="96" viewBox="0 0 96 96" aria-hidden="true">
|
| 19 |
-
<circle cx="48" cy="48" r="40" fill="#007BFF" />
|
| 20 |
-
<path d="M30 38c0-4 2.7-7 6-7s6 3 6 7" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 21 |
-
<path d="M54 38c0-4 2.7-7 6-7s6 3 6 7" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 22 |
-
<path d="M30 52c3 11 10 17 18 17s15-6 18-17" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 23 |
-
</svg>
|
| 24 |
-
);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
function CameraIcon() {
|
| 28 |
-
return (
|
| 29 |
-
<svg width="110" height="110" viewBox="0 0 110 110" aria-hidden="true">
|
| 30 |
-
<rect x="30" y="36" width="50" height="34" rx="5" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 31 |
-
<path d="M24 72h62c0 9-7 16-16 16H40c-9 0-16-7-16-16Z" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 32 |
-
<path d="M55 28v8" stroke="#007BFF" strokeWidth="6" strokeLinecap="round" />
|
| 33 |
-
<circle cx="55" cy="36" r="14" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 34 |
-
<circle cx="55" cy="36" r="4" fill="#007BFF" />
|
| 35 |
-
<path d="M46 83h18" stroke="#007BFF" strokeWidth="6" strokeLinecap="round" />
|
| 36 |
-
</svg>
|
| 37 |
-
);
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
const MODEL_ORDER = ['hybrid', 'xgboost', 'mlp', 'geometric'];
|
| 41 |
-
|
| 42 |
-
const MODEL_INFO = {
|
| 43 |
-
hybrid: {
|
| 44 |
-
label: 'Hybrid',
|
| 45 |
-
tagline: 'Best overall — combines ML with geometric scoring',
|
| 46 |
-
how: 'Fuses XGBoost predictions (30%) with geometric face/eye scores (70%). Uses a weighted blend tuned with LOPO evaluation.',
|
| 47 |
-
accuracy: 'N/A',
|
| 48 |
-
f1: '0.8409',
|
| 49 |
-
auc: 'N/A',
|
| 50 |
-
threshold: '0.46',
|
| 51 |
-
evaluation: 'LOPO tuning (9 participants, 144K frames)',
|
| 52 |
-
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 53 |
-
strengths: 'Most robust across different people. Latest LOPO mean F1 is 0.8409 at w_mlp=0.3.',
|
| 54 |
-
badge: 'Recommended',
|
| 55 |
-
},
|
| 56 |
-
xgboost: {
|
| 57 |
-
label: 'XGBoost',
|
| 58 |
-
tagline: 'Highest raw accuracy — gradient-boosted decision trees',
|
| 59 |
-
how: 'Ensemble of 600 decision trees (max depth 8). Each tree learns to correct errors from previous trees. Outputs probability of focused state.',
|
| 60 |
-
accuracy: '95.87%',
|
| 61 |
-
f1: '0.9585',
|
| 62 |
-
auc: '0.9908',
|
| 63 |
-
threshold: '0.38',
|
| 64 |
-
evaluation: 'Random split test (15%) + LOPO thresholds',
|
| 65 |
-
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 66 |
-
strengths: 'Strong pattern recognition and fast inference. LOPO: AUC 0.8695, optimal threshold 0.280, F1 0.8549.',
|
| 67 |
-
badge: null,
|
| 68 |
-
},
|
| 69 |
-
mlp: {
|
| 70 |
-
label: 'MLP',
|
| 71 |
-
tagline: 'Lightweight neural network — fast and efficient',
|
| 72 |
-
how: 'Two-layer neural network (64→32 neurons). Takes 10 face features, applies learned weights, outputs focused/unfocused probability via softmax.',
|
| 73 |
-
accuracy: '92.92%',
|
| 74 |
-
f1: '0.9287',
|
| 75 |
-
auc: '0.9714',
|
| 76 |
-
threshold: '0.23',
|
| 77 |
-
evaluation: 'Random split test (15%) + LOPO thresholds',
|
| 78 |
-
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 79 |
-
strengths: 'Fastest inference and smallest model size. LOPO: AUC 0.8624, optimal threshold 0.228, F1 0.8578.',
|
| 80 |
-
badge: null,
|
| 81 |
-
},
|
| 82 |
-
geometric: {
|
| 83 |
-
label: 'Geometric',
|
| 84 |
-
tagline: 'Baseline only — hardcoded thresholds, no learning',
|
| 85 |
-
how: 'Uses fixed thresholds on head orientation (70%) and eye openness (30%). No training — just hand-tuned rules on 478 face landmarks. Cannot adapt to new faces or environments.',
|
| 86 |
-
accuracy: 'N/A',
|
| 87 |
-
f1: '0.8195',
|
| 88 |
-
auc: 'N/A',
|
| 89 |
-
threshold: '0.55',
|
| 90 |
-
evaluation: 'LOPO geometric sweep',
|
| 91 |
-
features: 'Head yaw/pitch/roll angles, eye aspect ratio (EAR), iris gaze offset, mouth aspect ratio (MAR)',
|
| 92 |
-
strengths: 'No model files needed. Useful fallback when model checkpoints are unavailable.',
|
| 93 |
-
badge: 'Baseline',
|
| 94 |
-
},
|
| 95 |
-
};
|
| 96 |
-
|
| 97 |
-
function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActive }) {
|
| 98 |
-
const [currentFrame, setCurrentFrame] = useState(15);
|
| 99 |
-
const [timelineEvents, setTimelineEvents] = useState([]);
|
| 100 |
-
const [stats, setStats] = useState(null);
|
| 101 |
-
const [systemStats, setSystemStats] = useState(null);
|
| 102 |
-
const [availableModels, setAvailableModels] = useState([]);
|
| 103 |
-
const [currentModel, setCurrentModel] = useState('mlp');
|
| 104 |
-
const [flowStep, setFlowStep] = useState(FLOW_STEPS.
|
| 105 |
-
const [cameraReady, setCameraReady] = useState(false);
|
| 106 |
-
const [isStarting, setIsStarting] = useState(false);
|
| 107 |
-
const [focusState, setFocusState] = useState(FOCUS_STATES.pending);
|
| 108 |
-
const [cameraError, setCameraError] = useState('');
|
| 109 |
-
const [calibration, setCalibration] = useState(null);
|
| 110 |
-
const [l2csBoost, setL2csBoost] = useState(false);
|
| 111 |
-
const [l2csBoostAvailable, setL2csBoostAvailable] = useState(false);
|
| 112 |
-
|
| 113 |
-
const
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
const
|
| 118 |
-
|
| 119 |
-
const
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
videoManager.callbacks.onSessionEnd
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
if (data.
|
| 230 |
-
if (data.
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
.
|
| 268 |
-
.
|
| 269 |
-
|
| 270 |
-
};
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
}
|
| 505 |
-
};
|
| 506 |
-
|
| 507 |
-
const
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
const
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
{
|
| 565 |
-
title: '
|
| 566 |
-
text: '
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
<
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
<
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
</div>
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
<div
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
<
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
<div className="
|
| 737 |
-
<
|
| 738 |
-
|
| 739 |
-
<
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
}
|
| 758 |
-
>
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
<
|
| 771 |
-
|
| 772 |
-
{
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
{
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
<
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
<
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
<
|
| 847 |
-
|
| 848 |
-
<
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
</
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import CalibrationOverlay from './CalibrationOverlay';
|
| 3 |
+
|
| 4 |
+
const FLOW_STEPS = {
|
| 5 |
+
intro: 'intro',
|
| 6 |
+
permission: 'permission',
|
| 7 |
+
ready: 'ready'
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
const FOCUS_STATES = {
|
| 11 |
+
pending: 'pending',
|
| 12 |
+
focused: 'focused',
|
| 13 |
+
notFocused: 'not-focused'
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
function HelloIcon() {
|
| 17 |
+
return (
|
| 18 |
+
<svg width="96" height="96" viewBox="0 0 96 96" aria-hidden="true">
|
| 19 |
+
<circle cx="48" cy="48" r="40" fill="#007BFF" />
|
| 20 |
+
<path d="M30 38c0-4 2.7-7 6-7s6 3 6 7" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 21 |
+
<path d="M54 38c0-4 2.7-7 6-7s6 3 6 7" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 22 |
+
<path d="M30 52c3 11 10 17 18 17s15-6 18-17" fill="none" stroke="#fff" strokeWidth="6" strokeLinecap="round" />
|
| 23 |
+
</svg>
|
| 24 |
+
);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function CameraIcon() {
|
| 28 |
+
return (
|
| 29 |
+
<svg width="110" height="110" viewBox="0 0 110 110" aria-hidden="true">
|
| 30 |
+
<rect x="30" y="36" width="50" height="34" rx="5" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 31 |
+
<path d="M24 72h62c0 9-7 16-16 16H40c-9 0-16-7-16-16Z" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 32 |
+
<path d="M55 28v8" stroke="#007BFF" strokeWidth="6" strokeLinecap="round" />
|
| 33 |
+
<circle cx="55" cy="36" r="14" fill="none" stroke="#007BFF" strokeWidth="6" />
|
| 34 |
+
<circle cx="55" cy="36" r="4" fill="#007BFF" />
|
| 35 |
+
<path d="M46 83h18" stroke="#007BFF" strokeWidth="6" strokeLinecap="round" />
|
| 36 |
+
</svg>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const MODEL_ORDER = ['hybrid', 'xgboost', 'mlp', 'geometric'];
|
| 41 |
+
|
| 42 |
+
const MODEL_INFO = {
|
| 43 |
+
hybrid: {
|
| 44 |
+
label: 'Hybrid',
|
| 45 |
+
tagline: 'Best overall — combines ML with geometric scoring',
|
| 46 |
+
how: 'Fuses XGBoost predictions (30%) with geometric face/eye scores (70%). Uses a weighted blend tuned with LOPO evaluation.',
|
| 47 |
+
accuracy: 'N/A',
|
| 48 |
+
f1: '0.8409',
|
| 49 |
+
auc: 'N/A',
|
| 50 |
+
threshold: '0.46',
|
| 51 |
+
evaluation: 'LOPO tuning (9 participants, 144K frames)',
|
| 52 |
+
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 53 |
+
strengths: 'Most robust across different people. Latest LOPO mean F1 is 0.8409 at w_mlp=0.3.',
|
| 54 |
+
badge: 'Recommended',
|
| 55 |
+
},
|
| 56 |
+
xgboost: {
|
| 57 |
+
label: 'XGBoost',
|
| 58 |
+
tagline: 'Highest raw accuracy — gradient-boosted decision trees',
|
| 59 |
+
how: 'Ensemble of 600 decision trees (max depth 8). Each tree learns to correct errors from previous trees. Outputs probability of focused state.',
|
| 60 |
+
accuracy: '95.87%',
|
| 61 |
+
f1: '0.9585',
|
| 62 |
+
auc: '0.9908',
|
| 63 |
+
threshold: '0.38',
|
| 64 |
+
evaluation: 'Random split test (15%) + LOPO thresholds',
|
| 65 |
+
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 66 |
+
strengths: 'Strong pattern recognition and fast inference. LOPO: AUC 0.8695, optimal threshold 0.280, F1 0.8549.',
|
| 67 |
+
badge: null,
|
| 68 |
+
},
|
| 69 |
+
mlp: {
|
| 70 |
+
label: 'MLP',
|
| 71 |
+
tagline: 'Lightweight neural network — fast and efficient',
|
| 72 |
+
how: 'Two-layer neural network (64→32 neurons). Takes 10 face features, applies learned weights, outputs focused/unfocused probability via softmax.',
|
| 73 |
+
accuracy: '92.92%',
|
| 74 |
+
f1: '0.9287',
|
| 75 |
+
auc: '0.9714',
|
| 76 |
+
threshold: '0.23',
|
| 77 |
+
evaluation: 'Random split test (15%) + LOPO thresholds',
|
| 78 |
+
features: '10 features: head deviation, face score, eye scores (EAR), gaze offset, pitch, horizontal gaze, PERCLOS',
|
| 79 |
+
strengths: 'Fastest inference and smallest model size. LOPO: AUC 0.8624, optimal threshold 0.228, F1 0.8578.',
|
| 80 |
+
badge: null,
|
| 81 |
+
},
|
| 82 |
+
geometric: {
|
| 83 |
+
label: 'Geometric',
|
| 84 |
+
tagline: 'Baseline only — hardcoded thresholds, no learning',
|
| 85 |
+
how: 'Uses fixed thresholds on head orientation (70%) and eye openness (30%). No training — just hand-tuned rules on 478 face landmarks. Cannot adapt to new faces or environments.',
|
| 86 |
+
accuracy: 'N/A',
|
| 87 |
+
f1: '0.8195',
|
| 88 |
+
auc: 'N/A',
|
| 89 |
+
threshold: '0.55',
|
| 90 |
+
evaluation: 'LOPO geometric sweep',
|
| 91 |
+
features: 'Head yaw/pitch/roll angles, eye aspect ratio (EAR), iris gaze offset, mouth aspect ratio (MAR)',
|
| 92 |
+
strengths: 'No model files needed. Useful fallback when model checkpoints are unavailable.',
|
| 93 |
+
badge: 'Baseline',
|
| 94 |
+
},
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActive, isTutorialActive, setIsTutorialActive, setHasSeenTutorial }) {
|
| 98 |
+
const [currentFrame, setCurrentFrame] = useState(15);
|
| 99 |
+
const [timelineEvents, setTimelineEvents] = useState([]);
|
| 100 |
+
const [stats, setStats] = useState(null);
|
| 101 |
+
const [systemStats, setSystemStats] = useState(null);
|
| 102 |
+
const [availableModels, setAvailableModels] = useState([]);
|
| 103 |
+
const [currentModel, setCurrentModel] = useState('mlp');
|
| 104 |
+
const [flowStep, setFlowStep] = useState(FLOW_STEPS.ready);
|
| 105 |
+
const [cameraReady, setCameraReady] = useState(false);
|
| 106 |
+
const [isStarting, setIsStarting] = useState(false);
|
| 107 |
+
const [focusState, setFocusState] = useState(FOCUS_STATES.pending);
|
| 108 |
+
const [cameraError, setCameraError] = useState('');
|
| 109 |
+
const [calibration, setCalibration] = useState(null);
|
| 110 |
+
const [l2csBoost, setL2csBoost] = useState(false);
|
| 111 |
+
const [l2csBoostAvailable, setL2csBoostAvailable] = useState(false);
|
| 112 |
+
const [showEyeGazeModal, setShowEyeGazeModal] = useState(false);
|
| 113 |
+
const [eyeGazeDontShow, setEyeGazeDontShow] = useState(
|
| 114 |
+
() => localStorage.getItem('focusguard_eyegaze_skipmodal') === 'true'
|
| 115 |
+
);
|
| 116 |
+
|
| 117 |
+
const localVideoRef = useRef(null);
|
| 118 |
+
const displayCanvasRef = useRef(null);
|
| 119 |
+
const pipVideoRef = useRef(null);
|
| 120 |
+
const pipStreamRef = useRef(null);
|
| 121 |
+
const previewFrameRef = useRef(null);
|
| 122 |
+
|
| 123 |
+
useEffect(() => {
|
| 124 |
+
if (isTutorialActive) {
|
| 125 |
+
setFlowStep(FLOW_STEPS.intro);
|
| 126 |
+
} else {
|
| 127 |
+
setFlowStep(FLOW_STEPS.ready);
|
| 128 |
+
}
|
| 129 |
+
}, [isTutorialActive]);
|
| 130 |
+
|
| 131 |
+
const formatDuration = (seconds) => {
|
| 132 |
+
if (seconds === 0) return '0s';
|
| 133 |
+
const mins = Math.floor(seconds / 60);
|
| 134 |
+
const secs = Math.floor(seconds % 60);
|
| 135 |
+
return `${mins}m ${secs}s`;
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
const stopPreviewLoop = () => {
|
| 139 |
+
if (previewFrameRef.current) {
|
| 140 |
+
cancelAnimationFrame(previewFrameRef.current);
|
| 141 |
+
previewFrameRef.current = null;
|
| 142 |
+
}
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const startPreviewLoop = () => {
|
| 146 |
+
stopPreviewLoop();
|
| 147 |
+
const renderPreview = () => {
|
| 148 |
+
const canvas = displayCanvasRef.current;
|
| 149 |
+
const video = localVideoRef.current;
|
| 150 |
+
|
| 151 |
+
if (!canvas || !video || !cameraReady || videoManager?.isStreaming) {
|
| 152 |
+
previewFrameRef.current = null;
|
| 153 |
+
return;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if (video.readyState >= 2) {
|
| 157 |
+
const ctx = canvas.getContext('2d');
|
| 158 |
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
previewFrameRef.current = requestAnimationFrame(renderPreview);
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
previewFrameRef.current = requestAnimationFrame(renderPreview);
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
const getErrorMessage = (err) => {
|
| 168 |
+
if (err?.name === 'NotAllowedError') {
|
| 169 |
+
return 'Camera permission denied. Please allow camera access.';
|
| 170 |
+
}
|
| 171 |
+
if (err?.name === 'NotFoundError') {
|
| 172 |
+
return 'No camera found. Please connect a camera.';
|
| 173 |
+
}
|
| 174 |
+
if (err?.name === 'NotReadableError') {
|
| 175 |
+
return 'Camera is already in use by another application.';
|
| 176 |
+
}
|
| 177 |
+
if (err?.target?.url) {
|
| 178 |
+
return `WebSocket connection failed: ${err.target.url}. Check that the backend server is running.`;
|
| 179 |
+
}
|
| 180 |
+
return err?.message || 'Failed to start focus session.';
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
useEffect(() => {
|
| 184 |
+
if (!videoManager) return;
|
| 185 |
+
|
| 186 |
+
const originalOnStatusUpdate = videoManager.callbacks.onStatusUpdate;
|
| 187 |
+
const originalOnSessionEnd = videoManager.callbacks.onSessionEnd;
|
| 188 |
+
|
| 189 |
+
videoManager.callbacks.onStatusUpdate = (isFocused) => {
|
| 190 |
+
setTimelineEvents((prev) => {
|
| 191 |
+
const newEvents = [...prev, { isFocused, timestamp: Date.now() }];
|
| 192 |
+
if (newEvents.length > 60) newEvents.shift();
|
| 193 |
+
return newEvents;
|
| 194 |
+
});
|
| 195 |
+
setFocusState(isFocused ? FOCUS_STATES.focused : FOCUS_STATES.notFocused);
|
| 196 |
+
if (originalOnStatusUpdate) originalOnStatusUpdate(isFocused);
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
videoManager.callbacks.onSessionEnd = (summary) => {
|
| 200 |
+
setFocusState(FOCUS_STATES.pending);
|
| 201 |
+
setCameraReady(false);
|
| 202 |
+
if (originalOnSessionEnd) originalOnSessionEnd(summary);
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
videoManager.callbacks.onCalibrationUpdate = (cal) => {
|
| 206 |
+
setCalibration(cal && cal.active ? { ...cal } : null);
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
const statsInterval = setInterval(() => {
|
| 210 |
+
if (videoManager && videoManager.getStats) {
|
| 211 |
+
setStats(videoManager.getStats());
|
| 212 |
+
}
|
| 213 |
+
}, 1000);
|
| 214 |
+
|
| 215 |
+
return () => {
|
| 216 |
+
if (videoManager) {
|
| 217 |
+
videoManager.callbacks.onStatusUpdate = originalOnStatusUpdate;
|
| 218 |
+
videoManager.callbacks.onSessionEnd = originalOnSessionEnd;
|
| 219 |
+
videoManager.callbacks.onCalibrationUpdate = null;
|
| 220 |
+
}
|
| 221 |
+
clearInterval(statsInterval);
|
| 222 |
+
};
|
| 223 |
+
}, [videoManager]);
|
| 224 |
+
|
| 225 |
+
useEffect(() => {
|
| 226 |
+
fetch('/api/models')
|
| 227 |
+
.then((res) => res.json())
|
| 228 |
+
.then((data) => {
|
| 229 |
+
if (data.available) setAvailableModels(data.available);
|
| 230 |
+
if (data.current) {
|
| 231 |
+
if (data.current === 'l2cs') {
|
| 232 |
+
const fallback = data.available.find((m) => m !== 'l2cs') || 'mlp';
|
| 233 |
+
setCurrentModel(fallback);
|
| 234 |
+
handleModelChange(fallback);
|
| 235 |
+
} else {
|
| 236 |
+
setCurrentModel(data.current);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
if (data.l2cs_boost !== undefined) setL2csBoost(data.l2cs_boost);
|
| 240 |
+
if (data.l2cs_boost_available !== undefined) setL2csBoostAvailable(data.l2cs_boost_available);
|
| 241 |
+
})
|
| 242 |
+
.catch((err) => console.error('Failed to fetch models:', err));
|
| 243 |
+
}, []);
|
| 244 |
+
|
| 245 |
+
useEffect(() => {
|
| 246 |
+
if (flowStep === FLOW_STEPS.ready && cameraReady && !videoManager?.isStreaming) {
|
| 247 |
+
startPreviewLoop();
|
| 248 |
+
return;
|
| 249 |
+
}
|
| 250 |
+
stopPreviewLoop();
|
| 251 |
+
}, [cameraReady, flowStep, videoManager?.isStreaming]);
|
| 252 |
+
|
| 253 |
+
useEffect(() => {
|
| 254 |
+
if (!isActive) {
|
| 255 |
+
stopPreviewLoop();
|
| 256 |
+
}
|
| 257 |
+
}, [isActive]);
|
| 258 |
+
|
| 259 |
+
useEffect(() => {
|
| 260 |
+
return () => {
|
| 261 |
+
stopPreviewLoop();
|
| 262 |
+
if (pipVideoRef.current) {
|
| 263 |
+
pipVideoRef.current.pause();
|
| 264 |
+
pipVideoRef.current.srcObject = null;
|
| 265 |
+
}
|
| 266 |
+
if (pipStreamRef.current) {
|
| 267 |
+
pipStreamRef.current.getTracks().forEach((t) => t.stop());
|
| 268 |
+
pipStreamRef.current = null;
|
| 269 |
+
}
|
| 270 |
+
};
|
| 271 |
+
}, []);
|
| 272 |
+
|
| 273 |
+
useEffect(() => {
|
| 274 |
+
const fetchSystem = () => {
|
| 275 |
+
fetch('/api/stats/system')
|
| 276 |
+
.then(res => res.json())
|
| 277 |
+
.then(data => setSystemStats(data))
|
| 278 |
+
.catch(() => setSystemStats(null));
|
| 279 |
+
};
|
| 280 |
+
fetchSystem();
|
| 281 |
+
const interval = setInterval(fetchSystem, 3000);
|
| 282 |
+
return () => clearInterval(interval);
|
| 283 |
+
}, []);
|
| 284 |
+
|
| 285 |
+
const handleModelChange = async (modelName) => {
|
| 286 |
+
try {
|
| 287 |
+
const res = await fetch('/api/settings', {
|
| 288 |
+
method: 'PUT',
|
| 289 |
+
headers: { 'Content-Type': 'application/json' },
|
| 290 |
+
body: JSON.stringify({ model_name: modelName })
|
| 291 |
+
});
|
| 292 |
+
const result = await res.json();
|
| 293 |
+
if (result.updated) {
|
| 294 |
+
setCurrentModel(modelName);
|
| 295 |
+
}
|
| 296 |
+
} catch (err) {
|
| 297 |
+
console.error('Failed to switch model:', err);
|
| 298 |
+
}
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
const closeTutorial = () => {
|
| 302 |
+
setFlowStep(FLOW_STEPS.ready);
|
| 303 |
+
setIsTutorialActive(false);
|
| 304 |
+
setHasSeenTutorial(true);
|
| 305 |
+
};
|
| 306 |
+
|
| 307 |
+
const handleEnableCamera = async () => {
|
| 308 |
+
if (!videoManager) return;
|
| 309 |
+
try {
|
| 310 |
+
setCameraError('');
|
| 311 |
+
await videoManager.initCamera(localVideoRef.current, displayCanvasRef.current);
|
| 312 |
+
setCameraReady(true);
|
| 313 |
+
closeTutorial();
|
| 314 |
+
setFocusState(FOCUS_STATES.pending);
|
| 315 |
+
} catch (err) {
|
| 316 |
+
const errorMessage = getErrorMessage(err);
|
| 317 |
+
setCameraError(errorMessage);
|
| 318 |
+
console.error('Camera init error:', err);
|
| 319 |
+
}
|
| 320 |
+
};
|
| 321 |
+
|
| 322 |
+
const applyEyeGazeChange = async (enable, withCalibration = true) => {
|
| 323 |
+
try {
|
| 324 |
+
const res = await fetch('/api/settings', {
|
| 325 |
+
method: 'PUT',
|
| 326 |
+
headers: { 'Content-Type': 'application/json' },
|
| 327 |
+
body: JSON.stringify({ l2cs_boost: enable })
|
| 328 |
+
});
|
| 329 |
+
if (!res.ok) return;
|
| 330 |
+
setL2csBoost(enable);
|
| 331 |
+
|
| 332 |
+
if (enable && withCalibration && videoManager && videoManager.isStreaming) {
|
| 333 |
+
videoManager.startCalibration();
|
| 334 |
+
} else if (!enable && videoManager) {
|
| 335 |
+
videoManager.cancelCalibration();
|
| 336 |
+
}
|
| 337 |
+
} catch (err) {
|
| 338 |
+
console.error('Failed to toggle eye gaze:', err);
|
| 339 |
+
}
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
const handleEyeGazeToggle = async () => {
|
| 343 |
+
const next = !l2csBoost;
|
| 344 |
+
if (next && !eyeGazeDontShow) {
|
| 345 |
+
setShowEyeGazeModal(true);
|
| 346 |
+
return;
|
| 347 |
+
}
|
| 348 |
+
await applyEyeGazeChange(next, false);
|
| 349 |
+
};
|
| 350 |
+
|
| 351 |
+
const handleEyeGazeModalAction = async (withCalibration) => {
|
| 352 |
+
if (eyeGazeDontShow) {
|
| 353 |
+
localStorage.setItem('focusguard_eyegaze_skipmodal', 'true');
|
| 354 |
+
}
|
| 355 |
+
setShowEyeGazeModal(false);
|
| 356 |
+
await applyEyeGazeChange(true, withCalibration);
|
| 357 |
+
};
|
| 358 |
+
|
| 359 |
+
const handleStart = async () => {
|
| 360 |
+
try {
|
| 361 |
+
setIsStarting(true);
|
| 362 |
+
setSessionResult(null);
|
| 363 |
+
setTimelineEvents([]);
|
| 364 |
+
setFocusState(FOCUS_STATES.pending);
|
| 365 |
+
setCameraError('');
|
| 366 |
+
|
| 367 |
+
if (!cameraReady) {
|
| 368 |
+
await videoManager.initCamera(localVideoRef.current, displayCanvasRef.current);
|
| 369 |
+
setCameraReady(true);
|
| 370 |
+
setFlowStep(FLOW_STEPS.ready);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
await videoManager.startStreaming();
|
| 374 |
+
} catch (err) {
|
| 375 |
+
const errorMessage = getErrorMessage(err);
|
| 376 |
+
setCameraError(errorMessage);
|
| 377 |
+
setFocusState(FOCUS_STATES.pending);
|
| 378 |
+
console.error('Start error:', err);
|
| 379 |
+
alert(`Failed to start: ${errorMessage}\n\nCheck browser console for details.`);
|
| 380 |
+
} finally {
|
| 381 |
+
setIsStarting(false);
|
| 382 |
+
}
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
const handleStop = async () => {
|
| 386 |
+
if (videoManager) {
|
| 387 |
+
await videoManager.stopStreaming();
|
| 388 |
+
}
|
| 389 |
+
try {
|
| 390 |
+
if (document.pictureInPictureElement === pipVideoRef.current) {
|
| 391 |
+
await document.exitPictureInPicture();
|
| 392 |
+
}
|
| 393 |
+
} catch (_) {}
|
| 394 |
+
if (pipVideoRef.current) {
|
| 395 |
+
pipVideoRef.current.pause();
|
| 396 |
+
pipVideoRef.current.srcObject = null;
|
| 397 |
+
}
|
| 398 |
+
if (pipStreamRef.current) {
|
| 399 |
+
pipStreamRef.current.getTracks().forEach((t) => t.stop());
|
| 400 |
+
pipStreamRef.current = null;
|
| 401 |
+
}
|
| 402 |
+
stopPreviewLoop();
|
| 403 |
+
setFocusState(FOCUS_STATES.pending);
|
| 404 |
+
setCameraReady(false);
|
| 405 |
+
};
|
| 406 |
+
|
| 407 |
+
const handlePiP = async () => {
|
| 408 |
+
try {
|
| 409 |
+
if (!videoManager || !videoManager.isStreaming) {
|
| 410 |
+
alert('Please start the video first.');
|
| 411 |
+
return;
|
| 412 |
+
}
|
| 413 |
+
if (!displayCanvasRef.current) {
|
| 414 |
+
alert('Video not ready.');
|
| 415 |
+
return;
|
| 416 |
+
}
|
| 417 |
+
if (document.pictureInPictureElement === pipVideoRef.current) {
|
| 418 |
+
await document.exitPictureInPicture();
|
| 419 |
+
console.log('PiP exited');
|
| 420 |
+
return;
|
| 421 |
+
}
|
| 422 |
+
if (!document.pictureInPictureEnabled) {
|
| 423 |
+
alert('Picture-in-Picture is not supported in this browser.');
|
| 424 |
+
return;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
const pipVideo = pipVideoRef.current;
|
| 428 |
+
if (!pipVideo) {
|
| 429 |
+
alert('PiP video element not ready.');
|
| 430 |
+
return;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
const isSafariPiP = typeof pipVideo.webkitSetPresentationMode === 'function';
|
| 434 |
+
let stream = pipStreamRef.current;
|
| 435 |
+
if (!stream) {
|
| 436 |
+
const capture = displayCanvasRef.current.captureStream;
|
| 437 |
+
if (typeof capture === 'function') {
|
| 438 |
+
stream = capture.call(displayCanvasRef.current, 30);
|
| 439 |
+
}
|
| 440 |
+
if (!stream || stream.getTracks().length === 0) {
|
| 441 |
+
const cameraStream = localVideoRef.current?.srcObject;
|
| 442 |
+
if (!cameraStream) {
|
| 443 |
+
alert('Camera stream not ready.');
|
| 444 |
+
return;
|
| 445 |
+
}
|
| 446 |
+
stream = cameraStream;
|
| 447 |
+
}
|
| 448 |
+
pipStreamRef.current = stream;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
if (!stream || stream.getTracks().length === 0) {
|
| 452 |
+
alert('Failed to capture video stream from canvas.');
|
| 453 |
+
return;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
pipVideo.srcObject = stream;
|
| 457 |
+
if (pipVideo.readyState < 2) {
|
| 458 |
+
await new Promise((resolve) => {
|
| 459 |
+
const onReady = () => {
|
| 460 |
+
pipVideo.removeEventListener('loadeddata', onReady);
|
| 461 |
+
pipVideo.removeEventListener('canplay', onReady);
|
| 462 |
+
resolve();
|
| 463 |
+
};
|
| 464 |
+
pipVideo.addEventListener('loadeddata', onReady);
|
| 465 |
+
pipVideo.addEventListener('canplay', onReady);
|
| 466 |
+
setTimeout(resolve, 600);
|
| 467 |
+
});
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
try {
|
| 471 |
+
await pipVideo.play();
|
| 472 |
+
} catch (_) {}
|
| 473 |
+
|
| 474 |
+
if (isSafariPiP) {
|
| 475 |
+
try {
|
| 476 |
+
pipVideo.webkitSetPresentationMode('picture-in-picture');
|
| 477 |
+
console.log('PiP activated (Safari)');
|
| 478 |
+
return;
|
| 479 |
+
} catch (e) {
|
| 480 |
+
const cameraStream = localVideoRef.current?.srcObject;
|
| 481 |
+
if (cameraStream && cameraStream !== pipVideo.srcObject) {
|
| 482 |
+
pipVideo.srcObject = cameraStream;
|
| 483 |
+
try {
|
| 484 |
+
await pipVideo.play();
|
| 485 |
+
} catch (_) {}
|
| 486 |
+
pipVideo.webkitSetPresentationMode('picture-in-picture');
|
| 487 |
+
console.log('PiP activated (Safari fallback)');
|
| 488 |
+
return;
|
| 489 |
+
}
|
| 490 |
+
throw e;
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
if (typeof pipVideo.requestPictureInPicture === 'function') {
|
| 495 |
+
await pipVideo.requestPictureInPicture();
|
| 496 |
+
console.log('PiP activated');
|
| 497 |
+
} else {
|
| 498 |
+
alert('Picture-in-Picture is not supported in this browser.');
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
} catch (err) {
|
| 502 |
+
console.error('PiP error:', err);
|
| 503 |
+
alert(`Failed to enter Picture-in-Picture: ${err.message}`);
|
| 504 |
+
}
|
| 505 |
+
};
|
| 506 |
+
|
| 507 |
+
const handleFloatingWindow = () => {
|
| 508 |
+
handlePiP();
|
| 509 |
+
};
|
| 510 |
+
|
| 511 |
+
const handleFrameChange = (val) => {
|
| 512 |
+
const rate = parseInt(val, 10);
|
| 513 |
+
setCurrentFrame(rate);
|
| 514 |
+
if (videoManager) {
|
| 515 |
+
videoManager.setFrameRate(rate);
|
| 516 |
+
}
|
| 517 |
+
};
|
| 518 |
+
|
| 519 |
+
const handlePreview = () => {
|
| 520 |
+
if (!videoManager || !videoManager.isStreaming) {
|
| 521 |
+
alert('Please start a session first.');
|
| 522 |
+
return;
|
| 523 |
+
}
|
| 524 |
+
const currentStats = videoManager.getStats();
|
| 525 |
+
if (!currentStats.sessionId) {
|
| 526 |
+
alert('No active session.');
|
| 527 |
+
return;
|
| 528 |
+
}
|
| 529 |
+
const sessionDuration = Math.floor((Date.now() - (videoManager.sessionStartTime || Date.now())) / 1000);
|
| 530 |
+
const totalFrames = currentStats.framesProcessed || 0;
|
| 531 |
+
const focusedFrames = currentStats.focusedFrames ?? 0;
|
| 532 |
+
const focusScore = totalFrames > 0 ? focusedFrames / totalFrames : 0;
|
| 533 |
+
|
| 534 |
+
setSessionResult({
|
| 535 |
+
duration_seconds: sessionDuration,
|
| 536 |
+
focus_score: focusScore,
|
| 537 |
+
total_frames: totalFrames,
|
| 538 |
+
focused_frames: focusedFrames
|
| 539 |
+
});
|
| 540 |
+
};
|
| 541 |
+
|
| 542 |
+
const handleCloseOverlay = () => {
|
| 543 |
+
setSessionResult(null);
|
| 544 |
+
};
|
| 545 |
+
|
| 546 |
+
const pageStyle = isActive
|
| 547 |
+
? undefined
|
| 548 |
+
: {
|
| 549 |
+
position: 'absolute',
|
| 550 |
+
width: '1px',
|
| 551 |
+
height: '1px',
|
| 552 |
+
overflow: 'hidden',
|
| 553 |
+
opacity: 0,
|
| 554 |
+
pointerEvents: 'none'
|
| 555 |
+
};
|
| 556 |
+
|
| 557 |
+
const focusStateLabel = {
|
| 558 |
+
[FOCUS_STATES.pending]: 'Pending',
|
| 559 |
+
[FOCUS_STATES.focused]: 'Focused',
|
| 560 |
+
[FOCUS_STATES.notFocused]: 'Not Focused'
|
| 561 |
+
}[focusState];
|
| 562 |
+
|
| 563 |
+
const introHighlights = [
|
| 564 |
+
{
|
| 565 |
+
title: 'Live focus tracking',
|
| 566 |
+
text: 'Head pose, gaze, and eye openness are read continuously during the session.'
|
| 567 |
+
},
|
| 568 |
+
{
|
| 569 |
+
title: 'Quick setup',
|
| 570 |
+
text: 'Front-facing light and a stable camera angle give the cleanest preview.'
|
| 571 |
+
},
|
| 572 |
+
{
|
| 573 |
+
title: 'Private by default',
|
| 574 |
+
text: 'Only session metadata is stored, not the raw camera footage.'
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
title: 'Sync across devices',
|
| 578 |
+
text: 'Your history auto-saves to this browser. To switch devices, use the Data Management tools at the bottom of the My Records tab to export or import your data.'
|
| 579 |
+
}
|
| 580 |
+
];
|
| 581 |
+
|
| 582 |
+
const permissionSteps = [
|
| 583 |
+
{
|
| 584 |
+
title: 'Allow browser access',
|
| 585 |
+
text: 'Approve the camera prompt so the preview can appear immediately.'
|
| 586 |
+
},
|
| 587 |
+
{
|
| 588 |
+
title: 'Check your framing',
|
| 589 |
+
text: 'Keep your face visible and centered for more stable landmark detection.'
|
| 590 |
+
},
|
| 591 |
+
{
|
| 592 |
+
title: 'Start when ready',
|
| 593 |
+
text: 'After the preview appears, use the page controls to begin or stop.'
|
| 594 |
+
}
|
| 595 |
+
];
|
| 596 |
+
|
| 597 |
+
const renderIntroCard = () => {
|
| 598 |
+
if (flowStep === FLOW_STEPS.intro) {
|
| 599 |
+
return (
|
| 600 |
+
<div className="focus-flow-overlay">
|
| 601 |
+
<div className="focus-flow-card">
|
| 602 |
+
<div className="focus-flow-header">
|
| 603 |
+
<div>
|
| 604 |
+
<div className="focus-flow-eyebrow">Focus Session</div>
|
| 605 |
+
<h2>Before you begin</h2>
|
| 606 |
+
</div>
|
| 607 |
+
<div className="focus-flow-icon">
|
| 608 |
+
<HelloIcon />
|
| 609 |
+
</div>
|
| 610 |
+
</div>
|
| 611 |
+
|
| 612 |
+
<p className="focus-flow-lead">
|
| 613 |
+
The focus page uses your live camera preview to estimate attention in real time.
|
| 614 |
+
Review the setup notes below, then continue to camera access.
|
| 615 |
+
</p>
|
| 616 |
+
|
| 617 |
+
<div className="focus-flow-grid">
|
| 618 |
+
{introHighlights.map((item) => (
|
| 619 |
+
<article key={item.title} className="focus-flow-panel">
|
| 620 |
+
<h3>{item.title}</h3>
|
| 621 |
+
<p>{item.text}</p>
|
| 622 |
+
</article>
|
| 623 |
+
))}
|
| 624 |
+
</div>
|
| 625 |
+
|
| 626 |
+
<div className="focus-flow-glasses-note">
|
| 627 |
+
<strong>Wearing glasses?</strong> Glasses may reduce detection accuracy on some models. If results seem inaccurate, try switching to a different model (e.g. Geometric or MLP).
|
| 628 |
+
</div>
|
| 629 |
+
|
| 630 |
+
<div className="focus-flow-footer">
|
| 631 |
+
<div className="focus-flow-note">
|
| 632 |
+
You can still change frame rate and available model options after the preview loads.
|
| 633 |
+
</div>
|
| 634 |
+
<div style={{ display: 'flex', gap: '10px' }}>
|
| 635 |
+
<button className="focus-flow-secondary" onClick={closeTutorial}>
|
| 636 |
+
Skip
|
| 637 |
+
</button>
|
| 638 |
+
<button className="focus-flow-button" onClick={() => setFlowStep(FLOW_STEPS.permission)}>
|
| 639 |
+
Continue
|
| 640 |
+
</button>
|
| 641 |
+
</div>
|
| 642 |
+
</div>
|
| 643 |
+
</div>
|
| 644 |
+
</div>
|
| 645 |
+
);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
if (flowStep === FLOW_STEPS.permission && !cameraReady) {
|
| 649 |
+
return (
|
| 650 |
+
<div className="focus-flow-overlay">
|
| 651 |
+
<div className="focus-flow-card">
|
| 652 |
+
<div className="focus-flow-header">
|
| 653 |
+
<div>
|
| 654 |
+
<div className="focus-flow-eyebrow">Camera Setup</div>
|
| 655 |
+
<h2>Enable camera access</h2>
|
| 656 |
+
</div>
|
| 657 |
+
<div className="focus-flow-icon">
|
| 658 |
+
<CameraIcon />
|
| 659 |
+
</div>
|
| 660 |
+
</div>
|
| 661 |
+
|
| 662 |
+
<p className="focus-flow-lead">
|
| 663 |
+
Once access is granted, your preview appears here and the rest of the Focus page
|
| 664 |
+
behaves like the other dashboard screens.
|
| 665 |
+
</p>
|
| 666 |
+
|
| 667 |
+
<div className="focus-flow-steps">
|
| 668 |
+
{permissionSteps.map((item, index) => (
|
| 669 |
+
<div key={item.title} className="focus-flow-step">
|
| 670 |
+
<div className="focus-flow-step-number">{index + 1}</div>
|
| 671 |
+
<div className="focus-flow-step-copy">
|
| 672 |
+
<h3>{item.title}</h3>
|
| 673 |
+
<p>{item.text}</p>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
))}
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
{cameraError ? <div className="focus-inline-error">{cameraError}</div> : null}
|
| 680 |
+
|
| 681 |
+
<div className="focus-flow-footer">
|
| 682 |
+
<button
|
| 683 |
+
type="button"
|
| 684 |
+
className="focus-flow-secondary"
|
| 685 |
+
onClick={() => setFlowStep(FLOW_STEPS.intro)}
|
| 686 |
+
>
|
| 687 |
+
Back
|
| 688 |
+
</button>
|
| 689 |
+
<button className="focus-flow-button" onClick={handleEnableCamera}>
|
| 690 |
+
Enable Camera
|
| 691 |
+
</button>
|
| 692 |
+
</div>
|
| 693 |
+
</div>
|
| 694 |
+
</div>
|
| 695 |
+
);
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
return null;
|
| 699 |
+
};
|
| 700 |
+
|
| 701 |
+
const renderEyeGazeModal = () => {
|
| 702 |
+
if (!showEyeGazeModal) return null;
|
| 703 |
+
return (
|
| 704 |
+
<div className="focus-flow-overlay" style={{ zIndex: 2000 }}>
|
| 705 |
+
<div className="focus-flow-card">
|
| 706 |
+
<div className="focus-flow-header">
|
| 707 |
+
<div>
|
| 708 |
+
<div className="focus-flow-eyebrow">Eye Gaze Tracking</div>
|
| 709 |
+
<h2>Before you enable</h2>
|
| 710 |
+
</div>
|
| 711 |
+
<div className="focus-flow-icon">
|
| 712 |
+
<svg width="96" height="96" viewBox="0 0 96 96" aria-hidden="true">
|
| 713 |
+
<ellipse cx="48" cy="48" rx="38" ry="24" fill="none" stroke="#007BFF" strokeWidth="5" />
|
| 714 |
+
<circle cx="48" cy="48" r="13" fill="none" stroke="#007BFF" strokeWidth="5" />
|
| 715 |
+
<circle cx="48" cy="48" r="5" fill="#007BFF" />
|
| 716 |
+
</svg>
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
|
| 720 |
+
<p className="focus-flow-lead">
|
| 721 |
+
Eye gaze tracking runs an additional deep neural network (L2CS-Net) alongside your current model.
|
| 722 |
+
Please read the notes below before proceeding.
|
| 723 |
+
</p>
|
| 724 |
+
|
| 725 |
+
<div className="focus-flow-grid">
|
| 726 |
+
<article className="focus-flow-panel focus-flow-panel-warn">
|
| 727 |
+
<h3>Performance impact</h3>
|
| 728 |
+
<p>Enabling eye gaze tracking increases CPU usage and may reduce frame rate. If the system feels sluggish, consider disabling it.</p>
|
| 729 |
+
</article>
|
| 730 |
+
<article className="focus-flow-panel">
|
| 731 |
+
<h3>Calibration (recommended)</h3>
|
| 732 |
+
<p>For best accuracy, calibrate by looking at 9 screen positions one at a time, followed by 1 validation point. The whole process takes about 30 seconds.</p>
|
| 733 |
+
</article>
|
| 734 |
+
</div>
|
| 735 |
+
|
| 736 |
+
<div className="focus-flow-steps">
|
| 737 |
+
<div className="focus-flow-step">
|
| 738 |
+
<div className="focus-flow-step-number">1</div>
|
| 739 |
+
<div className="focus-flow-step-copy">
|
| 740 |
+
<h3>Click "Start Calibration"</h3>
|
| 741 |
+
<p>A dot will appear on screen. Look directly at it and keep your gaze steady. It will cycle through 9 positions then show a final validation dot.</p>
|
| 742 |
+
</div>
|
| 743 |
+
</div>
|
| 744 |
+
<div className="focus-flow-step">
|
| 745 |
+
<div className="focus-flow-step-number">2</div>
|
| 746 |
+
<div className="focus-flow-step-copy">
|
| 747 |
+
<h3>Or skip for now</h3>
|
| 748 |
+
<p>Click "Skip" to enable eye gaze tracking without calibrating. You can recalibrate at any time using the "Recalibrate" button during a session.</p>
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
</div>
|
| 752 |
+
|
| 753 |
+
<label className="eye-gaze-modal-checkbox">
|
| 754 |
+
<input
|
| 755 |
+
type="checkbox"
|
| 756 |
+
checked={eyeGazeDontShow}
|
| 757 |
+
onChange={(e) => setEyeGazeDontShow(e.target.checked)}
|
| 758 |
+
/>
|
| 759 |
+
Don't show this again
|
| 760 |
+
</label>
|
| 761 |
+
|
| 762 |
+
<div className="focus-flow-footer">
|
| 763 |
+
<button
|
| 764 |
+
type="button"
|
| 765 |
+
className="focus-flow-secondary"
|
| 766 |
+
onClick={() => handleEyeGazeModalAction(false)}
|
| 767 |
+
>
|
| 768 |
+
Skip
|
| 769 |
+
</button>
|
| 770 |
+
<button
|
| 771 |
+
className="focus-flow-button"
|
| 772 |
+
onClick={() => handleEyeGazeModalAction(true)}
|
| 773 |
+
>
|
| 774 |
+
Start Calibration
|
| 775 |
+
</button>
|
| 776 |
+
</div>
|
| 777 |
+
</div>
|
| 778 |
+
</div>
|
| 779 |
+
);
|
| 780 |
+
};
|
| 781 |
+
|
| 782 |
+
return (
|
| 783 |
+
<main id="page-b" className="page" style={pageStyle}>
|
| 784 |
+
{renderIntroCard()}
|
| 785 |
+
{renderEyeGazeModal()}
|
| 786 |
+
|
| 787 |
+
<section id="display-area" className="focus-display-shell">
|
| 788 |
+
<video
|
| 789 |
+
ref={pipVideoRef}
|
| 790 |
+
muted
|
| 791 |
+
playsInline
|
| 792 |
+
autoPlay
|
| 793 |
+
style={{
|
| 794 |
+
position: 'absolute',
|
| 795 |
+
width: '1px',
|
| 796 |
+
height: '1px',
|
| 797 |
+
opacity: 0,
|
| 798 |
+
pointerEvents: 'none'
|
| 799 |
+
}}
|
| 800 |
+
/>
|
| 801 |
+
<video
|
| 802 |
+
ref={localVideoRef}
|
| 803 |
+
muted
|
| 804 |
+
playsInline
|
| 805 |
+
autoPlay
|
| 806 |
+
style={{ display: 'none' }}
|
| 807 |
+
/>
|
| 808 |
+
|
| 809 |
+
<canvas
|
| 810 |
+
ref={displayCanvasRef}
|
| 811 |
+
width={640}
|
| 812 |
+
height={480}
|
| 813 |
+
style={{
|
| 814 |
+
width: '100%',
|
| 815 |
+
height: '100%',
|
| 816 |
+
objectFit: 'contain',
|
| 817 |
+
backgroundColor: '#101010'
|
| 818 |
+
}}
|
| 819 |
+
/>
|
| 820 |
+
|
| 821 |
+
{flowStep === FLOW_STEPS.ready ? (
|
| 822 |
+
<>
|
| 823 |
+
<div className={`focus-state-pill ${focusState}`}>
|
| 824 |
+
<span className="focus-state-dot" />
|
| 825 |
+
{focusStateLabel}
|
| 826 |
+
</div>
|
| 827 |
+
{!cameraReady && !videoManager?.isStreaming ? (
|
| 828 |
+
<div className="focus-idle-overlay">
|
| 829 |
+
<p>Camera is paused.</p>
|
| 830 |
+
<span>Use Start to enable the camera and begin detection.</span>
|
| 831 |
+
</div>
|
| 832 |
+
) : null}
|
| 833 |
+
</>
|
| 834 |
+
) : null}
|
| 835 |
+
|
| 836 |
+
{sessionResult && (
|
| 837 |
+
<div className="session-result-overlay">
|
| 838 |
+
<h3>Session Complete!</h3>
|
| 839 |
+
<div className="result-item">
|
| 840 |
+
<span className="label">Duration:</span>
|
| 841 |
+
<span className="value">{formatDuration(sessionResult.duration_seconds)}</span>
|
| 842 |
+
</div>
|
| 843 |
+
<div className="result-item">
|
| 844 |
+
<span className="label">Focus Score:</span>
|
| 845 |
+
<span className="value">{(sessionResult.focus_score * 100).toFixed(1)}%</span>
|
| 846 |
+
</div>
|
| 847 |
+
|
| 848 |
+
<button
|
| 849 |
+
onClick={handleCloseOverlay}
|
| 850 |
+
style={{
|
| 851 |
+
marginTop: '20px',
|
| 852 |
+
padding: '8px 20px',
|
| 853 |
+
background: 'transparent',
|
| 854 |
+
border: '1px solid white',
|
| 855 |
+
color: 'white',
|
| 856 |
+
borderRadius: '20px',
|
| 857 |
+
cursor: 'pointer'
|
| 858 |
+
}}
|
| 859 |
+
>
|
| 860 |
+
Close
|
| 861 |
+
</button>
|
| 862 |
+
</div>
|
| 863 |
+
)}
|
| 864 |
+
|
| 865 |
+
</section>
|
| 866 |
+
|
| 867 |
+
{flowStep === FLOW_STEPS.ready ? (
|
| 868 |
+
<>
|
| 869 |
+
{availableModels.length > 0 ? (
|
| 870 |
+
<section className="focus-model-strip">
|
| 871 |
+
<span className="focus-model-label">Model:</span>
|
| 872 |
+
{MODEL_ORDER.filter((n) => availableModels.includes(n)).map((name) => (
|
| 873 |
+
<button
|
| 874 |
+
key={name}
|
| 875 |
+
onClick={() => handleModelChange(name)}
|
| 876 |
+
className={`focus-model-button ${currentModel === name ? 'active' : ''}`}
|
| 877 |
+
>
|
| 878 |
+
{MODEL_INFO[name]?.label || name}
|
| 879 |
+
</button>
|
| 880 |
+
))}
|
| 881 |
+
|
| 882 |
+
{l2csBoostAvailable && (
|
| 883 |
+
<>
|
| 884 |
+
<span className="focus-model-sep" />
|
| 885 |
+
<button
|
| 886 |
+
onClick={handleEyeGazeToggle}
|
| 887 |
+
className={`eye-gaze-toggle ${l2csBoost ? 'on' : 'off'}`}
|
| 888 |
+
title={l2csBoost ? 'Eye gaze tracking active — click to disable' : 'Enable eye gaze tracking (requires calibration)'}
|
| 889 |
+
>
|
| 890 |
+
<svg width="16" height="16" viewBox="0 0 16 16" className="eye-gaze-icon" aria-hidden="true">
|
| 891 |
+
<ellipse cx="8" cy="8" rx="7" ry="4.5" fill="none" stroke="currentColor" strokeWidth="1.4" />
|
| 892 |
+
<circle cx="8" cy="8" r="2.2" fill="currentColor" />
|
| 893 |
+
</svg>
|
| 894 |
+
{l2csBoost ? 'Eye Gaze On' : 'Eye Gaze'}
|
| 895 |
+
</button>
|
| 896 |
+
{l2csBoost && stats && stats.isStreaming && (
|
| 897 |
+
<button
|
| 898 |
+
onClick={() => videoManager && videoManager.startCalibration()}
|
| 899 |
+
className="focus-model-button recalibrate"
|
| 900 |
+
title="Re-run gaze calibration"
|
| 901 |
+
>
|
| 902 |
+
Recalibrate
|
| 903 |
+
</button>
|
| 904 |
+
)}
|
| 905 |
+
</>
|
| 906 |
+
)}
|
| 907 |
+
</section>
|
| 908 |
+
) : null}
|
| 909 |
+
|
| 910 |
+
{systemStats && systemStats.cpu_percent != null && (
|
| 911 |
+
<section className="focus-system-stats">
|
| 912 |
+
<span>CPU: <strong>{systemStats.cpu_percent}%</strong></span>
|
| 913 |
+
<span className="focus-system-stats-sep" />
|
| 914 |
+
<span>RAM: <strong>{systemStats.memory_percent}%</strong> ({systemStats.memory_used_mb}/{systemStats.memory_total_mb} MB)</span>
|
| 915 |
+
</section>
|
| 916 |
+
)}
|
| 917 |
+
|
| 918 |
+
<section id="timeline-area">
|
| 919 |
+
<div className="timeline-label">Timeline</div>
|
| 920 |
+
<div id="timeline-visuals">
|
| 921 |
+
{timelineEvents.map((event, index) => (
|
| 922 |
+
<div
|
| 923 |
+
key={index}
|
| 924 |
+
className="timeline-block"
|
| 925 |
+
style={{
|
| 926 |
+
backgroundColor: event.isFocused ? '#43ff6f' : '#ff2c41',
|
| 927 |
+
width: '10px',
|
| 928 |
+
height: '20px',
|
| 929 |
+
borderRadius: '2px',
|
| 930 |
+
flexShrink: 0
|
| 931 |
+
}}
|
| 932 |
+
title={event.isFocused ? 'Focused' : 'Distracted'}
|
| 933 |
+
/>
|
| 934 |
+
))}
|
| 935 |
+
</div>
|
| 936 |
+
<div id="timeline-line" />
|
| 937 |
+
</section>
|
| 938 |
+
|
| 939 |
+
<section id="control-panel">
|
| 940 |
+
<button id="btn-cam-start" className="action-btn green" onClick={handleStart} disabled={isStarting}>
|
| 941 |
+
{isStarting ? 'Starting...' : 'Start'}
|
| 942 |
+
</button>
|
| 943 |
+
|
| 944 |
+
<button id="btn-floating" className="action-btn blue" onClick={handlePiP}>
|
| 945 |
+
Floating Window
|
| 946 |
+
</button>
|
| 947 |
+
|
| 948 |
+
<button id="btn-preview" className="action-btn orange" onClick={handlePreview}>
|
| 949 |
+
Preview Result
|
| 950 |
+
</button>
|
| 951 |
+
|
| 952 |
+
<button id="btn-cam-stop" className="action-btn red" onClick={handleStop}>
|
| 953 |
+
Stop
|
| 954 |
+
</button>
|
| 955 |
+
</section>
|
| 956 |
+
|
| 957 |
+
{cameraError ? (
|
| 958 |
+
<div className="focus-inline-error focus-inline-error-standalone">{cameraError}</div>
|
| 959 |
+
) : null}
|
| 960 |
+
|
| 961 |
+
{MODEL_INFO[currentModel] && (
|
| 962 |
+
<section className="model-card">
|
| 963 |
+
<div className="model-card-header">
|
| 964 |
+
<h3 className="model-card-title">{MODEL_INFO[currentModel].label}</h3>
|
| 965 |
+
{MODEL_INFO[currentModel].badge && (
|
| 966 |
+
<span className={MODEL_INFO[currentModel].badge === 'Baseline' ? 'model-card-badge-baseline' : 'model-card-badge'}>
|
| 967 |
+
{MODEL_INFO[currentModel].badge}
|
| 968 |
+
</span>
|
| 969 |
+
)}
|
| 970 |
+
</div>
|
| 971 |
+
<p className="model-card-tagline">{MODEL_INFO[currentModel].tagline}</p>
|
| 972 |
+
|
| 973 |
+
<div className="model-card-metrics">
|
| 974 |
+
<div className="model-card-metric">
|
| 975 |
+
<span className="model-card-metric-value">{MODEL_INFO[currentModel].accuracy}</span>
|
| 976 |
+
<span className="model-card-metric-label">Accuracy</span>
|
| 977 |
+
</div>
|
| 978 |
+
<div className="model-card-metric">
|
| 979 |
+
<span className="model-card-metric-value">{MODEL_INFO[currentModel].f1}</span>
|
| 980 |
+
<span className="model-card-metric-label">F1 Score</span>
|
| 981 |
+
</div>
|
| 982 |
+
<div className="model-card-metric">
|
| 983 |
+
<span className="model-card-metric-value">{MODEL_INFO[currentModel].auc}</span>
|
| 984 |
+
<span className="model-card-metric-label">ROC-AUC</span>
|
| 985 |
+
</div>
|
| 986 |
+
<div className="model-card-metric">
|
| 987 |
+
<span className="model-card-metric-value">{MODEL_INFO[currentModel].threshold}</span>
|
| 988 |
+
<span className="model-card-metric-label">Threshold</span>
|
| 989 |
+
</div>
|
| 990 |
+
</div>
|
| 991 |
+
|
| 992 |
+
<div className="model-card-details">
|
| 993 |
+
<div className="model-card-section">
|
| 994 |
+
<h4>How it works</h4>
|
| 995 |
+
<p>{MODEL_INFO[currentModel].how}</p>
|
| 996 |
+
</div>
|
| 997 |
+
<div className="model-card-section">
|
| 998 |
+
<h4>Features used</h4>
|
| 999 |
+
<p>{MODEL_INFO[currentModel].features}</p>
|
| 1000 |
+
</div>
|
| 1001 |
+
<div className="model-card-section">
|
| 1002 |
+
<h4>Strengths</h4>
|
| 1003 |
+
<p>{MODEL_INFO[currentModel].strengths}</p>
|
| 1004 |
+
</div>
|
| 1005 |
+
</div>
|
| 1006 |
+
|
| 1007 |
+
<div className="model-card-eval">
|
| 1008 |
+
Evaluated with {MODEL_INFO[currentModel].evaluation}
|
| 1009 |
+
</div>
|
| 1010 |
+
</section>
|
| 1011 |
+
)}
|
| 1012 |
+
|
| 1013 |
+
<section id="frame-control">
|
| 1014 |
+
<label htmlFor="frame-slider">Frame Rate (FPS)</label>
|
| 1015 |
+
<input
|
| 1016 |
+
type="range"
|
| 1017 |
+
id="frame-slider"
|
| 1018 |
+
min="10"
|
| 1019 |
+
max="30"
|
| 1020 |
+
value={currentFrame}
|
| 1021 |
+
onChange={(e) => handleFrameChange(e.target.value)}
|
| 1022 |
+
/>
|
| 1023 |
+
<input
|
| 1024 |
+
type="number"
|
| 1025 |
+
id="frame-input"
|
| 1026 |
+
min="10"
|
| 1027 |
+
max="30"
|
| 1028 |
+
value={currentFrame}
|
| 1029 |
+
onChange={(e) => handleFrameChange(e.target.value)}
|
| 1030 |
+
/>
|
| 1031 |
+
</section>
|
| 1032 |
+
</>
|
| 1033 |
+
) : null}
|
| 1034 |
+
|
| 1035 |
+
<CalibrationOverlay calibration={calibration} videoManager={videoManager} />
|
| 1036 |
+
</main>
|
| 1037 |
+
);
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
export default FocusPageLocal;
|
src/components/Help.jsx
CHANGED
|
@@ -28,11 +28,12 @@ function Help() {
|
|
| 28 |
<h2>How to Use Focus Guard</h2>
|
| 29 |
<ol>
|
| 30 |
<li>Navigate to the Focus page from the menu</li>
|
| 31 |
-
<li>
|
| 32 |
<li>Click the green "Start" button to begin monitoring</li>
|
| 33 |
-
<li>Position yourself in front of the camera</li>
|
| 34 |
<li>The system will track your focus in real-time using face mesh analysis</li>
|
| 35 |
-
<li>Use the model selector to switch between detection models (
|
|
|
|
| 36 |
<li>Click "Stop" when you're done to save the session</li>
|
| 37 |
</ol>
|
| 38 |
</section>
|
|
@@ -51,20 +52,38 @@ function Help() {
|
|
| 51 |
|
| 52 |
<section className="help-section">
|
| 53 |
<h2>Available Models</h2>
|
| 54 |
-
<p><strong>
|
| 55 |
-
<p><strong>XGBoost:</strong> Gradient-boosted tree model using 10 selected features. Strong on tabular data with fast inference.</p>
|
| 56 |
-
<p><strong>
|
| 57 |
-
<p><strong>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
</section>
|
| 59 |
|
| 60 |
<section className="help-section">
|
| 61 |
<h2>Adjusting Settings</h2>
|
| 62 |
-
<p><strong>Frame Rate:</strong> Controls how many frames per second are sent for analysis.
|
| 63 |
-
<p><strong>Model Selection:</strong> Switch models in real-time using the pill buttons above the timeline. Different models may perform better depending on your lighting and
|
|
|
|
| 64 |
</section>
|
| 65 |
|
| 66 |
<section className="help-section">
|
| 67 |
-
<h2>Privacy & Data</h2>
|
| 68 |
<p>Video frames are processed in real-time on the server and are never stored. Only focus status metadata (timestamps, confidence scores) is saved to the session database. View past runs under <strong>My Records</strong>; stats and badges live under <strong>My Achievement</strong>.</p>
|
| 69 |
<p style={{ marginTop: '12px' }}>
|
| 70 |
<button
|
|
@@ -94,13 +113,17 @@ function Help() {
|
|
| 94 |
<summary>Why is my focus score low?</summary>
|
| 95 |
<p>Ensure good lighting so the face mesh can detect your landmarks clearly. Face the camera directly and avoid large head movements. Try switching to a different model if one isn't working well for your setup.</p>
|
| 96 |
</details>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
<details>
|
| 98 |
<summary>Can I use this without a camera?</summary>
|
| 99 |
<p>No, camera access is required. The system relies on real-time face landmark detection to determine focus.</p>
|
| 100 |
</details>
|
| 101 |
<details>
|
| 102 |
<summary>Does this work on mobile?</summary>
|
| 103 |
-
<p>Yes, it works on mobile browsers that support camera access and WebSocket connections. Performance depends on your device and network speed.</p>
|
| 104 |
</details>
|
| 105 |
<details>
|
| 106 |
<summary>Is my data private?</summary>
|
|
@@ -108,17 +131,23 @@ function Help() {
|
|
| 108 |
</details>
|
| 109 |
<details>
|
| 110 |
<summary>Why does the face mesh lag behind my movements?</summary>
|
| 111 |
-
<p>The face mesh overlay updates each time the server returns a detection result. The camera feed itself renders
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
</details>
|
| 113 |
</section>
|
| 114 |
|
| 115 |
<section className="help-section">
|
| 116 |
<h2>Technical Info</h2>
|
| 117 |
<p><strong>Face Detection:</strong> MediaPipe Face Mesh (478 landmarks)</p>
|
| 118 |
-
<p><strong>Feature Extraction:</strong> Head pose (yaw/pitch/roll), EAR, MAR, gaze offset, PERCLOS, blink rate</p>
|
| 119 |
-
<p><strong>ML Models:</strong> MLP (
|
| 120 |
-
<p><strong>
|
|
|
|
| 121 |
<p><strong>Framework:</strong> FastAPI + React (Vite) + WebSocket</p>
|
|
|
|
| 122 |
</section>
|
| 123 |
</div>
|
| 124 |
</main>
|
|
|
|
| 28 |
<h2>How to Use Focus Guard</h2>
|
| 29 |
<ol>
|
| 30 |
<li>Navigate to the Focus page from the menu</li>
|
| 31 |
+
<li>Read the setup notes in the intro screen, then allow camera access when prompted</li>
|
| 32 |
<li>Click the green "Start" button to begin monitoring</li>
|
| 33 |
+
<li>Position yourself in front of the camera with good lighting</li>
|
| 34 |
<li>The system will track your focus in real-time using face mesh analysis</li>
|
| 35 |
+
<li>Use the model selector to switch between detection models (Hybrid, XGBoost, MLP, Geometric)</li>
|
| 36 |
+
<li>Optionally enable <strong>Eye Gaze</strong> tracking for additional gaze-based focus signals</li>
|
| 37 |
<li>Click "Stop" when you're done to save the session</li>
|
| 38 |
</ol>
|
| 39 |
</section>
|
|
|
|
| 52 |
|
| 53 |
<section className="help-section">
|
| 54 |
<h2>Available Models</h2>
|
| 55 |
+
<p><strong>Hybrid</strong> <em>(Recommended)</em>: Blends MLP predictions (30%) with geometric face/eye scoring (70%) using a weighted average tuned with LOPO evaluation. Most robust across different people. LOPO F1: 0.8409.</p>
|
| 56 |
+
<p><strong>XGBoost:</strong> Gradient-boosted tree model using 10 selected features. Highest raw accuracy (95.87% pooled, LOPO AUC 0.8695). Strong on tabular data with fast inference.</p>
|
| 57 |
+
<p><strong>MLP:</strong> Two-layer neural network (10→64→32 neurons) trained with PyTorch. Good balance of speed and accuracy (92.92% pooled, LOPO AUC 0.8624). Fastest inference.</p>
|
| 58 |
+
<p><strong>Geometric:</strong> Rule-based scoring using head pose and eye openness. No ML model needed — lightweight fallback when model checkpoints are unavailable. LOPO F1: 0.8195.</p>
|
| 59 |
+
<p style={{ marginTop: '10px', color: '#667281', fontSize: '0.9rem' }}>
|
| 60 |
+
<strong>Tip:</strong> If you wear glasses or have unusual lighting, try different models to find the one that works best for your setup.
|
| 61 |
+
</p>
|
| 62 |
+
</section>
|
| 63 |
+
|
| 64 |
+
<section className="help-section">
|
| 65 |
+
<h2>Eye Gaze Tracking</h2>
|
| 66 |
+
<p>The <strong>Eye Gaze</strong> button enables L2CS-Net, a deep neural network that estimates your gaze direction from the eye region. It runs alongside your selected base model and can improve focus detection accuracy.</p>
|
| 67 |
+
<p style={{ marginTop: '8px' }}><strong>Performance note:</strong> Eye gaze tracking increases CPU usage and may reduce frame rate. If the system feels sluggish, disable it.</p>
|
| 68 |
+
<h3 style={{ marginTop: '14px', fontSize: '1rem' }}>Calibration</h3>
|
| 69 |
+
<p>For best accuracy, calibrate when Eye Gaze is active:</p>
|
| 70 |
+
<ol>
|
| 71 |
+
<li>Start a session, then click the <strong>"Recalibrate"</strong> button that appears in the model strip when Eye Gaze is on</li>
|
| 72 |
+
<li>Look directly at each dot as it appears on screen — there are <strong>9 calibration points</strong> across the screen</li>
|
| 73 |
+
<li>A final <strong>validation point</strong> confirms accuracy before calibration is applied</li>
|
| 74 |
+
</ol>
|
| 75 |
+
<p>You can recalibrate at any time using the "Recalibrate" button, which appears in the model strip when Eye Gaze is on and a session is running.</p>
|
| 76 |
</section>
|
| 77 |
|
| 78 |
<section className="help-section">
|
| 79 |
<h2>Adjusting Settings</h2>
|
| 80 |
+
<p><strong>Frame Rate:</strong> Controls how many frames per second are sent for analysis. Range: 10–30 FPS. A minimum of 10 FPS is enforced to keep temporal features (blink rate, PERCLOS) accurate.</p>
|
| 81 |
+
<p><strong>Model Selection:</strong> Switch models in real-time using the pill buttons above the timeline. The active model is highlighted. Different models may perform better depending on your lighting, setup, and whether you wear glasses.</p>
|
| 82 |
+
<p><strong>Floating Window:</strong> Opens a Picture-in-Picture window with your camera feed so you can keep the video visible while working in other apps.</p>
|
| 83 |
</section>
|
| 84 |
|
| 85 |
<section className="help-section">
|
| 86 |
+
<h2>Privacy & Data</h2>
|
| 87 |
<p>Video frames are processed in real-time on the server and are never stored. Only focus status metadata (timestamps, confidence scores) is saved to the session database. View past runs under <strong>My Records</strong>; stats and badges live under <strong>My Achievement</strong>.</p>
|
| 88 |
<p style={{ marginTop: '12px' }}>
|
| 89 |
<button
|
|
|
|
| 113 |
<summary>Why is my focus score low?</summary>
|
| 114 |
<p>Ensure good lighting so the face mesh can detect your landmarks clearly. Face the camera directly and avoid large head movements. Try switching to a different model if one isn't working well for your setup.</p>
|
| 115 |
</details>
|
| 116 |
+
<details>
|
| 117 |
+
<summary>Does wearing glasses affect accuracy?</summary>
|
| 118 |
+
<p>Yes, glasses can reduce accuracy — especially for eye-based features like EAR and gaze offset — because the lenses may distort landmark positions or cause reflections. If you wear glasses, try different models (e.g. Geometric or MLP may handle glasses better than XGBoost for some users). Avoid using Eye Gaze tracking with glasses as it may significantly degrade results.</p>
|
| 119 |
+
</details>
|
| 120 |
<details>
|
| 121 |
<summary>Can I use this without a camera?</summary>
|
| 122 |
<p>No, camera access is required. The system relies on real-time face landmark detection to determine focus.</p>
|
| 123 |
</details>
|
| 124 |
<details>
|
| 125 |
<summary>Does this work on mobile?</summary>
|
| 126 |
+
<p>Yes, it works on mobile browsers that support camera access and WebSocket connections. Performance depends on your device and network speed. Eye Gaze tracking is not recommended on mobile due to performance constraints.</p>
|
| 127 |
</details>
|
| 128 |
<details>
|
| 129 |
<summary>Is my data private?</summary>
|
|
|
|
| 131 |
</details>
|
| 132 |
<details>
|
| 133 |
<summary>Why does the face mesh lag behind my movements?</summary>
|
| 134 |
+
<p>The face mesh overlay updates each time the server returns a detection result. The camera feed itself renders locally. Any visible lag depends on network latency and server processing time. Reducing the frame rate slider can help if lag is noticeable.</p>
|
| 135 |
+
</details>
|
| 136 |
+
<details>
|
| 137 |
+
<summary>The Hybrid model doesn't seem to work differently from MLP — why?</summary>
|
| 138 |
+
<p>The Hybrid model blends MLP (30%) and geometric (70%) scores using a fixed weighted average. Because its geometric component dominates, it tends to be more conservative than raw MLP — especially when head pose or eye openness signals are borderline. It is tuned to be the most consistent across different people rather than the most aggressive.</p>
|
| 139 |
</details>
|
| 140 |
</section>
|
| 141 |
|
| 142 |
<section className="help-section">
|
| 143 |
<h2>Technical Info</h2>
|
| 144 |
<p><strong>Face Detection:</strong> MediaPipe Face Mesh (478 landmarks)</p>
|
| 145 |
+
<p><strong>Feature Extraction:</strong> Head pose (yaw/pitch/roll), EAR, MAR, gaze offset, PERCLOS, blink rate — 10 features selected via LOFO analysis</p>
|
| 146 |
+
<p><strong>ML Models:</strong> PyTorch MLP (10→64→32→2), XGBoost (600 trees), Geometric (rule-based), Hybrid (MLP 30% + Geometric 70% weighted blend)</p>
|
| 147 |
+
<p><strong>Eye Gaze:</strong> L2CS-Net (ResNet50 backbone, trained on Gaze360) with 9-point polynomial calibration</p>
|
| 148 |
+
<p><strong>Storage:</strong> SQLite database (sessions, events, settings)</p>
|
| 149 |
<p><strong>Framework:</strong> FastAPI + React (Vite) + WebSocket</p>
|
| 150 |
+
<p><strong>Evaluation:</strong> Leave-One-Person-Out (LOPO) cross-validation on 9 participants, 144K frames</p>
|
| 151 |
</section>
|
| 152 |
</div>
|
| 153 |
</main>
|
src/components/Home.jsx
CHANGED
|
@@ -1,18 +1,22 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
|
| 3 |
-
function Home({
|
| 4 |
-
return (
|
| 5 |
-
<main id="page-a" className="page">
|
| 6 |
-
<h1>FocusGuard</h1>
|
| 7 |
-
<p>Your productivity monitor assistant.</p>
|
| 8 |
-
|
| 9 |
-
<div
|
| 10 |
-
<button
|
| 11 |
-
Start Focus
|
| 12 |
-
</button>
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
function Home({ onStartFocus, onStartTutorial }) {
|
| 4 |
+
return (
|
| 5 |
+
<main id="page-a" className="page">
|
| 6 |
+
<h1>FocusGuard</h1>
|
| 7 |
+
<p>Your productivity monitor assistant.</p>
|
| 8 |
+
|
| 9 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', alignItems: 'center', marginTop: '30px' }}>
|
| 10 |
+
<button className="btn-main" onClick={onStartFocus} style={{ width: '250px' }}>
|
| 11 |
+
Start Focus
|
| 12 |
+
</button>
|
| 13 |
+
|
| 14 |
+
<button className="btn-main" onClick={onStartTutorial} style={{ width: '250px' }}>
|
| 15 |
+
Tutorial
|
| 16 |
+
</button>
|
| 17 |
+
</div>
|
| 18 |
+
</main>
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default Home;
|
src/components/Records.jsx
CHANGED
|
@@ -1,645 +1,757 @@
|
|
| 1 |
-
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
-
|
| 3 |
-
function Records() {
|
| 4 |
-
const [filter, setFilter] = useState('all');
|
| 5 |
-
const [sessions, setSessions] = useState([]);
|
| 6 |
-
const [loading, setLoading] = useState(false);
|
| 7 |
-
const [detailState, setDetailState] = useState({
|
| 8 |
-
open: false,
|
| 9 |
-
loading: false,
|
| 10 |
-
error: '',
|
| 11 |
-
session: null
|
| 12 |
-
});
|
| 13 |
-
const chartRef = useRef(null);
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
const
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
return
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
const
|
| 72 |
-
const
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
?
|
| 106 |
-
:
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
const
|
| 117 |
-
const
|
| 118 |
-
const
|
| 119 |
-
|
| 120 |
-
const
|
| 121 |
-
|
| 122 |
-
const
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
const
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
if (score >= 0.
|
| 148 |
-
return '
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
const
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
ctx.
|
| 191 |
-
ctx.
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
const
|
| 199 |
-
const
|
| 200 |
-
const
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
ctx.
|
| 209 |
-
ctx.
|
| 210 |
-
ctx.
|
| 211 |
-
ctx.
|
| 212 |
-
ctx.
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
ctx.
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
ctx.
|
| 227 |
-
ctx.
|
| 228 |
-
ctx.
|
| 229 |
-
ctx.
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
const
|
| 237 |
-
const
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
else color = '#
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
ctx.
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
ctx.
|
| 260 |
-
ctx.
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
<
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
<
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
|
| 3 |
+
function Records() {
|
| 4 |
+
const [filter, setFilter] = useState('all');
|
| 5 |
+
const [sessions, setSessions] = useState([]);
|
| 6 |
+
const [loading, setLoading] = useState(false);
|
| 7 |
+
const [detailState, setDetailState] = useState({
|
| 8 |
+
open: false,
|
| 9 |
+
loading: false,
|
| 10 |
+
error: '',
|
| 11 |
+
session: null
|
| 12 |
+
});
|
| 13 |
+
const chartRef = useRef(null);
|
| 14 |
+
|
| 15 |
+
const fileInputRef = useRef(null);
|
| 16 |
+
|
| 17 |
+
// Format a session duration.
|
| 18 |
+
const formatDuration = (seconds) => {
|
| 19 |
+
const safeSeconds = Math.max(0, Number(seconds) || 0);
|
| 20 |
+
const mins = Math.floor(safeSeconds / 60);
|
| 21 |
+
const secs = safeSeconds % 60;
|
| 22 |
+
return `${mins}m ${secs}s`;
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
// Format a session timestamp for table display.
|
| 26 |
+
const formatDate = (dateString) => {
|
| 27 |
+
const date = new Date(dateString);
|
| 28 |
+
return date.toLocaleDateString('en-US', {
|
| 29 |
+
month: 'short',
|
| 30 |
+
day: 'numeric',
|
| 31 |
+
hour: '2-digit',
|
| 32 |
+
minute: '2-digit'
|
| 33 |
+
});
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const formatDateTime = (dateString) => {
|
| 37 |
+
if (!dateString) return 'Not available';
|
| 38 |
+
const date = new Date(dateString);
|
| 39 |
+
return date.toLocaleString('en-US', {
|
| 40 |
+
month: 'short',
|
| 41 |
+
day: 'numeric',
|
| 42 |
+
year: 'numeric',
|
| 43 |
+
hour: '2-digit',
|
| 44 |
+
minute: '2-digit'
|
| 45 |
+
});
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
const parseMetadata = (detectionData) => {
|
| 49 |
+
if (!detectionData) return {};
|
| 50 |
+
if (typeof detectionData === 'object') return detectionData;
|
| 51 |
+
try {
|
| 52 |
+
return JSON.parse(detectionData);
|
| 53 |
+
} catch (_) {
|
| 54 |
+
return {};
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
const averageOf = (values) => {
|
| 59 |
+
const valid = values.filter((value) => Number.isFinite(value));
|
| 60 |
+
if (valid.length === 0) return null;
|
| 61 |
+
return valid.reduce((sum, value) => sum + value, 0) / valid.length;
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const buildTimelineSegments = (events, maxSegments = 48) => {
|
| 65 |
+
if (!events.length) return [];
|
| 66 |
+
|
| 67 |
+
const segmentSize = Math.ceil(events.length / maxSegments);
|
| 68 |
+
const segments = [];
|
| 69 |
+
|
| 70 |
+
for (let i = 0; i < events.length; i += segmentSize) {
|
| 71 |
+
const slice = events.slice(i, i + segmentSize);
|
| 72 |
+
const focusedCount = slice.filter((event) => event.isFocused).length;
|
| 73 |
+
const focusRatio = focusedCount / slice.length;
|
| 74 |
+
const confidence = averageOf(slice.map((event) => event.confidence));
|
| 75 |
+
|
| 76 |
+
let tone = 'distracted';
|
| 77 |
+
if (focusRatio >= 0.75) tone = 'focused';
|
| 78 |
+
else if (focusRatio >= 0.35) tone = 'mixed';
|
| 79 |
+
|
| 80 |
+
segments.push({
|
| 81 |
+
tone,
|
| 82 |
+
focusRatio,
|
| 83 |
+
confidence,
|
| 84 |
+
count: slice.length
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
return segments;
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
const buildDetailView = (session) => {
|
| 92 |
+
if (!session) return null;
|
| 93 |
+
|
| 94 |
+
const parsedEvents = (session.events || []).map((event) => {
|
| 95 |
+
const metadata = parseMetadata(event.detection_data);
|
| 96 |
+
return {
|
| 97 |
+
...event,
|
| 98 |
+
metadata,
|
| 99 |
+
isFocused: Boolean(event.is_focused),
|
| 100 |
+
confidence: Number(event.confidence) || 0
|
| 101 |
+
};
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
const focusRatio = session.total_frames
|
| 105 |
+
? session.focused_frames / session.total_frames
|
| 106 |
+
: parsedEvents.length
|
| 107 |
+
? parsedEvents.filter((event) => event.isFocused).length / parsedEvents.length
|
| 108 |
+
: 0;
|
| 109 |
+
|
| 110 |
+
const modelCounts = parsedEvents.reduce((counts, event) => {
|
| 111 |
+
const model = event.metadata?.model;
|
| 112 |
+
if (model) counts[model] = (counts[model] || 0) + 1;
|
| 113 |
+
return counts;
|
| 114 |
+
}, {});
|
| 115 |
+
|
| 116 |
+
const dominantModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'Unavailable';
|
| 117 |
+
const avgConfidence = averageOf(parsedEvents.map((event) => event.confidence));
|
| 118 |
+
const avgFaceScore = averageOf(parsedEvents.map((event) => Number(event.metadata?.s_face)));
|
| 119 |
+
const avgEyeScore = averageOf(parsedEvents.map((event) => Number(event.metadata?.s_eye)));
|
| 120 |
+
const avgMar = averageOf(parsedEvents.map((event) => Number(event.metadata?.mar)));
|
| 121 |
+
|
| 122 |
+
const startTime = session.start_time ? new Date(session.start_time) : null;
|
| 123 |
+
const timeline = buildTimelineSegments(parsedEvents);
|
| 124 |
+
const recentEvents = parsedEvents.slice(-10).reverse();
|
| 125 |
+
|
| 126 |
+
return {
|
| 127 |
+
parsedEvents,
|
| 128 |
+
focusRatio,
|
| 129 |
+
dominantModel,
|
| 130 |
+
avgConfidence,
|
| 131 |
+
avgFaceScore,
|
| 132 |
+
avgEyeScore,
|
| 133 |
+
avgMar,
|
| 134 |
+
timeline,
|
| 135 |
+
recentEvents,
|
| 136 |
+
formatOffset(timestamp) {
|
| 137 |
+
if (!startTime || !timestamp) return '--';
|
| 138 |
+
const offsetSeconds = Math.max(0, Math.round((new Date(timestamp) - startTime) / 1000));
|
| 139 |
+
const mins = Math.floor(offsetSeconds / 60);
|
| 140 |
+
const secs = offsetSeconds % 60;
|
| 141 |
+
return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
| 142 |
+
}
|
| 143 |
+
};
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const getScoreTone = (score) => {
|
| 147 |
+
if (score >= 0.8) return 'excellent';
|
| 148 |
+
if (score >= 0.6) return 'good';
|
| 149 |
+
if (score >= 0.4) return 'fair';
|
| 150 |
+
return 'low';
|
| 151 |
+
};
|
| 152 |
+
|
| 153 |
+
const closeDetails = () => {
|
| 154 |
+
setDetailState({
|
| 155 |
+
open: false,
|
| 156 |
+
loading: false,
|
| 157 |
+
error: '',
|
| 158 |
+
session: null
|
| 159 |
+
});
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
// Load session rows for the selected filter.
|
| 163 |
+
const loadSessions = async (filterType) => {
|
| 164 |
+
setLoading(true);
|
| 165 |
+
try {
|
| 166 |
+
const response = await fetch(`/api/sessions?filter=${filterType}&limit=50`);
|
| 167 |
+
const data = await response.json();
|
| 168 |
+
setSessions(data);
|
| 169 |
+
drawChart(data);
|
| 170 |
+
} catch (error) {
|
| 171 |
+
console.error('Failed to load sessions:', error);
|
| 172 |
+
} finally {
|
| 173 |
+
setLoading(false);
|
| 174 |
+
}
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
// Draw the session score chart.
|
| 178 |
+
const drawChart = (data) => {
|
| 179 |
+
const canvas = chartRef.current;
|
| 180 |
+
if (!canvas) return;
|
| 181 |
+
|
| 182 |
+
const ctx = canvas.getContext('2d');
|
| 183 |
+
const width = canvas.width = canvas.offsetWidth;
|
| 184 |
+
const height = canvas.height = 300;
|
| 185 |
+
|
| 186 |
+
// Clear the canvas before each redraw.
|
| 187 |
+
ctx.clearRect(0, 0, width, height);
|
| 188 |
+
|
| 189 |
+
if (data.length === 0) {
|
| 190 |
+
ctx.fillStyle = '#999';
|
| 191 |
+
ctx.font = '16px Nunito';
|
| 192 |
+
ctx.textAlign = 'center';
|
| 193 |
+
ctx.fillText('No data available', width / 2, height / 2);
|
| 194 |
+
return;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Use at most the latest 20 sessions in the chart.
|
| 198 |
+
const displayData = data.slice(0, 20).reverse();
|
| 199 |
+
const padding = 50;
|
| 200 |
+
const chartWidth = width - padding * 2;
|
| 201 |
+
const chartHeight = height - padding * 2;
|
| 202 |
+
const barWidth = chartWidth / displayData.length;
|
| 203 |
+
|
| 204 |
+
// Use a normalized max score for chart scaling.
|
| 205 |
+
const maxScore = 1.0;
|
| 206 |
+
|
| 207 |
+
// Draw the chart axes.
|
| 208 |
+
ctx.strokeStyle = '#E0E0E0';
|
| 209 |
+
ctx.lineWidth = 2;
|
| 210 |
+
ctx.beginPath();
|
| 211 |
+
ctx.moveTo(padding, padding);
|
| 212 |
+
ctx.lineTo(padding, height - padding);
|
| 213 |
+
ctx.lineTo(width - padding, height - padding);
|
| 214 |
+
ctx.stroke();
|
| 215 |
+
|
| 216 |
+
// Draw Y-axis labels.
|
| 217 |
+
ctx.fillStyle = '#666';
|
| 218 |
+
ctx.font = '12px Nunito';
|
| 219 |
+
ctx.textAlign = 'right';
|
| 220 |
+
for (let i = 0; i <= 4; i++) {
|
| 221 |
+
const y = height - padding - (chartHeight * i / 4);
|
| 222 |
+
const value = (maxScore * i / 4 * 100).toFixed(0);
|
| 223 |
+
ctx.fillText(value + '%', padding - 10, y + 4);
|
| 224 |
+
|
| 225 |
+
// Draw horizontal grid lines.
|
| 226 |
+
ctx.strokeStyle = '#F0F0F0';
|
| 227 |
+
ctx.lineWidth = 1;
|
| 228 |
+
ctx.beginPath();
|
| 229 |
+
ctx.moveTo(padding, y);
|
| 230 |
+
ctx.lineTo(width - padding, y);
|
| 231 |
+
ctx.stroke();
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Draw the bar chart.
|
| 235 |
+
displayData.forEach((session, index) => {
|
| 236 |
+
const barHeight = (session.focus_score / maxScore) * chartHeight;
|
| 237 |
+
const x = padding + index * barWidth + barWidth * 0.1;
|
| 238 |
+
const y = height - padding - barHeight;
|
| 239 |
+
const barActualWidth = barWidth * 0.8;
|
| 240 |
+
|
| 241 |
+
// Map each score to a blue-toned color band.
|
| 242 |
+
const score = session.focus_score;
|
| 243 |
+
let color;
|
| 244 |
+
if (score >= 0.8) color = '#4A90E2';
|
| 245 |
+
else if (score >= 0.6) color = '#5DADE2';
|
| 246 |
+
else if (score >= 0.4) color = '#85C1E9';
|
| 247 |
+
else color = '#AED6F1';
|
| 248 |
+
|
| 249 |
+
ctx.fillStyle = color;
|
| 250 |
+
ctx.fillRect(x, y, barActualWidth, barHeight);
|
| 251 |
+
|
| 252 |
+
// Draw a matching outline around each bar.
|
| 253 |
+
ctx.strokeStyle = color;
|
| 254 |
+
ctx.lineWidth = 1;
|
| 255 |
+
ctx.strokeRect(x, y, barActualWidth, barHeight);
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
// Draw the chart title.
|
| 259 |
+
ctx.textAlign = 'left';
|
| 260 |
+
ctx.font = 'bold 14px Nunito';
|
| 261 |
+
ctx.fillStyle = '#4A90E2';
|
| 262 |
+
ctx.fillText('Focus Score by Session', padding, 30);
|
| 263 |
+
};
|
| 264 |
+
|
| 265 |
+
// Initial load.
|
| 266 |
+
useEffect(() => {
|
| 267 |
+
loadSessions(filter);
|
| 268 |
+
}, [filter]);
|
| 269 |
+
|
| 270 |
+
useEffect(() => {
|
| 271 |
+
if (!detailState.open) return undefined;
|
| 272 |
+
|
| 273 |
+
const previousOverflow = document.body.style.overflow;
|
| 274 |
+
document.body.style.overflow = 'hidden';
|
| 275 |
+
|
| 276 |
+
const handleKeyDown = (event) => {
|
| 277 |
+
if (event.key === 'Escape') {
|
| 278 |
+
closeDetails();
|
| 279 |
+
}
|
| 280 |
+
};
|
| 281 |
+
|
| 282 |
+
window.addEventListener('keydown', handleKeyDown);
|
| 283 |
+
|
| 284 |
+
return () => {
|
| 285 |
+
document.body.style.overflow = previousOverflow;
|
| 286 |
+
window.removeEventListener('keydown', handleKeyDown);
|
| 287 |
+
};
|
| 288 |
+
}, [detailState.open]);
|
| 289 |
+
|
| 290 |
+
// Filter button handler.
|
| 291 |
+
const handleFilterClick = (filterType) => {
|
| 292 |
+
setFilter(filterType);
|
| 293 |
+
};
|
| 294 |
+
|
| 295 |
+
// Open the detail modal for one session.
|
| 296 |
+
const handleViewDetails = async (sessionId) => {
|
| 297 |
+
setDetailState({
|
| 298 |
+
open: true,
|
| 299 |
+
loading: true,
|
| 300 |
+
error: '',
|
| 301 |
+
session: null
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
try {
|
| 305 |
+
const response = await fetch(`/api/sessions/${sessionId}`);
|
| 306 |
+
if (!response.ok) {
|
| 307 |
+
throw new Error('Failed to load session details.');
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
const data = await response.json();
|
| 311 |
+
setDetailState({
|
| 312 |
+
open: true,
|
| 313 |
+
loading: false,
|
| 314 |
+
error: '',
|
| 315 |
+
session: data
|
| 316 |
+
});
|
| 317 |
+
} catch (error) {
|
| 318 |
+
setDetailState({
|
| 319 |
+
open: true,
|
| 320 |
+
loading: false,
|
| 321 |
+
error: error.message || 'Failed to load session details.',
|
| 322 |
+
session: null
|
| 323 |
+
});
|
| 324 |
+
}
|
| 325 |
+
};
|
| 326 |
+
|
| 327 |
+
const handleExport = async () => {
|
| 328 |
+
try {
|
| 329 |
+
const response = await fetch('/api/sessions?filter=all');
|
| 330 |
+
if (!response.ok) throw new Error("Failed to fetch data");
|
| 331 |
+
const data = await response.json();
|
| 332 |
+
const jsonString = JSON.stringify(data, null, 2);
|
| 333 |
+
localStorage.setItem('focus_magic_backup', jsonString);
|
| 334 |
+
|
| 335 |
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
| 336 |
+
const url = URL.createObjectURL(blob);
|
| 337 |
+
const link = document.createElement('a');
|
| 338 |
+
link.href = url;
|
| 339 |
+
link.download = `focus-guard-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
| 340 |
+
document.body.appendChild(link);
|
| 341 |
+
link.click();
|
| 342 |
+
document.body.removeChild(link);
|
| 343 |
+
URL.revokeObjectURL(url);
|
| 344 |
+
} catch (error) {
|
| 345 |
+
console.error(error);
|
| 346 |
+
alert("Export failed: " + error.message);
|
| 347 |
+
}
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
const triggerImport = () => {
|
| 351 |
+
if (fileInputRef.current) fileInputRef.current.click();
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
const handleFileChange = async (event) => {
|
| 355 |
+
const file = event.target.files[0];
|
| 356 |
+
if (!file) return;
|
| 357 |
+
|
| 358 |
+
const reader = new FileReader();
|
| 359 |
+
reader.onload = async (e) => {
|
| 360 |
+
try {
|
| 361 |
+
const content = e.target.result;
|
| 362 |
+
const sessions = JSON.parse(content);
|
| 363 |
+
if (!Array.isArray(sessions)) {
|
| 364 |
+
throw new Error("Invalid file format: Expected a list of sessions.");
|
| 365 |
+
}
|
| 366 |
+
const response = await fetch('/api/import', {
|
| 367 |
+
method: 'POST',
|
| 368 |
+
headers: { 'Content-Type': 'application/json' },
|
| 369 |
+
body: JSON.stringify(sessions)
|
| 370 |
+
});
|
| 371 |
+
if (response.ok) {
|
| 372 |
+
const result = await response.json();
|
| 373 |
+
alert(`Success! Imported ${result.count} sessions.`);
|
| 374 |
+
loadSessions(filter);
|
| 375 |
+
} else {
|
| 376 |
+
alert("Import failed on server side.");
|
| 377 |
+
}
|
| 378 |
+
} catch (err) {
|
| 379 |
+
alert("Error parsing file: " + err.message);
|
| 380 |
+
}
|
| 381 |
+
event.target.value = '';
|
| 382 |
+
};
|
| 383 |
+
reader.readAsText(file);
|
| 384 |
+
};
|
| 385 |
+
|
| 386 |
+
const handleClearHistory = async () => {
|
| 387 |
+
if (!window.confirm("Are you sure? This will delete ALL your session history permanently.")) {
|
| 388 |
+
return;
|
| 389 |
+
}
|
| 390 |
+
try {
|
| 391 |
+
const response = await fetch('/api/history', { method: 'DELETE' });
|
| 392 |
+
if (response.ok) {
|
| 393 |
+
alert("All history has been cleared.");
|
| 394 |
+
loadSessions(filter);
|
| 395 |
+
} else {
|
| 396 |
+
alert("Failed to clear history.");
|
| 397 |
+
}
|
| 398 |
+
} catch (err) {
|
| 399 |
+
alert("Error: " + err.message);
|
| 400 |
+
}
|
| 401 |
+
};
|
| 402 |
+
|
| 403 |
+
const detailView = buildDetailView(detailState.session);
|
| 404 |
+
|
| 405 |
+
return (
|
| 406 |
+
<main id="page-d" className="page">
|
| 407 |
+
<h1 className="page-title" style={{ marginBottom: '10px' }}>My Records</h1>
|
| 408 |
+
|
| 409 |
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '15px', marginBottom: '25px' }}>
|
| 410 |
+
<input
|
| 411 |
+
type="file"
|
| 412 |
+
ref={fileInputRef}
|
| 413 |
+
style={{ display: 'none' }}
|
| 414 |
+
accept=".json"
|
| 415 |
+
onChange={handleFileChange}
|
| 416 |
+
/>
|
| 417 |
+
<button
|
| 418 |
+
onClick={handleExport}
|
| 419 |
+
style={{ background: '#eef3f8', border: '1px solid #d9eaff', color: '#4b5a6b', padding: '6px 16px', borderRadius: '20px', fontSize: '12px', fontWeight: '700', cursor: 'pointer', transition: 'all 0.2s' }}
|
| 420 |
+
onMouseOver={(e) => { e.target.style.background = '#e2eaf3'; }}
|
| 421 |
+
onMouseOut={(e) => { e.target.style.background = '#eef3f8'; }}
|
| 422 |
+
>
|
| 423 |
+
⬇️ Export
|
| 424 |
+
</button>
|
| 425 |
+
<button
|
| 426 |
+
onClick={triggerImport}
|
| 427 |
+
style={{ background: '#eef3f8', border: '1px solid #d9eaff', color: '#4b5a6b', padding: '6px 16px', borderRadius: '20px', fontSize: '12px', fontWeight: '700', cursor: 'pointer', transition: 'all 0.2s' }}
|
| 428 |
+
onMouseOver={(e) => { e.target.style.background = '#e2eaf3'; }}
|
| 429 |
+
onMouseOut={(e) => { e.target.style.background = '#eef3f8'; }}
|
| 430 |
+
>
|
| 431 |
+
⬆️ Import
|
| 432 |
+
</button>
|
| 433 |
+
<button
|
| 434 |
+
onClick={handleClearHistory}
|
| 435 |
+
style={{ background: '#fff1ee', border: '1px solid #f3c7c7', color: '#b54028', padding: '6px 16px', borderRadius: '20px', fontSize: '12px', fontWeight: '700', cursor: 'pointer', transition: 'all 0.2s' }}
|
| 436 |
+
onMouseOver={(e) => { e.target.style.background = '#fbe5e1'; }}
|
| 437 |
+
onMouseOut={(e) => { e.target.style.background = '#fff1ee'; }}
|
| 438 |
+
>
|
| 439 |
+
🗑️ Clear
|
| 440 |
+
</button>
|
| 441 |
+
</div>
|
| 442 |
+
|
| 443 |
+
<div className="records-controls" style={{ display: 'flex', justifyContent: 'center', gap: '10px', marginBottom: '30px' }}>
|
| 444 |
+
<button
|
| 445 |
+
id="filter-today"
|
| 446 |
+
onClick={() => handleFilterClick('today')}
|
| 447 |
+
style={{
|
| 448 |
+
padding: '10px 30px',
|
| 449 |
+
borderRadius: '8px',
|
| 450 |
+
border: filter === 'today' ? 'none' : '2px solid #4A90E2',
|
| 451 |
+
background: filter === 'today' ? '#4A90E2' : 'transparent',
|
| 452 |
+
color: filter === 'today' ? 'white' : '#4A90E2',
|
| 453 |
+
fontSize: '14px',
|
| 454 |
+
fontWeight: '500',
|
| 455 |
+
cursor: 'pointer',
|
| 456 |
+
transition: 'all 0.3s'
|
| 457 |
+
}}
|
| 458 |
+
>
|
| 459 |
+
Today
|
| 460 |
+
</button>
|
| 461 |
+
<button
|
| 462 |
+
id="filter-week"
|
| 463 |
+
onClick={() => handleFilterClick('week')}
|
| 464 |
+
style={{
|
| 465 |
+
padding: '10px 30px',
|
| 466 |
+
borderRadius: '8px',
|
| 467 |
+
border: filter === 'week' ? 'none' : '2px solid #4A90E2',
|
| 468 |
+
background: filter === 'week' ? '#4A90E2' : 'transparent',
|
| 469 |
+
color: filter === 'week' ? 'white' : '#4A90E2',
|
| 470 |
+
fontSize: '14px',
|
| 471 |
+
fontWeight: '500',
|
| 472 |
+
cursor: 'pointer',
|
| 473 |
+
transition: 'all 0.3s'
|
| 474 |
+
}}
|
| 475 |
+
>
|
| 476 |
+
This Week
|
| 477 |
+
</button>
|
| 478 |
+
<button
|
| 479 |
+
id="filter-month"
|
| 480 |
+
onClick={() => handleFilterClick('month')}
|
| 481 |
+
style={{
|
| 482 |
+
padding: '10px 30px',
|
| 483 |
+
borderRadius: '8px',
|
| 484 |
+
border: filter === 'month' ? 'none' : '2px solid #4A90E2',
|
| 485 |
+
background: filter === 'month' ? '#4A90E2' : 'transparent',
|
| 486 |
+
color: filter === 'month' ? 'white' : '#4A90E2',
|
| 487 |
+
fontSize: '14px',
|
| 488 |
+
fontWeight: '500',
|
| 489 |
+
cursor: 'pointer',
|
| 490 |
+
transition: 'all 0.3s'
|
| 491 |
+
}}
|
| 492 |
+
>
|
| 493 |
+
This Month
|
| 494 |
+
</button>
|
| 495 |
+
<button
|
| 496 |
+
id="filter-all"
|
| 497 |
+
onClick={() => handleFilterClick('all')}
|
| 498 |
+
style={{
|
| 499 |
+
padding: '10px 30px',
|
| 500 |
+
borderRadius: '8px',
|
| 501 |
+
border: filter === 'all' ? 'none' : '2px solid #4A90E2',
|
| 502 |
+
background: filter === 'all' ? '#4A90E2' : 'transparent',
|
| 503 |
+
color: filter === 'all' ? 'white' : '#4A90E2',
|
| 504 |
+
fontSize: '14px',
|
| 505 |
+
fontWeight: '500',
|
| 506 |
+
cursor: 'pointer',
|
| 507 |
+
transition: 'all 0.3s'
|
| 508 |
+
}}
|
| 509 |
+
>
|
| 510 |
+
All Time
|
| 511 |
+
</button>
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
<div className="chart-container" style={{
|
| 515 |
+
background: 'white',
|
| 516 |
+
padding: '20px',
|
| 517 |
+
borderRadius: '10px',
|
| 518 |
+
marginBottom: '30px',
|
| 519 |
+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
| 520 |
+
}}>
|
| 521 |
+
<canvas ref={chartRef} id="focus-chart" style={{ width: '100%', height: '300px' }}></canvas>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<div className="sessions-list" style={{
|
| 525 |
+
background: 'white',
|
| 526 |
+
padding: '20px',
|
| 527 |
+
borderRadius: '10px',
|
| 528 |
+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
| 529 |
+
}}>
|
| 530 |
+
<h2 style={{ color: '#333', marginBottom: '20px', fontSize: '18px', fontWeight: '600' }}>Recent Sessions</h2>
|
| 531 |
+
{loading ? (
|
| 532 |
+
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
| 533 |
+
Loading sessions...
|
| 534 |
+
</div>
|
| 535 |
+
) : sessions.length === 0 ? (
|
| 536 |
+
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
| 537 |
+
No sessions found for this period.
|
| 538 |
+
</div>
|
| 539 |
+
) : (
|
| 540 |
+
<table id="sessions-table" style={{ width: '100%', borderCollapse: 'collapse', borderRadius: '10px', overflow: 'hidden' }}>
|
| 541 |
+
<thead>
|
| 542 |
+
<tr style={{ background: '#4A90E2' }}>
|
| 543 |
+
<th style={{ padding: '15px', textAlign: 'left', color: 'white', fontWeight: '600', fontSize: '14px' }}>Date</th>
|
| 544 |
+
<th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Duration</th>
|
| 545 |
+
<th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Focus Score</th>
|
| 546 |
+
<th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Action</th>
|
| 547 |
+
</tr>
|
| 548 |
+
</thead>
|
| 549 |
+
<tbody id="sessions-tbody">
|
| 550 |
+
{sessions.map((session, index) => (
|
| 551 |
+
<tr key={session.id} style={{
|
| 552 |
+
background: index % 2 === 0 ? '#f8f9fa' : 'white',
|
| 553 |
+
borderBottom: '1px solid #e9ecef'
|
| 554 |
+
}}>
|
| 555 |
+
<td style={{ padding: '15px', color: '#333', fontSize: '13px' }}>{formatDate(session.start_time)}</td>
|
| 556 |
+
<td style={{ padding: '15px', textAlign: 'center', color: '#333', fontSize: '13px' }}>{formatDuration(session.duration_seconds)}</td>
|
| 557 |
+
<td style={{ padding: '15px', textAlign: 'center' }}>
|
| 558 |
+
<span
|
| 559 |
+
style={{
|
| 560 |
+
color:
|
| 561 |
+
session.focus_score >= 0.8
|
| 562 |
+
? '#28a745'
|
| 563 |
+
: session.focus_score >= 0.6
|
| 564 |
+
? '#ffc107'
|
| 565 |
+
: session.focus_score >= 0.4
|
| 566 |
+
? '#fd7e14'
|
| 567 |
+
: '#dc3545',
|
| 568 |
+
fontWeight: '600',
|
| 569 |
+
fontSize: '13px'
|
| 570 |
+
}}
|
| 571 |
+
>
|
| 572 |
+
{(session.focus_score * 100).toFixed(1)}%
|
| 573 |
+
</span>
|
| 574 |
+
</td>
|
| 575 |
+
<td style={{ padding: '15px', textAlign: 'center' }}>
|
| 576 |
+
<button
|
| 577 |
+
onClick={() => handleViewDetails(session.id)}
|
| 578 |
+
className="btn-view"
|
| 579 |
+
>
|
| 580 |
+
View
|
| 581 |
+
</button>
|
| 582 |
+
</td>
|
| 583 |
+
</tr>
|
| 584 |
+
))}
|
| 585 |
+
</tbody>
|
| 586 |
+
</table>
|
| 587 |
+
)}
|
| 588 |
+
</div>
|
| 589 |
+
|
| 590 |
+
{detailState.open ? (
|
| 591 |
+
<div className="modal-overlay" onClick={closeDetails}>
|
| 592 |
+
<div className="modal-content records-detail-modal" onClick={(event) => event.stopPropagation()}>
|
| 593 |
+
<div className="records-detail-header">
|
| 594 |
+
<div>
|
| 595 |
+
<div className="records-detail-kicker">Session Detail</div>
|
| 596 |
+
<h2>
|
| 597 |
+
{detailState.session ? formatDateTime(detailState.session.start_time) : 'Loading session'}
|
| 598 |
+
</h2>
|
| 599 |
+
<p className="records-detail-subtitle">
|
| 600 |
+
Review score, capture quality, and a condensed event timeline for this session.
|
| 601 |
+
</p>
|
| 602 |
+
</div>
|
| 603 |
+
<button type="button" className="records-detail-close" onClick={closeDetails}>
|
| 604 |
+
Close
|
| 605 |
+
</button>
|
| 606 |
+
</div>
|
| 607 |
+
|
| 608 |
+
{detailState.loading ? (
|
| 609 |
+
<div className="records-detail-feedback">Loading session details...</div>
|
| 610 |
+
) : detailState.error ? (
|
| 611 |
+
<div className="records-detail-feedback records-detail-feedback-error">{detailState.error}</div>
|
| 612 |
+
) : detailState.session && detailView ? (
|
| 613 |
+
<>
|
| 614 |
+
<section className="records-detail-summary">
|
| 615 |
+
<article className={`records-detail-stat ${getScoreTone(detailState.session.focus_score)}`}>
|
| 616 |
+
<span className="records-detail-stat-label">Focus Score</span>
|
| 617 |
+
<strong className="records-detail-stat-value">
|
| 618 |
+
{(detailState.session.focus_score * 100).toFixed(1)}%
|
| 619 |
+
</strong>
|
| 620 |
+
</article>
|
| 621 |
+
<article className="records-detail-stat">
|
| 622 |
+
<span className="records-detail-stat-label">Duration</span>
|
| 623 |
+
<strong className="records-detail-stat-value">
|
| 624 |
+
{formatDuration(detailState.session.duration_seconds)}
|
| 625 |
+
</strong>
|
| 626 |
+
</article>
|
| 627 |
+
<article className="records-detail-stat">
|
| 628 |
+
<span className="records-detail-stat-label">Frames Analysed</span>
|
| 629 |
+
<strong className="records-detail-stat-value">{detailState.session.total_frames}</strong>
|
| 630 |
+
</article>
|
| 631 |
+
<article className="records-detail-stat">
|
| 632 |
+
<span className="records-detail-stat-label">Focused Frames</span>
|
| 633 |
+
<strong className="records-detail-stat-value">
|
| 634 |
+
{(detailView.focusRatio * 100).toFixed(1)}%
|
| 635 |
+
</strong>
|
| 636 |
+
</article>
|
| 637 |
+
</section>
|
| 638 |
+
|
| 639 |
+
<section className="records-detail-grid">
|
| 640 |
+
<article className="records-detail-card">
|
| 641 |
+
<h3>Session Info</h3>
|
| 642 |
+
<div className="records-detail-list">
|
| 643 |
+
<div className="records-detail-item">
|
| 644 |
+
<span className="records-detail-item-label">Started</span>
|
| 645 |
+
<span className="records-detail-item-value">{formatDateTime(detailState.session.start_time)}</span>
|
| 646 |
+
</div>
|
| 647 |
+
<div className="records-detail-item">
|
| 648 |
+
<span className="records-detail-item-label">Ended</span>
|
| 649 |
+
<span className="records-detail-item-value">{formatDateTime(detailState.session.end_time)}</span>
|
| 650 |
+
</div>
|
| 651 |
+
<div className="records-detail-item">
|
| 652 |
+
<span className="records-detail-item-label">Dominant Model</span>
|
| 653 |
+
<span className="records-detail-item-value">{detailView.dominantModel}</span>
|
| 654 |
+
</div>
|
| 655 |
+
<div className="records-detail-item">
|
| 656 |
+
<span className="records-detail-item-label">Event Samples</span>
|
| 657 |
+
<span className="records-detail-item-value">{detailView.parsedEvents.length}</span>
|
| 658 |
+
</div>
|
| 659 |
+
</div>
|
| 660 |
+
</article>
|
| 661 |
+
|
| 662 |
+
<article className="records-detail-card">
|
| 663 |
+
<h3>Signal Quality</h3>
|
| 664 |
+
<div className="records-detail-list">
|
| 665 |
+
<div className="records-detail-item">
|
| 666 |
+
<span className="records-detail-item-label">Avg Confidence</span>
|
| 667 |
+
<span className="records-detail-item-value">
|
| 668 |
+
{detailView.avgConfidence !== null ? `${(detailView.avgConfidence * 100).toFixed(1)}%` : '--'}
|
| 669 |
+
</span>
|
| 670 |
+
</div>
|
| 671 |
+
<div className="records-detail-item">
|
| 672 |
+
<span className="records-detail-item-label">Avg Face Score</span>
|
| 673 |
+
<span className="records-detail-item-value">
|
| 674 |
+
{detailView.avgFaceScore !== null ? detailView.avgFaceScore.toFixed(3) : '--'}
|
| 675 |
+
</span>
|
| 676 |
+
</div>
|
| 677 |
+
<div className="records-detail-item">
|
| 678 |
+
<span className="records-detail-item-label">Avg Eye Score</span>
|
| 679 |
+
<span className="records-detail-item-value">
|
| 680 |
+
{detailView.avgEyeScore !== null ? detailView.avgEyeScore.toFixed(3) : '--'}
|
| 681 |
+
</span>
|
| 682 |
+
</div>
|
| 683 |
+
<div className="records-detail-item">
|
| 684 |
+
<span className="records-detail-item-label">Avg MAR</span>
|
| 685 |
+
<span className="records-detail-item-value">
|
| 686 |
+
{detailView.avgMar !== null ? detailView.avgMar.toFixed(3) : '--'}
|
| 687 |
+
</span>
|
| 688 |
+
</div>
|
| 689 |
+
</div>
|
| 690 |
+
</article>
|
| 691 |
+
</section>
|
| 692 |
+
|
| 693 |
+
<section className="records-detail-card">
|
| 694 |
+
<div className="records-detail-section-head">
|
| 695 |
+
<h3>Focus Timeline</h3>
|
| 696 |
+
<span>{detailView.parsedEvents.length} events condensed</span>
|
| 697 |
+
</div>
|
| 698 |
+
{detailView.timeline.length > 0 ? (
|
| 699 |
+
<>
|
| 700 |
+
<div className="records-detail-timeline">
|
| 701 |
+
{detailView.timeline.map((segment, index) => (
|
| 702 |
+
<div
|
| 703 |
+
key={`${segment.tone}-${index}`}
|
| 704 |
+
className={`records-detail-segment ${segment.tone}`}
|
| 705 |
+
title={`${(segment.focusRatio * 100).toFixed(0)}% focused, ${segment.count} events`}
|
| 706 |
+
/>
|
| 707 |
+
))}
|
| 708 |
+
</div>
|
| 709 |
+
<div className="records-detail-legend">
|
| 710 |
+
<span><i className="records-detail-dot focused" />Focused</span>
|
| 711 |
+
<span><i className="records-detail-dot mixed" />Mixed</span>
|
| 712 |
+
<span><i className="records-detail-dot distracted" />Distracted</span>
|
| 713 |
+
</div>
|
| 714 |
+
</>
|
| 715 |
+
) : (
|
| 716 |
+
<div className="records-detail-empty">No event timeline was recorded for this session.</div>
|
| 717 |
+
)}
|
| 718 |
+
</section>
|
| 719 |
+
|
| 720 |
+
<section className="records-detail-card">
|
| 721 |
+
<div className="records-detail-section-head">
|
| 722 |
+
<h3>Recent Events</h3>
|
| 723 |
+
<span>Last {detailView.recentEvents.length} samples</span>
|
| 724 |
+
</div>
|
| 725 |
+
{detailView.recentEvents.length > 0 ? (
|
| 726 |
+
<div className="records-detail-events">
|
| 727 |
+
{detailView.recentEvents.map((event) => (
|
| 728 |
+
<article key={event.id} className="records-detail-event">
|
| 729 |
+
<div className="records-detail-event-time">{detailView.formatOffset(event.timestamp)}</div>
|
| 730 |
+
<div className="records-detail-event-copy">
|
| 731 |
+
<div className="records-detail-event-status">
|
| 732 |
+
{event.isFocused ? 'Focused' : 'Distracted'}
|
| 733 |
+
</div>
|
| 734 |
+
<div className="records-detail-event-meta">
|
| 735 |
+
{event.metadata?.model || 'model n/a'} · confidence {(event.confidence * 100).toFixed(1)}%
|
| 736 |
+
</div>
|
| 737 |
+
</div>
|
| 738 |
+
<div className={`records-detail-event-badge ${event.isFocused ? 'focused' : 'distracted'}`}>
|
| 739 |
+
{event.isFocused ? 'OK' : 'Alert'}
|
| 740 |
+
</div>
|
| 741 |
+
</article>
|
| 742 |
+
))}
|
| 743 |
+
</div>
|
| 744 |
+
) : (
|
| 745 |
+
<div className="records-detail-empty">No individual event samples are available.</div>
|
| 746 |
+
)}
|
| 747 |
+
</section>
|
| 748 |
+
</>
|
| 749 |
+
) : null}
|
| 750 |
+
</div>
|
| 751 |
+
</div>
|
| 752 |
+
) : null}
|
| 753 |
+
</main>
|
| 754 |
+
);
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
export default Records;
|
src/src/App.css
ADDED
|
@@ -0,0 +1,2047 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =========================================
|
| 2 |
+
1. REACT layout setting
|
| 3 |
+
========================================= */
|
| 4 |
+
html, body, #root {
|
| 5 |
+
width: 100%;
|
| 6 |
+
height: 100%;
|
| 7 |
+
margin: 0;
|
| 8 |
+
padding: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.app-container {
|
| 12 |
+
width: 100%;
|
| 13 |
+
min-height: 100vh; /* screen height */
|
| 14 |
+
display: flex;
|
| 15 |
+
flex-direction: column;
|
| 16 |
+
background-color: #f9f9f9;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* =========================================
|
| 20 |
+
2. original layout
|
| 21 |
+
========================================= */
|
| 22 |
+
|
| 23 |
+
/* GLOBAL STYLES */
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Nunito', sans-serif;
|
| 26 |
+
background-color: #f9f9f9;
|
| 27 |
+
overflow-x: hidden;
|
| 28 |
+
overflow-y: auto;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* dynamic class name */
|
| 32 |
+
.hidden {
|
| 33 |
+
display: none !important;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* TOP MENU */
|
| 37 |
+
#top-menu {
|
| 38 |
+
height: 60px;
|
| 39 |
+
background-color: white;
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
justify-content: center;
|
| 43 |
+
gap: 0;
|
| 44 |
+
padding: 0 16px 0 20px;
|
| 45 |
+
box-sizing: border-box;
|
| 46 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 47 |
+
position: fixed;
|
| 48 |
+
top: 0;
|
| 49 |
+
left: 0;
|
| 50 |
+
right: 0;
|
| 51 |
+
width: 100%;
|
| 52 |
+
z-index: 1000;
|
| 53 |
+
overflow-x: auto;
|
| 54 |
+
overflow-y: hidden;
|
| 55 |
+
white-space: nowrap;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.top-menu-links {
|
| 59 |
+
flex: 1;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
flex-wrap: wrap;
|
| 64 |
+
gap: 0;
|
| 65 |
+
min-width: 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.menu-btn {
|
| 69 |
+
background: none;
|
| 70 |
+
border: none;
|
| 71 |
+
font-family: 'Nunito', sans-serif;
|
| 72 |
+
font-size: 16px;
|
| 73 |
+
color: #333;
|
| 74 |
+
padding: 10px 20px;
|
| 75 |
+
cursor: pointer;
|
| 76 |
+
transition: background-color 0.2s;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.menu-btn:hover {
|
| 80 |
+
background-color: #f0f0f0;
|
| 81 |
+
border-radius: 4px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* active for React */
|
| 85 |
+
.menu-btn.active {
|
| 86 |
+
font-weight: bold;
|
| 87 |
+
color: #007BFF;
|
| 88 |
+
background-color: #eef7ff;
|
| 89 |
+
border-radius: 4px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.separator {
|
| 93 |
+
width: 1px;
|
| 94 |
+
height: 20px;
|
| 95 |
+
background-color: #555; /* Dark gray separator */
|
| 96 |
+
margin: 0 5px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* PAGE CONTAINER */
|
| 100 |
+
.page {
|
| 101 |
+
/* content under menu */
|
| 102 |
+
min-height: calc(100vh - 60px);
|
| 103 |
+
width: 100%;
|
| 104 |
+
padding-top: 60px; /* Space for fixed menu */
|
| 105 |
+
padding-bottom: 40px; /* Space at bottom for scrolling */
|
| 106 |
+
box-sizing: border-box;
|
| 107 |
+
display: flex;
|
| 108 |
+
flex-direction: column;
|
| 109 |
+
align-items: center;
|
| 110 |
+
overflow-y: auto;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Ensure page titles are black */
|
| 114 |
+
.page h1 {
|
| 115 |
+
color: #000 !important;
|
| 116 |
+
background: transparent !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.page-title {
|
| 120 |
+
color: #000 !important;
|
| 121 |
+
background: transparent !important;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* PAGE A SPECIFIC */
|
| 125 |
+
#page-a {
|
| 126 |
+
justify-content: center; /* Center vertically */
|
| 127 |
+
/* Fine-tune this margin if the Home screen sits slightly too low. */
|
| 128 |
+
margin-top: -40px;
|
| 129 |
+
flex: 1; /* Fill the remaining height so vertical centering still works. */
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
#page-a h1 {
|
| 133 |
+
font-size: 80px;
|
| 134 |
+
margin: 0 0 10px 0;
|
| 135 |
+
color: #000;
|
| 136 |
+
text-align: center; /* Keep the heading centered. */
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
#page-a p {
|
| 140 |
+
color: #666;
|
| 141 |
+
font-size: 20px;
|
| 142 |
+
margin-bottom: 40px;
|
| 143 |
+
text-align: center;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn-main {
|
| 147 |
+
background-color: #007BFF; /* Blue */
|
| 148 |
+
color: white;
|
| 149 |
+
border: none;
|
| 150 |
+
padding: 15px 50px;
|
| 151 |
+
font-size: 20px;
|
| 152 |
+
font-family: 'Nunito', sans-serif;
|
| 153 |
+
border-radius: 30px; /* Fully rounded corners */
|
| 154 |
+
cursor: pointer;
|
| 155 |
+
transition: transform 0.2s ease;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.btn-main:hover {
|
| 159 |
+
transform: scale(1.1); /* Zoom effect */
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* PAGE B SPECIFIC */
|
| 163 |
+
#page-b {
|
| 164 |
+
justify-content: space-evenly; /* Distribute vertical space */
|
| 165 |
+
padding-bottom: 20px;
|
| 166 |
+
min-height: calc(100vh - 60px); /* Ensure the page still fills the viewport. */
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* 1. Display Area */
|
| 170 |
+
#display-area {
|
| 171 |
+
width: 60%;
|
| 172 |
+
height: 50vh; /* Use viewport height to scale more consistently across screens. */
|
| 173 |
+
min-height: 300px;
|
| 174 |
+
border: 2px solid #ddd;
|
| 175 |
+
border-radius: 12px;
|
| 176 |
+
background-color: #fff;
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
color: #555;
|
| 181 |
+
font-size: 24px;
|
| 182 |
+
position: relative;
|
| 183 |
+
/* Keep video content centered without overflowing the frame. */
|
| 184 |
+
overflow: hidden;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.focus-display-shell {
|
| 188 |
+
background: #101010;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
@keyframes fadeInOverlay {
|
| 192 |
+
from { opacity: 0; }
|
| 193 |
+
to { opacity: 1; }
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
@keyframes slideUpCard {
|
| 197 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 198 |
+
to { opacity: 1; transform: translateY(0); }
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
.focus-flow-overlay {
|
| 203 |
+
position: fixed;
|
| 204 |
+
top: 76px;
|
| 205 |
+
right: 20px;
|
| 206 |
+
bottom: 20px;
|
| 207 |
+
left: 20px;
|
| 208 |
+
display: flex;
|
| 209 |
+
align-items: center;
|
| 210 |
+
justify-content: center;
|
| 211 |
+
padding: 0;
|
| 212 |
+
background: rgba(17, 31, 52, 0.18);
|
| 213 |
+
backdrop-filter: blur(10px);
|
| 214 |
+
z-index: 900;
|
| 215 |
+
animation: fadeInOverlay 0.3s ease-out forwards;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.focus-flow-card {
|
| 219 |
+
width: min(1040px, 100%);
|
| 220 |
+
background: #fff;
|
| 221 |
+
border-radius: 24px;
|
| 222 |
+
padding: 30px 34px;
|
| 223 |
+
box-shadow: 0 28px 80px rgba(14, 44, 88, 0.18);
|
| 224 |
+
border: 1px solid rgba(0, 123, 255, 0.12);
|
| 225 |
+
box-sizing: border-box;
|
| 226 |
+
animation: slideUpCard 0.4s ease-out forwards;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.focus-flow-header {
|
| 230 |
+
display: flex;
|
| 231 |
+
align-items: center;
|
| 232 |
+
justify-content: space-between;
|
| 233 |
+
gap: 24px;
|
| 234 |
+
margin-bottom: 18px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.focus-flow-eyebrow {
|
| 238 |
+
display: inline-block;
|
| 239 |
+
padding: 6px 12px;
|
| 240 |
+
border-radius: 999px;
|
| 241 |
+
background: #e7f3ff;
|
| 242 |
+
color: #007BFF;
|
| 243 |
+
font-size: 0.82rem;
|
| 244 |
+
font-weight: 800;
|
| 245 |
+
letter-spacing: 0.04em;
|
| 246 |
+
text-transform: uppercase;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.focus-flow-header h2 {
|
| 250 |
+
margin: 14px 0 0;
|
| 251 |
+
color: #333;
|
| 252 |
+
font-size: clamp(1.8rem, 2.5vw, 2.5rem);
|
| 253 |
+
line-height: 1.1;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.focus-flow-icon {
|
| 257 |
+
flex: 0 0 auto;
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
justify-content: center;
|
| 261 |
+
width: 116px;
|
| 262 |
+
height: 116px;
|
| 263 |
+
border-radius: 24px;
|
| 264 |
+
background: linear-gradient(180deg, #f4f9ff 0%, #edf5ff 100%);
|
| 265 |
+
border: 1px solid rgba(0, 123, 255, 0.12);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.focus-flow-lead {
|
| 269 |
+
margin: 0 0 20px;
|
| 270 |
+
color: #4a4a4a;
|
| 271 |
+
font-size: 1rem;
|
| 272 |
+
line-height: 1.6;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.focus-flow-grid {
|
| 276 |
+
display: grid;
|
| 277 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 278 |
+
gap: 16px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.focus-flow-panel {
|
| 282 |
+
background: #f8fbff;
|
| 283 |
+
border: 1px solid #d9eaff;
|
| 284 |
+
border-radius: 14px;
|
| 285 |
+
padding: 18px;
|
| 286 |
+
}
|
| 287 |
+
.focus-flow-grid .focus-flow-panel:last-child {
|
| 288 |
+
grid-column: 1 / -1;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.focus-flow-panel h3,
|
| 292 |
+
.focus-flow-step-copy h3 {
|
| 293 |
+
margin: 0 0 8px;
|
| 294 |
+
color: #333;
|
| 295 |
+
font-size: 1rem;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.focus-flow-panel p,
|
| 299 |
+
.focus-flow-step-copy p {
|
| 300 |
+
margin: 0;
|
| 301 |
+
color: #5e6670;
|
| 302 |
+
font-size: 0.95rem;
|
| 303 |
+
line-height: 1.6;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.focus-flow-steps {
|
| 307 |
+
display: grid;
|
| 308 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 309 |
+
gap: 14px;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.focus-flow-step {
|
| 313 |
+
display: flex;
|
| 314 |
+
align-items: flex-start;
|
| 315 |
+
gap: 14px;
|
| 316 |
+
background: #f8fbff;
|
| 317 |
+
border: 1px solid #d9eaff;
|
| 318 |
+
border-radius: 14px;
|
| 319 |
+
padding: 16px 18px;
|
| 320 |
+
min-height: 100px;
|
| 321 |
+
box-sizing: border-box;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.focus-flow-step-number {
|
| 325 |
+
flex: 0 0 auto;
|
| 326 |
+
display: inline-flex;
|
| 327 |
+
align-items: center;
|
| 328 |
+
justify-content: center;
|
| 329 |
+
width: 34px;
|
| 330 |
+
height: 34px;
|
| 331 |
+
border-radius: 50%;
|
| 332 |
+
background: #007BFF;
|
| 333 |
+
color: #fff;
|
| 334 |
+
font-size: 0.95rem;
|
| 335 |
+
font-weight: 800;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.focus-flow-step-copy {
|
| 339 |
+
min-width: 0;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.focus-flow-footer {
|
| 343 |
+
display: flex;
|
| 344 |
+
align-items: center;
|
| 345 |
+
justify-content: space-between;
|
| 346 |
+
gap: 16px;
|
| 347 |
+
margin-top: 20px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.focus-flow-note {
|
| 351 |
+
color: #667281;
|
| 352 |
+
font-size: 0.94rem;
|
| 353 |
+
line-height: 1.6;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.focus-flow-glasses-note {
|
| 357 |
+
background: #fffbea;
|
| 358 |
+
border: 1px solid #f5c518;
|
| 359 |
+
border-radius: 10px;
|
| 360 |
+
padding: 12px 16px;
|
| 361 |
+
font-size: 0.9rem;
|
| 362 |
+
color: #5a4a00;
|
| 363 |
+
line-height: 1.55;
|
| 364 |
+
margin-top: 4px;
|
| 365 |
+
margin-bottom: 4px;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.focus-flow-panel-warn {
|
| 369 |
+
border-left: 3px solid #f5a623;
|
| 370 |
+
background: #fff9f0;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.eye-gaze-modal-checkbox {
|
| 374 |
+
display: flex;
|
| 375 |
+
align-items: center;
|
| 376 |
+
gap: 8px;
|
| 377 |
+
margin-top: 16px;
|
| 378 |
+
font-size: 0.9rem;
|
| 379 |
+
color: #667281;
|
| 380 |
+
cursor: pointer;
|
| 381 |
+
user-select: none;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.focus-flow-button,
|
| 385 |
+
.focus-flow-secondary {
|
| 386 |
+
border: none;
|
| 387 |
+
border-radius: 999px;
|
| 388 |
+
padding: 13px 24px;
|
| 389 |
+
font-family: 'Nunito', sans-serif;
|
| 390 |
+
font-size: 0.98rem;
|
| 391 |
+
font-weight: 800;
|
| 392 |
+
cursor: pointer;
|
| 393 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.focus-flow-button {
|
| 397 |
+
background: #007BFF;
|
| 398 |
+
color: #fff;
|
| 399 |
+
box-shadow: 0 12px 24px rgba(0, 123, 255, 0.18);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.focus-flow-button:hover {
|
| 403 |
+
background: #0069d9;
|
| 404 |
+
border-color: transparent;
|
| 405 |
+
transform: translateY(-1px);
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.focus-flow-secondary {
|
| 409 |
+
background: #eef3f8;
|
| 410 |
+
color: #4b5a6b;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.focus-flow-secondary:hover {
|
| 414 |
+
background: #e2eaf3;
|
| 415 |
+
border-color: transparent;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.focus-state-pill {
|
| 419 |
+
position: absolute;
|
| 420 |
+
top: 18px;
|
| 421 |
+
left: 18px;
|
| 422 |
+
display: inline-flex;
|
| 423 |
+
align-items: center;
|
| 424 |
+
gap: 10px;
|
| 425 |
+
padding: 10px 16px;
|
| 426 |
+
border-radius: 999px;
|
| 427 |
+
color: #fff;
|
| 428 |
+
font-size: 0.88rem;
|
| 429 |
+
font-weight: 800;
|
| 430 |
+
letter-spacing: 0.04em;
|
| 431 |
+
text-transform: uppercase;
|
| 432 |
+
z-index: 2;
|
| 433 |
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.focus-state-pill.pending {
|
| 437 |
+
background: rgba(87, 96, 111, 0.92);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.focus-state-pill.focused {
|
| 441 |
+
background: rgba(33, 163, 102, 0.94);
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.focus-state-pill.not-focused {
|
| 445 |
+
background: rgba(215, 68, 68, 0.94);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.focus-state-dot {
|
| 449 |
+
width: 10px;
|
| 450 |
+
height: 10px;
|
| 451 |
+
border-radius: 50%;
|
| 452 |
+
background: currentColor;
|
| 453 |
+
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.16);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.focus-idle-overlay {
|
| 457 |
+
position: absolute;
|
| 458 |
+
inset: 0;
|
| 459 |
+
display: flex;
|
| 460 |
+
flex-direction: column;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
gap: 10px;
|
| 464 |
+
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.72));
|
| 465 |
+
color: #fff;
|
| 466 |
+
text-align: center;
|
| 467 |
+
z-index: 1;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.focus-idle-overlay p {
|
| 471 |
+
margin: 0;
|
| 472 |
+
font-size: 1.6rem;
|
| 473 |
+
font-weight: 800;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.focus-idle-overlay span {
|
| 477 |
+
max-width: 420px;
|
| 478 |
+
color: rgba(255, 255, 255, 0.82);
|
| 479 |
+
font-size: 0.98rem;
|
| 480 |
+
line-height: 1.5;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.focus-inline-error {
|
| 484 |
+
margin-top: 18px;
|
| 485 |
+
padding: 12px 16px;
|
| 486 |
+
max-width: 620px;
|
| 487 |
+
border-radius: 12px;
|
| 488 |
+
background: #fff1ee;
|
| 489 |
+
color: #b54028;
|
| 490 |
+
font-size: 0.95rem;
|
| 491 |
+
font-weight: 700;
|
| 492 |
+
box-shadow: 0 10px 20px rgba(181, 64, 40, 0.08);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.focus-inline-error-standalone {
|
| 496 |
+
width: 60%;
|
| 497 |
+
box-sizing: border-box;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.focus-debug-panel {
|
| 501 |
+
position: absolute;
|
| 502 |
+
top: 10px;
|
| 503 |
+
right: 10px;
|
| 504 |
+
background: rgba(0,0,0,0.7);
|
| 505 |
+
color: white;
|
| 506 |
+
padding: 10px;
|
| 507 |
+
border-radius: 5px;
|
| 508 |
+
font-size: 12px;
|
| 509 |
+
font-family: monospace;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.focus-model-strip {
|
| 513 |
+
display: flex;
|
| 514 |
+
align-items: center;
|
| 515 |
+
justify-content: center;
|
| 516 |
+
flex-wrap: wrap;
|
| 517 |
+
gap: 8px;
|
| 518 |
+
padding: 10px 16px;
|
| 519 |
+
background: #fff;
|
| 520 |
+
border: 1px solid #e0e0e0;
|
| 521 |
+
border-radius: 12px;
|
| 522 |
+
margin: 10px auto;
|
| 523 |
+
max-width: 700px;
|
| 524 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* --- Model info card --- */
|
| 528 |
+
.model-card {
|
| 529 |
+
width: 60%;
|
| 530 |
+
margin: 14px auto 0;
|
| 531 |
+
background: #fff;
|
| 532 |
+
border: 1px solid #e0e0e0;
|
| 533 |
+
border-radius: 14px;
|
| 534 |
+
padding: 18px 22px 14px;
|
| 535 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
| 536 |
+
animation: cardFadeIn 0.25s ease;
|
| 537 |
+
box-sizing: border-box;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.model-card-details {
|
| 541 |
+
display: grid;
|
| 542 |
+
grid-template-columns: repeat(3, 1fr);
|
| 543 |
+
gap: 12px;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
@keyframes cardFadeIn {
|
| 547 |
+
from { opacity: 0; transform: translateY(4px); }
|
| 548 |
+
to { opacity: 1; transform: translateY(0); }
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.model-card-header {
|
| 552 |
+
display: flex;
|
| 553 |
+
align-items: center;
|
| 554 |
+
gap: 10px;
|
| 555 |
+
margin-bottom: 4px;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.model-card-title {
|
| 559 |
+
margin: 0;
|
| 560 |
+
font-size: 1.05rem;
|
| 561 |
+
color: #1a1a2e;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.model-card-badge {
|
| 565 |
+
padding: 3px 10px;
|
| 566 |
+
border-radius: 999px;
|
| 567 |
+
background: #e7f3ff;
|
| 568 |
+
color: #007BFF;
|
| 569 |
+
font-size: 0.7rem;
|
| 570 |
+
font-weight: 800;
|
| 571 |
+
letter-spacing: 0.04em;
|
| 572 |
+
text-transform: uppercase;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.model-card-badge-baseline {
|
| 576 |
+
padding: 3px 10px;
|
| 577 |
+
border-radius: 999px;
|
| 578 |
+
background: #fff3e0;
|
| 579 |
+
color: #e67e22;
|
| 580 |
+
font-size: 0.7rem;
|
| 581 |
+
font-weight: 800;
|
| 582 |
+
letter-spacing: 0.04em;
|
| 583 |
+
text-transform: uppercase;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.model-card-tagline {
|
| 587 |
+
margin: 0 0 12px;
|
| 588 |
+
color: #667281;
|
| 589 |
+
font-size: 0.85rem;
|
| 590 |
+
line-height: 1.4;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.model-card-metrics {
|
| 594 |
+
display: grid;
|
| 595 |
+
grid-template-columns: repeat(4, 1fr);
|
| 596 |
+
gap: 8px;
|
| 597 |
+
margin-bottom: 14px;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.model-card-metric {
|
| 601 |
+
text-align: center;
|
| 602 |
+
padding: 8px 4px;
|
| 603 |
+
background: #f8fbff;
|
| 604 |
+
border: 1px solid #e8f0fe;
|
| 605 |
+
border-radius: 10px;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.model-card-metric-value {
|
| 609 |
+
display: block;
|
| 610 |
+
font-size: 1.1rem;
|
| 611 |
+
font-weight: 800;
|
| 612 |
+
color: #007BFF;
|
| 613 |
+
line-height: 1.2;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.model-card-metric-label {
|
| 617 |
+
display: block;
|
| 618 |
+
font-size: 0.65rem;
|
| 619 |
+
color: #8899aa;
|
| 620 |
+
font-weight: 700;
|
| 621 |
+
text-transform: uppercase;
|
| 622 |
+
letter-spacing: 0.04em;
|
| 623 |
+
margin-top: 2px;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.model-card-section {
|
| 627 |
+
margin-bottom: 8px;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.model-card-section h4 {
|
| 631 |
+
margin: 0 0 2px;
|
| 632 |
+
font-size: 0.78rem;
|
| 633 |
+
color: #555;
|
| 634 |
+
font-weight: 800;
|
| 635 |
+
text-transform: uppercase;
|
| 636 |
+
letter-spacing: 0.03em;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.model-card-section p {
|
| 640 |
+
margin: 0;
|
| 641 |
+
font-size: 0.82rem;
|
| 642 |
+
color: #4a4a4a;
|
| 643 |
+
line-height: 1.5;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.model-card-eval {
|
| 647 |
+
margin-top: 10px;
|
| 648 |
+
padding: 6px 10px;
|
| 649 |
+
background: #f5f7fa;
|
| 650 |
+
border-radius: 8px;
|
| 651 |
+
font-size: 0.72rem;
|
| 652 |
+
color: #7a8a9a;
|
| 653 |
+
font-weight: 600;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
@media (max-width: 768px) {
|
| 657 |
+
.model-card {
|
| 658 |
+
width: 90%;
|
| 659 |
+
}
|
| 660 |
+
.model-card-metrics {
|
| 661 |
+
grid-template-columns: repeat(2, 1fr);
|
| 662 |
+
}
|
| 663 |
+
.model-card-details {
|
| 664 |
+
grid-template-columns: 1fr;
|
| 665 |
+
}
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.focus-model-label {
|
| 669 |
+
color: #666;
|
| 670 |
+
font-size: 13px;
|
| 671 |
+
font-weight: 700;
|
| 672 |
+
margin-right: 4px;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.focus-model-button {
|
| 676 |
+
padding: 6px 16px;
|
| 677 |
+
border-radius: 16px;
|
| 678 |
+
border: 1px solid #d0d0d0;
|
| 679 |
+
background: #f5f5f5;
|
| 680 |
+
color: #555;
|
| 681 |
+
font-size: 12px;
|
| 682 |
+
font-weight: 600;
|
| 683 |
+
text-transform: uppercase;
|
| 684 |
+
cursor: pointer;
|
| 685 |
+
transition: all 0.2s;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.focus-model-button:hover {
|
| 689 |
+
border-color: #007BFF;
|
| 690 |
+
color: #007BFF;
|
| 691 |
+
background: #f0f7ff;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.focus-model-button.active {
|
| 695 |
+
border: 2px solid #007BFF;
|
| 696 |
+
background: #007BFF;
|
| 697 |
+
color: #fff;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.focus-model-sep {
|
| 701 |
+
width: 1px;
|
| 702 |
+
height: 24px;
|
| 703 |
+
background: #d0d0d0;
|
| 704 |
+
margin: 0 4px;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.eye-gaze-toggle {
|
| 708 |
+
display: inline-flex;
|
| 709 |
+
align-items: center;
|
| 710 |
+
gap: 6px;
|
| 711 |
+
padding: 6px 14px;
|
| 712 |
+
border-radius: 16px;
|
| 713 |
+
font-size: 12px;
|
| 714 |
+
font-weight: 700;
|
| 715 |
+
cursor: pointer;
|
| 716 |
+
transition: all 0.25s ease;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.eye-gaze-toggle.off {
|
| 720 |
+
border: 1px solid #d0d0d0;
|
| 721 |
+
background: #f5f5f5;
|
| 722 |
+
color: #888;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.eye-gaze-toggle.off:hover {
|
| 726 |
+
border-color: #007BFF;
|
| 727 |
+
color: #007BFF;
|
| 728 |
+
background: #f0f7ff;
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
.eye-gaze-toggle.on {
|
| 732 |
+
border: 2px solid #007BFF;
|
| 733 |
+
background: #007BFF;
|
| 734 |
+
color: #fff;
|
| 735 |
+
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.25);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
.eye-gaze-toggle.on:hover {
|
| 739 |
+
background: #0069d9;
|
| 740 |
+
border-color: #0069d9;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.eye-gaze-icon {
|
| 744 |
+
flex-shrink: 0;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.focus-model-button.recalibrate {
|
| 748 |
+
border: 1px solid #007BFF;
|
| 749 |
+
background: transparent;
|
| 750 |
+
color: #007BFF;
|
| 751 |
+
font-weight: 600;
|
| 752 |
+
font-size: 11px;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.focus-model-button.recalibrate:hover {
|
| 756 |
+
background: #f0f7ff;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.focus-system-stats {
|
| 760 |
+
display: flex;
|
| 761 |
+
align-items: center;
|
| 762 |
+
justify-content: center;
|
| 763 |
+
gap: 12px;
|
| 764 |
+
padding: 4px 16px;
|
| 765 |
+
margin: 4px auto;
|
| 766 |
+
max-width: 400px;
|
| 767 |
+
font-size: 12px;
|
| 768 |
+
color: #888;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.focus-system-stats strong {
|
| 772 |
+
color: #555;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.focus-system-stats-sep {
|
| 776 |
+
width: 1px;
|
| 777 |
+
height: 12px;
|
| 778 |
+
background: #ccc;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
#display-area video {
|
| 782 |
+
width: 100%;
|
| 783 |
+
height: 100%;
|
| 784 |
+
object-fit: cover; /* Behaves similarly to background-size: cover. */
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
/* 2. Timeline Area */
|
| 788 |
+
#timeline-area {
|
| 789 |
+
width: 60%;
|
| 790 |
+
height: 80px;
|
| 791 |
+
position: relative;
|
| 792 |
+
display: flex;
|
| 793 |
+
flex-direction: column;
|
| 794 |
+
justify-content: flex-end;
|
| 795 |
+
align-self: center;
|
| 796 |
+
margin: 0 auto;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
#timeline-visuals {
|
| 800 |
+
display: flex;
|
| 801 |
+
justify-content: center;
|
| 802 |
+
flex-wrap: wrap;
|
| 803 |
+
align-items: flex-end;
|
| 804 |
+
gap: 2px;
|
| 805 |
+
width: 100%;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.timeline-label {
|
| 809 |
+
position: absolute;
|
| 810 |
+
top: 0;
|
| 811 |
+
left: 0;
|
| 812 |
+
color: #888;
|
| 813 |
+
font-size: 14px;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
#timeline-line {
|
| 817 |
+
width: 100%;
|
| 818 |
+
height: 2px;
|
| 819 |
+
background-color: #87CEEB; /* Light blue */
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
/* 3. Control Panel */
|
| 823 |
+
#control-panel {
|
| 824 |
+
display: flex;
|
| 825 |
+
gap: 20px;
|
| 826 |
+
width: 60%;
|
| 827 |
+
justify-content: space-between;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.action-btn {
|
| 831 |
+
flex: 1; /* Evenly distributed width */
|
| 832 |
+
padding: 12px 0;
|
| 833 |
+
border: none;
|
| 834 |
+
border-radius: 12px;
|
| 835 |
+
font-size: 16px;
|
| 836 |
+
font-family: 'Nunito', sans-serif;
|
| 837 |
+
font-weight: 700;
|
| 838 |
+
cursor: pointer;
|
| 839 |
+
color: white;
|
| 840 |
+
transition: opacity 0.2s;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.action-btn:hover {
|
| 844 |
+
opacity: 0.9;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.action-btn.green { background-color: #28a745; }
|
| 848 |
+
.action-btn.blue { background-color: #007BFF; }
|
| 849 |
+
.action-btn.orange { background-color: #e67e22; }
|
| 850 |
+
.action-btn.red { background-color: #dc3545; }
|
| 851 |
+
|
| 852 |
+
/* 4. Frame Control */
|
| 853 |
+
#frame-control {
|
| 854 |
+
display: flex;
|
| 855 |
+
align-items: center;
|
| 856 |
+
gap: 15px;
|
| 857 |
+
color: #333;
|
| 858 |
+
font-weight: bold;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
#frame-slider {
|
| 862 |
+
width: 200px;
|
| 863 |
+
cursor: pointer;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
#frame-input {
|
| 867 |
+
width: 50px;
|
| 868 |
+
padding: 5px;
|
| 869 |
+
border: 1px solid #ccc;
|
| 870 |
+
border-radius: 5px;
|
| 871 |
+
text-align: center;
|
| 872 |
+
font-family: 'Nunito', sans-serif;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
/* ================ ACHIEVEMENT PAGE ================ */
|
| 876 |
+
|
| 877 |
+
.stats-grid {
|
| 878 |
+
display: grid;
|
| 879 |
+
grid-template-columns: repeat(4, 1fr);
|
| 880 |
+
gap: 20px;
|
| 881 |
+
width: 80%;
|
| 882 |
+
margin: 40px auto;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.stat-card {
|
| 886 |
+
background: white;
|
| 887 |
+
padding: 30px;
|
| 888 |
+
border-radius: 12px;
|
| 889 |
+
text-align: center;
|
| 890 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.stat-number {
|
| 894 |
+
font-size: 48px;
|
| 895 |
+
font-weight: bold;
|
| 896 |
+
color: #007BFF;
|
| 897 |
+
margin-bottom: 10px;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
.stat-label {
|
| 901 |
+
font-size: 16px;
|
| 902 |
+
color: #666;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
.achievements-section {
|
| 906 |
+
width: 80%;
|
| 907 |
+
margin: 0 auto;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.achievements-section h2 {
|
| 911 |
+
color: #333;
|
| 912 |
+
margin-bottom: 20px;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.badges-grid {
|
| 916 |
+
display: grid;
|
| 917 |
+
grid-template-columns: repeat(3, 1fr);
|
| 918 |
+
gap: 20px;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
.badge {
|
| 922 |
+
background: white;
|
| 923 |
+
padding: 30px 20px;
|
| 924 |
+
border-radius: 12px;
|
| 925 |
+
text-align: center;
|
| 926 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 927 |
+
transition: transform 0.2s;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.badge:hover {
|
| 931 |
+
transform: translateY(-5px);
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.badge.locked {
|
| 935 |
+
opacity: 0.4;
|
| 936 |
+
filter: grayscale(100%);
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
.badge-icon {
|
| 940 |
+
font-size: 64px;
|
| 941 |
+
margin-bottom: 15px;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.badge-name {
|
| 945 |
+
font-size: 16px;
|
| 946 |
+
font-weight: bold;
|
| 947 |
+
color: #333;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
/* ================ RECORDS PAGE ================ */
|
| 951 |
+
|
| 952 |
+
.records-controls {
|
| 953 |
+
display: flex;
|
| 954 |
+
gap: 10px;
|
| 955 |
+
margin: 20px auto;
|
| 956 |
+
width: 80%;
|
| 957 |
+
justify-content: center;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.filter-btn {
|
| 961 |
+
padding: 10px 20px;
|
| 962 |
+
border: 2px solid #007BFF;
|
| 963 |
+
background: white;
|
| 964 |
+
color: #007BFF;
|
| 965 |
+
border-radius: 8px;
|
| 966 |
+
cursor: pointer;
|
| 967 |
+
font-family: 'Nunito', sans-serif;
|
| 968 |
+
font-weight: 600;
|
| 969 |
+
transition: all 0.2s;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.filter-btn:hover {
|
| 973 |
+
background: #e7f3ff;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
.filter-btn.active {
|
| 977 |
+
background: #007BFF;
|
| 978 |
+
color: white;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
.chart-container {
|
| 982 |
+
width: 80%;
|
| 983 |
+
background: white;
|
| 984 |
+
padding: 30px;
|
| 985 |
+
border-radius: 12px;
|
| 986 |
+
margin: 20px auto;
|
| 987 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
#focus-chart {
|
| 991 |
+
display: block;
|
| 992 |
+
margin: 0 auto;
|
| 993 |
+
/* Make sure the chart scales within its container. */
|
| 994 |
+
max-width: 100%;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
.sessions-list {
|
| 998 |
+
width: 80%;
|
| 999 |
+
margin: 20px auto;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.sessions-list h2 {
|
| 1003 |
+
color: #333;
|
| 1004 |
+
margin-bottom: 15px;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
#sessions-table {
|
| 1008 |
+
width: 100%;
|
| 1009 |
+
background: white;
|
| 1010 |
+
border-collapse: collapse;
|
| 1011 |
+
border-radius: 12px;
|
| 1012 |
+
overflow: hidden;
|
| 1013 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
#sessions-table th {
|
| 1017 |
+
background: #007BFF;
|
| 1018 |
+
color: white;
|
| 1019 |
+
padding: 15px;
|
| 1020 |
+
text-align: left;
|
| 1021 |
+
font-weight: 600;
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
#sessions-table td {
|
| 1025 |
+
padding: 12px 15px;
|
| 1026 |
+
border-bottom: 1px solid #eee;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
#sessions-table tr:last-child td {
|
| 1030 |
+
border-bottom: none;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
#sessions-table tbody tr:hover {
|
| 1034 |
+
background: #f8f9fa;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.btn-view {
|
| 1038 |
+
padding: 6px 18px;
|
| 1039 |
+
background: #007BFF;
|
| 1040 |
+
color: white;
|
| 1041 |
+
border: none;
|
| 1042 |
+
border-radius: 999px;
|
| 1043 |
+
cursor: pointer;
|
| 1044 |
+
font-family: 'Nunito', sans-serif;
|
| 1045 |
+
font-size: 12px;
|
| 1046 |
+
font-weight: 700;
|
| 1047 |
+
transition: background 0.2s;
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
.btn-view:hover {
|
| 1051 |
+
background: #0056b3;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.records-detail-modal {
|
| 1055 |
+
width: min(960px, 92vw);
|
| 1056 |
+
max-width: 960px;
|
| 1057 |
+
max-height: 86vh;
|
| 1058 |
+
overflow-y: auto;
|
| 1059 |
+
padding: 30px;
|
| 1060 |
+
box-sizing: border-box;
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
.records-detail-header {
|
| 1064 |
+
display: flex;
|
| 1065 |
+
justify-content: space-between;
|
| 1066 |
+
align-items: flex-start;
|
| 1067 |
+
gap: 20px;
|
| 1068 |
+
margin-bottom: 24px;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
.records-detail-kicker {
|
| 1072 |
+
color: #007BFF;
|
| 1073 |
+
font-size: 12px;
|
| 1074 |
+
font-weight: 800;
|
| 1075 |
+
letter-spacing: 0.08em;
|
| 1076 |
+
text-transform: uppercase;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.records-detail-header h2 {
|
| 1080 |
+
margin: 10px 0 8px;
|
| 1081 |
+
color: #333;
|
| 1082 |
+
text-align: left;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
.records-detail-subtitle {
|
| 1086 |
+
margin: 0;
|
| 1087 |
+
color: #667281;
|
| 1088 |
+
line-height: 1.6;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
.records-detail-close {
|
| 1092 |
+
border: 1px solid #d6e6fa;
|
| 1093 |
+
background: #f4f9ff;
|
| 1094 |
+
color: #3569a8;
|
| 1095 |
+
border-radius: 999px;
|
| 1096 |
+
padding: 10px 18px;
|
| 1097 |
+
font-family: 'Nunito', sans-serif;
|
| 1098 |
+
font-weight: 700;
|
| 1099 |
+
cursor: pointer;
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
.records-detail-close:hover {
|
| 1103 |
+
border-color: #bfd9f7;
|
| 1104 |
+
background: #e9f4ff;
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.records-detail-feedback {
|
| 1108 |
+
padding: 18px 20px;
|
| 1109 |
+
border-radius: 14px;
|
| 1110 |
+
background: #f7f9fc;
|
| 1111 |
+
color: #516173;
|
| 1112 |
+
font-weight: 700;
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
.records-detail-feedback-error {
|
| 1116 |
+
background: #fff1ee;
|
| 1117 |
+
color: #b54028;
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
.records-detail-summary {
|
| 1121 |
+
display: grid;
|
| 1122 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 1123 |
+
gap: 14px;
|
| 1124 |
+
margin-bottom: 18px;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
.records-detail-stat {
|
| 1128 |
+
padding: 18px;
|
| 1129 |
+
border-radius: 14px;
|
| 1130 |
+
background: #f8fbff;
|
| 1131 |
+
border: 1px solid #d9eaff;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.records-detail-stat.excellent {
|
| 1135 |
+
background: #eef9f0;
|
| 1136 |
+
border-color: #cdebd3;
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
.records-detail-stat.good {
|
| 1140 |
+
background: #fff9eb;
|
| 1141 |
+
border-color: #f8e3a8;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.records-detail-stat.fair {
|
| 1145 |
+
background: #fff4eb;
|
| 1146 |
+
border-color: #ffd6af;
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
.records-detail-stat.low {
|
| 1150 |
+
background: #fff0f0;
|
| 1151 |
+
border-color: #f3c7c7;
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
.records-detail-stat-label {
|
| 1155 |
+
display: block;
|
| 1156 |
+
margin-bottom: 8px;
|
| 1157 |
+
color: #667281;
|
| 1158 |
+
font-size: 13px;
|
| 1159 |
+
font-weight: 700;
|
| 1160 |
+
text-transform: uppercase;
|
| 1161 |
+
letter-spacing: 0.04em;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
.records-detail-stat-value {
|
| 1165 |
+
display: block;
|
| 1166 |
+
color: #1f2d3d;
|
| 1167 |
+
font-size: 28px;
|
| 1168 |
+
line-height: 1.1;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
.records-detail-grid {
|
| 1172 |
+
display: grid;
|
| 1173 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1174 |
+
gap: 16px;
|
| 1175 |
+
margin-bottom: 16px;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.records-detail-card {
|
| 1179 |
+
background: white;
|
| 1180 |
+
border: 1px solid #e8eef5;
|
| 1181 |
+
border-radius: 16px;
|
| 1182 |
+
padding: 20px;
|
| 1183 |
+
box-shadow: 0 8px 24px rgba(20, 44, 74, 0.06);
|
| 1184 |
+
margin-bottom: 16px;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
.records-detail-card:last-child {
|
| 1188 |
+
margin-bottom: 0;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
.records-detail-card h3 {
|
| 1192 |
+
margin: 0 0 16px;
|
| 1193 |
+
color: #333;
|
| 1194 |
+
font-size: 18px;
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
.records-detail-list {
|
| 1198 |
+
display: grid;
|
| 1199 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1200 |
+
gap: 14px 18px;
|
| 1201 |
+
}
|
| 1202 |
+
|
| 1203 |
+
.records-detail-item {
|
| 1204 |
+
display: flex;
|
| 1205 |
+
flex-direction: column;
|
| 1206 |
+
gap: 6px;
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.records-detail-item-label {
|
| 1210 |
+
color: #7a8795;
|
| 1211 |
+
font-size: 12px;
|
| 1212 |
+
font-weight: 700;
|
| 1213 |
+
text-transform: uppercase;
|
| 1214 |
+
letter-spacing: 0.05em;
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
.records-detail-item-value {
|
| 1218 |
+
color: #263445;
|
| 1219 |
+
font-size: 15px;
|
| 1220 |
+
font-weight: 700;
|
| 1221 |
+
line-height: 1.5;
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
.records-detail-section-head {
|
| 1225 |
+
display: flex;
|
| 1226 |
+
align-items: center;
|
| 1227 |
+
justify-content: space-between;
|
| 1228 |
+
gap: 12px;
|
| 1229 |
+
margin-bottom: 16px;
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
.records-detail-section-head span {
|
| 1233 |
+
color: #7a8795;
|
| 1234 |
+
font-size: 13px;
|
| 1235 |
+
font-weight: 700;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
.records-detail-timeline {
|
| 1239 |
+
display: grid;
|
| 1240 |
+
grid-template-columns: repeat(auto-fit, minmax(10px, 1fr));
|
| 1241 |
+
gap: 5px;
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
.records-detail-segment {
|
| 1245 |
+
height: 48px;
|
| 1246 |
+
border-radius: 999px;
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
.records-detail-segment.focused {
|
| 1250 |
+
background: linear-gradient(180deg, #3ab86a 0%, #23a057 100%);
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
.records-detail-segment.mixed {
|
| 1254 |
+
background: linear-gradient(180deg, #f1b447 0%, #df9a1e 100%);
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.records-detail-segment.distracted {
|
| 1258 |
+
background: linear-gradient(180deg, #ec7d7d 0%, #d9534f 100%);
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.records-detail-legend {
|
| 1262 |
+
display: flex;
|
| 1263 |
+
flex-wrap: wrap;
|
| 1264 |
+
gap: 16px;
|
| 1265 |
+
margin-top: 14px;
|
| 1266 |
+
color: #667281;
|
| 1267 |
+
font-size: 13px;
|
| 1268 |
+
font-weight: 700;
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
.records-detail-legend span {
|
| 1272 |
+
display: inline-flex;
|
| 1273 |
+
align-items: center;
|
| 1274 |
+
gap: 8px;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
.records-detail-dot {
|
| 1278 |
+
width: 10px;
|
| 1279 |
+
height: 10px;
|
| 1280 |
+
border-radius: 50%;
|
| 1281 |
+
display: inline-block;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
.records-detail-dot.focused {
|
| 1285 |
+
background: #23a057;
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
.records-detail-dot.mixed {
|
| 1289 |
+
background: #df9a1e;
|
| 1290 |
+
}
|
| 1291 |
+
|
| 1292 |
+
.records-detail-dot.distracted {
|
| 1293 |
+
background: #d9534f;
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
.records-detail-events {
|
| 1297 |
+
display: grid;
|
| 1298 |
+
gap: 10px;
|
| 1299 |
+
max-height: 280px;
|
| 1300 |
+
overflow-y: auto;
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
.records-detail-event {
|
| 1304 |
+
display: grid;
|
| 1305 |
+
grid-template-columns: auto 1fr auto;
|
| 1306 |
+
align-items: center;
|
| 1307 |
+
gap: 12px;
|
| 1308 |
+
padding: 12px 14px;
|
| 1309 |
+
background: #f8fbff;
|
| 1310 |
+
border: 1px solid #e1edf9;
|
| 1311 |
+
border-radius: 14px;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
.records-detail-event-time {
|
| 1315 |
+
min-width: 52px;
|
| 1316 |
+
color: #3569a8;
|
| 1317 |
+
font-size: 13px;
|
| 1318 |
+
font-weight: 800;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
.records-detail-event-copy {
|
| 1322 |
+
min-width: 0;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.records-detail-event-status {
|
| 1326 |
+
color: #243345;
|
| 1327 |
+
font-size: 14px;
|
| 1328 |
+
font-weight: 800;
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
.records-detail-event-meta {
|
| 1332 |
+
margin-top: 4px;
|
| 1333 |
+
color: #6f7d8c;
|
| 1334 |
+
font-size: 12px;
|
| 1335 |
+
line-height: 1.5;
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
.records-detail-event-badge {
|
| 1339 |
+
padding: 7px 12px;
|
| 1340 |
+
border-radius: 999px;
|
| 1341 |
+
font-size: 11px;
|
| 1342 |
+
font-weight: 800;
|
| 1343 |
+
letter-spacing: 0.04em;
|
| 1344 |
+
text-transform: uppercase;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
.records-detail-event-badge.focused {
|
| 1348 |
+
background: #eaf8ef;
|
| 1349 |
+
color: #1f8a4c;
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
+
.records-detail-event-badge.distracted {
|
| 1353 |
+
background: #fff1f1;
|
| 1354 |
+
color: #c24c49;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
.records-detail-empty {
|
| 1358 |
+
padding: 16px 18px;
|
| 1359 |
+
border-radius: 14px;
|
| 1360 |
+
background: #f7f9fc;
|
| 1361 |
+
color: #708090;
|
| 1362 |
+
font-weight: 700;
|
| 1363 |
+
}
|
| 1364 |
+
|
| 1365 |
+
/* ================ SETTINGS PAGE ================ */
|
| 1366 |
+
|
| 1367 |
+
.settings-container {
|
| 1368 |
+
width: 60%;
|
| 1369 |
+
max-width: 800px;
|
| 1370 |
+
margin: 20px auto;
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
.setting-group {
|
| 1374 |
+
background: white;
|
| 1375 |
+
padding: 30px;
|
| 1376 |
+
border-radius: 12px;
|
| 1377 |
+
margin-bottom: 20px;
|
| 1378 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.setting-group h2 {
|
| 1382 |
+
margin-top: 0;
|
| 1383 |
+
color: #333;
|
| 1384 |
+
font-size: 20px;
|
| 1385 |
+
margin-bottom: 20px;
|
| 1386 |
+
border-bottom: 2px solid #007BFF;
|
| 1387 |
+
padding-bottom: 10px;
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
.setting-item {
|
| 1391 |
+
margin-bottom: 25px;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
.setting-item:last-child {
|
| 1395 |
+
margin-bottom: 0;
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
.setting-item label {
|
| 1399 |
+
display: block;
|
| 1400 |
+
margin-bottom: 8px;
|
| 1401 |
+
color: #333;
|
| 1402 |
+
font-weight: 600;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
.slider-group {
|
| 1406 |
+
display: flex;
|
| 1407 |
+
align-items: center;
|
| 1408 |
+
gap: 15px;
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
.slider-group input[type="range"] {
|
| 1412 |
+
flex: 1;
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
.slider-group span {
|
| 1416 |
+
min-width: 40px;
|
| 1417 |
+
text-align: center;
|
| 1418 |
+
font-weight: bold;
|
| 1419 |
+
color: #007BFF;
|
| 1420 |
+
font-size: 18px;
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
.setting-description {
|
| 1424 |
+
font-size: 14px;
|
| 1425 |
+
color: #666;
|
| 1426 |
+
margin-top: 5px;
|
| 1427 |
+
font-style: italic;
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
input[type="checkbox"] {
|
| 1431 |
+
margin-right: 10px;
|
| 1432 |
+
cursor: pointer;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
input[type="number"] {
|
| 1436 |
+
width: 100px;
|
| 1437 |
+
padding: 8px;
|
| 1438 |
+
border: 1px solid #ccc;
|
| 1439 |
+
border-radius: 5px;
|
| 1440 |
+
font-family: 'Nunito', sans-serif;
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
/* Center the settings buttons and give them more width. */
|
| 1444 |
+
.setting-group .action-btn {
|
| 1445 |
+
display: inline-block; /* Allow buttons to sit side by side. */
|
| 1446 |
+
width: 48%; /* Roughly half-width each, with a small gutter. */
|
| 1447 |
+
margin: 15px 1%; /* Vertical spacing plus horizontal separation. */
|
| 1448 |
+
text-align: center; /* Center the label text. */
|
| 1449 |
+
box-sizing: border-box; /* Prevent borders from forcing an early wrap. */
|
| 1450 |
+
}
|
| 1451 |
+
|
| 1452 |
+
#save-settings {
|
| 1453 |
+
display: block;
|
| 1454 |
+
margin: 20px auto;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
/* ================ HELP PAGE ================ */
|
| 1458 |
+
|
| 1459 |
+
.help-container {
|
| 1460 |
+
width: 70%;
|
| 1461 |
+
max-width: 900px;
|
| 1462 |
+
margin: 20px auto;
|
| 1463 |
+
}
|
| 1464 |
+
|
| 1465 |
+
/* Fake ad block (Help page) */
|
| 1466 |
+
.fake-ad {
|
| 1467 |
+
position: relative;
|
| 1468 |
+
display: block;
|
| 1469 |
+
width: min(600px, 90%);
|
| 1470 |
+
margin: 10px auto 30px auto;
|
| 1471 |
+
border: 1px solid #e5e5e5;
|
| 1472 |
+
border-radius: 12px;
|
| 1473 |
+
overflow: hidden;
|
| 1474 |
+
background: #fff;
|
| 1475 |
+
text-decoration: none;
|
| 1476 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
| 1477 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
.fake-ad:hover {
|
| 1481 |
+
transform: translateY(-2px);
|
| 1482 |
+
box-shadow: 0 12px 30px rgba(0,0,0,0.16);
|
| 1483 |
+
}
|
| 1484 |
+
|
| 1485 |
+
.fake-ad img {
|
| 1486 |
+
display: block;
|
| 1487 |
+
width: 100%;
|
| 1488 |
+
height: auto;
|
| 1489 |
+
}
|
| 1490 |
+
|
| 1491 |
+
.fake-ad-badge {
|
| 1492 |
+
position: absolute;
|
| 1493 |
+
top: 12px;
|
| 1494 |
+
left: 12px;
|
| 1495 |
+
background: rgba(0,0,0,0.75);
|
| 1496 |
+
color: #fff;
|
| 1497 |
+
font-size: 12px;
|
| 1498 |
+
padding: 4px 8px;
|
| 1499 |
+
border-radius: 6px;
|
| 1500 |
+
letter-spacing: 0.5px;
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
.fake-ad-cta {
|
| 1504 |
+
position: absolute;
|
| 1505 |
+
right: 12px;
|
| 1506 |
+
bottom: 12px;
|
| 1507 |
+
background: #111;
|
| 1508 |
+
color: #fff;
|
| 1509 |
+
font-size: 14px;
|
| 1510 |
+
padding: 8px 12px;
|
| 1511 |
+
border-radius: 8px;
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
.help-section {
|
| 1515 |
+
background: white;
|
| 1516 |
+
padding: 30px;
|
| 1517 |
+
border-radius: 12px;
|
| 1518 |
+
margin-bottom: 20px;
|
| 1519 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.help-section h2 {
|
| 1523 |
+
color: #007BFF;
|
| 1524 |
+
margin-top: 0;
|
| 1525 |
+
margin-bottom: 15px;
|
| 1526 |
+
}
|
| 1527 |
+
|
| 1528 |
+
.help-section ol,
|
| 1529 |
+
.help-section ul {
|
| 1530 |
+
line-height: 1.8;
|
| 1531 |
+
color: #333;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.help-section p {
|
| 1535 |
+
line-height: 1.6;
|
| 1536 |
+
color: #333;
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
details {
|
| 1540 |
+
margin: 15px 0;
|
| 1541 |
+
cursor: pointer;
|
| 1542 |
+
padding: 10px;
|
| 1543 |
+
background: #f8f9fa;
|
| 1544 |
+
border-radius: 5px;
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
summary {
|
| 1548 |
+
font-weight: bold;
|
| 1549 |
+
padding: 5px;
|
| 1550 |
+
color: #007BFF;
|
| 1551 |
+
}
|
| 1552 |
+
|
| 1553 |
+
details[open] summary {
|
| 1554 |
+
margin-bottom: 10px;
|
| 1555 |
+
border-bottom: 1px solid #ddd;
|
| 1556 |
+
padding-bottom: 10px;
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
details p {
|
| 1560 |
+
margin: 10px 0 0 0;
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
/* ================ SESSION SUMMARY MODAL ================ */
|
| 1564 |
+
/* These modal styles can be reused for future overlays. */
|
| 1565 |
+
.modal-overlay {
|
| 1566 |
+
position: fixed;
|
| 1567 |
+
top: 0;
|
| 1568 |
+
left: 0;
|
| 1569 |
+
width: 100%;
|
| 1570 |
+
height: 100%;
|
| 1571 |
+
background: rgba(0, 0, 0, 0.7);
|
| 1572 |
+
display: flex;
|
| 1573 |
+
align-items: center;
|
| 1574 |
+
justify-content: center;
|
| 1575 |
+
z-index: 2000;
|
| 1576 |
+
}
|
| 1577 |
+
|
| 1578 |
+
.modal-content {
|
| 1579 |
+
background: white;
|
| 1580 |
+
padding: 40px;
|
| 1581 |
+
border-radius: 16px;
|
| 1582 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
| 1583 |
+
max-width: 500px;
|
| 1584 |
+
width: 90%;
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
.modal-content h2 {
|
| 1588 |
+
margin-top: 0;
|
| 1589 |
+
color: #333;
|
| 1590 |
+
text-align: center;
|
| 1591 |
+
margin-bottom: 30px;
|
| 1592 |
+
}
|
| 1593 |
+
|
| 1594 |
+
.summary-stats {
|
| 1595 |
+
margin-bottom: 30px;
|
| 1596 |
+
}
|
| 1597 |
+
|
| 1598 |
+
.summary-item {
|
| 1599 |
+
display: flex;
|
| 1600 |
+
justify-content: space-between;
|
| 1601 |
+
padding: 15px 0;
|
| 1602 |
+
border-bottom: 1px solid #eee;
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
.summary-item:last-child {
|
| 1606 |
+
border-bottom: none;
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
.summary-label {
|
| 1610 |
+
font-weight: 600;
|
| 1611 |
+
color: #666;
|
| 1612 |
+
}
|
| 1613 |
+
|
| 1614 |
+
.summary-value {
|
| 1615 |
+
font-weight: bold;
|
| 1616 |
+
color: #007BFF;
|
| 1617 |
+
font-size: 18px;
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
.modal-content .btn-main {
|
| 1621 |
+
display: block;
|
| 1622 |
+
margin: 0 auto;
|
| 1623 |
+
padding: 12px 40px;
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
/* ================ TIMELINE BLOCKS ================ */
|
| 1627 |
+
|
| 1628 |
+
.timeline-block {
|
| 1629 |
+
transition: opacity 0.2s;
|
| 1630 |
+
border-radius: 2px;
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
.timeline-block:hover {
|
| 1634 |
+
opacity: 0.7;
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
/* ================ RESPONSIVE DESIGN ================ */
|
| 1638 |
+
|
| 1639 |
+
@media (max-width: 1200px) {
|
| 1640 |
+
.stats-grid {
|
| 1641 |
+
grid-template-columns: repeat(2, 1fr);
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
.badges-grid {
|
| 1645 |
+
grid-template-columns: repeat(2, 1fr);
|
| 1646 |
+
}
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
@media (max-width: 768px) {
|
| 1650 |
+
.stats-grid,
|
| 1651 |
+
.badges-grid {
|
| 1652 |
+
grid-template-columns: 1fr;
|
| 1653 |
+
width: 90%;
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
.settings-container,
|
| 1657 |
+
.help-container,
|
| 1658 |
+
.chart-container,
|
| 1659 |
+
.sessions-list,
|
| 1660 |
+
.records-controls {
|
| 1661 |
+
width: 90%;
|
| 1662 |
+
}
|
| 1663 |
+
|
| 1664 |
+
#control-panel {
|
| 1665 |
+
width: 90%;
|
| 1666 |
+
flex-wrap: wrap;
|
| 1667 |
+
}
|
| 1668 |
+
|
| 1669 |
+
#display-area {
|
| 1670 |
+
width: 90%;
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
#timeline-area {
|
| 1674 |
+
width: 90%;
|
| 1675 |
+
}
|
| 1676 |
+
|
| 1677 |
+
#frame-control {
|
| 1678 |
+
width: 90%;
|
| 1679 |
+
flex-direction: column;
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
+
.focus-inline-error-standalone {
|
| 1683 |
+
width: 90%;
|
| 1684 |
+
}
|
| 1685 |
+
|
| 1686 |
+
.focus-flow-overlay {
|
| 1687 |
+
top: 70px;
|
| 1688 |
+
right: 10px;
|
| 1689 |
+
bottom: 10px;
|
| 1690 |
+
left: 10px;
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
.focus-flow-card {
|
| 1694 |
+
padding: 22px 20px;
|
| 1695 |
+
}
|
| 1696 |
+
|
| 1697 |
+
.focus-flow-header {
|
| 1698 |
+
flex-direction: column;
|
| 1699 |
+
align-items: flex-start;
|
| 1700 |
+
}
|
| 1701 |
+
|
| 1702 |
+
.focus-flow-icon {
|
| 1703 |
+
width: 92px;
|
| 1704 |
+
height: 92px;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
.focus-flow-grid {
|
| 1708 |
+
grid-template-columns: 1fr;
|
| 1709 |
+
}
|
| 1710 |
+
|
| 1711 |
+
.focus-flow-steps {
|
| 1712 |
+
grid-template-columns: 1fr;
|
| 1713 |
+
}
|
| 1714 |
+
|
| 1715 |
+
.focus-flow-footer {
|
| 1716 |
+
flex-direction: column;
|
| 1717 |
+
align-items: stretch;
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
.focus-flow-button,
|
| 1721 |
+
.focus-flow-secondary {
|
| 1722 |
+
width: 100%;
|
| 1723 |
+
}
|
| 1724 |
+
|
| 1725 |
+
.records-detail-modal {
|
| 1726 |
+
width: 94vw;
|
| 1727 |
+
padding: 22px 18px;
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
.records-detail-header,
|
| 1731 |
+
.records-detail-section-head {
|
| 1732 |
+
flex-direction: column;
|
| 1733 |
+
align-items: flex-start;
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
+
.records-detail-summary,
|
| 1737 |
+
.records-detail-grid,
|
| 1738 |
+
.records-detail-list {
|
| 1739 |
+
grid-template-columns: 1fr;
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
.records-detail-event {
|
| 1743 |
+
grid-template-columns: 1fr;
|
| 1744 |
+
align-items: flex-start;
|
| 1745 |
+
}
|
| 1746 |
+
}
|
| 1747 |
+
/* =========================================
|
| 1748 |
+
SESSION RESULT OVERLAY
|
| 1749 |
+
========================================= */
|
| 1750 |
+
|
| 1751 |
+
.session-result-overlay {
|
| 1752 |
+
position: absolute;
|
| 1753 |
+
top: 0;
|
| 1754 |
+
left: 0;
|
| 1755 |
+
width: 100%;
|
| 1756 |
+
height: 100%;
|
| 1757 |
+
background-color: rgba(0, 0, 0, 0.85); /* Dark semi-transparent backdrop. */
|
| 1758 |
+
display: flex;
|
| 1759 |
+
flex-direction: column;
|
| 1760 |
+
justify-content: center;
|
| 1761 |
+
align-items: center;
|
| 1762 |
+
color: white;
|
| 1763 |
+
z-index: 10;
|
| 1764 |
+
animation: fadeIn 0.5s ease;
|
| 1765 |
+
backdrop-filter: blur(5px); /* Optional background blur. */
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
.session-result-overlay h3 {
|
| 1769 |
+
font-size: 32px;
|
| 1770 |
+
margin-bottom: 30px;
|
| 1771 |
+
color: #4cd137; /* Green title accent. */
|
| 1772 |
+
text-transform: uppercase;
|
| 1773 |
+
letter-spacing: 2px;
|
| 1774 |
+
}
|
| 1775 |
+
|
| 1776 |
+
.session-result-overlay .result-item {
|
| 1777 |
+
display: flex;
|
| 1778 |
+
justify-content: space-between;
|
| 1779 |
+
width: 200px; /* Keep the stat row compact. */
|
| 1780 |
+
margin-bottom: 15px;
|
| 1781 |
+
font-size: 20px;
|
| 1782 |
+
border-bottom: 1px solid rgba(255,255,255,0.2);
|
| 1783 |
+
padding-bottom: 5px;
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
.session-result-overlay .label {
|
| 1787 |
+
color: #ccc;
|
| 1788 |
+
font-weight: normal;
|
| 1789 |
+
}
|
| 1790 |
+
|
| 1791 |
+
.session-result-overlay .value {
|
| 1792 |
+
color: #fff;
|
| 1793 |
+
font-weight: bold;
|
| 1794 |
+
font-family: 'Courier New', monospace; /* Give the values a data-like look. */
|
| 1795 |
+
}
|
| 1796 |
+
|
| 1797 |
+
@keyframes fadeIn {
|
| 1798 |
+
from { opacity: 0; transform: scale(0.95); }
|
| 1799 |
+
to { opacity: 1; transform: scale(1); }
|
| 1800 |
+
}
|
| 1801 |
+
|
| 1802 |
+
/* ================= Welcome modal styles ================= */
|
| 1803 |
+
.welcome-modal-overlay {
|
| 1804 |
+
position: fixed;
|
| 1805 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 1806 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 1807 |
+
display: flex;
|
| 1808 |
+
justify-content: center;
|
| 1809 |
+
align-items: center;
|
| 1810 |
+
z-index: 9999;
|
| 1811 |
+
}
|
| 1812 |
+
|
| 1813 |
+
.welcome-modal {
|
| 1814 |
+
background-color: #1e1e24;
|
| 1815 |
+
padding: 40px;
|
| 1816 |
+
border-radius: 15px;
|
| 1817 |
+
text-align: center;
|
| 1818 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 1819 |
+
border: 1px solid #333;
|
| 1820 |
+
}
|
| 1821 |
+
|
| 1822 |
+
.welcome-modal h2 { margin-top: 0; color: #fff; }
|
| 1823 |
+
.welcome-modal p { margin-bottom: 30px; color: #ccc; }
|
| 1824 |
+
.welcome-buttons { display: flex; gap: 20px; justify-content: center; }
|
| 1825 |
+
|
| 1826 |
+
/* ================= Top-left avatar styles ================= */
|
| 1827 |
+
.avatar-container {
|
| 1828 |
+
position: absolute;
|
| 1829 |
+
left: 20px;
|
| 1830 |
+
cursor: pointer;
|
| 1831 |
+
z-index: 1;
|
| 1832 |
+
}
|
| 1833 |
+
|
| 1834 |
+
.avatar-circle {
|
| 1835 |
+
width: 40px;
|
| 1836 |
+
height: 40px;
|
| 1837 |
+
border-radius: 50%;
|
| 1838 |
+
display: flex;
|
| 1839 |
+
justify-content: center;
|
| 1840 |
+
align-items: center;
|
| 1841 |
+
font-weight: bold;
|
| 1842 |
+
font-size: 1.2rem;
|
| 1843 |
+
color: white;
|
| 1844 |
+
transition: all 0.3s ease;
|
| 1845 |
+
border: 2px solid transparent;
|
| 1846 |
+
}
|
| 1847 |
+
|
| 1848 |
+
.avatar-circle.user { background-color: #555; }
|
| 1849 |
+
.avatar-circle.admin { background-color: #ffaa00; border-color: #fff; box-shadow: 0 0 10px rgba(255, 170, 0, 0.5); }
|
| 1850 |
+
|
| 1851 |
+
/* ================ CALIBRATION OVERLAY ================ */
|
| 1852 |
+
|
| 1853 |
+
.cal-overlay {
|
| 1854 |
+
position: fixed;
|
| 1855 |
+
top: 0;
|
| 1856 |
+
left: 0;
|
| 1857 |
+
width: 100vw;
|
| 1858 |
+
height: 100vh;
|
| 1859 |
+
background: rgba(8, 15, 28, 0.94);
|
| 1860 |
+
backdrop-filter: blur(6px);
|
| 1861 |
+
z-index: 10000;
|
| 1862 |
+
display: flex;
|
| 1863 |
+
align-items: center;
|
| 1864 |
+
justify-content: center;
|
| 1865 |
+
font-family: 'Nunito', sans-serif;
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
/* ---- header / instructions ---- */
|
| 1869 |
+
.cal-header {
|
| 1870 |
+
position: absolute;
|
| 1871 |
+
top: 36px;
|
| 1872 |
+
left: 50%;
|
| 1873 |
+
transform: translateX(-50%);
|
| 1874 |
+
text-align: center;
|
| 1875 |
+
pointer-events: none;
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
.cal-eyebrow {
|
| 1879 |
+
display: inline-block;
|
| 1880 |
+
padding: 6px 14px;
|
| 1881 |
+
border-radius: 999px;
|
| 1882 |
+
font-size: 0.82rem;
|
| 1883 |
+
font-weight: 800;
|
| 1884 |
+
letter-spacing: 0.04em;
|
| 1885 |
+
text-transform: uppercase;
|
| 1886 |
+
}
|
| 1887 |
+
|
| 1888 |
+
.cal-eyebrow-collect {
|
| 1889 |
+
background: rgba(40, 167, 69, 0.18);
|
| 1890 |
+
color: #5ee882;
|
| 1891 |
+
}
|
| 1892 |
+
|
| 1893 |
+
.cal-eyebrow-verify {
|
| 1894 |
+
background: rgba(0, 123, 255, 0.18);
|
| 1895 |
+
color: #6bb8ff;
|
| 1896 |
+
}
|
| 1897 |
+
|
| 1898 |
+
.cal-instruction {
|
| 1899 |
+
margin: 10px 0 0;
|
| 1900 |
+
color: rgba(255, 255, 255, 0.7);
|
| 1901 |
+
font-size: 0.95rem;
|
| 1902 |
+
line-height: 1.5;
|
| 1903 |
+
}
|
| 1904 |
+
|
| 1905 |
+
/* ---- target dot + ring ---- */
|
| 1906 |
+
.cal-target {
|
| 1907 |
+
position: absolute;
|
| 1908 |
+
transform: translate(-50%, -50%);
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
.cal-ring {
|
| 1912 |
+
position: absolute;
|
| 1913 |
+
left: -30px;
|
| 1914 |
+
top: -30px;
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
.cal-dot {
|
| 1918 |
+
width: 20px;
|
| 1919 |
+
height: 20px;
|
| 1920 |
+
border-radius: 50%;
|
| 1921 |
+
transition: box-shadow 0.3s ease;
|
| 1922 |
+
}
|
| 1923 |
+
|
| 1924 |
+
/* ---- cancel button (matches focus-flow-secondary) ---- */
|
| 1925 |
+
.cal-cancel {
|
| 1926 |
+
position: absolute;
|
| 1927 |
+
bottom: 40px;
|
| 1928 |
+
left: 50%;
|
| 1929 |
+
transform: translateX(-50%);
|
| 1930 |
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
| 1931 |
+
border-radius: 999px;
|
| 1932 |
+
padding: 12px 28px;
|
| 1933 |
+
background: rgba(255, 255, 255, 0.08);
|
| 1934 |
+
color: rgba(255, 255, 255, 0.85);
|
| 1935 |
+
font-family: 'Nunito', sans-serif;
|
| 1936 |
+
font-size: 0.95rem;
|
| 1937 |
+
font-weight: 700;
|
| 1938 |
+
cursor: pointer;
|
| 1939 |
+
transition: background 0.2s ease, border-color 0.2s ease;
|
| 1940 |
+
}
|
| 1941 |
+
|
| 1942 |
+
.cal-cancel:hover {
|
| 1943 |
+
background: rgba(255, 255, 255, 0.14);
|
| 1944 |
+
border-color: rgba(255, 255, 255, 0.4);
|
| 1945 |
+
}
|
| 1946 |
+
|
| 1947 |
+
/* ---- done card (matches focus-flow-card style) ---- */
|
| 1948 |
+
.cal-done-card {
|
| 1949 |
+
text-align: center;
|
| 1950 |
+
padding: 36px 44px;
|
| 1951 |
+
border-radius: 20px;
|
| 1952 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 1953 |
+
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.4);
|
| 1954 |
+
animation: fadeIn 0.4s ease;
|
| 1955 |
+
}
|
| 1956 |
+
|
| 1957 |
+
.cal-done-success {
|
| 1958 |
+
background: linear-gradient(168deg, rgba(40, 167, 69, 0.15) 0%, rgba(20, 30, 48, 0.95) 60%);
|
| 1959 |
+
border-color: rgba(40, 167, 69, 0.3);
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
.cal-done-fail {
|
| 1963 |
+
background: linear-gradient(168deg, rgba(220, 53, 69, 0.15) 0%, rgba(20, 30, 48, 0.95) 60%);
|
| 1964 |
+
border-color: rgba(220, 53, 69, 0.3);
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
.cal-done-eyebrow {
|
| 1968 |
+
display: inline-block;
|
| 1969 |
+
padding: 6px 14px;
|
| 1970 |
+
border-radius: 999px;
|
| 1971 |
+
font-size: 0.78rem;
|
| 1972 |
+
font-weight: 800;
|
| 1973 |
+
letter-spacing: 0.06em;
|
| 1974 |
+
text-transform: uppercase;
|
| 1975 |
+
margin-bottom: 14px;
|
| 1976 |
+
}
|
| 1977 |
+
|
| 1978 |
+
.cal-done-success .cal-done-eyebrow {
|
| 1979 |
+
background: rgba(40, 167, 69, 0.2);
|
| 1980 |
+
color: #5ee882;
|
| 1981 |
+
}
|
| 1982 |
+
|
| 1983 |
+
.cal-done-fail .cal-done-eyebrow {
|
| 1984 |
+
background: rgba(220, 53, 69, 0.2);
|
| 1985 |
+
color: #f87171;
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
.cal-done-title {
|
| 1989 |
+
margin: 0 0 8px;
|
| 1990 |
+
font-size: 1.6rem;
|
| 1991 |
+
color: #fff;
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.cal-done-subtitle {
|
| 1995 |
+
margin: 0;
|
| 1996 |
+
color: rgba(255, 255, 255, 0.6);
|
| 1997 |
+
font-size: 0.95rem;
|
| 1998 |
+
line-height: 1.5;
|
| 1999 |
+
}
|
| 2000 |
+
/* ================= Home page 2x2 responsive button grid ================= */
|
| 2001 |
+
.home-button-grid {
|
| 2002 |
+
display: flex;
|
| 2003 |
+
justify-content: center;
|
| 2004 |
+
width: 100%;
|
| 2005 |
+
max-width: 360px;
|
| 2006 |
+
margin: 40px auto 0;
|
| 2007 |
+
}
|
| 2008 |
+
|
| 2009 |
+
.home-button-grid .btn-main {
|
| 2010 |
+
width: 100%;
|
| 2011 |
+
height: 60px; /* Keep all tiles at the same height. */
|
| 2012 |
+
margin: 0; /* Remove default outer spacing. */
|
| 2013 |
+
padding: 10px;
|
| 2014 |
+
font-size: 1rem;
|
| 2015 |
+
display: flex;
|
| 2016 |
+
justify-content: center;
|
| 2017 |
+
align-items: center;
|
| 2018 |
+
text-align: center;
|
| 2019 |
+
box-sizing: border-box; /* Prevent padding and borders from breaking the grid. */
|
| 2020 |
+
}
|
| 2021 |
+
|
| 2022 |
+
/* Mobile-only scaling for screens below 600px. */
|
| 2023 |
+
@media (max-width: 600px) {
|
| 2024 |
+
#top-menu {
|
| 2025 |
+
justify-content: flex-start;
|
| 2026 |
+
padding: 0 12px 0 68px;
|
| 2027 |
+
}
|
| 2028 |
+
|
| 2029 |
+
.menu-btn {
|
| 2030 |
+
padding: 10px 14px;
|
| 2031 |
+
font-size: 0.92rem;
|
| 2032 |
+
}
|
| 2033 |
+
|
| 2034 |
+
.separator {
|
| 2035 |
+
margin: 0 2px;
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
.home-button-grid {
|
| 2039 |
+
gap: 15px;
|
| 2040 |
+
max-width: 90%;
|
| 2041 |
+
}
|
| 2042 |
+
|
| 2043 |
+
.home-button-grid .btn-main {
|
| 2044 |
+
height: 50px;
|
| 2045 |
+
font-size: 0.85rem;
|
| 2046 |
+
}
|
| 2047 |
+
}
|
src/src/App.jsx
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import './App.css';
|
| 3 |
+
import { VideoManagerLocal } from './utils/VideoManagerLocal';
|
| 4 |
+
|
| 5 |
+
import Home from './components/Home';
|
| 6 |
+
import FocusPageLocal from './components/FocusPageLocal';
|
| 7 |
+
import Achievement from './components/Achievement';
|
| 8 |
+
import Records from './components/Records';
|
| 9 |
+
import Help from './components/Help';
|
| 10 |
+
|
| 11 |
+
function App() {
|
| 12 |
+
const [activeTab, setActiveTab] = useState('home');
|
| 13 |
+
const videoManagerRef = useRef(null);
|
| 14 |
+
const [isSessionActive, setIsSessionActive] = useState(false);
|
| 15 |
+
const [sessionResult, setSessionResult] = useState(null);
|
| 16 |
+
const [isTutorialActive, setIsTutorialActive] = useState(false);
|
| 17 |
+
const [hasSeenTutorial, setHasSeenTutorial] = useState(false);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
fetch('/api/history', { method: 'DELETE' })
|
| 21 |
+
.then(() => {
|
| 22 |
+
const backup = localStorage.getItem('focus_magic_backup');
|
| 23 |
+
if (backup) {
|
| 24 |
+
try {
|
| 25 |
+
const sessions = JSON.parse(backup);
|
| 26 |
+
fetch('/api/import', {
|
| 27 |
+
method: 'POST',
|
| 28 |
+
headers: { 'Content-Type': 'application/json' },
|
| 29 |
+
body: JSON.stringify(sessions)
|
| 30 |
+
});
|
| 31 |
+
} catch (err) {
|
| 32 |
+
console.error(err);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
})
|
| 36 |
+
.catch(err => console.error(err));
|
| 37 |
+
|
| 38 |
+
const callbacks = {
|
| 39 |
+
onSessionStart: () => {
|
| 40 |
+
setIsSessionActive(true);
|
| 41 |
+
setSessionResult(null);
|
| 42 |
+
},
|
| 43 |
+
onSessionEnd: (summary) => {
|
| 44 |
+
setIsSessionActive(false);
|
| 45 |
+
if (summary) setSessionResult(summary);
|
| 46 |
+
|
| 47 |
+
fetch('/api/sessions?filter=all')
|
| 48 |
+
.then(res => res.json())
|
| 49 |
+
.then(data => {
|
| 50 |
+
if (data && Array.isArray(data)) {
|
| 51 |
+
localStorage.setItem('focus_magic_backup', JSON.stringify(data));
|
| 52 |
+
}
|
| 53 |
+
})
|
| 54 |
+
.catch(err => console.error(err));
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
videoManagerRef.current = new VideoManagerLocal(callbacks);
|
| 58 |
+
|
| 59 |
+
return () => {
|
| 60 |
+
if (videoManagerRef.current) videoManagerRef.current.stopStreaming();
|
| 61 |
+
};
|
| 62 |
+
}, []);
|
| 63 |
+
|
| 64 |
+
const handleStartFocus = () => {
|
| 65 |
+
if (!hasSeenTutorial) {
|
| 66 |
+
setIsTutorialActive(true);
|
| 67 |
+
}
|
| 68 |
+
setActiveTab('focus');
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const handleStartTutorial = () => {
|
| 72 |
+
setIsTutorialActive(true);
|
| 73 |
+
setActiveTab('focus');
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
return (
|
| 77 |
+
<div className="app-container">
|
| 78 |
+
<nav id="top-menu">
|
| 79 |
+
<button
|
| 80 |
+
className={`menu-btn ${activeTab === 'home' ? 'active' : ''}`}
|
| 81 |
+
onClick={() => setActiveTab('home')}
|
| 82 |
+
>
|
| 83 |
+
Home
|
| 84 |
+
</button>
|
| 85 |
+
<div className="separator"></div>
|
| 86 |
+
|
| 87 |
+
<button className={`menu-btn ${activeTab === 'focus' ? 'active' : ''}`} onClick={handleStartFocus}>
|
| 88 |
+
Start Focus {isSessionActive && <span style={{ marginLeft: '8px', color: '#00FF00' }}>●</span>}
|
| 89 |
+
</button>
|
| 90 |
+
<div className="separator"></div>
|
| 91 |
+
|
| 92 |
+
<button className={`menu-btn ${activeTab === 'achievement' ? 'active' : ''}`} onClick={() => setActiveTab('achievement')}>
|
| 93 |
+
My Achievement
|
| 94 |
+
</button>
|
| 95 |
+
<div className="separator"></div>
|
| 96 |
+
|
| 97 |
+
<button className={`menu-btn ${activeTab === 'records' ? 'active' : ''}`} onClick={() => setActiveTab('records')}>
|
| 98 |
+
My Records
|
| 99 |
+
</button>
|
| 100 |
+
<div className="separator"></div>
|
| 101 |
+
|
| 102 |
+
<button className={`menu-btn ${activeTab === 'help' ? 'active' : ''}`} onClick={() => setActiveTab('help')}>
|
| 103 |
+
Help
|
| 104 |
+
</button>
|
| 105 |
+
</nav>
|
| 106 |
+
|
| 107 |
+
{activeTab === 'home' && <Home onStartFocus={handleStartFocus} onStartTutorial={handleStartTutorial} />}
|
| 108 |
+
|
| 109 |
+
<FocusPageLocal
|
| 110 |
+
videoManager={videoManagerRef.current}
|
| 111 |
+
sessionResult={sessionResult}
|
| 112 |
+
setSessionResult={setSessionResult}
|
| 113 |
+
isActive={activeTab === 'focus'}
|
| 114 |
+
isTutorialActive={isTutorialActive}
|
| 115 |
+
setIsTutorialActive={setIsTutorialActive}
|
| 116 |
+
setHasSeenTutorial={setHasSeenTutorial}
|
| 117 |
+
/>
|
| 118 |
+
{activeTab === 'achievement' && <Achievement />}
|
| 119 |
+
{activeTab === 'records' && <Records />}
|
| 120 |
+
{activeTab === 'help' && <Help />}
|
| 121 |
+
</div>
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export default App;
|
ui/pipeline.py
CHANGED
|
@@ -442,11 +442,14 @@ class HybridFocusPipeline:
|
|
| 442 |
if not os.path.isfile(resolved_combiner):
|
| 443 |
resolved_combiner = os.path.join(_PROJECT_ROOT, combiner_path)
|
| 444 |
if os.path.isfile(resolved_combiner):
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
self._combiner
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
| 450 |
else:
|
| 451 |
print(f"[HYBRID] combiner_path not found: {resolved_combiner}, using heuristic weights")
|
| 452 |
if self._use_xgb:
|
|
|
|
| 442 |
if not os.path.isfile(resolved_combiner):
|
| 443 |
resolved_combiner = os.path.join(_PROJECT_ROOT, combiner_path)
|
| 444 |
if os.path.isfile(resolved_combiner):
|
| 445 |
+
try:
|
| 446 |
+
blob = joblib.load(resolved_combiner)
|
| 447 |
+
self._combiner = blob.get("combiner")
|
| 448 |
+
if self._combiner is None:
|
| 449 |
+
self._combiner = blob
|
| 450 |
+
print(f"[HYBRID] LR combiner loaded from {resolved_combiner}")
|
| 451 |
+
except Exception as e:
|
| 452 |
+
print(f"[HYBRID] Failed to load combiner ({e}); using heuristic weights")
|
| 453 |
else:
|
| 454 |
print(f"[HYBRID] combiner_path not found: {resolved_combiner}, using heuristic weights")
|
| 455 |
if self._use_xgb:
|