jev-chess 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/ARCHITECTURE.md +148 -0
  2. package/README.md +109 -0
  3. package/dist/chessEngine.d.ts +36 -0
  4. package/dist/chessEngine.d.ts.map +1 -0
  5. package/dist/chessEngine.js +173 -0
  6. package/dist/chessEngine.js.map +1 -0
  7. package/dist/classicMatches.d.ts +50 -0
  8. package/dist/classicMatches.d.ts.map +1 -0
  9. package/dist/classicMatches.js +223 -0
  10. package/dist/classicMatches.js.map +1 -0
  11. package/dist/demo.d.ts +2 -0
  12. package/dist/demo.d.ts.map +1 -0
  13. package/dist/demo.js +142 -0
  14. package/dist/demo.js.map +1 -0
  15. package/dist/gameReviewer.d.ts +11 -0
  16. package/dist/gameReviewer.d.ts.map +1 -0
  17. package/dist/gameReviewer.js +89 -0
  18. package/dist/gameReviewer.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/moveEvaluator.d.ts +11 -0
  24. package/dist/moveEvaluator.d.ts.map +1 -0
  25. package/dist/moveEvaluator.js +103 -0
  26. package/dist/moveEvaluator.js.map +1 -0
  27. package/dist/moveResolver.d.ts +15 -0
  28. package/dist/moveResolver.d.ts.map +1 -0
  29. package/dist/moveResolver.js +97 -0
  30. package/dist/moveResolver.js.map +1 -0
  31. package/dist/personaEngine.d.ts +15 -0
  32. package/dist/personaEngine.d.ts.map +1 -0
  33. package/dist/personaEngine.js +151 -0
  34. package/dist/personaEngine.js.map +1 -0
  35. package/dist/server.d.ts +2 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +1490 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/typeSafeClient.d.ts +13 -0
  40. package/dist/typeSafeClient.d.ts.map +1 -0
  41. package/dist/typeSafeClient.js +336 -0
  42. package/dist/typeSafeClient.js.map +1 -0
  43. package/dist/types.d.ts +100 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +5 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/web/main.d.ts +2 -0
  48. package/dist/web/main.d.ts.map +1 -0
  49. package/dist/web/main.js +620 -0
  50. package/dist/web/main.js.map +1 -0
  51. package/package.json +35 -0
  52. package/public/app.js +45 -0
  53. package/public/index.html +790 -0
  54. package/src/chessEngine.ts +191 -0
  55. package/src/classicMatches.ts +291 -0
  56. package/src/demo.ts +160 -0
  57. package/src/gameReviewer.ts +113 -0
  58. package/src/index.ts +8 -0
  59. package/src/moveEvaluator.ts +122 -0
  60. package/src/moveResolver.ts +120 -0
  61. package/src/personaEngine.ts +193 -0
  62. package/src/server.ts +1509 -0
  63. package/src/typeSafeClient.ts +321 -0
  64. package/src/types.ts +118 -0
  65. package/src/web/main.ts +615 -0
  66. package/tests/chess.test.ts +206 -0
  67. package/tsconfig.json +21 -0
