chesspal 0.3.3 → 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.
Files changed (61) hide show
  1. package/README.md +49 -10
  2. package/dist/commands/analysis/analyse.d.ts +4 -1
  3. package/dist/commands/analysis/analyse.js +46 -12
  4. package/dist/commands/analysis/index.d.ts +1 -0
  5. package/dist/commands/analysis/index.js +2 -1
  6. package/dist/commands/analysis/site-games.d.ts +27 -0
  7. package/dist/commands/analysis/site-games.js +84 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +3 -2
  10. package/dist/commands/profiles/index.d.ts +1 -1
  11. package/dist/commands/profiles/index.js +1 -1
  12. package/dist/commands/profiles/profiles.d.ts +3 -1
  13. package/dist/commands/profiles/profiles.js +2 -1
  14. package/dist/commands/registry/context.d.ts +4 -1
  15. package/dist/commands/registry/registry.d.ts +2 -0
  16. package/dist/core/accounts.d.ts +11 -0
  17. package/dist/core/accounts.js +14 -0
  18. package/dist/core/index.d.ts +1 -0
  19. package/dist/core/index.js +1 -0
  20. package/dist/page/app/app.js +1 -1
  21. package/dist/page/dialogs/analyses.js +3 -13
  22. package/dist/page/dialogs/dialogs.css +102 -0
  23. package/dist/page/dialogs/outcome.js +13 -0
  24. package/dist/page/dialogs/site-games.js +71 -0
  25. package/dist/page/dialogs/studied.js +3 -14
  26. package/dist/page/dialogs/study.js +30 -13
  27. package/dist/page/guide/en.md +1 -0
  28. package/dist/page/guide/it.md +1 -0
  29. package/dist/page/helps/helps.css +26 -18
  30. package/dist/page/helps/helps.js +2 -2
  31. package/dist/page/text.js +32 -2
  32. package/dist/server/mcp/mcp.js +2 -2
  33. package/dist/server/page/analyses.d.ts +5 -0
  34. package/dist/server/page/analyses.js +18 -1
  35. package/dist/server/page/game.js +3 -3
  36. package/dist/server/page/page.js +4 -1
  37. package/dist/sites/chesscom.d.ts +1 -1
  38. package/dist/sites/chesscom.js +1 -1
  39. package/dist/sites/games/chesscom.d.ts +4 -0
  40. package/dist/sites/games/chesscom.js +54 -0
  41. package/dist/sites/games/game.d.ts +34 -0
  42. package/dist/sites/games/game.js +64 -0
  43. package/dist/sites/games/games.d.ts +14 -0
  44. package/dist/sites/games/games.js +22 -0
  45. package/dist/sites/games/index.d.ts +2 -0
  46. package/dist/sites/games/index.js +3 -0
  47. package/dist/sites/games/lichess.d.ts +4 -0
  48. package/dist/sites/games/lichess.js +45 -0
  49. package/dist/sites/index.d.ts +2 -1
  50. package/dist/sites/index.js +5 -3
  51. package/dist/sites/lichess.d.ts +1 -1
  52. package/dist/sites/lichess.js +1 -1
  53. package/dist/sites/request/index.d.ts +1 -0
  54. package/dist/sites/request/index.js +2 -0
  55. package/dist/sites/{request.d.ts → request/request.d.ts} +7 -0
  56. package/dist/sites/request/request.js +37 -0
  57. package/dist/sites/sites.d.ts +1 -1
  58. package/dist/sites/sites.js +1 -1
  59. package/instructions/opponent.md +1 -1
  60. package/package.json +1 -1
  61. package/dist/sites/request.js +0 -15
@@ -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 as a PGN, pasted or
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
- setSending(true);
43
- void onStudy({ pgn }).then((refused) => {
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
  }
@@ -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
 
@@ -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
 
@@ -622,41 +622,49 @@
622
622
  gap: 10px;
623
623
  }
624
624
 
625
- /* The three phases in a row, joined by a thin line; the current one in a pill */
625
+ /* The three phases in a row of arrows, each set in the next: the current one full in the accent, those gone by in its tint, those to come unlit */
626
626
  .phases {
627
627
  display: flex;
628
- align-items: center;
629
628
  margin: 0;
630
629
  padding: 0;
631
630
  list-style: none;
632
631
  font-size: 12px;
632
+ line-height: 22px;
633
+ text-align: center;
633
634
  color: var(--soft);
634
635
  }
635
636
 
637
+ .phases li {
638
+ flex: 1;
639
+ padding: 0 6px 0 12px;
640
+ background: color-mix(in srgb, var(--soft) 14%, transparent);
641
+ white-space: nowrap;
642
+ clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%);
643
+ }
644
+
636
645
  .phases li + li {
637
- flex: 1 1 auto;
638
- display: flex;
639
- align-items: center;
640
- gap: 6px;
641
- margin-left: 6px;
646
+ margin-left: -5px;
642
647
  }
643
648
 
644
- .phases li + li::before {
645
- content: "";
646
- flex: 1;
647
- height: 1px;
648
- background: var(--line);
649
+ .phases li:first-child {
650
+ padding-left: 8px;
651
+ border-radius: 5px 0 0 5px;
652
+ clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%);
649
653
  }
650
654
 
651
- .phases li span {
652
- padding: 1px 9px;
653
- white-space: nowrap;
655
+ .phases li:last-child {
656
+ border-radius: 0 5px 5px 0;
657
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 8px 50%);
654
658
  }
655
659
 
656
- .phases li[aria-current="step"] span {
657
- border-radius: 999px;
658
- background: color-mix(in srgb, var(--accent) 20%, transparent);
660
+ .phases li.done {
661
+ background: color-mix(in srgb, var(--accent) 24%, transparent);
659
662
  color: var(--ink);
663
+ }
664
+
665
+ .phases li[aria-current="step"] {
666
+ background: var(--accent);
667
+ color: var(--on-accent);
660
668
  font-weight: 700;
661
669
  }
662
670
 
@@ -184,9 +184,9 @@ function squaresIn(line) {
184
184
  }
185
185
  return [line.white, line.black].flatMap((cell) => typeof cell === "object" ? cell.filter((item) => /^[a-h][1-8]$/.test(item)) : []);
186
186
  }
187
- /** The game's three phases in a row, the current one in a pill. */
187
+ /** The game's three phases in a row of arrows: the current one full, those gone by lit, those to come not. */
188
188
  function Phases({ current, words }) {
189
- return (_jsx("ol", { class: "phases", children: phases.map((phase) => (_jsx("li", { "aria-current": phase === current ? "step" : undefined, children: _jsx("span", { children: words.phases[phase] }) }, phase))) }));
189
+ return (_jsx("ol", { class: "phases", children: phases.map((phase, at) => (_jsx("li", { class: at < phases.indexOf(current) ? "done" : undefined, "aria-current": phase === current ? "step" : undefined, children: words.phases[phase] }, phase))) }));
190
190
  }
191
191
  /** The squares only one side controls and those both do, in a bar as long as their counts, the player's side on the left; each part's title says it in words. */
192
192
  function ControlBar({ controlled, sides, you, words }) {
package/dist/page/text.js CHANGED
@@ -303,7 +303,7 @@ const english = {
303
303
  start: "Analyse",
304
304
  yours: "Your analyses",
305
305
  bring: "Load a game",
306
- bringNote: "a PGN played elsewhere, pasted or from a file",
306
+ bringNote: "from Lichess or Chess.com, or a PGN pasted or from a file",
307
307
  search: "Names, models, openings…",
308
308
  noMatch: "No game found",
309
309
  reviewed: "reviewed",
@@ -324,6 +324,19 @@ const english = {
324
324
  askUnreview: (game) => ({ question: `Delete the analysis of ${game}?`, note: "Its badges, notes and the coach's judgments go; the game stays.", yes: "Delete" }),
325
325
  askRemove: (game, when) => ({ question: `Delete the game ${game}${when === undefined ? "" : ` of ${when}`}?`, note: "With its analysis. It cannot be undone.", yes: "Delete" }),
326
326
  cancel: "Cancel",
327
+ siteGames: "Your latest games on the sites",
328
+ siteGamesLoading: "Asking Lichess and Chess.com…",
329
+ siteGamesNone: "No finished game on the sites.",
330
+ siteGamesNoAccount: "Your names on Lichess and Chess.com bring your latest games here.",
331
+ siteGamesProfile: "Add them",
332
+ bothUnreached: "Lichess and Chess.com aren't answering: we'll try again later",
333
+ siteGamesRefused: {
334
+ "game-going": "A game is going on: finish it to bring another.",
335
+ "no-accounts": "Your profile names no account on Lichess or Chess.com.",
336
+ "no-network": "Chesspal cannot reach the sites from here.",
337
+ },
338
+ playedAs: { white: "You played White", black: "You played Black" },
339
+ speed: (name) => ({ ultraBullet: "ultrabullet", correspondence: "correspondence", daily: "daily" }[name] ?? readableSpeed(name)),
327
340
  download: "Annotated PGN",
328
341
  downloadHint: "The game with the review's comments and judgments",
329
342
  analyseIt: "Analyse the game",
@@ -716,7 +729,7 @@ const italian = {
716
729
  start: "Analizza",
717
730
  yours: "Le tue analisi",
718
731
  bring: "Carica una partita",
719
- bringNote: "un PGN giocato altrove, incollato o da file",
732
+ bringNote: "da Lichess o Chess.com, o un PGN incollato o da file",
720
733
  search: "Nomi, modelli, aperture…",
721
734
  noMatch: "Nessuna partita trovata",
722
735
  reviewed: "commentata",
@@ -737,6 +750,19 @@ const italian = {
737
750
  askUnreview: (game) => ({ question: `Eliminare l'analisi di ${game}?`, note: "Bollini, note e giudizi dell'allenatore si cancellano; la partita resta.", yes: "Elimina" }),
738
751
  askRemove: (game, when) => ({ question: `Eliminare la partita ${game}${when === undefined ? "" : ` del ${when}`}?`, note: "Con la sua analisi. Non si può annullare.", yes: "Elimina" }),
739
752
  cancel: "Annulla",
753
+ siteGames: "Le tue ultime partite sui siti",
754
+ siteGamesLoading: "Chiedo a Lichess e Chess.com…",
755
+ siteGamesNone: "Nessuna partita finita sui siti.",
756
+ siteGamesNoAccount: "I tuoi nomi su Lichess e Chess.com portano qui le tue ultime partite.",
757
+ siteGamesProfile: "Aggiungili",
758
+ bothUnreached: "Lichess e Chess.com non rispondono: riproviamo più tardi",
759
+ siteGamesRefused: {
760
+ "game-going": "C'è una partita in corso: finiscila per portarne un'altra.",
761
+ "no-accounts": "Il tuo profilo non ha nomi su Lichess o Chess.com.",
762
+ "no-network": "Da qui Chesspal non raggiunge i siti.",
763
+ },
764
+ playedAs: { white: "Hai giocato col Bianco", black: "Hai giocato col Nero" },
765
+ speed: (name) => ({ ultraBullet: "ultrabullet", classical: "classica", correspondence: "per corrispondenza", daily: "per corrispondenza" }[name] ?? readableSpeed(name)),
740
766
  download: "PGN annotato",
741
767
  downloadHint: "La partita con i commenti e i giudizi della revisione",
742
768
  analyseIt: "Analizza la partita",
@@ -921,3 +947,7 @@ function moveInWords(words, move) {
921
947
  function capital(words) {
922
948
  return words.charAt(0).toUpperCase() + words.slice(1);
923
949
  }
950
+ /** A speed the page does not know, as a word: ultraBullet as «ultra bullet». */
951
+ function readableSpeed(name) {
952
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
953
+ }
@@ -191,10 +191,10 @@ function track(call) {
191
191
  void call.finally(() => running.delete(call));
192
192
  return call;
193
193
  }
194
- /** Every hint stated, since the spec reads a missing one as the worst case: everything is local, so no tool reaches the open world. */
194
+ /** Every hint stated, since the spec reads a missing one as the worst case: everything is local, but for the tools that ask the sites the player plays on (D-110). */
195
195
  function toolConfig(command) {
196
196
  const readOnly = command.readOnly === true;
197
- const annotations = { readOnlyHint: readOnly, destructiveHint: command.destructive === true, idempotentHint: readOnly || command.idempotent === true, openWorldHint: false };
197
+ const annotations = { readOnlyHint: readOnly, destructiveHint: command.destructive === true, idempotentHint: readOnly || command.idempotent === true, openWorldHint: command.openWorld === true };
198
198
  const meta = command.alwaysLoad ? { "anthropic/alwaysLoad": true } : undefined;
199
199
  // The SDK writes the draft's address in every schema: it says nothing to an AI, and costs tokens in every tool.
200
200
  return { description: command.description, inputSchema: z.looseObject({}).meta({ ...shownInput(command), $schema: undefined }), annotations, _meta: meta };
@@ -23,3 +23,8 @@ export interface AnalysisItem {
23
23
  export declare function analysesOf(home: string): AnalysisItem[];
24
24
  /** Answers the page's deletions, of a game or of its review alone, done as the command line does them: true when the request was one. */
25
25
  export declare function answerAnalyses(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
26
+ /**
27
+ * Sends the player's latest games on the sites, as site_games gives them with their PGNs (D-110); its refusal as a 409, with the code the page says it by in
28
+ * its own words: a game going on, no name on the sites, no network.
29
+ */
30
+ export declare function sendSiteGames(response: ServerResponse, served: Served): void;
@@ -1,4 +1,4 @@
1
- import { bookOpening, deleteGame, deleteReview } from "../../commands/index.js";
1
+ import { bookOpening, deleteGame, deleteReview, siteGames, SiteGamesRefused } from "../../commands/index.js";
2
2
  import { aiModelIn, annotatorOf, isReviewed, leadOf, reviewOf } from "../../core/index.js";
3
3
  import { archivedGame, archivedGames, gameInAnalysis, gameStemOf, keptGame, keptGames } from "../../games/index.js";
4
4
  import { gameFolder } from "../../profiles/index.js";
@@ -33,6 +33,23 @@ export function answerAnalyses(request, response, url, served) {
33
33
  });
34
34
  return true;
35
35
  }
36
+ /**
37
+ * Sends the player's latest games on the sites, as site_games gives them with their PGNs (D-110); its refusal as a 409, with the code the page says it by in
38
+ * its own words: a game going on, no name on the sites, no network.
39
+ */
40
+ export function sendSiteGames(response, served) {
41
+ runRoute(response, async () => {
42
+ try {
43
+ sendJson(response, 200, await siteGames.run({}, served.context));
44
+ }
45
+ catch (error) {
46
+ if (!(error instanceof SiteGamesRefused)) {
47
+ throw error;
48
+ }
49
+ sendJson(response, 409, { error: error.message, refusal: error.refusal });
50
+ }
51
+ });
52
+ }
36
53
  const deletions = { "/api/delete-game": deleteGame, "/api/delete-review": deleteReview };
37
54
  function itemOf(name, read, taken) {
38
55
  let record;
@@ -245,7 +245,7 @@ async function studyFromPage(request, response, served) {
245
245
  let brought;
246
246
  try {
247
247
  // Without what the chat was told: the analyst's instructions stay for the AI's own analyse_game.
248
- const opened = last ? lastPlayed(served.context) : game === undefined ? kept(served, pgn) : namedGame(served.context, game);
248
+ const opened = last ? lastPlayed(served.context) : game === undefined ? await kept(served, pgn) : namedGame(served.context, game);
249
249
  reviewed = isReviewed(opened.record);
250
250
  brought = !reviewed && !look;
251
251
  // A game not yet reviewed is marked as brought, for status to ask the AI to go over it; one reviewed is ready, and the chat sleeps on.
@@ -377,8 +377,8 @@ function lastPlayed(context) {
377
377
  return { name, record };
378
378
  }
379
379
  /** A game brought as a PGN, kept to study as analyse_game keeps it: the one already kept when it is the same game, with its review. */
380
- function kept(served, pgn) {
381
- analyseGame.run({ pgn }, served.context);
380
+ async function kept(served, pgn) {
381
+ await analyseGame.run({ pgn }, served.context);
382
382
  return namedGame(served.context, gameInAnalysis(gameFolder(served.context.home())).game);
383
383
  }
384
384
  /** The longest PGN the page brings: a long game with its clocks, and the comments a site adds, which go. */
@@ -9,7 +9,7 @@ import { forgetAnalyst, gameStamp, tableTalkStamp } from "../../games/index.js";
9
9
  import { outdatedHostFiles } from "../../host-files/index.js";
10
10
  import { gameFolder } from "../../profiles/index.js";
11
11
  import { Bell, bellProbe, pagePorts } from "../bell/index.js";
12
- import { analysesOf, answerAnalyses } from "./analyses.js";
12
+ import { analysesOf, answerAnalyses, sendSiteGames } from "./analyses.js";
13
13
  import { answerChat } from "./attach.js";
14
14
  import { attachedSeen, chatSeenOf, pageContext } from "./chat.js";
15
15
  import { askNewGame, keepPosted, tellWatchers } from "./events.js";
@@ -131,6 +131,9 @@ function answer(request, response, served) {
131
131
  else if (path === "/api/analyses") {
132
132
  send(response, 200, json, JSON.stringify(analysesOf(served.context.home())));
133
133
  }
134
+ else if (path === "/api/site-games") {
135
+ sendSiteGames(response, served);
136
+ }
134
137
  else if (path === "/api/events") {
135
138
  keepPosted(request, response, served.watchers);
136
139
  // A page that opens reads the player's ratings again after a day (D-106).
@@ -1,4 +1,4 @@
1
1
  import type { SiteReading } from "../profiles/index.ts";
2
- import { type Fetch } from "./request.ts";
2
+ import { type Fetch } from "./request/index.ts";
3
3
  /** What Chess.com says of a name: undefined when it could not be reached. */
4
4
  export declare function chesscomReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { counted } from "./choice.js";
2
- import { askSite } from "./request.js";
2
+ import { askSite } from "./request/index.js";
3
3
  /** What Chess.com says of a name: undefined when it could not be reached. */
4
4
  export async function chesscomReading(account, fetch, signal) {
5
5
  const statistics = await askSite(`https://api.chess.com/pub/player/${encodeURIComponent(account.toLowerCase())}/stats`, fetch, signal);
@@ -0,0 +1,4 @@
1
+ import { type Fetch } from "../request/index.ts";
2
+ import { type SiteGamesAnswer } from "./game.ts";
3
+ /** What Chess.com gives of a name's latest games, the latest first. */
4
+ export declare function chesscomGames(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteGamesAnswer>;