react-chess-explorer 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +63 -63
- package/dist/features/explorer/components/DefaultBoardNav.d.ts +2 -7
- package/dist/features/explorer/components/PositionGamesPanel.d.ts +5 -10
- package/dist/features/explorer/core/PositionReferenceExplorerCore.d.ts +1 -1
- package/dist/features/explorer/core/renderProps.d.ts +24 -15
- package/dist/features/explorer/defaults/DefaultReferencePanel.d.ts +2 -1
- package/dist/features/explorer/defaults/DefaultVariationsStrip.d.ts +1 -1
- package/dist/features/explorer/explorerSessionCache.d.ts +15 -0
- package/dist/features/explorer/hooks/useExplorerPrefetch.d.ts +11 -0
- package/dist/features/explorer/hooks/usePositionHistory.d.ts +12 -1
- package/dist/features/explorer/hooks/usePositionReferenceData.d.ts +13 -14
- package/dist/features/explorer/hooks/useVariationLines.d.ts +1 -5
- package/dist/features/explorer/index.d.ts +5 -3
- package/dist/features/explorer/mocks.d.ts +2 -2
- package/dist/features/explorer/positionUtils.d.ts +2 -0
- package/dist/features/explorer/seedExplorerStartSession.d.ts +3 -0
- package/dist/features/explorer/types.d.ts +18 -11
- package/dist/features/explorer/variationLines.d.ts +0 -1
- package/dist/index.esm.js +691 -291
- package/dist/index.js +702 -289
- package/dist/stories/PositionReferenceExplorer.stories.d.ts +10 -0
- package/dist/stories/fixtures/nc6MockApi.d.ts +13 -0
- package/dist/stories/fixtures/nc6SampleGames.d.ts +36 -0
- package/package.json +59 -45
- package/dist/features/explorer/components/EloRangeFilter.d.ts +0 -9
- package/dist/features/explorer/components/LineHeader.d.ts +0 -4
package/dist/index.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,24 @@ 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" }), onGameSelect && jsx("th", { style: thStyle })] }) }), jsx("tbody", { children: games.length === 0 ? (jsx("tr", { children: jsx("td", { colSpan: onGameSelect ?
|
|
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, }) => {
|
|
328
|
+
const toggleSource = (source) => {
|
|
329
|
+
if (sources.includes(source)) {
|
|
330
|
+
if (sources.length === 1) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
onSourcesChange(sources.filter((value) => value !== source));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
onSourcesChange([...sources, source]);
|
|
337
|
+
};
|
|
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"] })] })] }));
|
|
339
|
+
};
|
|
293
340
|
|
|
294
341
|
const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxs("div", { style: referenceShellStyle, children: [jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
|
|
295
342
|
const defaultRenderLayout = (props) => (jsx(DefaultReferenceLayout, Object.assign({}, props)));
|
|
@@ -301,34 +348,37 @@ function isVariationLineActive(line, selectedLineKey, forwardSans = []) {
|
|
|
301
348
|
if (forwardSans.length === 0) {
|
|
302
349
|
return false;
|
|
303
350
|
}
|
|
304
|
-
|
|
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; });
|
|
305
356
|
}
|
|
306
357
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
})) })] }));
|
|
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
|
+
})) }) }));
|
|
332
382
|
const defaultRenderVariationsStrip = (props) => jsx(DefaultVariationsStrip, Object.assign({}, props));
|
|
333
383
|
|
|
334
384
|
const defaultRenderStatus = (props) => (jsx(ExplorerStatusBanner, Object.assign({}, props)));
|
|
@@ -367,11 +417,105 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
367
417
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
368
418
|
};
|
|
369
419
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
420
|
+
const ALL_GAME_SOURCES = ["lichess", "twic"];
|
|
421
|
+
|
|
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,
|
|
373
499
|
]);
|
|
374
|
-
|
|
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);
|
|
375
519
|
const canGoBack = historyIndex > 0;
|
|
376
520
|
const canGoForward = historyIndex < history.length - 1;
|
|
377
521
|
const pushEntry = useCallback((fen, lastSan) => {
|
|
@@ -399,6 +543,20 @@ function usePositionHistory(initialFen) {
|
|
|
399
543
|
setHistoryIndex(nextIndex);
|
|
400
544
|
return entry;
|
|
401
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]);
|
|
402
560
|
const resetHistory = useCallback((fen) => {
|
|
403
561
|
const entry = { fen };
|
|
404
562
|
setHistory([entry]);
|
|
@@ -423,38 +581,54 @@ function usePositionHistory(initialFen) {
|
|
|
423
581
|
.slice(historyIndex + 1)
|
|
424
582
|
.map((entry) => entry.lastSan)
|
|
425
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
|
+
}, []);
|
|
426
589
|
return {
|
|
427
590
|
canGoBack,
|
|
428
591
|
canGoForward,
|
|
429
592
|
lineSans,
|
|
430
593
|
forwardSans,
|
|
594
|
+
currentFen,
|
|
431
595
|
pushEntry,
|
|
432
596
|
pushEntries,
|
|
597
|
+
replaceLineEntries,
|
|
433
598
|
goBack,
|
|
434
599
|
goForward,
|
|
600
|
+
goFirst,
|
|
601
|
+
goLast,
|
|
435
602
|
resetHistory,
|
|
436
603
|
};
|
|
437
604
|
}
|
|
438
605
|
|
|
439
|
-
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames,
|
|
606
|
+
function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
|
|
607
|
+
var _a, _b, _c;
|
|
440
608
|
const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
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);
|
|
444
616
|
const [position, setPosition] = useState(null);
|
|
445
|
-
const [games, setGames] = useState(null);
|
|
617
|
+
const [games, setGames] = useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
|
|
446
618
|
/** Filter games to those that played this UCI from the current FEN (optional). */
|
|
447
619
|
const [gamesMoveFilterUci, setGamesMoveFilterUci] = useState();
|
|
448
|
-
const [
|
|
449
|
-
const [maxElo, setMaxElo] = useState(defaultMaxElo);
|
|
450
|
-
const [topOnly, setTopOnly] = useState(false);
|
|
620
|
+
const [sources, setSources] = useState([...ALL_GAME_SOURCES]);
|
|
451
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);
|
|
452
626
|
const [error, setError] = useState(null);
|
|
453
|
-
const [variationsTab, setVariationsTab] = useState("variations");
|
|
454
627
|
const [selectedVariationKey, setSelectedVariationKey] = useState();
|
|
455
628
|
const variationAnimationTimerRef = useRef(null);
|
|
456
629
|
const isAnimatingVariationRef = useRef(false);
|
|
457
|
-
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);
|
|
458
632
|
const cancelVariationAnimation = useCallback(() => {
|
|
459
633
|
if (variationAnimationTimerRef.current !== null) {
|
|
460
634
|
clearTimeout(variationAnimationTimerRef.current);
|
|
@@ -462,7 +636,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
462
636
|
}
|
|
463
637
|
isAnimatingVariationRef.current = false;
|
|
464
638
|
}, []);
|
|
465
|
-
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]);
|
|
466
650
|
const applyNavigation = useCallback((nextFen, clearMoveFilter = true) => {
|
|
467
651
|
setFen(nextFen);
|
|
468
652
|
setBoardFen(nextFen);
|
|
@@ -471,39 +655,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
471
655
|
}
|
|
472
656
|
onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
|
|
473
657
|
}, [onFenChange]);
|
|
474
|
-
const
|
|
475
|
-
|
|
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);
|
|
476
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).
|
|
477
676
|
useEffect(() => {
|
|
478
|
-
if (
|
|
479
|
-
return;
|
|
480
|
-
const currentLineKey = lineSans.join("|");
|
|
481
|
-
if (currentLineKey === initialLineKey) {
|
|
482
|
-
lastAppliedLineKeyRef.current = initialLineKey;
|
|
677
|
+
if (initialLineSans === undefined && onLineSansChange === undefined)
|
|
483
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
|
+
}
|
|
484
690
|
}
|
|
485
691
|
lastAppliedLineKeyRef.current = initialLineKey;
|
|
486
692
|
cancelVariationAnimation();
|
|
487
|
-
resetHistory(EXPLORER_START_FEN);
|
|
488
693
|
setSelectedVariationKey(undefined);
|
|
489
694
|
if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
|
|
490
695
|
const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
|
|
491
696
|
if (result) {
|
|
492
|
-
|
|
697
|
+
replaceLineEntries(EXPLORER_START_FEN, result.entries);
|
|
493
698
|
applyNavigation(result.fen, true);
|
|
494
699
|
return;
|
|
495
700
|
}
|
|
496
701
|
}
|
|
702
|
+
replaceLineEntries(EXPLORER_START_FEN, []);
|
|
497
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
|
|
498
706
|
}, [
|
|
499
707
|
initialLineKey,
|
|
500
708
|
initialLineSans,
|
|
501
|
-
|
|
709
|
+
expectedLineFen,
|
|
502
710
|
cancelVariationAnimation,
|
|
503
|
-
|
|
504
|
-
pushEntries,
|
|
711
|
+
replaceLineEntries,
|
|
505
712
|
applyNavigation,
|
|
506
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]);
|
|
507
724
|
useEffect(() => {
|
|
508
725
|
if (readyForLineSyncRef.current)
|
|
509
726
|
return;
|
|
@@ -515,88 +732,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
515
732
|
}
|
|
516
733
|
}, [lineSans, initialLineSans]);
|
|
517
734
|
useEffect(() => {
|
|
735
|
+
if (!onLineSansChange)
|
|
736
|
+
return;
|
|
518
737
|
if (!readyForLineSyncRef.current)
|
|
519
738
|
return;
|
|
520
|
-
onLineSansChange
|
|
739
|
+
onLineSansChange(lineSans);
|
|
521
740
|
}, [lineSans, onLineSansChange]);
|
|
522
741
|
useEffect(() => {
|
|
523
|
-
if (fenProp === undefined
|
|
742
|
+
if (fenProp === undefined)
|
|
743
|
+
return;
|
|
744
|
+
if (fenProp === queryFenRef.current)
|
|
524
745
|
return;
|
|
525
746
|
cancelVariationAnimation();
|
|
526
747
|
resetHistory(fenProp);
|
|
527
748
|
applyNavigation(fenProp, true);
|
|
528
|
-
}, [fenProp,
|
|
749
|
+
}, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
|
|
529
750
|
useEffect(() => {
|
|
530
751
|
let cancelled = false;
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
}
|
|
533
765
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
534
766
|
try {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
maxElo,
|
|
541
|
-
uci: gamesMoveFilterUci,
|
|
542
|
-
topOnly,
|
|
543
|
-
}),
|
|
544
|
-
]);
|
|
545
|
-
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)
|
|
546
772
|
return;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (!pos) {
|
|
550
|
-
setError("No explorer data for this position yet");
|
|
773
|
+
if (pos) {
|
|
774
|
+
positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
|
|
551
775
|
}
|
|
776
|
+
setPosition(pos);
|
|
777
|
+
setLoadedPositionFen(requestedFen);
|
|
778
|
+
setLoadedPositionFilterKey(positionCacheKey(requestedFen));
|
|
552
779
|
}
|
|
553
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) {
|
|
554
854
|
if (!cancelled) {
|
|
555
|
-
|
|
855
|
+
setGames(null);
|
|
556
856
|
}
|
|
557
857
|
}
|
|
558
858
|
finally {
|
|
559
859
|
if (!cancelled)
|
|
560
|
-
|
|
860
|
+
setGamesLoading(false);
|
|
561
861
|
}
|
|
562
862
|
}))();
|
|
563
863
|
return () => {
|
|
564
864
|
cancelled = true;
|
|
565
865
|
};
|
|
566
866
|
}, [
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
867
|
+
queryFen,
|
|
868
|
+
positionReady,
|
|
869
|
+
isStartPosition,
|
|
570
870
|
gamesMoveFilterUci,
|
|
571
|
-
|
|
572
|
-
fetchPosition,
|
|
871
|
+
sources,
|
|
573
872
|
fetchPositionGames,
|
|
873
|
+
gamesCacheKey,
|
|
574
874
|
]);
|
|
575
875
|
const handleMoveSelect = useCallback((move) => {
|
|
576
876
|
if (isAnimatingVariationRef.current) {
|
|
577
877
|
cancelVariationAnimation();
|
|
578
|
-
setBoardFen(
|
|
878
|
+
setBoardFen(queryFen);
|
|
579
879
|
setSelectedVariationKey(undefined);
|
|
580
880
|
}
|
|
581
|
-
const nextFen = fenAfterUci(
|
|
881
|
+
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
582
882
|
if (!nextFen)
|
|
583
883
|
return;
|
|
584
884
|
pushEntry(nextFen, move.san);
|
|
585
885
|
setSelectedVariationKey(undefined);
|
|
586
886
|
applyNavigation(nextFen, true);
|
|
587
|
-
}, [
|
|
887
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
588
888
|
const handleLineSelect = useCallback((line) => {
|
|
589
889
|
if (isAnimatingVariationRef.current)
|
|
590
890
|
return;
|
|
591
891
|
cancelVariationAnimation();
|
|
592
|
-
let
|
|
892
|
+
let lineFen = queryFen;
|
|
593
893
|
const entries = [];
|
|
594
894
|
for (let i = 0; i < line.uciPath.length; i += 1) {
|
|
595
|
-
const nextFen = fenAfterUci(
|
|
895
|
+
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
596
896
|
if (!nextFen)
|
|
597
897
|
return;
|
|
598
898
|
entries.push({ fen: nextFen, lastSan: line.moves[i].san });
|
|
599
|
-
|
|
899
|
+
lineFen = nextFen;
|
|
600
900
|
}
|
|
601
901
|
if (entries.length === 0)
|
|
602
902
|
return;
|
|
@@ -620,107 +920,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
620
920
|
}
|
|
621
921
|
};
|
|
622
922
|
tick();
|
|
623
|
-
}, [
|
|
923
|
+
}, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
|
|
624
924
|
const handlePieceDrop = useCallback((sourceSquare, targetSquare, piece) => {
|
|
625
925
|
if (isAnimatingVariationRef.current) {
|
|
626
926
|
cancelVariationAnimation();
|
|
627
|
-
setBoardFen(
|
|
927
|
+
setBoardFen(queryFen);
|
|
628
928
|
setSelectedVariationKey(undefined);
|
|
629
929
|
}
|
|
630
|
-
const result = applyBoardMove(
|
|
930
|
+
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
631
931
|
if (!result)
|
|
632
932
|
return false;
|
|
633
933
|
pushEntry(result.fen, result.san);
|
|
634
934
|
setSelectedVariationKey(undefined);
|
|
635
935
|
applyNavigation(result.fen, true);
|
|
636
936
|
return true;
|
|
637
|
-
}, [
|
|
937
|
+
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
638
938
|
const handleBack = useCallback(() => {
|
|
639
939
|
if (isAnimatingVariationRef.current) {
|
|
640
940
|
cancelVariationAnimation();
|
|
641
|
-
setBoardFen(
|
|
941
|
+
setBoardFen(queryFen);
|
|
642
942
|
setSelectedVariationKey(undefined);
|
|
643
943
|
return;
|
|
644
944
|
}
|
|
645
945
|
const entry = goBack();
|
|
646
946
|
if (entry)
|
|
647
947
|
applyNavigation(entry.fen, true);
|
|
648
|
-
}, [
|
|
948
|
+
}, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
|
|
649
949
|
const handleForward = useCallback(() => {
|
|
650
950
|
if (isAnimatingVariationRef.current) {
|
|
651
951
|
cancelVariationAnimation();
|
|
652
|
-
setBoardFen(
|
|
952
|
+
setBoardFen(queryFen);
|
|
653
953
|
setSelectedVariationKey(undefined);
|
|
654
954
|
return;
|
|
655
955
|
}
|
|
656
956
|
const entry = goForward();
|
|
657
957
|
if (entry)
|
|
658
958
|
applyNavigation(entry.fen, true);
|
|
659
|
-
}, [
|
|
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]);
|
|
660
982
|
const lineLabel = useMemo(() => {
|
|
661
983
|
if (lineSans.length > 0) {
|
|
662
|
-
return lineSans
|
|
984
|
+
return formatNumberedLineSans(lineSans);
|
|
663
985
|
}
|
|
664
|
-
if (position) {
|
|
986
|
+
if (position && typeof position.totalGames === "number") {
|
|
665
987
|
return `Starting position (${position.totalGames.toLocaleString()} games)`;
|
|
666
988
|
}
|
|
667
989
|
return "";
|
|
668
990
|
}, [lineSans, position]);
|
|
669
991
|
return {
|
|
670
|
-
fen,
|
|
992
|
+
fen: queryFen,
|
|
671
993
|
boardFen,
|
|
672
994
|
position,
|
|
673
995
|
games,
|
|
674
996
|
gamesMoveFilterUci,
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
topOnly,
|
|
997
|
+
sources,
|
|
998
|
+
lineSans,
|
|
678
999
|
loading,
|
|
1000
|
+
showPositionLoading,
|
|
1001
|
+
gamesLoading,
|
|
1002
|
+
positionReady,
|
|
1003
|
+
displayMoves,
|
|
679
1004
|
error,
|
|
680
1005
|
lineLabel,
|
|
681
1006
|
canGoBack,
|
|
682
1007
|
canGoForward,
|
|
683
|
-
variationsTab,
|
|
684
1008
|
forwardSans,
|
|
685
1009
|
selectedVariationKey,
|
|
686
|
-
|
|
687
|
-
setMaxElo,
|
|
688
|
-
setTopOnly,
|
|
689
|
-
setVariationsTab,
|
|
1010
|
+
setSources,
|
|
690
1011
|
setGamesMoveFilterUci,
|
|
691
1012
|
handleMoveSelect,
|
|
692
1013
|
handleLineSelect,
|
|
693
1014
|
handlePieceDrop,
|
|
694
1015
|
handleBack,
|
|
695
1016
|
handleForward,
|
|
1017
|
+
handleFirst,
|
|
1018
|
+
handleLast,
|
|
696
1019
|
};
|
|
697
1020
|
}
|
|
698
1021
|
|
|
699
|
-
function useVariationLines({ fen,
|
|
700
|
-
const
|
|
701
|
-
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]);
|
|
702
1050
|
useEffect(() => {
|
|
703
|
-
if (!enabled
|
|
1051
|
+
if (!enabled) {
|
|
704
1052
|
setLines([]);
|
|
705
1053
|
setLoading(false);
|
|
706
1054
|
return;
|
|
707
1055
|
}
|
|
708
1056
|
let cancelled = false;
|
|
709
|
-
|
|
710
|
-
|
|
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* () {
|
|
711
1066
|
var _a;
|
|
712
1067
|
try {
|
|
713
1068
|
const result = yield fetchPositionVariations({
|
|
714
1069
|
fen,
|
|
715
|
-
mode:
|
|
716
|
-
minElo,
|
|
717
|
-
maxElo,
|
|
1070
|
+
mode: "variations",
|
|
718
1071
|
lineCount,
|
|
719
1072
|
depth: lineDepth,
|
|
720
1073
|
});
|
|
721
1074
|
if (cancelled)
|
|
722
1075
|
return;
|
|
723
|
-
|
|
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);
|
|
724
1079
|
}
|
|
725
1080
|
catch (_b) {
|
|
726
1081
|
if (!cancelled) {
|
|
@@ -736,21 +1091,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
|
|
|
736
1091
|
return () => {
|
|
737
1092
|
cancelled = true;
|
|
738
1093
|
};
|
|
739
|
-
}, [
|
|
740
|
-
|
|
741
|
-
fen,
|
|
742
|
-
tab,
|
|
743
|
-
minElo,
|
|
744
|
-
maxElo,
|
|
745
|
-
fetchPositionVariations,
|
|
746
|
-
lineCount,
|
|
747
|
-
lineDepth,
|
|
748
|
-
]);
|
|
749
|
-
return { lines, loading };
|
|
1094
|
+
}, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
|
|
1095
|
+
return { lines, loading: enabled ? loading : false };
|
|
750
1096
|
}
|
|
751
1097
|
|
|
752
|
-
const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth,
|
|
753
|
-
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, }) => {
|
|
754
1099
|
const referenceData = usePositionReferenceData({
|
|
755
1100
|
fenProp,
|
|
756
1101
|
onFenChange,
|
|
@@ -758,57 +1103,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
758
1103
|
onLineSansChange,
|
|
759
1104
|
fetchPosition,
|
|
760
1105
|
fetchPositionGames,
|
|
761
|
-
|
|
762
|
-
|
|
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,
|
|
763
1117
|
});
|
|
764
|
-
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]);
|
|
765
1136
|
const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
|
|
766
1137
|
fen,
|
|
767
|
-
tab: variationsTab,
|
|
768
|
-
minElo,
|
|
769
|
-
maxElo,
|
|
770
1138
|
fetchPositionVariations,
|
|
771
|
-
enabled:
|
|
1139
|
+
enabled: variationsEnabled && (positionReady || isStartPosition),
|
|
772
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
|
+
]);
|
|
773
1165
|
const outerStyle = {
|
|
774
1166
|
width: "100%",
|
|
775
1167
|
height: fillHeight ? "100%" : "auto",
|
|
776
1168
|
minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
|
|
777
|
-
overflow: "hidden",
|
|
1169
|
+
overflow: fillHeight ? "hidden" : "visible",
|
|
778
1170
|
boxSizing: "border-box",
|
|
779
1171
|
};
|
|
780
|
-
const board = (jsxs(Fragment, { children: [jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: boardFen, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }) }), renderBoardNav(
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const referencePanel = (jsx(DefaultReferencePanel, { theme: theme, status: renderStatus({ error, loading }), moveStats: renderMoveStats({
|
|
787
|
-
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,
|
|
788
1178
|
onMoveSelect: handleMoveSelect,
|
|
789
1179
|
}), variationsStrip: renderVariationsStrip({
|
|
790
1180
|
theme,
|
|
791
|
-
tab: variationsTab,
|
|
792
|
-
onTabChange: setVariationsTab,
|
|
793
1181
|
lines: variationLines,
|
|
794
|
-
loading: variationLinesLoading,
|
|
1182
|
+
loading: !variationsEnabled || variationLinesLoading,
|
|
795
1183
|
selectedLineKey: selectedVariationKey,
|
|
796
1184
|
forwardSans,
|
|
797
1185
|
onLineSelect: handleLineSelect,
|
|
798
1186
|
}), gamesPanel: renderGamesPanel({
|
|
799
|
-
games: (
|
|
1187
|
+
games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
|
|
1188
|
+
loading: gamesLoading,
|
|
800
1189
|
lineLabel,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
defaultMaxElo,
|
|
805
|
-
topOnly,
|
|
806
|
-
onMinEloChange: setMinElo,
|
|
807
|
-
onMaxEloChange: setMaxElo,
|
|
808
|
-
onTopOnlyChange: setTopOnly,
|
|
1190
|
+
lineSans,
|
|
1191
|
+
sources,
|
|
1192
|
+
onSourcesChange: setSources,
|
|
809
1193
|
onGameSelect,
|
|
810
1194
|
}) }));
|
|
811
|
-
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 }) }) }));
|
|
812
1196
|
};
|
|
813
1197
|
|
|
814
1198
|
/** Reference explorer with library default layout and renderers (ChessBase-style grid). */
|
|
@@ -1016,14 +1400,9 @@ const mockPosition = {
|
|
|
1016
1400
|
avgElo: 2435,
|
|
1017
1401
|
},
|
|
1018
1402
|
],
|
|
1019
|
-
sampleGameIds: ["abc123", "def456"],
|
|
1020
1403
|
};
|
|
1021
1404
|
const mockGames = {
|
|
1022
1405
|
positionKey: "mock-start",
|
|
1023
|
-
fen: EXPLORER_START_FEN,
|
|
1024
|
-
minElo: 2200,
|
|
1025
|
-
maxElo: 2800,
|
|
1026
|
-
topOnly: false,
|
|
1027
1406
|
games: [
|
|
1028
1407
|
{
|
|
1029
1408
|
gameId: "abc123",
|
|
@@ -1037,6 +1416,7 @@ const mockGames = {
|
|
|
1037
1416
|
nextSan: "e4",
|
|
1038
1417
|
nextUci: "e2e4",
|
|
1039
1418
|
avgElo: 2843,
|
|
1419
|
+
source: "lichess",
|
|
1040
1420
|
},
|
|
1041
1421
|
{
|
|
1042
1422
|
gameId: "def456",
|
|
@@ -1050,10 +1430,11 @@ const mockGames = {
|
|
|
1050
1430
|
nextSan: "d4",
|
|
1051
1431
|
nextUci: "d2d4",
|
|
1052
1432
|
avgElo: 2778,
|
|
1433
|
+
source: "twic",
|
|
1053
1434
|
},
|
|
1054
1435
|
],
|
|
1055
1436
|
};
|
|
1056
|
-
function mockFetchPosition(
|
|
1437
|
+
function mockFetchPosition(params) {
|
|
1057
1438
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1058
1439
|
return mockPosition;
|
|
1059
1440
|
});
|
|
@@ -1087,7 +1468,11 @@ function mockVariationLines(params, position) {
|
|
|
1087
1468
|
}
|
|
1088
1469
|
function mockFetchPositionVariations(params) {
|
|
1089
1470
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1090
|
-
const position = yield mockFetchPosition(
|
|
1471
|
+
const position = yield mockFetchPosition({
|
|
1472
|
+
fen: params.fen,
|
|
1473
|
+
minElo: params.minElo,
|
|
1474
|
+
maxElo: params.maxElo,
|
|
1475
|
+
});
|
|
1091
1476
|
if (!position)
|
|
1092
1477
|
return null;
|
|
1093
1478
|
return mockVariationLines(params, position);
|
|
@@ -1095,11 +1480,26 @@ function mockFetchPositionVariations(params) {
|
|
|
1095
1480
|
}
|
|
1096
1481
|
function mockFetchPositionGames(params) {
|
|
1097
1482
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1098
|
-
|
|
1483
|
+
var _a, _b, _c;
|
|
1484
|
+
let filtered = params.uci
|
|
1099
1485
|
? mockGames.games.filter((g) => g.nextUci === params.uci)
|
|
1100
1486
|
: mockGames.games;
|
|
1101
|
-
|
|
1487
|
+
if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
|
|
1488
|
+
filtered = filtered.filter((game) => params.sources.includes(game.source));
|
|
1489
|
+
}
|
|
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
|
+
};
|
|
1102
1502
|
});
|
|
1103
1503
|
}
|
|
1104
1504
|
|
|
1105
|
-
export { 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 };
|