chesspal 0.3.2 → 0.3.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 +49 -10
- package/dist/commands/analysis/analyse.d.ts +4 -1
- package/dist/commands/analysis/analyse.js +46 -12
- package/dist/commands/analysis/index.d.ts +1 -0
- package/dist/commands/analysis/index.js +2 -1
- package/dist/commands/analysis/site-games.d.ts +27 -0
- package/dist/commands/analysis/site-games.js +84 -0
- package/dist/commands/answers/agents.d.ts +1 -1
- package/dist/commands/answers/agents.js +8 -4
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +3 -2
- package/dist/commands/profiles/index.d.ts +1 -1
- package/dist/commands/profiles/index.js +1 -1
- package/dist/commands/profiles/profiles.d.ts +3 -1
- package/dist/commands/profiles/profiles.js +2 -1
- package/dist/commands/registry/context.d.ts +4 -1
- package/dist/commands/registry/registry.d.ts +2 -0
- package/dist/core/accounts.d.ts +11 -0
- package/dist/core/accounts.js +14 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/page/app/app.js +37 -13
- package/dist/page/app/served.js +8 -3
- package/dist/page/dialogs/analyses.js +3 -13
- package/dist/page/dialogs/dialogs.css +102 -0
- package/dist/page/dialogs/outcome.js +13 -0
- package/dist/page/dialogs/site-games.js +71 -0
- package/dist/page/dialogs/studied.js +3 -14
- package/dist/page/dialogs/study.js +30 -13
- package/dist/page/game/column.js +1 -1
- package/dist/page/game/game.css +25 -2
- package/dist/page/guide/en.md +1 -0
- package/dist/page/guide/it.md +1 -0
- package/dist/page/helps/helps.css +100 -23
- package/dist/page/helps/helps.js +7 -7
- package/dist/page/table/window.js +6 -5
- package/dist/page/talk/talk.js +2 -2
- package/dist/page/text.js +44 -4
- package/dist/server/bell/bell.js +14 -1
- package/dist/server/mcp/mcp.js +2 -2
- package/dist/server/page/analyses.d.ts +5 -0
- package/dist/server/page/analyses.js +18 -1
- package/dist/server/page/attach.js +8 -3
- package/dist/server/page/game.js +8 -5
- package/dist/server/page/page.js +17 -2
- package/dist/sites/chesscom.d.ts +1 -1
- package/dist/sites/chesscom.js +1 -1
- package/dist/sites/games/chesscom.d.ts +4 -0
- package/dist/sites/games/chesscom.js +54 -0
- package/dist/sites/games/game.d.ts +34 -0
- package/dist/sites/games/game.js +64 -0
- package/dist/sites/games/games.d.ts +14 -0
- package/dist/sites/games/games.js +22 -0
- package/dist/sites/games/index.d.ts +2 -0
- package/dist/sites/games/index.js +3 -0
- package/dist/sites/games/lichess.d.ts +4 -0
- package/dist/sites/games/lichess.js +45 -0
- package/dist/sites/index.d.ts +2 -1
- package/dist/sites/index.js +5 -3
- package/dist/sites/lichess.d.ts +1 -1
- package/dist/sites/lichess.js +1 -1
- package/dist/sites/request/index.d.ts +1 -0
- package/dist/sites/request/index.js +2 -0
- package/dist/sites/{request.d.ts → request/request.d.ts} +7 -0
- package/dist/sites/request/request.js +37 -0
- package/dist/sites/sites.d.ts +1 -1
- package/dist/sites/sites.js +1 -1
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
- package/dist/sites/request.js +0 -15
package/dist/page/app/app.js
CHANGED
|
@@ -43,7 +43,7 @@ import { textFor } from "../text.js";
|
|
|
43
43
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
44
44
|
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
45
45
|
import { phaseOf, verdictOf } from "./phase.js";
|
|
46
|
-
import { post, sendMessage, sendMove, useServed } from "./served.js";
|
|
46
|
+
import { ask, post, sendMessage, sendMove, useServed } from "./served.js";
|
|
47
47
|
import { tabTitle, useTabTitle } from "./title.js";
|
|
48
48
|
/**
|
|
49
49
|
* The page: the board of the game in the player's folder, where the player moves on their turn, the talk with the AI beside it, and the game's column; in the
|
|
@@ -71,12 +71,23 @@ export function App({ start }) {
|
|
|
71
71
|
const { data: served, problem, offline, reload } = useServed(() => setDialog("new-game"));
|
|
72
72
|
// Nuova partita with no chat at the table asks for one first (D-107): the chat's window, then the new game's, however the first closes.
|
|
73
73
|
const [chatFirst, setChatFirst] = useState(false);
|
|
74
|
-
|
|
74
|
+
// At the table with no chat, and none opening: what asks for the AI opens the chat's window first, to choose its host and model (D-107, D-109).
|
|
75
|
+
const chatMissing = () => {
|
|
75
76
|
const table = offline ? undefined : served?.table;
|
|
76
|
-
|
|
77
|
+
return table !== undefined && !table.chat && table.opening === undefined;
|
|
78
|
+
};
|
|
79
|
+
// The chat's window opened for an analysis asks who reviews the game, not whom the player plays (Rosa, Zoe).
|
|
80
|
+
const [chatToReview, setChatToReview] = useState(false);
|
|
81
|
+
const askNewGame = () => {
|
|
82
|
+
const first = chatMissing();
|
|
77
83
|
setChatFirst(first);
|
|
84
|
+
setChatToReview(false);
|
|
78
85
|
setDialog(first ? "chat" : "new-game");
|
|
79
86
|
};
|
|
87
|
+
const askChat = () => {
|
|
88
|
+
setChatToReview(false);
|
|
89
|
+
setDialog("chat");
|
|
90
|
+
};
|
|
80
91
|
const closeChat = () => {
|
|
81
92
|
setDialog(chatFirst ? "new-game" : undefined);
|
|
82
93
|
setChatFirst(false);
|
|
@@ -302,6 +313,8 @@ export function App({ start }) {
|
|
|
302
313
|
const summary = studied === undefined ? undefined : gameReviewOf(live.record);
|
|
303
314
|
// An AI is going over the game only when the server says so: with no chat at it, nobody is (Stefano).
|
|
304
315
|
const reviewing = summary === undefined && studied?.reviewing === true && !offline;
|
|
316
|
+
// With nobody at it, a review not finished says so, and the player brings the game back to the AI (D-109).
|
|
317
|
+
const completeIt = summary === undefined && studied?.incomplete === true && !offline ? () => void studyGame({ game: studied.name }) : undefined;
|
|
305
318
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
306
319
|
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
307
320
|
const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
|
|
@@ -320,7 +333,7 @@ export function App({ start }) {
|
|
|
320
333
|
// Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
|
|
321
334
|
// On the table, what its chat is doing stands there, and in a game with no chat at it a line that opens the chat's window (D-107).
|
|
322
335
|
const table = offline ? undefined : served.table;
|
|
323
|
-
const atTable = table === undefined ? null : tableNote({ table, name, inGame: going, onOpen:
|
|
336
|
+
const atTable = table === undefined ? null : tableNote({ table, name, inGame: going, onOpen: askChat, text });
|
|
324
337
|
const asleepNote = atTable !== null
|
|
325
338
|
? atTable
|
|
326
339
|
: asleep && table?.chat !== false
|
|
@@ -402,15 +415,19 @@ export function App({ start }) {
|
|
|
402
415
|
};
|
|
403
416
|
// In analysis the AI's note goes with the other side's card and the talk with the player's, turned round with the board as in a game (D-101, Zoe).
|
|
404
417
|
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place, picture: pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
|
|
405
|
-
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
|
|
418
|
+
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog. One the AI has to go over, with no
|
|
419
|
+
// chat at the table, opens the chat's window: the analyst works with the chat's model (D-109).
|
|
406
420
|
const studyGame = async (studied) => {
|
|
407
|
-
const
|
|
408
|
-
if (refused
|
|
409
|
-
|
|
421
|
+
const sent = await ask("/api/study", studied, text, name);
|
|
422
|
+
if ("refused" in sent) {
|
|
423
|
+
return sent.refused.text;
|
|
410
424
|
}
|
|
411
|
-
|
|
425
|
+
const toReview = sent.answer.reviewed === false && chatMissing();
|
|
426
|
+
setChatToReview(toReview);
|
|
427
|
+
setDialog(toReview ? "chat" : undefined);
|
|
428
|
+
return undefined;
|
|
412
429
|
};
|
|
413
|
-
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
430
|
+
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), onProfile: () => setDialog("profile"), you: profile.name, accounts: profile.accounts, ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
414
431
|
? null
|
|
415
432
|
: (_jsx(UpdateDialog, { open: dialog === "update", version: served.update.version, current: served.version, installing: served.update.installing === true, installed: served.update.installed === true, going: going, chat: served.table.chat ? (served.table.held ? "held" : "byHand") : undefined, name: name, onClose: () => setDialog(undefined), text: text })), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true
|
|
416
433
|
? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) })
|
|
@@ -419,11 +436,11 @@ export function App({ start }) {
|
|
|
419
436
|
: null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
420
437
|
? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
|
|
421
438
|
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
|
|
422
|
-
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
439
|
+
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
423
440
|
setDraft(words);
|
|
424
441
|
setUnsent(undefined);
|
|
425
442
|
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
|
|
426
|
-
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen:
|
|
443
|
+
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
427
444
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
428
445
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
429
446
|
setDraft(words);
|
|
@@ -434,7 +451,14 @@ export function App({ start }) {
|
|
|
434
451
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
435
452
|
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleepNote !== null || clockWaits)
|
|
436
453
|
? (_jsxs("div", { class: "over-board", children: [asleepNote, startClocks] }))
|
|
437
|
-
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : {
|
|
454
|
+
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : {
|
|
455
|
+
analysis: {
|
|
456
|
+
by: analyst,
|
|
457
|
+
...(summary === undefined ? {} : { summary }),
|
|
458
|
+
...(reviewing ? { reviewing: true } : {}),
|
|
459
|
+
...(completeIt === undefined ? {} : { onComplete: completeIt }),
|
|
460
|
+
},
|
|
461
|
+
}), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
|
|
438
462
|
}
|
|
439
463
|
/** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
|
|
440
464
|
function pictureOf({ who }, profile, size, mood) {
|
package/dist/page/app/served.js
CHANGED
|
@@ -56,17 +56,22 @@ export function useServed(onNewGame) {
|
|
|
56
56
|
* it sends the player, or undefined.
|
|
57
57
|
*/
|
|
58
58
|
export async function post(path, body, text, name) {
|
|
59
|
+
const sent = await ask(path, body, text, name);
|
|
60
|
+
return "refused" in sent ? sent.refused : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Sends a request to the server as `post` does: its answer, or the reason it was refused. */
|
|
63
|
+
export async function ask(path, body, text, name) {
|
|
59
64
|
try {
|
|
60
65
|
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
61
66
|
if (response.ok) {
|
|
62
|
-
return
|
|
67
|
+
return { answer: await response.json().catch(() => ({})) };
|
|
63
68
|
}
|
|
64
69
|
const refused = (await response.json());
|
|
65
70
|
const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
|
|
66
|
-
return { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true };
|
|
71
|
+
return { refused: { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
69
|
-
return { text: String(error), words: false };
|
|
74
|
+
return { refused: { text: String(error), words: false } };
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
/** Sends the player's move to the server, which plays it as the chat would, with the words written for the AI: the reason it was refused, or undefined. */
|
|
@@ -2,10 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
|
|
|
2
2
|
// The list of the analyses (D-92): the games analysed or brought to study, found by a search, to open again, analyse again or delete, each row with its
|
|
3
3
|
// menu over the dialog and the question it asks in its place.
|
|
4
4
|
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
5
|
+
import { accountSide } from "../../core/index.js";
|
|
5
6
|
import { ResultTile } from "../game/result-tile.js";
|
|
6
7
|
import { signature } from "../players/model-name.js";
|
|
8
|
+
import { OutcomeBadge, outcomeOf } from "./outcome.js";
|
|
7
9
|
import { searched } from "./search.js";
|
|
8
|
-
import {
|
|
10
|
+
import { playerIn, studiedSides } from "./studied.js";
|
|
9
11
|
/** The list, read anew at each opening: the search, the row that loads a game, then a row for each game found, with its menu or its question. */
|
|
10
12
|
export function Analyses({ onStudy, onLoad, you, accounts, ai, text }) {
|
|
11
13
|
const words = text.study;
|
|
@@ -109,15 +111,10 @@ function searchText(item, sides, outcome, text) {
|
|
|
109
111
|
const went = outcome === undefined ? undefined : words.outcome[outcome];
|
|
110
112
|
return [sides, item.aiModel?.model, by(item), item.annotator?.model, dayOf(item.date, text.language), item.opening, scoreOf(item.result), went, state].join(" ");
|
|
111
113
|
}
|
|
112
|
-
/** How a game went for the player, as chess.com marks it: a green square with a plus, a red one with a minus, a grey one with an equals sign. */
|
|
113
|
-
function OutcomeBadge({ outcome, words }) {
|
|
114
|
-
return (_jsxs("svg", { class: `outcome ${outcome}`, viewBox: "0 0 16 16", role: "img", "aria-label": words.outcome[outcome], children: [_jsx("title", { children: words.outcome[outcome] }), _jsx("rect", { width: "16", height: "16", rx: "4" }), _jsx("path", { d: outcomeSigns[outcome] })] }));
|
|
115
|
-
}
|
|
116
114
|
/** A game the AI reviewed: its speech bubble, and the model that wrote the review when the tags say it. */
|
|
117
115
|
function Reviewed({ by, words }) {
|
|
118
116
|
return (_jsxs("span", { class: "state reviewed", title: by === undefined ? words.reviewed : `${words.reviewed}: ${by}`, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", role: "img", "aria-label": words.reviewed, children: _jsx("path", { d: "M5 5H19V15.5H11L7 19.5V15.5H5Z" }) }), by === undefined ? null : _jsx("span", { children: by })] }));
|
|
119
117
|
}
|
|
120
|
-
const outcomeSigns = { won: "M5 8h6M8 5v6", lost: "M5 8h6", drawn: "M5 6.5h6M5 9.5h6" };
|
|
121
118
|
/** The tags of a game listed that name its sides. */
|
|
122
119
|
function tagsOf(item) {
|
|
123
120
|
return { White: item.white, Black: item.black, ...(item.site === undefined ? {} : { Site: item.site }) };
|
|
@@ -130,13 +127,6 @@ function by(item) {
|
|
|
130
127
|
function scoreOf(result) {
|
|
131
128
|
return result === "1/2-1/2" ? "½–½" : result.replace("-", "–");
|
|
132
129
|
}
|
|
133
|
-
/** How a game went for the player: won, lost or drawn; nothing for a game without a result. */
|
|
134
|
-
function outcomeOf(result, you) {
|
|
135
|
-
if (result === "1/2-1/2") {
|
|
136
|
-
return "drawn";
|
|
137
|
-
}
|
|
138
|
-
return result === "1-0" || result === "0-1" ? (result === "1-0") === (you === "white") ? "won" : "lost" : undefined;
|
|
139
|
-
}
|
|
140
130
|
/** A PGN date, 2026.11.04, as the player reads it: «4 nov», with the year when it is not this one. */
|
|
141
131
|
function dayOf(date, language) {
|
|
142
132
|
const [, year, month, day] = /^(\d{4})\.(\d{2})\.(\d{2})$/.exec(date ?? "") ?? [];
|
|
@@ -839,6 +839,97 @@ h3 + .switch {
|
|
|
839
839
|
cursor: pointer;
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
+
/* The latest games on the sites above the paste field (D-110): the analyses' rows, lower, with the side the player had on a small tile; the list takes the
|
|
843
|
+
height the step leaves at the foot of one screen and scrolls with no scrollbar, fading at its foot as the side column does (Zoe) */
|
|
844
|
+
.site-games .games {
|
|
845
|
+
max-height: max(174px, calc(100vh - 500px));
|
|
846
|
+
padding-bottom: 12px;
|
|
847
|
+
overflow-y: auto;
|
|
848
|
+
overscroll-behavior: contain;
|
|
849
|
+
scrollbar-width: none;
|
|
850
|
+
mask-image: linear-gradient(#000 calc(100% - 16px), transparent);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.site-games .games::-webkit-scrollbar {
|
|
854
|
+
display: none;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* While the list comes, a box as tall as it will be, so that nothing moves when it arrives */
|
|
858
|
+
.site-games .asking {
|
|
859
|
+
display: flex;
|
|
860
|
+
align-items: center;
|
|
861
|
+
justify-content: center;
|
|
862
|
+
gap: 8px;
|
|
863
|
+
height: max(174px, calc(100vh - 500px));
|
|
864
|
+
border: 1px dashed var(--line);
|
|
865
|
+
border-radius: 10px;
|
|
866
|
+
color: var(--soft);
|
|
867
|
+
font-size: 14px;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.analyses.site-games .reopen {
|
|
871
|
+
gap: 10px;
|
|
872
|
+
min-height: 52px;
|
|
873
|
+
padding-left: 8px;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.site-games .reopen:focus-visible {
|
|
877
|
+
outline-offset: -2px;
|
|
878
|
+
border-radius: 9px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.analyses.site-games .tile {
|
|
882
|
+
width: 34px;
|
|
883
|
+
height: 34px;
|
|
884
|
+
border-radius: 7px;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.analyses.site-games .tile .piece {
|
|
888
|
+
width: 28px;
|
|
889
|
+
height: 28px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* The row picked keeps the accent and a spinner in its mark's place; the others wait, faded as a button that cannot be pressed */
|
|
893
|
+
.site-games .game.picked {
|
|
894
|
+
border-color: var(--accent);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.site-games .picking .game:not(.picked) {
|
|
898
|
+
opacity: 0.6;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.site-games .reopen:disabled {
|
|
902
|
+
cursor: default;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.site-games .score .spinner {
|
|
906
|
+
justify-self: center;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.site-games > .site-problem {
|
|
910
|
+
margin: 0 0 8px;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.site-games > .none {
|
|
914
|
+
margin: 4px 0;
|
|
915
|
+
text-align: left;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.site-games .add-names {
|
|
919
|
+
margin-left: 4px;
|
|
920
|
+
padding: 4px 10px;
|
|
921
|
+
border: 1px solid var(--line);
|
|
922
|
+
border-radius: 8px;
|
|
923
|
+
background: var(--card);
|
|
924
|
+
color: var(--ink);
|
|
925
|
+
font: inherit;
|
|
926
|
+
cursor: pointer;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.site-games .add-names:hover {
|
|
930
|
+
border-color: var(--accent);
|
|
931
|
+
}
|
|
932
|
+
|
|
842
933
|
.ask button.yes {
|
|
843
934
|
border-color: var(--asked);
|
|
844
935
|
background: var(--asked);
|
|
@@ -882,6 +973,17 @@ h3 + .switch {
|
|
|
882
973
|
.analyses .ask {
|
|
883
974
|
grid-template-columns: 1fr;
|
|
884
975
|
}
|
|
976
|
+
|
|
977
|
+
/* On a phone the mark alone tells how it went, and the meta drops the site */
|
|
978
|
+
.site-games .score b,
|
|
979
|
+
.site-games .meta .site {
|
|
980
|
+
display: none;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.analyses.site-games .score {
|
|
984
|
+
grid-template-columns: 16px;
|
|
985
|
+
min-width: 0;
|
|
986
|
+
}
|
|
885
987
|
}
|
|
886
988
|
|
|
887
989
|
.profile-form textarea:focus-visible,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** How a game went for the player, as chess.com marks it: a green square with a plus, a red one with a minus, a grey one with an equals sign. */
|
|
3
|
+
export function OutcomeBadge({ outcome, words }) {
|
|
4
|
+
return (_jsxs("svg", { class: `outcome ${outcome}`, viewBox: "0 0 16 16", role: "img", "aria-label": words.outcome[outcome], children: [_jsx("title", { children: words.outcome[outcome] }), _jsx("rect", { width: "16", height: "16", rx: "4" }), _jsx("path", { d: outcomeSigns[outcome] })] }));
|
|
5
|
+
}
|
|
6
|
+
/** How a game went for the player: won, lost or drawn; nothing for a game without a result. */
|
|
7
|
+
export function outcomeOf(result, you) {
|
|
8
|
+
if (result === "1/2-1/2") {
|
|
9
|
+
return "drawn";
|
|
10
|
+
}
|
|
11
|
+
return result === "1-0" || result === "0-1" ? (result === "1-0") === (you === "white") ? "won" : "lost" : undefined;
|
|
12
|
+
}
|
|
13
|
+
const outcomeSigns = { won: "M5 8h6M8 5v6", lost: "M5 8h6", drawn: "M5 6.5h6M5 9.5h6" };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
// The player's latest games on Lichess and Chess.com (D-110), above the paste field: a row each, which brings that game to study as a pasted PGN would.
|
|
3
|
+
import { useEffect, useState } from "preact/hooks";
|
|
4
|
+
import { Piece } from "../pieces/pieces.js";
|
|
5
|
+
import { OutcomeBadge, outcomeOf } from "./outcome.js";
|
|
6
|
+
/**
|
|
7
|
+
* The list, asked of the server when the step opens and the profile has a name on a site. `picked` is the game being sent, whose row keeps the accent and
|
|
8
|
+
* a spinner while the others wait; `problem`, why the server refused it, shown under the list; `onProfile` opens the profile, where the names go.
|
|
9
|
+
*/
|
|
10
|
+
export function SiteGameList({ accounts, picked, problem, onPick, onProfile, text }) {
|
|
11
|
+
const words = text.study;
|
|
12
|
+
const named = accounts?.lichess !== undefined || accounts?.chesscom !== undefined;
|
|
13
|
+
const answer = useSiteGames(named, words.siteGamesRefused);
|
|
14
|
+
if (!named) {
|
|
15
|
+
return (_jsxs("div", { class: "field analyses site-games", children: [_jsx("span", { children: words.siteGames }), _jsxs("p", { class: "none", children: [`${words.siteGamesNoAccount} `, _jsx("button", { class: "add-names", type: "button", onClick: onProfile, children: words.siteGamesProfile })] })] }));
|
|
16
|
+
}
|
|
17
|
+
return (_jsxs("div", { class: "field analyses site-games", children: [_jsx("span", { children: words.siteGames }), answer === undefined
|
|
18
|
+
? (_jsxs("div", { class: "asking", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), words.siteGamesLoading] }))
|
|
19
|
+
: "refusal" in answer
|
|
20
|
+
? _jsx("p", { class: "site-problem", children: answer.refusal })
|
|
21
|
+
: (_jsxs(_Fragment, { children: [_jsx(SiteProblems, { answer: answer, text: text }), answer.games.length === 0
|
|
22
|
+
? (answer.answered.length === 0 ? null : _jsx("p", { class: "none", children: words.siteGamesNone }))
|
|
23
|
+
: (_jsx("ul", { class: `games${picked === undefined ? "" : " picking"}`, "aria-label": words.siteGames, children: answer.games.map((game) => _jsx(GameRow, { game: game, picked: picked, onPick: onPick, text: text }, game.name)) })), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }))] }));
|
|
24
|
+
}
|
|
25
|
+
const siteNames = { "lichess": "Lichess", "chess.com": "Chess.com" };
|
|
26
|
+
/** The sites that did not answer, both in one line, and those without a player by the profile's name, in the profile window's words. */
|
|
27
|
+
function SiteProblems({ answer, text }) {
|
|
28
|
+
const silent = answer.unreached.length === 2 ? [text.study.bothUnreached] : answer.unreached.map((site) => text.profile.unreached(siteNames[site]));
|
|
29
|
+
return (_jsxs(_Fragment, { children: [silent.map((line) => _jsx("p", { class: "site-problem unreached", children: line }, line)), answer.unknown.map((site) => _jsx("p", { class: "site-problem", children: text.profile.notFound(siteNames[site]) }, site))] }));
|
|
30
|
+
}
|
|
31
|
+
/** A game's row: the side the player had, the opponent and their rating, when, how fast and how long, the site, the result and how it went for the player. */
|
|
32
|
+
function GameRow({ game, picked, onPick, text }) {
|
|
33
|
+
const words = text.study;
|
|
34
|
+
const rated = (name, rating) => rating === undefined ? name : `${name} (${rating})`;
|
|
35
|
+
const white = rated(game.white, game.whiteRating);
|
|
36
|
+
const black = rated(game.black, game.blackRating);
|
|
37
|
+
const opponent = game.player === undefined ? `${white} – ${black}` : game.player === "white" ? black : white;
|
|
38
|
+
const outcome = game.player === undefined ? undefined : outcomeOf(game.result, game.player);
|
|
39
|
+
const chosen = picked === game.name;
|
|
40
|
+
return (_jsx("li", { class: `game${chosen ? " picked" : ""}`, children: _jsxs("button", { class: "reopen", type: "button", disabled: picked !== undefined, onClick: () => onPick(game), children: [game.player === undefined ? null : (_jsx("span", { class: "tile", role: "img", title: words.playedAs[game.player], "aria-label": words.playedAs[game.player], children: _jsx(Piece, { letter: game.player === "white" ? "K" : "k" }) })), _jsxs("span", { class: "what", children: [_jsx("b", { class: "who", children: opponent }), _jsxs("span", { class: "meta", children: [[whenOf(game.played, text.language), words.speed(game.speed), words.moves(game.moves)].join(" · "), _jsx("span", { class: "site", children: ` · ${siteNames[game.site]}` })] })] }), _jsxs("span", { class: "score", children: [_jsx("b", { children: scoreOf(game.result) }), chosen ? _jsx("span", { class: "spinner", "aria-hidden": "true" }) : outcome === undefined ? _jsx("span", {}) : _jsx(OutcomeBadge, { outcome: outcome, words: words })] })] }) }));
|
|
41
|
+
}
|
|
42
|
+
/** When a game ended, in the player's time: «11 nov, 18:26», with the year when it is not this one. */
|
|
43
|
+
function whenOf(played, language) {
|
|
44
|
+
const when = new Date(played);
|
|
45
|
+
const day = when.toLocaleDateString(language, { day: "numeric", month: "short", ...(when.getFullYear() === new Date().getFullYear() ? {} : { year: "numeric" }) });
|
|
46
|
+
return `${day}, ${when.toLocaleTimeString(language, { hour: "2-digit", minute: "2-digit" })}`;
|
|
47
|
+
}
|
|
48
|
+
/** A result as the row shows it: 1–0, 0–1 or ½–½. */
|
|
49
|
+
function scoreOf(result) {
|
|
50
|
+
return result === "1/2-1/2" ? "½–½" : result.replace("-", "–");
|
|
51
|
+
}
|
|
52
|
+
/** The list asked of the server once, when there is a name to ask by: none while it comes; a refusal in the player's words when the server gives its code. */
|
|
53
|
+
function useSiteGames(asked, refused) {
|
|
54
|
+
const [answer, setAnswer] = useState(undefined);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!asked) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void fetch("/api/site-games")
|
|
60
|
+
.then(async (response) => {
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
return await response.json();
|
|
63
|
+
}
|
|
64
|
+
const { error, refusal } = (await response.json());
|
|
65
|
+
return { refusal: (refusal === undefined ? undefined : refused[refusal]) ?? error ?? response.statusText };
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => ({ refusal: error.message }))
|
|
68
|
+
.then(setAnswer);
|
|
69
|
+
}, [asked]);
|
|
70
|
+
return answer;
|
|
71
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The players of a game in analysis (D-69), as the page shows them: a game played here names them Player and AI, one played elsewhere by their names; the
|
|
2
|
+
// player's own, by their names on Lichess and chess.com (D-93).
|
|
3
|
+
import { accountSide } from "../../core/index.js";
|
|
1
4
|
/** The player's side in a game in analysis: none when they played neither side, or both. */
|
|
2
5
|
export function playerIn(sides) {
|
|
3
6
|
const white = sides.white.who === "player";
|
|
@@ -23,17 +26,3 @@ export function studiedSides(tags, you, ai, sides, accounts) {
|
|
|
23
26
|
const black = sideOf("black");
|
|
24
27
|
return { white, black, bottom: black.who === "player" && white.who !== "player" ? "black" : "white" };
|
|
25
28
|
}
|
|
26
|
-
/**
|
|
27
|
-
* The side a game loaded from elsewhere names by one of the player's names on the sites (D-93): the one of its site when the Site tag says Lichess or
|
|
28
|
-
* chess.com, either otherwise; none when neither side, or both, has one.
|
|
29
|
-
*/
|
|
30
|
-
export function accountSide(tags, accounts) {
|
|
31
|
-
const site = (tags.Site ?? "").toLowerCase();
|
|
32
|
-
const names = [
|
|
33
|
-
site.includes("chess.com") ? undefined : accounts?.lichess,
|
|
34
|
-
site.includes("lichess") ? undefined : accounts?.chesscom,
|
|
35
|
-
].filter((name) => name !== undefined).map((name) => name.trim().toLowerCase());
|
|
36
|
-
const white = names.includes(tags.White?.trim().toLowerCase() ?? "");
|
|
37
|
-
const black = names.includes(tags.Black?.trim().toLowerCase() ?? "");
|
|
38
|
-
return white === black ? undefined : white ? "white" : "black";
|
|
39
|
-
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
-
// The dialog of the analyses (D-92): the list of the games analysed or brought to study, and a step that loads a game played elsewhere
|
|
3
|
-
// read from a file, which the server keeps to study and puts in analysis.
|
|
2
|
+
// The dialog of the analyses (D-92): the list of the games analysed or brought to study, and a step that loads a game played elsewhere, one of the latest
|
|
3
|
+
// on the sites (D-110) or a PGN pasted or read from a file, which the server keeps to study and puts in analysis.
|
|
4
4
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
5
5
|
import { Analyses } from "./analyses.js";
|
|
6
6
|
import { Dialog } from "./dialog.js";
|
|
7
|
+
import { SiteGameList } from "./site-games.js";
|
|
7
8
|
/**
|
|
8
9
|
* The dialog the header's button opens; `onStudy` sends the choice and gives back why the server refused it, or undefined once it took it. The games played
|
|
9
10
|
* here name their sides as their analysis does: the player by `you`, the AI by its model when the game keeps it (D-82), else by `ai`.
|
|
10
11
|
*/
|
|
11
|
-
export function StudyDialog({ open, onStudy, onClose, you, accounts, ai, text }) {
|
|
12
|
+
export function StudyDialog({ open, onStudy, onClose, onProfile, you, accounts, ai, text }) {
|
|
12
13
|
const [loading, setLoading] = useState(false);
|
|
13
14
|
// Each opening starts from the list.
|
|
14
15
|
useEffect(() => {
|
|
@@ -16,14 +17,17 @@ export function StudyDialog({ open, onStudy, onClose, you, accounts, ai, text })
|
|
|
16
17
|
setLoading(false);
|
|
17
18
|
}
|
|
18
19
|
}, [open]);
|
|
19
|
-
return (_jsx(Dialog, { open: open, title: loading ? text.study.bring : text.study.title, onBack: loading ? () => setLoading(false) : undefined, onClose: onClose, text: text, children: loading ? _jsx(LoadForm, { onStudy: onStudy, text: text }) : _jsx(Analyses, { onStudy: onStudy, onLoad: () => setLoading(true), you: you, accounts: accounts, ai: ai, text: text }) }));
|
|
20
|
+
return (_jsx(Dialog, { open: open, title: loading ? text.study.bring : text.study.title, onBack: loading ? () => setLoading(false) : undefined, onClose: onClose, text: text, children: loading ? _jsx(LoadForm, { onStudy: onStudy, onProfile: onProfile, accounts: accounts, text: text }) : _jsx(Analyses, { onStudy: onStudy, onLoad: () => setLoading(true), you: you, accounts: accounts, ai: ai, text: text }) }));
|
|
20
21
|
}
|
|
21
|
-
/** The step that loads a game played elsewhere: its PGN pasted, or read from a file. */
|
|
22
|
-
function LoadForm({ onStudy, text }) {
|
|
22
|
+
/** The step that loads a game played elsewhere: one of the latest on the sites, or its PGN pasted, or read from a file. */
|
|
23
|
+
function LoadForm({ onStudy, onProfile, accounts, text }) {
|
|
23
24
|
const words = text.study;
|
|
24
25
|
const [pgn, setPgn] = useState("");
|
|
25
26
|
const [problem, setProblem] = useState(undefined);
|
|
26
27
|
const [sending, setSending] = useState(false);
|
|
28
|
+
// The game of the sites being sent, and why the server refused the last one picked, said under the list.
|
|
29
|
+
const [picked, setPicked] = useState(undefined);
|
|
30
|
+
const [pickRefused, setPickRefused] = useState(undefined);
|
|
27
31
|
const field = useRef(null);
|
|
28
32
|
// With a mouse only: on a phone the keyboard would cover the step.
|
|
29
33
|
useEffect(() => {
|
|
@@ -31,6 +35,23 @@ function LoadForm({ onStudy, text }) {
|
|
|
31
35
|
field.current?.focus();
|
|
32
36
|
}
|
|
33
37
|
}, []);
|
|
38
|
+
const send = (sent) => {
|
|
39
|
+
setSending(true);
|
|
40
|
+
setPickRefused(undefined);
|
|
41
|
+
void onStudy({ pgn: sent }).then((refused) => {
|
|
42
|
+
setSending(false);
|
|
43
|
+
setProblem(refused);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const pick = (game) => {
|
|
47
|
+
setPicked(game.name);
|
|
48
|
+
setPickRefused(undefined);
|
|
49
|
+
setProblem(undefined);
|
|
50
|
+
void onStudy({ pgn: game.pgn }).then((refused) => {
|
|
51
|
+
setPicked(undefined);
|
|
52
|
+
setPickRefused(refused);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
34
55
|
const read = (file) => {
|
|
35
56
|
void file?.text().then((read) => {
|
|
36
57
|
setPgn(read);
|
|
@@ -39,13 +60,9 @@ function LoadForm({ onStudy, text }) {
|
|
|
39
60
|
};
|
|
40
61
|
return (_jsxs("form", { class: "profile-form study", onSubmit: (event) => {
|
|
41
62
|
event.preventDefault();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
setSending(false);
|
|
45
|
-
setProblem(refused);
|
|
46
|
-
});
|
|
47
|
-
}, children: [_jsxs("label", { class: "field", children: [_jsx("span", { children: words.paste }), _jsx("textarea", { ref: field, rows: 10, value: pgn, spellcheck: false, placeholder: '[White "…"]\n[Black "…"]\n\n1. e4 e5 2. Nf3 …', onInput: (event) => {
|
|
63
|
+
send(pgn);
|
|
64
|
+
}, children: [_jsx(SiteGameList, { accounts: accounts, picked: picked, problem: pickRefused, onPick: pick, onProfile: onProfile, text: text }), _jsxs("label", { class: "field", children: [_jsx("span", { children: words.paste }), _jsx("textarea", { ref: field, rows: 4, value: pgn, spellcheck: false, placeholder: '[White "…"]\n[Black "…"]\n\n1. e4 e5 2. Nf3 …', onInput: (event) => {
|
|
48
65
|
setPgn(event.currentTarget.value);
|
|
49
66
|
setProblem(undefined);
|
|
50
|
-
} })] }), _jsxs("label", { class: "field file", children: [_jsx("span", { children: words.file }), _jsx("input", { type: "file", accept: ".pgn,application/x-chess-pgn,text/plain", onChange: (event) => read(event.currentTarget.files?.[0]) })] }), _jsx("p", { class: "note", children: words.note }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx("button", { class: "start", type: "submit", disabled: pgn.trim() === "" || sending, children: words.start })] }));
|
|
67
|
+
} })] }), _jsxs("label", { class: "field file", children: [_jsx("span", { children: words.file }), _jsx("input", { type: "file", accept: ".pgn,application/x-chess-pgn,text/plain", onChange: (event) => read(event.currentTarget.files?.[0]) })] }), _jsx("p", { class: "note", children: words.note }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx("button", { class: "start", type: "submit", disabled: pgn.trim() === "" || sending || picked !== undefined, children: words.start })] }));
|
|
51
68
|
}
|
package/dist/page/game/column.js
CHANGED
|
@@ -14,7 +14,7 @@ export function GameColumn({ plies, result, how, lit, started, move, toMove, ope
|
|
|
14
14
|
const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
|
|
15
15
|
return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
|
|
16
16
|
? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
|
|
17
|
-
: (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis
|
|
17
|
+
: (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsx("p", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })] })) }));
|
|
18
18
|
}
|
|
19
19
|
/** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
|
|
20
20
|
function SaveButton({ label, hint, onSave }) {
|
package/dist/page/game/game.css
CHANGED
|
@@ -320,6 +320,27 @@
|
|
|
320
320
|
color: var(--soft);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/* A review nobody is finishing: the note, and beside it the button that brings the game back to the AI (D-109); neutral, as no error (Zoe) */
|
|
324
|
+
.review.incomplete,
|
|
325
|
+
.say.incomplete {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: space-between;
|
|
329
|
+
gap: 8px;
|
|
330
|
+
margin: 0;
|
|
331
|
+
color: var(--soft);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.review.incomplete button,
|
|
335
|
+
.say.incomplete button {
|
|
336
|
+
flex: none;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* On the bubble's tint the button keeps the card's ground, to read as one */
|
|
340
|
+
.say.incomplete button {
|
|
341
|
+
background: var(--card);
|
|
342
|
+
}
|
|
343
|
+
|
|
323
344
|
.review ul {
|
|
324
345
|
margin: 0;
|
|
325
346
|
padding-left: 18px;
|
|
@@ -497,7 +518,8 @@
|
|
|
497
518
|
padding: 8px;
|
|
498
519
|
}
|
|
499
520
|
|
|
500
|
-
.analysis-actions button
|
|
521
|
+
.analysis-actions button,
|
|
522
|
+
.incomplete button {
|
|
501
523
|
flex: 1;
|
|
502
524
|
display: inline-flex;
|
|
503
525
|
align-items: center;
|
|
@@ -513,7 +535,8 @@
|
|
|
513
535
|
cursor: pointer;
|
|
514
536
|
}
|
|
515
537
|
|
|
516
|
-
.analysis-actions button:hover
|
|
538
|
+
.analysis-actions button:hover,
|
|
539
|
+
.incomplete button:hover {
|
|
517
540
|
border-color: var(--accent);
|
|
518
541
|
}
|
|
519
542
|
|
package/dist/page/guide/en.md
CHANGED
|
@@ -54,6 +54,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
|
|
|
54
54
|
|
|
55
55
|
- When the game is over, press **Analyse the game**: the AI goes over it and puts its judgments and notes on the moves. Read them by stepping through the moves with the arrows at the foot of the column, or by clicking a move.
|
|
56
56
|
- **Analyses**, in the header, opens your analyses, and loads a game played elsewhere: paste its PGN, or choose a .pgn file.
|
|
57
|
+
- With your names on Lichess and Chess.com in your profile, **Load a game** lists your latest games there: click one to analyse it.
|
|
57
58
|
- Ask the AI about the game in the box under its name.
|
|
58
59
|
- **Annotated PGN** saves the game with its comments.
|
|
59
60
|
|
package/dist/page/guide/it.md
CHANGED
|
@@ -54,6 +54,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
|
|
|
54
54
|
|
|
55
55
|
- A partita finita premi **Analizza la partita**: l'AI la ripassa e mette sulle mosse i suoi giudizi e le sue note. Le leggi scorrendo le mosse con le frecce in fondo alla colonna, o cliccando una mossa.
|
|
56
56
|
- **Analisi**, nell'intestazione, apre le tue analisi e carica una partita giocata altrove: incolla il suo PGN, o scegli un file .pgn.
|
|
57
|
+
- Con i tuoi nomi su Lichess e Chess.com nel profilo, **Carica una partita** elenca le tue ultime partite là: clicca su una per analizzarla.
|
|
57
58
|
- Chiedi all'AI della partita nella casella sotto il suo nome.
|
|
58
59
|
- **PGN annotato** salva la partita coi commenti.
|
|
59
60
|
|