package/src/server.ts ADDED
@@ -0,0 +1,1509 @@
1
+ import http from "node:http";
2
+ import { ChessEngine } from "./chessEngine.js";
3
+ import { MoveResolver } from "./moveResolver.js";
4
+ import { MoveEvaluator } from "./moveEvaluator.js";
5
+ import { PersonaEngine } from "./personaEngine.js";
6
+ import { GameReviewer } from "./gameReviewer.js";
7
+ import { setApiKey, getApiKeyStatus } from "./typeSafeClient.js";
8
+ import { ClassicMatchStudio, CLASSIC_MATCHES } from "./classicMatches.js";
9
+
10
+ const PORT = 3333;
11
+ const resolver = new MoveResolver();
12
+ const evaluator = new MoveEvaluator();
13
+ const personaEngine = new PersonaEngine();
14
+ const reviewer = new GameReviewer();
15
+ const matchStudio = new ClassicMatchStudio();
16
+
17
+ function getHtml(): string {
18
+ return `<!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>Jev Chess — TypeSafe AI System One Studio</title>
24
+ <style>
25
+ :root {
26
+ --bg: #0d1117;
27
+ --card-bg: #161b22;
28
+ --card-border: #30363d;
29
+ --text: #c9d1d9;
30
+ --text-bright: #f0f6fc;
31
+ --accent: #58a6ff;
32
+ --accent-hover: #79c0ff;
33
+ --success: #3fb950;
34
+ --warning: #d29922;
35
+ --danger: #f85149;
36
+ --board-light: #f0d9b5;
37
+ --board-dark: #b58863;
38
+ --sq-highlight: rgba(255, 255, 0, 0.45);
39
+ --sq-selected: rgba(88, 166, 255, 0.6);
40
+ --sq-target: rgba(63, 185, 80, 0.5);
41
+ }
42
+ * { box-sizing: border-box; margin: 0; padding: 0; }
43
+ body {
44
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
45
+ background: var(--bg);
46
+ color: var(--text);
47
+ min-height: 100vh;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+ header {
52
+ background: var(--card-bg);
53
+ border-bottom: 1px solid var(--card-border);
54
+ padding: 12px 24px;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ }
59
+ .brand {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 12px;
63
+ }
64
+ .brand h1 {
65
+ font-size: 1.15rem;
66
+ font-weight: 700;
67
+ color: var(--text-bright);
68
+ letter-spacing: -0.02em;
69
+ }
70
+ .status-dot {
71
+ width: 8px;
72
+ height: 8px;
73
+ border-radius: 50%;
74
+ display: inline-block;
75
+ flex-shrink: 0;
76
+ }
77
+ .status-dot.live {
78
+ background: var(--success);
79
+ box-shadow: 0 0 8px rgba(63, 185, 80, 0.7);
80
+ }
81
+ .status-dot.sim {
82
+ background: var(--warning);
83
+ box-shadow: 0 0 6px rgba(210, 153, 34, 0.5);
84
+ }
85
+ .badge-status {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: 7px;
89
+ font-size: 0.75rem;
90
+ padding: 4px 10px;
91
+ border-radius: 999px;
92
+ background: rgba(63, 185, 80, 0.12);
93
+ color: var(--success);
94
+ border: 1px solid rgba(63, 185, 80, 0.3);
95
+ font-weight: 600;
96
+ cursor: pointer;
97
+ user-select: none;
98
+ }
99
+ .badge-sim {
100
+ background: rgba(210, 153, 34, 0.12);
101
+ color: var(--warning);
102
+ border-color: rgba(210, 153, 34, 0.3);
103
+ }
104
+ main {
105
+ flex: 1;
106
+ max-width: 1400px;
107
+ width: 100%;
108
+ margin: 0 auto;
109
+ padding: 24px;
110
+ display: grid;
111
+ grid-template-columns: 500px 1fr;
112
+ gap: 28px;
113
+ }
114
+ @media (max-width: 1080px) {
115
+ main { grid-template-columns: 1fr; }
116
+ }
117
+ /* Chessboard Container */
118
+ .board-card {
119
+ background: var(--card-bg);
120
+ border: 1px solid var(--card-border);
121
+ border-radius: 12px;
122
+ padding: 20px;
123
+ display: flex;
124
+ flex-direction: column;
125
+ align-items: center;
126
+ gap: 16px;
127
+ height: fit-content;
128
+ }
129
+ .board-header {
130
+ width: 100%;
131
+ display: flex;
132
+ justify-content: space-between;
133
+ align-items: center;
134
+ font-size: 0.9rem;
135
+ }
136
+ .turn-indicator {
137
+ font-weight: 600;
138
+ color: var(--text-bright);
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 8px;
142
+ }
143
+ .turn-dot {
144
+ width: 12px;
145
+ height: 12px;
146
+ border-radius: 50%;
147
+ border: 1px solid #666;
148
+ }
149
+ .turn-dot.white { background: #fff; }
150
+ .turn-dot.black { background: #111; }
151
+ #board {
152
+ width: 460px;
153
+ height: 460px;
154
+ display: grid;
155
+ grid-template-columns: repeat(8, 1fr);
156
+ grid-template-rows: repeat(8, 1fr);
157
+ border: 2px solid #444;
158
+ border-radius: 4px;
159
+ overflow: hidden;
160
+ user-select: none;
161
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
162
+ }
163
+ .square {
164
+ position: relative;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ font-size: 36px;
169
+ cursor: pointer;
170
+ transition: background 0.1s;
171
+ }
172
+ .square.light { background: var(--board-light); color: #222; }
173
+ .square.dark { background: var(--board-dark); color: #111; }
174
+ .square.selected { background: var(--sq-selected) !important; }
175
+ .square.target::after {
176
+ content: "";
177
+ width: 14px;
178
+ height: 14px;
179
+ border-radius: 50%;
180
+ background: var(--sq-target);
181
+ pointer-events: none;
182
+ }
183
+ .square.last-from, .square.last-to { background: var(--sq-highlight) !important; }
184
+ .coord-label {
185
+ position: absolute;
186
+ font-size: 10px;
187
+ font-weight: 700;
188
+ pointer-events: none;
189
+ opacity: 0.6;
190
+ }
191
+ .coord-rank { top: 2px; left: 3px; }
192
+ .coord-file { bottom: 2px; right: 3px; }
193
+ .board-actions {
194
+ display: flex;
195
+ gap: 8px;
196
+ width: 100%;
197
+ }
198
+ .btn {
199
+ flex: 1;
200
+ background: #21262d;
201
+ border: 1px solid var(--card-border);
202
+ color: var(--text-bright);
203
+ padding: 8px 14px;
204
+ border-radius: 6px;
205
+ font-size: 0.85rem;
206
+ font-weight: 600;
207
+ cursor: pointer;
208
+ transition: all 0.15s;
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ gap: 6px;
213
+ }
214
+ .btn:hover { background: #30363d; border-color: #8b949e; }
215
+ .btn-primary {
216
+ background: #238636;
217
+ border-color: rgba(240, 246, 252, 0.1);
218
+ }
219
+ .btn-primary:hover { background: #2ea043; }
220
+ .btn-accent {
221
+ background: #1f6feb;
222
+ border-color: rgba(240, 246, 252, 0.1);
223
+ }
224
+ .btn-accent:hover { background: #388bfd; }
225
+
226
+ /* Studio Panels */
227
+ .studio {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 20px;
231
+ }
232
+ .panel {
233
+ background: var(--card-bg);
234
+ border: 1px solid var(--card-border);
235
+ border-radius: 12px;
236
+ padding: 18px 20px;
237
+ }
238
+ .panel-title {
239
+ font-size: 0.95rem;
240
+ font-weight: 700;
241
+ color: var(--text-bright);
242
+ margin-bottom: 12px;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: space-between;
246
+ }
247
+ .panel-subtitle {
248
+ font-size: 0.8rem;
249
+ color: #8b949e;
250
+ font-weight: normal;
251
+ }
252
+ /* Natural Language Input */
253
+ .nl-input-row {
254
+ display: flex;
255
+ gap: 8px;
256
+ margin-bottom: 10px;
257
+ }
258
+ .nl-input {
259
+ flex: 1;
260
+ background: #0d1117;
261
+ border: 1px solid var(--card-border);
262
+ color: var(--text-bright);
263
+ padding: 10px 14px;
264
+ border-radius: 6px;
265
+ font-size: 0.9rem;
266
+ outline: none;
267
+ }
268
+ .nl-input:focus { border-color: var(--accent); }
269
+ .quick-chips {
270
+ display: flex;
271
+ flex-wrap: wrap;
272
+ gap: 6px;
273
+ margin-bottom: 12px;
274
+ }
275
+ .chip {
276
+ background: #21262d;
277
+ border: 1px solid var(--card-border);
278
+ color: #8b949e;
279
+ font-size: 0.75rem;
280
+ padding: 4px 10px;
281
+ border-radius: 16px;
282
+ cursor: pointer;
283
+ transition: all 0.15s;
284
+ }
285
+ .chip:hover { color: var(--text-bright); border-color: var(--accent); }
286
+ .resolution-banner {
287
+ background: #0d1117;
288
+ border: 1px solid var(--card-border);
289
+ border-radius: 6px;
290
+ padding: 10px 14px;
291
+ font-size: 0.85rem;
292
+ display: flex;
293
+ justify-content: space-between;
294
+ align-items: center;
295
+ }
296
+ /* Intelligence Metrics Grid */
297
+ .metrics-grid {
298
+ display: grid;
299
+ grid-template-columns: repeat(2, 1fr);
300
+ gap: 12px;
301
+ }
302
+ .metric-card {
303
+ background: #0d1117;
304
+ border: 1px solid var(--card-border);
305
+ border-radius: 8px;
306
+ padding: 12px;
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 6px;
310
+ }
311
+ .metric-name {
312
+ font-size: 0.75rem;
313
+ color: #8b949e;
314
+ text-transform: uppercase;
315
+ letter-spacing: 0.05em;
316
+ font-weight: 600;
317
+ }
318
+ .metric-value {
319
+ font-size: 1.15rem;
320
+ font-weight: 700;
321
+ color: var(--text-bright);
322
+ display: flex;
323
+ align-items: baseline;
324
+ gap: 6px;
325
+ }
326
+ .metric-sub {
327
+ font-size: 0.75rem;
328
+ color: #8b949e;
329
+ font-weight: normal;
330
+ }
331
+ .progress-bar {
332
+ height: 6px;
333
+ background: #21262d;
334
+ border-radius: 3px;
335
+ overflow: hidden;
336
+ margin-top: 4px;
337
+ }
338
+ .progress-fill {
339
+ height: 100%;
340
+ background: var(--accent);
341
+ width: 0%;
342
+ transition: width 0.3s;
343
+ }
344
+ /* Persona Selector */
345
+ .persona-select-grid {
346
+ display: grid;
347
+ grid-template-columns: repeat(4, 1fr);
348
+ gap: 8px;
349
+ margin-bottom: 12px;
350
+ }
351
+ .persona-card {
352
+ background: #0d1117;
353
+ border: 1px solid var(--card-border);
354
+ border-radius: 8px;
355
+ padding: 10px;
356
+ cursor: pointer;
357
+ text-align: center;
358
+ transition: all 0.15s;
359
+ }
360
+ .persona-card:hover { border-color: #8b949e; }
361
+ .persona-card.active {
362
+ border-color: var(--accent);
363
+ background: rgba(88, 166, 255, 0.08);
364
+ }
365
+ .persona-name { font-size: 0.85rem; font-weight: 700; color: var(--text-bright); }
366
+ .persona-title { font-size: 0.7rem; color: #8b949e; margin-top: 2px; }
367
+ .persona-breakdown {
368
+ background: #0d1117;
369
+ border: 1px solid var(--card-border);
370
+ border-radius: 6px;
371
+ padding: 10px 14px;
372
+ font-size: 0.8rem;
373
+ color: var(--text);
374
+ line-height: 1.5;
375
+ }
376
+ .history-list {
377
+ max-height: 110px;
378
+ overflow-y: auto;
379
+ font-family: ui-monospace, monospace;
380
+ font-size: 0.85rem;
381
+ color: #8b949e;
382
+ display: flex;
383
+ flex-wrap: wrap;
384
+ gap: 8px;
385
+ padding-top: 6px;
386
+ }
387
+ .history-item { color: var(--text-bright); }
388
+
389
+ /* Classic Matches Styles */
390
+ .match-select {
391
+ width: 100%;
392
+ background: #0d1117;
393
+ border: 1px solid var(--card-border);
394
+ color: var(--text-bright);
395
+ padding: 9px 12px;
396
+ border-radius: 6px;
397
+ font-size: 0.85rem;
398
+ outline: none;
399
+ cursor: pointer;
400
+ }
401
+ .match-select:focus { border-color: var(--accent); }
402
+ .match-meta-box {
403
+ background: #0d1117;
404
+ border: 1px solid var(--card-border);
405
+ border-radius: 8px;
406
+ padding: 12px 14px;
407
+ margin-top: 10px;
408
+ font-size: 0.82rem;
409
+ line-height: 1.5;
410
+ }
411
+ .match-meta-header {
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: baseline;
415
+ margin-bottom: 4px;
416
+ }
417
+ .match-players {
418
+ font-weight: 700;
419
+ color: var(--text-bright);
420
+ font-size: 0.9rem;
421
+ }
422
+ .match-tags {
423
+ display: flex;
424
+ gap: 6px;
425
+ margin-top: 6px;
426
+ flex-wrap: wrap;
427
+ }
428
+ .match-tag {
429
+ font-size: 0.7rem;
430
+ padding: 2px 7px;
431
+ border-radius: 4px;
432
+ background: #21262d;
433
+ color: #8b949e;
434
+ }
435
+ .match-tag.result {
436
+ color: var(--accent);
437
+ background: rgba(88, 166, 255, 0.1);
438
+ }
439
+ .replay-bar {
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 8px;
443
+ margin-top: 12px;
444
+ }
445
+ .replay-btn {
446
+ background: #21262d;
447
+ border: 1px solid var(--card-border);
448
+ color: var(--text-bright);
449
+ padding: 7px 12px;
450
+ border-radius: 6px;
451
+ font-size: 0.8rem;
452
+ font-weight: 600;
453
+ cursor: pointer;
454
+ transition: all 0.15s;
455
+ }
456
+ .replay-btn:hover { background: #30363d; border-color: #8b949e; }
457
+ .replay-progress-wrap {
458
+ flex: 1;
459
+ display: flex;
460
+ flex-direction: column;
461
+ gap: 4px;
462
+ }
463
+ .replay-progress-text {
464
+ display: flex;
465
+ justify-content: space-between;
466
+ font-size: 0.75rem;
467
+ color: #8b949e;
468
+ font-family: ui-monospace, monospace;
469
+ }
470
+ .classification-panel {
471
+ background: #0d1117;
472
+ border: 1px solid var(--card-border);
473
+ border-radius: 8px;
474
+ padding: 14px;
475
+ margin-top: 12px;
476
+ display: flex;
477
+ flex-direction: column;
478
+ gap: 12px;
479
+ }
480
+ .archetype-banner {
481
+ display: inline-flex;
482
+ align-items: center;
483
+ gap: 6px;
484
+ padding: 4px 10px;
485
+ border-radius: 999px;
486
+ background: rgba(88, 166, 255, 0.12);
487
+ border: 1px solid rgba(88, 166, 255, 0.3);
488
+ color: var(--accent);
489
+ font-size: 0.75rem;
490
+ font-weight: 700;
491
+ letter-spacing: 0.05em;
492
+ align-self: flex-start;
493
+ }
494
+ .class-grid {
495
+ display: grid;
496
+ grid-template-columns: repeat(3, 1fr);
497
+ gap: 8px;
498
+ }
499
+ .class-stat {
500
+ background: #161b22;
501
+ border: 1px solid var(--card-border);
502
+ border-radius: 6px;
503
+ padding: 8px 10px;
504
+ display: flex;
505
+ flex-direction: column;
506
+ gap: 4px;
507
+ }
508
+ .class-stat-name { font-size: 0.7rem; color: #8b949e; text-transform: uppercase; font-weight: 600; }
509
+ .class-stat-val { font-size: 0.95rem; font-weight: 700; color: var(--text-bright); }
510
+ .class-stat-desc { font-size: 0.72rem; color: #8b949e; }
511
+ .breakdown-row {
512
+ display: flex;
513
+ gap: 6px;
514
+ font-size: 0.75rem;
515
+ flex-wrap: wrap;
516
+ }
517
+ .breakdown-pill {
518
+ background: #161b22;
519
+ border: 1px solid var(--card-border);
520
+ border-radius: 4px;
521
+ padding: 4px 8px;
522
+ color: var(--text);
523
+ }
524
+ </style>
525
+ </head>
526
+ <body>
527
+ <header>
528
+ <div class="brand">
529
+ <div>
530
+ <h1>Jev Chess Studio</h1>
531
+ <div style="font-size: 0.75rem; color: #8b949e;">TypeSafe AI System One Architecture</div>
532
+ </div>
533
+ </div>
534
+ <div style="display: flex; align-items: center; gap: 10px;">
535
+ <button class="btn" onclick="openKeyModal()" style="padding: 5px 12px; font-size: 0.8rem; background: #21262d; border: 1px solid var(--card-border);">
536
+ <span id="keyBtnLabel">API Key</span>
537
+ </button>
538
+ <div id="backendBadge" class="badge-status badge-sim" onclick="openKeyModal()" title="Click to manage TypeSafe API Key">
539
+ <span id="backendStatusDot" class="status-dot sim"></span>
540
+ <span id="backendStatusText">Simulation Mode (jev-latest calibrated)</span>
541
+ </div>
542
+ </div>
543
+ </header>
544
+
545
+ <!-- TypeSafe API Key Modal -->
546
+ <div id="keyModal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(5px); z-index: 999; align-items: center; justify-content: center;">
547
+ <div style="background: #161b22; border: 1px solid #30363d; border-radius: 12px; width: 480px; max-width: 92vw; padding: 24px; box-shadow: 0 20px 48px rgba(0,0,0,0.7);">
548
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
549
+ <h3 style="color: #f0f6fc; font-size: 1.15rem; font-weight: 700;">
550
+ TypeSafe API Configuration
551
+ </h3>
552
+ <button onclick="closeKeyModal()" style="background: none; border: none; color: #8b949e; font-size: 1.2rem; cursor: pointer; padding: 4px;">✕</button>
553
+ </div>
554
+
555
+ <p style="font-size: 0.85rem; color: #8b949e; margin-bottom: 16px; line-height: 1.45;">
556
+ Connect directly to TypeSafe's flagship System One model (<strong style="color: #f0f6fc;">jev-latest</strong>). When active, all natural language move parsing, evaluations, and AI personas run directly against TypeSafe's live inference endpoints.
557
+ </p>
558
+
559
+ <div style="margin-bottom: 14px;">
560
+ <label style="display: block; font-size: 0.8rem; font-weight: 600; color: #c9d1d9; margin-bottom: 6px;">
561
+ TypeSafe API Key
562
+ </label>
563
+ <div style="display: flex; gap: 6px;">
564
+ <input id="keyInput" type="password" placeholder="ts_..." style="flex: 1; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 10px; color: #f0f6fc; font-family: ui-monospace, monospace; font-size: 0.9rem; outline: none;" onkeydown="if(event.key==='Enter') saveApiKey()" />
565
+ <button class="btn" onclick="toggleKeyVisibility()" style="flex: 0 0 60px; padding: 0;" title="Toggle Visibility">Show</button>
566
+ </div>
567
+ </div>
568
+
569
+ <div id="modalStatus" style="font-size: 0.82rem; margin-bottom: 16px; min-height: 20px;"></div>
570
+
571
+ <div style="display: flex; gap: 8px; flex-direction: column;">
572
+ <button class="btn btn-accent" onclick="saveApiKey()" style="padding: 10px; font-weight: 700;">
573
+ Connect to Live API (jev-latest)
574
+ </button>
575
+ <div style="display: flex; gap: 8px;">
576
+ <button class="btn" onclick="clearApiKey()" style="flex: 1;">
577
+ Revert to Simulation Mode
578
+ </button>
579
+ <button class="btn" onclick="closeKeyModal()" style="flex: 1;">
580
+ Cancel
581
+ </button>
582
+ </div>
583
+ </div>
584
+
585
+ <div style="margin-top: 16px; text-align: center; font-size: 0.75rem; color: #8b949e;">
586
+ Obtain an API key at <a href="https://console.typesafe.ai/" target="_blank" style="color: #58a6ff; text-decoration: underline;">console.typesafe.ai</a>
587
+ </div>
588
+ </div>
589
+ </div>
590
+
591
+ <main>
592
+ <!-- Left: Chessboard -->
593
+ <div class="board-card">
594
+ <div class="board-header">
595
+ <div class="turn-indicator">
596
+ <div id="turnDot" class="turn-dot white"></div>
597
+ <span id="turnLabel">White to move</span>
598
+ </div>
599
+ <div id="materialBalance" style="color: #8b949e; font-size: 0.8rem;">Material: Equal</div>
600
+ </div>
601
+
602
+ <div id="board"></div>
603
+
604
+ <div class="board-actions">
605
+ <button class="btn" onclick="resetGame()">Reset Game</button>
606
+ <button class="btn" onclick="undoMove()">Undo</button>
607
+ <button class="btn btn-primary" onclick="makePersonaMove()">Ask Opponent Move</button>
608
+ </div>
609
+
610
+ <div style="width: 100%;">
611
+ <div style="font-size: 0.75rem; color: #8b949e; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600;">Move History</div>
612
+ <div id="historyList" class="history-list"></div>
613
+ </div>
614
+ </div>
615
+
616
+ <!-- Right: Studio Intelligence Panels -->
617
+ <div class="studio">
618
+ <!-- Natural Language Move Resolver -->
619
+ <div class="panel">
620
+ <div class="panel-title">
621
+ <span>Natural Language Move Intent</span>
622
+ <span class="panel-subtitle">Select Instead of Generate (Choice)</span>
623
+ </div>
624
+ <div class="nl-input-row">
625
+ <input id="nlInput" class="nl-input" type="text" placeholder="e.g. Develop knight towards center, Castle kingside..." onkeydown="if(event.key==='Enter') resolveAndPlayMove()" />
626
+ <button class="btn btn-accent" onclick="resolveAndPlayMove()">Resolve & Play</button>
627
+ </div>
628
+ <div class="quick-chips">
629
+ <div class="chip" onclick="setChip('Develop knight towards center and attack pawn')">Develop knight to f3</div>
630
+ <div class="chip" onclick="setChip('Push the queen pawn two squares forward')">Push d-pawn two squares</div>
631
+ <div class="chip" onclick="setChip('Castle to safety on the kingside')">Castle kingside</div>
632
+ <div class="chip" onclick="setChip('Attack with bishop on f7')">Sacrifice bishop on f7</div>
633
+ <div class="chip" onclick="setChip('Teleport rook across board')">Illegal Teleport (Test Fallback)</div>
634
+ </div>
635
+ <div id="resolutionResult" class="resolution-banner" style="display: none;">
636
+ <span id="resText">...</span>
637
+ <span id="resConf" style="font-weight: 700; color: var(--accent);"></span>
638
+ </div>
639
+ </div>
640
+
641
+ <!-- Live Move Intelligence -->
642
+ <div class="panel">
643
+ <div class="panel-title">
644
+ <span>System One Move Intelligence</span>
645
+ <span id="lastMoveBadge" style="font-size: 0.78rem; font-weight: 600; padding: 3px 10px; border-radius: 4px; background: #21262d; color: var(--text-bright); border: 1px solid var(--card-border);">Waiting for move...</span>
646
+ </div>
647
+ <div class="metrics-grid">
648
+ <div class="metric-card">
649
+ <div class="metric-name">Tactical Sharpness (Score)</div>
650
+ <div class="metric-value">
651
+ <span id="mSharp">--</span>
652
+ <span class="metric-sub">/ 3.0</span>
653
+ </div>
654
+ <div class="progress-bar"><div id="mSharpBar" class="progress-fill"></div></div>
655
+ </div>
656
+
657
+ <div class="metric-card">
658
+ <div class="metric-name">Strategic Motif (Choice)</div>
659
+ <div class="metric-value" style="font-size: 1rem;" id="mTheme">--</div>
660
+ <div class="metric-sub" id="mThemeConf">Confidence: --</div>
661
+ </div>
662
+
663
+ <div class="metric-card">
664
+ <div class="metric-name">King Threat (Noul)</div>
665
+ <div class="metric-value"><span id="mKing">--</span></div>
666
+ <div class="progress-bar"><div id="mKingBar" class="progress-fill" style="background: var(--danger);"></div></div>
667
+ </div>
668
+
669
+ <div class="metric-card">
670
+ <div class="metric-name">Psychological Pressure (Noul)</div>
671
+ <div class="metric-value"><span id="mPressure">--</span></div>
672
+ <div class="progress-bar"><div id="mPressureBar" class="progress-fill" style="background: var(--warning);"></div></div>
673
+ </div>
674
+ </div>
675
+ </div>
676
+
677
+ <!-- Persona AI Opponent -->
678
+ <div class="panel">
679
+ <div class="panel-title">
680
+ <span>Persona AI Opponent</span>
681
+ <span class="panel-subtitle">Composite Scoring: weights · dimensions</span>
682
+ </div>
683
+ <div class="persona-select-grid">
684
+ <div class="persona-card active" onclick="selectPersona('tal', this)">
685
+ <div class="persona-name">Tal</div>
686
+ <div class="persona-title">Aggression & Chaos</div>
687
+ </div>
688
+ <div class="persona-card" onclick="selectPersona('petrosian', this)">
689
+ <div class="persona-name">Petrosian</div>
690
+ <div class="persona-title">Iron Prophylaxis</div>
691
+ </div>
692
+ <div class="persona-card" onclick="selectPersona('capablanca', this)">
693
+ <div class="persona-name">Capablanca</div>
694
+ <div class="persona-title">Simplification</div>
695
+ </div>
696
+ <div class="persona-card" onclick="selectPersona('coffeehouse', this)">
697
+ <div class="persona-name">Coffeehouse</div>
698
+ <div class="persona-title">Romantic Gambits</div>
699
+ </div>
700
+ </div>
701
+ <div id="personaRationale" class="persona-breakdown">
702
+ Select an opponent archetype and click "Ask Opponent Move" to watch TypeSafe score candidate moves across atomic dimensions.
703
+ </div>
704
+ </div>
705
+
706
+ <!-- Classic Matches & Game Classification -->
707
+ <div class="panel">
708
+ <div class="panel-title">
709
+ <span>Classic Matches & Replay Studio</span>
710
+ <span class="panel-subtitle">Historic Games & System One Classification</span>
711
+ </div>
712
+
713
+ <select id="matchSelect" class="match-select" onchange="onMatchSelectChange(this.value)">
714
+ <option value="">Loading classic games...</option>
715
+ </select>
716
+
717
+ <div id="matchMetaBox" class="match-meta-box" style="display: none;">
718
+ <div class="match-meta-header">
719
+ <span id="matchPlayers" class="match-players"></span>
720
+ <span id="matchYearEvent" style="color: #8b949e; font-size: 0.75rem;"></span>
721
+ </div>
722
+ <div id="matchDesc" style="color: var(--text); margin-top: 4px;"></div>
723
+ <div class="match-tags">
724
+ <span id="matchEco" class="match-tag"></span>
725
+ <span id="matchOpening" class="match-tag"></span>
726
+ <span id="matchResult" class="match-tag result"></span>
727
+ </div>
728
+ </div>
729
+
730
+ <!-- Replay Toolbar -->
731
+ <div class="replay-bar">
732
+ <button class="replay-btn" onclick="stepFirst()" title="First Position">|&lt;</button>
733
+ <button class="replay-btn" onclick="stepPrev()" title="Previous Move">&lt;</button>
734
+ <button class="replay-btn" onclick="stepNext()" title="Next Move">&gt;</button>
735
+ <button class="replay-btn" onclick="stepLast()" title="Final Position">&gt;|</button>
736
+ <button id="autoPlayBtn" class="replay-btn" onclick="toggleAutoPlay()">Auto Play</button>
737
+ <button class="replay-btn" onclick="jumpToTurningPoint()" style="color: var(--accent);">Turning Point</button>
738
+ </div>
739
+
740
+ <div class="replay-progress-wrap" style="margin-top: 10px;">
741
+ <div class="replay-progress-text">
742
+ <span id="replayMoveLabel">Move 0 / 0</span>
743
+ <span id="replayTurnLabel">Initial Position</span>
744
+ </div>
745
+ <div class="progress-bar"><div id="replayProgressBar" class="progress-fill" style="width: 0%;"></div></div>
746
+ </div>
747
+
748
+ <!-- Classify Button -->
749
+ <div style="margin-top: 14px;">
750
+ <button id="classifyBtn" class="btn btn-accent" style="width: 100%;" onclick="classifyCurrentMatch()">
751
+ Classify Match with System One
752
+ </button>
753
+ </div>
754
+
755
+ <!-- Classification Results Card -->
756
+ <div id="classificationCard" class="classification-panel" style="display: none;">
757
+ <div style="display: flex; justify-content: space-between; align-items: center;">
758
+ <div id="classArchetype" class="archetype-banner">ARCHETYPE</div>
759
+ <span id="classSacrifice" style="font-size: 0.75rem; color: #8b949e;"></span>
760
+ </div>
761
+
762
+ <div class="class-grid">
763
+ <div class="class-stat">
764
+ <span class="class-stat-name">Brilliance</span>
765
+ <span id="classBrillianceScore" class="class-stat-val">--</span>
766
+ <span id="classBrillianceLevel" class="class-stat-desc">--</span>
767
+ </div>
768
+ <div class="class-stat">
769
+ <span class="class-stat-name">Sharpness</span>
770
+ <span id="classSharpnessScore" class="class-stat-val">--</span>
771
+ <span id="classSharpnessLevel" class="class-stat-desc">--</span>
772
+ </div>
773
+ <div class="class-stat">
774
+ <span class="class-stat-name">Turning Point</span>
775
+ <span id="classTurningMove" class="class-stat-val" style="color: var(--accent);">--</span>
776
+ <span id="classTurningNum" class="class-stat-desc">Move --</span>
777
+ </div>
778
+ </div>
779
+
780
+ <div style="font-size: 0.75rem; color: #8b949e; text-transform: uppercase; font-weight: 600; letter-spacing: 0.05em;">Strategic Profile</div>
781
+ <div class="breakdown-row">
782
+ <div class="breakdown-pill">Tactical: <strong id="bdTactical" style="color: var(--text-bright);">0%</strong></div>
783
+ <div class="breakdown-pill">Pawns: <strong id="bdPawn" style="color: var(--text-bright);">0%</strong></div>
784
+ <div class="breakdown-pill">Piece Play: <strong id="bdPiece" style="color: var(--text-bright);">0%</strong></div>
785
+ <div class="breakdown-pill">Prophylaxis: <strong id="bdProphyl" style="color: var(--text-bright);">0%</strong></div>
786
+ </div>
787
+
788
+ <div id="classVerdict" style="background: #161b22; border: 1px solid var(--card-border); border-radius: 6px; padding: 10px 12px; font-size: 0.8rem; color: var(--text); line-height: 1.5;">
789
+ </div>
790
+ </div>
791
+ </div>
792
+ </div>
793
+ </main>
794
+
795
+ <script>
796
+ const PIECE_UNICODE = {
797
+ p: '♟', n: '♞', b: '♝', r: '♜', q: '♛', k: '♚',
798
+ P: '♙', N: '♘', B: '♗', R: '♖', Q: '♕', K: '♔'
799
+ };
800
+
801
+ let selectedSquare = null;
802
+ let legalMoves = [];
803
+ let currentFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
804
+ let activePersona = "tal";
805
+ let lastFrom = null;
806
+ let lastTo = null;
807
+
808
+ // Classic match state
809
+ let classicMatches = [];
810
+ let activeMatch = null;
811
+ let matchMoveIndex = 0;
812
+ let autoPlayTimer = null;
813
+
814
+ function renderBoard(fen) {
815
+ const boardEl = document.getElementById('board');
816
+ boardEl.innerHTML = '';
817
+ const [placement, turn] = fen.split(' ');
818
+
819
+ const turnDot = document.getElementById('turnDot');
820
+ const turnLabel = document.getElementById('turnLabel');
821
+ if (turn === 'w') {
822
+ turnDot.className = 'turn-dot white';
823
+ turnLabel.innerText = 'White to move';
824
+ } else {
825
+ turnDot.className = 'turn-dot black';
826
+ turnLabel.innerText = 'Black to move';
827
+ }
828
+
829
+ const rows = placement.split('/');
830
+ for (let r = 0; r < 8; r++) {
831
+ let col = 0;
832
+ for (let ch of rows[r]) {
833
+ if (!isNaN(ch)) {
834
+ for (let empty = 0; empty < parseInt(ch); empty++) {
835
+ createSquare(r, col, null);
836
+ col++;
837
+ }
838
+ } else {
839
+ createSquare(r, col, ch);
840
+ col++;
841
+ }
842
+ }
843
+ }
844
+ }
845
+
846
+ function createSquare(r, c, piece) {
847
+ const files = ['a','b','c','d','e','f','g','h'];
848
+ const ranks = ['8','7','6','5','4','3','2','1'];
849
+ const sqName = files[c] + ranks[r];
850
+ const isLight = (r + c) % 2 === 0;
851
+
852
+ const div = document.createElement('div');
853
+ div.className = 'square ' + (isLight ? 'light' : 'dark');
854
+ div.dataset.sq = sqName;
855
+
856
+ if (sqName === selectedSquare) div.classList.add('selected');
857
+ if (sqName === lastFrom) div.classList.add('last-from');
858
+ if (sqName === lastTo) div.classList.add('last-to');
859
+
860
+ if (selectedSquare) {
861
+ const canMove = legalMoves.some(m => m.from === selectedSquare && m.to === sqName);
862
+ if (canMove) div.classList.add('target');
863
+ }
864
+
865
+ if (c === 0) {
866
+ const rankLabel = document.createElement('span');
867
+ rankLabel.className = 'coord-label coord-rank';
868
+ rankLabel.innerText = ranks[r];
869
+ div.appendChild(rankLabel);
870
+ }
871
+ if (r === 7) {
872
+ const fileLabel = document.createElement('span');
873
+ fileLabel.className = 'coord-label coord-file';
874
+ fileLabel.innerText = files[c];
875
+ div.appendChild(fileLabel);
876
+ }
877
+
878
+ if (piece) {
879
+ const span = document.createElement('span');
880
+ span.innerText = PIECE_UNICODE[piece] || piece;
881
+ div.appendChild(span);
882
+ }
883
+
884
+ div.onclick = () => onSquareClick(sqName);
885
+ document.getElementById('board').appendChild(div);
886
+ }
887
+
888
+ async function fetchLegalMoves() {
889
+ const res = await fetch('/api/legal-moves', {
890
+ method: 'POST',
891
+ headers: { 'Content-Type': 'application/json' },
892
+ body: JSON.stringify({ fen: currentFen })
893
+ });
894
+ const data = await res.json();
895
+ legalMoves = data.moves || [];
896
+ document.getElementById('materialBalance').innerText = data.material || 'Equal';
897
+ updateHistory(data.history || []);
898
+ }
899
+
900
+ function updateHistory(history) {
901
+ const el = document.getElementById('historyList');
902
+ if (!history || history.length === 0) {
903
+ el.innerHTML = '<span style="color: #666;">No moves played yet</span>';
904
+ return;
905
+ }
906
+ el.innerHTML = history.map((m, i) => '<span class="history-item">' + (i % 2 === 0 ? Math.floor(i/2 + 1) + '. ' : '') + m + '</span>').join(' ');
907
+ el.scrollTop = el.scrollHeight;
908
+ }
909
+
910
+ async function onSquareClick(sq) {
911
+ if (selectedSquare) {
912
+ const move = legalMoves.find(m => m.from === selectedSquare && m.to === sq);
913
+ if (move) {
914
+ await playMove(move.san);
915
+ selectedSquare = null;
916
+ return;
917
+ }
918
+ }
919
+ const hasMoves = legalMoves.some(m => m.from === sq);
920
+ selectedSquare = hasMoves ? sq : null;
921
+ renderBoard(currentFen);
922
+ }
923
+
924
+ async function playMove(san) {
925
+ const res = await fetch('/api/make-move', {
926
+ method: 'POST',
927
+ headers: { 'Content-Type': 'application/json' },
928
+ body: JSON.stringify({ fen: currentFen, san })
929
+ });
930
+ const data = await res.json();
931
+ if (data.success) {
932
+ lastFrom = data.move.from;
933
+ lastTo = data.move.to;
934
+ currentFen = data.fen;
935
+ renderBoard(currentFen);
936
+ await fetchLegalMoves();
937
+ updateIntelligence(data.evaluation);
938
+ }
939
+ }
940
+
941
+ function formatTheme(theme) {
942
+ if (!theme) return '--';
943
+ return theme.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
944
+ }
945
+
946
+ function updateIntelligence(ev) {
947
+ if (!ev) return;
948
+ document.getElementById('lastMoveBadge').innerText = ev.commentaryBadge || 'Move Evaluated';
949
+ document.getElementById('mSharp').innerText = ev.tacticalSharpness.score.toFixed(1);
950
+ document.getElementById('mSharpBar').style.width = Math.min(100, (ev.tacticalSharpness.score / 3.0) * 100) + '%';
951
+ document.getElementById('mTheme').innerText = formatTheme(ev.strategicTheme.theme);
952
+ document.getElementById('mThemeConf').innerText = 'Confidence: ' + (ev.strategicTheme.confidence * 100).toFixed(0) + '%';
953
+ document.getElementById('mKing').innerText = (ev.kingAttackRisk.probability * 100).toFixed(0) + '%';
954
+ document.getElementById('mKingBar').style.width = (ev.kingAttackRisk.probability * 100) + '%';
955
+ document.getElementById('mPressure').innerText = (ev.psychologicalPressure.probability * 100).toFixed(0) + '%';
956
+ document.getElementById('mPressureBar').style.width = (ev.psychologicalPressure.probability * 100) + '%';
957
+ }
958
+
959
+ function resetIntelligence() {
960
+ document.getElementById('lastMoveBadge').innerText = 'Waiting for move...';
961
+ document.getElementById('mSharp').innerText = '--';
962
+ document.getElementById('mSharpBar').style.width = '0%';
963
+ document.getElementById('mTheme').innerText = '--';
964
+ document.getElementById('mThemeConf').innerText = 'Confidence: --';
965
+ document.getElementById('mKing').innerText = '--';
966
+ document.getElementById('mKingBar').style.width = '0%';
967
+ document.getElementById('mPressure').innerText = '--';
968
+ document.getElementById('mPressureBar').style.width = '0%';
969
+ }
970
+
971
+ async function resolveAndPlayMove() {
972
+ const input = document.getElementById('nlInput');
973
+ const q = input.value.trim();
974
+ if (!q) return;
975
+
976
+ const banner = document.getElementById('resolutionResult');
977
+ const resText = document.getElementById('resText');
978
+ const resConf = document.getElementById('resConf');
979
+ banner.style.display = 'flex';
980
+ resText.innerText = 'Resolving intent via TypeSafe Choice...';
981
+ resConf.innerText = '';
982
+
983
+ const res = await fetch('/api/resolve-move', {
984
+ method: 'POST',
985
+ headers: { 'Content-Type': 'application/json' },
986
+ body: JSON.stringify({ query: q, fen: currentFen })
987
+ });
988
+ const data = await res.json();
989
+
990
+ if (data.matchedMove) {
991
+ resText.innerText = 'Matched: ' + data.matchedMove.san + ' (' + data.matchedMove.description + ')';
992
+ resConf.innerText = (data.confidence * 100).toFixed(1) + '% confidence';
993
+ input.value = '';
994
+ await playMove(data.matchedMove.san);
995
+ } else {
996
+ resText.innerText = 'Ambiguous: ' + (data.alternativeCandidates?.join(', ') || 'No legal match');
997
+ resConf.innerText = 'Low confidence (' + (data.confidence * 100).toFixed(1) + '%)';
998
+ }
999
+ }
1000
+
1001
+ function setChip(txt) {
1002
+ document.getElementById('nlInput').value = txt;
1003
+ resolveAndPlayMove();
1004
+ }
1005
+
1006
+ function selectPersona(id, el) {
1007
+ activePersona = id;
1008
+ document.querySelectorAll('.persona-card').forEach(c => c.classList.remove('active'));
1009
+ el.classList.add('active');
1010
+ }
1011
+
1012
+ async function makePersonaMove() {
1013
+ const rationaleEl = document.getElementById('personaRationale');
1014
+ rationaleEl.innerText = 'Evaluating candidates with TypeSafe Composite Scoring...';
1015
+
1016
+ const res = await fetch('/api/persona-move', {
1017
+ method: 'POST',
1018
+ headers: { 'Content-Type': 'application/json' },
1019
+ body: JSON.stringify({ personaId: activePersona, fen: currentFen })
1020
+ });
1021
+ const data = await res.json();
1022
+ if (data.success) {
1023
+ rationaleEl.innerText = data.decision.rationale;
1024
+ await playMove(data.decision.selectedMove.san);
1025
+ } else {
1026
+ rationaleEl.innerText = data.error || 'Unable to make persona move.';
1027
+ }
1028
+ }
1029
+
1030
+ async function resetGame() {
1031
+ stopAutoPlay();
1032
+ currentFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
1033
+ lastFrom = null;
1034
+ lastTo = null;
1035
+ selectedSquare = null;
1036
+ document.getElementById('resolutionResult').style.display = 'none';
1037
+ renderBoard(currentFen);
1038
+ await fetchLegalMoves();
1039
+ resetIntelligence();
1040
+ }
1041
+
1042
+ async function undoMove() {
1043
+ stopAutoPlay();
1044
+ const res = await fetch('/api/undo-move', {
1045
+ method: 'POST',
1046
+ headers: { 'Content-Type': 'application/json' },
1047
+ body: JSON.stringify({ fen: currentFen })
1048
+ });
1049
+ const data = await res.json();
1050
+ if (data.success) {
1051
+ currentFen = data.fen;
1052
+ renderBoard(currentFen);
1053
+ await fetchLegalMoves();
1054
+ }
1055
+ }
1056
+
1057
+ /* Classic Matches Functions */
1058
+ async function initClassicMatches() {
1059
+ try {
1060
+ const res = await fetch('/api/classic-matches');
1061
+ classicMatches = await res.json();
1062
+ const select = document.getElementById('matchSelect');
1063
+ select.innerHTML = classicMatches.map(m =>
1064
+ '<option value="' + m.id + '">' + m.title + ' (' + m.year + ') — ' + m.white + ' vs ' + m.black + '</option>'
1065
+ ).join('');
1066
+
1067
+ if (classicMatches.length > 0) {
1068
+ selectMatch(classicMatches[0].id);
1069
+ }
1070
+ } catch (e) {
1071
+ console.error('Failed to load classic matches', e);
1072
+ }
1073
+ }
1074
+
1075
+ function onMatchSelectChange(matchId) {
1076
+ if (matchId) selectMatch(matchId);
1077
+ }
1078
+
1079
+ async function selectMatch(matchId) {
1080
+ stopAutoPlay();
1081
+ activeMatch = classicMatches.find(m => m.id === matchId);
1082
+ if (!activeMatch) return;
1083
+
1084
+ // Update meta UI
1085
+ const metaBox = document.getElementById('matchMetaBox');
1086
+ metaBox.style.display = 'block';
1087
+ document.getElementById('matchPlayers').innerText = activeMatch.white + ' vs ' + activeMatch.black;
1088
+ document.getElementById('matchYearEvent').innerText = activeMatch.year + ' · ' + activeMatch.event;
1089
+ document.getElementById('matchDesc').innerText = activeMatch.description;
1090
+ document.getElementById('matchEco').innerText = activeMatch.eco;
1091
+ document.getElementById('matchOpening').innerText = activeMatch.opening;
1092
+ document.getElementById('matchResult').innerText = activeMatch.result;
1093
+
1094
+ // Reset replay state to move 0
1095
+ await goToReplayMove(0);
1096
+ document.getElementById('classificationCard').style.display = 'none';
1097
+ }
1098
+
1099
+ async function goToReplayMove(index) {
1100
+ if (!activeMatch) return;
1101
+ const target = Math.max(0, Math.min(activeMatch.moves.length, index));
1102
+ matchMoveIndex = target;
1103
+
1104
+ // Update progress indicators
1105
+ document.getElementById('replayMoveLabel').innerText = 'Move ' + matchMoveIndex + ' / ' + activeMatch.moves.length;
1106
+ const pct = (matchMoveIndex / Math.max(1, activeMatch.moves.length)) * 100;
1107
+ document.getElementById('replayProgressBar').style.width = pct + '%';
1108
+
1109
+ if (matchMoveIndex === 0) {
1110
+ document.getElementById('replayTurnLabel').innerText = 'Starting Position';
1111
+ } else {
1112
+ const lastSan = activeMatch.moves[matchMoveIndex - 1];
1113
+ const moveNum = Math.floor((matchMoveIndex - 1) / 2) + 1;
1114
+ const color = (matchMoveIndex - 1) % 2 === 0 ? 'White' : 'Black';
1115
+ document.getElementById('replayTurnLabel').innerText = moveNum + (color === 'White' ? '. ' : '... ') + lastSan;
1116
+ }
1117
+
1118
+ const res = await fetch('/api/replay-to-move', {
1119
+ method: 'POST',
1120
+ headers: { 'Content-Type': 'application/json' },
1121
+ body: JSON.stringify({ matchId: activeMatch.id, moveIndex: matchMoveIndex })
1122
+ });
1123
+ const data = await res.json();
1124
+
1125
+ currentFen = data.fen;
1126
+ if (data.move) {
1127
+ lastFrom = data.move.from;
1128
+ lastTo = data.move.to;
1129
+ } else {
1130
+ lastFrom = null;
1131
+ lastTo = null;
1132
+ }
1133
+
1134
+ legalMoves = data.moves || [];
1135
+ document.getElementById('materialBalance').innerText = data.material || 'Equal';
1136
+ updateHistory(data.history || []);
1137
+ renderBoard(currentFen);
1138
+
1139
+ if (data.evaluation) {
1140
+ updateIntelligence(data.evaluation);
1141
+ } else {
1142
+ resetIntelligence();
1143
+ }
1144
+ }
1145
+
1146
+ function stepFirst() {
1147
+ stopAutoPlay();
1148
+ goToReplayMove(0);
1149
+ }
1150
+
1151
+ function stepPrev() {
1152
+ stopAutoPlay();
1153
+ if (matchMoveIndex > 0) goToReplayMove(matchMoveIndex - 1);
1154
+ }
1155
+
1156
+ function stepNext() {
1157
+ if (activeMatch && matchMoveIndex < activeMatch.moves.length) {
1158
+ goToReplayMove(matchMoveIndex + 1);
1159
+ }
1160
+ }
1161
+
1162
+ function stepLast() {
1163
+ stopAutoPlay();
1164
+ if (activeMatch) goToReplayMove(activeMatch.moves.length);
1165
+ }
1166
+
1167
+ function jumpToTurningPoint() {
1168
+ stopAutoPlay();
1169
+ if (activeMatch) goToReplayMove(activeMatch.keyMoveIndex);
1170
+ }
1171
+
1172
+ function toggleAutoPlay() {
1173
+ if (autoPlayTimer) {
1174
+ stopAutoPlay();
1175
+ } else {
1176
+ if (!activeMatch) return;
1177
+ if (matchMoveIndex >= activeMatch.moves.length) {
1178
+ goToReplayMove(0);
1179
+ }
1180
+ document.getElementById('autoPlayBtn').innerText = 'Pause';
1181
+ autoPlayTimer = setInterval(() => {
1182
+ if (activeMatch && matchMoveIndex < activeMatch.moves.length) {
1183
+ stepNext();
1184
+ } else {
1185
+ stopAutoPlay();
1186
+ }
1187
+ }, 1200);
1188
+ }
1189
+ }
1190
+
1191
+ function stopAutoPlay() {
1192
+ if (autoPlayTimer) {
1193
+ clearInterval(autoPlayTimer);
1194
+ autoPlayTimer = null;
1195
+ const btn = document.getElementById('autoPlayBtn');
1196
+ if (btn) btn.innerText = 'Auto Play';
1197
+ }
1198
+ }
1199
+
1200
+ async function classifyCurrentMatch() {
1201
+ if (!activeMatch) return;
1202
+ const btn = document.getElementById('classifyBtn');
1203
+ btn.innerText = 'Classifying with System One...';
1204
+ btn.disabled = true;
1205
+
1206
+ try {
1207
+ const res = await fetch('/api/classify-match', {
1208
+ method: 'POST',
1209
+ headers: { 'Content-Type': 'application/json' },
1210
+ body: JSON.stringify({ matchId: activeMatch.id })
1211
+ });
1212
+ const cl = await res.json();
1213
+
1214
+ document.getElementById('classificationCard').style.display = 'flex';
1215
+ document.getElementById('classArchetype').innerText = cl.archetype;
1216
+ document.getElementById('classSacrifice').innerText = cl.hasDecisiveSacrifice ? 'Decisive Sacrifice Confirmed' : 'No Decisive Sacrifice';
1217
+ document.getElementById('classBrillianceScore').innerText = cl.aestheticBrilliance.score.toFixed(1) + ' / 3.0';
1218
+ document.getElementById('classBrillianceLevel').innerText = cl.aestheticBrilliance.level;
1219
+ document.getElementById('classSharpnessScore').innerText = cl.overallSharpness.score.toFixed(1) + ' / 3.0';
1220
+ document.getElementById('classSharpnessLevel').innerText = cl.overallSharpness.level;
1221
+ document.getElementById('classTurningMove').innerText = cl.turningPoint.san;
1222
+ document.getElementById('classTurningNum').innerText = 'Move ' + cl.turningPoint.moveNumber;
1223
+
1224
+ document.getElementById('bdTactical').innerText = cl.strategicBreakdown.tacticalStrikesPct + '%';
1225
+ document.getElementById('bdPawn').innerText = cl.strategicBreakdown.pawnBreaksPct + '%';
1226
+ document.getElementById('bdPiece').innerText = cl.strategicBreakdown.pieceActivationPct + '%';
1227
+ document.getElementById('bdProphyl').innerText = cl.strategicBreakdown.prophylaxisPct + '%';
1228
+
1229
+ document.getElementById('classVerdict').innerText = cl.verdict;
1230
+ } catch (e) {
1231
+ console.error('Failed to classify match', e);
1232
+ } finally {
1233
+ btn.innerText = 'Classify Match with System One';
1234
+ btn.disabled = false;
1235
+ }
1236
+ }
1237
+
1238
+ /* API Key Configuration Functions */
1239
+ async function checkKeyStatus() {
1240
+ try {
1241
+ const res = await fetch('/api/key-status');
1242
+ const data = await res.json();
1243
+ const badge = document.getElementById('backendBadge');
1244
+ const dot = document.getElementById('backendStatusDot');
1245
+ const text = document.getElementById('backendStatusText');
1246
+ const btnLabel = document.getElementById('keyBtnLabel');
1247
+
1248
+ if (data.isLive) {
1249
+ badge.className = 'badge-status';
1250
+ dot.className = 'status-dot live';
1251
+ text.innerText = 'Live API (jev-latest)';
1252
+ btnLabel.innerText = data.maskedKey || 'Live Key';
1253
+ } else {
1254
+ badge.className = 'badge-status badge-sim';
1255
+ dot.className = 'status-dot sim';
1256
+ text.innerText = 'Simulation Mode (jev-latest calibrated)';
1257
+ btnLabel.innerText = 'API Key';
1258
+ }
1259
+ } catch (e) {
1260
+ console.error('Failed to check key status', e);
1261
+ }
1262
+ }
1263
+
1264
+ function openKeyModal() {
1265
+ document.getElementById('keyModal').style.display = 'flex';
1266
+ document.getElementById('modalStatus').innerText = '';
1267
+ }
1268
+
1269
+ function closeKeyModal() {
1270
+ document.getElementById('keyModal').style.display = 'none';
1271
+ }
1272
+
1273
+ function toggleKeyVisibility() {
1274
+ const input = document.getElementById('keyInput');
1275
+ const isPass = input.type === 'password';
1276
+ input.type = isPass ? 'text' : 'password';
1277
+ event.target.innerText = isPass ? 'Hide' : 'Show';
1278
+ }
1279
+
1280
+ async function saveApiKey() {
1281
+ const input = document.getElementById('keyInput');
1282
+ const key = input.value.trim();
1283
+ const statusEl = document.getElementById('modalStatus');
1284
+ if (!key) {
1285
+ statusEl.innerHTML = '<span style="color: var(--danger)">Please enter a valid API key.</span>';
1286
+ return;
1287
+ }
1288
+ statusEl.innerHTML = '<span style="color: var(--accent)">Connecting to TypeSafe API...</span>';
1289
+
1290
+ const res = await fetch('/api/set-key', {
1291
+ method: 'POST',
1292
+ headers: { 'Content-Type': 'application/json' },
1293
+ body: JSON.stringify({ apiKey: key })
1294
+ });
1295
+ const data = await res.json();
1296
+ if (data.success) {
1297
+ statusEl.innerHTML = '<span style="color: var(--success)">Successfully connected to live jev-latest model.</span>';
1298
+ input.value = '';
1299
+ await checkKeyStatus();
1300
+ setTimeout(closeKeyModal, 1200);
1301
+ } else {
1302
+ statusEl.innerHTML = '<span style="color: var(--danger)">' + (data.error || 'Failed to set key') + '</span>';
1303
+ }
1304
+ }
1305
+
1306
+ async function clearApiKey() {
1307
+ const res = await fetch('/api/set-key', {
1308
+ method: 'POST',
1309
+ headers: { 'Content-Type': 'application/json' },
1310
+ body: JSON.stringify({ apiKey: '' })
1311
+ });
1312
+ await res.json();
1313
+ document.getElementById('modalStatus').innerHTML = '<span style="color: var(--warning)">Switched back to calibrated simulation mode.</span>';
1314
+ await checkKeyStatus();
1315
+ setTimeout(closeKeyModal, 900);
1316
+ }
1317
+
1318
+ // Initial setup
1319
+ renderBoard(currentFen);
1320
+ fetchLegalMoves();
1321
+ checkKeyStatus();
1322
+ initClassicMatches();
1323
+ </script>
1324
+ </body>
1325
+ </html>`;
1326
+ }
1327
+
1328
+ // Global active engine instance for the session
1329
+ let activeEngine = new ChessEngine();
1330
+
1331
+ const server = http.createServer(async (req, res) => {
1332
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1333
+
1334
+ // CORS headers
1335
+ res.setHeader("Access-Control-Allow-Origin", "*");
1336
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1337
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1338
+
1339
+ if (req.method === "OPTIONS") {
1340
+ res.writeHead(204);
1341
+ res.end();
1342
+ return;
1343
+ }
1344
+
1345
+ if (req.method === "GET" && url.pathname === "/api/key-status") {
1346
+ res.writeHead(200, { "Content-Type": "application/json" });
1347
+ res.end(JSON.stringify(getApiKeyStatus()));
1348
+ return;
1349
+ }
1350
+
1351
+ if (req.method === "GET" && url.pathname === "/api/classic-matches") {
1352
+ res.writeHead(200, { "Content-Type": "application/json" });
1353
+ res.end(JSON.stringify(CLASSIC_MATCHES));
1354
+ return;
1355
+ }
1356
+
1357
+ if ((req.method === "GET" || req.method === "HEAD") && url.pathname === "/") {
1358
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1359
+ if (req.method === "HEAD") {
1360
+ res.end();
1361
+ } else {
1362
+ res.end(getHtml());
1363
+ }
1364
+ return;
1365
+ }
1366
+
1367
+ // Helper to parse JSON body
1368
+ async function readBody(): Promise<any> {
1369
+ return new Promise((resolve, reject) => {
1370
+ let body = "";
1371
+ req.on("data", (chunk) => (body += chunk));
1372
+ req.on("end", () => {
1373
+ try {
1374
+ resolve(body ? JSON.parse(body) : {});
1375
+ } catch (e) {
1376
+ reject(e);
1377
+ }
1378
+ });
1379
+ });
1380
+ }
1381
+
1382
+ if (req.method === "POST") {
1383
+ try {
1384
+ const body = await readBody();
1385
+
1386
+ if (url.pathname === "/api/set-key") {
1387
+ setApiKey(body.apiKey);
1388
+ const status = getApiKeyStatus();
1389
+ res.writeHead(200, { "Content-Type": "application/json" });
1390
+ res.end(JSON.stringify({ success: true, ...status }));
1391
+ return;
1392
+ }
1393
+
1394
+ if (url.pathname === "/api/classify-match") {
1395
+ const match = CLASSIC_MATCHES.find((m) => m.id === body.matchId);
1396
+ if (!match) {
1397
+ res.writeHead(404, { "Content-Type": "application/json" });
1398
+ res.end(JSON.stringify({ error: "Match not found" }));
1399
+ return;
1400
+ }
1401
+ const classification = await matchStudio.classifyMatch(match);
1402
+ res.writeHead(200, { "Content-Type": "application/json" });
1403
+ res.end(JSON.stringify(classification));
1404
+ return;
1405
+ }
1406
+
1407
+ if (url.pathname === "/api/replay-to-move") {
1408
+ const match = CLASSIC_MATCHES.find((m) => m.id === body.matchId);
1409
+ if (!match) {
1410
+ res.writeHead(404, { "Content-Type": "application/json" });
1411
+ res.end(JSON.stringify({ error: "Match not found" }));
1412
+ return;
1413
+ }
1414
+ activeEngine.reset();
1415
+ let lastEvaluatedMove: any = null;
1416
+ const target = Math.max(0, Math.min(match.moves.length, Number(body.moveIndex) || 0));
1417
+ for (let i = 0; i < target; i++) {
1418
+ const mv = activeEngine.makeMove(match.moves[i]!);
1419
+ if (i === target - 1) {
1420
+ lastEvaluatedMove = mv;
1421
+ }
1422
+ }
1423
+ let evaluation = null;
1424
+ if (lastEvaluatedMove) {
1425
+ evaluation = await evaluator.evaluateMove(activeEngine, lastEvaluatedMove);
1426
+ }
1427
+ const moves = activeEngine.getAnnotatedMoves();
1428
+ const mat = activeEngine.getMaterialBalance();
1429
+ res.writeHead(200, { "Content-Type": "application/json" });
1430
+ res.end(
1431
+ JSON.stringify({
1432
+ fen: activeEngine.fen(),
1433
+ move: lastEvaluatedMove,
1434
+ evaluation,
1435
+ material: mat.description,
1436
+ history: activeEngine.history(),
1437
+ moves,
1438
+ })
1439
+ );
1440
+ return;
1441
+ }
1442
+
1443
+ if (url.pathname === "/api/legal-moves") {
1444
+ if (body.fen && body.fen !== activeEngine.fen()) {
1445
+ activeEngine.load(body.fen);
1446
+ }
1447
+ const moves = activeEngine.getAnnotatedMoves();
1448
+ const mat = activeEngine.getMaterialBalance();
1449
+ res.writeHead(200, { "Content-Type": "application/json" });
1450
+ res.end(JSON.stringify({ moves, material: mat.description, history: activeEngine.history() }));
1451
+ return;
1452
+ }
1453
+
1454
+ if (url.pathname === "/api/make-move") {
1455
+ if (body.fen && body.fen !== activeEngine.fen()) {
1456
+ activeEngine.load(body.fen);
1457
+ }
1458
+ const move = activeEngine.makeMove(body.san);
1459
+ if (!move) {
1460
+ res.writeHead(400, { "Content-Type": "application/json" });
1461
+ res.end(JSON.stringify({ success: false, error: "Illegal move" }));
1462
+ return;
1463
+ }
1464
+ const evaluation = await evaluator.evaluateMove(activeEngine, move);
1465
+ res.writeHead(200, { "Content-Type": "application/json" });
1466
+ res.end(JSON.stringify({ success: true, fen: activeEngine.fen(), move, evaluation }));
1467
+ return;
1468
+ }
1469
+
1470
+ if (url.pathname === "/api/resolve-move") {
1471
+ if (body.fen && body.fen !== activeEngine.fen()) {
1472
+ activeEngine.load(body.fen);
1473
+ }
1474
+ const result = await resolver.resolveIntent(activeEngine, body.query);
1475
+ res.writeHead(200, { "Content-Type": "application/json" });
1476
+ res.end(JSON.stringify(result));
1477
+ return;
1478
+ }
1479
+
1480
+ if (url.pathname === "/api/persona-move") {
1481
+ if (body.fen && body.fen !== activeEngine.fen()) {
1482
+ activeEngine.load(body.fen);
1483
+ }
1484
+ const decision = await personaEngine.selectMove(activeEngine, body.personaId || "tal");
1485
+ res.writeHead(200, { "Content-Type": "application/json" });
1486
+ res.end(JSON.stringify({ success: true, decision }));
1487
+ return;
1488
+ }
1489
+
1490
+ if (url.pathname === "/api/undo-move") {
1491
+ activeEngine.chess.undo();
1492
+ res.writeHead(200, { "Content-Type": "application/json" });
1493
+ res.end(JSON.stringify({ success: true, fen: activeEngine.fen() }));
1494
+ return;
1495
+ }
1496
+ } catch (err: any) {
1497
+ res.writeHead(500, { "Content-Type": "application/json" });
1498
+ res.end(JSON.stringify({ error: err?.message || "Internal server error" }));
1499
+ return;
1500
+ }
1501
+ }
1502
+
1503
+ res.writeHead(404, { "Content-Type": "text/plain" });
1504
+ res.end("Not Found");
1505
+ });
1506
+
1507
+ server.listen(PORT, () => {
1508
+ console.log(`Jev Chess Web Studio running live at http://localhost:${PORT}`);
1509
+ });