Fu01978 commited on
Commit
1e99326
·
verified ·
1 Parent(s): 58bbd8e

Delete templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +0 -943
templates/index.html DELETED
@@ -1,943 +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">
6
- <title>Chess Analysis</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Crimson+Pro:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
- <style>
11
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
-
13
- :root {
14
- --bg: #07070e;
15
- --bg-card: #0f0f1d;
16
- --bg-elevated: #16162a;
17
- --bg-deepest: #050509;
18
- --gold: #c9a84c;
19
- --gold-light: #e8c97a;
20
- --gold-dim: #7a6430;
21
- --gold-glow: rgba(201,168,76,0.15);
22
- --text: #ede8dc;
23
- --text-sub: #9a9282;
24
- --text-muted: #5a5650;
25
- --border: #1e1e36;
26
- --border-bright:#2e2e4e;
27
- --sq: 66px;
28
- }
29
-
30
- html, body {
31
- height: 100%;
32
- font-family: 'Crimson Pro', Georgia, serif;
33
- background: var(--bg);
34
- color: var(--text);
35
- overflow: hidden;
36
- }
37
-
38
- body::before {
39
- content: '';
40
- position: fixed; inset: 0; z-index: 0;
41
- background:
42
- radial-gradient(ellipse 80% 60% at 15% 50%, rgba(20,18,60,0.5) 0%, transparent 70%),
43
- radial-gradient(ellipse 60% 80% at 85% 10%, rgba(10,25,60,0.4) 0%, transparent 60%);
44
- pointer-events: none;
45
- }
46
-
47
- /* ── Screen system ───────────────────────────────────────────────────── */
48
- .screen {
49
- position: fixed; inset: 0; z-index: 1;
50
- display: flex; flex-direction: column; align-items: center;
51
- overflow-y: auto;
52
- opacity: 0;
53
- pointer-events: none;
54
- transition: opacity 0.35s ease;
55
- }
56
- .screen.active {
57
- opacity: 1;
58
- pointer-events: all;
59
- }
60
-
61
- /* ── Shared ──────────────────────────────────────────────────────────── */
62
- .mono { font-family: 'JetBrains Mono', 'Consolas', monospace; }
63
-
64
- .btn {
65
- display: inline-flex; align-items: center; gap: 8px;
66
- border: none; cursor: pointer;
67
- font-family: 'Crimson Pro', serif;
68
- font-weight: 500;
69
- letter-spacing: 0.05em;
70
- transition: all 0.2s;
71
- }
72
- .btn:disabled { opacity: 0.3; cursor: default; }
73
-
74
- .btn-gold {
75
- background: linear-gradient(135deg, #b8922a 0%, #c9a84c 40%, #e8c97a 100%);
76
- color: #07070e;
77
- font-size: 17px;
78
- padding: 13px 40px;
79
- border-radius: 3px;
80
- box-shadow: 0 4px 20px rgba(201,168,76,0.25), inset 0 1px 0 rgba(255,255,255,0.15);
81
- }
82
- .btn-gold:hover {
83
- background: linear-gradient(135deg, #c9a84c 0%, #e8c97a 60%, #f5dca0 100%);
84
- box-shadow: 0 6px 30px rgba(201,168,76,0.4), inset 0 1px 0 rgba(255,255,255,0.2);
85
- transform: translateY(-1px);
86
- }
87
- .btn-ghost {
88
- background: transparent;
89
- color: var(--text-sub);
90
- font-size: 14px;
91
- padding: 7px 14px;
92
- border: 1px solid var(--border-bright);
93
- border-radius: 3px;
94
- }
95
- .btn-ghost:hover { color: var(--text); border-color: var(--gold-dim); background: rgba(201,168,76,0.05); }
96
-
97
- /* ── Screen 1: Input ─────────────────────────────────────────────────── */
98
- #s-input {
99
- justify-content: center;
100
- padding: 40px 20px;
101
- }
102
-
103
- .brand {
104
- text-align: center;
105
- margin-bottom: 36px;
106
- }
107
- .brand-name {
108
- font-family: 'Playfair Display', serif;
109
- font-size: 13px;
110
- font-weight: 400;
111
- letter-spacing: 0.35em;
112
- text-transform: uppercase;
113
- color: var(--gold);
114
- }
115
- .brand-line {
116
- display: inline-block;
117
- width: 40px; height: 1px;
118
- background: var(--gold-dim);
119
- vertical-align: middle;
120
- margin: 0 12px;
121
- opacity: 0.5;
122
- }
123
-
124
- .input-card {
125
- background: var(--bg-card);
126
- border: 1px solid var(--border);
127
- border-radius: 4px;
128
- padding: 48px 52px;
129
- width: 100%; max-width: 640px;
130
- box-shadow:
131
- 0 40px 100px rgba(0,0,0,0.6),
132
- 0 0 0 1px rgba(201,168,76,0.06),
133
- inset 0 1px 0 rgba(255,255,255,0.03);
134
- }
135
-
136
- .input-card h1 {
137
- font-family: 'Playfair Display', serif;
138
- font-size: 32px; font-weight: 600;
139
- color: var(--text);
140
- margin-bottom: 6px;
141
- }
142
- .input-card .subtitle {
143
- font-size: 16px; font-weight: 300;
144
- color: var(--text-sub);
145
- margin-bottom: 32px;
146
- }
147
-
148
- .field-label {
149
- display: block;
150
- font-size: 11px; font-weight: 500;
151
- letter-spacing: 0.2em; text-transform: uppercase;
152
- color: var(--gold-dim);
153
- margin-bottom: 8px;
154
- }
155
-
156
- #pgn {
157
- width: 100%; height: 200px;
158
- background: var(--bg-deepest);
159
- border: 1px solid var(--border);
160
- border-radius: 3px;
161
- color: var(--text);
162
- font-family: 'JetBrains Mono', monospace;
163
- font-size: 13px;
164
- line-height: 1.6;
165
- padding: 16px;
166
- resize: vertical;
167
- outline: none;
168
- transition: border-color 0.2s;
169
- margin-bottom: 28px;
170
- }
171
- #pgn::placeholder { color: var(--text-muted); }
172
- #pgn:focus { border-color: var(--gold-dim); }
173
-
174
- .depth-group { margin-bottom: 36px; }
175
- .depth-row {
176
- display: flex; align-items: center; gap: 16px;
177
- }
178
- #depth {
179
- flex: 1;
180
- -webkit-appearance: none;
181
- height: 3px;
182
- background: var(--border-bright);
183
- border-radius: 2px;
184
- outline: none;
185
- }
186
- #depth::-webkit-slider-thumb {
187
- -webkit-appearance: none;
188
- width: 16px; height: 16px;
189
- border-radius: 50%;
190
- background: var(--gold);
191
- cursor: pointer;
192
- box-shadow: 0 0 8px rgba(201,168,76,0.4);
193
- }
194
- #depth::-moz-range-thumb {
195
- width: 16px; height: 16px;
196
- border-radius: 50%;
197
- background: var(--gold);
198
- cursor: pointer; border: none;
199
- }
200
- #depth-val {
201
- font-family: 'JetBrains Mono', monospace;
202
- font-size: 20px; font-weight: 500;
203
- color: var(--gold);
204
- min-width: 28px; text-align: right;
205
- }
206
- .depth-hint {
207
- display: block;
208
- font-size: 13px; font-weight: 300;
209
- color: var(--text-muted);
210
- margin-top: 8px;
211
- }
212
-
213
- /* ── Screen 2: Loading ───────────────────────────────────────────────── */
214
- #s-loading { justify-content: center; align-items: center; }
215
-
216
- .loading-inner {
217
- text-align: center;
218
- width: 480px; max-width: 90vw;
219
- }
220
- .loading-inner h2 {
221
- font-family: 'Playfair Display', serif;
222
- font-size: 22px; font-weight: 400;
223
- letter-spacing: 0.15em;
224
- color: var(--text-sub);
225
- margin-bottom: 36px;
226
- text-transform: uppercase;
227
- }
228
-
229
- .progress-track {
230
- width: 100%; height: 3px;
231
- background: var(--border);
232
- border-radius: 2px;
233
- overflow: hidden;
234
- margin-bottom: 20px;
235
- }
236
- #progress-bar {
237
- height: 100%;
238
- background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-light));
239
- border-radius: 2px;
240
- width: 0%;
241
- transition: width 0.45s cubic-bezier(0.4,0,0.2,1);
242
- box-shadow: 0 0 12px rgba(201,168,76,0.5);
243
- }
244
-
245
- #progress-msg {
246
- font-size: 14px; font-weight: 300;
247
- color: var(--text-sub);
248
- letter-spacing: 0.05em;
249
- }
250
- #progress-pct {
251
- font-family: 'JetBrains Mono', monospace;
252
- font-size: 12px;
253
- color: var(--gold-dim);
254
- margin-top: 10px;
255
- }
256
-
257
- /* ── Screen 3: Analysis ──────────────────────────────────────────────── */
258
- #s-analysis {
259
- align-items: center;
260
- padding: 0 20px 40px;
261
- }
262
-
263
- .analysis-header {
264
- display: flex; align-items: center; justify-content: space-between;
265
- width: 100%; max-width: 620px;
266
- padding: 20px 0 18px;
267
- border-bottom: 1px solid var(--border);
268
- margin-bottom: 24px;
269
- gap: 12px;
270
- }
271
-
272
- .players {
273
- text-align: center; flex: 1;
274
- display: flex; align-items: center; justify-content: center; gap: 14px;
275
- }
276
- .player-name {
277
- font-family: 'Playfair Display', serif;
278
- font-size: 18px; font-weight: 500;
279
- }
280
- .white-player { color: #f0ede4; }
281
- .black-player { color: #a09880; }
282
- .vs {
283
- font-size: 12px; font-weight: 300;
284
- letter-spacing: 0.2em; text-transform: uppercase;
285
- color: var(--text-muted);
286
- }
287
-
288
- /* ── Board ───────────────────────────────────────────────────────────── */
289
- .board-area { position: relative; }
290
-
291
- .board-with-coords {
292
- display: flex; align-items: stretch; gap: 0;
293
- }
294
-
295
- .rank-labels {
296
- display: flex; flex-direction: column;
297
- justify-content: space-around;
298
- padding-right: 8px;
299
- padding-bottom: calc(var(--sq) * 0); /* align with board */
300
- }
301
- .coord {
302
- font-family: 'JetBrains Mono', monospace;
303
- font-size: 11px;
304
- color: var(--text-muted);
305
- width: var(--sq); height: var(--sq);
306
- display: flex; align-items: center; justify-content: center;
307
- user-select: none;
308
- }
309
- .rank-labels .coord { width: auto; }
310
-
311
- .board-column { display: flex; flex-direction: column; }
312
-
313
- #board {
314
- display: grid;
315
- grid-template-columns: repeat(8, var(--sq));
316
- grid-template-rows: repeat(8, var(--sq));
317
- box-shadow:
318
- 0 0 0 2px var(--border),
319
- 0 0 0 3px var(--border-bright),
320
- 0 24px 80px rgba(0,0,0,0.8),
321
- 0 4px 16px rgba(0,0,0,0.5);
322
- }
323
-
324
- .file-labels {
325
- display: flex;
326
- margin-top: 4px;
327
- }
328
- .file-labels .coord { height: auto; padding: 4px 0; }
329
-
330
- .sq {
331
- width: var(--sq); height: var(--sq);
332
- position: relative;
333
- display: flex; align-items: center; justify-content: center;
334
- overflow: hidden;
335
- }
336
- .sq.light { background: #f0d9b5; }
337
- .sq.dark { background: #b58863; }
338
-
339
- .overlay {
340
- position: absolute; inset: 0; z-index: 1;
341
- pointer-events: none;
342
- }
343
- .from-ov { background: rgba(255, 220, 60, 0.35); }
344
- .to-ov { opacity: 0.72; }
345
-
346
- .cls-badge {
347
- position: absolute; top: 2px; right: 2px; z-index: 4;
348
- width: 20px; height: 20px;
349
- border-radius: 50%;
350
- display: flex; align-items: center; justify-content: center;
351
- font-size: 10px; font-weight: 700;
352
- color: #fff;
353
- font-family: 'JetBrains Mono', monospace;
354
- pointer-events: none;
355
- box-shadow: 0 1px 4px rgba(0,0,0,0.6);
356
- }
357
-
358
- .piece {
359
- position: relative; z-index: 2;
360
- font-size: 42px; line-height: 1;
361
- display: flex; align-items: center; justify-content: center;
362
- width: 100%; height: 100%;
363
- pointer-events: none; user-select: none;
364
- }
365
- .wp {
366
- color: #fff;
367
- text-shadow:
368
- 0 0 2px #000, 0 0 3px #000,
369
- 1px 1px 0 #000, -1px -1px 0 #000,
370
- 1px -1px 0 #000, -1px 1px 0 #000;
371
- }
372
- .bp {
373
- color: #1c1a18;
374
- text-shadow:
375
- 0 0 2px rgba(255,255,255,0.55),
376
- 1px 1px 0 rgba(255,255,255,0.3),
377
- -1px -1px 0 rgba(255,255,255,0.3);
378
- }
379
-
380
- /* ── Navigation ──────────────────────────────────────────────────────── */
381
- .nav-row {
382
- display: flex; align-items: center; gap: 20px;
383
- margin-top: 20px;
384
- }
385
- .nav-btn {
386
- background: var(--bg-card);
387
- border: 1px solid var(--border-bright);
388
- color: var(--text-sub);
389
- width: 40px; height: 40px;
390
- border-radius: 3px;
391
- font-size: 16px; cursor: pointer;
392
- display: flex; align-items: center; justify-content: center;
393
- transition: all 0.15s;
394
- }
395
- .nav-btn:hover:not(:disabled) { border-color: var(--gold-dim); color: var(--text); background: var(--bg-elevated); }
396
- .nav-btn:disabled { opacity: 0.25; cursor: default; }
397
-
398
- #move-indicator {
399
- font-family: 'JetBrains Mono', monospace;
400
- font-size: 13px; font-weight: 500;
401
- color: var(--text-sub);
402
- min-width: 120px; text-align: center;
403
- }
404
-
405
- /* ── Move timeline ───────────────────────────────────────────────────── */
406
- #move-timeline {
407
- display: flex; gap: 3px;
408
- margin-top: 14px;
409
- max-width: calc(var(--sq) * 8 + 28px);
410
- overflow-x: auto;
411
- padding-bottom: 4px;
412
- scrollbar-width: thin;
413
- scrollbar-color: var(--border-bright) transparent;
414
- }
415
- .tl-dot {
416
- flex-shrink: 0;
417
- width: 10px; height: 10px;
418
- border-radius: 50%;
419
- cursor: pointer;
420
- transition: transform 0.15s, opacity 0.15s;
421
- opacity: 0.65;
422
- }
423
- .tl-dot:hover { opacity: 1; transform: scale(1.3); }
424
- .tl-dot.tl-active {
425
- opacity: 1; transform: scale(1.25);
426
- box-shadow: 0 0 6px currentColor;
427
- }
428
-
429
- /* ── Info box ────────────────────────────────────────────────────────── */
430
- #info-box {
431
- margin-top: 18px;
432
- width: 100%;
433
- max-width: calc(var(--sq) * 8 + 28px);
434
- background: var(--bg-card);
435
- border: 1px solid var(--border);
436
- border-radius: 4px;
437
- overflow: hidden;
438
- box-shadow: 0 8px 32px rgba(0,0,0,0.4);
439
- }
440
-
441
- .info-start {
442
- padding: 28px 32px;
443
- color: var(--text-muted);
444
- font-size: 15px; font-weight: 300;
445
- font-style: italic;
446
- }
447
-
448
- .info-header {
449
- padding: 20px 24px 18px;
450
- border-bottom: 1px solid var(--border);
451
- display: grid;
452
- grid-template-columns: auto 1fr auto;
453
- align-items: center;
454
- gap: 16px;
455
- }
456
-
457
- .cls-pill {
458
- display: inline-flex; align-items: center; gap: 7px;
459
- padding: 5px 12px 5px 9px;
460
- border-radius: 99px;
461
- border: 1px solid;
462
- white-space: nowrap;
463
- }
464
- .cls-icon {
465
- font-size: 12px;
466
- font-family: 'JetBrains Mono', monospace;
467
- }
468
- .cls-label {
469
- font-size: 11px; font-weight: 600;
470
- letter-spacing: 0.15em;
471
- font-family: 'Crimson Pro', serif;
472
- }
473
-
474
- .move-name {
475
- font-family: 'Playfair Display', serif;
476
- font-size: 20px; font-weight: 600;
477
- color: var(--text);
478
- }
479
-
480
- .ep-badge {
481
- font-family: 'JetBrains Mono', monospace;
482
- font-size: 11px;
483
- color: var(--text-muted);
484
- white-space: nowrap;
485
- }
486
-
487
- .info-comment {
488
- padding: 16px 24px;
489
- font-size: 15px; font-weight: 300;
490
- font-style: italic;
491
- color: var(--text-sub);
492
- border-bottom: 1px solid var(--border);
493
- line-height: 1.5;
494
- }
495
-
496
- .info-details { padding: 4px 0 8px; }
497
-
498
- .info-row {
499
- display: grid;
500
- grid-template-columns: 120px 1fr;
501
- align-items: baseline;
502
- gap: 12px;
503
- padding: 10px 24px;
504
- }
505
- .info-row + .info-row { border-top: 1px solid var(--border); }
506
-
507
- .info-label {
508
- font-size: 11px; font-weight: 500;
509
- letter-spacing: 0.18em; text-transform: uppercase;
510
- color: var(--text-muted);
511
- }
512
- .info-val {
513
- font-size: 15px;
514
- color: var(--text);
515
- }
516
- .info-val.mono {
517
- font-family: 'JetBrains Mono', monospace;
518
- font-size: 13px;
519
- }
520
- .cont-text {
521
- color: var(--text-sub);
522
- line-height: 1.7;
523
- word-break: break-word;
524
- }
525
-
526
- .best-row.found .info-val {
527
- color: #6aa84f;
528
- font-weight: 500;
529
- }
530
- </style>
531
- </head>
532
- <body>
533
-
534
- <!-- ═══════════════════════════════════════════════════════════════════════
535
- SCREEN 1 — PGN Input
536
- ══════════════════════════════════════════════════════════════════════════ -->
537
- <div id="s-input" class="screen active">
538
- <div style="margin-top: auto; margin-bottom: auto; width: 100%; max-width: 640px; padding: 40px 20px;">
539
- <div class="brand">
540
- <span class="brand-line"></span>
541
- <span class="brand-name">Chess Analysis</span>
542
- <span class="brand-line"></span>
543
- </div>
544
-
545
- <div class="input-card">
546
- <h1>Game Analysis</h1>
547
- <p class="subtitle">Paste your PGN below and set analysis depth</p>
548
-
549
- <label class="field-label" for="pgn">PGN Notation</label>
550
- <textarea id="pgn" placeholder="[Event &quot;Chess Match&quot;]&#10;[White &quot;Player One&quot;]&#10;[Black &quot;Player Two&quot;]&#10;&#10;1. e4 e5 2. Nf3 Nc6 3. Bb5 ..."></textarea>
551
-
552
- <div class="depth-group">
553
- <label class="field-label" for="depth">Analysis Depth</label>
554
- <div class="depth-row">
555
- <input type="range" id="depth" min="6" max="22" value="12">
556
- <span id="depth-val">12</span>
557
- </div>
558
- <span class="depth-hint">Deeper = stronger analysis, longer wait</span>
559
- </div>
560
-
561
- <div style="text-align: center;">
562
- <button id="analyze-btn" class="btn btn-gold">Analyze Game</button>
563
- </div>
564
- </div>
565
- </div>
566
- </div>
567
-
568
-
569
- <!-- ═══════════════════════════════════════════════════════════════════════
570
- SCREEN 2 — Loading
571
- ══════════════════════════════════════════════════════════════════════════ -->
572
- <div id="s-loading" class="screen">
573
- <div class="loading-inner" style="margin: auto;">
574
- <h2>Analyzing</h2>
575
- <div class="progress-track">
576
- <div id="progress-bar"></div>
577
- </div>
578
- <p id="progress-msg">Initializing engine…</p>
579
- <p id="progress-pct">0%</p>
580
- </div>
581
- </div>
582
-
583
-
584
- <!-- ═══════════════════════════════════════════════════════════════════════
585
- SCREEN 3 — Analysis
586
- ══════════════════════════════════════════════════════════════════════════ -->
587
- <div id="s-analysis" class="screen">
588
-
589
- <div class="analysis-header">
590
- <button id="back-btn" class="btn btn-ghost">← New Analysis</button>
591
- <div class="players">
592
- <span id="white-name" class="player-name white-player">White</span>
593
- <span class="vs">vs</span>
594
- <span id="black-name" class="player-name black-player">Black</span>
595
- </div>
596
- <button id="flip-btn" class="btn btn-ghost">⇅ Flip</button>
597
- </div>
598
-
599
- <div class="board-area">
600
- <div class="board-with-coords">
601
- <div class="rank-labels" id="rank-labels"></div>
602
- <div class="board-column">
603
- <div id="board"></div>
604
- <div class="file-labels" id="file-labels"></div>
605
- </div>
606
- </div>
607
- </div>
608
-
609
- <div class="nav-row">
610
- <button id="prev-btn" class="nav-btn" title="Previous move (←)">←</button>
611
- <span id="move-indicator">Start</span>
612
- <button id="next-btn" class="nav-btn" title="Next move (→)">→</button>
613
- </div>
614
-
615
- <div id="move-timeline"></div>
616
-
617
- <div id="info-box">
618
- <div class="info-start"><p>Press → to begin stepping through the game.</p></div>
619
- </div>
620
-
621
- </div>
622
-
623
- <script>
624
- // ── Constants ──────────────────────────────────────────────────────────
625
- const PIECE_MAP = {
626
- 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙',
627
- 'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟'
628
- };
629
-
630
- const CLS_META = {
631
- Brilliant: { color:'#1bada8', icon:'✦', label:'Brilliant' },
632
- Great: { color:'#4c82e8', icon:'‼', label:'Great' },
633
- Miss: { color:'#b52020', icon:'✕', label:'Miss' },
634
- Best: { color:'#5a9e44', icon:'✓', label:'Best' },
635
- Excellent: { color:'#82b83c', icon:'!', label:'Excellent' },
636
- Good: { color:'#4891b8', icon:'⬤', label:'Good' },
637
- Inaccuracy: { color:'#d4a820', icon:'△', label:'Inaccuracy' },
638
- Mistake: { color:'#d96830', icon:'?', label:'Mistake' },
639
- Blunder: { color:'#b82020', icon:'??', label:'Blunder' },
640
- };
641
-
642
- // ── State ──────────────────────────────────────────────────────────────
643
- let gameData = null;
644
- let curIdx = -1; // -1 = start, 0..n-1 = after that move
645
- let flipped = false;
646
-
647
- // ── Screen switching ───────────────────────────────────────────────────
648
- function showScreen(id) {
649
- document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
650
- document.getElementById(id).classList.add('active');
651
- }
652
-
653
- // ── FEN parser ─────────────────────────────────────────────────────────
654
- function parseFEN(fen) {
655
- const rows = fen.split(' ')[0].split('/');
656
- const board = {};
657
- for (let r = 0; r < 8; r++) {
658
- const rank = 8 - r;
659
- let file = 0;
660
- for (const ch of rows[r]) {
661
- if (ch >= '1' && ch <= '8') { file += +ch; }
662
- else { board['abcdefgh'[file] + rank] = ch; file++; }
663
- }
664
- }
665
- return board;
666
- }
667
-
668
- // ── Board renderer ─────────────────────────────────────────────────────
669
- function renderBoard(fen, fromSq, toSq, cls) {
670
- const pieces = parseFEN(fen);
671
- const boardEl = document.getElementById('board');
672
- boardEl.innerHTML = '';
673
-
674
- const rankOrder = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1];
675
- const fileOrder = flipped
676
- ? ['h','g','f','e','d','c','b','a']
677
- : ['a','b','c','d','e','f','g','h'];
678
-
679
- const meta = cls ? (CLS_META[cls] || null) : null;
680
-
681
- for (const rank of rankOrder) {
682
- for (let fi = 0; fi < 8; fi++) {
683
- const file = fileOrder[fi];
684
- const sq = file + rank;
685
- const isLight = (fi + rank) % 2 === 0;
686
-
687
- const div = document.createElement('div');
688
- div.className = `sq ${isLight ? 'light' : 'dark'}`;
689
-
690
- // From-square highlight
691
- if (sq === fromSq) {
692
- const ov = document.createElement('div');
693
- ov.className = 'overlay from-ov';
694
- div.appendChild(ov);
695
- }
696
-
697
- // To-square highlight + badge
698
- if (sq === toSq && meta) {
699
- const ov = document.createElement('div');
700
- ov.className = 'overlay to-ov';
701
- ov.style.background = meta.color;
702
- div.appendChild(ov);
703
-
704
- const badge = document.createElement('div');
705
- badge.className = 'cls-badge';
706
- badge.textContent = meta.icon;
707
- badge.style.background = meta.color;
708
- div.appendChild(badge);
709
- }
710
-
711
- // Piece
712
- const piece = pieces[sq];
713
- if (piece) {
714
- const span = document.createElement('span');
715
- span.className = `piece ${piece === piece.toUpperCase() ? 'wp' : 'bp'}`;
716
- span.textContent = PIECE_MAP[piece] || piece;
717
- div.appendChild(span);
718
- }
719
-
720
- boardEl.appendChild(div);
721
- }
722
- }
723
-
724
- updateCoords(rankOrder, fileOrder);
725
- }
726
-
727
- function updateCoords(rankOrder, fileOrder) {
728
- const rl = document.getElementById('rank-labels');
729
- rl.innerHTML = rankOrder.map(r =>
730
- `<div class="coord" style="width:auto; padding-right:0">${r}</div>`
731
- ).join('');
732
-
733
- const fl = document.getElementById('file-labels');
734
- fl.innerHTML = fileOrder.map(f =>
735
- `<div class="coord" style="height:auto">${f}</div>`
736
- ).join('');
737
- }
738
-
739
- // ── Timeline ───────────────────────────────────────────────────────────
740
- function buildTimeline() {
741
- const tl = document.getElementById('move-timeline');
742
- tl.innerHTML = '';
743
- gameData.moves.forEach((mv, i) => {
744
- const dot = document.createElement('div');
745
- dot.className = 'tl-dot';
746
- dot.style.background = (CLS_META[mv.classification] || {color:'#555'}).color;
747
- dot.title = `${mv.move_number}${mv.color==='white'?'.':'...'} ${mv.san} — ${mv.classification}`;
748
- dot.addEventListener('click', () => navigateTo(i));
749
- tl.appendChild(dot);
750
- });
751
- }
752
-
753
- function refreshTimeline() {
754
- const dots = document.querySelectorAll('.tl-dot');
755
- dots.forEach((d, i) => {
756
- d.classList.toggle('tl-active', i === curIdx);
757
- });
758
- // Scroll active dot into view
759
- if (curIdx >= 0 && dots[curIdx]) {
760
- dots[curIdx].scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
761
- }
762
- }
763
-
764
- // ── Info box ───────────────────────────────────────────────────────────
765
- function renderStartInfo() {
766
- document.getElementById('info-box').innerHTML = `
767
- <div class="info-start">
768
- <p>Press <strong>→</strong> or the right arrow key to step through the game.</p>
769
- </div>`;
770
- }
771
-
772
- function renderMoveInfo(mv) {
773
- const meta = CLS_META[mv.classification] || { color:'#888', icon:'?', label: mv.classification };
774
- const ep = mv.ep_loss > 0.001
775
- ? `−${(mv.ep_loss * 100).toFixed(1)}% EP`
776
- : 'No EP lost';
777
-
778
- const moveLabel = mv.color === 'white'
779
- ? `${mv.move_number}. ${mv.san}`
780
- : `${mv.move_number}… ${mv.san}`;
781
-
782
- const bestHtml = mv.best_move_san
783
- ? `<div class="info-row best-row">
784
- <span class="info-label">Best move</span>
785
- <span class="info-val mono">${mv.color === 'white' ? mv.move_number + '.' : mv.move_number + '…'} ${mv.best_move_san}</span>
786
- </div>`
787
- : `<div class="info-row best-row found">
788
- <span class="info-label">Best move</span>
789
- <span class="info-val found">✓ You found the best move</span>
790
- </div>`;
791
-
792
- const contHtml = mv.continuation_fmt
793
- ? `<div class="info-row cont-row">
794
- <span class="info-label">Continuation</span>
795
- <span class="info-val mono cont-text">${mv.continuation_fmt}</span>
796
- </div>`
797
- : '';
798
-
799
- document.getElementById('info-box').innerHTML = `
800
- <div class="info-header">
801
- <div class="cls-pill" style="background:${meta.color}18; border-color:${meta.color}55; color:${meta.color}">
802
- <span class="cls-icon">${meta.icon}</span>
803
- <span class="cls-label">${meta.label.toUpperCase()}</span>
804
- </div>
805
- <div class="move-name">${moveLabel}</div>
806
- <div class="ep-badge">${ep}</div>
807
- </div>
808
- <div class="info-comment">"${mv.comment}"</div>
809
- <div class="info-details">
810
- ${bestHtml}
811
- ${contHtml}
812
- </div>`;
813
- }
814
-
815
- // ── Navigation ─────────────────────────────────────────────────────────
816
- function navigateTo(idx) {
817
- if (!gameData) return;
818
- const max = gameData.moves.length - 1;
819
- curIdx = Math.max(-1, Math.min(idx, max));
820
-
821
- document.getElementById('prev-btn').disabled = curIdx === -1;
822
- document.getElementById('next-btn').disabled = curIdx === max;
823
-
824
- if (curIdx === -1) {
825
- renderBoard(gameData.initial_fen, null, null, null);
826
- document.getElementById('move-indicator').textContent = 'Start';
827
- renderStartInfo();
828
- } else {
829
- const mv = gameData.moves[curIdx];
830
- renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification);
831
- document.getElementById('move-indicator').textContent =
832
- `Move ${curIdx + 1} / ${gameData.moves.length}`;
833
- renderMoveInfo(mv);
834
- }
835
-
836
- refreshTimeline();
837
- }
838
-
839
- // ── Load completed analysis ────────────────────────────────────────────
840
- function loadAnalysis(data) {
841
- gameData = data;
842
- curIdx = -1;
843
- flipped = false;
844
-
845
- document.getElementById('white-name').textContent = data.white;
846
- document.getElementById('black-name').textContent = data.black;
847
-
848
- buildTimeline();
849
- navigateTo(-1);
850
- }
851
-
852
- // ── Progress bar ───────────────────────────────────────────────────────
853
- function setProgress(pct, msg) {
854
- document.getElementById('progress-bar').style.width = `${Math.round(pct * 100)}%`;
855
- document.getElementById('progress-msg').textContent = msg;
856
- document.getElementById('progress-pct').textContent = `${Math.round(pct * 100)}%`;
857
- }
858
-
859
- // ── Submit analysis ────────────────────────────────────────────────────
860
- async function submitAnalysis() {
861
- const pgn = document.getElementById('pgn').value.trim();
862
- const depth = parseInt(document.getElementById('depth').value, 10);
863
-
864
- if (!pgn) { alert('Please paste a PGN to analyze.'); return; }
865
-
866
- showScreen('s-loading');
867
- setProgress(0, 'Connecting to analysis engine…');
868
-
869
- let jobId;
870
- try {
871
- const res = await fetch('/api/analyze', {
872
- method: 'POST',
873
- headers: { 'Content-Type': 'application/json' },
874
- body: JSON.stringify({ pgn, depth })
875
- });
876
- const d = await res.json();
877
- if (d.error) throw new Error(d.error);
878
- jobId = d.job_id;
879
- } catch (err) {
880
- alert('Error: ' + err.message);
881
- showScreen('s-input');
882
- return;
883
- }
884
-
885
- const evtSrc = new EventSource(`/api/stream/${jobId}`);
886
-
887
- evtSrc.onmessage = (e) => {
888
- const data = JSON.parse(e.data);
889
- if (data.type === 'progress') {
890
- setProgress(data.progress, data.message);
891
- } else if (data.type === 'complete') {
892
- evtSrc.close();
893
- loadAnalysis(data.data);
894
- showScreen('s-analysis');
895
- } else if (data.type === 'error') {
896
- evtSrc.close();
897
- alert('Analysis error: ' + data.message);
898
- showScreen('s-input');
899
- }
900
- };
901
-
902
- evtSrc.onerror = () => {
903
- evtSrc.close();
904
- alert('Connection lost. Please try again.');
905
- showScreen('s-input');
906
- };
907
- }
908
-
909
- // ── Wire up controls ───────────────────────────────────────────────────
910
- document.getElementById('depth').addEventListener('input', e => {
911
- document.getElementById('depth-val').textContent = e.target.value;
912
- });
913
-
914
- document.getElementById('analyze-btn').addEventListener('click', submitAnalysis);
915
-
916
- document.getElementById('back-btn').addEventListener('click', () => {
917
- showScreen('s-input');
918
- });
919
-
920
- document.getElementById('prev-btn').addEventListener('click', () => navigateTo(curIdx - 1));
921
- document.getElementById('next-btn').addEventListener('click', () => navigateTo(curIdx + 1));
922
-
923
- document.getElementById('flip-btn').addEventListener('click', () => {
924
- flipped = !flipped;
925
- if (!gameData) return;
926
- if (curIdx === -1) {
927
- renderBoard(gameData.initial_fen, null, null, null);
928
- } else {
929
- const mv = gameData.moves[curIdx];
930
- renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification);
931
- }
932
- });
933
-
934
- document.addEventListener('keydown', (e) => {
935
- if (!gameData) return;
936
- const active = document.querySelector('.screen.active');
937
- if (!active || active.id !== 's-analysis') return;
938
- if (e.key === 'ArrowRight') { e.preventDefault(); navigateTo(curIdx + 1); }
939
- if (e.key === 'ArrowLeft') { e.preventDefault(); navigateTo(curIdx - 1); }
940
- });
941
- </script>
942
- </body>
943
- </html>