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