react-chess-explorer 0.0.2 → 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 +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 +666 -292
- package/dist/index.js +676 -290
- package/dist/stories/fixtures/nc6MockApi.d.ts +2 -2
- package/dist/stories/fixtures/nc6SampleGames.d.ts +0 -3
- package/package.json +59 -59
- 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,103 @@ 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
|
+
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,
|
|
389
501
|
]);
|
|
390
|
-
|
|
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);
|
|
391
521
|
const canGoBack = historyIndex > 0;
|
|
392
522
|
const canGoForward = historyIndex < history.length - 1;
|
|
393
523
|
const pushEntry = react.useCallback((fen, lastSan) => {
|
|
@@ -415,6 +545,20 @@ function usePositionHistory(initialFen) {
|
|
|
415
545
|
setHistoryIndex(nextIndex);
|
|
416
546
|
return entry;
|
|
417
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]);
|
|
418
562
|
const resetHistory = react.useCallback((fen) => {
|
|
419
563
|
const entry = { fen };
|
|
420
564
|
setHistory([entry]);
|
|
@@ -439,39 +583,54 @@ function usePositionHistory(initialFen) {
|
|
|
439
583
|
.slice(historyIndex + 1)
|
|
440
584
|
.map((entry) => entry.lastSan)
|
|
441
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
|
+
}, []);
|
|
442
591
|
return {
|
|
443
592
|
canGoBack,
|
|
444
593
|
canGoForward,
|
|
445
594
|
lineSans,
|
|
446
595
|
forwardSans,
|
|
596
|
+
currentFen,
|
|
447
597
|
pushEntry,
|
|
448
598
|
pushEntries,
|
|
599
|
+
replaceLineEntries,
|
|
449
600
|
goBack,
|
|
450
601
|
goForward,
|
|
602
|
+
goFirst,
|
|
603
|
+
goLast,
|
|
451
604
|
resetHistory,
|
|
452
605
|
};
|
|
453
606
|
}
|
|
454
607
|
|
|
455
|
-
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames,
|
|
608
|
+
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
|
|
609
|
+
var _a, _b, _c;
|
|
456
610
|
const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
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);
|
|
460
618
|
const [position, setPosition] = react.useState(null);
|
|
461
|
-
const [games, setGames] = react.useState(null);
|
|
619
|
+
const [games, setGames] = react.useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
|
|
462
620
|
/** Filter games to those that played this UCI from the current FEN (optional). */
|
|
463
621
|
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
622
|
const [sources, setSources] = react.useState([...ALL_GAME_SOURCES]);
|
|
468
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);
|
|
469
628
|
const [error, setError] = react.useState(null);
|
|
470
|
-
const [variationsTab, setVariationsTab] = react.useState("variations");
|
|
471
629
|
const [selectedVariationKey, setSelectedVariationKey] = react.useState();
|
|
472
630
|
const variationAnimationTimerRef = react.useRef(null);
|
|
473
631
|
const isAnimatingVariationRef = react.useRef(false);
|
|
474
|
-
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);
|
|
475
634
|
const cancelVariationAnimation = react.useCallback(() => {
|
|
476
635
|
if (variationAnimationTimerRef.current !== null) {
|
|
477
636
|
clearTimeout(variationAnimationTimerRef.current);
|
|
@@ -479,7 +638,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
479
638
|
}
|
|
480
639
|
isAnimatingVariationRef.current = false;
|
|
481
640
|
}, []);
|
|
482
|
-
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]);
|
|
483
652
|
const applyNavigation = react.useCallback((nextFen, clearMoveFilter = true) => {
|
|
484
653
|
setFen(nextFen);
|
|
485
654
|
setBoardFen(nextFen);
|
|
@@ -488,39 +657,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
488
657
|
}
|
|
489
658
|
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
|
|
490
659
|
}, [onFenChange]);
|
|
491
|
-
const
|
|
492
|
-
|
|
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);
|
|
493
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).
|
|
494
678
|
react.useEffect(() => {
|
|
495
|
-
if (
|
|
496
|
-
return;
|
|
497
|
-
const currentLineKey = lineSans.join("|");
|
|
498
|
-
if (currentLineKey === initialLineKey) {
|
|
499
|
-
lastAppliedLineKeyRef.current = initialLineKey;
|
|
679
|
+
if (initialLineSans === undefined && onLineSansChange === undefined)
|
|
500
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
|
+
}
|
|
501
692
|
}
|
|
502
693
|
lastAppliedLineKeyRef.current = initialLineKey;
|
|
503
694
|
cancelVariationAnimation();
|
|
504
|
-
resetHistory(EXPLORER_START_FEN);
|
|
505
695
|
setSelectedVariationKey(undefined);
|
|
506
696
|
if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
|
|
507
697
|
const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
|
|
508
698
|
if (result) {
|
|
509
|
-
|
|
699
|
+
replaceLineEntries(EXPLORER_START_FEN, result.entries);
|
|
510
700
|
applyNavigation(result.fen, true);
|
|
511
701
|
return;
|
|
512
702
|
}
|
|
513
703
|
}
|
|
704
|
+
replaceLineEntries(EXPLORER_START_FEN, []);
|
|
514
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
|
|
515
708
|
}, [
|
|
516
709
|
initialLineKey,
|
|
517
710
|
initialLineSans,
|
|
518
|
-
|
|
711
|
+
expectedLineFen,
|
|
519
712
|
cancelVariationAnimation,
|
|
520
|
-
|
|
521
|
-
pushEntries,
|
|
713
|
+
replaceLineEntries,
|
|
522
714
|
applyNavigation,
|
|
523
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]);
|
|
524
726
|
react.useEffect(() => {
|
|
525
727
|
if (readyForLineSyncRef.current)
|
|
526
728
|
return;
|
|
@@ -532,90 +734,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
532
734
|
}
|
|
533
735
|
}, [lineSans, initialLineSans]);
|
|
534
736
|
react.useEffect(() => {
|
|
737
|
+
if (!onLineSansChange)
|
|
738
|
+
return;
|
|
535
739
|
if (!readyForLineSyncRef.current)
|
|
536
740
|
return;
|
|
537
|
-
onLineSansChange
|
|
741
|
+
onLineSansChange(lineSans);
|
|
538
742
|
}, [lineSans, onLineSansChange]);
|
|
539
743
|
react.useEffect(() => {
|
|
540
|
-
if (fenProp === undefined
|
|
744
|
+
if (fenProp === undefined)
|
|
745
|
+
return;
|
|
746
|
+
if (fenProp === queryFenRef.current)
|
|
541
747
|
return;
|
|
542
748
|
cancelVariationAnimation();
|
|
543
749
|
resetHistory(fenProp);
|
|
544
750
|
applyNavigation(fenProp, true);
|
|
545
|
-
}, [fenProp,
|
|
751
|
+
}, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
|
|
546
752
|
react.useEffect(() => {
|
|
547
753
|
let cancelled = false;
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
}
|
|
550
767
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
551
768
|
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)
|
|
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)
|
|
564
774
|
return;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!pos) {
|
|
568
|
-
setError("No explorer data for this position yet");
|
|
775
|
+
if (pos) {
|
|
776
|
+
positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
|
|
569
777
|
}
|
|
778
|
+
setPosition(pos);
|
|
779
|
+
setLoadedPositionFen(requestedFen);
|
|
780
|
+
setLoadedPositionFilterKey(positionCacheKey(requestedFen));
|
|
570
781
|
}
|
|
571
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) {
|
|
572
856
|
if (!cancelled) {
|
|
573
|
-
|
|
857
|
+
setGames(null);
|
|
574
858
|
}
|
|
575
859
|
}
|
|
576
860
|
finally {
|
|
577
861
|
if (!cancelled)
|
|
578
|
-
|
|
862
|
+
setGamesLoading(false);
|
|
579
863
|
}
|
|
580
864
|
}))();
|
|
581
865
|
return () => {
|
|
582
866
|
cancelled = true;
|
|
583
867
|
};
|
|
584
868
|
}, [
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
869
|
+
queryFen,
|
|
870
|
+
positionReady,
|
|
871
|
+
isStartPosition,
|
|
588
872
|
gamesMoveFilterUci,
|
|
589
|
-
topOnly,
|
|
590
873
|
sources,
|
|
591
|
-
fetchPosition,
|
|
592
874
|
fetchPositionGames,
|
|
875
|
+
gamesCacheKey,
|
|
593
876
|
]);
|
|
594
877
|
const handleMoveSelect = react.useCallback((move) => {
|
|
595
878
|
if (isAnimatingVariationRef.current) {
|
|
596
879
|
cancelVariationAnimation();
|
|
597
|
-
setBoardFen(
|
|
880
|
+
setBoardFen(queryFen);
|
|
598
881
|
setSelectedVariationKey(undefined);
|
|
599
882
|
}
|
|
600
|
-
const nextFen = fenAfterUci(
|
|
883
|
+
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
601
884
|
if (!nextFen)
|
|
602
885
|
return;
|
|
603
886
|
pushEntry(nextFen, move.san);
|
|
604
887
|
setSelectedVariationKey(undefined);
|
|
605
888
|
applyNavigation(nextFen, true);
|
|
606
|
-
}, [
|
|
889
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
607
890
|
const handleLineSelect = react.useCallback((line) => {
|
|
608
891
|
if (isAnimatingVariationRef.current)
|
|
609
892
|
return;
|
|
610
893
|
cancelVariationAnimation();
|
|
611
|
-
let
|
|
894
|
+
let lineFen = queryFen;
|
|
612
895
|
const entries = [];
|
|
613
896
|
for (let i = 0; i < line.uciPath.length; i += 1) {
|
|
614
|
-
const nextFen = fenAfterUci(
|
|
897
|
+
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
615
898
|
if (!nextFen)
|
|
616
899
|
return;
|
|
617
900
|
entries.push({ fen: nextFen, lastSan: line.moves[i].san });
|
|
618
|
-
|
|
901
|
+
lineFen = nextFen;
|
|
619
902
|
}
|
|
620
903
|
if (entries.length === 0)
|
|
621
904
|
return;
|
|
@@ -639,109 +922,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
639
922
|
}
|
|
640
923
|
};
|
|
641
924
|
tick();
|
|
642
|
-
}, [
|
|
925
|
+
}, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
|
|
643
926
|
const handlePieceDrop = react.useCallback((sourceSquare, targetSquare, piece) => {
|
|
644
927
|
if (isAnimatingVariationRef.current) {
|
|
645
928
|
cancelVariationAnimation();
|
|
646
|
-
setBoardFen(
|
|
929
|
+
setBoardFen(queryFen);
|
|
647
930
|
setSelectedVariationKey(undefined);
|
|
648
931
|
}
|
|
649
|
-
const result = applyBoardMove(
|
|
932
|
+
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
650
933
|
if (!result)
|
|
651
934
|
return false;
|
|
652
935
|
pushEntry(result.fen, result.san);
|
|
653
936
|
setSelectedVariationKey(undefined);
|
|
654
937
|
applyNavigation(result.fen, true);
|
|
655
938
|
return true;
|
|
656
|
-
}, [
|
|
939
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
657
940
|
const handleBack = react.useCallback(() => {
|
|
658
941
|
if (isAnimatingVariationRef.current) {
|
|
659
942
|
cancelVariationAnimation();
|
|
660
|
-
setBoardFen(
|
|
943
|
+
setBoardFen(queryFen);
|
|
661
944
|
setSelectedVariationKey(undefined);
|
|
662
945
|
return;
|
|
663
946
|
}
|
|
664
947
|
const entry = goBack();
|
|
665
948
|
if (entry)
|
|
666
949
|
applyNavigation(entry.fen, true);
|
|
667
|
-
}, [
|
|
950
|
+
}, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
|
|
668
951
|
const handleForward = react.useCallback(() => {
|
|
669
952
|
if (isAnimatingVariationRef.current) {
|
|
670
953
|
cancelVariationAnimation();
|
|
671
|
-
setBoardFen(
|
|
954
|
+
setBoardFen(queryFen);
|
|
672
955
|
setSelectedVariationKey(undefined);
|
|
673
956
|
return;
|
|
674
957
|
}
|
|
675
958
|
const entry = goForward();
|
|
676
959
|
if (entry)
|
|
677
960
|
applyNavigation(entry.fen, true);
|
|
678
|
-
}, [
|
|
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]);
|
|
679
984
|
const lineLabel = react.useMemo(() => {
|
|
680
985
|
if (lineSans.length > 0) {
|
|
681
|
-
return lineSans
|
|
986
|
+
return formatNumberedLineSans(lineSans);
|
|
682
987
|
}
|
|
683
|
-
if (position) {
|
|
988
|
+
if (position && typeof position.totalGames === "number") {
|
|
684
989
|
return `Starting position (${position.totalGames.toLocaleString()} games)`;
|
|
685
990
|
}
|
|
686
991
|
return "";
|
|
687
992
|
}, [lineSans, position]);
|
|
688
993
|
return {
|
|
689
|
-
fen,
|
|
994
|
+
fen: queryFen,
|
|
690
995
|
boardFen,
|
|
691
996
|
position,
|
|
692
997
|
games,
|
|
693
998
|
gamesMoveFilterUci,
|
|
694
|
-
minElo,
|
|
695
|
-
maxElo,
|
|
696
|
-
topOnly,
|
|
697
999
|
sources,
|
|
1000
|
+
lineSans,
|
|
698
1001
|
loading,
|
|
1002
|
+
showPositionLoading,
|
|
1003
|
+
gamesLoading,
|
|
1004
|
+
positionReady,
|
|
1005
|
+
displayMoves,
|
|
699
1006
|
error,
|
|
700
1007
|
lineLabel,
|
|
701
1008
|
canGoBack,
|
|
702
1009
|
canGoForward,
|
|
703
|
-
variationsTab,
|
|
704
1010
|
forwardSans,
|
|
705
1011
|
selectedVariationKey,
|
|
706
|
-
setMinElo,
|
|
707
|
-
setMaxElo,
|
|
708
|
-
setTopOnly,
|
|
709
1012
|
setSources,
|
|
710
|
-
setVariationsTab,
|
|
711
1013
|
setGamesMoveFilterUci,
|
|
712
1014
|
handleMoveSelect,
|
|
713
1015
|
handleLineSelect,
|
|
714
1016
|
handlePieceDrop,
|
|
715
1017
|
handleBack,
|
|
716
1018
|
handleForward,
|
|
1019
|
+
handleFirst,
|
|
1020
|
+
handleLast,
|
|
717
1021
|
};
|
|
718
1022
|
}
|
|
719
1023
|
|
|
720
|
-
function useVariationLines({ fen,
|
|
721
|
-
const
|
|
722
|
-
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]);
|
|
723
1052
|
react.useEffect(() => {
|
|
724
|
-
if (!enabled
|
|
1053
|
+
if (!enabled) {
|
|
725
1054
|
setLines([]);
|
|
726
1055
|
setLoading(false);
|
|
727
1056
|
return;
|
|
728
1057
|
}
|
|
729
1058
|
let cancelled = false;
|
|
730
|
-
|
|
731
|
-
|
|
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* () {
|
|
732
1068
|
var _a;
|
|
733
1069
|
try {
|
|
734
1070
|
const result = yield fetchPositionVariations({
|
|
735
1071
|
fen,
|
|
736
|
-
mode:
|
|
737
|
-
minElo,
|
|
738
|
-
maxElo,
|
|
1072
|
+
mode: "variations",
|
|
739
1073
|
lineCount,
|
|
740
1074
|
depth: lineDepth,
|
|
741
1075
|
});
|
|
742
1076
|
if (cancelled)
|
|
743
1077
|
return;
|
|
744
|
-
|
|
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);
|
|
745
1081
|
}
|
|
746
1082
|
catch (_b) {
|
|
747
1083
|
if (!cancelled) {
|
|
@@ -757,21 +1093,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
|
|
|
757
1093
|
return () => {
|
|
758
1094
|
cancelled = true;
|
|
759
1095
|
};
|
|
760
|
-
}, [
|
|
761
|
-
|
|
762
|
-
fen,
|
|
763
|
-
tab,
|
|
764
|
-
minElo,
|
|
765
|
-
maxElo,
|
|
766
|
-
fetchPositionVariations,
|
|
767
|
-
lineCount,
|
|
768
|
-
lineDepth,
|
|
769
|
-
]);
|
|
770
|
-
return { lines, loading };
|
|
1096
|
+
}, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
|
|
1097
|
+
return { lines, loading: enabled ? loading : false };
|
|
771
1098
|
}
|
|
772
1099
|
|
|
773
|
-
const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth,
|
|
774
|
-
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, }) => {
|
|
775
1101
|
const referenceData = usePositionReferenceData({
|
|
776
1102
|
fenProp,
|
|
777
1103
|
onFenChange,
|
|
@@ -779,59 +1105,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
779
1105
|
onLineSansChange,
|
|
780
1106
|
fetchPosition,
|
|
781
1107
|
fetchPositionGames,
|
|
782
|
-
|
|
783
|
-
|
|
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,
|
|
784
1119
|
});
|
|
785
|
-
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]);
|
|
786
1138
|
const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
|
|
787
1139
|
fen,
|
|
788
|
-
tab: variationsTab,
|
|
789
|
-
minElo,
|
|
790
|
-
maxElo,
|
|
791
1140
|
fetchPositionVariations,
|
|
792
|
-
enabled:
|
|
1141
|
+
enabled: variationsEnabled && (positionReady || isStartPosition),
|
|
793
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
|
+
]);
|
|
794
1167
|
const outerStyle = {
|
|
795
1168
|
width: "100%",
|
|
796
1169
|
height: fillHeight ? "100%" : "auto",
|
|
797
1170
|
minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
|
|
798
|
-
overflow: "hidden",
|
|
1171
|
+
overflow: fillHeight ? "hidden" : "visible",
|
|
799
1172
|
boxSizing: "border-box",
|
|
800
1173
|
};
|
|
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 : [],
|
|
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,
|
|
809
1180
|
onMoveSelect: handleMoveSelect,
|
|
810
1181
|
}), variationsStrip: renderVariationsStrip({
|
|
811
1182
|
theme,
|
|
812
|
-
tab: variationsTab,
|
|
813
|
-
onTabChange: setVariationsTab,
|
|
814
1183
|
lines: variationLines,
|
|
815
|
-
loading: variationLinesLoading,
|
|
1184
|
+
loading: !variationsEnabled || variationLinesLoading,
|
|
816
1185
|
selectedLineKey: selectedVariationKey,
|
|
817
1186
|
forwardSans,
|
|
818
1187
|
onLineSelect: handleLineSelect,
|
|
819
1188
|
}), gamesPanel: renderGamesPanel({
|
|
820
|
-
games: (
|
|
1189
|
+
games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
|
|
1190
|
+
loading: gamesLoading,
|
|
821
1191
|
lineLabel,
|
|
822
|
-
|
|
823
|
-
maxElo,
|
|
824
|
-
defaultMinElo,
|
|
825
|
-
defaultMaxElo,
|
|
826
|
-
topOnly,
|
|
1192
|
+
lineSans,
|
|
827
1193
|
sources,
|
|
828
|
-
onMinEloChange: setMinElo,
|
|
829
|
-
onMaxEloChange: setMaxElo,
|
|
830
|
-
onTopOnlyChange: setTopOnly,
|
|
831
1194
|
onSourcesChange: setSources,
|
|
832
1195
|
onGameSelect,
|
|
833
1196
|
}) }));
|
|
834
|
-
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 }) }) }));
|
|
835
1198
|
};
|
|
836
1199
|
|
|
837
1200
|
/** Reference explorer with library default layout and renderers (ChessBase-style grid). */
|
|
@@ -1003,7 +1366,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
|
|
|
1003
1366
|
!error &&
|
|
1004
1367
|
!complete &&
|
|
1005
1368
|
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(
|
|
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" }))] }) }));
|
|
1007
1370
|
};
|
|
1008
1371
|
|
|
1009
1372
|
const mockPosition = {
|
|
@@ -1042,10 +1405,6 @@ const mockPosition = {
|
|
|
1042
1405
|
};
|
|
1043
1406
|
const mockGames = {
|
|
1044
1407
|
positionKey: "mock-start",
|
|
1045
|
-
fen: EXPLORER_START_FEN,
|
|
1046
|
-
minElo: 2200,
|
|
1047
|
-
maxElo: 2800,
|
|
1048
|
-
topOnly: false,
|
|
1049
1408
|
games: [
|
|
1050
1409
|
{
|
|
1051
1410
|
gameId: "abc123",
|
|
@@ -1077,7 +1436,7 @@ const mockGames = {
|
|
|
1077
1436
|
},
|
|
1078
1437
|
],
|
|
1079
1438
|
};
|
|
1080
|
-
function mockFetchPosition(
|
|
1439
|
+
function mockFetchPosition(params) {
|
|
1081
1440
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1082
1441
|
return mockPosition;
|
|
1083
1442
|
});
|
|
@@ -1111,7 +1470,11 @@ function mockVariationLines(params, position) {
|
|
|
1111
1470
|
}
|
|
1112
1471
|
function mockFetchPositionVariations(params) {
|
|
1113
1472
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1114
|
-
const position = yield mockFetchPosition(
|
|
1473
|
+
const position = yield mockFetchPosition({
|
|
1474
|
+
fen: params.fen,
|
|
1475
|
+
minElo: params.minElo,
|
|
1476
|
+
maxElo: params.maxElo,
|
|
1477
|
+
});
|
|
1115
1478
|
if (!position)
|
|
1116
1479
|
return null;
|
|
1117
1480
|
return mockVariationLines(params, position);
|
|
@@ -1119,14 +1482,25 @@ function mockFetchPositionVariations(params) {
|
|
|
1119
1482
|
}
|
|
1120
1483
|
function mockFetchPositionGames(params) {
|
|
1121
1484
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1122
|
-
var _a;
|
|
1485
|
+
var _a, _b, _c;
|
|
1123
1486
|
let filtered = params.uci
|
|
1124
1487
|
? mockGames.games.filter((g) => g.nextUci === params.uci)
|
|
1125
1488
|
: mockGames.games;
|
|
1126
1489
|
if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
|
|
1127
1490
|
filtered = filtered.filter((game) => params.sources.includes(game.source));
|
|
1128
1491
|
}
|
|
1129
|
-
|
|
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
|
+
};
|
|
1130
1504
|
});
|
|
1131
1505
|
}
|
|
1132
1506
|
|
|
@@ -1136,6 +1510,9 @@ exports.DefaultBoardNav = DefaultBoardNav;
|
|
|
1136
1510
|
exports.DefaultReferenceLayout = DefaultReferenceLayout;
|
|
1137
1511
|
exports.DefaultReferencePanel = DefaultReferencePanel;
|
|
1138
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;
|
|
1139
1516
|
exports.EXPLORER_START_FEN = EXPLORER_START_FEN;
|
|
1140
1517
|
exports.ExplorerPlaceholder = ExplorerPlaceholder;
|
|
1141
1518
|
exports.GameReplayTrainer = GameReplayTrainer;
|
|
@@ -1151,10 +1528,19 @@ exports.defaultRenderVariationsStrip = defaultRenderVariationsStrip;
|
|
|
1151
1528
|
exports.fenAfterUci = fenAfterUci;
|
|
1152
1529
|
exports.fenAtPly = fenAtPly;
|
|
1153
1530
|
exports.findPlyIndexForFen = findPlyIndexForFen;
|
|
1531
|
+
exports.formatNumberedLineSans = formatNumberedLineSans;
|
|
1532
|
+
exports.gamesSessionKey = gamesSessionKey;
|
|
1154
1533
|
exports.isVariationLineActive = isVariationLineActive;
|
|
1155
1534
|
exports.mockFetchPosition = mockFetchPosition;
|
|
1156
1535
|
exports.mockFetchPositionGames = mockFetchPositionGames;
|
|
1157
1536
|
exports.mockFetchPositionVariations = mockFetchPositionVariations;
|
|
1158
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;
|
|
1159
1544
|
exports.useGameReplayTraining = useGameReplayTraining;
|
|
1545
|
+
exports.variationsSessionKey = variationsSessionKey;
|
|
1160
1546
|
exports.whiteScorePercent = whiteScorePercent;
|