react-chess-explorer 0.0.2 → 0.0.4
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/README.md +2 -0
- package/dist/features/explorer/components/DefaultBoardNav.d.ts +2 -7
- package/dist/features/explorer/components/PositionGamesPanel.d.ts +2 -9
- package/dist/features/explorer/core/PositionReferenceExplorerCore.d.ts +1 -1
- package/dist/features/explorer/core/renderProps.d.ts +22 -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 +11 -14
- package/dist/features/explorer/hooks/useVariationLines.d.ts +1 -5
- package/dist/features/explorer/index.d.ts +4 -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 +13 -10
- package/dist/features/explorer/variationLines.d.ts +0 -1
- package/dist/index.esm.js +676 -292
- package/dist/index.js +686 -290
- package/dist/stories/fixtures/nc6MockApi.d.ts +2 -2
- package/dist/stories/fixtures/nc6SampleGames.d.ts +0 -3
- package/package.json +3 -3
- 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,12 @@ 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" }), 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 filter.
|
|
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, }) => {
|
|
295
330
|
const toggleSource = (source) => {
|
|
296
331
|
if (sources.includes(source)) {
|
|
297
332
|
if (sources.length === 1) {
|
|
@@ -302,7 +337,7 @@ const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, d
|
|
|
302
337
|
}
|
|
303
338
|
onSourcesChange([...sources, source]);
|
|
304
339
|
};
|
|
305
|
-
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.
|
|
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"] })] })] }));
|
|
306
341
|
};
|
|
307
342
|
|
|
308
343
|
const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxRuntime.jsxs("div", { style: referenceShellStyle, children: [jsxRuntime.jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
|
|
@@ -315,34 +350,37 @@ function isVariationLineActive(line, selectedLineKey, forwardSans = []) {
|
|
|
315
350
|
if (forwardSans.length === 0) {
|
|
316
351
|
return false;
|
|
317
352
|
}
|
|
318
|
-
|
|
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; });
|
|
319
358
|
}
|
|
320
359
|
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
})) })] }));
|
|
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
|
+
})) }) }));
|
|
346
384
|
const defaultRenderVariationsStrip = (props) => jsxRuntime.jsx(DefaultVariationsStrip, Object.assign({}, props));
|
|
347
385
|
|
|
348
386
|
const defaultRenderStatus = (props) => (jsxRuntime.jsx(ExplorerStatusBanner, Object.assign({}, props)));
|
|
@@ -383,11 +421,113 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
383
421
|
|
|
384
422
|
const ALL_GAME_SOURCES = ["lichess", "twic"];
|
|
385
423
|
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
const prefetchMove = (move) => {
|
|
438
|
+
const childFen = fenAfterUci(fen, move.uci);
|
|
439
|
+
if (!childFen) {
|
|
440
|
+
return Promise.resolve();
|
|
441
|
+
}
|
|
442
|
+
const gamesKey = gamesSessionKey({
|
|
443
|
+
fen: childFen,
|
|
444
|
+
sources: sourcesParam,
|
|
445
|
+
});
|
|
446
|
+
const gamesPromise = peekSessionGames(gamesKey)
|
|
447
|
+
? Promise.resolve()
|
|
448
|
+
: fetchPositionGames({ fen: childFen, sources: sourcesParam })
|
|
449
|
+
.then((games) => {
|
|
450
|
+
if (!cancelled) {
|
|
451
|
+
setSessionGames(gamesKey, games);
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
.catch(() => undefined);
|
|
455
|
+
if (!fetchPositionVariations) {
|
|
456
|
+
return gamesPromise;
|
|
457
|
+
}
|
|
458
|
+
const variationsKey = variationsSessionKey(childFen);
|
|
459
|
+
const variationsPromise = peekSessionVariations(variationsKey)
|
|
460
|
+
? Promise.resolve()
|
|
461
|
+
: fetchPositionVariations({
|
|
462
|
+
fen: childFen,
|
|
463
|
+
mode: "variations",
|
|
464
|
+
lineCount: EXPLORER_DEFAULT_VARIATION_LINE_COUNT,
|
|
465
|
+
depth: EXPLORER_DEFAULT_VARIATION_DEPTH,
|
|
466
|
+
})
|
|
467
|
+
.then((result) => {
|
|
468
|
+
var _a;
|
|
469
|
+
if (!cancelled) {
|
|
470
|
+
setSessionVariations(variationsKey, (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
.catch(() => undefined);
|
|
474
|
+
return Promise.all([gamesPromise, variationsPromise]).then(() => undefined);
|
|
475
|
+
};
|
|
476
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
477
|
+
const movesToPrefetch = moves.slice(0, childCount);
|
|
478
|
+
for (let index = 0; index < movesToPrefetch.length; index += 2) {
|
|
479
|
+
if (cancelled) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
yield Promise.all(movesToPrefetch.slice(index, index + 2).map(prefetchMove));
|
|
483
|
+
}
|
|
484
|
+
}))();
|
|
485
|
+
};
|
|
486
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
487
|
+
idleHandle = window.requestIdleCallback(prefetchChildren, {
|
|
488
|
+
timeout: 1500,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
deferTimer = setTimeout(prefetchChildren, 750);
|
|
493
|
+
}
|
|
494
|
+
return () => {
|
|
495
|
+
cancelled = true;
|
|
496
|
+
if (idleHandle !== undefined) {
|
|
497
|
+
window.cancelIdleCallback(idleHandle);
|
|
498
|
+
}
|
|
499
|
+
if (deferTimer !== undefined) {
|
|
500
|
+
clearTimeout(deferTimer);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}, [
|
|
504
|
+
fen,
|
|
505
|
+
moves,
|
|
506
|
+
positionReady,
|
|
507
|
+
sources,
|
|
508
|
+
fetchPositionGames,
|
|
509
|
+
fetchPositionVariations,
|
|
510
|
+
childCount,
|
|
389
511
|
]);
|
|
390
|
-
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function initialHistoryState(initialFen, initialLineSans) {
|
|
515
|
+
if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
|
|
516
|
+
return { history: [{ fen: initialFen }], historyIndex: 0 };
|
|
517
|
+
}
|
|
518
|
+
const result = applyLineSans(initialFen, initialLineSans);
|
|
519
|
+
if (!result) {
|
|
520
|
+
return { history: [{ fen: initialFen }], historyIndex: 0 };
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
history: [{ fen: initialFen }, ...result.entries],
|
|
524
|
+
historyIndex: result.entries.length,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function usePositionHistory(initialFen, initialLineSans) {
|
|
528
|
+
var _a, _b;
|
|
529
|
+
const [history, setHistory] = react.useState(() => initialHistoryState(initialFen, initialLineSans).history);
|
|
530
|
+
const [historyIndex, setHistoryIndex] = react.useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
|
|
391
531
|
const canGoBack = historyIndex > 0;
|
|
392
532
|
const canGoForward = historyIndex < history.length - 1;
|
|
393
533
|
const pushEntry = react.useCallback((fen, lastSan) => {
|
|
@@ -415,6 +555,20 @@ function usePositionHistory(initialFen) {
|
|
|
415
555
|
setHistoryIndex(nextIndex);
|
|
416
556
|
return entry;
|
|
417
557
|
}, [history, historyIndex]);
|
|
558
|
+
const goFirst = react.useCallback(() => {
|
|
559
|
+
if (historyIndex <= 0)
|
|
560
|
+
return null;
|
|
561
|
+
const entry = history[0];
|
|
562
|
+
setHistoryIndex(0);
|
|
563
|
+
return entry;
|
|
564
|
+
}, [history, historyIndex]);
|
|
565
|
+
const goLast = react.useCallback(() => {
|
|
566
|
+
if (historyIndex >= history.length - 1)
|
|
567
|
+
return null;
|
|
568
|
+
const entry = history[history.length - 1];
|
|
569
|
+
setHistoryIndex(history.length - 1);
|
|
570
|
+
return entry;
|
|
571
|
+
}, [history, historyIndex]);
|
|
418
572
|
const resetHistory = react.useCallback((fen) => {
|
|
419
573
|
const entry = { fen };
|
|
420
574
|
setHistory([entry]);
|
|
@@ -439,39 +593,54 @@ function usePositionHistory(initialFen) {
|
|
|
439
593
|
.slice(historyIndex + 1)
|
|
440
594
|
.map((entry) => entry.lastSan)
|
|
441
595
|
.filter((san) => Boolean(san));
|
|
596
|
+
const currentFen = (_b = (_a = history[historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
|
|
597
|
+
const replaceLineEntries = react.useCallback((startFen, entries) => {
|
|
598
|
+
setHistory([{ fen: startFen }, ...entries]);
|
|
599
|
+
setHistoryIndex(entries.length);
|
|
600
|
+
}, []);
|
|
442
601
|
return {
|
|
443
602
|
canGoBack,
|
|
444
603
|
canGoForward,
|
|
445
604
|
lineSans,
|
|
446
605
|
forwardSans,
|
|
606
|
+
currentFen,
|
|
447
607
|
pushEntry,
|
|
448
608
|
pushEntries,
|
|
609
|
+
replaceLineEntries,
|
|
449
610
|
goBack,
|
|
450
611
|
goForward,
|
|
612
|
+
goFirst,
|
|
613
|
+
goLast,
|
|
451
614
|
resetHistory,
|
|
452
615
|
};
|
|
453
616
|
}
|
|
454
617
|
|
|
455
|
-
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames,
|
|
618
|
+
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
|
|
619
|
+
var _a, _b, _c;
|
|
456
620
|
const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
const
|
|
621
|
+
const bootstrappedRef = react.useRef(initialHistoryState(initialFen, initialLineSans));
|
|
622
|
+
const bootstrappedFen = (_b = (_a = bootstrappedRef.current.history[bootstrappedRef.current.historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
|
|
623
|
+
const initialGamesKey = gamesSessionKey({ fen: bootstrappedFen });
|
|
624
|
+
const initialCachedGames = peekSessionGames(initialGamesKey);
|
|
625
|
+
const [fen, setFen] = react.useState(bootstrappedFen);
|
|
626
|
+
/** Board display FEN; may lead {@link queryFen} while a variation line is animating. */
|
|
627
|
+
const [boardFen, setBoardFen] = react.useState(bootstrappedFen);
|
|
460
628
|
const [position, setPosition] = react.useState(null);
|
|
461
|
-
const [games, setGames] = react.useState(null);
|
|
629
|
+
const [games, setGames] = react.useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
|
|
462
630
|
/** Filter games to those that played this UCI from the current FEN (optional). */
|
|
463
631
|
const [gamesMoveFilterUci, setGamesMoveFilterUci] = react.useState();
|
|
464
|
-
const [minElo, setMinElo] = react.useState(defaultMinElo);
|
|
465
|
-
const [maxElo, setMaxElo] = react.useState(defaultMaxElo);
|
|
466
|
-
const [topOnly, setTopOnly] = react.useState(false);
|
|
467
632
|
const [sources, setSources] = react.useState([...ALL_GAME_SOURCES]);
|
|
468
633
|
const [loading, setLoading] = react.useState(false);
|
|
634
|
+
const [gamesLoading, setGamesLoading] = react.useState(() => initialCachedGames === undefined);
|
|
635
|
+
/** FEN that {@link position} was loaded for (may lag {@link fen} while fetching). */
|
|
636
|
+
const [loadedPositionFen, setLoadedPositionFen] = react.useState(null);
|
|
637
|
+
const [loadedPositionFilterKey, setLoadedPositionFilterKey] = react.useState(null);
|
|
469
638
|
const [error, setError] = react.useState(null);
|
|
470
|
-
const [variationsTab, setVariationsTab] = react.useState("variations");
|
|
471
639
|
const [selectedVariationKey, setSelectedVariationKey] = react.useState();
|
|
472
640
|
const variationAnimationTimerRef = react.useRef(null);
|
|
473
641
|
const isAnimatingVariationRef = react.useRef(false);
|
|
474
|
-
const readyForLineSyncRef = react.useRef(
|
|
642
|
+
const readyForLineSyncRef = react.useRef(bootstrappedRef.current.historyIndex > 0 ||
|
|
643
|
+
((_c = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) !== null && _c !== void 0 ? _c : 0) === 0);
|
|
475
644
|
const cancelVariationAnimation = react.useCallback(() => {
|
|
476
645
|
if (variationAnimationTimerRef.current !== null) {
|
|
477
646
|
clearTimeout(variationAnimationTimerRef.current);
|
|
@@ -479,7 +648,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
479
648
|
}
|
|
480
649
|
isAnimatingVariationRef.current = false;
|
|
481
650
|
}, []);
|
|
482
|
-
const { canGoBack, canGoForward, lineSans, forwardSans, pushEntry, pushEntries, goBack, goForward, resetHistory, } = usePositionHistory(initialFen);
|
|
651
|
+
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
|
|
652
|
+
/** FEN used for explorer API queries — follows move history, not animation frames. */
|
|
653
|
+
const queryFen = currentFen;
|
|
654
|
+
const initialLineKey = react.useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
|
|
655
|
+
const expectedLineFen = react.useMemo(() => {
|
|
656
|
+
var _a, _b;
|
|
657
|
+
if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
|
|
658
|
+
return EXPLORER_START_FEN;
|
|
659
|
+
}
|
|
660
|
+
return ((_b = (_a = applyLineSans(EXPLORER_START_FEN, initialLineSans)) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : EXPLORER_START_FEN);
|
|
661
|
+
}, [initialLineKey, initialLineSans]);
|
|
483
662
|
const applyNavigation = react.useCallback((nextFen, clearMoveFilter = true) => {
|
|
484
663
|
setFen(nextFen);
|
|
485
664
|
setBoardFen(nextFen);
|
|
@@ -488,39 +667,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
488
667
|
}
|
|
489
668
|
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
|
|
490
669
|
}, [onFenChange]);
|
|
491
|
-
const
|
|
492
|
-
|
|
670
|
+
const queryFenRef = react.useRef(queryFen);
|
|
671
|
+
queryFenRef.current = queryFen;
|
|
672
|
+
const positionsByFenRef = react.useRef(new Map());
|
|
673
|
+
const positionCacheKey = react.useCallback((fenValue) => JSON.stringify({
|
|
674
|
+
fen: fenValue,
|
|
675
|
+
sources: sources.length < ALL_GAME_SOURCES.length
|
|
676
|
+
? sources.slice().sort().join(",")
|
|
677
|
+
: "",
|
|
678
|
+
}), [sources]);
|
|
679
|
+
const gamesCacheKey = react.useCallback((fenValue, uci) => gamesSessionKey({
|
|
680
|
+
fen: fenValue,
|
|
681
|
+
uci,
|
|
682
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
683
|
+
}), [sources]);
|
|
684
|
+
const lastAppliedLineKeyRef = react.useRef(initialLineKey);
|
|
493
685
|
react.useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
|
|
686
|
+
// Apply URL line changes only when the external line key changes — not when the
|
|
687
|
+
// user clicks moves (internal lineSans updates must not re-sync from stale props).
|
|
494
688
|
react.useEffect(() => {
|
|
495
|
-
if (
|
|
496
|
-
return;
|
|
497
|
-
const currentLineKey = lineSans.join("|");
|
|
498
|
-
if (currentLineKey === initialLineKey) {
|
|
499
|
-
lastAppliedLineKeyRef.current = initialLineKey;
|
|
689
|
+
if (initialLineSans === undefined && onLineSansChange === undefined)
|
|
500
690
|
return;
|
|
691
|
+
const fenMatches = normalizeFen(queryFen) === normalizeFen(expectedLineFen);
|
|
692
|
+
if (lastAppliedLineKeyRef.current === initialLineKey) {
|
|
693
|
+
if (fenMatches)
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const currentLineKey = lineSans.join("|");
|
|
698
|
+
if (currentLineKey === initialLineKey && fenMatches) {
|
|
699
|
+
lastAppliedLineKeyRef.current = initialLineKey;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
501
702
|
}
|
|
502
703
|
lastAppliedLineKeyRef.current = initialLineKey;
|
|
503
704
|
cancelVariationAnimation();
|
|
504
|
-
resetHistory(EXPLORER_START_FEN);
|
|
505
705
|
setSelectedVariationKey(undefined);
|
|
506
706
|
if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
|
|
507
707
|
const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
|
|
508
708
|
if (result) {
|
|
509
|
-
|
|
709
|
+
replaceLineEntries(EXPLORER_START_FEN, result.entries);
|
|
510
710
|
applyNavigation(result.fen, true);
|
|
511
711
|
return;
|
|
512
712
|
}
|
|
513
713
|
}
|
|
714
|
+
replaceLineEntries(EXPLORER_START_FEN, []);
|
|
514
715
|
applyNavigation(EXPLORER_START_FEN, true);
|
|
716
|
+
// lineSans / queryFen intentionally omitted — only re-sync when the URL line key changes.
|
|
717
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
515
718
|
}, [
|
|
516
719
|
initialLineKey,
|
|
517
720
|
initialLineSans,
|
|
518
|
-
|
|
721
|
+
expectedLineFen,
|
|
519
722
|
cancelVariationAnimation,
|
|
520
|
-
|
|
521
|
-
pushEntries,
|
|
723
|
+
replaceLineEntries,
|
|
522
724
|
applyNavigation,
|
|
523
725
|
]);
|
|
726
|
+
// Keep legacy fen state aligned with move history when they drift apart.
|
|
727
|
+
react.useEffect(() => {
|
|
728
|
+
if (isAnimatingVariationRef.current)
|
|
729
|
+
return;
|
|
730
|
+
if (normalizeFen(fen) === normalizeFen(queryFen))
|
|
731
|
+
return;
|
|
732
|
+
setFen(queryFen);
|
|
733
|
+
setBoardFen(queryFen);
|
|
734
|
+
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(queryFen);
|
|
735
|
+
}, [fen, queryFen, onFenChange]);
|
|
524
736
|
react.useEffect(() => {
|
|
525
737
|
if (readyForLineSyncRef.current)
|
|
526
738
|
return;
|
|
@@ -532,90 +744,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
532
744
|
}
|
|
533
745
|
}, [lineSans, initialLineSans]);
|
|
534
746
|
react.useEffect(() => {
|
|
747
|
+
if (!onLineSansChange)
|
|
748
|
+
return;
|
|
535
749
|
if (!readyForLineSyncRef.current)
|
|
536
750
|
return;
|
|
537
|
-
onLineSansChange
|
|
751
|
+
onLineSansChange(lineSans);
|
|
538
752
|
}, [lineSans, onLineSansChange]);
|
|
539
753
|
react.useEffect(() => {
|
|
540
|
-
if (fenProp === undefined
|
|
754
|
+
if (fenProp === undefined)
|
|
755
|
+
return;
|
|
756
|
+
if (fenProp === queryFenRef.current)
|
|
541
757
|
return;
|
|
542
758
|
cancelVariationAnimation();
|
|
543
759
|
resetHistory(fenProp);
|
|
544
760
|
applyNavigation(fenProp, true);
|
|
545
|
-
}, [fenProp,
|
|
761
|
+
}, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
|
|
546
762
|
react.useEffect(() => {
|
|
547
763
|
let cancelled = false;
|
|
548
|
-
|
|
549
|
-
|
|
764
|
+
const requestedFen = queryFen;
|
|
765
|
+
const requestedFilterKey = positionCacheKey(requestedFen);
|
|
766
|
+
if (positionsByFenRef.current.has(requestedFilterKey)) {
|
|
767
|
+
setPosition(positionsByFenRef.current.get(requestedFilterKey));
|
|
768
|
+
setLoadedPositionFen(requestedFen);
|
|
769
|
+
setLoadedPositionFilterKey(requestedFilterKey);
|
|
770
|
+
setLoading(false);
|
|
771
|
+
setError(null);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
setLoading(true);
|
|
775
|
+
setError(null);
|
|
776
|
+
}
|
|
550
777
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
551
778
|
try {
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
maxElo,
|
|
558
|
-
uci: gamesMoveFilterUci,
|
|
559
|
-
topOnly,
|
|
560
|
-
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
561
|
-
}),
|
|
562
|
-
]);
|
|
563
|
-
if (cancelled)
|
|
779
|
+
const pos = yield fetchPosition({
|
|
780
|
+
fen: requestedFen,
|
|
781
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
782
|
+
});
|
|
783
|
+
if (cancelled || requestedFen !== queryFenRef.current)
|
|
564
784
|
return;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!pos) {
|
|
568
|
-
setError("No explorer data for this position yet");
|
|
785
|
+
if (pos) {
|
|
786
|
+
positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
|
|
569
787
|
}
|
|
788
|
+
setPosition(pos);
|
|
789
|
+
setLoadedPositionFen(requestedFen);
|
|
790
|
+
setLoadedPositionFilterKey(positionCacheKey(requestedFen));
|
|
570
791
|
}
|
|
571
792
|
catch (e) {
|
|
793
|
+
if (cancelled || requestedFen !== queryFenRef.current)
|
|
794
|
+
return;
|
|
795
|
+
setPosition(null);
|
|
796
|
+
setLoadedPositionFen(null);
|
|
797
|
+
setLoadedPositionFilterKey(null);
|
|
798
|
+
setError(e instanceof Error ? e.message : "Failed to load explorer data");
|
|
799
|
+
}
|
|
800
|
+
finally {
|
|
801
|
+
if (!cancelled && requestedFen === queryFenRef.current) {
|
|
802
|
+
setLoading(false);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}))();
|
|
806
|
+
return () => {
|
|
807
|
+
cancelled = true;
|
|
808
|
+
};
|
|
809
|
+
}, [queryFen, sources, fetchPosition, positionCacheKey]);
|
|
810
|
+
const activePositionFilterKey = positionCacheKey(queryFen);
|
|
811
|
+
const cachedPosition = positionsByFenRef.current.get(activePositionFilterKey);
|
|
812
|
+
const positionReady = loadedPositionFen === queryFen &&
|
|
813
|
+
loadedPositionFilterKey === activePositionFilterKey;
|
|
814
|
+
const displayPosition = cachedPosition !== null && cachedPosition !== void 0 ? cachedPosition : (positionReady ? position : null);
|
|
815
|
+
const displayMoves = Array.isArray(displayPosition === null || displayPosition === void 0 ? void 0 : displayPosition.moves)
|
|
816
|
+
? displayPosition.moves
|
|
817
|
+
: [];
|
|
818
|
+
const showPositionLoading = loading && displayMoves.length === 0;
|
|
819
|
+
const isStartPosition = normalizeFen(queryFen) === normalizeFen(EXPLORER_START_FEN);
|
|
820
|
+
useExplorerPrefetch({
|
|
821
|
+
fen: queryFen,
|
|
822
|
+
moves: displayMoves,
|
|
823
|
+
positionReady,
|
|
824
|
+
sources,
|
|
825
|
+
fetchPositionGames,
|
|
826
|
+
fetchPositionVariations,
|
|
827
|
+
});
|
|
828
|
+
react.useEffect(() => {
|
|
829
|
+
return subscribeExplorerSessionCache(() => {
|
|
830
|
+
const key = gamesCacheKey(queryFen, gamesMoveFilterUci);
|
|
831
|
+
const cached = peekSessionGames(key);
|
|
832
|
+
if (!cached) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
setGames(cached);
|
|
836
|
+
setGamesLoading(false);
|
|
837
|
+
});
|
|
838
|
+
}, [queryFen, gamesMoveFilterUci, gamesCacheKey]);
|
|
839
|
+
react.useEffect(() => {
|
|
840
|
+
const canLoadGames = positionReady || (isStartPosition && !gamesMoveFilterUci);
|
|
841
|
+
if (!canLoadGames)
|
|
842
|
+
return;
|
|
843
|
+
let cancelled = false;
|
|
844
|
+
const requestedKey = gamesCacheKey(queryFen, gamesMoveFilterUci);
|
|
845
|
+
const cachedGames = peekSessionGames(requestedKey);
|
|
846
|
+
if (cachedGames) {
|
|
847
|
+
setGames(cachedGames);
|
|
848
|
+
setGamesLoading(false);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
setGamesLoading(true);
|
|
852
|
+
}
|
|
853
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
854
|
+
try {
|
|
855
|
+
const gameList = yield fetchPositionGames({
|
|
856
|
+
fen: queryFen,
|
|
857
|
+
uci: gamesMoveFilterUci,
|
|
858
|
+
sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
|
|
859
|
+
});
|
|
860
|
+
if (cancelled)
|
|
861
|
+
return;
|
|
862
|
+
setSessionGames(requestedKey, gameList);
|
|
863
|
+
setGames(gameList);
|
|
864
|
+
}
|
|
865
|
+
catch (_a) {
|
|
572
866
|
if (!cancelled) {
|
|
573
|
-
|
|
867
|
+
setGames(null);
|
|
574
868
|
}
|
|
575
869
|
}
|
|
576
870
|
finally {
|
|
577
871
|
if (!cancelled)
|
|
578
|
-
|
|
872
|
+
setGamesLoading(false);
|
|
579
873
|
}
|
|
580
874
|
}))();
|
|
581
875
|
return () => {
|
|
582
876
|
cancelled = true;
|
|
583
877
|
};
|
|
584
878
|
}, [
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
879
|
+
queryFen,
|
|
880
|
+
positionReady,
|
|
881
|
+
isStartPosition,
|
|
588
882
|
gamesMoveFilterUci,
|
|
589
|
-
topOnly,
|
|
590
883
|
sources,
|
|
591
|
-
fetchPosition,
|
|
592
884
|
fetchPositionGames,
|
|
885
|
+
gamesCacheKey,
|
|
593
886
|
]);
|
|
594
887
|
const handleMoveSelect = react.useCallback((move) => {
|
|
595
888
|
if (isAnimatingVariationRef.current) {
|
|
596
889
|
cancelVariationAnimation();
|
|
597
|
-
setBoardFen(
|
|
890
|
+
setBoardFen(queryFen);
|
|
598
891
|
setSelectedVariationKey(undefined);
|
|
599
892
|
}
|
|
600
|
-
const nextFen = fenAfterUci(
|
|
893
|
+
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
601
894
|
if (!nextFen)
|
|
602
895
|
return;
|
|
603
896
|
pushEntry(nextFen, move.san);
|
|
604
897
|
setSelectedVariationKey(undefined);
|
|
605
898
|
applyNavigation(nextFen, true);
|
|
606
|
-
}, [
|
|
899
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
607
900
|
const handleLineSelect = react.useCallback((line) => {
|
|
608
901
|
if (isAnimatingVariationRef.current)
|
|
609
902
|
return;
|
|
610
903
|
cancelVariationAnimation();
|
|
611
|
-
let
|
|
904
|
+
let lineFen = queryFen;
|
|
612
905
|
const entries = [];
|
|
613
906
|
for (let i = 0; i < line.uciPath.length; i += 1) {
|
|
614
|
-
const nextFen = fenAfterUci(
|
|
907
|
+
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
615
908
|
if (!nextFen)
|
|
616
909
|
return;
|
|
617
910
|
entries.push({ fen: nextFen, lastSan: line.moves[i].san });
|
|
618
|
-
|
|
911
|
+
lineFen = nextFen;
|
|
619
912
|
}
|
|
620
913
|
if (entries.length === 0)
|
|
621
914
|
return;
|
|
@@ -639,109 +932,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
639
932
|
}
|
|
640
933
|
};
|
|
641
934
|
tick();
|
|
642
|
-
}, [
|
|
935
|
+
}, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
|
|
643
936
|
const handlePieceDrop = react.useCallback((sourceSquare, targetSquare, piece) => {
|
|
644
937
|
if (isAnimatingVariationRef.current) {
|
|
645
938
|
cancelVariationAnimation();
|
|
646
|
-
setBoardFen(
|
|
939
|
+
setBoardFen(queryFen);
|
|
647
940
|
setSelectedVariationKey(undefined);
|
|
648
941
|
}
|
|
649
|
-
const result = applyBoardMove(
|
|
942
|
+
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
650
943
|
if (!result)
|
|
651
944
|
return false;
|
|
652
945
|
pushEntry(result.fen, result.san);
|
|
653
946
|
setSelectedVariationKey(undefined);
|
|
654
947
|
applyNavigation(result.fen, true);
|
|
655
948
|
return true;
|
|
656
|
-
}, [
|
|
949
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
657
950
|
const handleBack = react.useCallback(() => {
|
|
658
951
|
if (isAnimatingVariationRef.current) {
|
|
659
952
|
cancelVariationAnimation();
|
|
660
|
-
setBoardFen(
|
|
953
|
+
setBoardFen(queryFen);
|
|
661
954
|
setSelectedVariationKey(undefined);
|
|
662
955
|
return;
|
|
663
956
|
}
|
|
664
957
|
const entry = goBack();
|
|
665
958
|
if (entry)
|
|
666
959
|
applyNavigation(entry.fen, true);
|
|
667
|
-
}, [
|
|
960
|
+
}, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
|
|
668
961
|
const handleForward = react.useCallback(() => {
|
|
669
962
|
if (isAnimatingVariationRef.current) {
|
|
670
963
|
cancelVariationAnimation();
|
|
671
|
-
setBoardFen(
|
|
964
|
+
setBoardFen(queryFen);
|
|
672
965
|
setSelectedVariationKey(undefined);
|
|
673
966
|
return;
|
|
674
967
|
}
|
|
675
968
|
const entry = goForward();
|
|
676
969
|
if (entry)
|
|
677
970
|
applyNavigation(entry.fen, true);
|
|
678
|
-
}, [
|
|
971
|
+
}, [queryFen, goForward, applyNavigation, cancelVariationAnimation]);
|
|
972
|
+
const handleFirst = react.useCallback(() => {
|
|
973
|
+
if (isAnimatingVariationRef.current) {
|
|
974
|
+
cancelVariationAnimation();
|
|
975
|
+
setBoardFen(queryFen);
|
|
976
|
+
setSelectedVariationKey(undefined);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const entry = goFirst();
|
|
980
|
+
if (entry)
|
|
981
|
+
applyNavigation(entry.fen, true);
|
|
982
|
+
}, [queryFen, goFirst, applyNavigation, cancelVariationAnimation]);
|
|
983
|
+
const handleLast = react.useCallback(() => {
|
|
984
|
+
if (isAnimatingVariationRef.current) {
|
|
985
|
+
cancelVariationAnimation();
|
|
986
|
+
setBoardFen(queryFen);
|
|
987
|
+
setSelectedVariationKey(undefined);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const entry = goLast();
|
|
991
|
+
if (entry)
|
|
992
|
+
applyNavigation(entry.fen, true);
|
|
993
|
+
}, [queryFen, goLast, applyNavigation, cancelVariationAnimation]);
|
|
679
994
|
const lineLabel = react.useMemo(() => {
|
|
680
995
|
if (lineSans.length > 0) {
|
|
681
|
-
return lineSans
|
|
996
|
+
return formatNumberedLineSans(lineSans);
|
|
682
997
|
}
|
|
683
|
-
if (position) {
|
|
998
|
+
if (position && typeof position.totalGames === "number") {
|
|
684
999
|
return `Starting position (${position.totalGames.toLocaleString()} games)`;
|
|
685
1000
|
}
|
|
686
1001
|
return "";
|
|
687
1002
|
}, [lineSans, position]);
|
|
688
1003
|
return {
|
|
689
|
-
fen,
|
|
1004
|
+
fen: queryFen,
|
|
690
1005
|
boardFen,
|
|
691
1006
|
position,
|
|
692
1007
|
games,
|
|
693
1008
|
gamesMoveFilterUci,
|
|
694
|
-
minElo,
|
|
695
|
-
maxElo,
|
|
696
|
-
topOnly,
|
|
697
1009
|
sources,
|
|
1010
|
+
lineSans,
|
|
698
1011
|
loading,
|
|
1012
|
+
showPositionLoading,
|
|
1013
|
+
gamesLoading,
|
|
1014
|
+
positionReady,
|
|
1015
|
+
displayMoves,
|
|
699
1016
|
error,
|
|
700
1017
|
lineLabel,
|
|
701
1018
|
canGoBack,
|
|
702
1019
|
canGoForward,
|
|
703
|
-
variationsTab,
|
|
704
1020
|
forwardSans,
|
|
705
1021
|
selectedVariationKey,
|
|
706
|
-
setMinElo,
|
|
707
|
-
setMaxElo,
|
|
708
|
-
setTopOnly,
|
|
709
1022
|
setSources,
|
|
710
|
-
setVariationsTab,
|
|
711
1023
|
setGamesMoveFilterUci,
|
|
712
1024
|
handleMoveSelect,
|
|
713
1025
|
handleLineSelect,
|
|
714
1026
|
handlePieceDrop,
|
|
715
1027
|
handleBack,
|
|
716
1028
|
handleForward,
|
|
1029
|
+
handleFirst,
|
|
1030
|
+
handleLast,
|
|
717
1031
|
};
|
|
718
1032
|
}
|
|
719
1033
|
|
|
720
|
-
function useVariationLines({ fen,
|
|
721
|
-
const
|
|
722
|
-
const [
|
|
1034
|
+
function useVariationLines({ fen, fetchPositionVariations, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH, enabled = true, }) {
|
|
1035
|
+
const cacheKey = react.useMemo(() => variationsSessionKey(fen, lineCount, lineDepth), [fen, lineCount, lineDepth]);
|
|
1036
|
+
const [lines, setLines] = react.useState(() => {
|
|
1037
|
+
var _a;
|
|
1038
|
+
if (!enabled) {
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
return (_a = peekSessionVariations(cacheKey)) !== null && _a !== void 0 ? _a : [];
|
|
1042
|
+
});
|
|
1043
|
+
const [loading, setLoading] = react.useState(() => {
|
|
1044
|
+
if (!enabled) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
return peekSessionVariations(cacheKey) === undefined;
|
|
1048
|
+
});
|
|
1049
|
+
react.useEffect(() => {
|
|
1050
|
+
return subscribeExplorerSessionCache(() => {
|
|
1051
|
+
if (!enabled) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const cachedLines = peekSessionVariations(cacheKey);
|
|
1055
|
+
if (!cachedLines) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
setLines(cachedLines);
|
|
1059
|
+
setLoading(false);
|
|
1060
|
+
});
|
|
1061
|
+
}, [enabled, cacheKey]);
|
|
723
1062
|
react.useEffect(() => {
|
|
724
|
-
if (!enabled
|
|
1063
|
+
if (!enabled) {
|
|
725
1064
|
setLines([]);
|
|
726
1065
|
setLoading(false);
|
|
727
1066
|
return;
|
|
728
1067
|
}
|
|
729
1068
|
let cancelled = false;
|
|
730
|
-
|
|
731
|
-
|
|
1069
|
+
const cachedLines = peekSessionVariations(cacheKey);
|
|
1070
|
+
if (cachedLines) {
|
|
1071
|
+
setLines(cachedLines);
|
|
1072
|
+
setLoading(false);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
setLoading(true);
|
|
1076
|
+
}
|
|
1077
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
732
1078
|
var _a;
|
|
733
1079
|
try {
|
|
734
1080
|
const result = yield fetchPositionVariations({
|
|
735
1081
|
fen,
|
|
736
|
-
mode:
|
|
737
|
-
minElo,
|
|
738
|
-
maxElo,
|
|
1082
|
+
mode: "variations",
|
|
739
1083
|
lineCount,
|
|
740
1084
|
depth: lineDepth,
|
|
741
1085
|
});
|
|
742
1086
|
if (cancelled)
|
|
743
1087
|
return;
|
|
744
|
-
|
|
1088
|
+
const nextLines = (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : [];
|
|
1089
|
+
setSessionVariations(cacheKey, nextLines);
|
|
1090
|
+
setLines(nextLines);
|
|
745
1091
|
}
|
|
746
1092
|
catch (_b) {
|
|
747
1093
|
if (!cancelled) {
|
|
@@ -757,21 +1103,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
|
|
|
757
1103
|
return () => {
|
|
758
1104
|
cancelled = true;
|
|
759
1105
|
};
|
|
760
|
-
}, [
|
|
761
|
-
|
|
762
|
-
fen,
|
|
763
|
-
tab,
|
|
764
|
-
minElo,
|
|
765
|
-
maxElo,
|
|
766
|
-
fetchPositionVariations,
|
|
767
|
-
lineCount,
|
|
768
|
-
lineDepth,
|
|
769
|
-
]);
|
|
770
|
-
return { lines, loading };
|
|
1106
|
+
}, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
|
|
1107
|
+
return { lines, loading: enabled ? loading : false };
|
|
771
1108
|
}
|
|
772
1109
|
|
|
773
|
-
const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth,
|
|
774
|
-
var _a, _b;
|
|
1110
|
+
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, }) => {
|
|
775
1111
|
const referenceData = usePositionReferenceData({
|
|
776
1112
|
fenProp,
|
|
777
1113
|
onFenChange,
|
|
@@ -779,59 +1115,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
779
1115
|
onLineSansChange,
|
|
780
1116
|
fetchPosition,
|
|
781
1117
|
fetchPositionGames,
|
|
782
|
-
|
|
783
|
-
|
|
1118
|
+
fetchPositionVariations,
|
|
1119
|
+
});
|
|
1120
|
+
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;
|
|
1121
|
+
reactChessCore.usePositionKeyboardNav({
|
|
1122
|
+
enabled: keyboardNav,
|
|
1123
|
+
canPrev: canGoBack,
|
|
1124
|
+
canNext: canGoForward,
|
|
1125
|
+
onPrev: handleBack,
|
|
1126
|
+
onNext: handleForward,
|
|
1127
|
+
onFirst: handleFirst,
|
|
1128
|
+
onLast: handleLast,
|
|
784
1129
|
});
|
|
785
|
-
const
|
|
1130
|
+
const [variationsEnabled, setVariationsEnabled] = react.useState(() => normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN));
|
|
1131
|
+
const isStartPosition = normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN);
|
|
1132
|
+
react.useEffect(() => {
|
|
1133
|
+
const cachedVariations = peekSessionVariations(variationsSessionKey(fen, EXPLORER_DEFAULT_VARIATION_LINE_COUNT, EXPLORER_DEFAULT_VARIATION_DEPTH));
|
|
1134
|
+
if (cachedVariations) {
|
|
1135
|
+
setVariationsEnabled(true);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (!positionReady && !isStartPosition) {
|
|
1139
|
+
setVariationsEnabled(false);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (loading && !isStartPosition) {
|
|
1143
|
+
setVariationsEnabled(false);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
setVariationsEnabled(true);
|
|
1147
|
+
}, [fen, positionReady, loading, isStartPosition]);
|
|
786
1148
|
const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
|
|
787
1149
|
fen,
|
|
788
|
-
tab: variationsTab,
|
|
789
|
-
minElo,
|
|
790
|
-
maxElo,
|
|
791
1150
|
fetchPositionVariations,
|
|
792
|
-
enabled:
|
|
1151
|
+
enabled: variationsEnabled && (positionReady || isStartPosition),
|
|
793
1152
|
});
|
|
1153
|
+
const [internalBoardOrientation, setInternalBoardOrientation] = react.useState(defaultBoardOrientation);
|
|
1154
|
+
const boardOrientation = boardOrientationProp !== null && boardOrientationProp !== void 0 ? boardOrientationProp : internalBoardOrientation;
|
|
1155
|
+
const handleFlipBoard = react.useCallback(() => {
|
|
1156
|
+
const nextOrientation = boardOrientation === "white" ? "black" : "white";
|
|
1157
|
+
if (boardOrientationProp === undefined) {
|
|
1158
|
+
setInternalBoardOrientation(nextOrientation);
|
|
1159
|
+
}
|
|
1160
|
+
onBoardOrientationChange === null || onBoardOrientationChange === void 0 ? void 0 : onBoardOrientationChange(nextOrientation);
|
|
1161
|
+
}, [boardOrientation, boardOrientationProp, onBoardOrientationChange]);
|
|
1162
|
+
const boardNavProps = react.useMemo(() => ({
|
|
1163
|
+
canGoBack,
|
|
1164
|
+
canGoForward,
|
|
1165
|
+
onBack: handleBack,
|
|
1166
|
+
onForward: handleForward,
|
|
1167
|
+
boardOrientation,
|
|
1168
|
+
onFlipBoard: handleFlipBoard,
|
|
1169
|
+
}), [
|
|
1170
|
+
boardOrientation,
|
|
1171
|
+
canGoBack,
|
|
1172
|
+
canGoForward,
|
|
1173
|
+
handleBack,
|
|
1174
|
+
handleFlipBoard,
|
|
1175
|
+
handleForward,
|
|
1176
|
+
]);
|
|
794
1177
|
const outerStyle = {
|
|
795
1178
|
width: "100%",
|
|
796
1179
|
height: fillHeight ? "100%" : "auto",
|
|
797
1180
|
minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
|
|
798
|
-
overflow: "hidden",
|
|
1181
|
+
overflow: fillHeight ? "hidden" : "visible",
|
|
799
1182
|
boxSizing: "border-box",
|
|
800
1183
|
};
|
|
801
|
-
const board = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, status: renderStatus({ error, loading }), moveStats: renderMoveStats({
|
|
808
|
-
moves: (_a = position === null || position === void 0 ? void 0 : position.moves) !== null && _a !== void 0 ? _a : [],
|
|
1184
|
+
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)] }));
|
|
1185
|
+
const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
|
|
1186
|
+
error,
|
|
1187
|
+
loading: showPositionLoading,
|
|
1188
|
+
}), moveStats: renderMoveStats({
|
|
1189
|
+
moves: displayMoves,
|
|
809
1190
|
onMoveSelect: handleMoveSelect,
|
|
810
1191
|
}), variationsStrip: renderVariationsStrip({
|
|
811
1192
|
theme,
|
|
812
|
-
tab: variationsTab,
|
|
813
|
-
onTabChange: setVariationsTab,
|
|
814
1193
|
lines: variationLines,
|
|
815
|
-
loading: variationLinesLoading,
|
|
1194
|
+
loading: !variationsEnabled || variationLinesLoading,
|
|
816
1195
|
selectedLineKey: selectedVariationKey,
|
|
817
1196
|
forwardSans,
|
|
818
1197
|
onLineSelect: handleLineSelect,
|
|
819
1198
|
}), gamesPanel: renderGamesPanel({
|
|
820
|
-
games: (
|
|
1199
|
+
games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
|
|
1200
|
+
loading: gamesLoading,
|
|
821
1201
|
lineLabel,
|
|
822
|
-
|
|
823
|
-
maxElo,
|
|
824
|
-
defaultMinElo,
|
|
825
|
-
defaultMaxElo,
|
|
826
|
-
topOnly,
|
|
1202
|
+
lineSans,
|
|
827
1203
|
sources,
|
|
828
|
-
onMinEloChange: setMinElo,
|
|
829
|
-
onMaxEloChange: setMaxElo,
|
|
830
|
-
onTopOnlyChange: setTopOnly,
|
|
831
1204
|
onSourcesChange: setSources,
|
|
832
1205
|
onGameSelect,
|
|
833
1206
|
}) }));
|
|
834
|
-
return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, children: jsxRuntime.jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
|
|
1207
|
+
return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
|
|
835
1208
|
};
|
|
836
1209
|
|
|
837
1210
|
/** Reference explorer with library default layout and renderers (ChessBase-style grid). */
|
|
@@ -1003,7 +1376,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
|
|
|
1003
1376
|
!error &&
|
|
1004
1377
|
!complete &&
|
|
1005
1378
|
feedback === "incorrect" &&
|
|
1006
|
-
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(
|
|
1379
|
+
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" }))] }) }));
|
|
1007
1380
|
};
|
|
1008
1381
|
|
|
1009
1382
|
const mockPosition = {
|
|
@@ -1042,10 +1415,6 @@ const mockPosition = {
|
|
|
1042
1415
|
};
|
|
1043
1416
|
const mockGames = {
|
|
1044
1417
|
positionKey: "mock-start",
|
|
1045
|
-
fen: EXPLORER_START_FEN,
|
|
1046
|
-
minElo: 2200,
|
|
1047
|
-
maxElo: 2800,
|
|
1048
|
-
topOnly: false,
|
|
1049
1418
|
games: [
|
|
1050
1419
|
{
|
|
1051
1420
|
gameId: "abc123",
|
|
@@ -1077,7 +1446,7 @@ const mockGames = {
|
|
|
1077
1446
|
},
|
|
1078
1447
|
],
|
|
1079
1448
|
};
|
|
1080
|
-
function mockFetchPosition(
|
|
1449
|
+
function mockFetchPosition(params) {
|
|
1081
1450
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1082
1451
|
return mockPosition;
|
|
1083
1452
|
});
|
|
@@ -1111,7 +1480,11 @@ function mockVariationLines(params, position) {
|
|
|
1111
1480
|
}
|
|
1112
1481
|
function mockFetchPositionVariations(params) {
|
|
1113
1482
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1114
|
-
const position = yield mockFetchPosition(
|
|
1483
|
+
const position = yield mockFetchPosition({
|
|
1484
|
+
fen: params.fen,
|
|
1485
|
+
minElo: params.minElo,
|
|
1486
|
+
maxElo: params.maxElo,
|
|
1487
|
+
});
|
|
1115
1488
|
if (!position)
|
|
1116
1489
|
return null;
|
|
1117
1490
|
return mockVariationLines(params, position);
|
|
@@ -1119,14 +1492,25 @@ function mockFetchPositionVariations(params) {
|
|
|
1119
1492
|
}
|
|
1120
1493
|
function mockFetchPositionGames(params) {
|
|
1121
1494
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1122
|
-
var _a;
|
|
1495
|
+
var _a, _b, _c;
|
|
1123
1496
|
let filtered = params.uci
|
|
1124
1497
|
? mockGames.games.filter((g) => g.nextUci === params.uci)
|
|
1125
1498
|
: mockGames.games;
|
|
1126
1499
|
if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
|
|
1127
1500
|
filtered = filtered.filter((game) => params.sources.includes(game.source));
|
|
1128
1501
|
}
|
|
1129
|
-
|
|
1502
|
+
const offset = (_b = params.offset) !== null && _b !== void 0 ? _b : 0;
|
|
1503
|
+
const limit = (_c = params.limit) !== null && _c !== void 0 ? _c : filtered.length;
|
|
1504
|
+
const page = filtered.slice(offset, offset + limit);
|
|
1505
|
+
const hasMore = offset + limit < filtered.length;
|
|
1506
|
+
return {
|
|
1507
|
+
positionKey: mockGames.positionKey,
|
|
1508
|
+
fen: params.fen,
|
|
1509
|
+
uci: params.uci,
|
|
1510
|
+
offset,
|
|
1511
|
+
hasMore,
|
|
1512
|
+
games: page,
|
|
1513
|
+
};
|
|
1130
1514
|
});
|
|
1131
1515
|
}
|
|
1132
1516
|
|
|
@@ -1136,6 +1520,9 @@ exports.DefaultBoardNav = DefaultBoardNav;
|
|
|
1136
1520
|
exports.DefaultReferenceLayout = DefaultReferenceLayout;
|
|
1137
1521
|
exports.DefaultReferencePanel = DefaultReferencePanel;
|
|
1138
1522
|
exports.DefaultVariationsStrip = DefaultVariationsStrip;
|
|
1523
|
+
exports.EXPLORER_DEFAULT_VARIATION_DEPTH = EXPLORER_DEFAULT_VARIATION_DEPTH;
|
|
1524
|
+
exports.EXPLORER_DEFAULT_VARIATION_LINE_COUNT = EXPLORER_DEFAULT_VARIATION_LINE_COUNT;
|
|
1525
|
+
exports.EXPLORER_PREFETCH_CHILD_COUNT = EXPLORER_PREFETCH_CHILD_COUNT;
|
|
1139
1526
|
exports.EXPLORER_START_FEN = EXPLORER_START_FEN;
|
|
1140
1527
|
exports.ExplorerPlaceholder = ExplorerPlaceholder;
|
|
1141
1528
|
exports.GameReplayTrainer = GameReplayTrainer;
|
|
@@ -1151,10 +1538,19 @@ exports.defaultRenderVariationsStrip = defaultRenderVariationsStrip;
|
|
|
1151
1538
|
exports.fenAfterUci = fenAfterUci;
|
|
1152
1539
|
exports.fenAtPly = fenAtPly;
|
|
1153
1540
|
exports.findPlyIndexForFen = findPlyIndexForFen;
|
|
1541
|
+
exports.formatNumberedLineSans = formatNumberedLineSans;
|
|
1542
|
+
exports.gamesSessionKey = gamesSessionKey;
|
|
1154
1543
|
exports.isVariationLineActive = isVariationLineActive;
|
|
1155
1544
|
exports.mockFetchPosition = mockFetchPosition;
|
|
1156
1545
|
exports.mockFetchPositionGames = mockFetchPositionGames;
|
|
1157
1546
|
exports.mockFetchPositionVariations = mockFetchPositionVariations;
|
|
1158
1547
|
exports.normalizeFen = normalizeFen;
|
|
1548
|
+
exports.peekSessionGames = peekSessionGames;
|
|
1549
|
+
exports.peekSessionVariations = peekSessionVariations;
|
|
1550
|
+
exports.seedExplorerStartSession = seedExplorerStartSession;
|
|
1551
|
+
exports.setSessionGames = setSessionGames;
|
|
1552
|
+
exports.setSessionVariations = setSessionVariations;
|
|
1553
|
+
exports.subscribeExplorerSessionCache = subscribeExplorerSessionCache;
|
|
1159
1554
|
exports.useGameReplayTraining = useGameReplayTraining;
|
|
1555
|
+
exports.variationsSessionKey = variationsSessionKey;
|
|
1160
1556
|
exports.whiteScorePercent = whiteScorePercent;
|