chesspal 0.2.3 → 0.3.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 (214) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -8,7 +8,7 @@ export function PlayerCard({ place, picture, name, signed, side, level, taken, a
8
8
  const opponent = side === "white" ? "black" : "white";
9
9
  return (_jsx("section", { class: `card player ${place}`, children: _jsxs("div", { class: "player-line", children: [picture, _jsxs("div", { class: "who", children: [_jsx("div", { class: "name", title: typeof name === "string" ? name : undefined, children: name }), signed === undefined ? null : _jsx("div", { class: "signed", title: signed, children: signed }), side === undefined ? null : (_jsxs("div", { class: "note", children: [text.sides[side], level === undefined ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", _jsx(LevelTip, { level: level })] })), turn
10
10
  ? (_jsxs(_Fragment, { children: [" ", "\u00B7 ", _jsx("b", { class: "turn", children: text.yourTurn })] }))
11
- : null] })), _jsxs("div", { class: "taken", role: "img", "aria-label": text.taken(taken.map((piece) => text.piece(piece, opponent)), ahead), children: [groupsOf(taken).map((group) => (_jsx("span", { class: group[0]?.toUpperCase() === "P" ? "group pawns" : "group", children: group.map((piece, index) => _jsx(Piece, { letter: opponent === "white" ? piece : piece.toLowerCase(), small: true }, index)) }, group[0]))), ahead > 0 ? _jsxs("span", { class: "balance", children: ["+", ahead] }) : null] }), badges] }), clock] }) }));
11
+ : null] }))] }), clock, side === undefined && taken.length === 0 && badges === undefined ? null : (_jsxs("div", { class: "haul", children: [_jsxs("div", { class: "taken", role: "img", "aria-label": text.taken(taken.map((piece) => text.piece(piece, opponent)), ahead), children: [groupsOf(taken).map((group) => (_jsx("span", { class: group[0]?.toUpperCase() === "P" ? "group pawns" : "group", children: group.map((piece, index) => _jsx(Piece, { letter: opponent === "white" ? piece : piece.toLowerCase(), small: true }, index)) }, group[0]))), ahead > 0 ? _jsxs("span", { class: "balance", children: ["+", ahead] }) : null] }), badges] }))] }) }));
12
12
  }
13
13
  /**
14
14
  * A side's time on the game's clock, as the server keeps it: it runs while the side is to move and the game goes on, unless `held` (a move on its way);
@@ -1,12 +1,15 @@
1
1
  // The player's profile on the page (D-23, D-45): as the server sends it, the choices the wizard offers, and the requests that change it.
2
2
  /** How the player says they play, as the server keeps it. */
3
3
  export const levels = ["learning", "casual", "regular", "tournament"];
4
- /** Where a rating comes from. */
5
- export const ratingSites = ["lichess", "chess.com", "fide"];
6
- /** The lowest and highest rating the server takes. */
7
- export const ratingLimits = { lowest: 100, highest: 3500 };
4
+ /** The sites a rating is read from, by the player's names there (D-105). */
5
+ export const ratingSites = ["lichess", "chess.com"];
6
+ /** The speeds whose rating counts, in the order they count (D-106). */
7
+ export const speeds = ["rapid", "classical", "blitz"];
8
8
  /** The longest name the server keeps. */
9
9
  export const nameLimit = 40;
10
+ /** The FIDE ratings the server takes, as whole numbers (D-106). */
11
+ export const fideLowest = 1400;
12
+ export const fideHighest = 3000;
10
13
  /** The drawn avatars, by the ids the server keeps (D-45). */
11
14
  export const avatarIds = ["short-hair", "bun", "glasses", "beanie", "beard", "curls", "moustache", "silver-bob", "cat", "owl"];
12
15
  /** The address of a profile's own image: it changes with the image. */
@@ -14,12 +17,14 @@ export function imageAddress(profile) {
14
17
  return profile.image === undefined ? undefined : `/api/profile/image?profile=${encodeURIComponent(profile.id)}&v=${encodeURIComponent(profile.image)}`;
15
18
  }
16
19
  /** Makes a profile, in use unless `use` is false, with the photo after it, made small first so that a photo that cannot be read makes no profile: the refusal, or undefined. */
