Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chess Analysis</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <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"> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #07070e; | |
| --bg-card: #0f0f1d; | |
| --bg-elevated: #16162a; | |
| --bg-deepest: #050509; | |
| --gold: #c9a84c; | |
| --gold-light: #e8c97a; | |
| --gold-dim: #7a6430; | |
| --gold-glow: rgba(201,168,76,0.15); | |
| --text: #ede8dc; | |
| --text-sub: #9a9282; | |
| --text-muted: #5a5650; | |
| --border: #1e1e36; | |
| --border-bright:#2e2e4e; | |
| /* eval-bar 12px + gap 6px + rank-labels 20px + gap 8px + body-padding 20px = 66px */ | |
| --sq: min(66px, calc((100vw - 80px) / 8)); | |
| } | |
| html, body { | |
| height: 100%; | |
| font-family: 'Crimson Pro', Georgia, serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| overflow: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; z-index: 0; | |
| background: | |
| radial-gradient(ellipse 80% 60% at 15% 50%, rgba(20,18,60,0.5) 0%, transparent 70%), | |
| radial-gradient(ellipse 60% 80% at 85% 10%, rgba(10,25,60,0.4) 0%, transparent 60%); | |
| pointer-events: none; | |
| } | |
| /* ── Screen system ───────────────────────────────────────────────────── */ | |
| .screen { | |
| position: fixed; inset: 0; z-index: 1; | |
| display: flex; flex-direction: column; align-items: center; | |
| overflow-y: auto; | |
| -webkit-overflow-scrolling: touch; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.35s ease; | |
| } | |
| .screen.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| /* ── Shared ──────────────────────────────────────────────────────────── */ | |
| .mono { font-family: 'JetBrains Mono', 'Consolas', monospace; } | |
| .btn { | |
| display: inline-flex; align-items: center; gap: 8px; | |
| border: none; cursor: pointer; | |
| font-family: 'Crimson Pro', serif; | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| transition: all 0.2s; | |
| } | |
| .btn:disabled { opacity: 0.3; cursor: default; } | |
| .btn-gold { | |
| background: linear-gradient(135deg, #b8922a 0%, #c9a84c 40%, #e8c97a 100%); | |
| color: #07070e; | |
| font-size: 17px; | |
| padding: 13px 40px; | |
| border-radius: 3px; | |
| box-shadow: 0 4px 20px rgba(201,168,76,0.25), inset 0 1px 0 rgba(255,255,255,0.15); | |
| } | |
| .btn-gold:hover { | |
| background: linear-gradient(135deg, #c9a84c 0%, #e8c97a 60%, #f5dca0 100%); | |
| box-shadow: 0 6px 30px rgba(201,168,76,0.4), inset 0 1px 0 rgba(255,255,255,0.2); | |
| transform: translateY(-1px); | |
| } | |
| .btn-ghost { | |
| background: transparent; | |
| color: var(--text-sub); | |
| font-size: 14px; | |
| padding: 7px 14px; | |
| border: 1px solid var(--border-bright); | |
| border-radius: 3px; | |
| } | |
| .btn-ghost:hover { color: var(--text); border-color: var(--gold-dim); background: rgba(201,168,76,0.05); } | |
| /* ── Screen 1: Input ─────────────────────────────────────────────────── */ | |
| #s-input { | |
| justify-content: center; | |
| padding: 40px 20px; | |
| } | |
| .brand { | |
| text-align: center; | |
| margin-bottom: 36px; | |
| } | |
| .brand-name { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 13px; | |
| font-weight: 400; | |
| letter-spacing: 0.35em; | |
| text-transform: uppercase; | |
| color: var(--gold); | |
| } | |
| .brand-line { | |
| display: inline-block; | |
| width: 40px; height: 1px; | |
| background: var(--gold-dim); | |
| vertical-align: middle; | |
| margin: 0 12px; | |
| opacity: 0.5; | |
| } | |
| .input-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 48px 52px; | |
| width: 100%; max-width: 640px; | |
| box-shadow: | |
| 0 40px 100px rgba(0,0,0,0.6), | |
| 0 0 0 1px rgba(201,168,76,0.06), | |
| inset 0 1px 0 rgba(255,255,255,0.03); | |
| } | |
| @media (max-width: 520px) { | |
| .input-card { padding: 28px 22px; } | |
| .input-card h1 { font-size: 24px; } | |
| #pgn { height: 160px; font-size: 12px; } | |
| } | |
| .input-card h1 { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 32px; font-weight: 600; | |
| color: var(--text); | |
| margin-bottom: 6px; | |
| } | |
| .input-card .subtitle { | |
| font-size: 16px; font-weight: 300; | |
| color: var(--text-sub); | |
| margin-bottom: 32px; | |
| } | |
| .field-label { | |
| display: block; | |
| font-size: 11px; font-weight: 500; | |
| letter-spacing: 0.2em; text-transform: uppercase; | |
| color: var(--gold-dim); | |
| margin-bottom: 8px; | |
| } | |
| #pgn { | |
| width: 100%; height: 200px; | |
| background: var(--bg-deepest); | |
| border: 1px solid var(--border); | |
| border-radius: 3px; | |
| color: var(--text); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| padding: 16px; | |
| resize: vertical; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| margin-bottom: 28px; | |
| } | |
| #pgn::placeholder { color: var(--text-muted); } | |
| #pgn:focus { border-color: var(--gold-dim); } | |
| /* ── Speed picker ────────────────────────────────────────────────────── */ | |
| .speed-group { margin-bottom: 10px; } | |
| .speed-btns { | |
| display: flex; gap: 8px; | |
| } | |
| .speed-btn { | |
| flex: 1; | |
| background: var(--bg-deepest); | |
| border: 1px solid var(--border-bright); | |
| border-radius: 3px; | |
| color: var(--text-sub); | |
| font-family: 'Crimson Pro', serif; | |
| font-size: 15px; font-weight: 500; | |
| padding: 10px 0; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| letter-spacing: 0.04em; | |
| } | |
| .speed-btn:hover { border-color: var(--gold-dim); color: var(--text); } | |
| .speed-btn.active { | |
| background: rgba(201,168,76,0.1); | |
| border-color: var(--gold); | |
| color: var(--gold); | |
| } | |
| .speed-btn .speed-sub { | |
| display: block; | |
| font-size: 11px; font-weight: 300; | |
| color: var(--text-muted); | |
| margin-top: 2px; | |
| letter-spacing: 0.08em; | |
| } | |
| .speed-btn.active .speed-sub { color: var(--gold-dim); } | |
| /* ── Advanced options toggle ─────────────────────────────────────────── */ | |
| .adv-toggle { | |
| margin-top: 10px; margin-bottom: 0; | |
| text-align: right; | |
| } | |
| #adv-btn { | |
| background: none; border: none; | |
| font-family: 'Crimson Pro', serif; | |
| font-size: 12px; font-weight: 400; | |
| letter-spacing: 0.15em; text-transform: uppercase; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 4px 0; | |
| transition: color 0.15s; | |
| } | |
| #adv-btn:hover { color: var(--gold-dim); } | |
| .depth-group { margin-top: 16px; margin-bottom: 0; display: none; } | |
| .depth-row { | |
| display: flex; align-items: center; gap: 16px; | |
| } | |
| #depth { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 3px; | |
| background: var(--border-bright); | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| #depth::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; height: 16px; | |
| border-radius: 50%; | |
| background: var(--gold); | |
| cursor: pointer; | |
| box-shadow: 0 0 8px rgba(201,168,76,0.4); | |
| } | |
| #depth::-moz-range-thumb { | |
| width: 16px; height: 16px; | |
| border-radius: 50%; | |
| background: var(--gold); | |
| cursor: pointer; border: none; | |
| } | |
| #depth-val { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 20px; font-weight: 500; | |
| color: var(--gold); | |
| min-width: 28px; text-align: right; | |
| } | |
| .depth-hint { | |
| display: block; | |
| font-size: 13px; font-weight: 300; | |
| color: var(--text-muted); | |
| margin-top: 8px; | |
| margin-bottom: 24px; | |
| } | |
| /* ── Screen 2: Loading ───────────────────────────────────────────────── */ | |
| #s-loading { justify-content: center; align-items: center; } | |
| .loading-inner { | |
| text-align: center; | |
| width: 480px; max-width: 90vw; | |
| } | |
| .loading-inner h2 { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 22px; font-weight: 400; | |
| letter-spacing: 0.15em; | |
| color: var(--text-sub); | |
| margin-bottom: 36px; | |
| text-transform: uppercase; | |
| } | |
| .progress-track { | |
| width: 100%; height: 3px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-bottom: 20px; | |
| } | |
| #progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-light)); | |
| border-radius: 2px; | |
| width: 0%; | |
| transition: width 0.45s cubic-bezier(0.4,0,0.2,1); | |
| box-shadow: 0 0 12px rgba(201,168,76,0.5); | |
| } | |
| #progress-msg { | |
| font-size: 14px; font-weight: 300; | |
| color: var(--text-sub); | |
| letter-spacing: 0.05em; | |
| } | |
| #progress-pct { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| color: var(--gold-dim); | |
| margin-top: 10px; | |
| } | |
| /* ── Screen 3: Analysis ──────────────────────────────────────────────── */ | |
| #s-analysis { | |
| align-items: center; | |
| padding: 0 10px 40px; | |
| } | |
| .analysis-header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| width: 100%; max-width: 620px; | |
| padding: 12px 0 12px; | |
| border-bottom: 1px solid var(--border); | |
| margin-bottom: 14px; | |
| gap: 8px; | |
| } | |
| .players { | |
| text-align: center; flex: 1; | |
| display: flex; align-items: center; justify-content: center; gap: 10px; | |
| } | |
| .player-name { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 18px; font-weight: 500; | |
| } | |
| .white-player { color: #f0ede4; } | |
| .black-player { color: #a09880; } | |
| .vs { | |
| font-size: 12px; font-weight: 300; | |
| letter-spacing: 0.2em; text-transform: uppercase; | |
| color: var(--text-muted); | |
| } | |
| @media (max-width: 420px) { | |
| .player-name { font-size: 13px; } | |
| .vs { display: none; } | |
| .players { gap: 6px; } | |
| .btn-ghost { font-size: 12px; padding: 5px 8px; } | |
| } | |
| /* ── Board ───────────────────────────────────────────────────────────── */ | |
| .board-area { position: relative; } | |
| .board-with-coords { | |
| display: flex; align-items: flex-start; gap: 0; | |
| } | |
| /* ── Eval bar ────────────────────────────────────────────────────────── */ | |
| #eval-bar-wrap { | |
| position: relative; | |
| width: 12px; | |
| height: calc(var(--sq) * 8); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| background: #1c1a18; | |
| margin-right: 6px; | |
| flex-shrink: 0; | |
| box-shadow: 0 0 0 1px var(--border); | |
| } | |
| #eval-bar-fill { | |
| position: absolute; | |
| bottom: 0; left: 0; right: 0; | |
| background: #f0ede4; | |
| height: 50%; | |
| transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| border-radius: 0 0 3px 3px; | |
| } | |
| .rank-labels { | |
| display: flex; flex-direction: column; | |
| justify-content: space-around; | |
| padding-right: 8px; | |
| padding-bottom: calc(var(--sq) * 0); /* align with board */ | |
| } | |
| .coord { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| width: var(--sq); height: var(--sq); | |
| display: flex; align-items: center; justify-content: center; | |
| user-select: none; | |
| } | |
| .rank-labels .coord { width: auto; } | |
| .board-column { display: flex; flex-direction: column; } | |
| #board { | |
| display: grid; | |
| grid-template-columns: repeat(8, var(--sq)); | |
| grid-template-rows: repeat(8, var(--sq)); | |
| box-shadow: | |
| 0 0 0 2px var(--border), | |
| 0 0 0 3px var(--border-bright), | |
| 0 24px 80px rgba(0,0,0,0.8), | |
| 0 4px 16px rgba(0,0,0,0.5); | |
| } | |
| .file-labels { | |
| display: flex; | |
| margin-top: 4px; | |
| } | |
| .file-labels .coord { height: auto; padding: 4px 0; } | |
| .sq { | |
| width: var(--sq); height: var(--sq); | |
| position: relative; | |
| display: flex; align-items: center; justify-content: center; | |
| overflow: hidden; | |
| } | |
| .sq.light { background: #f0d9b5; } | |
| .sq.dark { background: #b58863; } | |
| .overlay { | |
| position: absolute; inset: 0; z-index: 1; | |
| pointer-events: none; | |
| } | |
| .from-ov { background: rgba(255, 220, 60, 0.35); } | |
| .to-ov { opacity: 0.72; } | |
| .cls-badge { | |
| position: absolute; top: 2px; right: 2px; z-index: 4; | |
| width: calc(var(--sq) * 0.31); height: calc(var(--sq) * 0.31); | |
| min-width: 14px; min-height: 14px; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: calc(var(--sq) * 0.16); font-weight: 700; | |
| color: #fff; | |
| font-family: 'JetBrains Mono', monospace; | |
| pointer-events: none; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.6); | |
| } | |
| .piece { | |
| position: relative; z-index: 2; | |
| font-size: calc(var(--sq) * 0.64); line-height: 1; | |
| display: flex; align-items: center; justify-content: center; | |
| width: 100%; height: 100%; | |
| pointer-events: none; user-select: none; | |
| } | |
| .wp { | |
| color: #fff; | |
| text-shadow: | |
| 0 0 2px #000, 0 0 3px #000, | |
| 1px 1px 0 #000, -1px -1px 0 #000, | |
| 1px -1px 0 #000, -1px 1px 0 #000; | |
| } | |
| .bp { | |
| color: #1c1a18; | |
| text-shadow: | |
| 0 0 2px rgba(255,255,255,0.55), | |
| 1px 1px 0 rgba(255,255,255,0.3), | |
| -1px -1px 0 rgba(255,255,255,0.3); | |
| } | |
| /* ── Best move arrow ─────────────────────────────────────────────────── */ | |
| .board-wrap { | |
| position: relative; | |
| display: inline-block; | |
| line-height: 0; | |
| } | |
| #best-arrow-svg { | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| /* ── Navigation ──────────────────────────────────────────────────────── */ | |
| .nav-row { | |
| display: flex; align-items: center; gap: 20px; | |
| margin-top: 12px; | |
| } | |
| .nav-btn { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-bright); | |
| color: var(--text-sub); | |
| width: 40px; height: 40px; | |
| border-radius: 3px; | |
| font-size: 16px; cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all 0.15s; | |
| } | |
| .nav-btn:hover:not(:disabled) { border-color: var(--gold-dim); color: var(--text); background: var(--bg-elevated); } | |
| .nav-btn:disabled { opacity: 0.25; cursor: default; } | |
| #move-indicator { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; font-weight: 500; | |
| color: var(--text-sub); | |
| min-width: 100px; text-align: center; | |
| } | |
| /* ── Move timeline ───────────────────────────────────────────────────── */ | |
| #move-timeline { | |
| display: flex; gap: 3px; | |
| margin-top: 10px; | |
| max-width: calc(var(--sq) * 8 + 46px); | |
| width: 100%; | |
| overflow-x: auto; | |
| padding-bottom: 4px; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border-bright) transparent; | |
| } | |
| .tl-dot { | |
| flex-shrink: 0; | |
| width: 10px; height: 10px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: transform 0.15s, opacity 0.15s; | |
| opacity: 0.65; | |
| } | |
| .tl-dot:hover { opacity: 1; transform: scale(1.3); } | |
| .tl-dot.tl-active { | |
| opacity: 1; transform: scale(1.25); | |
| box-shadow: 0 0 6px currentColor; | |
| } | |
| /* ── Info box ────────────────────────────────────────────────────────── */ | |
| #info-box { | |
| margin-top: 12px; | |
| width: 100%; | |
| max-width: calc(var(--sq) * 8 + 46px); | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| overflow: visible; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.4); | |
| margin-bottom: 24px; | |
| } | |
| .info-start { | |
| padding: 28px 32px; | |
| color: var(--text-muted); | |
| font-size: 15px; font-weight: 300; | |
| font-style: italic; | |
| } | |
| .info-header { | |
| padding: 16px 18px 14px; | |
| border-bottom: 1px solid var(--border); | |
| display: grid; | |
| grid-template-columns: auto 1fr auto; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| @media (max-width: 420px) { | |
| .info-header { grid-template-columns: auto 1fr; gap: 8px; padding: 12px 14px; } | |
| .ep-badge { display: none; } | |
| .info-comment { padding: 10px 14px; font-size: 13px; } | |
| .info-row { padding: 8px 14px; gap: 8px; grid-template-columns: 90px 1fr; } | |
| .info-label { font-size: 10px; } | |
| .info-val { font-size: 13px; } | |
| .info-val.mono { font-size: 11px; } | |
| .move-name { font-size: 16px; } | |
| .cls-label { font-size: 10px; } | |
| } | |
| .cls-pill { | |
| display: inline-flex; align-items: center; gap: 7px; | |
| padding: 5px 12px 5px 9px; | |
| border-radius: 99px; | |
| border: 1px solid; | |
| white-space: nowrap; | |
| } | |
| .cls-icon { | |
| font-size: 12px; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .cls-label { | |
| font-size: 11px; font-weight: 600; | |
| letter-spacing: 0.15em; | |
| font-family: 'Crimson Pro', serif; | |
| } | |
| .move-name { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 20px; font-weight: 600; | |
| color: var(--text); | |
| } | |
| .ep-badge { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| white-space: nowrap; | |
| } | |
| .info-comment { | |
| padding: 13px 18px; | |
| font-size: 15px; font-weight: 300; | |
| font-style: italic; | |
| color: var(--text-sub); | |
| border-bottom: 1px solid var(--border); | |
| line-height: 1.5; | |
| } | |
| .info-details { padding: 4px 0 8px; } | |
| .info-row { | |
| display: grid; | |
| grid-template-columns: 120px 1fr; | |
| align-items: baseline; | |
| gap: 12px; | |
| padding: 9px 18px; | |
| } | |
| .info-row + .info-row { border-top: 1px solid var(--border); } | |
| .info-label { | |
| font-size: 11px; font-weight: 500; | |
| letter-spacing: 0.18em; text-transform: uppercase; | |
| color: var(--text-muted); | |
| } | |
| .info-val { | |
| font-size: 15px; | |
| color: var(--text); | |
| } | |
| .info-val.mono { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| } | |
| .cont-text { | |
| color: var(--text-sub); | |
| line-height: 1.7; | |
| word-break: break-word; | |
| } | |
| .best-row.found .info-val { | |
| color: #6aa84f; | |
| font-weight: 500; | |
| } | |
| /* ── Screen 4: Summary ───────────────────────────────────────────────── */ | |
| #s-summary { | |
| justify-content: flex-start; | |
| align-items: center; | |
| padding: 0 16px 40px; | |
| } | |
| .summary-header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| width: 100%; max-width: 680px; | |
| padding: 12px 0; | |
| border-bottom: 1px solid var(--border); | |
| margin-bottom: 24px; | |
| gap: 8px; | |
| } | |
| .summary-title { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 20px; font-weight: 600; | |
| color: var(--text); | |
| text-align: center; flex: 1; | |
| } | |
| .summary-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| width: 100%; max-width: 680px; | |
| } | |
| @media (max-width: 520px) { | |
| .summary-grid { grid-template-columns: 1fr; } | |
| } | |
| .summary-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.4); | |
| } | |
| .summary-card-header { | |
| padding: 14px 20px 12px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: baseline; justify-content: space-between; | |
| gap: 12px; | |
| } | |
| .summary-player-name { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 17px; font-weight: 600; | |
| } | |
| .summary-accuracy { | |
| display: flex; align-items: baseline; gap: 5px; | |
| } | |
| .summary-accuracy-num { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 22px; font-weight: 500; | |
| color: var(--gold); | |
| } | |
| .summary-accuracy-label { | |
| font-size: 11px; font-weight: 400; | |
| letter-spacing: 0.12em; text-transform: uppercase; | |
| color: var(--text-muted); | |
| } | |
| .summary-rows { padding: 6px 0 10px; } | |
| .summary-row { | |
| display: flex; align-items: center; | |
| padding: 6px 20px; | |
| gap: 10px; | |
| } | |
| .summary-row:hover { background: rgba(255,255,255,0.02); } | |
| .summary-icon { | |
| width: 22px; height: 22px; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 9px; font-weight: 700; | |
| font-family: 'JetBrains Mono', monospace; | |
| color: #fff; | |
| flex-shrink: 0; | |
| } | |
| .summary-cls-name { | |
| font-size: 13px; font-weight: 400; | |
| color: var(--text-sub); | |
| flex: 1; | |
| } | |
| .summary-count { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 14px; font-weight: 500; | |
| color: var(--text); | |
| min-width: 20px; text-align: right; | |
| } | |
| .summary-bar-wrap { | |
| width: 60px; height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .summary-bar-fill { | |
| height: 100%; border-radius: 2px; | |
| } | |
| .summary-cta { | |
| margin-top: 20px; | |
| display: flex; gap: 10px; justify-content: center; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ═══════════════════════════════════════════════════════════════════════ | |
| SCREEN 1 — PGN Input | |
| ══════════════════════════════════════════════════════════════════════════ --> | |
| <div id="s-input" class="screen active"> | |
| <div style="margin-top: auto; margin-bottom: auto; width: 100%; max-width: 640px; padding: 40px 20px;"> | |
| <div class="brand"> | |
| <span class="brand-line"></span> | |
| <span class="brand-name">Chess Analysis</span> | |
| <span class="brand-line"></span> | |
| </div> | |
| <div class="input-card"> | |
| <h1>Game Analysis</h1> | |
| <p class="subtitle">Paste your PGN below and set analysis depth</p> | |
| <label class="field-label" for="pgn">PGN Notation</label> | |
| <textarea id="pgn" placeholder="[Event "Chess Match"] [White "Player One"] [Black "Player Two"] 1. e4 e5 2. Nf3 Nc6 3. Bb5 ..."></textarea> | |
| <div class="speed-group"> | |
| <label class="field-label">Analysis Speed</label> | |
| <div class="speed-btns"> | |
| <button class="speed-btn" data-depth="8">Fast<span class="speed-sub">Depth 8</span></button> | |
| <button class="speed-btn active" data-depth="10">Default<span class="speed-sub">Depth 10</span></button> | |
| <button class="speed-btn" data-depth="13">Slow<span class="speed-sub">Depth 13</span></button> | |
| </div> | |
| </div> | |
| <div class="adv-toggle"> | |
| <button id="adv-btn">Advanced Options ▾</button> | |
| </div> | |
| <div class="depth-group" id="adv-group"> | |
| <label class="field-label" for="depth">Custom Depth</label> | |
| <div class="depth-row"> | |
| <input type="range" id="depth" min="6" max="22" value="10"> | |
| <span id="depth-val">10</span> | |
| </div> | |
| <span class="depth-hint">Deeper = stronger analysis, longer wait</span> | |
| </div> | |
| <div style="text-align: center; margin-top: 28px;"> | |
| <button id="analyze-btn" class="btn btn-gold">Analyze Game</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ═══════════════════════════════════════════════════════════════════════ | |
| SCREEN 2 — Loading | |
| ══════════════════════════════════════════════════════════════════════════ --> | |
| <div id="s-loading" class="screen"> | |
| <div class="loading-inner" style="margin: auto;"> | |
| <h2>Analyzing</h2> | |
| <div class="progress-track"> | |
| <div id="progress-bar"></div> | |
| </div> | |
| <p id="progress-msg">Initializing engine…</p> | |
| <p id="progress-pct">0%</p> | |
| </div> | |
| </div> | |
| <!-- ═══════════════════════════════════════════════════════════════════════ | |
| SCREEN 3 — Summary | |
| ══════════════════════════════════════════════════════════════════════════ --> | |
| <div id="s-summary" class="screen"> | |
| <div class="summary-header"> | |
| <button id="summary-back-btn" class="btn btn-ghost">← New Analysis</button> | |
| <div class="summary-title">Game Summary</div> | |
| <button id="summary-review-btn" class="btn btn-gold" style="font-size:14px; padding:9px 20px;">Review Board →</button> | |
| </div> | |
| <div class="summary-grid" id="summary-grid"> | |
| <!-- populated by JS --> | |
| </div> | |
| </div> | |
| <!-- ═══════════════════════════════════════════════════════════════════════ | |
| SCREEN 4 — Analysis | |
| ══════════════════════════════════════════════════════════════════════════ --> | |
| <div id="s-analysis" class="screen"> | |
| <div class="analysis-header"> | |
| <button id="back-btn" class="btn btn-ghost">← New Analysis</button> | |
| <div class="players"> | |
| <span id="white-name" class="player-name white-player">White</span> | |
| <span class="vs">vs</span> | |
| <span id="black-name" class="player-name black-player">Black</span> | |
| </div> | |
| <button id="flip-btn" class="btn btn-ghost">⇅ Flip</button> | |
| </div> | |
| <div class="board-area"> | |
| <div class="board-with-coords"> | |
| <div id="eval-bar-wrap"><div id="eval-bar-fill"></div></div> | |
| <div class="rank-labels" id="rank-labels"></div> | |
| <div class="board-column"> | |
| <div class="board-wrap"> | |
| <div id="board"></div> | |
| <svg id="best-arrow-svg" viewBox="0 0 8 8" preserveAspectRatio="none"> | |
| <defs> | |
| <marker id="arrowhead" markerWidth="3" markerHeight="3" | |
| refX="2.5" refY="1.5" orient="auto"> | |
| <polygon points="0 0, 3 1.5, 0 3" fill="rgba(91,141,217,0.9)"/> | |
| </marker> | |
| </defs> | |
| <line id="best-arrow-line" x1="0" y1="0" x2="0" y2="0" | |
| stroke="rgba(91,141,217,0.85)" stroke-width="0.22" | |
| stroke-linecap="round" marker-end="url(#arrowhead)" | |
| display="none"/> | |
| </svg> | |
| </div> | |
| <div class="file-labels" id="file-labels"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="nav-row"> | |
| <button id="prev-btn" class="nav-btn" title="Previous move (←)">←</button> | |
| <span id="move-indicator">Start</span> | |
| <button id="next-btn" class="nav-btn" title="Next move (→)">→</button> | |
| </div> | |
| <div id="move-timeline"></div> | |
| <div id="info-box"> | |
| <div class="info-start"><p>Press → to begin stepping through the game.</p></div> | |
| </div> | |
| </div> | |
| <script> | |
| // ── Constants ────────────────────────────────────────────────────────── | |
| const PIECE_MAP = { | |
| 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙', | |
| 'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟' | |
| }; | |
| const CLS_META = { | |
| Book: { color:'#b5854a', icon:'📖', label:'Book' }, | |
| Brilliant: { color:'#22d1cc', icon:'!!', label:'Brilliant' }, | |
| Great: { color:'#5b8dd9', icon:'!', label:'Great' }, | |
| Miss: { color:'#e05555', icon:'×', label:'Miss' }, | |
| Best: { color:'#5fb153', icon:'★', label:'Best' }, | |
| Excellent: { color:'#96d16a', icon:'👍', label:'Excellent' }, | |
| Good: { color:'#a8d672', icon:'✓', label:'Good' }, | |
| Inaccuracy: { color:'#f0c040', icon:'?!', label:'Inaccuracy' }, | |
| Mistake: { color:'#e08030', icon:'?', label:'Mistake' }, | |
| Blunder: { color:'#d43030', icon:'??', label:'Blunder' }, | |
| }; | |
| // ── State ────────────────────────────────────────────────────────────── | |
| let gameData = null; | |
| let curIdx = -1; // -1 = start, 0..n-1 = after that move | |
| let flipped = false; | |
| // ── Eval bar ─────────────────────────────────────────────────────────── | |
| function getWhiteEP(mv) { | |
| return mv.color === 'white' ? mv.ep_after : 1.0 - mv.ep_after; | |
| } | |
| function updateEvalBar(epWhite) { | |
| // epWhite: 0.0 (black winning) → 1.0 (white winning), 0.5 = equal | |
| const fill = document.getElementById('eval-bar-fill'); | |
| if (!fill) return; | |
| const pct = Math.max(2, Math.min(98, epWhite * 100)); | |
| fill.style.height = pct + '%'; | |
| } | |
| // ── Summary screen ───────────────────────────────────────────────────── | |
| const CLS_ORDER = [ | |
| "Book","Brilliant","Great","Best","Excellent","Good", | |
| "Inaccuracy","Mistake","Blunder","Miss" | |
| ]; | |
| const CLS_DISPLAY = { | |
| "Miss": "Missed Tactic", | |
| }; | |
| function renderSummary(data) { | |
| const grid = document.getElementById('summary-grid'); | |
| grid.innerHTML = ''; | |
| const colors = [ | |
| { key: 'white', name: data.white, nameClass: 'white-player' }, | |
| { key: 'black', name: data.black, nameClass: 'black-player' }, | |
| ]; | |
| for (const { key, name, nameClass } of colors) { | |
| const stats = data.summary[key]; | |
| const counts = stats.counts; | |
| const total = Object.values(counts).reduce((a, b) => a + b, 0); | |
| const maxCount = Math.max(1, ...Object.values(counts)); | |
| const rows = CLS_ORDER.map(cls => { | |
| const meta = CLS_META[cls]; | |
| const count = counts[cls] || 0; | |
| const barPct = Math.round((count / maxCount) * 100); | |
| const label = CLS_DISPLAY[cls] || cls; | |
| return ` | |
| <div class="summary-row"> | |
| <div class="summary-icon" style="background:${meta.color}">${meta.icon}</div> | |
| <span class="summary-cls-name">${label}</span> | |
| <div class="summary-bar-wrap"> | |
| <div class="summary-bar-fill" style="width:${barPct}%;background:${meta.color}88"></div> | |
| </div> | |
| <span class="summary-count">${count}</span> | |
| </div>`; | |
| }).join(''); | |
| grid.innerHTML += ` | |
| <div class="summary-card"> | |
| <div class="summary-card-header"> | |
| <span class="summary-player-name ${nameClass}">${name}</span> | |
| <div class="summary-accuracy"> | |
| <span class="summary-accuracy-num">${stats.accuracy}%</span> | |
| <span class="summary-accuracy-label">accuracy</span> | |
| </div> | |
| </div> | |
| <div class="summary-rows">${rows}</div> | |
| </div>`; | |
| } | |
| } | |
| // ── Screen switching ─────────────────────────────────────────────────── | |
| function showScreen(id) { | |
| document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); | |
| document.getElementById(id).classList.add('active'); | |
| } | |
| // ── FEN parser ───────────────────────────────────────────────────────── | |
| function parseFEN(fen) { | |
| const rows = fen.split(' ')[0].split('/'); | |
| const board = {}; | |
| for (let r = 0; r < 8; r++) { | |
| const rank = 8 - r; | |
| let file = 0; | |
| for (const ch of rows[r]) { | |
| if (ch >= '1' && ch <= '8') { file += +ch; } | |
| else { board['abcdefgh'[file] + rank] = ch; file++; } | |
| } | |
| } | |
| return board; | |
| } | |
| // ── Board renderer ───────────────────────────────────────────────────── | |
| function sqToGrid(sqName) { | |
| // Returns {col, row} in 0-7 grid coords for current flip state | |
| const files = ['a','b','c','d','e','f','g','h']; | |
| const file = sqName[0]; | |
| const rank = parseInt(sqName[1]); | |
| const fi = files.indexOf(file); | |
| const col = flipped ? 7 - fi : fi; | |
| const row = flipped ? rank - 1 : 8 - rank; | |
| return { col, row }; | |
| } | |
| function renderBestArrow(bestUci) { | |
| const line = document.getElementById('best-arrow-line'); | |
| if (!line) return; | |
| if (!bestUci || bestUci.length < 4) { line.setAttribute('display','none'); return; } | |
| const from = bestUci.slice(0, 2); | |
| const to = bestUci.slice(2, 4); | |
| const f = sqToGrid(from); | |
| const t = sqToGrid(to); | |
| // Centre of each square in 0-8 SVG coordinate space | |
| const x1 = f.col + 0.5, y1 = f.row + 0.5; | |
| const x2 = t.col + 0.5, y2 = t.row + 0.5; | |
| // Shorten slightly so arrowhead sits inside the square | |
| const dx = x2 - x1, dy = y2 - y1; | |
| const len = Math.sqrt(dx*dx + dy*dy); | |
| const shorten = 0.38; | |
| const nx = dx/len, ny = dy/len; | |
| line.setAttribute('x1', x1 + nx * 0.25); | |
| line.setAttribute('y1', y1 + ny * 0.25); | |
| line.setAttribute('x2', x2 - nx * shorten); | |
| line.setAttribute('y2', y2 - ny * shorten); | |
| line.setAttribute('display', ''); | |
| } | |
| function renderBoard(fen, fromSq, toSq, cls, bestUci) { | |
| const pieces = parseFEN(fen); | |
| const boardEl = document.getElementById('board'); | |
| boardEl.innerHTML = ''; | |
| const rankOrder = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1]; | |
| const fileOrder = flipped | |
| ? ['h','g','f','e','d','c','b','a'] | |
| : ['a','b','c','d','e','f','g','h']; | |
| const meta = cls ? (CLS_META[cls] || null) : null; | |
| for (const rank of rankOrder) { | |
| for (let fi = 0; fi < 8; fi++) { | |
| const file = fileOrder[fi]; | |
| const sq = file + rank; | |
| const isLight = (fi + rank) % 2 === 0; | |
| const div = document.createElement('div'); | |
| div.className = `sq ${isLight ? 'light' : 'dark'}`; | |
| // From-square highlight | |
| if (sq === fromSq) { | |
| const ov = document.createElement('div'); | |
| ov.className = 'overlay from-ov'; | |
| div.appendChild(ov); | |
| } | |
| // To-square highlight + badge | |
| if (sq === toSq && meta) { | |
| const ov = document.createElement('div'); | |
| ov.className = 'overlay to-ov'; | |
| ov.style.background = meta.color; | |
| div.appendChild(ov); | |
| const badge = document.createElement('div'); | |
| badge.className = 'cls-badge'; | |
| badge.textContent = meta.icon; | |
| badge.style.background = meta.color; | |
| div.appendChild(badge); | |
| } | |
| // Piece | |
| const piece = pieces[sq]; | |
| if (piece) { | |
| const span = document.createElement('span'); | |
| span.className = `piece ${piece === piece.toUpperCase() ? 'wp' : 'bp'}`; | |
| span.textContent = PIECE_MAP[piece] || piece; | |
| div.appendChild(span); | |
| } | |
| boardEl.appendChild(div); | |
| } | |
| } | |
| updateCoords(rankOrder, fileOrder); | |
| renderBestArrow(bestUci || null); | |
| } | |
| function updateCoords(rankOrder, fileOrder) { | |
| const rl = document.getElementById('rank-labels'); | |
| rl.innerHTML = rankOrder.map(r => | |
| `<div class="coord" style="width:auto; padding-right:0">${r}</div>` | |
| ).join(''); | |
| const fl = document.getElementById('file-labels'); | |
| fl.innerHTML = fileOrder.map(f => | |
| `<div class="coord" style="height:auto">${f}</div>` | |
| ).join(''); | |
| } | |
| // ── Timeline ─────────────────────────────────────────────────────────── | |
| function buildTimeline() { | |
| const tl = document.getElementById('move-timeline'); | |
| tl.innerHTML = ''; | |
| gameData.moves.forEach((mv, i) => { | |
| const dot = document.createElement('div'); | |
| dot.className = 'tl-dot'; | |
| dot.style.background = (CLS_META[mv.classification] || {color:'#555'}).color; | |
| dot.title = `${mv.move_number}${mv.color==='white'?'.':'...'} ${mv.san} — ${mv.classification}`; | |
| dot.addEventListener('click', () => navigateTo(i)); | |
| tl.appendChild(dot); | |
| }); | |
| } | |
| function refreshTimeline() { | |
| const dots = document.querySelectorAll('.tl-dot'); | |
| dots.forEach((d, i) => { | |
| d.classList.toggle('tl-active', i === curIdx); | |
| }); | |
| // Scroll active dot into view | |
| if (curIdx >= 0 && dots[curIdx]) { | |
| dots[curIdx].scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' }); | |
| } | |
| } | |
| // ── Info box ─────────────────────────────────────────────────────────── | |
| function renderStartInfo() { | |
| document.getElementById('info-box').innerHTML = ` | |
| <div class="info-start"> | |
| <p>Press <strong>→</strong> or the right arrow key to step through the game.</p> | |
| </div>`; | |
| } | |
| function renderMoveInfo(mv) { | |
| const meta = CLS_META[mv.classification] || { color:'#888', icon:'?', label: mv.classification }; | |
| const ep = mv.ep_loss > 0.001 | |
| ? `−${(mv.ep_loss * 100).toFixed(1)}% EP` | |
| : 'No EP lost'; | |
| const moveLabel = mv.color === 'white' | |
| ? `${mv.move_number}. ${mv.san}` | |
| : `${mv.move_number}… ${mv.san}`; | |
| const bestHtml = mv.best_move_san | |
| ? `<div class="info-row best-row"> | |
| <span class="info-label">Best move</span> | |
| <span class="info-val mono">${mv.color === 'white' ? mv.move_number + '.' : mv.move_number + '…'} ${mv.best_move_san}</span> | |
| </div>` | |
| : `<div class="info-row best-row found"> | |
| <span class="info-label">Best move</span> | |
| <span class="info-val found">✓ You found the best move</span> | |
| </div>`; | |
| const openingHtml = mv.opening_name | |
| ? `<div class="info-row"> | |
| <span class="info-label">Opening</span> | |
| <span class="info-val">${mv.opening_eco ? '<span style="font-family:monospace;color:var(--gold-dim);font-size:12px;margin-right:6px">' + mv.opening_eco + '</span>' : ''}${mv.opening_name}</span> | |
| </div>` | |
| : ''; | |
| const contHtml = mv.continuation_fmt | |
| ? `<div class="info-row cont-row"> | |
| <span class="info-label">Continuation</span> | |
| <span class="info-val mono cont-text">${mv.continuation_fmt}</span> | |
| </div>` | |
| : ''; | |
| document.getElementById('info-box').innerHTML = ` | |
| <div class="info-header"> | |
| <div class="cls-pill" style="background:${meta.color}18; border-color:${meta.color}55; color:${meta.color}"> | |
| <span class="cls-icon">${meta.icon}</span> | |
| <span class="cls-label">${meta.label.toUpperCase()}</span> | |
| </div> | |
| <div class="move-name">${moveLabel}</div> | |
| <div class="ep-badge">${ep}</div> | |
| </div> | |
| <div class="info-comment">"${mv.comment}"</div> | |
| <div class="info-details"> | |
| ${openingHtml} | |
| ${bestHtml} | |
| ${contHtml} | |
| </div>`; | |
| } | |
| // ── Navigation ───────────────────────────────────────────────────────── | |
| function navigateTo(idx) { | |
| if (!gameData) return; | |
| const max = gameData.moves.length - 1; | |
| curIdx = Math.max(-1, Math.min(idx, max)); | |
| document.getElementById('prev-btn').disabled = curIdx === -1; | |
| document.getElementById('next-btn').disabled = curIdx === max; | |
| if (curIdx === -1) { | |
| renderBoard(gameData.initial_fen, null, null, null, null); | |
| document.getElementById('move-indicator').textContent = 'Start'; | |
| renderStartInfo(); | |
| updateEvalBar(0.5); | |
| } else { | |
| const mv = gameData.moves[curIdx]; | |
| // Show best-move arrow except for Book (theory) and Best (already played it) | |
| const showArrow = mv.best_move_uci && mv.classification !== 'Book' && mv.classification !== 'Best'; | |
| renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification, | |
| showArrow ? mv.best_move_uci : null); | |
| document.getElementById('move-indicator').textContent = | |
| `Move ${curIdx + 1} / ${gameData.moves.length}`; | |
| renderMoveInfo(mv); | |
| updateEvalBar(getWhiteEP(mv)); | |
| } | |
| refreshTimeline(); | |
| } | |
| // ── Load completed analysis ──────────────────────────────────────────── | |
| function loadAnalysis(data) { | |
| gameData = data; | |
| curIdx = -1; | |
| flipped = false; | |
| document.getElementById('white-name').textContent = data.white; | |
| document.getElementById('black-name').textContent = data.black; | |
| buildTimeline(); | |
| navigateTo(-1); | |
| // Show summary first | |
| renderSummary(data); | |
| showScreen('s-summary'); | |
| } | |
| // ── Progress bar ─────────────────────────────────────────────────────── | |
| function setProgress(pct, msg) { | |
| document.getElementById('progress-bar').style.width = `${Math.round(pct * 100)}%`; | |
| document.getElementById('progress-msg').textContent = msg; | |
| document.getElementById('progress-pct').textContent = `${Math.round(pct * 100)}%`; | |
| } | |
| // ── Submit analysis ──────────────────────────────────────────────────── | |
| async function submitAnalysis() { | |
| const pgn = document.getElementById('pgn').value.trim(); | |
| const depth = parseInt(document.getElementById('depth').value, 10); | |
| if (!pgn) { alert('Please paste a PGN to analyze.'); return; } | |
| showScreen('s-loading'); | |
| setProgress(0, 'Connecting to analysis engine…'); | |
| let jobId; | |
| try { | |
| const res = await fetch('/api/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ pgn, depth }) | |
| }); | |
| const d = await res.json(); | |
| if (d.error) throw new Error(d.error); | |
| jobId = d.job_id; | |
| } catch (err) { | |
| alert('Error: ' + err.message); | |
| showScreen('s-input'); | |
| return; | |
| } | |
| const evtSrc = new EventSource(`/api/stream/${jobId}`); | |
| evtSrc.onmessage = (e) => { | |
| const data = JSON.parse(e.data); | |
| if (data.type === 'progress') { | |
| setProgress(data.progress, data.message); | |
| } else if (data.type === 'complete') { | |
| evtSrc.close(); | |
| loadAnalysis(data.data); | |
| } else if (data.type === 'error') { | |
| evtSrc.close(); | |
| alert('Analysis error: ' + data.message); | |
| showScreen('s-input'); | |
| } | |
| }; | |
| evtSrc.onerror = () => { | |
| evtSrc.close(); | |
| alert('Connection lost. Please try again.'); | |
| showScreen('s-input'); | |
| }; | |
| } | |
| // ── Speed picker + advanced options ─────────────────────────────────── | |
| let advancedOpen = false; | |
| document.querySelectorAll('.speed-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| document.getElementById('depth').value = btn.dataset.depth; | |
| document.getElementById('depth-val').textContent = btn.dataset.depth; | |
| }); | |
| }); | |
| document.getElementById('adv-btn').addEventListener('click', () => { | |
| advancedOpen = !advancedOpen; | |
| document.getElementById('adv-group').style.display = advancedOpen ? 'block' : 'none'; | |
| document.querySelector('.speed-group').style.display = advancedOpen ? 'none' : 'block'; | |
| document.getElementById('adv-btn').textContent = | |
| advancedOpen ? 'Advanced Options ▴' : 'Advanced Options ▾'; | |
| }); | |
| document.getElementById('depth').addEventListener('input', e => { | |
| document.getElementById('depth-val').textContent = e.target.value; | |
| }); | |
| document.getElementById('analyze-btn').addEventListener('click', submitAnalysis); | |
| document.getElementById('summary-back-btn').addEventListener('click', () => { | |
| showScreen('s-input'); | |
| }); | |
| document.getElementById('summary-review-btn').addEventListener('click', () => { | |
| showScreen('s-analysis'); | |
| }); | |
| document.getElementById('back-btn').addEventListener('click', () => { | |
| showScreen('s-summary'); | |
| }); | |
| document.getElementById('prev-btn').addEventListener('click', () => navigateTo(curIdx - 1)); | |
| document.getElementById('next-btn').addEventListener('click', () => navigateTo(curIdx + 1)); | |
| document.getElementById('flip-btn').addEventListener('click', () => { | |
| flipped = !flipped; | |
| if (!gameData) return; | |
| if (curIdx === -1) { | |
| renderBoard(gameData.initial_fen, null, null, null, null); | |
| } else { | |
| const mv = gameData.moves[curIdx]; | |
| const showArrow = mv.best_move_uci && mv.classification !== 'Book' && mv.classification !== 'Best'; | |
| renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification, | |
| showArrow ? mv.best_move_uci : null); | |
| } | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (!gameData) return; | |
| const active = document.querySelector('.screen.active'); | |
| if (!active || active.id !== 's-analysis') return; | |
| if (e.key === 'ArrowRight') { e.preventDefault(); navigateTo(curIdx + 1); } | |
| if (e.key === 'ArrowLeft') { e.preventDefault(); navigateTo(curIdx - 1); } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |