chesspal 0.3.6 → 0.4.0

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 (136) hide show
  1. package/dist/cli/cli.js +7 -2
  2. package/dist/cli/command-line.js +5 -3
  3. package/dist/cli/prompt-hook.d.ts +17 -0
  4. package/dist/cli/prompt-hook.js +100 -0
  5. package/dist/commands/analysis/analyse.js +8 -14
  6. package/dist/commands/analysis/annotate.d.ts +8 -0
  7. package/dist/commands/analysis/annotate.js +19 -3
  8. package/dist/commands/answers/agents.d.ts +7 -5
  9. package/dist/commands/answers/agents.js +16 -9
  10. package/dist/commands/answers/clock.js +2 -1
  11. package/dist/commands/answers/drawings.js +6 -2
  12. package/dist/commands/answers/index.d.ts +4 -3
  13. package/dist/commands/answers/index.js +4 -3
  14. package/dist/commands/answers/lines.d.ts +5 -3
  15. package/dist/commands/answers/lines.js +14 -7
  16. package/dist/commands/answers/opening-book.d.ts +13 -7
  17. package/dist/commands/answers/opening-book.js +34 -38
  18. package/dist/commands/answers/opening-tags.d.ts +4 -0
  19. package/dist/commands/answers/opening-tags.js +11 -0
  20. package/dist/commands/answers/play-move.d.ts +3 -1
  21. package/dist/commands/answers/play-move.js +9 -6
  22. package/dist/commands/answers/say.js +2 -1
  23. package/dist/commands/answers/summary.d.ts +5 -2
  24. package/dist/commands/answers/summary.js +16 -5
  25. package/dist/commands/game/ai-move.js +4 -4
  26. package/dist/commands/game/coach.js +7 -6
  27. package/dist/commands/game/end.js +2 -2
  28. package/dist/commands/game/export.d.ts +1 -1
  29. package/dist/commands/game/index.d.ts +1 -1
  30. package/dist/commands/game/index.js +1 -1
  31. package/dist/commands/game/new.js +41 -11
  32. package/dist/commands/game/player-move.d.ts +16 -0
  33. package/dist/commands/game/player-move.js +69 -19
  34. package/dist/commands/game/ponder.js +4 -1
  35. package/dist/commands/game/status.d.ts +1 -1
  36. package/dist/commands/game/status.js +6 -6
  37. package/dist/commands/index.d.ts +3 -3
  38. package/dist/commands/index.js +3 -3
  39. package/dist/commands/profiles/past-games.d.ts +1 -1
  40. package/dist/commands/profiles/past-games.js +21 -10
  41. package/dist/commands/registry/context.d.ts +2 -0
  42. package/dist/commands/registry/context.js +5 -0
  43. package/dist/commands/registry/index.d.ts +1 -1
  44. package/dist/commands/registry/index.js +1 -1
  45. package/dist/commands/talk/ai-say.d.ts +1 -1
  46. package/dist/commands/talk/ai-say.js +6 -5
  47. package/dist/core/game.d.ts +13 -0
  48. package/dist/core/game.js +29 -3
  49. package/dist/core/index.d.ts +4 -4
  50. package/dist/core/index.js +3 -3
  51. package/dist/core/openings.d.ts +20 -6
  52. package/dist/core/openings.js +45 -22
  53. package/dist/core/pgn.d.ts +4 -0
  54. package/dist/core/pgn.js +35 -6
  55. package/dist/games/archive.d.ts +25 -5
  56. package/dist/games/archive.js +52 -19
  57. package/dist/games/chat-start.d.ts +2 -2
  58. package/dist/games/chat-start.js +6 -1
  59. package/dist/games/current-game.d.ts +2 -2
  60. package/dist/games/current-game.js +3 -2
  61. package/dist/games/index.d.ts +2 -1
  62. package/dist/games/index.js +2 -1
  63. package/dist/games/position-shown.d.ts +24 -0
  64. package/dist/games/position-shown.js +51 -0
  65. package/dist/home/index.d.ts +1 -1
  66. package/dist/home/index.js +1 -1
  67. package/dist/home/same-path.d.ts +6 -1
  68. package/dist/home/same-path.js +22 -3
  69. package/dist/host-files/host-files.d.ts +5 -0
  70. package/dist/host-files/host-files.js +13 -4
  71. package/dist/host-files/index.d.ts +1 -1
  72. package/dist/host-files/index.js +1 -1
  73. package/dist/page/app/app.js +30 -7
  74. package/dist/page/app/served.js +9 -4
  75. package/dist/page/avatars/torretta.css +4 -4
  76. package/dist/page/base.css +1 -1
  77. package/dist/page/board/board.css +4 -2
  78. package/dist/page/dialogs/dialogs.css +3 -1
  79. package/dist/page/game/column.js +10 -8
  80. package/dist/page/game/game.css +86 -6
  81. package/dist/page/game/plies.js +4 -0
  82. package/dist/page/game/review-progress.js +11 -0
  83. package/dist/page/guide/drawings/drawings.css +0 -1
  84. package/dist/page/guide/en.md +2 -2
  85. package/dist/page/guide/it.md +2 -2
  86. package/dist/page/layout/icon.ico +0 -0
  87. package/dist/page/layout/layout.css +32 -15
  88. package/dist/page/layout/window.js +41 -2
  89. package/dist/page/legend/legend.css +4 -16
  90. package/dist/page/legend/legend.js +28 -15
  91. package/dist/page/profile/settings.js +1 -0
  92. package/dist/page/table/action.js +42 -0
  93. package/dist/page/table/table.css +57 -39
  94. package/dist/page/table/window.js +67 -67
  95. package/dist/page/talk/talk.css +52 -9
  96. package/dist/page/talk/talk.js +65 -20
  97. package/dist/page/talk/voice.js +92 -21
  98. package/dist/page/text.js +24 -8
  99. package/dist/server/bell/bell.d.ts +24 -27
  100. package/dist/server/bell/bell.js +79 -143
  101. package/dist/server/bell/index.d.ts +1 -1
  102. package/dist/server/bell/index.js +2 -2
  103. package/dist/server/mcp/mcp.js +49 -7
  104. package/dist/server/page/analyses.js +15 -4
  105. package/dist/server/page/files.js +3 -1
  106. package/dist/server/page/game.d.ts +5 -0
  107. package/dist/server/page/game.js +78 -13
  108. package/dist/server/page/http.d.ts +1 -1
  109. package/dist/server/page/page.d.ts +14 -9
  110. package/dist/server/page/page.js +111 -26
  111. package/dist/server/page/table.d.ts +38 -4
  112. package/dist/server/page/table.js +61 -23
  113. package/dist/server/page/windows.d.ts +22 -0
  114. package/dist/server/page/windows.js +45 -0
  115. package/dist/server/ports/index.d.ts +1 -0
  116. package/dist/server/ports/index.js +2 -0
  117. package/dist/server/ports/ports.d.ts +78 -0
  118. package/dist/server/ports/ports.js +222 -0
  119. package/dist/server/table/browser.d.ts +22 -6
  120. package/dist/server/table/browser.js +98 -20
  121. package/dist/server/table/hosts/hooks.d.ts +6 -0
  122. package/dist/server/table/hosts/hooks.js +38 -0
  123. package/dist/server/table/hosts/hosts.d.ts +13 -5
  124. package/dist/server/table/hosts/hosts.js +16 -10
  125. package/dist/server/table/hosts/index.d.ts +1 -0
  126. package/dist/server/table/hosts/index.js +1 -0
  127. package/dist/server/table/hosts/play.d.ts +6 -4
  128. package/dist/server/table/hosts/play.js +58 -16
  129. package/dist/server/table/index.d.ts +1 -1
  130. package/dist/server/table/launch.d.ts +5 -4
  131. package/dist/server/table/launch.js +18 -5
  132. package/dist/server/table/link.js +1 -0
  133. package/dist/server/table/table.d.ts +7 -0
  134. package/dist/server/table/table.js +39 -11
  135. package/instructions/opponent.md +1 -1
  136. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
4
  import { makeFolder, writeAtomically } from "../games/index.js";
5
- import { agentFiles, instructionsFile } from "./agents.js";
5
+ import { agentFiles, agentHosts, instructionsFile } from "./agents.js";
6
6
  /**
7
7
  * Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the lines that start a chat between
8
8
  * Chesspal's marks (D-102), CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
@@ -31,6 +31,13 @@ export function outdatedHostFiles(folder, platform) {
31
31
  export function codexHooksPath(folder) {
32
32
  return join(folder, ".codex", "hooks.json");
33
33
  }
34
+ /**
35
+ * Whether the host runs the doorbell's hook init writes, Claude Code and Codex by the names they give the server, so the player's act on the page wakes
36
+ * the chat; the command line, which names no host, is taken as one. Elsewhere the player tells the chat in its window.
37
+ */
38
+ export function hasDoorbell(host) {
39
+ return host === undefined || agentHosts.includes(host);
40
+ }
34
41
  /** The instructions of the AI opponent, read from the package wherever it is installed: the MCP server's `instructions` tool gives them (D-102). */
35
42
  export function opponentInstructions() {
36
43
  return instructionsFile("opponent");
@@ -88,8 +95,8 @@ function claudeFile(folder) {
88
95
  return before.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md") ? undefined : { path, text: `@AGENTS.md\n${before}` };
89
96
  }
90
97
  /**
91
- * Claude Code's settings: the chesspal server of .mcp.json approved and its tools allowed, so that the first chat asks for neither, and the doorbell of the
92
- * moves on the page (D-47), and the note of how each chat starts (D-57); the player's own stay.
98
+ * Claude Code's settings: the chesspal server of .mcp.json approved and its tools allowed, so that the first chat asks for neither, the doorbell of the
99
+ * moves on the page (D-47), the note of how each chat starts (D-57), and the move the player writes alone in the chat (D-36); the player's own stay.
93
100
  */
94
101
  function claudeSettingsFile(folder) {
95
102
  const path = join(folder, ".claude", "settings.json");
@@ -100,7 +107,7 @@ function claudeSettingsFile(folder) {
100
107
  ...settings,
101
108
  enabledMcpjsonServers: withItem(settings.enabledMcpjsonServers, "chesspal"),
102
109
  permissions: { ...permissions, allow: withItem(permissions.allow, "mcp__chesspal") },
103
- hooks: withHook(withHook(hooks, "Stop", bellHook), "SessionStart", chatHook),
110
+ hooks: withHook(withHook(withHook(hooks, "Stop", bellHook), "SessionStart", chatHook), "UserPromptSubmit", promptHook),
104
111
  });
105
112
  }