17
- export async function makeProfile({ name, level, rating, site, avatar, photo, use }) {
20
+ export async function makeProfile({ name, level, lichess, chesscom, fide, avatar, photo, use }) {
18
21
  const image = photo === undefined ? undefined : await shrunk(photo).catch(() => undefined);
19
22
  if (photo !== undefined && image === undefined) {
20
23
  return badImage;
21
24
  }
22
- const answer = await post("/api/profile/new", jsonOf({ name: name.trim(), level, ...(rating === undefined ? {} : { rating, site }), ...(avatar === undefined ? {} : { avatar }), ...(use === false ? { use } : {}) }));
25
+ const names = { ...(lichess?.trim() ? { lichess: lichess.trim() } : {}), ...(chesscom?.trim() ? { chesscom: chesscom.trim() } : {}) };
26
+ const chosen = { ...(fide === undefined ? {} : { fide }), ...(avatar === undefined ? {} : { avatar }), ...(use === false ? { use } : {}) };
27
+ const answer = await post("/api/profile/new", jsonOf({ name: name.trim(), level, ...names, ...chosen }));
23
28
  if ("refused" in answer) {
24
29
  return answer.refused;
25
30
  }
@@ -33,9 +38,9 @@ export async function useProfile(id) {
33
38
  export async function chooseAvatar(profile, avatar) {
34
39
  return refusalOf(await post(forProfile("/api/profile/avatar", profile), jsonOf({ avatar })));
35
40
  }
36
- /** The player's names on Lichess and chess.com for the profile the page shows, an empty one removed: the server's refusal, or undefined. */
37
- export async function saveAccounts(profile, accounts) {
38
- return refusalOf(await post(forProfile("/api/profile/accounts", profile), jsonOf({ lichess: accounts.lichess.trim(), chesscom: accounts.chesscom.trim() })));
41
+ /** What the player changes in the profile the page shows: an empty level or site name removed. The server's refusal, or undefined. */
42
+ export async function editProfile(profile, changes) {
43
+ return refusalOf(await post(forProfile("/api/profile/edit", profile), jsonOf(changes)));
39
44
  }
40
45
  /** Opens the data folder in the system's file manager, where the page runs: the server's refusal, or undefined. */
41
46
  export async function openDataFolder() {
@@ -30,6 +30,11 @@ export function settingsFrom(value, defaults = { theme: "system", sound: true })
30
30
  sound: typeof stored.sound === "boolean" ? stored.sound : defaults.sound,
31
31
  voice: typeof stored.voice === "boolean" ? stored.voice : false,
32
32
  language: oneOf(stored.language, ["auto", "it", "en"], "auto"),
33
+ // Checked against what the host offers when the chat opens, which this page does not know.
34
+ claudeModel: word(stored.claudeModel),
35
+ claudeEffort: word(stored.claudeEffort),
36
+ codexModel: word(stored.codexModel),
37
+ codexEffort: word(stored.codexEffort),
33
38
  };
34
39
  }
35
40
  /** Remembers the settings in this browser. */
@@ -75,3 +80,7 @@ function parsed(saved) {
75
80
  function oneOf(value, choices, fallback) {
76
81
  return choices.find((choice) => choice === value) ?? fallback;
77
82
  }
83
+ // A host's model or effort, a short word; empty when it is none.
84
+ function word(value) {
85
+ return typeof value === "string" && /^[\w.-]{1,40}$/.test(value) ? value : "";
86
+ }
@@ -4,6 +4,10 @@
4
4
  @import "layout/layout.css";
5
5
  @import "board/board.css";
6
6
  @import "talk/talk.css";
7
+ @import "talk/chat.css";
8
+ @import "table/opener.css";
7
9
  @import "game/game.css";
8
10
  @import "helps/helps.css";
11
+ @import "legend/legend.css";
9
12
  @import "dialogs/dialogs.css";
13
+ @import "guide/guide.css";
@@ -0,0 +1,65 @@
1
+ /* The table with no chat at it (D-104): the asleep note's box, with a row for each host, its button and its choices, and the command to type instead */
2
+ .opener {
3
+ display: grid;
4
+ grid-template-columns: minmax(0, 1fr) auto auto;
5
+ gap: 8px 6px;
6
+ }
7
+
8
+ .opener > p {
9
+ grid-column: 1 / -1;
10
+ margin: 0;
11
+ }
12
+
13
+ /* One row a host, its columns shared with the other's through the subgrid: the choices as wide as their longest name, the button takes the rest, all as tall */
14
+ .opener-host {
15
+ display: grid;
16
+ grid-column: 1 / -1;
17
+ grid-template-columns: subgrid;
18
+ }
19
+
20
+ .opener-host > .command {
21
+ justify-content: center;
22
+ padding: 6px 8px;
23
+ overflow: hidden;
24
+ white-space: nowrap;
25
+ }
26
+
27
+ .opener-host > .command:disabled,
28
+ .opener-host > .command:disabled:hover {
29
+ cursor: default;
30
+ opacity: 0.5;
31
+ background: var(--accent);
32
+ border-color: var(--accent);
33
+ }
34
+
35
+ .opener-host select {
36
+ padding: 4px;
37
+ border: 1px solid var(--line);
38
+ border-radius: 6px;
39
+ background: var(--card);
40
+ color: var(--ink);
41
+ font: inherit;
42
+ font-size: 13px;
43
+ }
44
+
45
+ /* Empty, the state takes no room: its margin gives back the row's gap */
46
+ .opener-state:empty {
47
+ margin-bottom: -8px;
48
+ }
49
+
50
+ .opener-command {
51
+ color: var(--soft);
52
+ font-size: 12.5px;
53
+ }
54
+
55
+ /* Each command whole on its line; two families, so the browser does not shrink the monospace */
56
+ .opener-command code {
57
+ font-family: ui-monospace, monospace;
58
+ white-space: nowrap;
59
+ user-select: all;
60
+ }
61
+
62
+ /* Over the board, where the note lets clicks through to the squares, only its buttons, choices and commands take them */
63
+ .over-board > .opener :is(button, select, code) {
64
+ pointer-events: auto;
65
+ }
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ // The table with no chat at it (D-104): a button for each host installed, which opens a terminal with a chat at the table, the model and effort it plays
3
+ // with beside it, kept in the profile, and the command to type in a terminal instead.
4
+ import { useState } from "preact/hooks";
5
+ /** Where the asleep note stands while no chat is at the table: the hosts that open one, their choices, and the command to type. */
6
+ export function ChatOpener({ table, settings, onChange, name, text }) {
7
+ const [sending, setSending] = useState(false);
8
+ const [failed, setFailed] = useState(false);
9
+ const words = text.table;
10
+ const installed = table.hosts.filter((host) => host.installed && host.name in choiceKeys);
11
+ const busy = sending || table.opening !== undefined;
12
+ const open = async (host) => {
13
+ setSending(true);
14
+ setFailed(false);
15
+ const opened = await openChat(host);
16
+ setSending(false);
17
+ setFailed(!opened);
18
+ };
19
+ // The chat started again for a new game needs nothing of the player.
20
+ if (table.opening === "again") {
21
+ return _jsx("p", { class: "asleep", role: "status", children: words.again(name) });
22
+ }
23
+ if (installed.length === 0) {
24
+ return _jsx("p", { class: "asleep", role: "status", children: words.install(name) });
25
+ }
26
+ // The state stays in the page, empty when there is none, so a screen reader hears it change.
27
+ return (_jsxs("section", { class: "asleep opener", "aria-label": words.label, children: [_jsx("p", { children: words.noChat(name) }), installed.map((host) => (_jsxs("div", { class: "opener-host", children: [_jsx("button", { type: "button", class: "command primary", disabled: busy, onClick: () => void open(host.name), children: hostTitles[host.name] ?? host.name }), _jsx(Choice, { host: host, kind: "model", settings: settings, onChange: onChange, text: text }), _jsx(Choice, { host: host, kind: "effort", settings: settings, onChange: onChange, text: text })] }, host.name))), _jsx("p", { class: "opener-state", role: "status", children: busy ? words.opening : failed ? words.failed : null }), _jsxs("p", { class: "opener-command", children: [words.typeIt, " ", installed.map((host, index) => (_jsxs("span", { children: [index === 0 ? "" : " · ", _jsx("code", { children: host.command })] }, host.name)))] })] }));
28
+ }
29
+ const hostTitles = { claude: "Claude Code", codex: "Codex" };
30
+ // The settings that keep each host's choice.
31
+ const choiceKeys = {
32
+ claude: { model: "claudeModel", effort: "claudeEffort" },
33
+ codex: { model: "codexModel", effort: "codexEffort" },
34
+ };
35
+ /** A host's model or effort: a dash keeps the host's own; a choice it no longer offers shows as the dash. Its title says what it stands at, the dash too. */
36
+ function Choice({ host, kind, settings, onChange, text }) {
37
+ const key = choiceKeys[host.name][kind];
38
+ const offered = kind === "model" ? host.models : host.efforts;
39
+ const chosen = offered.includes(settings[key]) ? settings[key] : "";
40
+ const title = hostTitles[host.name] ?? host.name;
41
+ if (offered.length === 0) {
42
+ return null;
43
+ }
44
+ return (_jsxs("select", { "aria-label": `${text.table[kind]}, ${title}`, title: `${text.table[kind]}: ${chosen === "" ? text.table.asHost(title) : chosen}`, value: chosen, onChange: (event) => onChange({ ...settings, [key]: event.currentTarget.value }), children: [_jsx("option", { value: "", children: "\u2014" }), offered.map((value) => _jsx("option", { value: value, children: shortName(value) }, value))] }));
45
+ }
46
+ // Codex's models without the "gpt-" they all start with, so the choice fits beside the button in the column; the title keeps the whole name.
47
+ function shortName(value) {
48
+ return value.replace(/^gpt-/, "");
49
+ }
50
+ /** Asks the table to open a terminal with a chat in the host: whether it did. */
51
+ async function openChat(host) {
52
+ try {
53
+ const response = await fetch("/api/table/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ host }) });
54
+ return response.ok && (await response.json()).opened === true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,97 @@
1
+ /* The talk over a game in analysis (D-101): the player's words and the AI's answers in one thread over the box, by the player's card; the AI's note stays
2
+ with the other side's card, and the thread gives way before the cards do (Zoe) */
3
+ .talk.studied > .you-block {
4
+ flex: 1 1 auto;
5
+ max-height: none;
6
+ }
7
+
8
+ /* A line apart from the AI's note, which another voice signed (Zoe) */
9
+ .talk.studied > section > .chat {
10
+ display: flex;
11
+ flex-direction: column;
12
+ flex: 1 1 auto;
13
+ min-height: 0;
14
+ margin-top: 12px;
15
+ padding-top: 10px;
16
+ border-top: 1px solid var(--line);
17
+ }
18
+
19
+ .chat > .compose {
20
+ flex: none;
21
+ }
22
+
23
+ /* The thread reads in time's order whichever way the board is turned: the newest words just over the box (Zoe) */
24
+ .flipped .talk.studied,
25
+ .flipped .talk.studied > section {
26
+ flex-direction: column;
27
+ }
28
+
29
+ .flipped .talk.studied .sent {
30
+ justify-content: flex-end;
31
+ margin: 0 0 8px;
32
+ }
33
+
34
+ .flipped .talk.studied .compose {
35
+ margin: 0;
36
+ }
37
+
38
+ /* Beside the board the talk takes the room the AI's notes leave, not half the column as in a game; turned round, the player's card goes on top and the
39
+ AI's note below the box; on a phone the talk has an area of its own (layout.css) */
40
+ @media (min-width: 841px) {
41
+ .chat > .sent {
42
+ flex: 1 1 0;
43
+ }
44
+
45
+ .flipped .talk.studied,
46
+ .flipped .talk.studied > section {
47
+ flex-direction: column-reverse;
48
+ }
49
+
50
+ .flipped .talk.studied > section > .chat {
51
+ margin: 0 0 12px;
52
+ padding: 0 0 10px;
53
+ border-top: 0;
54
+ border-bottom: 1px solid var(--line);
55
+ }
56
+ }
57
+
58
+ /* The AI's answers on the left, in its bubbles, without the tail that points at the Torretta: it is not by them */
59
+ .sent li.from-ai {
60
+ align-self: flex-start;
61
+ align-items: flex-start;
62
+ }
63
+
64
+ .talk.studied .sent .say::before,
65
+ .talk.studied .sent .say::after {
66
+ content: none;
67
+ }
68
+
69
+ /* The box has no tail either: the card it would point at may be someone else's */
70
+ .compose.asking::after {
71
+ content: none;
72
+ }
73
+
74
+ /* The move a line was written on, small at its head: a press shows it again */
75
+ .shown-move {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 1px;
79
+ min-height: 24px;
80
+ margin: -2px 0 2px -6px;
81
+ padding: 2px 6px;
82
+ border: 0;
83
+ border-radius: 6px;
84
+ background: none;
85
+ color: inherit;
86
+ font: inherit;
87
+ font-size: 12px;
88
+ font-weight: 600;
89
+ opacity: 0.85;
90
+ cursor: pointer;
91
+ }
92
+
93
+ .shown-move:hover,
94
+ .shown-move:focus-visible {
95
+ opacity: 1;
96
+ background: color-mix(in oklab, currentColor 15%, transparent);
97
+ }
@@ -1,23 +1,42 @@
1
1
  /* The talk's column: the players' cards with their pictures and names, the faces, the words said and the box to write in, the card of a page offline,
2
2
  and the notes laid over the board between games */
3
3
 
4
- /* The picture stays at the card's top left, however many lines the card has; the clock in the middle */
4
+ /* The picture at the card's top left, the clock at its top right (Stefano); the pieces taken and the badges under the name, run to the card's edge, under
5
+ the clock too; the button that starts the clocks, or the next game, takes the card's width below */
5
6
  .player-line {
6
- display: flex;
7
- align-items: flex-start;
8
- gap: 12px;
7
+ display: grid;
8
+ grid-template-columns: auto minmax(0, 1fr) auto;
9
+ grid-template-rows: auto 1fr;
10
+ column-gap: 12px;
11
+ align-items: start;
12
+ }
13
+
14
+ .player-line > :first-child {
15
+ grid-column: 1;
16
+ grid-row: 1;
17
+ }
18
+
19
+ .player-line:has(> .haul) > :first-child {
20
+ grid-row: 1 / span 2;
21
+ }
22
+
23
+ .player-line > .who {
24
+ grid-column: 2;
25
+ grid-row: 1;
9
26
  }
10
27
 
11
28
  .player-line > .clock {
12
- align-self: center;
29
+ grid-column: 3;
30
+ grid-row: 1;
13
31
  }
14
32
 
15
- .player-line > svg {
16
- flex: none;
33
+ .player-line > .haul {
34
+ grid-column: 2 / -1;
35
+ grid-row: 2;
36
+ min-width: 0;
17
37
  }
18
38
 
19
39
  .who {
20
- flex: 1;
21
40
  min-width: 0;
22
41
  line-height: 1.25;
23
42
  }
@@ -76,14 +95,11 @@
76
95
 
77
96
  /* The clocks wait for the player (Zoe): the button that starts them takes a line of the player's card, under the clock it starts, off the board's squares;
78
97
  between games the new game takes the same line, in the clock's place (D-77) */
79
- .player-line:has(> .start-clocks, > .next-game) {
80
- flex-wrap: wrap;
81
- row-gap: 10px;
82
- }
83
-
84
98
  .start-clocks,
85
99
  .next-game {
86
- flex: 1 0 100%;
100
+ grid-column: 1 / -1;
101
+ grid-row: 3;
102
+ margin-top: 10px;
87
103
  justify-content: center;
88
104
  padding: 8px 14px;
89
105
  font-size: 15px;
@@ -107,7 +123,8 @@
107
123
  align-self: center;
108
124
  }
109
125
 
110
- .player-line > .who:not(:has(.note)) .taken:empty {
126
+ /* In analysis, a side that took nothing has its badges right under its name */
127
+ .haul:has(> .badge-counts) > .taken:empty {
111
128
  display: none;
112
129
  }
113
130
 
@@ -430,6 +447,12 @@
430
447
  display: none;
431
448
  }
432
449
 
450
+ /* A bubble keeps its height: one that does not fit is hidden whole, the newest scrolls in itself (talk.tsx) */
451
+ .says > li,
452
+ .sent > li {
453
+ flex: none;
454
+ }
455
+
433
456
  .say {
434
457
  position: relative;
435
458
  background: var(--say);
@@ -646,11 +669,6 @@
646
669
  margin: 8px 0 0;
647
670
  }
648
671
 
649
- /* In analysis nothing in the talk gives way: on a short screen the talk scrolls whole */
650
- .talk.studied {
651
- overflow-y: auto;
652
- }
653
-
654
672
  .talk.studied > section,
655
673
  .talk.studied > section > * {
656
674
  flex: none;
@@ -703,12 +721,6 @@
703
721
  margin-top: 4px;
704
722
  }
705
723
 
706
- /* In analysis the board turned round changes which side's card is on top, never the order of the talk */
707
- .flipped .talk.studied,
708
- .flipped .talk.studied > section {
709
- flex-direction: column;
710
- }
711
-
712
724
  .move-note .head {
713
725
  display: flex;
714
726
  align-items: center;
@@ -59,29 +59,35 @@ export function Legend({ review, words }) {
59
59
  }
60
60
  /**
61
61
  * The player's words of the game, the newest by the box they write in with how far it got, and the box: Enter or the button sends the words alone, the next
62
- * move takes them with it. 200 characters from one move of the AI's to the next: once they are written, the box goes until its move (D-54).
62
+ * move takes them with it. 200 characters from one move of the AI's to the next: once they are written, the box goes until its move (D-54). In analysis the
63
+ * AI's answers are in the same thread, and each line of the player's names the move it was written on (D-101).
63
64
  */
64
- export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, text }) {
65
+ export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, about, text }) {
65
66
  const list = useRef(null);
66
67
  useOnlyWhole(list, lines.map((line) => line.text));
67
68
  const left = talkLimits.line - spent;
68
69
  const blank = draft.trim() === "";
70
+ const lastWritten = lines.findLastIndex((line) => line.who === "player");
71
+ const lastAnswer = lines.findLastIndex((line) => line.who === "ai");
72
+ const prompt = about === undefined ? text.writeTo(name) : text.askAbout(name);
69
73
  const submit = () => {
70
74
  if (!blank) {
71
75
  onSend();
72
76
  }
73
77
  };
74
- return (_jsxs(_Fragment, { children: [_jsx("ol", { ref: list, class: "sent", "aria-live": "polite", children: inOrder(lines.map((line, index) => (_jsxs("li", { style: aged(lines.length - 1 - index), children: [_jsx("p", { class: "said", children: line.text }), index === lines.length - 1 ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), left <= 0 ? null : (_jsxs("form", { class: "compose", onSubmit: (event) => {
78
+ return (_jsxs(_Fragment, { children: [_jsx("ol", { ref: list, class: "sent", "aria-live": "polite", children: inOrder(lines.map((line, index) => (_jsxs("li", { class: line.who === "ai" ? "from-ai" : undefined, style: about === undefined ? aged(lines.length - 1 - index) : reread(index, lastAnswer), children: [line.who === "ai"
79
+ ? (_jsx("p", { class: "say", children: _jsx(Marked, { text: line.text }) }))
80
+ : (_jsxs("p", { class: "said", children: [about === undefined || line.shown?.move === undefined ? null : _jsx(ShownMove, { move: line.shown.move, onShow: () => about.onShow(line.shown.at), text: text }), line.text] })), index === lastWritten ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), left <= 0 ? null : (_jsxs("form", { class: about === undefined ? "compose" : "compose asking", onSubmit: (event) => {
75
81
  event.preventDefault();
76
82
  submit();
77
- }, children: [_jsx("p", { class: "limit", role: "status", children: problem ?? "" }), _jsx("textarea", { rows: 2, maxLength: left, placeholder: text.writeTo(name), "aria-label": text.writeTo(name), value: draft, onInput: (event) => onDraft(event.currentTarget.value), onKeyDown: (event) => {
83
+ }, children: [_jsx("p", { class: "limit", role: "status", children: problem ?? "" }), _jsx("textarea", { rows: 2, maxLength: left, placeholder: prompt, "aria-label": prompt, value: draft, onInput: (event) => onDraft(event.currentTarget.value), onKeyDown: (event) => {
78
84
  if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
79
85
  event.preventDefault();
80
86
  submit();
81
87
  }
82
88
  } }), _jsxs("div", { class: "foot", children: [spent + draft.length >= talkLimits.line - 50
83
89
  ? _jsxs("span", { class: "count", children: [spent + draft.length, "/", talkLimits.line] })
84
- : _jsx("span", { class: "hint", children: !blank && canMove ? text.withMove : "" }), _jsxs("button", { class: "send", type: "submit", disabled: blank, children: [text.send, _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H18M13 7L18 12L13 17" }) })] })] })] }))] }));
90
+ : _jsx("span", { class: "hint", children: about === undefined ? !blank && canMove ? text.withMove : "" : _jsx(OnMove, { ply: about.ply, text: text }) }), _jsxs("button", { class: "send", type: "submit", disabled: blank, children: [text.send, _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H18M13 7L18 12L13 17" }) })] })] })] }))] }));
85
91
  }
86
92
  /**
87
93
  * The talk narrowed to a card beside the board: the Torretta above, the player below, each with their name and side written down the card, a ring on
@@ -90,6 +96,18 @@ export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, can
90
96
  export function TalkFaces({ name, aiPicture, you, picture, ai, player, aiMoves, playerMoves, unread, text }) {
91
97
  return (_jsxs("div", { class: "faces", children: [_jsx(RailPlayer, { place: "ai", picture: aiPicture ?? _jsx(Torretta, { size: 40 }), name: name, ...(ai === undefined ? {} : { side: text.sides[ai] }), moves: aiMoves, unread: unread }), _jsx(RailPlayer, { place: "you", picture: picture, name: you, ...(player === undefined ? {} : { side: text.sides[player] }), moves: playerMoves })] }));
92
98
  }
99
+ /** The move a line of the player's was written on, 14.♘f3, small at the line's head: a press shows it on the board again (Zoe). */
100
+ function ShownMove({ move, onShow, text }) {
101
+ const [, number = "", san = move] = /^(\d+\.+)(.+)$/.exec(move) ?? [];
102
+ return (_jsxs("button", { type: "button", class: "shown-move", title: text.goTo(move), "aria-label": text.goTo(move), onClick: onShow, children: [_jsx("span", { class: "move-number", children: number }), _jsx(Figurines, { san: san, side: number.endsWith("...") ? "black" : "white" })] }));
103
+ }
104
+ /** What the words go with in analysis: the move shown, or the whole game at its start. */
105
+ function OnMove({ ply, text }) {
106
+ if (ply === undefined) {
107
+ return _jsx(_Fragment, { children: text.onWholeGame });
108
+ }
109
+ return (_jsxs(_Fragment, { children: [text.onMove, " ", _jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] }));
110
+ }
93
111
  function OnBoard({ text }) {
94
112
  const point = useContext(PointSquare);
95
113
  return (_jsx(_Fragment, { children: text.split(boardWords).map((part, index) => index % 2 === 1
@@ -130,6 +148,10 @@ function inOrder(items, newestFirst) {
130
148
  function aged(age) {
131
149
  return { opacity: age === 0 ? 1 : Math.max(0.2, 0.4 - (age - 1) * 0.1) };
132
150
  }
151
+ /** In analysis the talk is read again: the AI's last answer and what follows it whole, the older lines a little back, still readable (Zoe). */
152
+ function reread(index, lastAnswer) {
153
+ return { opacity: index >= lastAnswer ? 1 : 0.75 };
154
+ }
133
155
  /**
134
156
  * A list that never scrolls: a bubble that does not fit whole in it is hidden, the oldest first since the newest sit by the card, and shows again when the
135
157
  * window grows.
@@ -142,13 +164,15 @@ function useOnlyWhole(list, lines, newestFirst = false) {
142
164
  }
143
165
  const fit = () => {
144
166
  const box = element.getBoundingClientRect();
167
+ const padding = getComputedStyle(element);
168
+ const room = element.clientHeight - parseFloat(padding.paddingTop) - parseFloat(padding.paddingBottom);
145
169
  const newest = newestFirst ? element.firstElementChild : element.lastElementChild;
146
170
  for (const item of element.children) {
147
171
  const place = item.getBoundingClientRect();
148
172
  const style = item.style;
149
173
  // The newest never hides: one longer than the column, as a note with the helps on (D-78), scrolls in itself.
150
- style.maxHeight = item === newest ? box.height + "px" : "";
151
- style.overflowY = item === newest && item.scrollHeight > box.height ? "auto" : "";
174
+ style.maxHeight = item === newest ? room + "px" : "";
175
+ style.overflowY = item === newest && item.scrollHeight > room ? "auto" : "";
152
176
  style.visibility = item === newest || (place.top >= box.top - 1 && place.bottom <= box.bottom + 1) ? "" : "hidden";
153
177
  }
154
178
  };