Chess_Analyzer / templates /index.html
Fu01978's picture
Rename templates/index (16).html to templates/index.html
0bc9c0e verified
<!DOCTYPE html>
<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 &quot;Chess Match&quot;]&#10;[White &quot;Player One&quot;]&#10;[Black &quot;Player Two&quot;]&#10;&#10;1. e4 e5 2. Nf3 Nc6 3. Bb5 ..."></textarea>
<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>