Fu01978 commited on
Commit
a1949a6
·
verified ·
1 Parent(s): c588584

Delete templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +0 -1902
templates/index.html DELETED
@@ -1,1902 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
- <title>Chess Analyzer</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap" rel="stylesheet">
9
-
10
- <style>
11
- /* ── Root Variables ─────────────────────────────────────────────── */
12
- :root {
13
- --bg: #0c0c0e;
14
- --surface: #141416;
15
- --card: #1c1c20;
16
- --card2: #222228;
17
- --border: #2e2e36;
18
- --border-soft: #252530;
19
- --text: #e4dfd6;
20
- --text-dim: #7a7068;
21
- --text-muted: #4a4848;
22
- --gold: #c8a84c;
23
- --gold-dim: #8a7030;
24
-
25
- --sq-light: #f0d9b5;
26
- --sq-dark: #b58863;
27
- --sq-hi-light: #cdd26a;
28
- --sq-hi-dark: #aaa23a;
29
- --sq-hi-best: rgba(0, 160, 80, 0.5);
30
-
31
- --cl-brilliant: #00b5ad;
32
- --cl-great: #1a4fc4;
33
- --cl-best: #7ec820;
34
- --cl-excellent: #7ec820;
35
- --cl-good: #4db896;
36
- --cl-book: #a0754a;
37
- --cl-inaccuracy:#d4b030;
38
- --cl-mistake: #d07020;
39
- --cl-blunder: #c83030;
40
- --cl-omission: #e05050;
41
-
42
- --board-size: 480px;
43
- --sq-size: 60px;
44
- }
45
-
46
- * { box-sizing: border-box; margin: 0; padding: 0; }
47
-
48
- html, body {
49
- height: 100%;
50
- background: var(--bg);
51
- color: var(--text);
52
- font-family: 'Crimson Pro', Georgia, serif;
53
- font-size: 16px;
54
- line-height: 1.5;
55
- }
56
-
57
- /* ─────────────────────────────────────────────────────────────────
58
- SCREENS
59
- ───────────────────────────────────────────────────────────────── */
60
- .screen { display: none; min-height: 100vh; flex-direction: column; }
61
- .screen.active { display: flex; }
62
-
63
- /* ── Header ──────────────────────────────────────────────────── */
64
- header {
65
- display: flex;
66
- align-items: center;
67
- gap: 14px;
68
- padding: 18px 32px;
69
- border-bottom: 1px solid var(--border);
70
- background: var(--surface);
71
- flex-shrink: 0;
72
- }
73
-
74
- .logo {
75
- font-family: 'Cinzel', serif;
76
- font-size: 1.3rem;
77
- font-weight: 700;
78
- color: var(--gold);
79
- letter-spacing: 0.04em;
80
- }
81
-
82
- .logo-sub {
83
- font-family: 'Cinzel', serif;
84
- font-size: 0.7rem;
85
- color: var(--text-dim);
86
- letter-spacing: 0.12em;
87
- text-transform: uppercase;
88
- }
89
-
90
- /* ── Upload Screen ────────────────────────────────────────────── */
91
- #upload-screen {
92
- align-items: center;
93
- justify-content: center;
94
- gap: 0;
95
- }
96
-
97
- .upload-box {
98
- width: 560px;
99
- max-width: 95vw;
100
- }
101
-
102
- .upload-title {
103
- font-family: 'Cinzel', serif;
104
- font-size: 2rem;
105
- font-weight: 600;
106
- color: var(--text);
107
- margin-bottom: 6px;
108
- }
109
-
110
- .upload-sub {
111
- color: var(--text-dim);
112
- font-size: 1rem;
113
- font-style: italic;
114
- margin-bottom: 32px;
115
- }
116
-
117
- .drop-zone {
118
- border: 1.5px dashed var(--border);
119
- border-radius: 10px;
120
- padding: 24px;
121
- background: var(--card);
122
- transition: border-color 0.2s, background 0.2s;
123
- cursor: pointer;
124
- }
125
-
126
- .drop-zone:hover, .drop-zone.dragover {
127
- border-color: var(--gold);
128
- background: var(--card2);
129
- }
130
-
131
- .drop-zone-label {
132
- font-size: 0.85rem;
133
- color: var(--text-dim);
134
- text-align: center;
135
- margin-bottom: 12px;
136
- }
137
-
138
- .drop-zone-label span { color: var(--gold); cursor: pointer; }
139
-
140
- #pgn-textarea {
141
- width: 100%;
142
- height: 180px;
143
- background: var(--card2);
144
- border: 1px solid var(--border-soft);
145
- border-radius: 6px;
146
- color: var(--text);
147
- font-family: 'Crimson Pro', monospace;
148
- font-size: 0.88rem;
149
- padding: 12px;
150
- resize: vertical;
151
- outline: none;
152
- transition: border-color 0.2s;
153
- }
154
- #pgn-textarea:focus { border-color: var(--gold-dim); }
155
- #pgn-textarea::placeholder { color: var(--text-muted); }
156
-
157
- .upload-row {
158
- display: flex;
159
- align-items: center;
160
- justify-content: space-between;
161
- margin-top: 16px;
162
- gap: 12px;
163
- }
164
-
165
- .depth-row {
166
- display: flex;
167
- align-items: center;
168
- gap: 10px;
169
- font-size: 0.88rem;
170
- color: var(--text-dim);
171
- }
172
-
173
- .depth-row input[type=range] {
174
- width: 100px;
175
- accent-color: var(--gold);
176
- }
177
-
178
- #depth-val { color: var(--text); font-weight: 600; }
179
-
180
- .btn-primary {
181
- background: var(--gold);
182
- color: #1a1200;
183
- font-family: 'Cinzel', serif;
184
- font-size: 0.85rem;
185
- font-weight: 700;
186
- letter-spacing: 0.06em;
187
- border: none;
188
- border-radius: 6px;
189
- padding: 10px 28px;
190
- cursor: pointer;
191
- transition: background 0.15s, transform 0.1s;
192
- }
193
- .btn-primary:hover { background: #dfc060; }
194
- .btn-primary:active { transform: scale(0.97); }
195
-
196
- #file-input { display: none; }
197
-
198
- /* ── Loading Screen ───────────────────────────────────────────── */
199
- #loading-screen {
200
- align-items: center;
201
- justify-content: center;
202
- gap: 32px;
203
- }
204
-
205
- .loading-chess-icon {
206
- font-size: 3.5rem;
207
- animation: float 2s ease-in-out infinite;
208
- }
209
-
210
- @keyframes float {
211
- 0%, 100% { transform: translateY(0); }
212
- 50% { transform: translateY(-10px); }
213
- }
214
-
215
- .loading-title {
216
- font-family: 'Cinzel', serif;
217
- font-size: 1.1rem;
218
- color: var(--gold);
219
- letter-spacing: 0.08em;
220
- }
221
-
222
- .loading-info {
223
- font-size: 0.95rem;
224
- color: var(--text-dim);
225
- font-style: italic;
226
- }
227
-
228
- .progress-wrap {
229
- width: 360px;
230
- background: var(--card);
231
- border-radius: 100px;
232
- height: 6px;
233
- overflow: hidden;
234
- border: 1px solid var(--border);
235
- }
236
-
237
- .progress-bar {
238
- height: 100%;
239
- background: linear-gradient(90deg, var(--gold-dim), var(--gold));
240
- border-radius: 100px;
241
- transition: width 0.3s ease;
242
- width: 0%;
243
- }
244
-
245
- #loading-move-label {
246
- font-size: 0.9rem;
247
- color: var(--text-dim);
248
- font-family: 'Cinzel', serif;
249
- letter-spacing: 0.06em;
250
- min-height: 1.4em;
251
- }
252
-
253
- /* ── Game Screen ──────────────────────────────────────────────── */
254
- #game-screen {
255
- flex: 1;
256
- }
257
-
258
- .game-header {
259
- display: flex;
260
- align-items: center;
261
- justify-content: space-between;
262
- padding: 14px 28px;
263
- border-bottom: 1px solid var(--border);
264
- background: var(--surface);
265
- }
266
-
267
- .game-players {
268
- display: flex;
269
- align-items: center;
270
- gap: 12px;
271
- font-family: 'Cinzel', serif;
272
- }
273
-
274
- .player-white { color: #e8e0d0; font-size: 1rem; font-weight: 600; }
275
- .player-black { color: #888; font-size: 1rem; font-weight: 600; }
276
- .vs-sep { color: var(--text-muted); font-size: 0.8rem; }
277
- .game-meta { font-size: 0.82rem; color: var(--text-dim); font-style: italic; }
278
-
279
- .btn-new-game {
280
- background: transparent;
281
- border: 1px solid var(--border);
282
- color: var(--text-dim);
283
- font-family: 'Crimson Pro', serif;
284
- font-size: 0.85rem;
285
- padding: 6px 16px;
286
- border-radius: 5px;
287
- cursor: pointer;
288
- transition: border-color 0.2s, color 0.2s;
289
- }
290
- .btn-new-game:hover { border-color: var(--gold-dim); color: var(--gold); }
291
-
292
- /* ── Main layout ─────────────────────────────────────────────── */
293
- .game-body {
294
- display: flex;
295
- flex: 1;
296
- gap: 0;
297
- padding: 24px 24px 16px;
298
- align-items: flex-start;
299
- justify-content: center;
300
- gap: 20px;
301
- }
302
-
303
- /* ── Eval bar ─────────────────────────────────────────────────── */
304
- .eval-col {
305
- display: flex;
306
- flex-direction: column;
307
- align-items: center;
308
- gap: 6px;
309
- padding-top: 4px;
310
- }
311
-
312
- .eval-bar-wrap {
313
- width: 16px;
314
- height: var(--board-size);
315
- background: #1a1010;
316
- border-radius: 5px;
317
- overflow: hidden;
318
- position: relative;
319
- border: 1px solid var(--border);
320
- }
321
-
322
- .eval-bar-white {
323
- position: absolute;
324
- bottom: 0;
325
- left: 0;
326
- right: 0;
327
- background: #ece8e0;
328
- transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
329
- height: 50%;
330
- }
331
-
332
- .eval-label {
333
- font-family: 'Cinzel', serif;
334
- font-size: 0.72rem;
335
- color: var(--text-dim);
336
- letter-spacing: 0.04em;
337
- text-align: center;
338
- min-height: 1.2em;
339
- }
340
-
341
- /* ── Board col ────────────────────────────────────────────────── */
342
- .board-col {
343
- display: flex;
344
- flex-direction: column;
345
- gap: 0;
346
- }
347
-
348
- .board-outer {
349
- display: flex;
350
- align-items: flex-start;
351
- }
352
-
353
- /* rank labels */
354
- .rank-labels {
355
- display: flex;
356
- flex-direction: column;
357
- justify-content: space-around;
358
- width: 18px;
359
- height: var(--board-size);
360
- padding: 2px 0;
361
- }
362
-
363
- .rank-label {
364
- font-size: 0.7rem;
365
- color: var(--text-dim);
366
- text-align: center;
367
- height: var(--sq-size);
368
- line-height: var(--sq-size);
369
- font-family: 'Cinzel', serif;
370
- }
371
-
372
- /* file labels */
373
- .file-labels {
374
- display: flex;
375
- width: var(--board-size);
376
- margin-left: 18px;
377
- padding-top: 4px;
378
- }
379
-
380
- .file-label {
381
- width: var(--sq-size);
382
- text-align: center;
383
- font-size: 0.7rem;
384
- color: var(--text-dim);
385
- font-family: 'Cinzel', serif;
386
- }
387
-
388
- /* board grid */
389
- #board {
390
- display: grid;
391
- grid-template-columns: repeat(8, var(--sq-size));
392
- grid-template-rows: repeat(8, var(--sq-size));
393
- width: var(--board-size);
394
- height: var(--board-size);
395
- position: relative;
396
- }
397
-
398
- .sq {
399
- width: var(--sq-size);
400
- height: var(--sq-size);
401
- position: relative;
402
- display: flex;
403
- align-items: center;
404
- justify-content: center;
405
- cursor: default;
406
- }
407
-
408
- .sq.light { background: var(--sq-light); }
409
- .sq.dark { background: var(--sq-dark); }
410
-
411
- .sq.hi-from.light { background: var(--sq-hi-light); }
412
- .sq.hi-from.dark { background: var(--sq-hi-dark); }
413
- .sq.hi-to.light { background: var(--sq-hi-light); }
414
- .sq.hi-to.dark { background: var(--sq-hi-dark); }
415
-
416
- /* piece image */
417
- .piece-img {
418
- width: 54px;
419
- height: 54px;
420
- object-fit: contain;
421
- user-select: none;
422
- pointer-events: none;
423
- display: block;
424
- }
425
-
426
- /* Unicode fallback */
427
- .piece-uni {
428
- font-size: 44px;
429
- line-height: 1;
430
- user-select: none;
431
- pointer-events: none;
432
- }
433
-
434
- /* ── Classification badge ─────────────────────────────────────── */
435
- .badge {
436
- position: absolute;
437
- top: 1px;
438
- right: 1px;
439
- width: 22px;
440
- height: 22px;
441
- border-radius: 50%;
442
- display: flex;
443
- align-items: center;
444
- justify-content: center;
445
- font-size: 9px;
446
- font-weight: 800;
447
- font-family: Arial, sans-serif;
448
- color: white;
449
- z-index: 20;
450
- border: 2px solid rgba(0,0,0,0.35);
451
- box-shadow: 0 2px 6px rgba(0,0,0,0.6);
452
- letter-spacing: -0.5px;
453
- pointer-events: none;
454
- }
455
-
456
- .badge-BRILLIANT { background: var(--cl-brilliant); }
457
- .badge-GREAT { background: var(--cl-great); }
458
- .badge-BEST { background: var(--cl-best); }
459
- .badge-EXCELLENT { background: var(--cl-excellent); }
460
- .badge-GOOD { background: var(--cl-good); }
461
- .badge-BOOK { background: var(--cl-book); }
462
- .badge-INACCURACY { background: var(--cl-inaccuracy);}
463
- .badge-MISTAKE { background: var(--cl-mistake); }
464
- .badge-BLUNDER { background: var(--cl-blunder); }
465
- .badge-OMISSION { background: var(--cl-omission); }
466
-
467
- /* ── Info panel below board ───────────────────────────────────── */
468
- .board-info {
469
- margin-top: 10px;
470
- margin-left: 18px;
471
- width: var(--board-size);
472
- background: var(--card);
473
- border: 1px solid var(--border-soft);
474
- border-radius: 8px;
475
- padding: 14px 18px;
476
- display: flex;
477
- flex-direction: column;
478
- gap: 6px;
479
- }
480
-
481
- .move-label {
482
- font-family: 'Cinzel', serif;
483
- font-size: 1.05rem;
484
- color: var(--text);
485
- letter-spacing: 0.04em;
486
- }
487
-
488
- .move-class-row {
489
- display: flex;
490
- align-items: center;
491
- gap: 10px;
492
- }
493
-
494
- .move-class-dot {
495
- width: 12px;
496
- height: 12px;
497
- border-radius: 50%;
498
- flex-shrink: 0;
499
- }
500
-
501
- .move-class-text {
502
- font-size: 1rem;
503
- font-weight: 600;
504
- }
505
-
506
- .move-notes {
507
- font-size: 0.88rem;
508
- color: var(--text-dim);
509
- font-style: italic;
510
- }
511
-
512
- .move-cp {
513
- font-size: 0.82rem;
514
- color: var(--text-muted);
515
- }
516
-
517
- /* ── Navigation ──────────────────────────────────────────────── */
518
- .nav-row {
519
- display: flex;
520
- align-items: center;
521
- justify-content: center;
522
- gap: 10px;
523
- margin-top: 12px;
524
- }
525
-
526
- .nav-btn {
527
- background: var(--card);
528
- border: 1px solid var(--border);
529
- color: var(--text-dim);
530
- width: 44px;
531
- height: 44px;
532
- border-radius: 8px;
533
- font-size: 1.2rem;
534
- cursor: pointer;
535
- display: flex;
536
- align-items: center;
537
- justify-content: center;
538
- transition: background 0.15s, border-color 0.15s, color 0.15s;
539
- flex-shrink: 0;
540
- }
541
-
542
- .nav-btn:hover:not(:disabled) {
543
- background: var(--card2);
544
- border-color: var(--gold-dim);
545
- color: var(--gold);
546
- }
547
-
548
- .nav-btn:disabled { opacity: 0.3; cursor: default; }
549
-
550
- .nav-counter {
551
- font-family: 'Cinzel', serif;
552
- font-size: 0.78rem;
553
- color: var(--text-muted);
554
- letter-spacing: 0.06em;
555
- min-width: 80px;
556
- text-align: center;
557
- }
558
-
559
- /* ── Move list (right panel) ─────────────────────────────────── */
560
- .move-list-panel {
561
- width: 240px;
562
- flex-shrink: 0;
563
- background: var(--card);
564
- border: 1px solid var(--border-soft);
565
- border-radius: 10px;
566
- display: flex;
567
- flex-direction: column;
568
- overflow: hidden;
569
- max-height: calc(var(--board-size) + 50px);
570
- }
571
-
572
- .move-list-header {
573
- padding: 12px 14px;
574
- font-family: 'Cinzel', serif;
575
- font-size: 0.78rem;
576
- letter-spacing: 0.1em;
577
- color: var(--text-dim);
578
- border-bottom: 1px solid var(--border-soft);
579
- text-transform: uppercase;
580
- }
581
-
582
- .move-list-body {
583
- overflow-y: auto;
584
- flex: 1;
585
- padding: 6px 0;
586
- scrollbar-width: thin;
587
- scrollbar-color: var(--border) transparent;
588
- }
589
-
590
- .move-pair {
591
- display: flex;
592
- align-items: stretch;
593
- }
594
-
595
- .move-num {
596
- width: 32px;
597
- text-align: right;
598
- padding: 4px 6px;
599
- font-size: 0.78rem;
600
- color: var(--text-muted);
601
- font-family: 'Cinzel', serif;
602
- flex-shrink: 0;
603
- line-height: 26px;
604
- }
605
-
606
- .move-cell {
607
- flex: 1;
608
- padding: 4px 6px;
609
- font-size: 0.9rem;
610
- cursor: pointer;
611
- border-radius: 4px;
612
- display: flex;
613
- align-items: center;
614
- gap: 5px;
615
- transition: background 0.12s;
616
- min-height: 28px;
617
- margin: 1px 2px;
618
- }
619
-
620
- .move-cell:hover { background: var(--card2); }
621
- .move-cell.current { background: var(--card2); outline: 1px solid var(--gold-dim); }
622
-
623
- .move-cell .cl-dot {
624
- width: 7px;
625
- height: 7px;
626
- border-radius: 50%;
627
- flex-shrink: 0;
628
- }
629
-
630
- .move-cell .san-text {
631
- font-family: 'Crimson Pro', serif;
632
- font-size: 0.95rem;
633
- }
634
-
635
- /* ── Eval summary bars ───────────────────────────────────────── */
636
- .eval-summary {
637
- padding: 12px 14px;
638
- border-top: 1px solid var(--border-soft);
639
- }
640
-
641
- .eval-summary-row {
642
- display: flex;
643
- align-items: center;
644
- gap: 8px;
645
- margin-bottom: 4px;
646
- }
647
-
648
- .eval-summary-name {
649
- font-size: 0.78rem;
650
- color: var(--text-dim);
651
- width: 36px;
652
- font-family: 'Cinzel', serif;
653
- }
654
-
655
- .eval-summary-bar-wrap {
656
- flex: 1;
657
- height: 4px;
658
- background: var(--bg);
659
- border-radius: 2px;
660
- overflow: hidden;
661
- }
662
-
663
- .eval-summary-bar { height: 100%; border-radius: 2px; }
664
-
665
- .eval-accuracy {
666
- font-size: 0.82rem;
667
- color: var(--text-dim);
668
- text-align: center;
669
- font-style: italic;
670
- margin-top: 6px;
671
- }
672
-
673
- /* ── Flip button ─────────────────────────────────────────────── */
674
- .flip-btn {
675
- background: transparent;
676
- border: 1px solid var(--border);
677
- color: var(--text-muted);
678
- border-radius: 5px;
679
- font-size: 0.75rem;
680
- padding: 3px 10px;
681
- cursor: pointer;
682
- font-family: 'Crimson Pro', serif;
683
- transition: border-color 0.15s, color 0.15s;
684
- }
685
- .flip-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
686
-
687
- /* ── Classification color helper ─────────────────────────────── */
688
- .cl-BRILLIANT { color: var(--cl-brilliant); }
689
- .cl-GREAT { color: var(--cl-great); }
690
- .cl-BEST { color: var(--cl-best); }
691
- .cl-EXCELLENT { color: var(--cl-excellent); }
692
- .cl-GOOD { color: var(--cl-good); }
693
- .cl-BOOK { color: var(--cl-book); }
694
- .cl-INACCURACY { color: var(--cl-inaccuracy);}
695
- .cl-MISTAKE { color: var(--cl-mistake); }
696
- .cl-BLUNDER { color: var(--cl-blunder); }
697
- .cl-OMISSION { color: var(--cl-omission); }
698
-
699
- /* ── Scrollbar style ─────────────────────────────────────────── */
700
- ::-webkit-scrollbar { width: 5px; }
701
- ::-webkit-scrollbar-track { background: transparent; }
702
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
703
-
704
-
705
- /* ── Continuations ───────────────────────────────────────────── */
706
- .continuations {
707
- margin-top: 8px;
708
- display: flex;
709
- flex-direction: column;
710
- gap: 6px;
711
- }
712
-
713
- .cont-block {
714
- background: var(--bg);
715
- border: 1px solid var(--border-soft);
716
- border-radius: 6px;
717
- padding: 8px 12px;
718
- }
719
-
720
- .cont-label {
721
- font-size: 0.72rem;
722
- font-family: 'Cinzel', serif;
723
- letter-spacing: 0.08em;
724
- color: var(--text-muted);
725
- text-transform: uppercase;
726
- margin-bottom: 4px;
727
- }
728
-
729
- .cont-moves {
730
- font-family: 'Crimson Pro', serif;
731
- font-size: 0.92rem;
732
- color: var(--text-dim);
733
- line-height: 1.5;
734
- word-spacing: 4px;
735
- }
736
-
737
- .cont-moves .cont-best-move {
738
- color: var(--cl-best);
739
- font-weight: 600;
740
- }
741
-
742
- .cont-moves .cont-player-move {
743
- color: var(--text);
744
- font-weight: 600;
745
- }
746
-
747
-
748
- /* ══════════════════════════════════════════════════════════════
749
- SUMMARY SCREEN
750
- ══════════════════════════════════════════════════════════════ */
751
- #summary-screen {
752
- flex-direction: column;
753
- overflow-y: auto;
754
- }
755
-
756
- .summary-body {
757
- flex: 1;
758
- padding: 28px 32px;
759
- max-width: 900px;
760
- margin: 0 auto;
761
- width: 100%;
762
- }
763
-
764
- .summary-title {
765
- font-family: 'Cinzel', serif;
766
- font-size: 1.5rem;
767
- font-weight: 700;
768
- color: var(--gold);
769
- margin-bottom: 4px;
770
- }
771
-
772
- .summary-meta {
773
- font-size: 0.9rem;
774
- color: var(--text-dim);
775
- font-style: italic;
776
- margin-bottom: 28px;
777
- }
778
-
779
- /* Eval graph */
780
- .eval-graph-wrap {
781
- background: var(--card);
782
- border: 1px solid var(--border-soft);
783
- border-radius: 10px;
784
- padding: 16px 16px 10px;
785
- margin-bottom: 24px;
786
- }
787
-
788
- .eval-graph-label {
789
- font-family: 'Cinzel', serif;
790
- font-size: 0.72rem;
791
- letter-spacing: 0.1em;
792
- color: var(--text-muted);
793
- text-transform: uppercase;
794
- margin-bottom: 10px;
795
- }
796
-
797
- #eval-graph-canvas {
798
- width: 100%;
799
- height: 130px;
800
- display: block;
801
- border-radius: 5px;
802
- }
803
-
804
- /* Player comparison table */
805
- .summary-table-wrap {
806
- display: grid;
807
- grid-template-columns: 1fr 1fr;
808
- gap: 14px;
809
- margin-bottom: 24px;
810
- }
811
-
812
- .summary-player-card {
813
- background: var(--card);
814
- border: 1px solid var(--border-soft);
815
- border-radius: 10px;
816
- overflow: hidden;
817
- }
818
-
819
- .summary-player-header {
820
- padding: 10px 16px;
821
- font-family: 'Cinzel', serif;
822
- font-size: 0.95rem;
823
- font-weight: 600;
824
- background: var(--card2);
825
- border-bottom: 1px solid var(--border-soft);
826
- display: flex;
827
- align-items: center;
828
- justify-content: space-between;
829
- }
830
-
831
- .summary-player-header .player-color-dot {
832
- width: 9px;
833
- height: 9px;
834
- border-radius: 50%;
835
- border: 1.5px solid var(--border);
836
- }
837
-
838
- .summary-row {
839
- display: flex;
840
- align-items: center;
841
- padding: 7px 16px;
842
- border-bottom: 1px solid var(--border-soft);
843
- gap: 10px;
844
- }
845
-
846
- .summary-row:last-child { border-bottom: none; }
847
-
848
- .summary-cl-dot {
849
- width: 9px;
850
- height: 9px;
851
- border-radius: 50%;
852
- flex-shrink: 0;
853
- }
854
-
855
- .summary-cl-name {
856
- font-size: 0.9rem;
857
- color: var(--text-dim);
858
- flex: 1;
859
- }
860
-
861
- .summary-cl-count {
862
- font-family: 'Cinzel', serif;
863
- font-size: 0.95rem;
864
- color: var(--text);
865
- font-weight: 600;
866
- min-width: 24px;
867
- text-align: right;
868
- }
869
-
870
- .summary-cl-bar-wrap {
871
- width: 70px;
872
- height: 4px;
873
- background: var(--bg);
874
- border-radius: 2px;
875
- overflow: hidden;
876
- }
877
-
878
- .summary-cl-bar { height: 100%; border-radius: 2px; }
879
-
880
- /* Accuracy score */
881
- .summary-accuracy {
882
- text-align: center;
883
- padding: 8px 0 4px;
884
- }
885
-
886
- .summary-accuracy-num {
887
- font-family: 'Cinzel', serif;
888
- font-size: 1.6rem;
889
- font-weight: 700;
890
- }
891
-
892
- .summary-accuracy-label {
893
- font-size: 0.72rem;
894
- color: var(--text-muted);
895
- letter-spacing: 0.08em;
896
- text-transform: uppercase;
897
- }
898
-
899
- .summary-cta {
900
- display: flex;
901
- justify-content: center;
902
- margin-top: 8px;
903
- }
904
-
905
- /* ══════════════════════════════════════════════════════════════
906
- MOBILE RESPONSIVE
907
- ══════════════════════════════════════════════════════════════ */
908
-
909
- /* Fluid board: fill the viewport minus eval bar and padding */
910
- @media (max-width: 700px) {
911
- :root {
912
- --board-size: min(92vw, 420px);
913
- --sq-size: calc(min(92vw, 420px) / 8);
914
- }
915
-
916
- .game-body {
917
- flex-direction: column;
918
- align-items: center;
919
- padding: 10px 8px 8px;
920
- gap: 10px;
921
- }
922
-
923
- /* Eval bar goes horizontal above the board on mobile */
924
- .eval-col {
925
- flex-direction: row;
926
- align-items: center;
927
- width: var(--board-size);
928
- margin-left: 18px; /* align with board (rank label width) */
929
- gap: 6px;
930
- }
931
-
932
- .eval-bar-wrap {
933
- width: 100%;
934
- height: 10px;
935
- flex: 1;
936
- }
937
-
938
- .eval-bar-white {
939
- /* horizontal bar: width instead of height */
940
- height: 100%;
941
- width: 50%;
942
- top: 0;
943
- left: 0;
944
- transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
945
- }
946
-
947
- /* hide the W / B labels on mobile */
948
- .eval-col > div:first-child,
949
- .eval-col > div:last-child { display: none; }
950
-
951
- .eval-label { font-size: 0.65rem; }
952
-
953
- /* board info strip */
954
- .board-info { margin-left: 18px; padding: 10px 12px; }
955
-
956
- /* nav row tighter */
957
- .nav-btn { width: 38px; height: 38px; font-size: 1rem; }
958
- .nav-row { gap: 6px; }
959
-
960
- /* Move list panel goes below the board, full width */
961
- .move-list-panel {
962
- width: var(--board-size);
963
- max-height: 220px;
964
- margin-left: 18px;
965
- }
966
-
967
- /* game header: stack players */
968
- .game-header {
969
- flex-wrap: wrap;
970
- gap: 6px;
971
- padding: 10px 14px;
972
- }
973
-
974
- .game-players { font-size: 0.9rem; }
975
- .game-meta { font-size: 0.72rem; }
976
- .btn-new-game { padding: 5px 10px; font-size: 0.78rem; }
977
-
978
- /* upload box full width */
979
- .upload-box { width: 95vw; }
980
- .upload-title { font-size: 1.5rem; }
981
-
982
- /* badge slightly smaller */
983
- .badge { width: 18px; height: 18px; font-size: 8px; }
984
-
985
- /* piece images */
986
- .piece-img { width: calc(var(--sq-size) * 0.9); height: calc(var(--sq-size) * 0.9); }
987
- .piece-uni { font-size: calc(var(--sq-size) * 0.75); }
988
- }
989
-
990
- @media (max-width: 400px) {
991
- :root {
992
- --board-size: 98vw;
993
- --sq-size: calc(98vw / 8);
994
- }
995
-
996
- .rank-labels { width: 14px; }
997
- .rank-label { font-size: 0.6rem; }
998
- .file-label { font-size: 0.6rem; }
999
- .eval-col { margin-left: 14px; }
1000
- .board-info,
1001
- .move-list-panel { margin-left: 14px; width: calc(98vw - 14px); }
1002
- }
1003
- </style>
1004
- </head>
1005
- <body>
1006
-
1007
- <!-- ═══════════════════════════════════════════════════════════════
1008
- UPLOAD SCREEN
1009
- ════════════════════════════════════════════════════════════════ -->
1010
- <div id="upload-screen" class="screen active">
1011
- <header>
1012
- <div>
1013
- <div class="logo" style="text-align: center;">♟ ANALYSIS</div>
1014
- <div class="logo-sub" style="text-align: center;">Stockfish · Deep Engine</div>
1015
- </div>
1016
- </header>
1017
-
1018
- <div style="flex:1; display:flex; align-items:center; justify-content:center; padding:40px 20px;">
1019
- <div class="upload-box">
1020
- <h1 class="upload-title">Analyse a Game</h1>
1021
- <p class="upload-sub">Paste PGN notation or drop a file below</p>
1022
-
1023
- <div class="drop-zone" id="drop-zone">
1024
- <div class="drop-zone-label">
1025
- Drop a <span onclick="document.getElementById('file-input').click()">PGN file</span> here, or paste below
1026
- </div>
1027
- <textarea id="pgn-textarea"
1028
- placeholder='[Event "World Championship"]&#10;[White "Kasparov"]&#10;[Black "Karpov"]&#10;&#10;1. e4 e5 2. Nf3 Nc6 ...'></textarea>
1029
- </div>
1030
-
1031
- <input type="file" id="file-input" accept=".pgn,.txt">
1032
-
1033
- <div class="upload-row">
1034
- <div class="depth-row">
1035
- <select id="mode-select" onchange="updateModeLabel()"
1036
- style="background:var(--card2);border:1px solid var(--border);color:var(--text-dim);
1037
- border-radius:4px;padding:3px 7px;font-family:'Crimson Pro',serif;font-size:0.88rem;outline:none;cursor:pointer;">
1038
- <option value="depth">Depth</option>
1039
- <option value="time">Time/move</option>
1040
- </select>
1041
- <input type="range" id="depth-slider" min="6" max="24" value="14"
1042
- oninput="updateModeLabel()">
1043
- <span id="depth-val" style="min-width:44px;display:inline-block;">14</span>
1044
- </div>
1045
- <button class="btn-primary" onclick="startUpload()">Analyse →</button>
1046
- </div>
1047
- <div style="font-size:0.78rem;color:var(--text-muted);margin-top:8px;font-style:italic;" id="speed-hint">
1048
- Depth 14 ≈ fast · 18 ≈ balanced · 22+ ≈ slow
1049
- </div>
1050
-
1051
- <div id="upload-error" style="color:var(--cl-blunder); font-size:0.85rem; margin-top:12px; display:none;"></div>
1052
- </div>
1053
- </div>
1054
- </div>
1055
-
1056
- <!-- ═══════════════════════════════════════════════════════════════
1057
- LOADING SCREEN
1058
- ════════════════════════════════════════════════════════════════ -->
1059
- <div id="loading-screen" class="screen">
1060
- <header>
1061
- <div>
1062
- <div class="logo">♟ ANALYSIS</div>
1063
- <div class="logo-sub">Stockfish · Deep Engine</div>
1064
- </div>
1065
- </header>
1066
- <div style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px;">
1067
- <div class="loading-chess-icon">♞</div>
1068
- <div class="loading-title">ANALYSING GAME</div>
1069
- <div class="progress-wrap">
1070
- <div class="progress-bar" id="progress-bar"></div>
1071
- </div>
1072
- <div id="loading-move-label">Preparing engine…</div>
1073
- <div class="loading-info" id="loading-info">Running Stockfish at depth <span id="loading-depth">—</span></div>
1074
- </div>
1075
- </div>
1076
-
1077
-
1078
- <!-- ═══════════════════════════════════════════════════════════════
1079
- SUMMARY SCREEN
1080
- ════════════════════════════════════════════════════════════════ -->
1081
- <div id="summary-screen" class="screen">
1082
- <header>
1083
- <div>
1084
- <div class="logo">♟ ANALYSIS</div>
1085
- <div class="logo-sub">Stockfish · Deep Engine</div>
1086
- </div>
1087
- <button class="btn-new-game" onclick="newGame()">← New Game</button>
1088
- </header>
1089
-
1090
- <div class="summary-body">
1091
- <div class="summary-title" id="sum-title">Game Analysis</div>
1092
- <div class="summary-meta" id="sum-meta"></div>
1093
-
1094
- <!-- Eval graph -->
1095
- <div class="eval-graph-wrap">
1096
- <div class="eval-graph-label">Evaluation across the game</div>
1097
- <canvas id="eval-graph-canvas"></canvas>
1098
- </div>
1099
-
1100
- <!-- Player cards -->
1101
- <div class="summary-table-wrap">
1102
- <div class="summary-player-card" id="sum-white-card">
1103
- <div class="summary-player-header">
1104
- <span id="sum-white-name">White</span>
1105
- <div class="player-color-dot" style="background:#ece8e0;"></div>
1106
- </div>
1107
- <div class="summary-accuracy">
1108
- <div class="summary-accuracy-num" id="sum-white-acc" style="color:var(--cl-excellent);">—</div>
1109
- <div class="summary-accuracy-label">Accuracy</div>
1110
- </div>
1111
- <div id="sum-white-rows"></div>
1112
- </div>
1113
- <div class="summary-player-card" id="sum-black-card">
1114
- <div class="summary-player-header">
1115
- <span id="sum-black-name">Black</span>
1116
- <div class="player-color-dot" style="background:#444;"></div>
1117
- </div>
1118
- <div class="summary-accuracy">
1119
- <div class="summary-accuracy-num" id="sum-black-acc" style="color:var(--cl-excellent);">—</div>
1120
- <div class="summary-accuracy-label">Accuracy</div>
1121
- </div>
1122
- <div id="sum-black-rows"></div>
1123
- </div>
1124
- </div>
1125
-
1126
- <div class="summary-cta">
1127
- <button class="btn-primary" onclick="enterGame()">Review Game →</button>
1128
- </div>
1129
- </div>
1130
- </div>
1131
-
1132
- <!-- ═══════════════════════════════════════════════════════════════
1133
- GAME SCREEN
1134
- ════════════════════════════════════════════════════════════════ -->
1135
- <div id="game-screen" class="screen">
1136
- <header>
1137
- <div>
1138
- <div class="logo">♟ ANALYSIS</div>
1139
- <div class="logo-sub">Stockfish · Deep Engine</div>
1140
- </div>
1141
- <div style="display:flex; align-items:center; gap:24px; flex:1; justify-content:center;">
1142
- <div class="game-players">
1143
- <span class="player-white" id="hdr-white">White</span>
1144
- <span class="vs-sep">vs</span>
1145
- <span class="player-black" id="hdr-black">Black</span>
1146
- </div>
1147
- <div class="game-meta" id="hdr-meta"></div>
1148
- </div>
1149
- <button class="btn-new-game" onclick="newGame()">← New Game</button>
1150
- </header>
1151
-
1152
- <div class="game-body">
1153
-
1154
- <!-- Eval bar -->
1155
- <div class="eval-col">
1156
- <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-bottom:4px; letter-spacing:0.06em;">W</div>
1157
- <div class="eval-bar-wrap">
1158
- <div class="eval-bar-white" id="eval-bar-white"></div>
1159
- </div>
1160
- <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-top:4px; letter-spacing:0.06em;">B</div>
1161
- </div>
1162
-
1163
- <!-- Board + info -->
1164
- <div class="board-col">
1165
- <div class="board-outer">
1166
- <!-- Rank labels -->
1167
- <div class="rank-labels" id="rank-labels">
1168
- <div class="rank-label">8</div>
1169
- <div class="rank-label">7</div>
1170
- <div class="rank-label">6</div>
1171
- <div class="rank-label">5</div>
1172
- <div class="rank-label">4</div>
1173
- <div class="rank-label">3</div>
1174
- <div class="rank-label">2</div>
1175
- <div class="rank-label">1</div>
1176
- </div>
1177
- <!-- Board -->
1178
- <div id="board"></div>
1179
- </div>
1180
-
1181
- <!-- File labels -->
1182
- <div class="file-labels">
1183
- <div class="file-label">a</div>
1184
- <div class="file-label">b</div>
1185
- <div class="file-label">c</div>
1186
- <div class="file-label">d</div>
1187
- <div class="file-label">e</div>
1188
- <div class="file-label">f</div>
1189
- <div class="file-label">g</div>
1190
- <div class="file-label">h</div>
1191
- </div>
1192
-
1193
- <!-- Navigation -->
1194
- <div class="nav-row">
1195
- <button type="button" class="nav-btn" id="btn-start" onclick="navigateTo(0)" title="Start (Home)">⇤</button>
1196
- <button type="button" class="nav-btn" id="btn-prev" onclick="navigateTo(currentIdx - 1)" title="Previous (←)">‹</button>
1197
- <div class="nav-counter" id="nav-counter">— / —</div>
1198
- <button type="button" class="nav-btn" id="btn-next" onclick="navigateTo(currentIdx + 1)" title="Next (→)">›</button>
1199
- <button type="button" class="nav-btn" id="btn-end" onclick="navigateTo(gameData.positions.length - 1)" title="End (End)">⇥</button>
1200
- <button type="button" class="flip-btn" onclick="flipBoard()" title="Flip board">⇅ Flip</button>
1201
- </div>
1202
-
1203
- <!-- Move info -->
1204
- <div class="board-info">
1205
- <div style="display:flex; align-items:center; justify-content:space-between;">
1206
- <div class="move-label" id="info-move-label">Starting Position</div>
1207
- <div class="eval-label" id="info-eval">0.00</div>
1208
- </div>
1209
- <div class="move-class-row" id="info-class-row" style="display:none;">
1210
- <div class="move-class-dot" id="info-class-dot"></div>
1211
- <div class="move-class-text" id="info-class-text"></div>
1212
- <div class="move-cp" id="info-cp"></div>
1213
- </div>
1214
- <div class="move-notes" id="info-notes"></div>
1215
-
1216
- <!-- Continuations -->
1217
- <div class="continuations" id="continuations" style="display:none;">
1218
- <div class="cont-block" id="cont-player-block">
1219
- <div class="cont-label">Continuation after this move</div>
1220
- <div class="cont-moves" id="cont-player"></div>
1221
- </div>
1222
- <div class="cont-block" id="cont-best-block" style="display:none;">
1223
- <div class="cont-label">Best move was <span id="cont-best-move-name" class="cont-best-move"></span></div>
1224
- <div class="cont-label" style="margin-top:4px;">Continuation after best move</div>
1225
- <div class="cont-moves" id="cont-best"></div>
1226
- </div>
1227
- </div>
1228
- </div>
1229
- </div>
1230
-
1231
- <!-- Move list panel -->
1232
- <div class="move-list-panel">
1233
- <div class="move-list-header">Move List</div>
1234
- <div class="move-list-body" id="move-list"></div>
1235
- <div class="eval-summary" id="eval-summary" style="display:none;">
1236
- <div class="eval-summary-row">
1237
- <div class="eval-summary-name">White</div>
1238
- <div class="eval-summary-bar-wrap">
1239
- <div class="eval-summary-bar" id="white-acc-bar" style="background:var(--cl-excellent);"></div>
1240
- </div>
1241
- </div>
1242
- <div class="eval-summary-row">
1243
- <div class="eval-summary-name">Black</div>
1244
- <div class="eval-summary-bar-wrap">
1245
- <div class="eval-summary-bar" id="black-acc-bar" style="background:var(--cl-excellent);"></div>
1246
- </div>
1247
- </div>
1248
- <div class="eval-accuracy" id="accuracy-text"></div>
1249
- </div>
1250
- </div>
1251
-
1252
- </div><!-- /game-body -->
1253
- </div><!-- /game-screen -->
1254
-
1255
- <!-- ═══════════════════════════════════════════════════════════════
1256
- JAVASCRIPT
1257
- ════════════════════════════════════════════════════════════════ -->
1258
- <script>
1259
- /* ── State ─────────────────────────────────────────────────── */
1260
- let gameData = null;
1261
- let currentIdx = 0;
1262
- let flipped = false;
1263
- let currentGameId = null;
1264
- let progressEvt = null;
1265
-
1266
- /* ── Classification config ��────────────────────────────────── */
1267
- const CL = {
1268
- BRILLIANT: { color: '#00b5ad', badge: '!!', label: '!! Brilliant' },
1269
- GREAT: { color: '#1a4fc4', badge: '!', label: '! Great' },
1270
- BEST: { color: '#7ec820', badge: '★', label: '★ Best' },
1271
- EXCELLENT: { color: '#7ec820', badge: '👍', label: '👍 Excellent' },
1272
- GOOD: { color: '#4db896', badge: '✓', label: '✓ Good' },
1273
- BOOK: { color: '#a0754a', badge: '📖', label: '📖 Book' },
1274
- INACCURACY: { color: '#d4b030', badge: '?!', label: '?! Inaccuracy' },
1275
- MISTAKE: { color: '#d07020', badge: '?', label: '? Mistake' },
1276
- BLUNDER: { color: '#c83030', badge: '??', label: '?? Blunder' },
1277
- OMISSION: { color: '#e05050', badge: '✖', label: '✖ Missed Tactic' },
1278
- };
1279
-
1280
- /* ── Piece maps ────────────────────────────────────────────── */
1281
- const PIECE_CODE = {
1282
- K:'wK', Q:'wQ', R:'wR', B:'wB', N:'wN', P:'wP',
1283
- k:'bK', q:'bQ', r:'bR', b:'bB', n:'bN', p:'bP',
1284
- };
1285
- const PIECE_UNI = {
1286
- K:'♔', Q:'♕', R:'♖', B:'♗', N:'♘', P:'♙',
1287
- k:'♚', q:'♛', r:'♜', b:'♝', n:'♞', p:'♟',
1288
- };
1289
- /* Track which piece images failed to load (use unicode instead) */
1290
- const imgFailed = {};
1291
-
1292
- /* ── Screens ───────────────────────────────────────────────── */
1293
- function showScreen(id) {
1294
- document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
1295
- document.getElementById(id).classList.add('active');
1296
- }
1297
-
1298
- /* ── Upload ────────────────────────────────────────────────── */
1299
- function updateModeLabel() {
1300
- const mode = document.getElementById('mode-select').value;
1301
- const val = +document.getElementById('depth-slider').value;
1302
- const hint = document.getElementById('speed-hint');
1303
- if (mode === 'depth') {
1304
- document.getElementById('depth-val').textContent = val;
1305
- hint.textContent = `Depth ${val} ≈ ${val<=10?'very fast':val<=14?'fast':val<=18?'balanced':val<=21?'slow':'very slow'}`;
1306
- } else {
1307
- // remap slider 6-24 → 100ms-3000ms
1308
- const ms = Math.round(((val - 6) / 18) * 2900 + 100);
1309
- document.getElementById('depth-val').textContent = ms + ' ms';
1310
- hint.textContent = `${ms}ms per move ≈ ${ms<=300?'fast':ms<=800?'balanced':'thorough'}`;
1311
- }
1312
- }
1313
- updateModeLabel();
1314
-
1315
- function startUpload() {
1316
- const pgn = document.getElementById('pgn-textarea').value.trim();
1317
- const mode = document.getElementById('mode-select').value;
1318
- const sliderVal = +document.getElementById('depth-slider').value;
1319
- const errEl = document.getElementById('upload-error');
1320
-
1321
- if (!pgn) { errEl.textContent = 'Please paste a PGN first.'; errEl.style.display='block'; return; }
1322
- errEl.style.display = 'none';
1323
-
1324
- const fd = new FormData();
1325
- fd.append('pgn_text', pgn);
1326
- if (mode === 'depth') {
1327
- fd.append('depth', sliderVal);
1328
- } else {
1329
- const ms = Math.round(((sliderVal - 6) / 18) * 2900 + 100);
1330
- fd.append('time_ms', ms);
1331
- }
1332
-
1333
- fetch('/upload', { method: 'POST', body: fd })
1334
- .then(r => {
1335
- const ct = r.headers.get('content-type') || '';
1336
- if (!ct.includes('application/json')) {
1337
- throw new Error('Server returned non-JSON (status ' + r.status + '). ' +
1338
- 'If using localtunnel, open the tunnel URL directly in a new tab and click Continue first.');
1339
- }
1340
- return r.json();
1341
- })
1342
- .then(d => {
1343
- if (d.error) { showError(d.error); return; }
1344
- currentGameId = d.game_id;
1345
- document.getElementById('loading-depth').textContent =
1346
- mode === 'depth' ? 'depth ' + sliderVal : Math.round(((sliderVal-6)/18)*2900+100) + 'ms/move';
1347
- showScreen('loading-screen');
1348
- pollProgress(d.game_id);
1349
- })
1350
- .catch(e => showError('Upload failed: ' + e.message));
1351
- }
1352
-
1353
- function showError(msg) {
1354
- const el = document.getElementById('upload-error');
1355
- if (el) { el.textContent = msg; el.style.display = 'block'; }
1356
- else { alert(msg); }
1357
- showScreen('upload-screen');
1358
- }
1359
-
1360
- function pollProgress(gameId) {
1361
- if (progressEvt) progressEvt.close();
1362
- const bar = document.getElementById('progress-bar');
1363
- const label = document.getElementById('loading-move-label');
1364
-
1365
- progressEvt = new EventSource(`/progress/${gameId}`);
1366
- progressEvt.onmessage = (e) => {
1367
- const p = JSON.parse(e.data);
1368
-
1369
- if (p.error) {
1370
- progressEvt.close();
1371
- showError('Analysis error: ' + p.error);
1372
- return;
1373
- }
1374
-
1375
- const pct = p.total > 0 ? Math.round((p.done / p.total) * 100) : 0;
1376
- bar.style.width = pct + '%';
1377
- label.textContent = p.done > 0
1378
- ? `Move ${p.done} of ${p.total} analysed`
1379
- : 'Initialising engine…';
1380
-
1381
- if (p.complete && !p.error) {
1382
- progressEvt.close();
1383
- loadGame(gameId);
1384
- }
1385
- };
1386
- }
1387
-
1388
- function loadGame(gameId) {
1389
- fetch(`/game/${gameId}`)
1390
- .then(r => r.json())
1391
- .then(d => {
1392
- if (d.error) { alert(d.error); showScreen('upload-screen'); return; }
1393
- gameData = d;
1394
- setupGameScreen();
1395
- buildSummary();
1396
- showScreen('summary-screen');
1397
- });
1398
- }
1399
-
1400
- function enterGame() {
1401
- showScreen('game-screen');
1402
- }
1403
-
1404
- /* ── Summary screen ────────────────────────────────────────── */
1405
- function buildSummary() {
1406
- const h = gameData.headers;
1407
- const pos = gameData.positions;
1408
-
1409
- document.getElementById('sum-white-name').textContent = h.White || 'White';
1410
- document.getElementById('sum-black-name').textContent = h.Black || 'Black';
1411
- document.getElementById('sum-title').textContent =
1412
- `${h.White || 'White'} vs ${h.Black || 'Black'}`;
1413
- document.getElementById('sum-meta').textContent =
1414
- [h.Event, h.Date, h.Result].filter(Boolean).join(' · ');
1415
-
1416
- // ── Eval graph ─────────────────────────────────────────────
1417
- const canvas = document.getElementById('eval-graph-canvas');
1418
- const dpr = window.devicePixelRatio || 1;
1419
- const W = canvas.parentElement.clientWidth - 32;
1420
- const H = 130;
1421
- canvas.width = W * dpr;
1422
- canvas.height = H * dpr;
1423
- canvas.style.width = W + 'px';
1424
- canvas.style.height = H + 'px';
1425
- const ctx = canvas.getContext('2d');
1426
- ctx.scale(dpr, dpr);
1427
-
1428
- // Background
1429
- ctx.fillStyle = '#13131a';
1430
- ctx.fillRect(0, 0, W, H);
1431
-
1432
- // Zero line
1433
- ctx.strokeStyle = '#333';
1434
- ctx.lineWidth = 1;
1435
- ctx.beginPath();
1436
- ctx.moveTo(0, H / 2);
1437
- ctx.lineTo(W, H / 2);
1438
- ctx.stroke();
1439
-
1440
- // Eval to Y: clamp to ±5 (500cp), arctan squash for large values
1441
- const evalToY = (cpWhite) => {
1442
- const pct = 0.5 - 0.5 * (2 / Math.PI) * Math.atan(cpWhite / 400);
1443
- return pct * H;
1444
- };
1445
-
1446
- // Fill white / black areas
1447
- const evals = pos.map(p => p.eval_white);
1448
- const xs = evals.map((_, i) => (i / Math.max(evals.length - 1, 1)) * W);
1449
-
1450
- ctx.beginPath();
1451
- ctx.moveTo(xs[0], H / 2);
1452
- for (let i = 0; i < evals.length; i++) {
1453
- ctx.lineTo(xs[i], evalToY(evals[i]));
1454
- }
1455
- ctx.lineTo(xs[xs.length - 1], H / 2);
1456
- ctx.closePath();
1457
- ctx.fillStyle = 'rgba(230,225,215,0.18)';
1458
- ctx.fill();
1459
-
1460
- ctx.beginPath();
1461
- ctx.moveTo(xs[0], H / 2);
1462
- for (let i = 0; i < evals.length; i++) {
1463
- ctx.lineTo(xs[i], evalToY(evals[i]));
1464
- }
1465
- ctx.lineTo(xs[xs.length - 1], H);
1466
- ctx.lineTo(xs[0], H);
1467
- ctx.closePath();
1468
- ctx.fillStyle = 'rgba(60,60,70,0.4)';
1469
- ctx.fill();
1470
-
1471
- // Line
1472
- ctx.beginPath();
1473
- ctx.moveTo(xs[0], evalToY(evals[0]));
1474
- for (let i = 1; i < evals.length; i++) ctx.lineTo(xs[i], evalToY(evals[i]));
1475
- ctx.strokeStyle = '#c8a84c';
1476
- ctx.lineWidth = 2;
1477
- ctx.stroke();
1478
-
1479
- // Blunder/mistake dots
1480
- const DOT = { BLUNDER: '#c83030', MISTAKE: '#d07020', OMISSION: '#e05050' };
1481
- for (let i = 1; i < pos.length; i++) {
1482
- const cl = pos[i].classification;
1483
- if (DOT[cl]) {
1484
- ctx.beginPath();
1485
- ctx.arc(xs[i], evalToY(evals[i]), 3.5, 0, Math.PI * 2);
1486
- ctx.fillStyle = DOT[cl];
1487
- ctx.fill();
1488
- }
1489
- }
1490
-
1491
- // ── Classification rows ────────────────────────────────────
1492
- const ORDER = ['BRILLIANT','GREAT','BEST','EXCELLENT','GOOD','BOOK',
1493
- 'INACCURACY','MISTAKE','BLUNDER','OMISSION'];
1494
-
1495
- function buildRows(containerId, accId, colorIdx) {
1496
- const moves = pos.filter((_, i) => i > 0 && i % 2 === colorIdx);
1497
- const counts = {};
1498
- ORDER.forEach(k => counts[k] = 0);
1499
- moves.forEach(p => { if (p.classification) counts[p.classification] = (counts[p.classification]||0)+1; });
1500
-
1501
- // Accuracy score
1502
- const weights = {BRILLIANT:100,GREAT:97,BEST:95,EXCELLENT:88,GOOD:78,
1503
- BOOK:90,INACCURACY:50,MISTAKE:20,BLUNDER:0,OMISSION:10};
1504
- const acc = moves.length
1505
- ? Math.round(moves.reduce((s,p) => s + (weights[p.classification]||50), 0) / moves.length)
1506
- : 0;
1507
- const accEl = document.getElementById(accId);
1508
- accEl.textContent = acc + '%';
1509
- accEl.style.color = acc >= 90 ? 'var(--cl-best)' :
1510
- acc >= 75 ? 'var(--cl-excellent)' :
1511
- acc >= 60 ? 'var(--cl-inaccuracy)' : 'var(--cl-blunder)';
1512
-
1513
- const max = Math.max(1, ...Object.values(counts));
1514
- const container = document.getElementById(containerId);
1515
- container.innerHTML = '';
1516
- ORDER.forEach(key => {
1517
- const cl = CL[key];
1518
- if (!cl) return;
1519
- const row = document.createElement('div');
1520
- row.className = 'summary-row';
1521
- row.innerHTML = `
1522
- <div class="summary-cl-dot" style="background:${cl.color}"></div>
1523
- <div class="summary-cl-name">${cl.label.trim()}</div>
1524
- <div class="summary-cl-bar-wrap">
1525
- <div class="summary-cl-bar" style="width:${(counts[key]/max*100).toFixed(0)}%;background:${cl.color}"></div>
1526
- </div>
1527
- <div class="summary-cl-count">${counts[key]}</div>
1528
- `;
1529
- container.appendChild(row);
1530
- });
1531
- }
1532
-
1533
- buildRows('sum-white-rows', 'sum-white-acc', 1); // odd indices = white (move 1,3,5…)
1534
- buildRows('sum-black-rows', 'sum-black-acc', 0); // even indices = black (move 2,4,6…)
1535
- }
1536
-
1537
- /* ── Game screen setup ─────────────────────────────────────── */
1538
- function setupGameScreen() {
1539
- const h = gameData.headers;
1540
- document.getElementById('hdr-white').textContent = h.White || 'White';
1541
- document.getElementById('hdr-black').textContent = h.Black || 'Black';
1542
- document.getElementById('hdr-meta').textContent = [h.Event, h.Date, h.Result].filter(Boolean).join(' · ');
1543
-
1544
- buildMoveList();
1545
- computeAccuracy();
1546
- navigateTo(0);
1547
- }
1548
-
1549
- function newGame() {
1550
- gameData = null;
1551
- currentIdx = 0;
1552
- flipped = false;
1553
- document.getElementById('pgn-textarea').value = '';
1554
- showScreen('upload-screen');
1555
- }
1556
-
1557
- /* ── FEN parser ────────────────────────────────────────────── */
1558
- function parseFEN(fen) {
1559
- const [placement] = fen.split(' ');
1560
- const ranks = placement.split('/');
1561
- const grid = []; // grid[rank][file], rank 0 = rank 8
1562
- for (const row of ranks) {
1563
- const r = [];
1564
- for (const ch of row) {
1565
- if (/\d/.test(ch)) { for (let i=0; i<+ch; i++) r.push(null); }
1566
- else { r.push(ch); }
1567
- }
1568
- grid.push(r);
1569
- }
1570
- return grid;
1571
- }
1572
-
1573
- /* ── Board render ──────────────────────────────────────────── */
1574
- function renderBoard(pos) {
1575
- const grid = parseFEN(pos.fen);
1576
- const fromSq = pos.from_sq;
1577
- const toSq = pos.to_sq;
1578
- const board = document.getElementById('board');
1579
- board.innerHTML = '';
1580
-
1581
- // Update rank/file labels for flipped
1582
- const rankLabels = document.getElementById('rank-labels');
1583
- rankLabels.innerHTML = '';
1584
- for (let i=0; i<8; i++) {
1585
- const d = document.createElement('div');
1586
- d.className = 'rank-label';
1587
- d.textContent = flipped ? (i + 1) : (8 - i);
1588
- rankLabels.appendChild(d);
1589
- }
1590
-
1591
- const fileLabels = document.querySelector('.file-labels');
1592
- fileLabels.innerHTML = '';
1593
- for (let i=0; i<8; i++) {
1594
- const d = document.createElement('div');
1595
- d.className = 'file-label';
1596
- d.textContent = flipped
1597
- ? String.fromCharCode(104 - i) // h...a
1598
- : String.fromCharCode(97 + i); // a...h
1599
- fileLabels.appendChild(d);
1600
- }
1601
-
1602
- for (let displayRank=0; displayRank<8; displayRank++) {
1603
- for (let displayFile=0; displayFile<8; displayFile++) {
1604
- const gridRank = flipped ? (7 - displayRank) : displayRank;
1605
- const gridFile = flipped ? (7 - displayFile) : displayFile;
1606
-
1607
- const squareName = String.fromCharCode(97 + gridFile) + (8 - gridRank);
1608
- const piece = grid[gridRank][gridFile];
1609
- const isLight = (gridRank + gridFile) % 2 === 0;
1610
-
1611
- const sq = document.createElement('div');
1612
- sq.className = 'sq ' + (isLight ? 'light' : 'dark');
1613
- sq.dataset.sq = squareName;
1614
-
1615
- if (squareName === fromSq) sq.classList.add('hi-from');
1616
- if (squareName === toSq) sq.classList.add('hi-to');
1617
-
1618
- if (piece) {
1619
- if (imgFailed[PIECE_CODE[piece]]) {
1620
- // unicode fallback
1621
- const span = document.createElement('span');
1622
- span.className = 'piece-uni';
1623
- span.textContent = PIECE_UNI[piece];
1624
- sq.appendChild(span);
1625
- } else {
1626
- const img = document.createElement('img');
1627
- img.className = 'piece-img';
1628
- img.src = `https://lichess1.org/assets/piece/cburnett/${PIECE_CODE[piece]}.svg`;
1629
- img.alt = piece;
1630
- img.onerror = function() {
1631
- imgFailed[PIECE_CODE[piece]] = true;
1632
- const span = document.createElement('span');
1633
- span.className = 'piece-uni';
1634
- span.textContent = PIECE_UNI[piece];
1635
- this.parentNode.replaceChild(span, this);
1636
- };
1637
- sq.appendChild(img);
1638
- }
1639
- }
1640
-
1641
- // Classification badge on the destination square
1642
- if (squareName === toSq && pos.classification && CL[pos.classification]) {
1643
- const cl = CL[pos.classification];
1644
- const bdg = document.createElement('div');
1645
- bdg.className = `badge badge-${pos.classification}`;
1646
- bdg.textContent = cl.badge;
1647
- sq.appendChild(bdg);
1648
- }
1649
-
1650
- sq.addEventListener('click', () => { /* future: select piece */ });
1651
- board.appendChild(sq);
1652
- }
1653
- }
1654
- }
1655
-
1656
- /* ── Eval bar ──────────────────────────────────────────────── */
1657
- function updateEvalBar(evalWhite) {
1658
- const pct = 50 + 50 * (2 / Math.PI) * Math.atan(evalWhite / 400);
1659
- const bar = document.getElementById('eval-bar-white');
1660
- const val = (flipped ? (100 - pct) : pct) + '%';
1661
- bar.style.height = val;
1662
- bar.style.width = val; // used by horizontal mobile bar
1663
- }
1664
-
1665
- /* ── Move info panel ───────────────────────────────────────── */
1666
- function updateInfo(pos) {
1667
- const evalDisp = pos.eval_display;
1668
- document.getElementById('info-eval').textContent = evalDisp;
1669
- document.getElementById('info-eval').className = 'eval-label';
1670
-
1671
- const clRow = document.getElementById('info-class-row');
1672
- const clDot = document.getElementById('info-class-dot');
1673
- const clText = document.getElementById('info-class-text');
1674
- const cpEl = document.getElementById('info-cp');
1675
- const notes = document.getElementById('info-notes');
1676
- const label = document.getElementById('info-move-label');
1677
-
1678
- if (!pos.san) {
1679
- label.textContent = 'Starting Position';
1680
- clRow.style.display = 'none';
1681
- notes.textContent = '';
1682
- } else {
1683
- const colLabel = pos.color === 'white' ? 'White' : 'Black';
1684
- label.textContent = `${pos.move_number}${pos.color === 'white' ? '.' : '…'} ${pos.san} — ${colLabel}`;
1685
-
1686
- clRow.style.display = 'flex';
1687
- if (pos.classification && CL[pos.classification]) {
1688
- const cl = CL[pos.classification];
1689
- clDot.style.background = cl.color;
1690
- clText.textContent = cl.label;
1691
- clText.className = `move-class-text cl-${pos.classification}`;
1692
- const eStr = pos.e_loss > 0.005
1693
- ? `(−${pos.e_loss.toFixed(3)} EP)`
1694
- : '';
1695
- cpEl.textContent = eStr;
1696
- notes.textContent = pos.notes && pos.notes.length ? pos.notes.join(' · ') : '';
1697
- } else {
1698
- clRow.style.display = 'none';
1699
- }
1700
- }
1701
-
1702
- // ── Continuations ────────────────────────────────────────────
1703
- const contSection = document.getElementById('continuations');
1704
- const contPlayer = document.getElementById('cont-player');
1705
- const contBest = document.getElementById('cont-best');
1706
- const contBestBlock = document.getElementById('cont-best-block');
1707
- const contBestName = document.getElementById('cont-best-move-name');
1708
-
1709
- if (!pos.san) {
1710
- contSection.style.display = 'none';
1711
- } else {
1712
- contSection.style.display = 'flex';
1713
-
1714
- // Player continuation
1715
- if (pos.player_pv && pos.player_pv.length) {
1716
- const pMoves = pos.player_pv.map((m, i) => {
1717
- const cls = i === 0 ? 'cont-player-move' : '';
1718
- return cls ? `<span class="${cls}">${m}</span>` : m;
1719
- });
1720
- contPlayer.innerHTML = pMoves.join(' ');
1721
- } else {
1722
- contPlayer.textContent = '—';
1723
- }
1724
-
1725
- // Best move & continuation (only show when best != played move)
1726
- if (pos.best_san && pos.best_san !== pos.san) {
1727
- contBestBlock.style.display = 'block';
1728
- contBestName.textContent = pos.best_san;
1729
- if (pos.best_pv && pos.best_pv.length) {
1730
- const bMoves = pos.best_pv.map((m, i) => {
1731
- const cls = i === 0 ? 'cont-best-move' : '';
1732
- return cls ? `<span class="${cls}">${m}</span>` : m;
1733
- });
1734
- contBest.innerHTML = bMoves.join(' ');
1735
- } else {
1736
- contBest.textContent = '—';
1737
- }
1738
- } else {
1739
- contBestBlock.style.display = 'none';
1740
- }
1741
- }
1742
- }
1743
-
1744
- /* ── Navigate ──────────────────────────────────────────────── */
1745
- function navigateTo(idx) {
1746
- if (!gameData) return;
1747
- idx = Math.max(0, Math.min(idx, gameData.positions.length - 1));
1748
- currentIdx = idx;
1749
- const pos = gameData.positions[idx];
1750
-
1751
- renderBoard(pos);
1752
- updateEvalBar(pos.eval_white);
1753
- updateInfo(pos);
1754
-
1755
- document.getElementById('btn-start').disabled = idx === 0;
1756
- document.getElementById('btn-prev').disabled = idx === 0;
1757
- document.getElementById('btn-next').disabled = idx === gameData.positions.length - 1;
1758
- document.getElementById('btn-end').disabled = idx === gameData.positions.length - 1;
1759
-
1760
- const total = gameData.positions.length - 1;
1761
- document.getElementById('nav-counter').textContent = idx === 0
1762
- ? `Start / ${total}`
1763
- : `${idx} / ${total}`;
1764
-
1765
- // Highlight move list
1766
- document.querySelectorAll('.move-cell').forEach(c => c.classList.remove('current'));
1767
- if (idx > 0) {
1768
- const cell = document.querySelector(`[data-move-idx="${idx}"]`);
1769
- if (cell) {
1770
- cell.classList.add('current');
1771
- // Scroll the move-list container, not the whole page
1772
- const listBody = document.getElementById('move-list');
1773
- const cellTop = cell.offsetTop;
1774
- const cellBottom = cellTop + cell.offsetHeight;
1775
- const bodyTop = listBody.scrollTop;
1776
- const bodyBottom = bodyTop + listBody.clientHeight;
1777
- if (cellTop < bodyTop) {
1778
- listBody.scrollTop = cellTop - 8;
1779
- } else if (cellBottom > bodyBottom) {
1780
- listBody.scrollTop = cellBottom - listBody.clientHeight + 8;
1781
- }
1782
- }
1783
- }
1784
- }
1785
-
1786
- function flipBoard() {
1787
- flipped = !flipped;
1788
- navigateTo(currentIdx);
1789
- }
1790
-
1791
- /* ── Keyboard ──────────────────────────────────────────────── */
1792
- document.addEventListener('keydown', (e) => {
1793
- if (!gameData) return;
1794
- if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); navigateTo(currentIdx - 1); }
1795
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); navigateTo(currentIdx + 1); }
1796
- if (e.key === 'Home') { e.preventDefault(); navigateTo(0); }
1797
- if (e.key === 'End') { e.preventDefault(); navigateTo(gameData.positions.length - 1); }
1798
- });
1799
-
1800
- /* ── Move list ─────────────────────────────────────────────── */
1801
- function buildMoveList() {
1802
- const list = document.getElementById('move-list');
1803
- list.innerHTML = '';
1804
- const positions = gameData.positions;
1805
-
1806
- // positions[0] = start, positions[1] = white's first move, positions[2] = black's...
1807
- let pairDiv = null;
1808
- for (let i = 1; i < positions.length; i++) {
1809
- const pos = positions[i];
1810
- const isWhite = (i % 2 === 1);
1811
-
1812
- if (isWhite) {
1813
- pairDiv = document.createElement('div');
1814
- pairDiv.className = 'move-pair';
1815
-
1816
- const numEl = document.createElement('div');
1817
- numEl.className = 'move-num';
1818
- numEl.textContent = pos.move_number + '.';
1819
- pairDiv.appendChild(numEl);
1820
- list.appendChild(pairDiv);
1821
- }
1822
-
1823
- const cell = document.createElement('div');
1824
- cell.className = 'move-cell';
1825
- cell.dataset.moveIdx = i;
1826
-
1827
- const cl = pos.classification && CL[pos.classification] ? CL[pos.classification] : null;
1828
-
1829
- if (cl) {
1830
- const dot = document.createElement('div');
1831
- dot.className = 'cl-dot';
1832
- dot.style.background = cl.color;
1833
- cell.appendChild(dot);
1834
- } else {
1835
- // placeholder dot
1836
- const dot = document.createElement('div');
1837
- dot.className = 'cl-dot';
1838
- dot.style.background = 'transparent';
1839
- cell.appendChild(dot);
1840
- }
1841
-
1842
- const san = document.createElement('span');
1843
- san.className = 'san-text';
1844
- san.textContent = pos.san || '';
1845
- cell.appendChild(san);
1846
-
1847
- cell.addEventListener('click', () => navigateTo(i));
1848
- if (pairDiv) pairDiv.appendChild(cell);
1849
- }
1850
- }
1851
-
1852
- /* ── Accuracy summary ──────────────────────────────────────── */
1853
- function computeAccuracy() {
1854
- const positions = gameData.positions;
1855
- const WHITE_CL = []; const BLACK_CL = [];
1856
-
1857
- for (let i = 1; i < positions.length; i++) {
1858
- const pos = positions[i];
1859
- const arr = pos.color === 'white' ? WHITE_CL : BLACK_CL;
1860
- arr.push(pos.classification);
1861
- }
1862
-
1863
- function score(arr) {
1864
- const weights = {
1865
- BRILLIANT: 100, GREAT: 97, BEST: 95, EXCELLENT: 88, GOOD: 78,
1866
- BOOK: 90, INACCURACY: 50, MISTAKE: 20, BLUNDER: 0, OMISSION: 10,
1867
- };
1868
- if (!arr.length) return 0;
1869
- return arr.reduce((s, c) => s + (weights[c] || 50), 0) / arr.length;
1870
- }
1871
-
1872
- const wScore = score(WHITE_CL);
1873
- const bScore = score(BLACK_CL);
1874
-
1875
- document.getElementById('white-acc-bar').style.width = wScore + '%';
1876
- document.getElementById('black-acc-bar').style.width = bScore + '%';
1877
- document.getElementById('accuracy-text').textContent =
1878
- `White ${wScore.toFixed(0)}% · Black ${bScore.toFixed(0)}%`;
1879
- document.getElementById('eval-summary').style.display = 'block';
1880
- }
1881
-
1882
- /* ── Drag-and-drop ─────────────────────────────────────────── */
1883
- const dropZone = document.getElementById('drop-zone');
1884
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
1885
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
1886
- dropZone.addEventListener('drop', (e) => {
1887
- e.preventDefault();
1888
- dropZone.classList.remove('dragover');
1889
- const file = e.dataTransfer.files[0];
1890
- if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1891
- });
1892
-
1893
- document.getElementById('file-input').addEventListener('change', (e) => {
1894
- const file = e.target.files[0];
1895
- if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1896
- });
1897
-
1898
- /* ── Enter key on textarea ─────────────────────────────────── */
1899
- // Don't submit on Enter (user is typing PGN)
1900
- </script>
1901
- </body>
1902
- </html>