chesspal 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -12
- package/dist/cli/cli.js +83 -3
- package/dist/cli/command-line.js +26 -9
- package/dist/cli/icon.d.ts +25 -0
- package/dist/cli/icon.js +80 -0
- package/dist/commands/agents.d.ts +19 -2
- package/dist/commands/agents.js +39 -13
- package/dist/commands/ai-say.js +3 -4
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +46 -13
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +40 -18
- package/dist/commands/answer-lines.d.ts +2 -0
- package/dist/commands/answer-lines.js +13 -3
- package/dist/commands/coach.js +4 -2
- package/dist/commands/context.d.ts +11 -1
- package/dist/commands/context.js +5 -0
- package/dist/commands/delete-review.js +9 -5
- package/dist/commands/game-summary.d.ts +2 -0
- package/dist/commands/game-summary.js +4 -0
- package/dist/commands/index.d.ts +3 -2
- package/dist/commands/index.js +7 -4
- package/dist/commands/init.js +2 -4
- package/dist/commands/instructions.d.ts +9 -0
- package/dist/commands/instructions.js +30 -0
- package/dist/commands/lead-input.d.ts +1 -1
- package/dist/commands/lead-input.js +7 -4
- package/dist/commands/page-link.js +6 -5
- package/dist/commands/past-games.d.ts +1 -1
- package/dist/commands/past-games.js +12 -5
- package/dist/commands/player-move.js +2 -3
- package/dist/commands/profiles.d.ts +23 -9
- package/dist/commands/profiles.js +76 -35
- package/dist/commands/ratings.d.ts +8 -0
- package/dist/commands/ratings.js +45 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +3 -2
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +46 -27
- package/dist/commands/study.js +8 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/review.d.ts +2 -0
- package/dist/core/review.js +5 -1
- package/dist/games/ai-model.d.ts +4 -2
- package/dist/games/ai-model.js +10 -1
- package/dist/games/analysis.d.ts +26 -3
- package/dist/games/analysis.js +41 -6
- package/dist/games/archive.d.ts +6 -3
- package/dist/games/archive.js +13 -5
- package/dist/games/current-game.d.ts +10 -1
- package/dist/games/current-game.js +49 -15
- package/dist/games/files.d.ts +5 -0
- package/dist/games/files.js +42 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +3 -3
- package/dist/home/data-folder.d.ts +2 -0
- package/dist/home/data-folder.js +5 -1
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/host-files/agents.d.ts +2 -2
- package/dist/host-files/agents.js +12 -6
- package/dist/host-files/host-files.d.ts +5 -3
- package/dist/host-files/host-files.js +15 -8
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +2 -2
- package/dist/page/app/app.js +49 -20
- package/dist/page/app/title.js +17 -0
- package/dist/page/avatars/torretta.js +4 -4
- package/dist/page/dialogs/analyses.js +4 -4
- package/dist/page/dialogs/dialog.js +4 -1
- package/dist/page/dialogs/dialogs.css +84 -10
- package/dist/page/dialogs/profiles.js +96 -43
- package/dist/page/game/column.js +5 -1
- package/dist/page/game/plies.js +7 -4
- package/dist/page/guide/blocks.js +67 -0
- package/dist/page/guide/en.md +66 -0
- package/dist/page/guide/foot.js +18 -0
- package/dist/page/guide/guide.css +200 -0
- package/dist/page/guide/guide.js +89 -0
- package/dist/page/guide/it.md +66 -0
- package/dist/page/guide/markdown.js +67 -0
- package/dist/page/helps/helps.css +7 -10
- package/dist/page/layout/favicon.svg +13 -0
- package/dist/page/layout/header.js +3 -3
- package/dist/page/layout/icon.icns +0 -0
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/icon.png +0 -0
- package/dist/page/layout/layout.css +85 -4
- package/dist/page/layout/window.js +45 -0
- package/dist/page/main.js +4 -1
- package/dist/page/players/card.js +1 -1
- package/dist/page/profile/profile.js +14 -9
- package/dist/page/profile/settings.js +9 -0
- package/dist/page/style.css +2 -0
- package/dist/page/table/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +23 -5
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +6 -4
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +131 -17
- package/dist/page/theme.css +7 -0
- package/dist/profiles/index.d.ts +2 -1
- package/dist/profiles/index.js +2 -1
- package/dist/profiles/profiles.d.ts +10 -15
- package/dist/profiles/profiles.js +30 -15
- package/dist/profiles/ratings.d.ts +35 -0
- package/dist/profiles/ratings.js +61 -0
- package/dist/server/bell.d.ts +24 -3
- package/dist/server/bell.js +84 -10
- package/dist/server/caller.d.ts +2 -1
- package/dist/server/caller.js +3 -2
- package/dist/server/mcp.d.ts +7 -3
- package/dist/server/mcp.js +100 -52
- package/dist/server/page/analyses.d.ts +2 -0
- package/dist/server/page/analyses.js +20 -5
- package/dist/server/page/attach.d.ts +4 -0
- package/dist/server/page/attach.js +112 -0
- package/dist/server/page/chat.d.ts +65 -0
- package/dist/server/page/chat.js +87 -0
- package/dist/server/page/events.d.ts +7 -0
- package/dist/server/page/events.js +20 -0
- package/dist/server/page/files.js +10 -1
- package/dist/server/page/game.js +43 -23
- package/dist/server/page/page.d.ts +51 -7
- package/dist/server/page/page.js +74 -48
- package/dist/server/page/profiles.d.ts +5 -0
- package/dist/server/page/profiles.js +26 -10
- package/dist/server/page/table.d.ts +58 -0
- package/dist/server/page/table.js +210 -0
- package/dist/server/table/browser.d.ts +6 -0
- package/dist/server/table/browser.js +48 -0
- package/dist/server/table/hosts/chats.d.ts +7 -0
- package/dist/server/table/hosts/chats.js +34 -0
- package/dist/server/table/hosts/find.d.ts +2 -0
- package/dist/server/table/hosts/find.js +20 -0
- package/dist/server/table/hosts/hosts.d.ts +34 -0
- package/dist/server/table/hosts/hosts.js +49 -0
- package/dist/server/table/hosts/index.d.ts +3 -0
- package/dist/server/table/hosts/index.js +4 -0
- package/dist/server/table/hosts/play.d.ts +29 -0
- package/dist/server/table/hosts/play.js +180 -0
- package/dist/server/table/hosts/seat.d.ts +33 -0
- package/dist/server/table/hosts/seat.js +80 -0
- package/dist/server/table/hosts/terminal.d.ts +6 -0
- package/dist/server/table/hosts/terminal.js +63 -0
- package/dist/server/table/icon/index.d.ts +32 -0
- package/dist/server/table/icon/index.js +83 -0
- package/dist/server/table/icon/linux.d.ts +5 -0
- package/dist/server/table/icon/linux.js +47 -0
- package/dist/server/table/icon/macos.d.ts +3 -0
- package/dist/server/table/icon/macos.js +38 -0
- package/dist/server/table/icon/windows.d.ts +3 -0
- package/dist/server/table/icon/windows.js +48 -0
- package/dist/server/table/index.d.ts +6 -0
- package/dist/server/table/index.js +7 -0
- package/dist/server/table/launch.d.ts +19 -0
- package/dist/server/table/launch.js +78 -0
- package/dist/server/table/link.d.ts +10 -0
- package/dist/server/table/link.js +231 -0
- package/dist/server/table/table.d.ts +35 -0
- package/dist/server/table/table.js +94 -0
- package/dist/sites/chesscom.d.ts +4 -0
- package/dist/sites/chesscom.js +25 -0
- package/dist/sites/choice.d.ts +17 -0
- package/dist/sites/choice.js +13 -0
- package/dist/sites/index.d.ts +2 -0
- package/dist/sites/index.js +4 -0
- package/dist/sites/lichess.d.ts +4 -0
- package/dist/sites/lichess.js +32 -0
- package/dist/sites/request.d.ts +11 -0
- package/dist/sites/request.js +15 -0
- package/dist/sites/sites.d.ts +6 -0
- package/dist/sites/sites.js +33 -0
- package/instructions/analysis.md +1 -1
- package/instructions/analyst-agent.md +1 -1
- package/instructions/coach.md +2 -1
- package/instructions/opponent.md +6 -6
- package/instructions/ponder.md +1 -0
- package/instructions/start.md +1 -0
- package/package.json +1 -1
|
@@ -1005,12 +1005,6 @@ h3 + .switch {
|
|
|
1005
1005
|
outline-offset: 2px;
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
-
.rating {
|
|
1009
|
-
display: grid;
|
|
1010
|
-
grid-template-columns: 1fr 1fr;
|
|
1011
|
-
gap: 8px;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
1008
|
.profile-form .start {
|
|
1015
1009
|
width: 100%;
|
|
1016
1010
|
padding: 13px;
|
|
@@ -1089,13 +1083,53 @@ h3 + .switch {
|
|
|
1089
1083
|
height: 32px;
|
|
1090
1084
|
}
|
|
1091
1085
|
|
|
1092
|
-
/* The
|
|
1093
|
-
.
|
|
1086
|
+
/* The player in use, with the pencil that opens their profile beside their line */
|
|
1087
|
+
.profile-row {
|
|
1088
|
+
display: flex;
|
|
1089
|
+
gap: 6px;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.profile-row .choice {
|
|
1093
|
+
flex: 1;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.profile-row .edit-profile {
|
|
1097
|
+
flex: none;
|
|
1098
|
+
align-self: center;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/* The profile window: the name with its tick once saved, as the names on the sites have */
|
|
1102
|
+
.profile-edit .field {
|
|
1094
1103
|
display: grid;
|
|
1095
|
-
grid-template-columns:
|
|
1104
|
+
grid-template-columns: 1fr 18px;
|
|
1105
|
+
align-items: center;
|
|
1106
|
+
gap: 0 10px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.profile-edit .field > span {
|
|
1110
|
+
grid-column: 1 / -1;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.profile-edit .field .in-use {
|
|
1114
|
+
width: 18px;
|
|
1115
|
+
height: 18px;
|
|
1116
|
+
color: var(--accent);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.profile-edit input[type="text"]:invalid {
|
|
1120
|
+
border-color: var(--low-time);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/* The names on Lichess and chess.com: the site and the name; in the profile window the rating read there, and a tick once saved (D-93, D-105) */
|
|
1124
|
+
.accounts {
|
|
1125
|
+
display: grid;
|
|
1126
|
+
grid-template-columns: auto 1fr;
|
|
1096
1127
|
align-items: center;
|
|
1097
1128
|
gap: 6px 10px;
|
|
1098
|
-
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.accounts.read {
|
|
1132
|
+
grid-template-columns: auto 1fr minmax(4ch, auto) 18px;
|
|
1099
1133
|
}
|
|
1100
1134
|
|
|
1101
1135
|
.accounts label {
|
|
@@ -1132,6 +1166,46 @@ h3 + .switch {
|
|
|
1132
1166
|
grid-column: 1 / -1;
|
|
1133
1167
|
}
|
|
1134
1168
|
|
|
1169
|
+
/* The rating a site gives, its speed in its title; a cross for a name it does not know, a dash or the rating soft when it did not answer, dots while read */
|
|
1170
|
+
.accounts .site-mark {
|
|
1171
|
+
color: var(--ink);
|
|
1172
|
+
font-size: 15px;
|
|
1173
|
+
font-weight: 600;
|
|
1174
|
+
font-variant-numeric: tabular-nums;
|
|
1175
|
+
text-align: right;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.accounts .site-mark span {
|
|
1179
|
+
color: inherit;
|
|
1180
|
+
font-size: inherit;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.accounts .site-mark.missing {
|
|
1184
|
+
color: var(--low-time);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.accounts .site-mark.unreached,
|
|
1188
|
+
.accounts .site-mark.pending {
|
|
1189
|
+
color: var(--soft);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/* The cross and the dash in words, under the names */
|
|
1193
|
+
.site-problem {
|
|
1194
|
+
margin: 6px 0 0;
|
|
1195
|
+
color: var(--low-time);
|
|
1196
|
+
font-size: 13px;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.site-problem.unreached {
|
|
1200
|
+
color: var(--soft);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.privacy {
|
|
1204
|
+
margin: 8px 0 0;
|
|
1205
|
+
color: var(--soft);
|
|
1206
|
+
font-size: 13px;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1135
1209
|
.dialog .new-profile {
|
|
1136
1210
|
border-color: var(--line);
|
|
1137
1211
|
color: var(--ink);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
-
// The profiles on the page (D-23, D-45): the wizard that completes the first (D-71), the player's picture to change, and the menu that puts another in use or makes one.
|
|
3
2
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
4
3
|
import { DrawnAvatar, PlayerPicture } from "../avatars/avatars.js";
|
|
5
4
|
import { Torretta } from "../avatars/torretta.js";
|
|
6
|
-
import { avatarIds, chooseAvatar,
|
|
5
|
+
import { avatarIds, chooseAvatar, editProfile, fideHighest, fideLowest, imageAddress, levels, makeProfile, nameLimit, openDataFolder, sendPhoto, useProfile, } from "../profile/profile.js";
|
|
7
6
|
import { Dialog } from "./dialog.js";
|
|
8
7
|
/**
|
|
9
8
|
* The page at the first start, while the profile Chesspal made is new: who plays, and the game comes after (D-23, D-71); a game played before stays in it.
|
|
@@ -12,30 +11,26 @@ import { Dialog } from "./dialog.js";
|
|
|
12
11
|
export function Wizard({ text, hasGame, account, dataFolder, onMade }) {
|
|
13
12
|
return (_jsxs("main", { class: "wizard", children: [_jsx(Torretta, { size: 56 }), _jsx("h1", { children: text.profile.welcome }), _jsxs("p", { class: "lead", children: [text.profile.lead, hasGame ? _jsx("span", { children: text.profile.gameMoves }) : null] }), _jsx(ProfileForm, { text: text, submit: text.profile.start, suggestion: account, onMade: onMade }), _jsx(DataFolder, { folders: dataFolder, text: text })] }));
|
|
14
13
|
}
|
|
15
|
-
/** The player's picture on their card: a click opens
|
|
14
|
+
/** The player's picture on their card: a click opens their profile; a mark on it says so where nothing hovers. */
|
|
16
15
|
export function PictureButton({ profile, onOpen, text }) {
|
|
17
|
-
return (_jsxs("button", { class: "me", type: "button", title: text.profile.
|
|
16
|
+
return (_jsxs("button", { class: "me", type: "button", title: text.profile.editProfile, "aria-label": text.profile.yourPicture, "aria-haspopup": "dialog", onClick: onOpen, children: [_jsx(PlayerPicture, { profile: profile }), _jsx("span", { class: "me-cover", children: pencilIcon }), _jsx("span", { class: "me-badge", children: pencilIcon })] }));
|
|
18
17
|
}
|
|
19
18
|
/** The player's name on their card: a click opens the menu of the players. */
|
|
20
19
|
export function NameButton({ profile, onOpen, text }) {
|
|
21
20
|
return (_jsxs("button", { class: "profile-name", type: "button", title: text.profile.changePlayer, "aria-label": `${profile.name}: ${text.profile.changePlayer}`, "aria-haspopup": "dialog", onClick: onOpen, children: [_jsx("span", { children: profile.name }), _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M7 10L12 15L17 10" }) })] }));
|
|
22
21
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onClose();
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
return (_jsxs(Dialog, { open: open, title: text.profile.changePicture, onClose: onClose, text: text, children: [_jsx("div", { class: "avatars", children: avatarIds.map((id) => (_jsx("button", { class: "avatar-choice", type: "button", title: text.profile.avatars[id], "aria-pressed": profile.image === undefined && profile.avatar === id, onClick: () => void chooseAvatar(profile.id, id).then(done), children: _jsx(DrawnAvatar, { id: id, size: 52, label: text.profile.avatars[id] }) }, id))) }), _jsx(PhotoChoice, { on: profile.image !== undefined, current: imageAddress(profile), onPhoto: (photo) => void sendPhoto(profile.id, photo).then(done), text: text }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }));
|
|
22
|
+
/**
|
|
23
|
+
* The profile of the player in use, open while `open`: their picture, a drawn avatar or a photo of theirs, their name, how they play and their names on
|
|
24
|
+
* Lichess and chess.com, each saved once changed (D-93, D-106).
|
|
25
|
+
*/
|
|
26
|
+
export function ProfileDialog({ open, profile, onClose, text }) {
|
|
27
|
+
return (_jsx(Dialog, { open: open, title: text.profile.yourProfile, onClose: onClose, text: text, children: _jsx(ProfileEdit, { profile: profile, text: text }, profile.id) }));
|
|
33
28
|
}
|
|
34
29
|
/**
|
|
35
|
-
* The menu of the players, open while `open`: another put in use, never while a game goes on, or a new one made, which waits for it to end;
|
|
36
|
-
*
|
|
30
|
+
* The menu of the players, open while `open`: another put in use, never while a game goes on, or a new one made, which waits for it to end; a pencil beside the
|
|
31
|
+
* one in use opens their profile.
|
|
37
32
|
*/
|
|
38
|
-
export function ProfilesDialog({ open, profiles, active, locked, dataFolder, onClose, text }) {
|
|
33
|
+
export function ProfilesDialog({ open, profiles, active, locked, dataFolder, onEdit, onClose, text }) {
|
|
39
34
|
const [making, setMaking] = useState(false);
|
|
40
35
|
const [problem, setProblem] = useState(undefined);
|
|
41
36
|
useEffect(() => {
|
|
@@ -49,31 +44,85 @@ export function ProfilesDialog({ open, profiles, active, locked, dataFolder, onC
|
|
|
49
44
|
}
|
|
50
45
|
void useProfile(id).then((refused) => (refused === undefined ? onClose() : setProblem(refusalText(refused, text))));
|
|
51
46
|
};
|
|
52
|
-
return (_jsx(Dialog, { open: open, title: making ? text.profile.newProfile : text.profile.profiles, onBack: making ? () => setMaking(false) : undefined, onClose: onClose, text: text, children: making ? _jsx(ProfileForm, { text: text, submit: text.profile.create, use: locked === undefined, onMade: onClose }) : (_jsxs(_Fragment, { children: [_jsx("p", { class: "note", children: text.profile.eachOwn }), _jsx("div", { class: "profile-list", role: "radiogroup", "aria-label": text.profile.profiles, children: profiles.map((profile) => (_jsxs("button", { class: "choice", type: "button", role: "radio", title: profile.name, "aria-checked": profile.id === active.id, disabled: locked !== undefined && profile.id !== active.id, onClick: () => use(profile.id), children: [_jsx(PlayerPicture, { profile: profile, size: 32 }), _jsx("span", { children: profile.name }), profile.id === active.id ? checkIcon : null] }, profile.id
|
|
47
|
+
return (_jsx(Dialog, { open: open, title: making ? text.profile.newProfile : text.profile.profiles, onBack: making ? () => setMaking(false) : undefined, onClose: onClose, text: text, children: making ? _jsx(ProfileForm, { text: text, submit: text.profile.create, use: locked === undefined, onMade: onClose }) : (_jsxs(_Fragment, { children: [_jsx("p", { class: "note", children: text.profile.eachOwn }), _jsx("div", { class: "profile-list", role: "radiogroup", "aria-label": text.profile.profiles, children: profiles.map((profile) => (_jsxs("div", { class: "profile-row", children: [_jsxs("button", { class: "choice", type: "button", role: "radio", title: profile.name, "aria-checked": profile.id === active.id, disabled: locked !== undefined && profile.id !== active.id, onClick: () => use(profile.id), children: [_jsx(PlayerPicture, { profile: profile, size: 32 }), _jsx("span", { children: profile.name }), profile.id === active.id ? checkIcon : null] }), profile.id === active.id
|
|
48
|
+
? (_jsx("button", { class: "icon-button edit-profile", type: "button", title: text.profile.editProfile, "aria-label": text.profile.editProfile, onClick: onEdit, children: pencilIcon }))
|
|
49
|
+
: null] }, profile.id))) }), locked === undefined ? null : _jsx("p", { class: "note", children: locked }), _jsx("button", { class: "command new-profile", type: "button", onClick: () => setMaking(true), children: text.profile.newProfile }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx(DataFolder, { folders: dataFolder, text: text })] })) }));
|
|
53
50
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
51
|
+
/**
|
|
52
|
+
* What the player changes in their profile, each part saved once changed: the picture, the name, how they play, and their names on Lichess and Chess.com,
|
|
53
|
+
* by which a game loaded from there is theirs (D-93). A field saved gets a tick; one the server would not take stays as typed, marked.
|
|
54
|
+
*/
|
|
55
|
+
function ProfileEdit({ profile, text }) {
|
|
56
56
|
const words = text.profile;
|
|
57
|
-
const [names, setNames] = useState({ lichess: profile.accounts?.lichess ?? "", chesscom: profile.accounts?.chesscom ?? "" });
|
|
58
57
|
const [saved, setSaved] = useState(undefined);
|
|
59
58
|
const [problem, setProblem] = useState(undefined);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const changed = { ...names, [site]: field.value.trim() };
|
|
67
|
-
setNames(changed);
|
|
59
|
+
const [fideWrong, setFideWrong] = useState(false);
|
|
60
|
+
const report = (refused) => {
|
|
61
|
+
setProblem(refused === undefined ? undefined : refusalText(refused, text));
|
|
62
|
+
return refused === undefined;
|
|
63
|
+
};
|
|
64
|
+
const save = (field, input) => {
|
|
68
65
|
setSaved(undefined);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
if (field === "fide") {
|
|
67
|
+
setFideWrong(!input.validity.valid);
|
|
68
|
+
}
|
|
69
|
+
if (input.validity.valid) {
|
|
70
|
+
const value = input.value.trim();
|
|
71
|
+
const change = field === "fide" ? { fide: value === "" ? "" : Number(value) } : { [field]: value };
|
|
72
|
+
void editProfile(profile.id, change).then((refused) => setSaved(report(refused) ? field : undefined));
|
|
73
|
+
}
|
|
73
74
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const tick = (field) => (saved === field ? checkIcon : noTick);
|
|
76
|
+
return (_jsxs("div", { class: "profile-form profile-edit", children: [_jsxs("fieldset", { children: [_jsx("legend", { children: words.avatar }), _jsx("div", { class: "avatars", children: avatarIds.map((id) => (_jsx("button", { class: "avatar-choice", type: "button", title: words.avatars[id], "aria-pressed": profile.image === undefined && profile.avatar === id, onClick: () => void chooseAvatar(profile.id, id).then(report), children: _jsx(DrawnAvatar, { id: id, size: 52, label: words.avatars[id] }) }, id))) }), _jsx(PhotoChoice, { on: profile.image !== undefined, current: imageAddress(profile), onPhoto: (photo) => void sendPhoto(profile.id, photo).then(report), text: text })] }), _jsxs("label", { class: "field", children: [_jsx("span", { children: words.name }), _jsx("input", { type: "text", required: true, maxLength: nameLimit,
|
|
77
|
+
// Not bound to the profile: a name the server would not take stays as typed, marked.
|
|
78
|
+
defaultValue: profile.name, autocomplete: "given-name", onChange: (event) => save("name", event.currentTarget) }), tick("name")] }), _jsxs("fieldset", { children: [_jsx("legend", { children: words.level }), _jsx("div", { class: "levels", children: levels.map((id) => (_jsxs("label", { class: "level", children: [_jsx("input", { type: "radio", name: "profile-level", checked: profile.level === id, onChange: () => void editProfile(profile.id, { level: id }).then(report) }), _jsxs("span", { children: [_jsx("b", { children: words.levels[id][0] }), _jsx("small", { children: words.levels[id][1] })] })] }, id))) })] }), _jsxs("fieldset", { children: [_jsxs("legend", { children: [words.accounts, " ", _jsx("small", { children: words.optional })] }), _jsxs("div", { class: "accounts read", children: [accountSites.map((site) => (_jsxs("label", { title: words.accountTitle, children: [_jsx("span", { children: siteNames[site] }), _jsx(SiteName, { initial: profile.accounts?.[site] ?? "", site: site, onChange: (input) => save(site, input), text: text }), _jsx(SiteMark, { reading: readingOf(profile, site), pending: profile.accounts?.[site] !== undefined, text: text }), tick(site)] }, site))), _jsxs("label", { title: words.fideTitle, children: [_jsx("span", { children: "FIDE" }), _jsx(FideRating, { initial: profile.fide, onChange: (input) => save("fide", input), text: text }), _jsx("span", { class: "site-mark" }), tick("fide")] })] }), accountSites.map((site) => _jsx(SiteProblem, { reading: readingOf(profile, site), site: site, text: text }, site)), fideWrong ? _jsx("p", { class: "site-problem", children: words.fideRange(fideLowest, fideHighest) }) : null, _jsx("p", { class: "privacy", children: words.privacy })] }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A field of the player's name on Lichess or Chess.com: in the wizard bound to its value, in the profile window not bound, so that a name the sites would
|
|
82
|
+
* not give stays as typed, marked, until it reads.
|
|
83
|
+
*/
|
|
84
|
+
function SiteName({ value, initial, site, onInput, onChange, text }) {
|
|
85
|
+
return (_jsx("input", { type: "text", ...(value === undefined ? { defaultValue: initial } : { value }), "aria-label": siteNames[site], placeholder: text.profile.username, maxLength: nameLimit, pattern: "\\s*[\\w\\-]*\\s*", autocomplete: "off", spellcheck: false, onInput: onInput === undefined ? undefined : (event) => onInput(event.currentTarget.value), onChange: onChange === undefined ? undefined : (event) => onChange(event.currentTarget) }));
|
|
86
|
+
}
|
|
87
|
+
/** The player's FIDE rating, typed: FIDE has no API (D-106). In the wizard bound to its value, in the profile window not bound, as a site's name. */
|
|
88
|
+
function FideRating({ value, initial, onInput, onChange, text }) {
|
|
89
|
+
return (_jsx("input", { type: "number", ...(value === undefined ? { defaultValue: initial === undefined ? "" : String(initial) } : { value }), "aria-label": "FIDE", placeholder: text.profile.fidePlaceholder, min: fideLowest, max: fideHighest, step: 1, inputMode: "numeric", autocomplete: "off", onInput: onInput === undefined ? undefined : (event) => onInput(event.currentTarget.value), onChange: onChange === undefined ? undefined : (event) => onChange(event.currentTarget) }));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Beside a name on a site, what the site said of it (D-105): the rating that counts, its speed and last game in its title; a cross when the site has no
|
|
93
|
+
* such player; when it did not answer, the rating it gave before, soft, or a dash; dots while a name is being read. The line under the names says the
|
|
94
|
+
* cross and the dash in words.
|
|
95
|
+
*/
|
|
96
|
+
function SiteMark({ reading, pending, text }) {
|
|
97
|
+
const words = text.profile;
|
|
98
|
+
const rating = reading?.rating;
|
|
99
|
+
const number = rating === undefined ? null : _jsx(RatingNumber, { value: rating.value, said: words.ratingTitle(words.speeds[rating.speed], rating.played) });
|
|
100
|
+
if (reading === undefined) {
|
|
101
|
+
return pending
|
|
102
|
+
? (_jsxs("span", { class: "site-mark pending", title: words.pending, children: [_jsx("span", { "aria-hidden": "true", children: "\u2026" }), _jsx("span", { class: "visually-hidden", children: words.pending })] }))
|
|
103
|
+
: _jsx("span", { class: "site-mark" });
|
|
104
|
+
}
|
|
105
|
+
if (reading.unreached === true) {
|
|
106
|
+
return _jsx("span", { class: "site-mark unreached", children: number ?? _jsx("span", { "aria-hidden": "true", children: "\u2013" }) });
|
|
107
|
+
}
|
|
108
|
+
return reading.found === false ? _jsx("span", { class: "site-mark missing", "aria-hidden": "true", children: "\u2715" }) : _jsx("span", { class: "site-mark", children: number });
|
|
109
|
+
}
|
|
110
|
+
/** A rating, its speed and last game in its title, and read aloud after it. */
|
|
111
|
+
function RatingNumber({ value, said }) {
|
|
112
|
+
return (_jsxs("span", { title: said, children: [value, _jsx("span", { class: "visually-hidden", children: ` ${said}` })] }));
|
|
113
|
+
}
|
|
114
|
+
/** Under the names, in words, a name a site does not know or a site that did not answer: a phone has no title to hover. */
|
|
115
|
+
function SiteProblem({ reading, site, text }) {
|
|
116
|
+
if (reading?.unreached === true) {
|
|
117
|
+
return _jsx("p", { class: "site-problem unreached", children: text.profile.unreached(siteNames[site]) });
|
|
118
|
+
}
|
|
119
|
+
return reading?.found === false ? _jsx("p", { class: "site-problem", children: text.profile.notFound(siteNames[site]) }) : null;
|
|
120
|
+
}
|
|
121
|
+
/** What the site said of the name the profile has there now: none for a name changed since it was read. */
|
|
122
|
+
function readingOf(profile, site) {
|
|
123
|
+
const reading = profile.sites?.[site];
|
|
124
|
+
const account = profile.accounts?.[site];
|
|
125
|
+
return reading !== undefined && account !== undefined && reading.account.toLowerCase() === account.toLowerCase() ? reading : undefined;
|
|
77
126
|
}
|
|
78
127
|
const accountSites = ["lichess", "chesscom"];
|
|
79
128
|
const siteNames = { lichess: "Lichess", chesscom: "Chess.com" };
|
|
@@ -107,15 +156,15 @@ function ProfileForm({ text, submit, suggestion, use = true, onMade }) {
|
|
|
107
156
|
const [avatar, setAvatar] = useState(avatarIds[0]);
|
|
108
157
|
const [photo, setPhoto] = useState(undefined);
|
|
109
158
|
const [level, setLevel] = useState("casual");
|
|
110
|
-
const [
|
|
111
|
-
const [
|
|
159
|
+
const [names, setNames] = useState({ lichess: "", chesscom: "" });
|
|
160
|
+
const [fide, setFide] = useState("");
|
|
112
161
|
const [problem, setProblem] = useState(undefined);
|
|
113
162
|
const [busy, setBusy] = useState(false);
|
|
114
163
|
const send = (event) => {
|
|
115
164
|
event.preventDefault();
|
|
116
165
|
setBusy(true);
|
|
117
|
-
const
|
|
118
|
-
void makeProfile({ name, level,
|
|
166
|
+
const rated = fide.trim() === "" ? {} : { fide: Number(fide) };
|
|
167
|
+
void makeProfile({ name, level, ...names, ...rated, ...(photo === undefined ? { avatar } : { photo }), use }).then((refused) => {
|
|
119
168
|
setBusy(false);
|
|
120
169
|
setProblem(refused === undefined ? undefined : refusalText(refused, text));
|
|
121
170
|
if (refused === undefined) {
|
|
@@ -126,7 +175,7 @@ function ProfileForm({ text, submit, suggestion, use = true, onMade }) {
|
|
|
126
175
|
return (_jsxs("form", { class: "profile-form", onSubmit: send, children: [_jsxs("label", { class: "field", children: [_jsx("span", { children: words.name }), _jsx("input", { type: "text", required: true, maxLength: nameLimit, placeholder: words.namePlaceholder, autocomplete: "given-name", list: suggestion === undefined ? undefined : "name-suggestion", value: name, onInput: (event) => setName(event.currentTarget.value) }), suggestion === undefined ? null : (_jsx("datalist", { id: "name-suggestion", children: _jsx("option", { value: suggestion }) }))] }), _jsxs("fieldset", { children: [_jsx("legend", { children: words.avatar }), _jsx("div", { class: "avatars", children: avatarIds.map((id) => (_jsxs("label", { class: "avatar-choice", title: words.avatars[id], children: [_jsx("input", { type: "radio", name: "avatar", checked: photo === undefined && avatar === id, onChange: () => {
|
|
127
176
|
setAvatar(id);
|
|
128
177
|
setPhoto(undefined);
|
|
129
|
-
} }), _jsx("span", { class: "visually-hidden", children: words.avatars[id] }), _jsx(DrawnAvatar, { id: id, size: 52 })] }, id))) }), _jsx(PhotoChoice, { on: photo !== undefined, photo: photo, onPhoto: setPhoto, text: text })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: words.level }), _jsx("div", { class: "levels", children: levels.map((id) => (_jsxs("label", { class: "level", children: [_jsx("input", { type: "radio", name: "level", checked: level === id, onChange: () => setLevel(id) }), _jsxs("span", { children: [_jsx("b", { children: words.levels[id][0] }), _jsx("small", { children: words.levels[id][1] })] })] }, id))) })] }), _jsxs("fieldset", { children: [_jsxs("legend", { children: [words.
|
|
178
|
+
} }), _jsx("span", { class: "visually-hidden", children: words.avatars[id] }), _jsx(DrawnAvatar, { id: id, size: 52 })] }, id))) }), _jsx(PhotoChoice, { on: photo !== undefined, photo: photo, onPhoto: setPhoto, text: text })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: words.level }), _jsx("div", { class: "levels", children: levels.map((id) => (_jsxs("label", { class: "level", children: [_jsx("input", { type: "radio", name: "level", checked: level === id, onChange: () => setLevel(id) }), _jsxs("span", { children: [_jsx("b", { children: words.levels[id][0] }), _jsx("small", { children: words.levels[id][1] })] })] }, id))) })] }), _jsxs("fieldset", { children: [_jsxs("legend", { children: [words.accounts, " ", _jsx("small", { children: words.optional })] }), _jsxs("div", { class: "accounts", children: [accountSites.map((site) => (_jsxs("label", { title: words.accountTitle, children: [_jsx("span", { children: siteNames[site] }), _jsx(SiteName, { value: names[site], site: site, onInput: (value) => setNames({ ...names, [site]: value }), text: text })] }, site))), _jsxs("label", { title: words.fideTitle, children: [_jsx("span", { children: "FIDE" }), _jsx(FideRating, { value: fide, onInput: setFide, text: text })] })] }), _jsx("p", { class: "privacy", children: words.privacy })] }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx("button", { class: "start", type: "submit", disabled: busy, children: submit })] }));
|
|
130
179
|
}
|
|
131
180
|
/** The choice of a photo of the player's, on a line of its own under the drawn avatars: the one chosen, or the one they have, shows in the ring. */
|
|
132
181
|
function PhotoChoice({ on, photo, current, onPhoto, text }) {
|
|
@@ -148,5 +197,9 @@ function refusalText(refused, text) {
|
|
|
148
197
|
}
|
|
149
198
|
// Zoe's camera, on the picture to change.
|
|
150
199
|
const cameraIcon = (_jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M4 8H7L9 5H15L17 8H20V19H4Z" }), _jsx("circle", { cx: "12", cy: "13", r: "3.5" })] }));
|
|
151
|
-
|
|
152
|
-
const checkIcon = (_jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12.5L10 17.5L19 7.5" }) }));
|
|
200
|
+
/** The tick on the choice in use: the profile, a name saved, the chat at the table. */
|
|
201
|
+
export const checkIcon = (_jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12.5L10 17.5L19 7.5" }) }));
|
|
202
|
+
// The room of the tick beside a field not saved yet.
|
|
203
|
+
const noTick = _jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true" });
|
|
204
|
+
// The pencil on the player's picture and beside them in the menu: their profile opens.
|
|
205
|
+
const pencilIcon = (_jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M4 20L5 15L15.5 4.5L19.5 8.5L9 19Z" }), _jsx("path", { d: "M13.5 6.5L17.5 10.5" })] }));
|
package/dist/page/game/column.js
CHANGED
|
@@ -14,7 +14,7 @@ export function GameColumn({ plies, result, how, lit, started, move, toMove, ope
|
|
|
14
14
|
const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
|
|
15
15
|
return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
|
|
16
16
|
? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
|
|
17
|
-
: (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), 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, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _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 })] })) }));
|
|
17
|
+
: (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), 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, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
|
|
18
18
|
}
|
|
19
19
|
/** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
|
|
20
20
|
function SaveButton({ label, hint, onSave }) {
|
|
@@ -34,6 +34,10 @@ function GameCard({ plies, result, how, lit, started, move, toMove, bar, text })
|
|
|
34
34
|
if (result === undefined) {
|
|
35
35
|
return (_jsxs("section", { class: "card game-card", "aria-label": text.game, children: [_jsx("span", { class: "tile moving", "aria-hidden": "true", children: _jsx(Piece, { letter: toMove === "white" ? "K" : "k" }) }), _jsxs("div", { class: "what", children: [_jsxs("p", { class: "head", children: [text.move, " ", _jsx("b", { class: "big", children: move }), _jsxs("span", { class: "visually-hidden", children: [", ", text.toMove(toMove)] })] }), when === undefined ? null : _jsx("p", { class: "when", children: text.started(when) }), bar] })] }));
|
|
36
36
|
}
|
|
37
|
+
// A game closed before any move: aborted, with no result to show.
|
|
38
|
+
if (plies.length === 0) {
|
|
39
|
+
return (_jsxs("section", { class: `card game-card${lit ? " lit" : ""}`, "aria-label": text.game, children: [_jsx(ResultTile, { result: "*" }), _jsxs("div", { class: "what", children: [_jsx("p", { class: "head", children: _jsx("b", { class: "big", children: text.aborted }) }), _jsx("p", { class: "when", children: text.lasted(0) })] })] }));
|
|
40
|
+
}
|
|
37
41
|
const winner = result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
|
|
38
42
|
const first = plies[0];
|
|
39
43
|
const last = plies.at(-1);
|
package/dist/page/game/plies.js
CHANGED
|
@@ -59,12 +59,15 @@ export function judgmentsOf(plies, player, { judgments }) {
|
|
|
59
59
|
/** How long a line of the player's may be: the server's limit (D-54), which a test keeps equal. */
|
|
60
60
|
export const talkLimits = { line: 200 };
|
|
61
61
|
/**
|
|
62
|
-
* The talk over a game in analysis (D-101): from the player's first line on it, the lines said since,
|
|
63
|
-
* which is all half the column holds (Zoe).
|
|
62
|
+
* The talk over a game in analysis (D-101): from the player's first line on it, the lines said since on it, a line without a position belonging to the game
|
|
63
|
+
* of the player's last line before it; the last ten, which is all half the column holds (Zoe).
|
|
64
64
|
*/
|
|
65
65
|
export function analysisTalk(talk, game) {
|
|
66
|
-
|
|
67
|
-
return
|
|
66
|
+
let on;
|
|
67
|
+
return talk.filter((line) => {
|
|
68
|
+
on = line.shown?.game ?? on;
|
|
69
|
+
return on === game;
|
|
70
|
+
}).slice(-10);
|
|
68
71
|
}
|
|
69
72
|
/** How many times the AI has spoken in the game, its comments on its moves and its lines on the page: the page sees it speak when the count grows. */
|
|
70
73
|
export function aiSpoke(plies, ai, talk) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The markdown the guide is written in, read into blocks: headings, paragraphs, lists, tables, quotes, code; its comments left out. markdown.tsx draws them.
|
|
2
|
+
// Copied from rpg-engine's guide (src/dashboard/help/blocks.ts), without the headings' icons and the cards' previews.
|
|
3
|
+
/** The blocks of a markdown text. */
|
|
4
|
+
export function markdownBlocks(text) {
|
|
5
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/<!--[\s\S]*?-->/g, "").split("\n");
|
|
6
|
+
const blocks = [];
|
|
7
|
+
let index = 0;
|
|
8
|
+
// The lines from here on that go together: each the next, while it is one.
|
|
9
|
+
const run = (belongs) => {
|
|
10
|
+
const taken = [];
|
|
11
|
+
for (let line = lines[index]; line !== undefined && belongs(line); line = lines[index]) {
|
|
12
|
+
taken.push(line);
|
|
13
|
+
index += 1;
|
|
14
|
+
}
|
|
15
|
+
return taken;
|
|
16
|
+
};
|
|
17
|
+
for (let line = lines[index]; line !== undefined; line = lines[index]) {
|
|
18
|
+
const heading = /^(#{1,6})\s+(.*?)\s*$/.exec(line);
|
|
19
|
+
if (line.trim() === "") {
|
|
20
|
+
index += 1;
|
|
21
|
+
}
|
|
22
|
+
else if (heading !== null) {
|
|
23
|
+
blocks.push({ kind: "heading", level: heading[1]?.length ?? 1, text: heading[2] ?? "" });
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
else if (line.startsWith("```")) {
|
|
27
|
+
index += 1;
|
|
28
|
+
const code = run((one) => !one.startsWith("```"));
|
|
29
|
+
index += 1;
|
|
30
|
+
blocks.push({ kind: "code", text: code.join("\n") });
|
|
31
|
+
}
|
|
32
|
+
else if (/^-{3,}\s*$/.test(line)) {
|
|
33
|
+
blocks.push({ kind: "rule" });
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
else if (line.startsWith("|")) {
|
|
37
|
+
const [head = [], , ...rows] = run((one) => one.startsWith("|")).map(cells);
|
|
38
|
+
blocks.push({ kind: "table", head, rows });
|
|
39
|
+
}
|
|
40
|
+
else if (line.startsWith(">")) {
|
|
41
|
+
blocks.push({ kind: "quote", blocks: markdownBlocks(run((one) => one.startsWith(">")).map((one) => one.replace(/^> ?/, "")).join("\n")) });
|
|
42
|
+
}
|
|
43
|
+
else if (listItem(line) !== undefined) {
|
|
44
|
+
const ordered = listItem(line)?.ordered ?? false;
|
|
45
|
+
const items = run((one) => one.trim() !== "" && (listItem(one)?.ordered === ordered || (listItem(one) === undefined && /^\s/.test(one))));
|
|
46
|
+
blocks.push({ kind: "list", ordered, items: items.reduce((all, one) => (listItem(one) === undefined ? [...all.slice(0, -1), `${all.at(-1) ?? ""} ${one.trim()}`] : [...all, listItem(one)?.text ?? ""]), []) });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const paragraph = run((one) => one.trim() !== "" && !/^(#{1,6}\s|```|>|\|)/.test(one) && listItem(one) === undefined);
|
|
50
|
+
blocks.push({ kind: "paragraph", text: paragraph.map((one) => one.trim()).join(" ") });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
/** A heading's anchor as GitHub makes it: How it works, how-it-works. */
|
|
56
|
+
export function anchorOf(heading) {
|
|
57
|
+
return heading.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
|
|
58
|
+
}
|
|
59
|
+
/** A list item's mark and text: - text, or 1. text; undefined for another line. */
|
|
60
|
+
function listItem(line) {
|
|
61
|
+
const item = /^([-*]|\d+\.)\s+(.*)$/.exec(line);
|
|
62
|
+
return item === null ? undefined : { ordered: /\d/.test(item[1] ?? ""), text: item[2] ?? "" };
|
|
63
|
+
}
|
|
64
|
+
/** A table row's cells: | a | b |, a and b. */
|
|
65
|
+
function cells(line) {
|
|
66
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Chesspal: the guide
|
|
2
|
+
|
|
3
|
+
With **Chesspal** you play chess against an **AI**, on a real board in your browser, and learn with it. This guide is for players: you do not need to know how to program.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
| Who | What they do |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **You** | You move on the board and talk with the AI. |
|
|
10
|
+
| **The AI** | Plays, reasons, judges the moves and answers you, in its chat. |
|
|
11
|
+
| **Chesspal** | Keeps the rules, the board, the clock and your games, and hands the AI the facts of the position. |
|
|
12
|
+
|
|
13
|
+
There are two windows: this **page**, with the board, and the AI's **chat**, in a window of its own. The AI plays from there, and Chesspal connects it to the page.
|
|
14
|
+
|
|
15
|
+
No chess engine plays for the AI: every move and every judgment is its own. Chesspal checks that each move is legal and tells the AI what is on the board: the pieces attacked and defended, the pins, the opening's name.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
1. Install **Node.js** from [nodejs.org](https://nodejs.org), the recommended (LTS) version, and an AI with your own subscription: **Claude Code** or **Codex**.
|
|
20
|
+
2. Open a terminal, the window where you type commands (on Windows, Command Prompt: press Start, type `cmd`, press Enter; on macOS, Terminal), and type `npm install -g chesspal`, then `chesspal`: this page opens. The first time it offers an icon on your desktop, to open it with a double click.
|
|
21
|
+
3. Press **Claude Code** or **Codex**: the AI's chat opens in a window of its own. Keep it open while you play.
|
|
22
|
+
4. Press **New game**, choose your colour, the clock and the AI's level, and press **Start**.
|
|
23
|
+
|
|
24
|
+
## If you are lost
|
|
25
|
+
|
|
26
|
+
- **The AI does not answer**: look at its window. If it is asking you something, answer there; if you closed it, press **Claude Code** or **Codex** again.
|
|
27
|
+
- **The page does not open**: double-click Chesspal's icon, or open it as in the [Quick start](#quick-start).
|
|
28
|
+
- **Your games** are in a folder on your computer: **Who's playing?**, from your picture at the top right, shows where, and **Open the folder** opens it.
|
|
29
|
+
|
|
30
|
+
## The AI's chat
|
|
31
|
+
|
|
32
|
+
- The page opens the AI's **chat** with its buttons, in a window of its own: there the AI thinks and writes. Beside the buttons you choose its model (the version of the AI) and how much it reasons: the more it reasons, the slower it plays.
|
|
33
|
+
- 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.
|
|
34
|
+
- 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.
|
|
35
|
+
- At each **new game**, the chat the page opened starts again clean by itself, once the AI has stopped thinking: a long chat makes the AI slower.
|
|
36
|
+
|
|
37
|
+
## The game
|
|
38
|
+
|
|
39
|
+
- **Move** by dragging a piece, or by clicking the piece and then its square. The dots show the legal moves; when a move is not allowed, the page says why.
|
|
40
|
+
- **The clock**, if you chose one, starts with **Start**. The AI has its own and knows how much time it has left.
|
|
41
|
+
- **Offer a draw** on your turn, once each side has moved; **Resign** ends the game.
|
|
42
|
+
- **The helps**: with "The AI teaches as you play", in the settings, the **Helps** card shows the pieces in danger, the pins and the pawns, and the AI, as your coach, gives you an idea. The AI draws coloured arrows on the board: the **?** beside the moves explains every mark.
|
|
43
|
+
- **Flip** turns the board round; **PGN** saves the moves in the file every chess site reads, to open on Lichess or Chess.com.
|
|
44
|
+
|
|
45
|
+
## Review and analysis
|
|
46
|
+
|
|
47
|
+
- When the game is over, press **Analyse the game**: the AI goes over it and puts its judgments and notes on the moves. Read them by stepping through the moves with the arrows.
|
|
48
|
+
- **Analyses**, in the header, opens your analyses, and loads a game played elsewhere: paste its PGN, or choose a .pgn file.
|
|
49
|
+
- Ask the AI about the game in the box under its name.
|
|
50
|
+
- **Annotated PGN** saves the game with its comments.
|
|
51
|
+
|
|
52
|
+
## Players and profile
|
|
53
|
+
|
|
54
|
+
- **Who's playing?**: your picture at the top right chooses a player or adds one. Each has their own games and settings.
|
|
55
|
+
- **Your profile**: a click on your picture beside the board, or the pencil in **Who's playing?**, changes your picture, your name, how you play, your names on Lichess and Chess.com and your FIDE rating.
|
|
56
|
+
- **Your rating**: from your names there, Chesspal reads your public rating, the one of your rapid games if you play them, and tells the AI. No passwords: anyone can see that rating on the site. Without a name there, the AI knows how you play from **How do you play?**
|
|
57
|
+
- **Your FIDE rating**, if you play tournaments, you type yourself: no site gives it to Chesspal. The AI reads it beside the site's, each on its own.
|
|
58
|
+
- If you load a game played on Lichess or Chess.com, Chesspal knows which colour you played.
|
|
59
|
+
|
|
60
|
+
## Questions and problems
|
|
61
|
+
|
|
62
|
+
- **The AI is slow**: it really thinks over each move. Choose a faster model, or less reasoning, beside the chat's buttons.
|
|
63
|
+
- **What it costs**: Chesspal is free; the AI uses your own subscription, and you pay nothing more.
|
|
64
|
+
- **Can the AI play an illegal move?** No: Chesspal checks every move. Its judgments are its own, though, and an AI can be wrong, like a person.
|
|
65
|
+
- **A backup**: copy the folder of your games, with the chat and the page closed.
|
|
66
|
+
- **Something does not work?** Tell us on [GitHub](https://github.com/stefanocaronia/chesspal/issues): what you did, what you expected, what happened. GitHub is the site where Chesspal is made: you write there with a free account, and anyone can read it.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { Inline } from "./markdown.js";
|
|
3
|
+
/** Where Chesspal lives, and how to support it. */
|
|
4
|
+
export const footLinks = {
|
|
5
|
+
author: "https://stefanocaronia.it",
|
|
6
|
+
github: "https://github.com/stefanocaronia/chesspal",
|
|
7
|
+
npm: "https://www.npmjs.com/package/chesspal",
|
|
8
|
+
support: "https://www.paypal.com/paypalme/stefanocaronia",
|
|
9
|
+
};
|
|
10
|
+
/** The foot of the guide. */
|
|
11
|
+
export function GuideFoot({ text }) {
|
|
12
|
+
const words = text.guide;
|
|
13
|
+
return (_jsxs("footer", { class: "guide-foot", children: [_jsxs("p", { class: "foot-own", children: [_jsxs("span", { children: ["© 2026 ", _jsx("a", { href: footLinks.author, target: "_blank", rel: "noreferrer", children: "Stefano Caronia" }), " · Chesspal"] }), _jsx("a", { href: footLinks.github, target: "_blank", rel: "noreferrer", title: "GitHub", "aria-label": words.onGitHub, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: githubPath }) }) }), _jsx("a", { href: footLinks.npm, target: "_blank", rel: "noreferrer", title: "npm", "aria-label": words.onNpm, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: npmPath }) }) }), _jsxs("a", { class: "foot-support", href: footLinks.support, target: "_blank", rel: "noreferrer", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: heartPath }) }), words.support] })] }), _jsxs("details", { class: "credits", children: [_jsx("summary", { children: words.credits }), _jsx("ul", { children: words.creditLines.map((line) => (_jsx("li", { children: _jsx(Inline, { text: line, go: () => undefined }) }, line))) })] })] }));
|
|
14
|
+
}
|
|
15
|
+
// The icons of rpg-engine's foot.
|
|
16
|
+
const githubPath = "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4 M9 18c-4.51 2-5-2-7-2";
|
|
17
|
+
const npmPath = "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73Z M12 22V12 M3.29 7 12 12l8.71-5 M7.5 4.27l9 5.15";
|
|
18
|
+
const heartPath = "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z";
|