106
113
  /** The hooks with one of Chesspal's put back beside the player's own whenever it is missing; an event of theirs that is no list stays theirs. */
@@ -118,6 +125,8 @@ function withHook(hooks, event, ours) {
118
125
  const bellHook = { type: "command", command: "chesspal listen", asyncRewake: true, timeout: 86_400 };
119
126
  // Claude Code's SessionStart hook (D-57): it notes how each chat starts, so that a new game waits for a clean one.
120
127
  const chatHook = { type: "command", command: "chesspal chat" };
128
+ // Claude Code's UserPromptSubmit hook (D-36): a message that is one move and nothing else is played before the AI reads it, which then only replies.
129
+ const promptHook = { type: "command", command: "chesspal prompt", timeout: 10 };
121
130
  /**
122
131
  * Codex's settings, read once the player trusts the folder: the chesspal server with its tools approved, a table of Chesspal's rewritten but for the
123
132
  * environment the player gave it; CHESSPAL_HOME and CHESSPAL_SEAT always passed on to the server, which Codex otherwise starts without them, so that a
@@ -1,2 +1,2 @@
1
1
  export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
- export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.ts";
2
+ export { codexHooksPath, hasDoorbell, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.ts";
@@ -1,3 +1,3 @@
1
1
  // The files through which an AI host finds Chesspal in the player's folder: the server, the opponent's instructions, the hosts' settings.
2
2
  export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
- export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.js";
3
+ export { codexHooksPath, hasDoorbell, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.js";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "preact/hooks";
3
- import { aiHostIn, aiModelIn, aiNameOf, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
3
+ import { aiHostIn, aiModelIn, aiNameOf, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, outOfBookMove, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
4
4
  import { PlayerPicture } from "../avatars/avatars.js";
5
5
  import { Thinker, Thought } from "../avatars/thought.js";
6
6
  import { Torretta } from "../avatars/torretta.js";
@@ -19,7 +19,7 @@ import { GameColumn } from "../game/column.js";
19
19
  import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
20
20
  import { Steps } from "../game/move-list.js";
21
21
  import { useOpeningBook } from "../game/opening-book.js";
22
- import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
22
+ import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, reviewCount, withBook, withJudgments, withoutReviews } from "../game/plies.js";
23
23
  import { GuideDialog } from "../guide/guide.js";
24
24
  import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
25
25
  import { helpsOf } from "../helps/view.js";
@@ -44,7 +44,7 @@ import { textFor } from "../text.js";
44
44
  import { anchorName, useAnchor } from "./anchor.js";
45
45
  import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
46
46
  import { phaseOf, verdictOf } from "./phase.js";
47
- import { ask, post, sendMessage, sendMove, useServed } from "./served.js";
47
+ import { ask, post, refusedText, sendMessage, sendMove, useServed } from "./served.js";
48
48
  import { tabTitle, useTabTitle } from "./title.js";
49
49
  /**
50
50
  * 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
@@ -55,6 +55,8 @@ export function App({ start }) {
55
55
  const [setting, setSetting] = useState(false);
56
56
  const text = useMemo(() => textFor(languagesOf(settings)), [settings.language]);
57
57
  const sound = settings.sound;
58
+ // The sounds off hush the voice too: it is one of them.
59
+ const voiced = settings.voice && sound;
58
60
  useEffect(() => void (document.documentElement.lang = text.language), [text]);
59
61
  const apply = (next) => {
60
62
  setSettings(next);
@@ -132,6 +134,20 @@ export function App({ start }) {
132
134
  const name = served?.opponent ?? text.ai;
133
135
  // Under it, the model and effort the AI played its last move with (D-82).
134
136
  const signed = served?.aiModel === undefined ? undefined : signature(served.aiModel.model, served.aiModel.effort);
137
+ // A new game that waited for the chat and could not start says why on the board, as one refused at once (D-107): once, and never one from before the
138
+ // page loaded.
139
+ const waitRefused = served?.table?.refused;
140
+ const waitRefusedSeen = useRef(undefined);
141
+ useEffect(() => {
142
+ if (served === undefined) {
143
+ return;
144
+ }
145
+ const number = waitRefused?.number ?? 0;
146
+ if (waitRefused !== undefined && waitRefusedSeen.current !== undefined && number > waitRefusedSeen.current) {
147
+ setRefusal({ text: refusedText(waitRefused, text, name) ?? "" });
148
+ }
149
+ waitRefusedSeen.current = number;
150
+ }, [served === undefined, waitRefused?.number]);
135
151
  // The game is replayed from its PGN once for each one the server sends, not at every click. Once no game goes on, the game in analysis shows in its place
136
152
  // (D-69), with the AI's badges and notes: replayed again only when its review changed.
137
153
  const game = served?.game;
@@ -153,8 +169,10 @@ export function App({ start }) {
153
169
  const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
154
170
  const material = useMemo(() => materialOf(live.position.fen), [live]);
155
171
  const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
172
+ // How many of the game's first plies a line of the book reaches, as the server counts them (D-68).
173
+ const bookPlies = studied !== undefined ? studied.book : game?.book ?? 0;
156
174
  // While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
157
- const inBook = studied === undefined && settings.book ? game?.book ?? 0 : 0;
175
+ const inBook = studied === undefined && settings.book ? bookPlies : 0;
158
176
  // The judgments of the player's moves, and the AI's drawings, show during a game only with the helps on, always once it is over (D-78); during it the
159
177
  // coach's come from its own file, never in the PGN the opponent may read (D-88).
160
178
  const judged = !going || settings.helps;
@@ -230,6 +248,8 @@ export function App({ start }) {
230
248
  // In analysis, the opening the move shown has reached, while the book knows the moves (D-69).
231
249
  const bookAt = studied === undefined || book === undefined || at === 0 || at > studied.book ? undefined : { tags: live.record.tags, moves: live.record.moves.slice(0, at) };
232
250
  const shownOpening = useMemo(() => bookAt === undefined || book === undefined ? undefined : openingOf(bookAt, book), [live, book, bookAt === undefined ? 0 : at]);
251
+ // Whether the move shown is still the book's, whatever the badges show: the card says the move the game left it at, once it is past it.
252
+ const leftBook = outOfBookMove(at, bookPlies);
233
253
  // A jump of more than one move shows the position at once: only a step slides its pieces.
234
254
  const shownBefore = useRef(at);
235
255
  const jumped = Math.abs(shownBefore.current - at) > 1;
@@ -328,6 +348,8 @@ export function App({ start }) {
328
348
  // With nobody at it, a review not finished says so, and the player brings the game back to the AI (D-109).
329
349
  const completeIt = summary === undefined && studied?.incomplete === true && !offline ? () => void studyGame({ game: studied.name }) : undefined;
330
350
  const closing = studied === undefined ? undefined : closingOf(live.record);
351
+ // How far the review has got, counted as status counts it for the AI: who is ahead after how many of the game's moves.
352
+ const count = reviewing || completeIt !== undefined ? reviewCount(plies) : undefined;
331
353
  // In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
332
354
  const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
333
355
  const talk = game?.talk ?? served.talk ?? [];
@@ -448,11 +470,11 @@ export function App({ start }) {
448
470
  : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
449
471
  ? (_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 }))
450
472
  : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, served.profiles, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, served.profiles, 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
451
- ? (_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) => {
473
+ ? (_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 } : {}), ...(count === undefined ? {} : { count }), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(voiced ? { 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) => {
452
474
  setDraft(words);
453
475
  setUnsent(undefined);
454
476
  }, 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")] })] }))
455
- : (_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,
477
+ : (_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, ...(voiced ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
456
478
  // Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
457
479
  spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
458
480
  setDraft(words);
@@ -463,11 +485,12 @@ export function App({ start }) {
463
485
  // A press on the board while the player looks back brings the game as it is: the next one plays.
464
486
  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 ? undefined : refusal ?? (linkIgnored === undefined ? undefined : { text: text.link[linkIgnored] }), 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)
465
487
  ? (_jsxs("div", { class: "over-board", children: [asleepNote, startClocks] }))
466
- : null] }), studied === undefined ? null : (_jsxs("div", { class: "board-steps", children: [_jsx(Steps, { shown: at, total: total, onStep: step, text: text }), standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: at, onShow: goTo, 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 ? {} : {
488
+ : null] }), studied === undefined ? null : (_jsxs("div", { class: "board-steps", children: [_jsx(Steps, { shown: at, total: total, onStep: step, text: text }), standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: at, onShow: goTo, 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, ...(leftBook === undefined ? {} : { outOfBook: leftBook }), 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 ? {} : {
467
489
  analysis: {
468
490
  by: analyst,
469
491
  ...(summary === undefined ? {} : { summary }),
470
492
  ...(reviewing ? { reviewing: true } : {}),
493
+ ...(count === undefined ? {} : { count }),
471
494
  ...(completeIt === undefined ? {} : { onComplete: completeIt }),
472
495
  },
473
496
  }), 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 })] })] }) }));
@@ -1,6 +1,7 @@
1
1
  // What the page asks the server (D-47): the game of the player's folder, read again each time it changes, and the player's moves, words and choices sent
2
2
  // there, refused in the player's language.
3
3
  import { useEffect, useRef, useState } from "preact/hooks";
4
+ import { closesForNewer, eventsAddress } from "../layout/window.js";
4
5
  import { watchServed } from "./watch.js";
5
6
  /**
6
7
  * The game of the player's folder, read from the server, and read again each time the server says it changed, whoever changed it; `onNewGame` when the chat
@@ -18,9 +19,9 @@ export function useServed(onNewGame) {
18
19
  const watch = watchServed({
19
20
  read: () => fetch("/api/game").then((response) => response.json()),
20
21
  connect: (handlers) => {
21
- const events = new EventSource("/api/events");
22
+ const events = new EventSource(eventsAddress());
22
23
  events.onopen = handlers.open;
23
- events.onmessage = (event) => handlers.message(event.data);
24
+ events.onmessage = (event) => closesForNewer(event.data) || handlers.message(event.data);
24
25
  events.onerror = handlers.error;
25
26
  return events;
26
27
  },
@@ -54,13 +55,17 @@ export async function ask(path, body, text, name) {
54
55
  return { answer: await response.json().catch(() => ({})) };
55
56
  }
56
57
  const refused = (await response.json());
57
- const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
58
- return { refused: { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
58
+ return { refused: { text: refusedText(refused, text, name) ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
59
59
  }
60
60
  catch (error) {
61
61
  return { refused: { text: String(error), words: false } };
62
62
  }
63
63
  }
64
+ /** A refusal of the server's in the player's language when the page knows its code, with the host's name where it sends the player; else its words. */
65
+ export function refusedText(refused, text, name) {
66
+ const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
67
+ return known?.(refused.most ?? 0, name) ?? refused.error;
68
+ }
64
69
  /** 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. */
65
70
  export function sendMove(move, words, text, name) {
66
71
  const message = words.trim();
@@ -9,7 +9,7 @@
9
9
 
10
10
  .torretta .light {
11
11
  fill: var(--torretta-light);
12
- transition: fill 0.2s;
12
+ transition: fill 200ms;
13
13
  }
14
14
 
15
15
  .torretta.mood-worried .light {
@@ -17,7 +17,7 @@
17
17
  }
18
18
 
19
19
  .torretta.mood-thoughtful .light {
20
- animation: ponder 2.4s ease-in-out infinite;
20
+ animation: ponder 2400ms ease-in-out infinite;
21
21
  }
22
22
 
23
23
  .torretta.mood-sad .light {
@@ -33,7 +33,7 @@
33
33
  .torretta .expression.changed {
34
34
  transform-box: fill-box;
35
35
  transform-origin: center;
36
- animation: face-in 0.16s ease-out;
36
+ animation: face-in 160ms ease-out;
37
37
  }
38
38
 
39
39
  @keyframes face-in {
@@ -80,7 +80,7 @@
80
80
  .torretta .eyes {
81
81
  transform-box: fill-box;
82
82
  transform-origin: center;
83
- animation: blink 6s infinite;
83
+ animation: blink 6000ms infinite;
84
84
  }
85
85
 
86
86
  @keyframes blink {
@@ -120,7 +120,7 @@ button {
120
120
 
121
121
  @media (prefers-reduced-motion: reduce) {
122
122
  .spinner {
123
- animation-duration: 3s;
123
+ animation-duration: 3000ms;
124
124
  }
125
125
  }
126
126
 
@@ -789,7 +789,7 @@
789
789
  .notice.right,
790
790
  .notice.left {
791
791
  top: calc((var(--row) + 0.5) * 12.5cqw);
792
- max-width: min(calc((var(--room) * 12.5cqw) - 12px), 340px);
792
+ max-width: min(calc(var(--room) * 12.5cqw - 12px), 340px);
793
793
  }
794
794
 
795
795
  .notice.right {
@@ -887,7 +887,9 @@
887
887
  }
888
888
 
889
889
  .promotion-piece:hover {
890
- box-shadow: var(--shadow), inset 0 0 0 max(2px, 0.625cqw) var(--hover-edge);
890
+ box-shadow:
891
+ var(--shadow),
892
+ inset 0 0 0 max(2px, 0.625cqw) var(--hover-edge);
891
893
  }
892
894
 
893
895
  /* White inside dark reads on every square and every theme. */
@@ -622,7 +622,9 @@ h3 + .switch {
622
622
 
623
623
  /* A game without a result: its star on a coin of the card's colour, readable where the four squares meet */
624
624
  .analyses .tile.open {
625
- background: radial-gradient(circle, var(--card) 0 11px, transparent 11.5px), conic-gradient(var(--dark-square) 0 25%, var(--light-square) 0 50%, var(--dark-square) 0 75%, var(--light-square) 0);
625
+ background:
626
+ radial-gradient(circle, var(--card) 0 11px, transparent 11.5px),
627
+ conic-gradient(var(--dark-square) 0 25%, var(--light-square) 0 50%, var(--dark-square) 0 75%, var(--light-square) 0);
626
628
  color: var(--soft);
627
629
  font-size: 18px;
628
630
  font-weight: 700;
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { outOfBookMove } from "../../core/index.js";
2
+ import {} from "../../core/index.js";
3
3
  import { LeadBar, LeadChart } from "../lead/lead.js";
4
4
  import { standingAt } from "../lead/view.js";
5
5
  import { Piece } from "../pieces/pieces.js";
6
6
  import { FoldButton, useCardFold } from "./card-fold.js";
7
7
  import { MoveList, Steps } from "./move-list.js";
8
8
  import { ResultTile } from "./result-tile.js";
9
+ import { ReviewProgress } from "./review-progress.js";
9
10
  /**
10
11
  * The column, which the player folds with its tab; under the board, where three columns would leave the board small, always open. A game in analysis
11
12
  * (D-69) says above the moves that the AI is going over it, until its word on the whole game comes, which the talk shows.
12
13
  */
13
- export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
+ export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, outOfBook, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
15
  const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
15
16
  return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
16
17
  ? (_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?.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
+ : (_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("div", { class: "card review waiting", children: [_jsx("p", { role: "status", children: text.reviewing(analysis.by) }), analysis.count === undefined ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.reviewing(analysis.by) })] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsxs("p", { children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] }), analysis.count === undefined || analysis.count.done === 0 ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.incomplete, stopped: true })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, ...(outOfBook === undefined ? {} : { outOfBook }), 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
19
  }
19
20
  /** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
20
21
  function SaveButton({ label, hint, onSave }) {
@@ -48,12 +49,13 @@ function GameCard({ plies, result, how, lit, started, move, toMove, bar, text })
48
49
  * The opening by Lichess's book: a book, crossed out once the game left it, and the family, which fold the card to them (Stefano); the variation under them,
49
50
  * and the move the game left the book at.
50
51
  */
51
- function OpeningCard({ opening, text }) {
52
+ function OpeningCard({ opening, outOfBook, text }) {
52
53
  const [foldedHere, toggleFold] = useCardFold("opening");
53
- const book = opening.inBook ? text.inBook : `${text.outOfBook} ${outOfBookMove(opening)}`;
54
+ const inBook = outOfBook === undefined;
55
+ const book = inBook ? text.inBook : `${text.outOfBook} ${outOfBook}`;
54
56
  // The family alone has nothing under it to fold: no button, no chevron (Stefano).
55
- const foldable = opening.variation !== undefined || !opening.inBook;
57
+ const foldable = opening.variation !== undefined || !inBook;
56
58
  const folded = foldable && foldedHere;
57
- const title = (_jsxs(_Fragment, { children: [_jsx("span", { class: `book${opening.inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), opening.inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }));
58
- return (_jsxs("section", { class: `card opening${folded ? " folded" : ""}`, "aria-label": text.opening, children: [_jsx("h3", { class: "family", children: foldable ? _jsx(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: title }) : title }), folded || opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), folded || opening.inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBookMove(opening)] })] }));
59
+ const title = (_jsxs(_Fragment, { children: [_jsx("span", { class: `book${inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }));
60
+ return (_jsxs("section", { class: `card opening${folded ? " folded" : ""}`, "aria-label": text.opening, children: [_jsx("h3", { class: "family", children: foldable ? _jsx(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: title }) : title }), folded || opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), folded || inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBook] })] }));
59
61
  }
@@ -228,7 +228,7 @@
228
228
  width: 2px;
229
229
  margin-left: -1px;
230
230
  background: #ffc450;
231
- box-shadow: 0 0 0 1px rgb(20 23 27 / 0.6);
231
+ box-shadow: 0 0 0 1px rgba(20, 23, 27, 0.6);
232
232
  pointer-events: none;
233
233
  }
234
234
 
@@ -319,24 +319,104 @@
319
319
  line-height: 1.45;
320
320
  }
321
321
 
322
+ /* The line that says the AI reviews, and under it how far it has got */
322
323
  .review.waiting,
323
324
  .say.reviewing {
324
- display: flex;
325
- align-items: center;
325
+ display: grid;
326
326
  gap: 8px;
327
327
  margin: 0;
328
328
  color: var(--soft);
329
329
  }
330
330
 
331
- /* 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) */
331
+ /* 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). Under them,
332
+ how far it got */
332
333
  .review.incomplete,
333
334
  .say.incomplete {
335
+ display: grid;
336
+ gap: 8px;
337
+ margin: 0;
338
+ color: var(--soft);
339
+ }
340
+
341
+ .review.incomplete > p,
342
+ .say.incomplete > p {
334
343
  display: flex;
335
344
  align-items: center;
336
345
  justify-content: space-between;
337
346
  gap: 8px;
338
- margin: 0;
339
- color: var(--soft);
347
+ }
348
+
349
+ /* How far a review has got: a thin bar in the accent and the count, 12/33. Before the first move a short run goes back and forth; full, while the word on
350
+ the whole game is written, it breathes; stopped unfinished, it stands still in the soft ink */
351
+ .review-progress {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ }
356
+
357
+ .review-progress .track {
358
+ flex: 1;
359
+ height: 4px;
360
+ overflow: hidden;
361
+ border-radius: 2px;
362
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
363
+ }
364
+
365
+ .review-progress .fill {
366
+ display: block;
367
+ width: 0;
368
+ height: 100%;
369
+ border-radius: inherit;
370
+ background: var(--accent);
371
+ transition: width 300ms ease-out;
372
+ }
373
+
374
+ .review-progress .count {
375
+ flex: none;
376
+ font-size: 12px;
377
+ font-variant-numeric: tabular-nums;
378
+ }
379
+
380
+ .review-progress.waiting .fill {
381
+ width: 30%;
382
+ animation: review-run 1400ms ease-in-out infinite alternate;
383
+ }
384
+
385
+ .review-progress.writing .fill {
386
+ animation: review-breath 1600ms ease-in-out infinite;
387
+ }
388
+
389
+ .review-progress.stopped .track {
390
+ background: color-mix(in srgb, var(--soft) 20%, transparent);
391
+ }
392
+
393
+ .review-progress.stopped .fill {
394
+ background: var(--soft);
395
+ }
396
+
397
+ @keyframes review-run {
398
+ to {
399
+ translate: 233%;
400
+ }
401
+ }
402
+
403
+ @keyframes review-breath {
404
+ 50% {
405
+ opacity: 0.45;
406
+ }
407
+ }
408
+
409
+ /* Still: before the first move a faint full bar, the count then grows without sliding */
410
+ @media (prefers-reduced-motion: reduce) {
411
+ .review-progress .fill {
412
+ transition: none;
413
+ animation: none;
414
+ }
415
+
416
+ .review-progress.waiting .fill {
417
+ width: 100%;
418
+ opacity: 0.35;
419
+ }
340
420
  }
341
421
 
342
422
  .review.incomplete button,
@@ -91,3 +91,7 @@ export function playerSpent(plies, ai, talk) {
91
91
  function judged({ review }) {
92
92
  return review?.note !== undefined || (review?.badge !== undefined && review.badge !== "book");
93
93
  }
94
+ /** How far a review has got, as status tells the AI: the moves the analyst has said who is ahead after, of all the game's. */
95
+ export function reviewCount(plies) {
96
+ return { done: plies.filter((ply) => ply.lead !== undefined).length, total: plies.length };
97
+ }
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ /**
3
+ * The line that says the analyst works, and under it a thin bar with the count, 12/33: a bar that runs back and forth before the first move is said, full
4
+ * and breathing while the word on the whole game is written; still, in the soft ink, when the review stopped unfinished.
5
+ */
6
+ export function ReviewProgress({ count, label, stopped = false }) {
7
+ const { done, total } = count;
8
+ const started = done > 0;
9
+ const state = stopped ? " stopped" : !started ? " waiting" : done >= total ? " writing" : "";
10
+ return (_jsxs("span", { class: `review-progress${state}`, role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": total, ...(started || stopped ? { "aria-valuenow": done, "aria-valuetext": `${done}/${total}` } : {}), children: [_jsx("span", { class: "track", "aria-hidden": "true", children: _jsx("span", { class: "fill", style: started ? { width: `${(100 * done) / Math.max(total, 1)}%` } : undefined }) }), started || stopped ? _jsxs("span", { class: "count", "aria-hidden": "true", children: [done, "/", total] }) : null] }));
11
+ }
@@ -111,7 +111,6 @@
111
111
  stroke-width: 2.4;
112
112
  }
113
113
 
114
-
115
114
  .guide-art .knot,
116
115
  .guide-art .caret {
117
116
  fill: var(--accent);
@@ -18,7 +18,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
18
18
 
19
19
  ### Getting started
20
20
 
21
- 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code** or **Codex**. It opens in a window of its own: keep it open while you play. The first time it asks some questions. Whether you trust the folder: **Yes**. With Codex, about the hooks, Chesspal's two helpers: **Trust all and continue**. On Windows Codex also asks whether to set up its sandbox, the protected space it works in: choose the first option, and yes when Windows asks for permission.
21
+ 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code** or **Codex**. It opens in a window of its own: keep it open while you play. The first time it asks some questions. Whether you trust the folder: **Yes** (in Claude Code No is chosen already: go down to Yes with the arrow keys, then Enter). With Codex, about the hooks, Chesspal's two helpers: **Trust all and continue**. On Windows Codex also asks whether to set up its sandbox, the protected space it works in: choose the first option, and yes when Windows asks for permission.
22
22
  2. Press **New game**, choose your colour, the clock and the AI's level, and press **Start**.
23
23
  3. Move: the AI answers on the board, and with a few words under its name.
24
24
 
@@ -35,7 +35,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
35
35
 
36
36
  ![The box to write in under the AI's name, and its answer](talk)
37
37
 
38
- - You talk with the AI in the **box under its name**, a few words with each move: it reads them with your move. You can also write to it in its window.
38
+ - You talk with the AI in the **box under its name**, a few words with each move: it reads them with your move. You can also write to it in its window. With Codex, if you write there while it waits for your move, press **Esc** after Enter: else it reads your message only after your move.
39
39
  - A **move written** in the chat counts too: `e4`, `Nf3`, with the English letters of the pieces (K Q R B N), or in words.
40
40
  - **The AI's name** opens the choice of its chat: Claude Code or Codex, the model (which AI: some are faster, others stronger) and how much it reasons. The more it reasons, the slower it plays. If you change them, the chat starts afresh in the same window: the AI forgets what you talked about, and the game stays as it is.
41
41
  - At each **new game**, the chat the page opened starts afresh by itself, once the AI has stopped thinking: a long chat makes the AI slower.
@@ -18,7 +18,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
18
18
 
19
19
  ### Per cominciare
20
20
 
21
- 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code** o **Codex**. Si apre in una finestra sua: tienila aperta mentre giochi. La prima volta fa qualche domanda. Se ti fidi della cartella: **Sì**. Con Codex, sugli hook, i due aiutanti di Chesspal: **Trust all and continue**. Su Windows Codex chiede anche se preparare la sua sandbox, lo spazio protetto dove lavora: scegli la prima opzione, e sì quando Windows chiede il permesso.
21
+ 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code** o **Codex**. Si apre in una finestra sua: tienila aperta mentre giochi. La prima volta fa qualche domanda. Se ti fidi della cartella: **Sì** (in Claude Code è già scelto No: scendi su Yes con le frecce, poi Invio). Con Codex, sugli hook, i due aiutanti di Chesspal: **Trust all and continue**. Su Windows Codex chiede anche se preparare la sua sandbox, lo spazio protetto dove lavora: scegli la prima opzione, e sì quando Windows chiede il permesso.
22
22
  2. Premi **Nuova partita**, scegli il colore, l'orologio e il livello dell'AI, e premi **Inizia**.
23
23
  3. Muovi: l'AI risponde sulla scacchiera, e con qualche parola sotto il suo nome.
24
24
 
@@ -35,7 +35,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
35
35
 
36
36
  ![La casella per scrivere sotto il nome dell'AI, e la sua risposta](talk)
37
37
 
38
- - Parli con l'AI nella **casella sotto il suo nome**, qualche parola a ogni mossa: le legge con la tua mossa. Puoi scriverle anche nella sua finestra.
38
+ - Parli con l'AI nella **casella sotto il suo nome**, qualche parola a ogni mossa: le legge con la tua mossa. Puoi scriverle anche nella sua finestra. Con Codex, se gli scrivi lì mentre aspetta la tua mossa, dopo Invio premi **Esc**: se no legge il messaggio solo dopo la tua mossa.
39
39
  - Anche una **mossa scritta** nella chat vale: `e4`, `Nf3`, con le lettere inglesi dei pezzi (K Q R B N), o a parole.
40
40
  - **Il nome dell'AI** apre la scelta della chat: Claude Code o Codex, il modello (quale AI: alcune sono più veloci, altre più forti) e quanto ragiona. Più ragiona, più gioca lenta. Se cambi, la chat riparte da capo nella stessa finestra: l'AI dimentica la chiacchierata di prima, la partita resta com'è.
41
41
  - A ogni **nuova partita** la chat aperta dalla pagina riparte da capo da sola, se l'AI ha finito di pensare: una chat lunga rende l'AI più lenta.
Binary file