react-chess-explorer 0.0.1 → 0.0.3
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.
- package/LICENSE +21 -21
- package/README.md +63 -63
- package/dist/features/explorer/components/DefaultBoardNav.d.ts +2 -7
- package/dist/features/explorer/components/PositionGamesPanel.d.ts +5 -10
- package/dist/features/explorer/core/PositionReferenceExplorerCore.d.ts +1 -1
- package/dist/features/explorer/core/renderProps.d.ts +24 -15
- package/dist/features/explorer/defaults/DefaultReferencePanel.d.ts +2 -1
- package/dist/features/explorer/defaults/DefaultVariationsStrip.d.ts +1 -1
- package/dist/features/explorer/explorerSessionCache.d.ts +15 -0
- package/dist/features/explorer/hooks/useExplorerPrefetch.d.ts +11 -0
- package/dist/features/explorer/hooks/usePositionHistory.d.ts +12 -1
- package/dist/features/explorer/hooks/usePositionReferenceData.d.ts +13 -14
- package/dist/features/explorer/hooks/useVariationLines.d.ts +1 -5
- package/dist/features/explorer/index.d.ts +5 -3
- package/dist/features/explorer/mocks.d.ts +2 -2
- package/dist/features/explorer/positionUtils.d.ts +2 -0
- package/dist/features/explorer/seedExplorerStartSession.d.ts +3 -0
- package/dist/features/explorer/types.d.ts +18 -11
- package/dist/features/explorer/variationLines.d.ts +0 -1
- package/dist/index.esm.js +691 -291
- package/dist/index.js +702 -289
- package/dist/stories/PositionReferenceExplorer.stories.d.ts +10 -0
- package/dist/stories/fixtures/nc6MockApi.d.ts +13 -0
- package/dist/stories/fixtures/nc6SampleGames.d.ts +36 -0
- package/package.json +59 -45
- package/dist/features/explorer/components/EloRangeFilter.d.ts +0 -9
- package/dist/features/explorer/components/LineHeader.d.ts +0 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chess_js = require('chess.js');
|
|
3
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
5
|
var reactChessCore = require('react-chess-core');
|
|
5
|
-
var reactChessboard = require('react-chessboard');
|
|
6
|
-
var chess_js = require('chess.js');
|
|
7
6
|
var react = require('react');
|
|
8
7
|
|
|
9
8
|
/** Standard start position — placeholder until game replay is implemented. */
|
|
@@ -11,6 +10,145 @@ const EXPLORER_START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -
|
|
|
11
10
|
/** Delay between plies when animating a clicked variation line. */
|
|
12
11
|
const VARIATION_LINE_STEP_MS = 500;
|
|
13
12
|
|
|
13
|
+
/** Match endchess-backend / mass-games-import (first four FEN fields). */
|
|
14
|
+
function normalizeFen(fen) {
|
|
15
|
+
const parts = fen.trim().split(/\s+/);
|
|
16
|
+
if (parts.length < 4) {
|
|
17
|
+
throw new Error(`Invalid FEN: ${fen}`);
|
|
18
|
+
}
|
|
19
|
+
return `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]}`;
|
|
20
|
+
}
|
|
21
|
+
/** Legal move from a board drag/drop (react-chessboard piece string, e.g. wQ). */
|
|
22
|
+
function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
const chess = new chess_js.Chess(fen);
|
|
25
|
+
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
26
|
+
const legal = chess
|
|
27
|
+
.moves({ square: sourceSquare, verbose: true })
|
|
28
|
+
.find((move) => move.to === targetSquare &&
|
|
29
|
+
(!move.promotion || move.promotion === pieceType));
|
|
30
|
+
if (!legal)
|
|
31
|
+
return null;
|
|
32
|
+
const played = chess.move({
|
|
33
|
+
from: legal.from,
|
|
34
|
+
to: legal.to,
|
|
35
|
+
promotion: legal.promotion,
|
|
36
|
+
});
|
|
37
|
+
if (!played)
|
|
38
|
+
return null;
|
|
39
|
+
const uci = `${played.from}${played.to}${(_b = played.promotion) !== null && _b !== void 0 ? _b : ""}`;
|
|
40
|
+
return { fen: chess.fen(), uci, san: played.san };
|
|
41
|
+
}
|
|
42
|
+
/** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
|
|
43
|
+
function applyLineSans(startFen, sans) {
|
|
44
|
+
const chess = new chess_js.Chess(startFen);
|
|
45
|
+
const entries = [];
|
|
46
|
+
for (const san of sans) {
|
|
47
|
+
try {
|
|
48
|
+
const move = chess.move(san);
|
|
49
|
+
if (!move)
|
|
50
|
+
return null;
|
|
51
|
+
entries.push({ fen: chess.fen(), lastSan: move.san });
|
|
52
|
+
}
|
|
53
|
+
catch (_a) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { fen: chess.fen(), entries };
|
|
58
|
+
}
|
|
59
|
+
/** Apply a UCI move to a FEN; returns null if illegal. */
|
|
60
|
+
function fenAfterUci(fen, uci) {
|
|
61
|
+
const chess = new chess_js.Chess(fen);
|
|
62
|
+
const from = uci.slice(0, 2);
|
|
63
|
+
const to = uci.slice(2, 4);
|
|
64
|
+
const promotion = uci.length > 4 ? uci[4] : undefined;
|
|
65
|
+
try {
|
|
66
|
+
const move = chess.move({ from, to, promotion });
|
|
67
|
+
if (!move)
|
|
68
|
+
return null;
|
|
69
|
+
return chess.fen();
|
|
70
|
+
}
|
|
71
|
+
catch (_a) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function whiteScorePercent(whiteWins, draws, blackWins) {
|
|
76
|
+
const total = whiteWins + draws + blackWins;
|
|
77
|
+
if (total === 0)
|
|
78
|
+
return null;
|
|
79
|
+
return Math.round((100 * (whiteWins + 0.5 * draws)) / total);
|
|
80
|
+
}
|
|
81
|
+
/** Format SAN plies from the starting position as `1.d4 Nf6 2.c4 e6`. */
|
|
82
|
+
function formatNumberedLineSans(sans) {
|
|
83
|
+
if (sans.length === 0) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (let i = 0; i < sans.length; i += 2) {
|
|
88
|
+
const moveNumber = i / 2 + 1;
|
|
89
|
+
const white = sans[i];
|
|
90
|
+
const black = sans[i + 1];
|
|
91
|
+
parts.push(black ? `${moveNumber}.${white} ${black}` : `${moveNumber}.${white}`);
|
|
92
|
+
}
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const EXPLORER_PREFETCH_CHILD_COUNT = 4;
|
|
97
|
+
const EXPLORER_DEFAULT_VARIATION_LINE_COUNT = 8;
|
|
98
|
+
const EXPLORER_DEFAULT_VARIATION_DEPTH = 4;
|
|
99
|
+
function gamesSessionKey(params) {
|
|
100
|
+
var _a, _b, _c, _d, _e;
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
fen: normalizeFen(params.fen),
|
|
103
|
+
uci: (_a = params.uci) !== null && _a !== void 0 ? _a : "",
|
|
104
|
+
sources: (_c = (_b = params.sources) === null || _b === void 0 ? void 0 : _b.slice().sort().join(",")) !== null && _c !== void 0 ? _c : "",
|
|
105
|
+
limit: (_d = params.limit) !== null && _d !== void 0 ? _d : 50,
|
|
106
|
+
offset: (_e = params.offset) !== null && _e !== void 0 ? _e : 0,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function variationsSessionKey(fen, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH) {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
fen: normalizeFen(fen),
|
|
112
|
+
mode: "variations",
|
|
113
|
+
lineCount,
|
|
114
|
+
lineDepth,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const sessionGames = new Map();
|
|
118
|
+
const sessionVariations = new Map();
|
|
119
|
+
const sessionCacheListeners = new Set();
|
|
120
|
+
function subscribeExplorerSessionCache(listener) {
|
|
121
|
+
sessionCacheListeners.add(listener);
|
|
122
|
+
return () => {
|
|
123
|
+
sessionCacheListeners.delete(listener);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function notifySessionCacheListeners() {
|
|
127
|
+
for (const listener of sessionCacheListeners) {
|
|
128
|
+
listener();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function peekSessionGames(key) {
|
|
132
|
+
return sessionGames.get(key);
|
|
133
|
+
}
|
|
134
|
+
function setSessionGames(key, data) {
|
|
135
|
+
sessionGames.set(key, data);
|
|
136
|
+
notifySessionCacheListeners();
|
|
137
|
+
}
|
|
138
|
+
function peekSessionVariations(key) {
|
|
139
|
+
return sessionVariations.get(key);
|
|
140
|
+
}
|
|
141
|
+
function setSessionVariations(key, lines) {
|
|
142
|
+
sessionVariations.set(key, lines);
|
|
143
|
+
notifySessionCacheListeners();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Push start-hub payloads into the explorer session cache (used by app preload). */
|
|
147
|
+
function seedExplorerStartSession(games, variations) {
|
|
148
|
+
setSessionGames(gamesSessionKey({ fen: EXPLORER_START_FEN }), games);
|
|
149
|
+
setSessionVariations(variationsSessionKey(EXPLORER_START_FEN), variations.lines);
|
|
150
|
+
}
|
|
151
|
+
|
|
14
152
|
/**
|
|
15
153
|
* Scaffold UI — proves core wiring and Rollup build.
|
|
16
154
|
* Replace with game replay once requirements are defined.
|
|
@@ -49,15 +187,8 @@ const thStyle = {
|
|
|
49
187
|
const tdStyle = {
|
|
50
188
|
padding: "3px 8px",
|
|
51
189
|
borderBottom: "1px solid rgba(128,128,128,0.12)",
|
|
52
|
-
whiteSpace: "
|
|
53
|
-
|
|
54
|
-
const sectionTitleStyle = {
|
|
55
|
-
margin: 0,
|
|
56
|
-
fontSize: 12,
|
|
57
|
-
fontWeight: 600,
|
|
58
|
-
padding: "6px 8px",
|
|
59
|
-
borderBottom: "1px solid rgba(128,128,128,0.2)",
|
|
60
|
-
background: "rgba(128,128,128,0.06)",
|
|
190
|
+
whiteSpace: "normal",
|
|
191
|
+
wordBreak: "break-word",
|
|
61
192
|
};
|
|
62
193
|
/** Full-width shell: board | reference — no wrap, no footer below. */
|
|
63
194
|
const referenceShellStyle = {
|
|
@@ -106,19 +237,6 @@ const referencePanelStyle = {
|
|
|
106
237
|
borderLeft: "1px solid rgba(128,128,128,0.3)",
|
|
107
238
|
boxSizing: "border-box",
|
|
108
239
|
};
|
|
109
|
-
const referenceTabBarStyle = {
|
|
110
|
-
flex: "0 0 auto",
|
|
111
|
-
display: "flex",
|
|
112
|
-
alignItems: "center",
|
|
113
|
-
gap: 4,
|
|
114
|
-
padding: "6px 10px",
|
|
115
|
-
borderBottom: "1px solid rgba(128,128,128,0.35)",
|
|
116
|
-
background: "rgba(128,128,128,0.08)",
|
|
117
|
-
fontSize: 12,
|
|
118
|
-
};
|
|
119
|
-
const referenceTabLabelStyle = {
|
|
120
|
-
padding: "2px 8px",
|
|
121
|
-
};
|
|
122
240
|
const moveStatsSectionStyle = {
|
|
123
241
|
flex: "0 1 auto",
|
|
124
242
|
maxHeight: "38%",
|
|
@@ -136,9 +254,6 @@ const variationsStripStyle = {
|
|
|
136
254
|
fontSize: 12,
|
|
137
255
|
background: "rgba(128,128,128,0.04)",
|
|
138
256
|
};
|
|
139
|
-
const variationsTabStyle = {
|
|
140
|
-
cursor: "default",
|
|
141
|
-
};
|
|
142
257
|
const gamesSectionStyle = {
|
|
143
258
|
flex: "1 1 auto",
|
|
144
259
|
display: "flex",
|
|
@@ -177,9 +292,11 @@ const statusInlineStyle = {
|
|
|
177
292
|
};
|
|
178
293
|
|
|
179
294
|
/** Right column shell: tab bar + stacked sections (no page footer below). */
|
|
180
|
-
const DefaultReferencePanel = ({ theme, status, moveStats, variationsStrip, gamesPanel, }) => (jsxRuntime.jsxs("div", { style: Object.assign(Object.assign(
|
|
295
|
+
const DefaultReferencePanel = ({ theme, fillHeight = true, status, moveStats, variationsStrip, gamesPanel, }) => (jsxRuntime.jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, referencePanelStyle), panelStyle), (fillHeight
|
|
296
|
+
? {}
|
|
297
|
+
: { height: "auto", minHeight: 0, overflow: "visible" })), "data-theme": theme, children: [status, jsxRuntime.jsx("div", { style: { flex: "0 0 auto", minHeight: 0 }, children: moveStats }), variationsStrip, gamesPanel] }));
|
|
181
298
|
|
|
182
|
-
const DefaultBoardNav = ({ canGoBack, canGoForward, onBack, onForward, }) => (jsxRuntime.jsxs("div", { style: boardNavStyle, children: [jsxRuntime.jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onBack, disabled: !canGoBack, "aria-label": "Previous position", title: "Back", children: "\u25C0" }), jsxRuntime.jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onForward, disabled: !canGoForward, "aria-label": "Next position", title: "Forward", children: "\u25B6" })] }));
|
|
299
|
+
const DefaultBoardNav = ({ canGoBack, canGoForward, onBack, onForward, onFlipBoard, }) => (jsxRuntime.jsxs("div", { style: boardNavStyle, children: [jsxRuntime.jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onBack, disabled: !canGoBack, "aria-label": "Previous position", title: "Back", children: "\u25C0" }), jsxRuntime.jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onForward, disabled: !canGoForward, "aria-label": "Next position", title: "Forward", children: "\u25B6" }), jsxRuntime.jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onFlipBoard, "aria-label": "Flip board", title: "Flip board", children: "\u21C5" })] }));
|
|
183
300
|
const defaultRenderBoardNav = (props) => (jsxRuntime.jsx(DefaultBoardNav, Object.assign({}, props)));
|
|
184
301
|
|
|
185
302
|
const ExplorerStatusBanner = ({ error, loading, }) => {
|
|
@@ -188,97 +305,15 @@ const ExplorerStatusBanner = ({ error, loading, }) => {
|
|
|
188
305
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [error && (jsxRuntime.jsx("p", { style: Object.assign(Object.assign({}, statusInlineStyle), { color: "#e57373" }), role: "alert", children: error })), loading && (jsxRuntime.jsx("p", { style: Object.assign(Object.assign({}, statusInlineStyle), { opacity: 0.7 }), children: "Loading\u2026" }))] }));
|
|
189
306
|
};
|
|
190
307
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
|
|
201
|
-
var _a, _b;
|
|
202
|
-
const chess = new chess_js.Chess(fen);
|
|
203
|
-
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
204
|
-
const legal = chess
|
|
205
|
-
.moves({ square: sourceSquare, verbose: true })
|
|
206
|
-
.find((move) => move.to === targetSquare &&
|
|
207
|
-
(!move.promotion || move.promotion === pieceType));
|
|
208
|
-
if (!legal)
|
|
209
|
-
return null;
|
|
210
|
-
const played = chess.move({
|
|
211
|
-
from: legal.from,
|
|
212
|
-
to: legal.to,
|
|
213
|
-
promotion: legal.promotion,
|
|
214
|
-
});
|
|
215
|
-
if (!played)
|
|
216
|
-
return null;
|
|
217
|
-
const uci = `${played.from}${played.to}${(_b = played.promotion) !== null && _b !== void 0 ? _b : ""}`;
|
|
218
|
-
return { fen: chess.fen(), uci, san: played.san };
|
|
219
|
-
}
|
|
220
|
-
/** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
|
|
221
|
-
function applyLineSans(startFen, sans) {
|
|
222
|
-
const chess = new chess_js.Chess(startFen);
|
|
223
|
-
const entries = [];
|
|
224
|
-
for (const san of sans) {
|
|
225
|
-
try {
|
|
226
|
-
const move = chess.move(san);
|
|
227
|
-
if (!move)
|
|
228
|
-
return null;
|
|
229
|
-
entries.push({ fen: chess.fen(), lastSan: move.san });
|
|
230
|
-
}
|
|
231
|
-
catch (_a) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { fen: chess.fen(), entries };
|
|
236
|
-
}
|
|
237
|
-
/** Apply a UCI move to a FEN; returns null if illegal. */
|
|
238
|
-
function fenAfterUci(fen, uci) {
|
|
239
|
-
const chess = new chess_js.Chess(fen);
|
|
240
|
-
const from = uci.slice(0, 2);
|
|
241
|
-
const to = uci.slice(2, 4);
|
|
242
|
-
const promotion = uci.length > 4 ? uci[4] : undefined;
|
|
243
|
-
try {
|
|
244
|
-
const move = chess.move({ from, to, promotion });
|
|
245
|
-
if (!move)
|
|
246
|
-
return null;
|
|
247
|
-
return chess.fen();
|
|
248
|
-
}
|
|
249
|
-
catch (_a) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
function whiteScorePercent(whiteWins, draws, blackWins) {
|
|
254
|
-
const total = whiteWins + draws + blackWins;
|
|
255
|
-
if (total === 0)
|
|
256
|
-
return null;
|
|
257
|
-
return Math.round((100 * (whiteWins + 0.5 * draws)) / total);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const MoveStatsTable = ({ moves, selectedUci, onMoveSelect, }) => (jsxRuntime.jsxs("div", { style: moveStatsSectionStyle, children: [jsxRuntime.jsx("div", { style: sectionTitleStyle, children: "Moves" }), jsxRuntime.jsxs("table", { style: tableStyle, children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { style: thStyle, children: "Move" }), jsxRuntime.jsx("th", { style: thStyle, children: "Games" }), jsxRuntime.jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Score %" }), jsxRuntime.jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Avg" })] }) }), jsxRuntime.jsx("tbody", { children: moves.map((move) => {
|
|
261
|
-
var _a;
|
|
262
|
-
const score = whiteScorePercent(move.whiteWins, move.draws, move.blackWins);
|
|
263
|
-
const selected = move.uci === selectedUci;
|
|
264
|
-
return (jsxRuntime.jsxs("tr", { onClick: () => onMoveSelect(move), style: {
|
|
265
|
-
cursor: "pointer",
|
|
266
|
-
background: selected ? "rgba(100,149,237,0.25)" : undefined,
|
|
267
|
-
}, children: [jsxRuntime.jsx("td", { style: tdStyle, children: move.san }), jsxRuntime.jsx("td", { style: tdStyle, children: move.games.toLocaleString() }), jsxRuntime.jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: score !== null ? `${score}` : "—" }), jsxRuntime.jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: (_a = move.avgElo) !== null && _a !== void 0 ? _a : "—" })] }, move.uci));
|
|
268
|
-
}) })] })] }));
|
|
269
|
-
|
|
270
|
-
const inputStyle = {
|
|
271
|
-
width: 64,
|
|
272
|
-
padding: "2px 6px",
|
|
273
|
-
fontSize: 12,
|
|
274
|
-
};
|
|
275
|
-
const rowStyle = {
|
|
276
|
-
display: "flex",
|
|
277
|
-
flexWrap: "wrap",
|
|
278
|
-
gap: 6,
|
|
279
|
-
alignItems: "center",
|
|
280
|
-
};
|
|
281
|
-
const EloRangeFilter = ({ minElo, maxElo, defaultMinElo, defaultMaxElo, onMinEloChange, onMaxEloChange, }) => (jsxRuntime.jsxs("div", { style: rowStyle, children: [jsxRuntime.jsx("span", { style: { fontWeight: 600 }, children: "Filter" }), jsxRuntime.jsx("input", { type: "number", value: minElo, min: 0, max: 3000, onChange: (e) => onMinEloChange(Number(e.target.value) || defaultMinElo), style: inputStyle, "aria-label": "Minimum Elo" }), jsxRuntime.jsx("span", { children: "\u2013" }), jsxRuntime.jsx("input", { type: "number", value: maxElo, min: 0, max: 3000, onChange: (e) => onMaxEloChange(Number(e.target.value) || defaultMaxElo), style: inputStyle, "aria-label": "Maximum Elo" })] }));
|
|
308
|
+
const MoveStatsTable = ({ moves, selectedUci, onMoveSelect, }) => (jsxRuntime.jsx("div", { style: moveStatsSectionStyle, children: jsxRuntime.jsxs("table", { style: tableStyle, children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { style: thStyle, children: "Move" }), jsxRuntime.jsx("th", { style: thStyle, children: "Games" }), jsxRuntime.jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Score %" }), jsxRuntime.jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Avg" })] }) }), jsxRuntime.jsx("tbody", { children: moves.map((move) => {
|
|
309
|
+
var _a;
|
|
310
|
+
const score = whiteScorePercent(move.whiteWins, move.draws, move.blackWins);
|
|
311
|
+
const selected = move.uci === selectedUci;
|
|
312
|
+
return (jsxRuntime.jsxs("tr", { onClick: () => onMoveSelect(move), style: {
|
|
313
|
+
cursor: "pointer",
|
|
314
|
+
background: selected ? "rgba(100,149,237,0.25)" : undefined,
|
|
315
|
+
}, children: [jsxRuntime.jsx("td", { style: tdStyle, children: move.san }), jsxRuntime.jsx("td", { style: tdStyle, children: move.games.toLocaleString() }), jsxRuntime.jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: score !== null ? `${score}` : "—" }), jsxRuntime.jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: (_a = move.avgElo) !== null && _a !== void 0 ? _a : "—" })] }, move.uci));
|
|
316
|
+
}) })] }) }));
|
|
282
317
|
|
|
283
318
|
const linkStyle = {
|
|
284
319
|
color: "inherit",
|
|
@@ -286,12 +321,24 @@ const linkStyle = {
|
|
|
286
321
|
textUnderlineOffset: 2,
|
|
287
322
|
};
|
|
288
323
|
const LichessGameLink = ({ url, name }) => (jsxRuntime.jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: name }));
|
|
289
|
-
const GamesTable = ({ games, onGameSelect }) => (jsxRuntime.jsxs("table", { style: tableStyle, children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { style: thStyle, children: "White" }), jsxRuntime.jsx("th", { style: thStyle, children: "Elo" }), jsxRuntime.jsx("th", { style: thStyle, children: "Black" }), jsxRuntime.jsx("th", { style: thStyle, children: "Elo" }), jsxRuntime.jsx("th", { style: thStyle, children: "Result" }), onGameSelect && jsxRuntime.jsx("th", { style: thStyle })] }) }), jsxRuntime.jsx("tbody", { children: games.length === 0 ? (jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { colSpan: onGameSelect ?
|
|
324
|
+
const GamesTable = ({ games, onGameSelect }) => (jsxRuntime.jsxs("table", { style: tableStyle, children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { style: thStyle, children: "White" }), jsxRuntime.jsx("th", { style: thStyle, children: "Elo" }), jsxRuntime.jsx("th", { style: thStyle, children: "Black" }), jsxRuntime.jsx("th", { style: thStyle, children: "Elo" }), jsxRuntime.jsx("th", { style: thStyle, children: "Result" }), jsxRuntime.jsx("th", { style: thStyle, children: "Source" }), onGameSelect && jsxRuntime.jsx("th", { style: thStyle })] }) }), jsxRuntime.jsx("tbody", { children: games.length === 0 ? (jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { colSpan: onGameSelect ? 7 : 6, style: Object.assign(Object.assign({}, tdStyle), { opacity: 0.7, fontStyle: "italic" }), children: "No games match this position and source filter." }) })) : (games.map((g) => (jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("td", { style: tdStyle, children: jsxRuntime.jsx(LichessGameLink, { url: g.url, name: g.white }) }), jsxRuntime.jsx("td", { style: tdStyle, children: g.whiteElo }), jsxRuntime.jsx("td", { style: tdStyle, children: jsxRuntime.jsx(LichessGameLink, { url: g.url, name: g.black }) }), jsxRuntime.jsx("td", { style: tdStyle, children: g.blackElo }), jsxRuntime.jsx("td", { style: tdStyle, children: g.result }), jsxRuntime.jsx("td", { style: tdStyle, children: g.source === "twic" ? "Master" : "Lichess" }), onGameSelect && (jsxRuntime.jsx("td", { style: tdStyle, children: jsxRuntime.jsx("button", { type: "button", onClick: () => onGameSelect(g), children: "Train" }) }))] }, g.gameId)))) })] }));
|
|
290
325
|
|
|
291
326
|
const mainLineTitleStyle = {
|
|
292
327
|
fontWeight: 600,
|
|
293
328
|
};
|
|
294
|
-
const PositionGamesPanel = ({ games, lineLabel,
|
|
329
|
+
const PositionGamesPanel = ({ games, lineLabel, lineSans: _lineSans, sources, onSourcesChange, onGameSelect, }) => {
|
|
330
|
+
const toggleSource = (source) => {
|
|
331
|
+
if (sources.includes(source)) {
|
|
332
|
+
if (sources.length === 1) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
onSourcesChange(sources.filter((value) => value !== source));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
onSourcesChange([...sources, source]);
|
|
339
|
+
};
|
|
340
|
+
return (jsxRuntime.jsxs("div", { style: gamesSectionStyle, children: [jsxRuntime.jsx("div", { style: gamesHeaderStyle, children: lineLabel ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { style: mainLineTitleStyle, children: "Main line: " }), lineLabel, jsxRuntime.jsxs("span", { style: { opacity: 0.75 }, children: [" (", games.length, " games)"] })] })) : (jsxRuntime.jsxs("span", { children: ["Games ", jsxRuntime.jsxs("span", { style: { opacity: 0.75 }, children: ["(", games.length, ")"] })] })) }), jsxRuntime.jsx("div", { style: gamesScrollStyle, children: jsxRuntime.jsx(GamesTable, { games: games, onGameSelect: onGameSelect }) }), jsxRuntime.jsxs("div", { style: gamesToolbarStyle, children: [jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: sources.includes("lichess"), onChange: () => toggleSource("lichess") }), "Lichess"] }), jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: sources.includes("twic"), onChange: () => toggleSource("twic") }), "Master"] })] })] }));
|
|
341
|
+
};
|
|
295
342
|
|
|
296
343
|
const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxRuntime.jsxs("div", { style: referenceShellStyle, children: [jsxRuntime.jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
|
|
297
344
|
const defaultRenderLayout = (props) => (jsxRuntime.jsx(DefaultReferenceLayout, Object.assign({}, props)));
|
|
@@ -303,34 +350,37 @@ function isVariationLineActive(line, selectedLineKey, forwardSans = []) {
|
|
|
303
350
|
if (forwardSans.length === 0) {
|
|
304
351
|
return false;
|
|
305
352
|
}
|
|
306
|
-
|
|
353
|
+
const moves = line.moves;
|
|
354
|
+
if (!Array.isArray(moves)) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return forwardSans.every((san, index) => { var _a; return ((_a = moves[index]) === null || _a === void 0 ? void 0 : _a.san) === san; });
|
|
307
358
|
}
|
|
308
359
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
})) })] }));
|
|
360
|
+
const DefaultVariationsStrip = ({ theme, lines, loading, selectedLineKey, forwardSans, onLineSelect, }) => (jsxRuntime.jsx("div", { style: Object.assign(Object.assign({}, variationsStripStyle), { flexDirection: "column", alignItems: "stretch", gap: 6 }), "data-theme": theme, children: jsxRuntime.jsx("div", { style: {
|
|
361
|
+
minWidth: 0,
|
|
362
|
+
maxHeight: 132,
|
|
363
|
+
overflow: "auto",
|
|
364
|
+
}, children: loading ? (jsxRuntime.jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "Loading\u2026" })) : lines.length === 0 ? (jsxRuntime.jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "No lines" })) : (lines.map((line) => {
|
|
365
|
+
var _a, _b;
|
|
366
|
+
const active = isVariationLineActive(line, selectedLineKey, forwardSans);
|
|
367
|
+
return (jsxRuntime.jsxs("button", { type: "button", onClick: () => onLineSelect(line), style: {
|
|
368
|
+
display: "flex",
|
|
369
|
+
width: "100%",
|
|
370
|
+
gap: 12,
|
|
371
|
+
alignItems: "baseline",
|
|
372
|
+
border: "none",
|
|
373
|
+
background: "transparent",
|
|
374
|
+
padding: "2px 0",
|
|
375
|
+
cursor: "pointer",
|
|
376
|
+
textAlign: "left",
|
|
377
|
+
color: active ? "#2e7d32" : "inherit",
|
|
378
|
+
font: "inherit",
|
|
379
|
+
fontSize: 12,
|
|
380
|
+
}, children: [jsxRuntime.jsx("span", { style: { flex: 1, minWidth: 0 }, children: line.label }), jsxRuntime.jsxs("span", { children: ["N = ", line.games.toLocaleString()] }), jsxRuntime.jsxs("span", { children: [(_a = line.scorePercent) !== null && _a !== void 0 ? _a : "—", "%"] }), jsxRuntime.jsx("span", { children: line.lastPlayedYear
|
|
381
|
+
? `Last played ${line.lastPlayedYear}`
|
|
382
|
+
: "Last played —" }), jsxRuntime.jsx("span", { children: (_b = line.avgElo) !== null && _b !== void 0 ? _b : "—" })] }, line.key));
|
|
383
|
+
})) }) }));
|
|
334
384
|
const defaultRenderVariationsStrip = (props) => jsxRuntime.jsx(DefaultVariationsStrip, Object.assign({}, props));
|
|
335
385
|
|
|
336
386
|
const defaultRenderStatus = (props) => (jsxRuntime.jsx(ExplorerStatusBanner, Object.assign({}, props)));
|
|
@@ -369,11 +419,105 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
369
419
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
370
420
|
};
|
|
371
421
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
422
|
+
const ALL_GAME_SOURCES = ["lichess", "twic"];
|
|
423
|
+
|
|
424
|
+
function useExplorerPrefetch({ fen, moves, positionReady, sources, fetchPositionGames, fetchPositionVariations, childCount = EXPLORER_PREFETCH_CHILD_COUNT, }) {
|
|
425
|
+
react.useEffect(() => {
|
|
426
|
+
if (!positionReady || moves.length === 0) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
let cancelled = false;
|
|
430
|
+
let idleHandle;
|
|
431
|
+
let deferTimer;
|
|
432
|
+
const prefetchChildren = () => {
|
|
433
|
+
if (cancelled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const sourcesParam = sources.length < ALL_GAME_SOURCES.length ? sources : undefined;
|
|
437
|
+
for (const move of moves.slice(0, childCount)) {
|
|
438
|
+
const childFen = fenAfterUci(fen, move.uci);
|
|
439
|
+
if (!childFen) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const gamesKey = gamesSessionKey({
|
|
443
|
+
fen: childFen,
|
|
444
|
+
sources: sourcesParam,
|
|
445
|
+
});
|
|
446
|
+
if (!peekSessionGames(gamesKey)) {
|
|
447
|
+
void fetchPositionGames({ fen: childFen, sources: sourcesParam })
|
|
448
|
+
.then((games) => {
|
|
449
|
+
if (!cancelled) {
|
|
450
|
+
setSessionGames(gamesKey, games);
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
.catch(() => undefined);
|
|
454
|
+
}
|
|
455
|
+
if (!fetchPositionVariations) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const variationsKey = variationsSessionKey(childFen);
|
|
459
|
+
if (!peekSessionVariations(variationsKey)) {
|
|
460
|
+
void fetchPositionVariations({
|
|
461
|
+
fen: childFen,
|
|
462
|
+
mode: "variations",
|
|
463
|
+
lineCount: EXPLORER_DEFAULT_VARIATION_LINE_COUNT,
|
|
464
|
+
depth: EXPLORER_DEFAULT_VARIATION_DEPTH,
|
|
465
|
+
})
|
|
466
|
+
.then((result) => {
|
|
467
|
+
var _a;
|
|
468
|
+
if (!cancelled) {
|
|
469
|
+
setSessionVariations(variationsKey, (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
.catch(() => undefined);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
477
|
+
idleHandle = window.requestIdleCallback(prefetchChildren, {
|
|
478
|
+
timeout: 1500,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
deferTimer = setTimeout(prefetchChildren, 750);
|
|
483
|
+
}
|
|
484
|
+
return () => {
|
|
485
|
+
cancelled = true;
|
|
486
|
+
if (idleHandle !== undefined) {
|
|
487
|
+
window.cancelIdleCallback(idleHandle);
|
|
488
|
+
}
|
|
489
|
+
if (deferTimer !== undefined) {
|
|
490
|
+
clearTimeout(deferTimer);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}, [
|
|
494
|
+
fen,
|
|
495
|
+
moves,
|
|
496
|
+
positionReady,
|
|
497
|
+
sources,
|
|
498
|
+
fetchPositionGames,
|
|
499
|
+
fetchPositionVariations,
|
|
500
|
+
childCount,
|
|
375
501
|
]);
|
|
376
|
-
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function initialHistoryState(initialFen, initialLineSans) {
|
|
505
|
+
if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
|
|
506
|
+
return { history: [{ fen: initialFen }], historyIndex: 0 };
|
|
507
|
+
}
|
|
508
|
+
const result = applyLineSans(initialFen, initialLineSans);
|
|
509
|
+
if (!result) {
|
|
510
|
+
return { history: [{ fen: initialFen }], historyIndex: 0 };
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
history: [{ fen: initialFen }, ...result.entries],
|
|
514
|
+
historyIndex: result.entries.length,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function usePositionHistory(initialFen, initialLineSans) {
|
|
518
|
+
var _a, _b;
|
|
519
|
+
const [history, setHistory] = react.useState(() => initialHistoryState(initialFen, initialLineSans).history);
|
|
520
|
+
const [historyIndex, setHistoryIndex] = react.useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
|
|
377
521
|
const canGoBack = historyIndex > 0;
|
|
378
522
|
const canGoForward = historyIndex < history.length - 1;
|
|
379
523
|
const pushEntry = react.useCallback((fen, lastSan) => {
|
|
@@ -401,6 +545,20 @@ function usePositionHistory(initialFen) {
|
|
|
401
545
|
setHistoryIndex(nextIndex);
|
|
402
546
|
return entry;
|
|
403
547
|
}, [history, historyIndex]);
|
|
548
|
+
const goFirst = react.useCallback(() => {
|
|
549
|
+
if (historyIndex <= 0)
|
|
550
|
+
return null;
|
|
551
|
+
const entry = history[0];
|
|
552
|
+
setHistoryIndex(0);
|
|
553
|
+
return entry;
|
|
554
|
+
}, [history, historyIndex]);
|
|
555
|
+
const goLast = react.useCallback(() => {
|
|
556
|
+
if (historyIndex >= history.length - 1)
|
|
557
|
+
return null;
|
|
558
|
+
const entry = history[history.length - 1];
|
|
559
|
+
setHistoryIndex(history.length - 1);
|
|
560
|
+
return entry;
|
|
561
|
+
}, [history, historyIndex]);
|
|
404
562
|
const resetHistory = react.useCallback((fen) => {
|
|
405
563
|
const entry = { fen };
|
|
406
564
|
setHistory([entry]);
|
|
@@ -425,38 +583,54 @@ function usePositionHistory(initialFen) {
|
|
|
425
583
|
.slice(historyIndex + 1)
|
|
426
584
|
.map((entry) => entry.lastSan)
|
|
427
585
|
.filter((san) => Boolean(san));
|
|
586
|
+
const currentFen = (_b = (_a = history[historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
|
|
587
|
+
const replaceLineEntries = react.useCallback((startFen, entries) => {
|
|
588
|
+
setHistory([{ fen: startFen }, ...entries]);
|
|
589
|
+
setHistoryIndex(entries.length);
|
|
590
|
+
}, []);
|
|
428
591
|
return {
|
|
429
592
|
canGoBack,
|
|
430
593
|
canGoForward,
|
|
431
594
|
lineSans,
|
|
432
595
|
forwardSans,
|
|
596
|
+
currentFen,
|
|
433
597
|
pushEntry,
|
|
434
598
|
pushEntries,
|
|
599
|
+
replaceLineEntries,
|
|
435
600
|
goBack,
|
|
436
601
|
goForward,
|
|
602
|
+
goFirst,
|
|
603
|
+
goLast,
|
|
437
604
|
resetHistory,
|
|
438
605
|
};
|
|
439
606
|
}
|
|
440
607
|
|
|
441
|
-
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames,
|
|
608
|
+
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
|
|
609
|
+
var _a, _b, _c;
|
|
442
610
|
const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
const
|
|
611
|
+
const bootstrappedRef = react.useRef(initialHistoryState(initialFen, initialLineSans));
|
|
612
|
+
const bootstrappedFen = (_b = (_a = bootstrappedRef.current.history[bootstrappedRef.current.historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
|
|
613
|
+
const initialGamesKey = gamesSessionKey({ fen: bootstrappedFen });
|
|
614
|
+
const initialCachedGames = peekSessionGames(initialGamesKey);
|
|
615
|
+
const [fen, setFen] = react.useState(bootstrappedFen);
|
|
616
|
+
/** Board display FEN; may lead {@link queryFen} while a variation line is animating. */
|
|
617
|
+
const [boardFen, setBoardFen] = react.useState(bootstrappedFen);
|
|
446
618
|
const [position, setPosition] = react.useState(null);
|
|
447
|
-
const [games, setGames] = react.useState(null);
|
|
619
|
+
const [games, setGames] = react.useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
|
|
448
620
|
/** Filter games to those that played this UCI from the current FEN (optional). */
|
|
449
621
|
const [gamesMoveFilterUci, setGamesMoveFilterUci] = react.useState();
|
|
450
|
-
const [
|
|
451
|
-
const [maxElo, setMaxElo] = react.useState(defaultMaxElo);
|
|
452
|
-
const [topOnly, setTopOnly] = react.useState(false);
|
|
622
|
+
const [sources, setSources] = react.useState([...ALL_GAME_SOURCES]);
|
|
453
623
|
const [loading, setLoading] = react.useState(false);
|
|
624
|
+
const [gamesLoading, setGamesLoading] = react.useState(() => initialCachedGames === undefined);
|
|
625
|
+
/** FEN that {@link position} was loaded for (may lag {@link fen} while fetching). */
|
|
626
|
+
const [loadedPositionFen, setLoadedPositionFen] = react.useState(null);
|
|
627
|
+
const [loadedPositionFilterKey, setLoadedPositionFilterKey] = react.useState(null);
|
|
454
628
|
const [error, setError] = react.useState(null);
|
|
455
|
-
const [variationsTab, setVariationsTab] = react.useState("variations");
|
|
456
629
|
const [selectedVariationKey, setSelectedVariationKey] = react.useState();
|
|
457
630
|
const variationAnimationTimerRef = react.useRef(null);
|
|
458
631
|
const isAnimatingVariationRef = react.useRef(false);
|
|
459
|
-
const readyForLineSyncRef = react.useRef(
|
|
632
|
+
const readyForLineSyncRef = react.useRef(bootstrappedRef.current.historyIndex > 0 ||
|
|
633
|
+
((_c = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) !== null && _c !== void 0 ? _c : 0) === 0);
|
|
460
634
|
const cancelVariationAnimation = react.useCallback(() => {
|
|
461
635
|
if (variationAnimationTimerRef.current !== null) {
|
|
462
636
|
clearTimeout(variationAnimationTimerRef.current);
|
|
@@ -464,7 +638,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
464
638
|
}
|
|
465
639
|
isAnimatingVariationRef.current = false;
|
|
466
640
|
}, []);
|
|
467
|
-
const { canGoBack, canGoForward, lineSans, forwardSans, pushEntry, pushEntries, goBack, goForward, resetHistory, } = usePositionHistory(initialFen);
|
|
641
|
+
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
|
|
642
|
+
/** FEN used for explorer API queries — follows move history, not animation frames. */
|
|
643
|
+
const queryFen = currentFen;
|
|
644
|
+
const initialLineKey = react.useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
|
|
645
|
+
const expectedLineFen = react.useMemo(() => {
|
|
646
|
+
var _a, _b;
|
|
647
|
+
if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
|
|
648
|
+
return EXPLORER_START_FEN;
|
|
649
|
+
}
|
|
650
|
+
return ((_b = (_a = applyLineSans(EXPLORER_START_FEN, initialLineSans)) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : EXPLORER_START_FEN);
|
|
651
|
+
}, [initialLineKey, initialLineSans]);
|
|
468
652
|
const applyNavigation = react.useCallback((nextFen, clearMoveFilter = true) => {
|
|
469
653
|
setFen(nextFen);
|
|
470
654
|
setBoardFen(nextFen);
|
|
@@ -473,39 +657,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
473
657
|
}
|
|
474
658
|
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
|
|
475
659
|
}, [onFenChange]);
|
|
476
|
-
const
|
|
477
|
-
|
|
660
|
+
const queryFenRef = react.useRef(queryFen);
|
|
661
|
+
queryFenRef.current = queryFen;
|
|
662
|
+
const positionsByFenRef = react.useRef(new Map());
|
|
663
|
+
const positionCacheKey = react.useCallback((fenValue) => JSON.stringify({
|
|
664
|
+
fen: fenValue,
|
|
665
|
+
sources: sources.length < ALL_GAME_SOURCES.length
|
|
666
|
+
? sources.slice().sort().join(",")
|
|
667
|
+
: "",
|
|
668
|
+
}), [sources]);
|
|
669
|
+
const gamesCacheKey = react.useCallback((fenValue, uci) => gamesSessionKey({
|
|
670
|
+
fen: fenValue,
|
|
671
|
+
uci,
|
|
672
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
673
|
+
}), [sources]);
|
|
674
|
+
const lastAppliedLineKeyRef = react.useRef(initialLineKey);
|
|
478
675
|
react.useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
|
|
676
|
+
// Apply URL line changes only when the external line key changes — not when the
|
|
677
|
+
// user clicks moves (internal lineSans updates must not re-sync from stale props).
|
|
479
678
|
react.useEffect(() => {
|
|
480
|
-
if (
|
|
481
|
-
return;
|
|
482
|
-
const currentLineKey = lineSans.join("|");
|
|
483
|
-
if (currentLineKey === initialLineKey) {
|
|
484
|
-
lastAppliedLineKeyRef.current = initialLineKey;
|
|
679
|
+
if (initialLineSans === undefined && onLineSansChange === undefined)
|
|
485
680
|
return;
|
|
681
|
+
const fenMatches = normalizeFen(queryFen) === normalizeFen(expectedLineFen);
|
|
682
|
+
if (lastAppliedLineKeyRef.current === initialLineKey) {
|
|
683
|
+
if (fenMatches)
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const currentLineKey = lineSans.join("|");
|
|
688
|
+
if (currentLineKey === initialLineKey && fenMatches) {
|
|
689
|
+
lastAppliedLineKeyRef.current = initialLineKey;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
486
692
|
}
|
|
487
693
|
lastAppliedLineKeyRef.current = initialLineKey;
|
|
488
694
|
cancelVariationAnimation();
|
|
489
|
-
resetHistory(EXPLORER_START_FEN);
|
|
490
695
|
setSelectedVariationKey(undefined);
|
|
491
696
|
if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
|
|
492
697
|
const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
|
|
493
698
|
if (result) {
|
|
494
|
-
|
|
699
|
+
replaceLineEntries(EXPLORER_START_FEN, result.entries);
|
|
495
700
|
applyNavigation(result.fen, true);
|
|
496
701
|
return;
|
|
497
702
|
}
|
|
498
703
|
}
|
|
704
|
+
replaceLineEntries(EXPLORER_START_FEN, []);
|
|
499
705
|
applyNavigation(EXPLORER_START_FEN, true);
|
|
706
|
+
// lineSans / queryFen intentionally omitted — only re-sync when the URL line key changes.
|
|
707
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
500
708
|
}, [
|
|
501
709
|
initialLineKey,
|
|
502
710
|
initialLineSans,
|
|
503
|
-
|
|
711
|
+
expectedLineFen,
|
|
504
712
|
cancelVariationAnimation,
|
|
505
|
-
|
|
506
|
-
pushEntries,
|
|
713
|
+
replaceLineEntries,
|
|
507
714
|
applyNavigation,
|
|
508
715
|
]);
|
|
716
|
+
// Keep legacy fen state aligned with move history when they drift apart.
|
|
717
|
+
react.useEffect(() => {
|
|
718
|
+
if (isAnimatingVariationRef.current)
|
|
719
|
+
return;
|
|
720
|
+
if (normalizeFen(fen) === normalizeFen(queryFen))
|
|
721
|
+
return;
|
|
722
|
+
setFen(queryFen);
|
|
723
|
+
setBoardFen(queryFen);
|
|
724
|
+
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(queryFen);
|
|
725
|
+
}, [fen, queryFen, onFenChange]);
|
|
509
726
|
react.useEffect(() => {
|
|
510
727
|
if (readyForLineSyncRef.current)
|
|
511
728
|
return;
|
|
@@ -517,88 +734,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
517
734
|
}
|
|
518
735
|
}, [lineSans, initialLineSans]);
|
|
519
736
|
react.useEffect(() => {
|
|
737
|
+
if (!onLineSansChange)
|
|
738
|
+
return;
|
|
520
739
|
if (!readyForLineSyncRef.current)
|
|
521
740
|
return;
|
|
522
|
-
onLineSansChange
|
|
741
|
+
onLineSansChange(lineSans);
|
|
523
742
|
}, [lineSans, onLineSansChange]);
|
|
524
743
|
react.useEffect(() => {
|
|
525
|
-
if (fenProp === undefined
|
|
744
|
+
if (fenProp === undefined)
|
|
745
|
+
return;
|
|
746
|
+
if (fenProp === queryFenRef.current)
|
|
526
747
|
return;
|
|
527
748
|
cancelVariationAnimation();
|
|
528
749
|
resetHistory(fenProp);
|
|
529
750
|
applyNavigation(fenProp, true);
|
|
530
|
-
}, [fenProp,
|
|
751
|
+
}, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
|
|
531
752
|
react.useEffect(() => {
|
|
532
753
|
let cancelled = false;
|
|
533
|
-
|
|
534
|
-
|
|
754
|
+
const requestedFen = queryFen;
|
|
755
|
+
const requestedFilterKey = positionCacheKey(requestedFen);
|
|
756
|
+
if (positionsByFenRef.current.has(requestedFilterKey)) {
|
|
757
|
+
setPosition(positionsByFenRef.current.get(requestedFilterKey));
|
|
758
|
+
setLoadedPositionFen(requestedFen);
|
|
759
|
+
setLoadedPositionFilterKey(requestedFilterKey);
|
|
760
|
+
setLoading(false);
|
|
761
|
+
setError(null);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
setLoading(true);
|
|
765
|
+
setError(null);
|
|
766
|
+
}
|
|
535
767
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
536
768
|
try {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
maxElo,
|
|
543
|
-
uci: gamesMoveFilterUci,
|
|
544
|
-
topOnly,
|
|
545
|
-
}),
|
|
546
|
-
]);
|
|
547
|
-
if (cancelled)
|
|
769
|
+
const pos = yield fetchPosition({
|
|
770
|
+
fen: requestedFen,
|
|
771
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
772
|
+
});
|
|
773
|
+
if (cancelled || requestedFen !== queryFenRef.current)
|
|
548
774
|
return;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (!pos) {
|
|
552
|
-
setError("No explorer data for this position yet");
|
|
775
|
+
if (pos) {
|
|
776
|
+
positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
|
|
553
777
|
}
|
|
778
|
+
setPosition(pos);
|
|
779
|
+
setLoadedPositionFen(requestedFen);
|
|
780
|
+
setLoadedPositionFilterKey(positionCacheKey(requestedFen));
|
|
554
781
|
}
|
|
555
782
|
catch (e) {
|
|
783
|
+
if (cancelled || requestedFen !== queryFenRef.current)
|
|
784
|
+
return;
|
|
785
|
+
setPosition(null);
|
|
786
|
+
setLoadedPositionFen(null);
|
|
787
|
+
setLoadedPositionFilterKey(null);
|
|
788
|
+
setError(e instanceof Error ? e.message : "Failed to load explorer data");
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
if (!cancelled && requestedFen === queryFenRef.current) {
|
|
792
|
+
setLoading(false);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}))();
|
|
796
|
+
return () => {
|
|
797
|
+
cancelled = true;
|
|
798
|
+
};
|
|
799
|
+
}, [queryFen, sources, fetchPosition, positionCacheKey]);
|
|
800
|
+
const activePositionFilterKey = positionCacheKey(queryFen);
|
|
801
|
+
const cachedPosition = positionsByFenRef.current.get(activePositionFilterKey);
|
|
802
|
+
const positionReady = loadedPositionFen === queryFen &&
|
|
803
|
+
loadedPositionFilterKey === activePositionFilterKey;
|
|
804
|
+
const displayPosition = cachedPosition !== null && cachedPosition !== void 0 ? cachedPosition : (positionReady ? position : null);
|
|
805
|
+
const displayMoves = Array.isArray(displayPosition === null || displayPosition === void 0 ? void 0 : displayPosition.moves)
|
|
806
|
+
? displayPosition.moves
|
|
807
|
+
: [];
|
|
808
|
+
const showPositionLoading = loading && displayMoves.length === 0;
|
|
809
|
+
const isStartPosition = normalizeFen(queryFen) === normalizeFen(EXPLORER_START_FEN);
|
|
810
|
+
useExplorerPrefetch({
|
|
811
|
+
fen: queryFen,
|
|
812
|
+
moves: displayMoves,
|
|
813
|
+
positionReady,
|
|
814
|
+
sources,
|
|
815
|
+
fetchPositionGames,
|
|
816
|
+
fetchPositionVariations,
|
|
817
|
+
});
|
|
818
|
+
react.useEffect(() => {
|
|
819
|
+
return subscribeExplorerSessionCache(() => {
|
|
820
|
+
const key = gamesCacheKey(queryFen, gamesMoveFilterUci);
|
|
821
|
+
const cached = peekSessionGames(key);
|
|
822
|
+
if (!cached) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
setGames(cached);
|
|
826
|
+
setGamesLoading(false);
|
|
827
|
+
});
|
|
828
|
+
}, [queryFen, gamesMoveFilterUci, gamesCacheKey]);
|
|
829
|
+
react.useEffect(() => {
|
|
830
|
+
const canLoadGames = positionReady || (isStartPosition && !gamesMoveFilterUci);
|
|
831
|
+
if (!canLoadGames)
|
|
832
|
+
return;
|
|
833
|
+
let cancelled = false;
|
|
834
|
+
const requestedKey = gamesCacheKey(queryFen, gamesMoveFilterUci);
|
|
835
|
+
const cachedGames = peekSessionGames(requestedKey);
|
|
836
|
+
if (cachedGames) {
|
|
837
|
+
setGames(cachedGames);
|
|
838
|
+
setGamesLoading(false);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
setGamesLoading(true);
|
|
842
|
+
}
|
|
843
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
844
|
+
try {
|
|
845
|
+
const gameList = yield fetchPositionGames({
|
|
846
|
+
fen: queryFen,
|
|
847
|
+
uci: gamesMoveFilterUci,
|
|
848
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
849
|
+
});
|
|
850
|
+
if (cancelled)
|
|
851
|
+
return;
|
|
852
|
+
setSessionGames(requestedKey, gameList);
|
|
853
|
+
setGames(gameList);
|
|
854
|
+
}
|
|
855
|
+
catch (_a) {
|
|
556
856
|
if (!cancelled) {
|
|
557
|
-
|
|
857
|
+
setGames(null);
|
|
558
858
|
}
|
|
559
859
|
}
|
|
560
860
|
finally {
|
|
561
861
|
if (!cancelled)
|
|
562
|
-
|
|
862
|
+
setGamesLoading(false);
|
|
563
863
|
}
|
|
564
864
|
}))();
|
|
565
865
|
return () => {
|
|
566
866
|
cancelled = true;
|
|
567
867
|
};
|
|
568
868
|
}, [
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
869
|
+
queryFen,
|
|
870
|
+
positionReady,
|
|
871
|
+
isStartPosition,
|
|
572
872
|
gamesMoveFilterUci,
|
|
573
|
-
|
|
574
|
-
fetchPosition,
|
|
873
|
+
sources,
|
|
575
874
|
fetchPositionGames,
|
|
875
|
+
gamesCacheKey,
|
|
576
876
|
]);
|
|
577
877
|
const handleMoveSelect = react.useCallback((move) => {
|
|
578
878
|
if (isAnimatingVariationRef.current) {
|
|
579
879
|
cancelVariationAnimation();
|
|
580
|
-
setBoardFen(
|
|
880
|
+
setBoardFen(queryFen);
|
|
581
881
|
setSelectedVariationKey(undefined);
|
|
582
882
|
}
|
|
583
|
-
const nextFen = fenAfterUci(
|
|
883
|
+
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
584
884
|
if (!nextFen)
|
|
585
885
|
return;
|
|
586
886
|
pushEntry(nextFen, move.san);
|
|
587
887
|
setSelectedVariationKey(undefined);
|
|
588
888
|
applyNavigation(nextFen, true);
|
|
589
|
-
}, [
|
|
889
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
590
890
|
const handleLineSelect = react.useCallback((line) => {
|
|
591
891
|
if (isAnimatingVariationRef.current)
|
|
592
892
|
return;
|
|
593
893
|
cancelVariationAnimation();
|
|
594
|
-
let
|
|
894
|
+
let lineFen = queryFen;
|
|
595
895
|
const entries = [];
|
|
596
896
|
for (let i = 0; i < line.uciPath.length; i += 1) {
|
|
597
|
-
const nextFen = fenAfterUci(
|
|
897
|
+
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
598
898
|
if (!nextFen)
|
|
599
899
|
return;
|
|
600
900
|
entries.push({ fen: nextFen, lastSan: line.moves[i].san });
|
|
601
|
-
|
|
901
|
+
lineFen = nextFen;
|
|
602
902
|
}
|
|
603
903
|
if (entries.length === 0)
|
|
604
904
|
return;
|
|
@@ -622,107 +922,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
622
922
|
}
|
|
623
923
|
};
|
|
624
924
|
tick();
|
|
625
|
-
}, [
|
|
925
|
+
}, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
|
|
626
926
|
const handlePieceDrop = react.useCallback((sourceSquare, targetSquare, piece) => {
|
|
627
927
|
if (isAnimatingVariationRef.current) {
|
|
628
928
|
cancelVariationAnimation();
|
|
629
|
-
setBoardFen(
|
|
929
|
+
setBoardFen(queryFen);
|
|
630
930
|
setSelectedVariationKey(undefined);
|
|
631
931
|
}
|
|
632
|
-
const result = applyBoardMove(
|
|
932
|
+
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
633
933
|
if (!result)
|
|
634
934
|
return false;
|
|
635
935
|
pushEntry(result.fen, result.san);
|
|
636
936
|
setSelectedVariationKey(undefined);
|
|
637
937
|
applyNavigation(result.fen, true);
|
|
638
938
|
return true;
|
|
639
|
-
}, [
|
|
939
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
640
940
|
const handleBack = react.useCallback(() => {
|
|
641
941
|
if (isAnimatingVariationRef.current) {
|
|
642
942
|
cancelVariationAnimation();
|
|
643
|
-
setBoardFen(
|
|
943
|
+
setBoardFen(queryFen);
|
|
644
944
|
setSelectedVariationKey(undefined);
|
|
645
945
|
return;
|
|
646
946
|
}
|
|
647
947
|
const entry = goBack();
|
|
648
948
|
if (entry)
|
|
649
949
|
applyNavigation(entry.fen, true);
|
|
650
|
-
}, [
|
|
950
|
+
}, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
|
|
651
951
|
const handleForward = react.useCallback(() => {
|
|
652
952
|
if (isAnimatingVariationRef.current) {
|
|
653
953
|
cancelVariationAnimation();
|
|
654
|
-
setBoardFen(
|
|
954
|
+
setBoardFen(queryFen);
|
|
655
955
|
setSelectedVariationKey(undefined);
|
|
656
956
|
return;
|
|
657
957
|
}
|
|
658
958
|
const entry = goForward();
|
|
659
959
|
if (entry)
|
|
660
960
|
applyNavigation(entry.fen, true);
|
|
661
|
-
}, [
|
|
961
|
+
}, [queryFen, goForward, applyNavigation, cancelVariationAnimation]);
|
|
962
|
+
const handleFirst = react.useCallback(() => {
|
|
963
|
+
if (isAnimatingVariationRef.current) {
|
|
964
|
+
cancelVariationAnimation();
|
|
965
|
+
setBoardFen(queryFen);
|
|
966
|
+
setSelectedVariationKey(undefined);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const entry = goFirst();
|
|
970
|
+
if (entry)
|
|
971
|
+
applyNavigation(entry.fen, true);
|
|
972
|
+
}, [queryFen, goFirst, applyNavigation, cancelVariationAnimation]);
|
|
973
|
+
const handleLast = react.useCallback(() => {
|
|
974
|
+
if (isAnimatingVariationRef.current) {
|
|
975
|
+
cancelVariationAnimation();
|
|
976
|
+
setBoardFen(queryFen);
|
|
977
|
+
setSelectedVariationKey(undefined);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const entry = goLast();
|
|
981
|
+
if (entry)
|
|
982
|
+
applyNavigation(entry.fen, true);
|
|
983
|
+
}, [queryFen, goLast, applyNavigation, cancelVariationAnimation]);
|
|
662
984
|
const lineLabel = react.useMemo(() => {
|
|
663
985
|
if (lineSans.length > 0) {
|
|
664
|
-
return lineSans
|
|
986
|
+
return formatNumberedLineSans(lineSans);
|
|
665
987
|
}
|
|
666
|
-
if (position) {
|
|
988
|
+
if (position && typeof position.totalGames === "number") {
|
|
667
989
|
return `Starting position (${position.totalGames.toLocaleString()} games)`;
|
|
668
990
|
}
|
|
669
991
|
return "";
|
|
670
992
|
}, [lineSans, position]);
|
|
671
993
|
return {
|
|
672
|
-
fen,
|
|
994
|
+
fen: queryFen,
|
|
673
995
|
boardFen,
|
|
674
996
|
position,
|
|
675
997
|
games,
|
|
676
998
|
gamesMoveFilterUci,
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
topOnly,
|
|
999
|
+
sources,
|
|
1000
|
+
lineSans,
|
|
680
1001
|
loading,
|
|
1002
|
+
showPositionLoading,
|
|
1003
|
+
gamesLoading,
|
|
1004
|
+
positionReady,
|
|
1005
|
+
displayMoves,
|
|
681
1006
|
error,
|
|
682
1007
|
lineLabel,
|
|
683
1008
|
canGoBack,
|
|
684
1009
|
canGoForward,
|
|
685
|
-
variationsTab,
|
|
686
1010
|
forwardSans,
|
|
687
1011
|
selectedVariationKey,
|
|
688
|
-
|
|
689
|
-
setMaxElo,
|
|
690
|
-
setTopOnly,
|
|
691
|
-
setVariationsTab,
|
|
1012
|
+
setSources,
|
|
692
1013
|
setGamesMoveFilterUci,
|
|
693
1014
|
handleMoveSelect,
|
|
694
1015
|
handleLineSelect,
|
|
695
1016
|
handlePieceDrop,
|
|
696
1017
|
handleBack,
|
|
697
1018
|
handleForward,
|
|
1019
|
+
handleFirst,
|
|
1020
|
+
handleLast,
|
|
698
1021
|
};
|
|
699
1022
|
}
|
|
700
1023
|
|
|
701
|
-
function useVariationLines({ fen,
|
|
702
|
-
const
|
|
703
|
-
const [
|
|
1024
|
+
function useVariationLines({ fen, fetchPositionVariations, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH, enabled = true, }) {
|
|
1025
|
+
const cacheKey = react.useMemo(() => variationsSessionKey(fen, lineCount, lineDepth), [fen, lineCount, lineDepth]);
|
|
1026
|
+
const [lines, setLines] = react.useState(() => {
|
|
1027
|
+
var _a;
|
|
1028
|
+
if (!enabled) {
|
|
1029
|
+
return [];
|
|
1030
|
+
}
|
|
1031
|
+
return (_a = peekSessionVariations(cacheKey)) !== null && _a !== void 0 ? _a : [];
|
|
1032
|
+
});
|
|
1033
|
+
const [loading, setLoading] = react.useState(() => {
|
|
1034
|
+
if (!enabled) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
return peekSessionVariations(cacheKey) === undefined;
|
|
1038
|
+
});
|
|
1039
|
+
react.useEffect(() => {
|
|
1040
|
+
return subscribeExplorerSessionCache(() => {
|
|
1041
|
+
if (!enabled) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const cachedLines = peekSessionVariations(cacheKey);
|
|
1045
|
+
if (!cachedLines) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
setLines(cachedLines);
|
|
1049
|
+
setLoading(false);
|
|
1050
|
+
});
|
|
1051
|
+
}, [enabled, cacheKey]);
|
|
704
1052
|
react.useEffect(() => {
|
|
705
|
-
if (!enabled
|
|
1053
|
+
if (!enabled) {
|
|
706
1054
|
setLines([]);
|
|
707
1055
|
setLoading(false);
|
|
708
1056
|
return;
|
|
709
1057
|
}
|
|
710
1058
|
let cancelled = false;
|
|
711
|
-
|
|
712
|
-
|
|
1059
|
+
const cachedLines = peekSessionVariations(cacheKey);
|
|
1060
|
+
if (cachedLines) {
|
|
1061
|
+
setLines(cachedLines);
|
|
1062
|
+
setLoading(false);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
setLoading(true);
|
|
1066
|
+
}
|
|
1067
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
713
1068
|
var _a;
|
|
714
1069
|
try {
|
|
715
1070
|
const result = yield fetchPositionVariations({
|
|
716
1071
|
fen,
|
|
717
|
-
mode:
|
|
718
|
-
minElo,
|
|
719
|
-
maxElo,
|
|
1072
|
+
mode: "variations",
|
|
720
1073
|
lineCount,
|
|
721
1074
|
depth: lineDepth,
|
|
722
1075
|
});
|
|
723
1076
|
if (cancelled)
|
|
724
1077
|
return;
|
|
725
|
-
|
|
1078
|
+
const nextLines = (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : [];
|
|
1079
|
+
setSessionVariations(cacheKey, nextLines);
|
|
1080
|
+
setLines(nextLines);
|
|
726
1081
|
}
|
|
727
1082
|
catch (_b) {
|
|
728
1083
|
if (!cancelled) {
|
|
@@ -738,21 +1093,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
|
|
|
738
1093
|
return () => {
|
|
739
1094
|
cancelled = true;
|
|
740
1095
|
};
|
|
741
|
-
}, [
|
|
742
|
-
|
|
743
|
-
fen,
|
|
744
|
-
tab,
|
|
745
|
-
minElo,
|
|
746
|
-
maxElo,
|
|
747
|
-
fetchPositionVariations,
|
|
748
|
-
lineCount,
|
|
749
|
-
lineDepth,
|
|
750
|
-
]);
|
|
751
|
-
return { lines, loading };
|
|
1096
|
+
}, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
|
|
1097
|
+
return { lines, loading: enabled ? loading : false };
|
|
752
1098
|
}
|
|
753
1099
|
|
|
754
|
-
const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth,
|
|
755
|
-
var _a, _b;
|
|
1100
|
+
const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardTheme, boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth, boardOrientation: boardOrientationProp, defaultBoardOrientation = "white", onBoardOrientationChange, fillHeight = true, layoutMinHeight, renderLayout = defaultRenderLayout, renderStatus = defaultRenderStatus, renderMoveStats = defaultRenderMoveStats, renderVariationsStrip = defaultRenderVariationsStrip, renderGamesPanel = defaultRenderGamesPanel, renderBoardNav = defaultRenderBoardNav, onGameSelect, keyboardNav = true, }) => {
|
|
756
1101
|
const referenceData = usePositionReferenceData({
|
|
757
1102
|
fenProp,
|
|
758
1103
|
onFenChange,
|
|
@@ -760,57 +1105,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
760
1105
|
onLineSansChange,
|
|
761
1106
|
fetchPosition,
|
|
762
1107
|
fetchPositionGames,
|
|
763
|
-
|
|
764
|
-
|
|
1108
|
+
fetchPositionVariations,
|
|
1109
|
+
});
|
|
1110
|
+
const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
|
|
1111
|
+
reactChessCore.usePositionKeyboardNav({
|
|
1112
|
+
enabled: keyboardNav,
|
|
1113
|
+
canPrev: canGoBack,
|
|
1114
|
+
canNext: canGoForward,
|
|
1115
|
+
onPrev: handleBack,
|
|
1116
|
+
onNext: handleForward,
|
|
1117
|
+
onFirst: handleFirst,
|
|
1118
|
+
onLast: handleLast,
|
|
765
1119
|
});
|
|
766
|
-
const
|
|
1120
|
+
const [variationsEnabled, setVariationsEnabled] = react.useState(() => normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN));
|
|
1121
|
+
const isStartPosition = normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN);
|
|
1122
|
+
react.useEffect(() => {
|
|
1123
|
+
const cachedVariations = peekSessionVariations(variationsSessionKey(fen, EXPLORER_DEFAULT_VARIATION_LINE_COUNT, EXPLORER_DEFAULT_VARIATION_DEPTH));
|
|
1124
|
+
if (cachedVariations) {
|
|
1125
|
+
setVariationsEnabled(true);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (!positionReady && !isStartPosition) {
|
|
1129
|
+
setVariationsEnabled(false);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (loading && !isStartPosition) {
|
|
1133
|
+
setVariationsEnabled(false);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
setVariationsEnabled(true);
|
|
1137
|
+
}, [fen, positionReady, loading, isStartPosition]);
|
|
767
1138
|
const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
|
|
768
1139
|
fen,
|
|
769
|
-
tab: variationsTab,
|
|
770
|
-
minElo,
|
|
771
|
-
maxElo,
|
|
772
1140
|
fetchPositionVariations,
|
|
773
|
-
enabled:
|
|
1141
|
+
enabled: variationsEnabled && (positionReady || isStartPosition),
|
|
774
1142
|
});
|
|
1143
|
+
const [internalBoardOrientation, setInternalBoardOrientation] = react.useState(defaultBoardOrientation);
|
|
1144
|
+
const boardOrientation = boardOrientationProp !== null && boardOrientationProp !== void 0 ? boardOrientationProp : internalBoardOrientation;
|
|
1145
|
+
const handleFlipBoard = react.useCallback(() => {
|
|
1146
|
+
const nextOrientation = boardOrientation === "white" ? "black" : "white";
|
|
1147
|
+
if (boardOrientationProp === undefined) {
|
|
1148
|
+
setInternalBoardOrientation(nextOrientation);
|
|
1149
|
+
}
|
|
1150
|
+
onBoardOrientationChange === null || onBoardOrientationChange === void 0 ? void 0 : onBoardOrientationChange(nextOrientation);
|
|
1151
|
+
}, [boardOrientation, boardOrientationProp, onBoardOrientationChange]);
|
|
1152
|
+
const boardNavProps = react.useMemo(() => ({
|
|
1153
|
+
canGoBack,
|
|
1154
|
+
canGoForward,
|
|
1155
|
+
onBack: handleBack,
|
|
1156
|
+
onForward: handleForward,
|
|
1157
|
+
boardOrientation,
|
|
1158
|
+
onFlipBoard: handleFlipBoard,
|
|
1159
|
+
}), [
|
|
1160
|
+
boardOrientation,
|
|
1161
|
+
canGoBack,
|
|
1162
|
+
canGoForward,
|
|
1163
|
+
handleBack,
|
|
1164
|
+
handleFlipBoard,
|
|
1165
|
+
handleForward,
|
|
1166
|
+
]);
|
|
775
1167
|
const outerStyle = {
|
|
776
1168
|
width: "100%",
|
|
777
1169
|
height: fillHeight ? "100%" : "auto",
|
|
778
1170
|
minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
|
|
779
|
-
overflow: "hidden",
|
|
1171
|
+
overflow: fillHeight ? "hidden" : "visible",
|
|
780
1172
|
boxSizing: "border-box",
|
|
781
1173
|
};
|
|
782
|
-
const board = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, status: renderStatus({ error, loading }), moveStats: renderMoveStats({
|
|
789
|
-
moves: (_a = position === null || position === void 0 ? void 0 : position.moves) !== null && _a !== void 0 ? _a : [],
|
|
1174
|
+
const board = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
|
|
1175
|
+
const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
|
|
1176
|
+
error,
|
|
1177
|
+
loading: showPositionLoading,
|
|
1178
|
+
}), moveStats: renderMoveStats({
|
|
1179
|
+
moves: displayMoves,
|
|
790
1180
|
onMoveSelect: handleMoveSelect,
|
|
791
1181
|
}), variationsStrip: renderVariationsStrip({
|
|
792
1182
|
theme,
|
|
793
|
-
tab: variationsTab,
|
|
794
|
-
onTabChange: setVariationsTab,
|
|
795
1183
|
lines: variationLines,
|
|
796
|
-
loading: variationLinesLoading,
|
|
1184
|
+
loading: !variationsEnabled || variationLinesLoading,
|
|
797
1185
|
selectedLineKey: selectedVariationKey,
|
|
798
1186
|
forwardSans,
|
|
799
1187
|
onLineSelect: handleLineSelect,
|
|
800
1188
|
}), gamesPanel: renderGamesPanel({
|
|
801
|
-
games: (
|
|
1189
|
+
games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
|
|
1190
|
+
loading: gamesLoading,
|
|
802
1191
|
lineLabel,
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
defaultMaxElo,
|
|
807
|
-
topOnly,
|
|
808
|
-
onMinEloChange: setMinElo,
|
|
809
|
-
onMaxEloChange: setMaxElo,
|
|
810
|
-
onTopOnlyChange: setTopOnly,
|
|
1192
|
+
lineSans,
|
|
1193
|
+
sources,
|
|
1194
|
+
onSourcesChange: setSources,
|
|
811
1195
|
onGameSelect,
|
|
812
1196
|
}) }));
|
|
813
|
-
return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, children: jsxRuntime.jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
|
|
1197
|
+
return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
|
|
814
1198
|
};
|
|
815
1199
|
|
|
816
1200
|
/** Reference explorer with library default layout and renderers (ChessBase-style grid). */
|
|
@@ -982,7 +1366,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
|
|
|
982
1366
|
!error &&
|
|
983
1367
|
!complete &&
|
|
984
1368
|
feedback === "incorrect" &&
|
|
985
|
-
lastExpectedSan && (jsxRuntime.jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxRuntime.jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsxRuntime.jsx(
|
|
1369
|
+
lastExpectedSan && (jsxRuntime.jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxRuntime.jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: fen, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }) }), !complete && !loading && !error && (jsxRuntime.jsx("button", { type: "button", style: defaultButtonStyle, onClick: revealMove, children: "Show move" }))] }) }));
|
|
986
1370
|
};
|
|
987
1371
|
|
|
988
1372
|
const mockPosition = {
|
|
@@ -1018,14 +1402,9 @@ const mockPosition = {
|
|
|
1018
1402
|
avgElo: 2435,
|
|
1019
1403
|
},
|
|
1020
1404
|
],
|
|
1021
|
-
sampleGameIds: ["abc123", "def456"],
|
|
1022
1405
|
};
|
|
1023
1406
|
const mockGames = {
|
|
1024
1407
|
positionKey: "mock-start",
|
|
1025
|
-
fen: EXPLORER_START_FEN,
|
|
1026
|
-
minElo: 2200,
|
|
1027
|
-
maxElo: 2800,
|
|
1028
|
-
topOnly: false,
|
|
1029
1408
|
games: [
|
|
1030
1409
|
{
|
|
1031
1410
|
gameId: "abc123",
|
|
@@ -1039,6 +1418,7 @@ const mockGames = {
|
|
|
1039
1418
|
nextSan: "e4",
|
|
1040
1419
|
nextUci: "e2e4",
|
|
1041
1420
|
avgElo: 2843,
|
|
1421
|
+
source: "lichess",
|
|
1042
1422
|
},
|
|
1043
1423
|
{
|
|
1044
1424
|
gameId: "def456",
|
|
@@ -1052,10 +1432,11 @@ const mockGames = {
|
|
|
1052
1432
|
nextSan: "d4",
|
|
1053
1433
|
nextUci: "d2d4",
|
|
1054
1434
|
avgElo: 2778,
|
|
1435
|
+
source: "twic",
|
|
1055
1436
|
},
|
|
1056
1437
|
],
|
|
1057
1438
|
};
|
|
1058
|
-
function mockFetchPosition(
|
|
1439
|
+
function mockFetchPosition(params) {
|
|
1059
1440
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1060
1441
|
return mockPosition;
|
|
1061
1442
|
});
|
|
@@ -1089,7 +1470,11 @@ function mockVariationLines(params, position) {
|
|
|
1089
1470
|
}
|
|
1090
1471
|
function mockFetchPositionVariations(params) {
|
|
1091
1472
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1092
|
-
const position = yield mockFetchPosition(
|
|
1473
|
+
const position = yield mockFetchPosition({
|
|
1474
|
+
fen: params.fen,
|
|
1475
|
+
minElo: params.minElo,
|
|
1476
|
+
maxElo: params.maxElo,
|
|
1477
|
+
});
|
|
1093
1478
|
if (!position)
|
|
1094
1479
|
return null;
|
|
1095
1480
|
return mockVariationLines(params, position);
|
|
@@ -1097,18 +1482,37 @@ function mockFetchPositionVariations(params) {
|
|
|
1097
1482
|
}
|
|
1098
1483
|
function mockFetchPositionGames(params) {
|
|
1099
1484
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1100
|
-
|
|
1485
|
+
var _a, _b, _c;
|
|
1486
|
+
let filtered = params.uci
|
|
1101
1487
|
? mockGames.games.filter((g) => g.nextUci === params.uci)
|
|
1102
1488
|
: mockGames.games;
|
|
1103
|
-
|
|
1489
|
+
if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
|
|
1490
|
+
filtered = filtered.filter((game) => params.sources.includes(game.source));
|
|
1491
|
+
}
|
|
1492
|
+
const offset = (_b = params.offset) !== null && _b !== void 0 ? _b : 0;
|
|
1493
|
+
const limit = (_c = params.limit) !== null && _c !== void 0 ? _c : filtered.length;
|
|
1494
|
+
const page = filtered.slice(offset, offset + limit);
|
|
1495
|
+
const hasMore = offset + limit < filtered.length;
|
|
1496
|
+
return {
|
|
1497
|
+
positionKey: mockGames.positionKey,
|
|
1498
|
+
fen: params.fen,
|
|
1499
|
+
uci: params.uci,
|
|
1500
|
+
offset,
|
|
1501
|
+
hasMore,
|
|
1502
|
+
games: page,
|
|
1503
|
+
};
|
|
1104
1504
|
});
|
|
1105
1505
|
}
|
|
1106
1506
|
|
|
1507
|
+
exports.ALL_GAME_SOURCES = ALL_GAME_SOURCES;
|
|
1107
1508
|
exports.DEFAULT_REFERENCE_LAYOUT = DEFAULT_REFERENCE_LAYOUT;
|
|
1108
1509
|
exports.DefaultBoardNav = DefaultBoardNav;
|
|
1109
1510
|
exports.DefaultReferenceLayout = DefaultReferenceLayout;
|
|
1110
1511
|
exports.DefaultReferencePanel = DefaultReferencePanel;
|
|
1111
1512
|
exports.DefaultVariationsStrip = DefaultVariationsStrip;
|
|
1513
|
+
exports.EXPLORER_DEFAULT_VARIATION_DEPTH = EXPLORER_DEFAULT_VARIATION_DEPTH;
|
|
1514
|
+
exports.EXPLORER_DEFAULT_VARIATION_LINE_COUNT = EXPLORER_DEFAULT_VARIATION_LINE_COUNT;
|
|
1515
|
+
exports.EXPLORER_PREFETCH_CHILD_COUNT = EXPLORER_PREFETCH_CHILD_COUNT;
|
|
1112
1516
|
exports.EXPLORER_START_FEN = EXPLORER_START_FEN;
|
|
1113
1517
|
exports.ExplorerPlaceholder = ExplorerPlaceholder;
|
|
1114
1518
|
exports.GameReplayTrainer = GameReplayTrainer;
|
|
@@ -1124,10 +1528,19 @@ exports.defaultRenderVariationsStrip = defaultRenderVariationsStrip;
|
|
|
1124
1528
|
exports.fenAfterUci = fenAfterUci;
|
|
1125
1529
|
exports.fenAtPly = fenAtPly;
|
|
1126
1530
|
exports.findPlyIndexForFen = findPlyIndexForFen;
|
|
1531
|
+
exports.formatNumberedLineSans = formatNumberedLineSans;
|
|
1532
|
+
exports.gamesSessionKey = gamesSessionKey;
|
|
1127
1533
|
exports.isVariationLineActive = isVariationLineActive;
|
|
1128
1534
|
exports.mockFetchPosition = mockFetchPosition;
|
|
1129
1535
|
exports.mockFetchPositionGames = mockFetchPositionGames;
|
|
1130
1536
|
exports.mockFetchPositionVariations = mockFetchPositionVariations;
|
|
1131
1537
|
exports.normalizeFen = normalizeFen;
|
|
1538
|
+
exports.peekSessionGames = peekSessionGames;
|
|
1539
|
+
exports.peekSessionVariations = peekSessionVariations;
|
|
1540
|
+
exports.seedExplorerStartSession = seedExplorerStartSession;
|
|
1541
|
+
exports.setSessionGames = setSessionGames;
|
|
1542
|
+
exports.setSessionVariations = setSessionVariations;
|
|
1543
|
+
exports.subscribeExplorerSessionCache = subscribeExplorerSessionCache;
|
|
1132
1544
|
exports.useGameReplayTraining = useGameReplayTraining;
|
|
1545
|
+
exports.variationsSessionKey = variationsSessionKey;
|
|
1133
1546
|
exports.whiteScorePercent = whiteScorePercent;
|