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