Fu01978 commited on
Commit
ed03ee1
·
verified ·
1 Parent(s): e70d315

Delete templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +0 -1551
templates/index.html DELETED
@@ -1,1551 +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
- MOBILE RESPONSIVE
749
- ══════════════════════════════════════════════════════════════ */
750
-
751
- /* Fluid board: fill the viewport minus eval bar and padding */
752
- @media (max-width: 700px) {
753
- :root {
754
- --board-size: min(92vw, 420px);
755
- --sq-size: calc(min(92vw, 420px) / 8);
756
- }
757
-
758
- .game-body {
759
- flex-direction: column;
760
- align-items: center;
761
- padding: 10px 8px 8px;
762
- gap: 10px;
763
- }
764
-
765
- /* Eval bar goes horizontal above the board on mobile */
766
- .eval-col {
767
- flex-direction: row;
768
- align-items: center;
769
- width: var(--board-size);
770
- margin-left: 18px; /* align with board (rank label width) */
771
- gap: 6px;
772
- }
773
-
774
- .eval-bar-wrap {
775
- width: 100%;
776
- height: 10px;
777
- flex: 1;
778
- }
779
-
780
- .eval-bar-white {
781
- /* horizontal bar: width instead of height */
782
- height: 100%;
783
- width: 50%;
784
- top: 0;
785
- left: 0;
786
- transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
787
- }
788
-
789
- /* hide the W / B labels on mobile */
790
- .eval-col > div:first-child,
791
- .eval-col > div:last-child { display: none; }
792
-
793
- .eval-label { font-size: 0.65rem; }
794
-
795
- /* board info strip */
796
- .board-info { margin-left: 18px; padding: 10px 12px; }
797
-
798
- /* nav row tighter */
799
- .nav-btn { width: 38px; height: 38px; font-size: 1rem; }
800
- .nav-row { gap: 6px; }
801
-
802
- /* Move list panel goes below the board, full width */
803
- .move-list-panel {
804
- width: var(--board-size);
805
- max-height: 220px;
806
- margin-left: 18px;
807
- }
808
-
809
- /* game header: stack players */
810
- .game-header {
811
- flex-wrap: wrap;
812
- gap: 6px;
813
- padding: 10px 14px;
814
- }
815
-
816
- .game-players { font-size: 0.9rem; }
817
- .game-meta { font-size: 0.72rem; }
818
- .btn-new-game { padding: 5px 10px; font-size: 0.78rem; }
819
-
820
- /* upload box full width */
821
- .upload-box { width: 95vw; }
822
- .upload-title { font-size: 1.5rem; }
823
-
824
- /* badge slightly smaller */
825
- .badge { width: 18px; height: 18px; font-size: 8px; }
826
-
827
- /* piece images */
828
- .piece-img { width: calc(var(--sq-size) * 0.9); height: calc(var(--sq-size) * 0.9); }
829
- .piece-uni { font-size: calc(var(--sq-size) * 0.75); }
830
- }
831
-
832
- @media (max-width: 400px) {
833
- :root {
834
- --board-size: 98vw;
835
- --sq-size: calc(98vw / 8);
836
- }
837
-
838
- .rank-labels { width: 14px; }
839
- .rank-label { font-size: 0.6rem; }
840
- .file-label { font-size: 0.6rem; }
841
- .eval-col { margin-left: 14px; }
842
- .board-info,
843
- .move-list-panel { margin-left: 14px; width: calc(98vw - 14px); }
844
- }
845
- </style>
846
- </head>
847
- <body>
848
-
849
- <!-- ═══════════════════════════════════════════════════════════════
850
- UPLOAD SCREEN
851
- ════════════════════════════════════════════════════════════════ -->
852
- <div id="upload-screen" class="screen active">
853
- <header>
854
- <div>
855
- <div class="logo">♟ ANALYSIS</div>
856
- <div class="logo-sub">Stockfish · Deep Engine</div>
857
- </div>
858
- </header>
859
-
860
- <div style="flex:1; display:flex; align-items:center; justify-content:center; padding:40px 20px;">
861
- <div class="upload-box">
862
- <h1 class="upload-title">Analyse a Game</h1>
863
- <p class="upload-sub">Paste PGN notation or drop a file below</p>
864
-
865
- <div class="drop-zone" id="drop-zone">
866
- <div class="drop-zone-label">
867
- Drop a <span onclick="document.getElementById('file-input').click()">PGN file</span> here, or paste below
868
- </div>
869
- <textarea id="pgn-textarea"
870
- placeholder='[Event "World Championship"]&#10;[White "Kasparov"]&#10;[Black "Karpov"]&#10;&#10;1. e4 e5 2. Nf3 Nc6 ...'></textarea>
871
- </div>
872
-
873
- <input type="file" id="file-input" accept=".pgn,.txt">
874
-
875
- <div class="upload-row">
876
- <div class="depth-row">
877
- <select id="mode-select" onchange="updateModeLabel()"
878
- style="background:var(--card2);border:1px solid var(--border);color:var(--text-dim);
879
- border-radius:4px;padding:3px 7px;font-family:'Crimson Pro',serif;font-size:0.88rem;outline:none;cursor:pointer;">
880
- <option value="depth">Depth</option>
881
- <option value="time">Time/move</option>
882
- </select>
883
- <input type="range" id="depth-slider" min="6" max="24" value="14"
884
- oninput="updateModeLabel()">
885
- <span id="depth-val" style="min-width:44px;display:inline-block;">14</span>
886
- </div>
887
- <button class="btn-primary" onclick="startUpload()">Analyse →</button>
888
- </div>
889
- <div style="font-size:0.78rem;color:var(--text-muted);margin-top:8px;font-style:italic;" id="speed-hint">
890
- Depth 14 ≈ fast · 18 ≈ balanced · 22+ ≈ slow
891
- </div>
892
-
893
- <div id="upload-error" style="color:var(--cl-blunder); font-size:0.85rem; margin-top:12px; display:none;"></div>
894
- </div>
895
- </div>
896
- </div>
897
-
898
- <!-- ═══════════════════════════════════════════════════════════════
899
- LOADING SCREEN
900
- ════════════════════════════════════════════════════════════════ -->
901
- <div id="loading-screen" class="screen">
902
- <header>
903
- <div>
904
- <div class="logo">♟ ANALYSIS</div>
905
- <div class="logo-sub">Stockfish · Deep Engine</div>
906
- </div>
907
- </header>
908
- <div style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px;">
909
- <div class="loading-chess-icon">♞</div>
910
- <div class="loading-title">ANALYSING GAME</div>
911
- <div class="progress-wrap">
912
- <div class="progress-bar" id="progress-bar"></div>
913
- </div>
914
- <div id="loading-move-label">Preparing engine…</div>
915
- <div class="loading-info" id="loading-info">Running Stockfish at depth <span id="loading-depth">—</span></div>
916
- </div>
917
- </div>
918
-
919
- <!-- ═══════════════════════════════════════════════════════════════
920
- GAME SCREEN
921
- ════════════════════════════════════════════════════════════════ -->
922
- <div id="game-screen" class="screen">
923
- <header>
924
- <div>
925
- <div class="logo">♟ ANALYSIS</div>
926
- <div class="logo-sub">Stockfish · Deep Engine</div>
927
- </div>
928
- <div style="display:flex; align-items:center; gap:24px; flex:1; justify-content:center;">
929
- <div class="game-players">
930
- <span class="player-white" id="hdr-white">White</span>
931
- <span class="vs-sep">vs</span>
932
- <span class="player-black" id="hdr-black">Black</span>
933
- </div>
934
- <div class="game-meta" id="hdr-meta"></div>
935
- </div>
936
- <button class="btn-new-game" onclick="newGame()">← New Game</button>
937
- </header>
938
-
939
- <div class="game-body">
940
-
941
- <!-- Eval bar -->
942
- <div class="eval-col">
943
- <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-bottom:4px; letter-spacing:0.06em;">W</div>
944
- <div class="eval-bar-wrap">
945
- <div class="eval-bar-white" id="eval-bar-white"></div>
946
- </div>
947
- <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-top:4px; letter-spacing:0.06em;">B</div>
948
- </div>
949
-
950
- <!-- Board + info -->
951
- <div class="board-col">
952
- <div class="board-outer">
953
- <!-- Rank labels -->
954
- <div class="rank-labels" id="rank-labels">
955
- <div class="rank-label">8</div>
956
- <div class="rank-label">7</div>
957
- <div class="rank-label">6</div>
958
- <div class="rank-label">5</div>
959
- <div class="rank-label">4</div>
960
- <div class="rank-label">3</div>
961
- <div class="rank-label">2</div>
962
- <div class="rank-label">1</div>
963
- </div>
964
- <!-- Board -->
965
- <div id="board"></div>
966
- </div>
967
-
968
- <!-- File labels -->
969
- <div class="file-labels">
970
- <div class="file-label">a</div>
971
- <div class="file-label">b</div>
972
- <div class="file-label">c</div>
973
- <div class="file-label">d</div>
974
- <div class="file-label">e</div>
975
- <div class="file-label">f</div>
976
- <div class="file-label">g</div>
977
- <div class="file-label">h</div>
978
- </div>
979
-
980
- <!-- Navigation -->
981
- <div class="nav-row">
982
- <button type="button" class="nav-btn" id="btn-start" onclick="navigateTo(0)" title="Start (Home)">⇤</button>
983
- <button type="button" class="nav-btn" id="btn-prev" onclick="navigateTo(currentIdx - 1)" title="Previous (←)">‹</button>
984
- <div class="nav-counter" id="nav-counter">— / —</div>
985
- <button type="button" class="nav-btn" id="btn-next" onclick="navigateTo(currentIdx + 1)" title="Next (→)">›</button>
986
- <button type="button" class="nav-btn" id="btn-end" onclick="navigateTo(gameData.positions.length - 1)" title="End (End)">⇥</button>
987
- <button type="button" class="flip-btn" onclick="flipBoard()" title="Flip board">⇅ Flip</button>
988
- </div>
989
-
990
- <!-- Move info -->
991
- <div class="board-info">
992
- <div style="display:flex; align-items:center; justify-content:space-between;">
993
- <div class="move-label" id="info-move-label">Starting Position</div>
994
- <div class="eval-label" id="info-eval">0.00</div>
995
- </div>
996
- <div class="move-class-row" id="info-class-row" style="display:none;">
997
- <div class="move-class-dot" id="info-class-dot"></div>
998
- <div class="move-class-text" id="info-class-text"></div>
999
- <div class="move-cp" id="info-cp"></div>
1000
- </div>
1001
- <div class="move-notes" id="info-notes"></div>
1002
-
1003
- <!-- Continuations -->
1004
- <div class="continuations" id="continuations" style="display:none;">
1005
- <div class="cont-block" id="cont-player-block">
1006
- <div class="cont-label">Continuation after this move</div>
1007
- <div class="cont-moves" id="cont-player"></div>
1008
- </div>
1009
- <div class="cont-block" id="cont-best-block" style="display:none;">
1010
- <div class="cont-label">Best move was <span id="cont-best-move-name" class="cont-best-move"></span></div>
1011
- <div class="cont-label" style="margin-top:4px;">Continuation after best move</div>
1012
- <div class="cont-moves" id="cont-best"></div>
1013
- </div>
1014
- </div>
1015
- </div>
1016
- </div>
1017
-
1018
- <!-- Move list panel -->
1019
- <div class="move-list-panel">
1020
- <div class="move-list-header">Move List</div>
1021
- <div class="move-list-body" id="move-list"></div>
1022
- <div class="eval-summary" id="eval-summary" style="display:none;">
1023
- <div class="eval-summary-row">
1024
- <div class="eval-summary-name">White</div>
1025
- <div class="eval-summary-bar-wrap">
1026
- <div class="eval-summary-bar" id="white-acc-bar" style="background:var(--cl-excellent);"></div>
1027
- </div>
1028
- </div>
1029
- <div class="eval-summary-row">
1030
- <div class="eval-summary-name">Black</div>
1031
- <div class="eval-summary-bar-wrap">
1032
- <div class="eval-summary-bar" id="black-acc-bar" style="background:var(--cl-excellent);"></div>
1033
- </div>
1034
- </div>
1035
- <div class="eval-accuracy" id="accuracy-text"></div>
1036
- </div>
1037
- </div>
1038
-
1039
- </div><!-- /game-body -->
1040
- </div><!-- /game-screen -->
1041
-
1042
- <!-- ═══════════════════════════════════════════════════════════════
1043
- JAVASCRIPT
1044
- ════════════════════════════════════════════════════════════════ -->
1045
- <script>
1046
- /* ── State ─────────────────────────────────────────────────── */
1047
- let gameData = null;
1048
- let currentIdx = 0;
1049
- let flipped = false;
1050
- let currentGameId = null;
1051
- let progressEvt = null;
1052
-
1053
- /* ── Classification config ─────────────────────────────────── */
1054
- const CL = {
1055
- BRILLIANT: { color: '#00b5ad', badge: '!!', label: '!! Brilliant' },
1056
- GREAT: { color: '#1a4fc4', badge: '!', label: '! Great' },
1057
- BEST: { color: '#7ec820', badge: '★', label: '★ Best' },
1058
- EXCELLENT: { color: '#7ec820', badge: '👍', label: '👍 Excellent' },
1059
- GOOD: { color: '#4db896', badge: '✓', label: '✓ Good' },
1060
- BOOK: { color: '#a0754a', badge: '📖', label: '📖 Book' },
1061
- INACCURACY: { color: '#d4b030', badge: '?!', label: '?! Inaccuracy' },
1062
- MISTAKE: { color: '#d07020', badge: '?', label: '? Mistake' },
1063
- BLUNDER: { color: '#c83030', badge: '??', label: '?? Blunder' },
1064
- OMISSION: { color: '#e05050', badge: '✖', label: '✖ Missed Tactic' },
1065
- };
1066
-
1067
- /* ── Piece maps ────────────────────────────────────────────── */
1068
- const PIECE_CODE = {
1069
- K:'wK', Q:'wQ', R:'wR', B:'wB', N:'wN', P:'wP',
1070
- k:'bK', q:'bQ', r:'bR', b:'bB', n:'bN', p:'bP',
1071
- };
1072
- const PIECE_UNI = {
1073
- K:'♔', Q:'♕', R:'♖', B:'♗', N:'♘', P:'♙',
1074
- k:'♚', q:'♛', r:'♜', b:'♝', n:'♞', p:'♟',
1075
- };
1076
- /* Track which piece images failed to load (use unicode instead) */
1077
- const imgFailed = {};
1078
-
1079
- /* ── Screens ───────────────────────────────────────────────── */
1080
- function showScreen(id) {
1081
- document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
1082
- document.getElementById(id).classList.add('active');
1083
- }
1084
-
1085
- /* ── Upload ────────────────────────────────────────────────── */
1086
- function updateModeLabel() {
1087
- const mode = document.getElementById('mode-select').value;
1088
- const val = +document.getElementById('depth-slider').value;
1089
- const hint = document.getElementById('speed-hint');
1090
- if (mode === 'depth') {
1091
- document.getElementById('depth-val').textContent = val;
1092
- hint.textContent = `Depth ${val} ≈ ${val<=10?'very fast':val<=14?'fast':val<=18?'balanced':val<=21?'slow':'very slow'}`;
1093
- } else {
1094
- // remap slider 6-24 → 100ms-3000ms
1095
- const ms = Math.round(((val - 6) / 18) * 2900 + 100);
1096
- document.getElementById('depth-val').textContent = ms + ' ms';
1097
- hint.textContent = `${ms}ms per move ≈ ${ms<=300?'fast':ms<=800?'balanced':'thorough'}`;
1098
- }
1099
- }
1100
- updateModeLabel();
1101
-
1102
- function startUpload() {
1103
- const pgn = document.getElementById('pgn-textarea').value.trim();
1104
- const mode = document.getElementById('mode-select').value;
1105
- const sliderVal = +document.getElementById('depth-slider').value;
1106
- const errEl = document.getElementById('upload-error');
1107
-
1108
- if (!pgn) { errEl.textContent = 'Please paste a PGN first.'; errEl.style.display='block'; return; }
1109
- errEl.style.display = 'none';
1110
-
1111
- const fd = new FormData();
1112
- fd.append('pgn_text', pgn);
1113
- if (mode === 'depth') {
1114
- fd.append('depth', sliderVal);
1115
- } else {
1116
- const ms = Math.round(((sliderVal - 6) / 18) * 2900 + 100);
1117
- fd.append('time_ms', ms);
1118
- }
1119
-
1120
- fetch('/upload', { method: 'POST', body: fd })
1121
- .then(r => {
1122
- const ct = r.headers.get('content-type') || '';
1123
- if (!ct.includes('application/json')) {
1124
- throw new Error('Server returned non-JSON (status ' + r.status + '). ' +
1125
- 'If using localtunnel, open the tunnel URL directly in a new tab and click Continue first.');
1126
- }
1127
- return r.json();
1128
- })
1129
- .then(d => {
1130
- if (d.error) { showError(d.error); return; }
1131
- currentGameId = d.game_id;
1132
- document.getElementById('loading-depth').textContent =
1133
- mode === 'depth' ? 'depth ' + sliderVal : Math.round(((sliderVal-6)/18)*2900+100) + 'ms/move';
1134
- showScreen('loading-screen');
1135
- pollProgress(d.game_id);
1136
- })
1137
- .catch(e => showError('Upload failed: ' + e.message));
1138
- }
1139
-
1140
- function showError(msg) {
1141
- const el = document.getElementById('upload-error');
1142
- if (el) { el.textContent = msg; el.style.display = 'block'; }
1143
- else { alert(msg); }
1144
- showScreen('upload-screen');
1145
- }
1146
-
1147
- function pollProgress(gameId) {
1148
- if (progressEvt) progressEvt.close();
1149
- const bar = document.getElementById('progress-bar');
1150
- const label = document.getElementById('loading-move-label');
1151
-
1152
- progressEvt = new EventSource(`/progress/${gameId}`);
1153
- progressEvt.onmessage = (e) => {
1154
- const p = JSON.parse(e.data);
1155
-
1156
- if (p.error) {
1157
- progressEvt.close();
1158
- showError('Analysis error: ' + p.error);
1159
- return;
1160
- }
1161
-
1162
- const pct = p.total > 0 ? Math.round((p.done / p.total) * 100) : 0;
1163
- bar.style.width = pct + '%';
1164
- label.textContent = p.done > 0
1165
- ? `Move ${p.done} of ${p.total} analysed`
1166
- : 'Initialising engine…';
1167
-
1168
- if (p.complete && !p.error) {
1169
- progressEvt.close();
1170
- loadGame(gameId);
1171
- }
1172
- };
1173
- }
1174
-
1175
- function loadGame(gameId) {
1176
- fetch(`/game/${gameId}`)
1177
- .then(r => r.json())
1178
- .then(d => {
1179
- if (d.error) { alert(d.error); showScreen('upload-screen'); return; }
1180
- gameData = d;
1181
- setupGameScreen();
1182
- showScreen('game-screen');
1183
- });
1184
- }
1185
-
1186
- /* ── Game screen setup ─────────────────────────────────────── */
1187
- function setupGameScreen() {
1188
- const h = gameData.headers;
1189
- document.getElementById('hdr-white').textContent = h.White || 'White';
1190
- document.getElementById('hdr-black').textContent = h.Black || 'Black';
1191
- document.getElementById('hdr-meta').textContent = [h.Event, h.Date, h.Result].filter(Boolean).join(' · ');
1192
-
1193
- buildMoveList();
1194
- computeAccuracy();
1195
- navigateTo(0);
1196
- }
1197
-
1198
- function newGame() {
1199
- gameData = null;
1200
- currentIdx = 0;
1201
- flipped = false;
1202
- document.getElementById('pgn-textarea').value = '';
1203
- showScreen('upload-screen');
1204
- }
1205
-
1206
- /* ── FEN parser ────────────────────────────────────────────── */
1207
- function parseFEN(fen) {
1208
- const [placement] = fen.split(' ');
1209
- const ranks = placement.split('/');
1210
- const grid = []; // grid[rank][file], rank 0 = rank 8
1211
- for (const row of ranks) {
1212
- const r = [];
1213
- for (const ch of row) {
1214
- if (/\d/.test(ch)) { for (let i=0; i<+ch; i++) r.push(null); }
1215
- else { r.push(ch); }
1216
- }
1217
- grid.push(r);
1218
- }
1219
- return grid;
1220
- }
1221
-
1222
- /* ── Board render ──────────────────────────────────────────── */
1223
- function renderBoard(pos) {
1224
- const grid = parseFEN(pos.fen);
1225
- const fromSq = pos.from_sq;
1226
- const toSq = pos.to_sq;
1227
- const board = document.getElementById('board');
1228
- board.innerHTML = '';
1229
-
1230
- // Update rank/file labels for flipped
1231
- const rankLabels = document.getElementById('rank-labels');
1232
- rankLabels.innerHTML = '';
1233
- for (let i=0; i<8; i++) {
1234
- const d = document.createElement('div');
1235
- d.className = 'rank-label';
1236
- d.textContent = flipped ? (i + 1) : (8 - i);
1237
- rankLabels.appendChild(d);
1238
- }
1239
-
1240
- const fileLabels = document.querySelector('.file-labels');
1241
- fileLabels.innerHTML = '';
1242
- for (let i=0; i<8; i++) {
1243
- const d = document.createElement('div');
1244
- d.className = 'file-label';
1245
- d.textContent = flipped
1246
- ? String.fromCharCode(104 - i) // h...a
1247
- : String.fromCharCode(97 + i); // a...h
1248
- fileLabels.appendChild(d);
1249
- }
1250
-
1251
- for (let displayRank=0; displayRank<8; displayRank++) {
1252
- for (let displayFile=0; displayFile<8; displayFile++) {
1253
- const gridRank = flipped ? (7 - displayRank) : displayRank;
1254
- const gridFile = flipped ? (7 - displayFile) : displayFile;
1255
-
1256
- const squareName = String.fromCharCode(97 + gridFile) + (8 - gridRank);
1257
- const piece = grid[gridRank][gridFile];
1258
- const isLight = (gridRank + gridFile) % 2 === 0;
1259
-
1260
- const sq = document.createElement('div');
1261
- sq.className = 'sq ' + (isLight ? 'light' : 'dark');
1262
- sq.dataset.sq = squareName;
1263
-
1264
- if (squareName === fromSq) sq.classList.add('hi-from');
1265
- if (squareName === toSq) sq.classList.add('hi-to');
1266
-
1267
- if (piece) {
1268
- if (imgFailed[PIECE_CODE[piece]]) {
1269
- // unicode fallback
1270
- const span = document.createElement('span');
1271
- span.className = 'piece-uni';
1272
- span.textContent = PIECE_UNI[piece];
1273
- sq.appendChild(span);
1274
- } else {
1275
- const img = document.createElement('img');
1276
- img.className = 'piece-img';
1277
- img.src = `https://lichess1.org/assets/piece/cburnett/${PIECE_CODE[piece]}.svg`;
1278
- img.alt = piece;
1279
- img.onerror = function() {
1280
- imgFailed[PIECE_CODE[piece]] = true;
1281
- const span = document.createElement('span');
1282
- span.className = 'piece-uni';
1283
- span.textContent = PIECE_UNI[piece];
1284
- this.parentNode.replaceChild(span, this);
1285
- };
1286
- sq.appendChild(img);
1287
- }
1288
- }
1289
-
1290
- // Classification badge on the destination square
1291
- if (squareName === toSq && pos.classification && CL[pos.classification]) {
1292
- const cl = CL[pos.classification];
1293
- const bdg = document.createElement('div');
1294
- bdg.className = `badge badge-${pos.classification}`;
1295
- bdg.textContent = cl.badge;
1296
- sq.appendChild(bdg);
1297
- }
1298
-
1299
- sq.addEventListener('click', () => { /* future: select piece */ });
1300
- board.appendChild(sq);
1301
- }
1302
- }
1303
- }
1304
-
1305
- /* ── Eval bar ──────────────────────────────────────────────── */
1306
- function updateEvalBar(evalWhite) {
1307
- const pct = 50 + 50 * (2 / Math.PI) * Math.atan(evalWhite / 400);
1308
- const bar = document.getElementById('eval-bar-white');
1309
- const val = (flipped ? (100 - pct) : pct) + '%';
1310
- bar.style.height = val;
1311
- bar.style.width = val; // used by horizontal mobile bar
1312
- }
1313
-
1314
- /* ── Move info panel ───────────────────────────────────────── */
1315
- function updateInfo(pos) {
1316
- const evalDisp = pos.eval_display;
1317
- document.getElementById('info-eval').textContent = evalDisp;
1318
- document.getElementById('info-eval').className = 'eval-label';
1319
-
1320
- const clRow = document.getElementById('info-class-row');
1321
- const clDot = document.getElementById('info-class-dot');
1322
- const clText = document.getElementById('info-class-text');
1323
- const cpEl = document.getElementById('info-cp');
1324
- const notes = document.getElementById('info-notes');
1325
- const label = document.getElementById('info-move-label');
1326
-
1327
- if (!pos.san) {
1328
- label.textContent = 'Starting Position';
1329
- clRow.style.display = 'none';
1330
- notes.textContent = '';
1331
- } else {
1332
- const colLabel = pos.color === 'white' ? 'White' : 'Black';
1333
- label.textContent = `${pos.move_number}${pos.color === 'white' ? '.' : '…'} ${pos.san} — ${colLabel}`;
1334
-
1335
- clRow.style.display = 'flex';
1336
- if (pos.classification && CL[pos.classification]) {
1337
- const cl = CL[pos.classification];
1338
- clDot.style.background = cl.color;
1339
- clText.textContent = cl.label;
1340
- clText.className = `move-class-text cl-${pos.classification}`;
1341
- const eStr = pos.e_loss > 0.005
1342
- ? `(−${pos.e_loss.toFixed(3)} EP)`
1343
- : '';
1344
- cpEl.textContent = eStr;
1345
- notes.textContent = pos.notes && pos.notes.length ? pos.notes.join(' · ') : '';
1346
- } else {
1347
- clRow.style.display = 'none';
1348
- }
1349
- }
1350
-
1351
- // ── Continuations ────────────────────────────────────────────
1352
- const contSection = document.getElementById('continuations');
1353
- const contPlayer = document.getElementById('cont-player');
1354
- const contBest = document.getElementById('cont-best');
1355
- const contBestBlock = document.getElementById('cont-best-block');
1356
- const contBestName = document.getElementById('cont-best-move-name');
1357
-
1358
- if (!pos.san) {
1359
- contSection.style.display = 'none';
1360
- } else {
1361
- contSection.style.display = 'flex';
1362
-
1363
- // Player continuation
1364
- if (pos.player_pv && pos.player_pv.length) {
1365
- const pMoves = pos.player_pv.map((m, i) => {
1366
- const cls = i === 0 ? 'cont-player-move' : '';
1367
- return cls ? `<span class="${cls}">${m}</span>` : m;
1368
- });
1369
- contPlayer.innerHTML = pMoves.join(' ');
1370
- } else {
1371
- contPlayer.textContent = '—';
1372
- }
1373
-
1374
- // Best move & continuation (only show when best != played move)
1375
- if (pos.best_san && pos.best_san !== pos.san) {
1376
- contBestBlock.style.display = 'block';
1377
- contBestName.textContent = pos.best_san;
1378
- if (pos.best_pv && pos.best_pv.length) {
1379
- const bMoves = pos.best_pv.map((m, i) => {
1380
- const cls = i === 0 ? 'cont-best-move' : '';
1381
- return cls ? `<span class="${cls}">${m}</span>` : m;
1382
- });
1383
- contBest.innerHTML = bMoves.join(' ');
1384
- } else {
1385
- contBest.textContent = '—';
1386
- }
1387
- } else {
1388
- contBestBlock.style.display = 'none';
1389
- }
1390
- }
1391
- }
1392
-
1393
- /* ── Navigate ──────────────────────────────────────────────── */
1394
- function navigateTo(idx) {
1395
- if (!gameData) return;
1396
- idx = Math.max(0, Math.min(idx, gameData.positions.length - 1));
1397
- currentIdx = idx;
1398
- const pos = gameData.positions[idx];
1399
-
1400
- renderBoard(pos);
1401
- updateEvalBar(pos.eval_white);
1402
- updateInfo(pos);
1403
-
1404
- document.getElementById('btn-start').disabled = idx === 0;
1405
- document.getElementById('btn-prev').disabled = idx === 0;
1406
- document.getElementById('btn-next').disabled = idx === gameData.positions.length - 1;
1407
- document.getElementById('btn-end').disabled = idx === gameData.positions.length - 1;
1408
-
1409
- const total = gameData.positions.length - 1;
1410
- document.getElementById('nav-counter').textContent = idx === 0
1411
- ? `Start / ${total}`
1412
- : `${idx} / ${total}`;
1413
-
1414
- // Highlight move list
1415
- document.querySelectorAll('.move-cell').forEach(c => c.classList.remove('current'));
1416
- if (idx > 0) {
1417
- const cell = document.querySelector(`[data-move-idx="${idx}"]`);
1418
- if (cell) {
1419
- cell.classList.add('current');
1420
- // Scroll the move-list container, not the whole page
1421
- const listBody = document.getElementById('move-list');
1422
- const cellTop = cell.offsetTop;
1423
- const cellBottom = cellTop + cell.offsetHeight;
1424
- const bodyTop = listBody.scrollTop;
1425
- const bodyBottom = bodyTop + listBody.clientHeight;
1426
- if (cellTop < bodyTop) {
1427
- listBody.scrollTop = cellTop - 8;
1428
- } else if (cellBottom > bodyBottom) {
1429
- listBody.scrollTop = cellBottom - listBody.clientHeight + 8;
1430
- }
1431
- }
1432
- }
1433
- }
1434
-
1435
- function flipBoard() {
1436
- flipped = !flipped;
1437
- navigateTo(currentIdx);
1438
- }
1439
-
1440
- /* ── Keyboard ──────────────────────────────────────────────── */
1441
- document.addEventListener('keydown', (e) => {
1442
- if (!gameData) return;
1443
- if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); navigateTo(currentIdx - 1); }
1444
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); navigateTo(currentIdx + 1); }
1445
- if (e.key === 'Home') { e.preventDefault(); navigateTo(0); }
1446
- if (e.key === 'End') { e.preventDefault(); navigateTo(gameData.positions.length - 1); }
1447
- });
1448
-
1449
- /* ── Move list ─────────────────────────────────────────────── */
1450
- function buildMoveList() {
1451
- const list = document.getElementById('move-list');
1452
- list.innerHTML = '';
1453
- const positions = gameData.positions;
1454
-
1455
- // positions[0] = start, positions[1] = white's first move, positions[2] = black's...
1456
- let pairDiv = null;
1457
- for (let i = 1; i < positions.length; i++) {
1458
- const pos = positions[i];
1459
- const isWhite = (i % 2 === 1);
1460
-
1461
- if (isWhite) {
1462
- pairDiv = document.createElement('div');
1463
- pairDiv.className = 'move-pair';
1464
-
1465
- const numEl = document.createElement('div');
1466
- numEl.className = 'move-num';
1467
- numEl.textContent = pos.move_number + '.';
1468
- pairDiv.appendChild(numEl);
1469
- list.appendChild(pairDiv);
1470
- }
1471
-
1472
- const cell = document.createElement('div');
1473
- cell.className = 'move-cell';
1474
- cell.dataset.moveIdx = i;
1475
-
1476
- const cl = pos.classification && CL[pos.classification] ? CL[pos.classification] : null;
1477
-
1478
- if (cl) {
1479
- const dot = document.createElement('div');
1480
- dot.className = 'cl-dot';
1481
- dot.style.background = cl.color;
1482
- cell.appendChild(dot);
1483
- } else {
1484
- // placeholder dot
1485
- const dot = document.createElement('div');
1486
- dot.className = 'cl-dot';
1487
- dot.style.background = 'transparent';
1488
- cell.appendChild(dot);
1489
- }
1490
-
1491
- const san = document.createElement('span');
1492
- san.className = 'san-text';
1493
- san.textContent = pos.san || '';
1494
- cell.appendChild(san);
1495
-
1496
- cell.addEventListener('click', () => navigateTo(i));
1497
- if (pairDiv) pairDiv.appendChild(cell);
1498
- }
1499
- }
1500
-
1501
- /* ── Accuracy summary ──────────────────────────────────────── */
1502
- function computeAccuracy() {
1503
- const positions = gameData.positions;
1504
- const WHITE_CL = []; const BLACK_CL = [];
1505
-
1506
- for (let i = 1; i < positions.length; i++) {
1507
- const pos = positions[i];
1508
- const arr = pos.color === 'white' ? WHITE_CL : BLACK_CL;
1509
- arr.push(pos.classification);
1510
- }
1511
-
1512
- function score(arr) {
1513
- const weights = {
1514
- BRILLIANT: 100, GREAT: 97, BEST: 95, EXCELLENT: 88, GOOD: 78,
1515
- BOOK: 90, INACCURACY: 50, MISTAKE: 20, BLUNDER: 0, OMISSION: 10,
1516
- };
1517
- if (!arr.length) return 0;
1518
- return arr.reduce((s, c) => s + (weights[c] || 50), 0) / arr.length;
1519
- }
1520
-
1521
- const wScore = score(WHITE_CL);
1522
- const bScore = score(BLACK_CL);
1523
-
1524
- document.getElementById('white-acc-bar').style.width = wScore + '%';
1525
- document.getElementById('black-acc-bar').style.width = bScore + '%';
1526
- document.getElementById('accuracy-text').textContent =
1527
- `White ${wScore.toFixed(0)}% · Black ${bScore.toFixed(0)}%`;
1528
- document.getElementById('eval-summary').style.display = 'block';
1529
- }
1530
-
1531
- /* ── Drag-and-drop ─────────────────────────────────────────── */
1532
- const dropZone = document.getElementById('drop-zone');
1533
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
1534
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
1535
- dropZone.addEventListener('drop', (e) => {
1536
- e.preventDefault();
1537
- dropZone.classList.remove('dragover');
1538
- const file = e.dataTransfer.files[0];
1539
- if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1540
- });
1541
-
1542
- document.getElementById('file-input').addEventListener('change', (e) => {
1543
- const file = e.target.files[0];
1544
- if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1545
- });
1546
-
1547
- /* ── Enter key on textarea ─────────────────────────────────── */
1548
- // Don't submit on Enter (user is typing PGN)
1549
- </script>
1550
- </body>
1551
- </html>