Fu01978 commited on
Commit
c832b51
·
verified ·
1 Parent(s): 71dec69

Delete templates/index.html

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