chesspal 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli/cli.js +17 -4
- package/dist/cli/command-line.js +22 -12
- package/dist/cli/icon.d.ts +2 -0
- package/dist/cli/icon.js +11 -1
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.d.ts +2 -2
- package/dist/commands/analysis/analyse.js +10 -16
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +17 -7
- package/dist/commands/answers/agents.js +46 -16
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +17 -9
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +7 -3
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/answers/view.d.ts +1 -1
- package/dist/commands/answers/view.js +2 -2
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/look.d.ts +7 -3
- package/dist/commands/game/look.js +9 -4
- package/dist/commands/game/new.js +42 -12
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +7 -4
- package/dist/commands/game/position.d.ts +2 -1
- package/dist/commands/game/position.js +9 -4
- package/dist/commands/game/status.d.ts +4 -2
- package/dist/commands/game/status.js +24 -14
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +7 -2
- package/dist/commands/profiles/past-games.js +45 -18
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +7 -6
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/analysis.d.ts +7 -4
- package/dist/games/analysis.js +5 -3
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +3 -2
- package/dist/games/index.js +3 -2
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/games/thoughts.d.ts +12 -4
- package/dist/games/thoughts.js +27 -10
- package/dist/home/index.d.ts +1 -0
- package/dist/home/index.js +1 -0
- package/dist/home/same-path.d.ts +7 -0
- package/dist/home/same-path.js +24 -0
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +21 -7
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/anchor.js +15 -11
- package/dist/page/app/app.js +49 -14
- package/dist/page/app/served.js +30 -38
- package/dist/page/app/watch.js +97 -0
- package/dist/page/avatars/torretta.css +108 -0
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +11 -8
- package/dist/page/board/marks.js +2 -2
- package/dist/page/board/pointer.js +2 -2
- package/dist/page/dialogs/analyses.js +5 -3
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/dialogs/rows.js +72 -0
- package/dist/page/dialogs/site-games.js +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +137 -16
- package/dist/page/game/move-list.js +7 -5
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/helps/helps.css +5 -5
- package/dist/page/helps/helps.js +3 -181
- package/dist/page/helps/rows.js +16 -0
- package/dist/page/helps/strategy.js +105 -0
- package/dist/page/helps/tactics.js +69 -0
- package/dist/page/layout/header.js +49 -6
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +49 -17
- package/dist/page/layout/window.js +45 -6
- package/dist/page/lead/lead.js +41 -4
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/sounds/sounds.js +1 -1
- package/dist/page/style.css +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -118
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +34 -8
- package/dist/page/theme.css +13 -0
- package/dist/profiles/profiles.js +5 -1
- package/dist/server/bell/bell.d.ts +24 -29
- package/dist/server/bell/bell.js +79 -133
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/chat.d.ts +5 -1
- package/dist/server/page/chat.js +5 -1
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +95 -15
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +40 -5
- package/dist/server/page/table.js +65 -25
- package/dist/server/page/update.d.ts +1 -1
- package/dist/server/page/update.js +4 -4
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/chats.d.ts +5 -0
- package/dist/server/table/hosts/chats.js +11 -4
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +7 -3
- package/dist/server/table/hosts/play.js +63 -9
- package/dist/server/table/icon/index.d.ts +2 -0
- package/dist/server/table/icon/index.js +16 -2
- package/dist/server/table/index.d.ts +2 -2
- package/dist/server/table/index.js +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +4 -1
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +40 -12
- package/dist/sites/index.d.ts +1 -1
- package/dist/sites/index.js +1 -1
- package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
- package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
- package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
- package/dist/sites/{choice.js → ratings/choice.js} +1 -1
- package/dist/sites/ratings/index.d.ts +1 -0
- package/dist/sites/ratings/index.js +2 -0
- package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
- package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
- package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
- package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
- package/instructions/opponent.md +1 -1
- package/instructions/start.md +1 -1
- package/package.json +1 -1
|
@@ -1,52 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "preact/hooks";
|
|
3
3
|
import { Dialog } from "../dialogs/dialog.js";
|
|
4
4
|
import { checkIcon } from "../dialogs/profiles.js";
|
|
5
|
+
import { canAct, chatAction, choiceKeys, chosen, firstHost, offeredHosts, startedWith } from "./action.js";
|
|
5
6
|
/** The chat's window: closed once a chat opens or starts again, the page then saying where it stands; `review` when it opened to have a game analysed. */
|
|
6
7
|
export function ChatDialog({ open, review = false, table, settings, onChange, name, onClose, text }) {
|
|
7
|
-
|
|
8
|
-
const [failed, setFailed] = useState(false);
|
|
9
|
-
useEffect(() => setFailed(false), [open]);
|
|
10
|
-
const words = text.table;
|
|
11
|
-
const installed = table.hosts.filter((host) => host.installed && host.name in choiceKeys);
|
|
12
|
-
// A chat opened by hand is the player's: Chesspal cannot start it again. Its rows stay readable, as the ones waiting.
|
|
13
|
-
const byHand = table.chat && !table.held;
|
|
14
|
-
const busy = sending || table.opening !== undefined || table.waiting === true;
|
|
15
|
-
const choose = async (host) => {
|
|
16
|
-
setSending(true);
|
|
17
|
-
setFailed(false);
|
|
18
|
-
const done = table.chat ? await restartChat(host) : await openChat(host);
|
|
19
|
-
setSending(false);
|
|
20
|
-
if (done) {
|
|
21
|
-
onClose();
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
setFailed(true);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
// While a chat opens or waits to start again, the rows wait too, and the note says why.
|
|
28
|
-
const chooseNote = review ? words.reviewChoose : words.choose;
|
|
29
|
-
const note = installed.length === 0
|
|
30
|
-
? words.install(name)
|
|
31
|
-
: busy
|
|
32
|
-
? chatState(table, name, text) ?? chooseNote
|
|
33
|
-
: byHand
|
|
34
|
-
? words.byHand(name)
|
|
35
|
-
: table.chat
|
|
36
|
-
? words.restartHint
|
|
37
|
-
: chooseNote;
|
|
38
|
-
const offersChoices = installed.some((host) => host.models.length > 0 || host.efforts.length > 0);
|
|
39
|
-
// The chat at the table started with other choices than the ones kept now: its row starts it again with these, else it only closes the window.
|
|
40
|
-
const started = table.held ? table.started : undefined;
|
|
41
|
-
const changed = (host) => started !== undefined && (chosen(host, "model", settings) !== (started.model ?? "") || chosen(host, "effort", settings) !== (started.effort ?? ""));
|
|
42
|
-
return (_jsxs(Dialog, { open: open, title: review ? words.reviewTitle : words.title, onClose: onClose, text: text, children: [_jsx("p", { class: "note", children: note }), _jsxs("div", { class: "chat-hosts", role: "group", "aria-label": review ? words.reviewTitle : words.title, children: [offersChoices
|
|
43
|
-
? (_jsxs("div", { class: "chat-head", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: words.model }), _jsx("span", { children: words.effort })] }))
|
|
44
|
-
: null, installed.map((host) => {
|
|
45
|
-
const atTable = table.chat && table.host === host.name;
|
|
46
|
-
const off = busy || byHand;
|
|
47
|
-
const again = !atTable || changed(host);
|
|
48
|
-
return (_jsxs("div", { class: "chat-host", children: [_jsxs("button", { class: "choice", type: "button", title: atTable && again ? words.restartChanged : undefined, "aria-current": atTable ? "true" : undefined, "aria-disabled": off, onClick: off ? undefined : again ? () => void choose(host.name) : onClose, children: [_jsx("span", { children: hostTitles[host.name] ?? host.name }), atTable ? (again ? restartIcon : checkIcon) : null] }), _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));
|
|
49
|
-
})] }), _jsx("p", { class: "chat-state", role: "status", children: failed ? (table.chat ? words.notRestarted : words.failed) : null }), table.chat || installed.length === 0 ? null : (_jsxs("p", { class: "chat-command", children: [words.typeIt, " ", installed.map((host, index) => (_jsxs("span", { children: [index === 0 ? "" : " · ", _jsx("code", { children: host.command })] }, host.name)))] }))] }));
|
|
8
|
+
return (_jsx(Dialog, { open: open, title: review ? text.table.reviewTitle : text.table.title, onClose: onClose, text: text, children: _jsx(ChatForm, { review: review, table: table, settings: settings, onChange: onChange, name: name, onClose: onClose, text: text }) }));
|
|
50
9
|
}
|
|
51
10
|
/** The AI's name on its card, which opens the chat's window: with no chat at the table, a hollow ring before it says so. */
|
|
52
11
|
export function AiNameButton({ name, table, onOpen, text }) {
|
|
@@ -54,14 +13,17 @@ export function AiNameButton({ name, table, onOpen, text }) {
|
|
|
54
13
|
return (_jsxs("button", { class: "profile-name ai-name", type: "button", title: title, "aria-label": `${name}: ${title}`, "aria-haspopup": "dialog", onClick: onOpen, children: [table.chat ? null : _jsx("span", { class: "no-chat", "aria-hidden": "true" }), _jsx("span", { children: name }), _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M7 10L12 15L17 10" }) })] }));
|
|
55
14
|
}
|
|
56
15
|
/**
|
|
57
|
-
* The note on the chat at the table where the asleep note stands: one opening or starting again; one waiting to,
|
|
58
|
-
* that lets the request go; in a game with no chat at the table, a line that opens the chat's window. None otherwise.
|
|
16
|
+
* The note on the chat at the table where the asleep note stands: one opening or starting again; one waiting to, or a new game waiting for it, with a ↻
|
|
17
|
+
* that starts it at once and a × that lets the request go; in a game with no chat at the table, a line that opens the chat's window. None otherwise.
|
|
59
18
|
*/
|
|
60
19
|
export function tableNote({ table, name, inGame, onOpen, text }) {
|
|
61
20
|
const words = text.table;
|
|
62
21
|
const state = chatState(table, name, text);
|
|
63
|
-
if (table.waiting
|
|
64
|
-
|
|
22
|
+
if (table.waiting !== undefined) {
|
|
23
|
+
const newGame = table.waiting === "new-game";
|
|
24
|
+
const now = newGame ? words.newGameNow : words.restartNow;
|
|
25
|
+
const cancel = newGame ? words.newGameCancel : words.cancel;
|
|
26
|
+
return (_jsxs("p", { class: "asleep table-note", role: "status", children: [_jsx("span", { children: state }), _jsx("button", { class: "icon-button", type: "button", title: now, "aria-label": now, onClick: () => void endWaiting(true), children: restartIcon }), _jsx("button", { class: "icon-button", type: "button", title: cancel, "aria-label": cancel, onClick: () => void endWaiting(false), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }));
|
|
65
27
|
}
|
|
66
28
|
if (state !== undefined) {
|
|
67
29
|
return _jsx("p", { class: "asleep", role: "status", children: state });
|
|
@@ -71,38 +33,76 @@ export function tableNote({ table, name, inGame, onOpen, text }) {
|
|
|
71
33
|
}
|
|
72
34
|
return null;
|
|
73
35
|
}
|
|
74
|
-
/** What the page says of the chat at the table: one opening, starting again, or waiting to; nothing else. */
|
|
36
|
+
/** What the page says of the chat at the table: one opening, starting again, or waiting to, for a new game too; nothing else. */
|
|
75
37
|
export function chatState(table, name, text) {
|
|
76
38
|
const words = text.table;
|
|
77
|
-
if (table.waiting
|
|
78
|
-
return words.waiting(name);
|
|
39
|
+
if (table.waiting !== undefined) {
|
|
40
|
+
return table.waiting === "new-game" ? words.newGameWaiting(name) : words.waiting(name);
|
|
79
41
|
}
|
|
80
42
|
return table.opening === "chat" ? words.opening : table.opening === "again" ? words.again(name) : table.opening === "restart" ? words.restart : undefined;
|
|
81
43
|
}
|
|
82
44
|
const hostTitles = { claude: "Claude Code", codex: "Codex" };
|
|
83
|
-
// The settings that keep each host's choice.
|
|
84
|
-
const choiceKeys = {
|
|
85
|
-
claude: { model: "claudeModel", effort: "claudeEffort" },
|
|
86
|
-
codex: { model: "codexModel", effort: "codexEffort" },
|
|
87
|
-
};
|
|
88
45
|
/** The ↻ of a chat that starts again. */
|
|
89
46
|
const restartIcon = (_jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4" }) }));
|
|
90
|
-
/**
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
47
|
+
/** The window's form, made again at each opening with the host at the table selected, else the last the button used, else the first. */
|
|
48
|
+
function ChatForm({ review, table, settings, onChange, name, onClose, text }) {
|
|
49
|
+
const words = text.table;
|
|
50
|
+
const installed = offeredHosts(table);
|
|
51
|
+
const [selected, setSelected] = useState(() => firstHost(table, settings));
|
|
52
|
+
const [sending, setSending] = useState(false);
|
|
53
|
+
const [failed, setFailed] = useState(false);
|
|
54
|
+
// The host at the table may come or go while the window is open: the selection follows it only if it was on none offered.
|
|
55
|
+
const host = installed.find((one) => one.name === selected) ?? installed.find((one) => one.name === firstHost(table, settings));
|
|
56
|
+
useEffect(() => setFailed(false), [selected]);
|
|
57
|
+
const action = chatAction(table, host, review);
|
|
58
|
+
// A chat opened by hand is the player's: Chesspal cannot start it again, so the button keeps its word, disabled, and the note says why.
|
|
59
|
+
const byHand = table.chat && !table.held;
|
|
60
|
+
const busy = table.opening !== undefined || table.waiting !== undefined;
|
|
61
|
+
const chooseNote = review ? words.reviewChoose : words.choose;
|
|
62
|
+
const note = installed.length === 0 ? words.install(name) : busy ? chatState(table, name, text) ?? chooseNote : byHand ? words.byHand(name) : table.chat ? words.restartHint : chooseNote;
|
|
63
|
+
// What the chat held started with, marked among the choices of its host.
|
|
64
|
+
const started = (kind) => (host !== undefined && table.held && table.host === host.name ? startedWith(table, host, kind) : undefined);
|
|
65
|
+
const submit = async (event) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
if (action === undefined || host === undefined || !canAct(table)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
setSending(true);
|
|
71
|
+
setFailed(false);
|
|
72
|
+
const done = table.chat ? await restartChat(host.name) : await openChat(host.name);
|
|
73
|
+
setSending(false);
|
|
74
|
+
if (done) {
|
|
75
|
+
onChange({ ...settings, chatHost: host.name });
|
|
76
|
+
onClose();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
setFailed(true);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return (_jsxs("form", { class: "profile-form chat-window", onSubmit: (event) => void submit(event), onKeyDown: submitOnEnter, children: [_jsx("p", { class: "note", children: note }), installed.length === 0 ? null : (_jsxs("fieldset", { children: [_jsx("legend", { children: words.chat }), _jsx("div", { class: "chat-hosts", children: installed.map((one) => {
|
|
83
|
+
const atTable = table.chat && table.host === one.name;
|
|
84
|
+
return (_jsxs("label", { class: "level", children: [_jsx("input", { type: "radio", name: "chat-host", checked: one.name === host?.name, onChange: () => setSelected(one.name), autofocus: one.name === host?.name }), _jsxs("span", { children: [_jsx("b", { children: hostTitles[one.name] ?? one.name }), atTable ? _jsx("span", { class: "at-table", title: words.atTable, role: "img", "aria-label": words.atTable, children: checkIcon }) : null] })] }, one.name));
|
|
85
|
+
}) })] })), host === undefined ? null : (_jsxs(_Fragment, { children: [_jsx(Choices, { host: host, kind: "model", settings: settings, started: started("model"), onChange: onChange, text: text }), _jsx(Choices, { host: host, kind: "effort", settings: settings, started: started("effort"), onChange: onChange, text: text })] })), _jsx("p", { class: "problem chat-state", role: "status", children: failed ? (table.chat ? words.notRestarted : words.failed) : null }), action === undefined || host === undefined ? null : (_jsx("button", { class: "start", type: "submit", disabled: sending || !canAct(table), "aria-busy": sending, children: text.table.act[action](hostTitles[host.name] ?? host.name) })), table.chat || host === undefined ? null : (_jsxs("p", { class: "chat-command", children: [words.typeIt, " ", _jsx("code", { children: host.command })] }))] }));
|
|
86
|
+
}
|
|
87
|
+
/** Enter on a choice presses the button, the window's default action, as it would in a text field: the browser does not for radios. */
|
|
88
|
+
function submitOnEnter(event) {
|
|
89
|
+
if (event.key === "Enter" && event.target instanceof HTMLInputElement) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
event.currentTarget.requestSubmit();
|
|
92
|
+
}
|
|
95
93
|
}
|
|
96
|
-
/** A host's model or effort
|
|
97
|
-
function
|
|
94
|
+
/** A host's model or effort as a row of choices, its middle one chosen until the player chooses; the one the chat at the table `started` with bears a dot. */
|
|
95
|
+
function Choices({ host, kind, settings, started, onChange, text }) {
|
|
98
96
|
const key = choiceKeys[host.name][kind];
|
|
99
97
|
const offered = kind === "model" ? host.models : host.efforts;
|
|
100
98
|
const value = chosen(host, kind, settings);
|
|
101
|
-
const title = hostTitles[host.name] ?? host.name;
|
|
102
99
|
if (offered.length === 0) {
|
|
103
|
-
return
|
|
100
|
+
return null;
|
|
104
101
|
}
|
|
105
|
-
return (_jsxs("
|
|
102
|
+
return (_jsxs("fieldset", { children: [_jsx("legend", { children: text.table[kind] }), _jsx("div", { class: "chat-choices", children: offered.map((one) => {
|
|
103
|
+
const words = one + (one === started ? ` · ${text.table.atTable}` : "");
|
|
104
|
+
return (_jsxs("label", { class: one === started ? "level started" : "level", title: words, children: [_jsx("input", { type: "radio", name: `chat-${kind}`, checked: value === one, onChange: () => onChange({ ...settings, [key]: one }), "aria-label": words }), _jsx("span", { children: shortName(one) })] }, one));
|
|
105
|
+
}) })] }));
|
|
106
106
|
}
|
|
107
107
|
// Codex's models without the "gpt-" they all start with, so the choice stays narrow; the title keeps the whole name.
|
|
108
108
|
function shortName(value) {
|
|
@@ -112,7 +112,7 @@ function shortName(value) {
|
|
|
112
112
|
function openChat(host) {
|
|
113
113
|
return asked("/api/table/chat", { host }, (answer) => answer.opened === true);
|
|
114
114
|
}
|
|
115
|
-
/** Asks the table to start at once the chat
|
|
115
|
+
/** Asks the table to start at once the chat or the new game that waits, cutting the chat's answer, or to let the request go. */
|
|
116
116
|
function endWaiting(now) {
|
|
117
117
|
return asked("/api/table/waiting", { now }, () => true);
|
|
118
118
|
}
|
package/dist/page/talk/talk.css
CHANGED
|
@@ -266,115 +266,6 @@
|
|
|
266
266
|
color: var(--accent);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/* The Torretta, whose eyes blink now and then, and its moods (D-70): a new face that pops in, never fading out, a red light when worried, out when sad, a light that breathes when
|
|
270
|
-
thoughtful */
|
|
271
|
-
.torretta .antenna {
|
|
272
|
-
fill: none;
|
|
273
|
-
stroke: var(--torretta-shade);
|
|
274
|
-
stroke-width: 4;
|
|
275
|
-
stroke-linecap: round;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.torretta .light {
|
|
279
|
-
fill: var(--torretta-light);
|
|
280
|
-
transition: fill 0.2s;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.torretta.mood-worried .light {
|
|
284
|
-
fill: var(--torretta-alarm);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.torretta.mood-thoughtful .light {
|
|
288
|
-
animation: ponder 2.4s ease-in-out infinite;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.torretta.mood-sad .light {
|
|
292
|
-
fill: var(--torretta-shade);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
@keyframes ponder {
|
|
296
|
-
50% {
|
|
297
|
-
fill: var(--torretta-ponder);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
.torretta .expression.changed {
|
|
302
|
-
transform-box: fill-box;
|
|
303
|
-
transform-origin: center;
|
|
304
|
-
animation: face-in 0.16s ease-out;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
@keyframes face-in {
|
|
308
|
-
from {
|
|
309
|
-
transform: scale(0.8);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/* Its bust on a player's card: a disc of the player's picture's colour, which cuts what falls out of it */
|
|
314
|
-
.torretta.bust {
|
|
315
|
-
clip-path: circle(50%);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.torretta .back {
|
|
319
|
-
fill: var(--say);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.torretta .shade {
|
|
323
|
-
fill: var(--torretta-shade);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.torretta .body {
|
|
327
|
-
fill: var(--torretta-body);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.torretta .screen {
|
|
331
|
-
fill: var(--torretta-screen);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.torretta .eyes,
|
|
335
|
-
.torretta .mouth {
|
|
336
|
-
fill: var(--torretta-face);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
.torretta .expression .line,
|
|
340
|
-
.torretta .brows {
|
|
341
|
-
fill: none;
|
|
342
|
-
stroke: var(--torretta-face);
|
|
343
|
-
stroke-width: 3.4;
|
|
344
|
-
stroke-linecap: round;
|
|
345
|
-
stroke-linejoin: round;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.torretta .eyes {
|
|
349
|
-
transform-box: fill-box;
|
|
350
|
-
transform-origin: center;
|
|
351
|
-
animation: blink 6s infinite;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
@keyframes blink {
|
|
355
|
-
0%,
|
|
356
|
-
95%,
|
|
357
|
-
100% {
|
|
358
|
-
transform: scaleY(1);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
97% {
|
|
362
|
-
transform: scaleY(0.1);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
@media (prefers-reduced-motion: reduce) {
|
|
367
|
-
.torretta .eyes,
|
|
368
|
-
.torretta .expression.changed,
|
|
369
|
-
.torretta.mood-thoughtful .light {
|
|
370
|
-
animation: none;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.torretta .light {
|
|
374
|
-
transition: none;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
269
|
/* The bubbles: the AI's under its card, the tail under the Torretta's centre (14 px of padding, 48 px picture), and the player's above their box */
|
|
379
270
|
.says,
|
|
380
271
|
.sent {
|
|
@@ -403,6 +294,14 @@
|
|
|
403
294
|
flex: none;
|
|
404
295
|
}
|
|
405
296
|
|
|
297
|
+
/* The newest, longer than its room, scrolls under the wheel or the finger with no scrollbar, fading where it is cut */
|
|
298
|
+
.says > li.scrolls,
|
|
299
|
+
.sent > li.scrolls {
|
|
300
|
+
overflow-y: auto;
|
|
301
|
+
scrollbar-width: none;
|
|
302
|
+
mask-image: linear-gradient(#000 calc(100% - 20px), transparent);
|
|
303
|
+
}
|
|
304
|
+
|
|
406
305
|
.say {
|
|
407
306
|
position: relative;
|
|
408
307
|
background: var(--say);
|
|
@@ -415,6 +314,42 @@
|
|
|
415
314
|
overflow-wrap: anywhere;
|
|
416
315
|
}
|
|
417
316
|
|
|
317
|
+
/* The mark that skips the bubble read aloud, in its corner; the corner is kept while the voice is on, so the words never move when it comes and goes (Zoe) */
|
|
318
|
+
.skip-reading {
|
|
319
|
+
float: right;
|
|
320
|
+
width: 22px;
|
|
321
|
+
height: 20px;
|
|
322
|
+
margin: 0 -7px 0 6px;
|
|
323
|
+
padding: 0;
|
|
324
|
+
border: 0;
|
|
325
|
+
border-radius: 6px;
|
|
326
|
+
background: none;
|
|
327
|
+
color: var(--accent);
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.skip-reading .icon {
|
|
332
|
+
display: block;
|
|
333
|
+
width: 16px;
|
|
334
|
+
height: 16px;
|
|
335
|
+
margin: auto;
|
|
336
|
+
fill: currentColor;
|
|
337
|
+
stroke: currentColor;
|
|
338
|
+
stroke-width: 1.5;
|
|
339
|
+
stroke-linejoin: round;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.skip-reading:hover,
|
|
343
|
+
.skip-reading:focus-visible {
|
|
344
|
+
color: var(--say-ink);
|
|
345
|
+
background: color-mix(in oklab, currentColor 14%, transparent);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.skip-reading:focus-visible {
|
|
349
|
+
outline: 2px solid var(--accent);
|
|
350
|
+
outline-offset: 1px;
|
|
351
|
+
}
|
|
352
|
+
|
|
418
353
|
.say:first-child::before {
|
|
419
354
|
content: "";
|
|
420
355
|
position: absolute;
|
|
@@ -452,12 +387,11 @@
|
|
|
452
387
|
overflow-wrap: anywhere;
|
|
453
388
|
}
|
|
454
389
|
|
|
390
|
+
/* A mark under the player's last line, its words in its title; empty once answered, still as high */
|
|
455
391
|
.status {
|
|
456
392
|
display: inline-flex;
|
|
457
|
-
|
|
458
|
-
gap: 3px;
|
|
393
|
+
min-height: 15px;
|
|
459
394
|
margin-top: 3px;
|
|
460
|
-
font-size: 12px;
|
|
461
395
|
color: var(--soft);
|
|
462
396
|
}
|
|
463
397
|
|
|
@@ -469,7 +403,6 @@
|
|
|
469
403
|
|
|
470
404
|
.status.read {
|
|
471
405
|
color: var(--accent);
|
|
472
|
-
font-weight: 600;
|
|
473
406
|
}
|
|
474
407
|
|
|
475
408
|
/* The box the player writes in: a bubble whose tail points at their picture */
|
|
@@ -653,7 +586,8 @@
|
|
|
653
586
|
|
|
654
587
|
/* Only the note's first bubble has a tail, pointing up at the Torretta beside the AI's name, the board turned round or not (Stefano) */
|
|
655
588
|
.ai-note > .say::before,
|
|
656
|
-
.ai-note > .say::after
|
|
589
|
+
.ai-note > .say::after,
|
|
590
|
+
.flipped .ai-note > .say::after {
|
|
657
591
|
content: none;
|
|
658
592
|
}
|
|
659
593
|
|
|
@@ -769,7 +703,7 @@
|
|
|
769
703
|
background: var(--card);
|
|
770
704
|
color: var(--ink);
|
|
771
705
|
/* The dialogs' backdrop, over the whole window */
|
|
772
|
-
box-shadow: 0 0 0 100vmax rgba(16, 22, 29, 0.5), 0 16px 40px
|
|
706
|
+
box-shadow: 0 0 0 100vmax rgba(16, 22, 29, 0.5), 0 16px 40px rgba(0, 0, 0, 0.22);
|
|
773
707
|
font-size: 15px;
|
|
774
708
|
line-height: 1.5;
|
|
775
709
|
text-align: center;
|
|
@@ -784,7 +718,7 @@
|
|
|
784
718
|
margin: 0 auto 12px;
|
|
785
719
|
border-radius: 50%;
|
|
786
720
|
background: var(--accent);
|
|
787
|
-
animation: reconnecting
|
|
721
|
+
animation: reconnecting 1600ms ease-in-out infinite;
|
|
788
722
|
}
|
|
789
723
|
|
|
790
724
|
@keyframes reconnecting {
|
|
@@ -846,13 +780,13 @@
|
|
|
846
780
|
|
|
847
781
|
.over-board > .asleep {
|
|
848
782
|
margin: 0;
|
|
849
|
-
box-shadow: 0 2px 6px
|
|
783
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14), 0 10px 28px rgba(0, 0, 0, 0.2);
|
|
850
784
|
}
|
|
851
785
|
|
|
852
786
|
.over-board > .start-clocks {
|
|
853
787
|
flex: none;
|
|
854
788
|
padding: 9px 22px;
|
|
855
|
-
box-shadow: 0 2px 6px
|
|
789
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14), 0 8px 20px rgba(0, 0, 0, 0.22);
|
|
856
790
|
pointer-events: auto;
|
|
857
791
|
}
|
|
858
792
|
|
package/dist/page/talk/talk.js
CHANGED
|
@@ -1,41 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
2
|
// The talk on the board page (D-51, D-53): the AI's words in bubbles that point at the Torretta, the player's above the box they write in.
|
|
3
3
|
import { createContext } from "preact";
|
|
4
|
-
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
|
4
|
+
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
5
5
|
import { Torretta } from "../avatars/torretta.js";
|
|
6
6
|
import { BadgeIcon, NoteIcon } from "../game/badges.js";
|
|
7
7
|
import { Figurines } from "../game/move-list.js";
|
|
8
8
|
import { talkLimits } from "../game/plies.js";
|
|
9
|
+
import { ReviewProgress } from "../game/review-progress.js";
|
|
9
10
|
import { signature } from "../players/model-name.js";
|
|
10
|
-
import { hush, speak } from "./voice.js";
|
|
11
|
+
import { hasVoice, hush, onReading, reading, skip, speak } from "./voice.js";
|
|
11
12
|
/** The AI's words of the game, a bubble each, the newest by the Torretta: below it, or above it on a board turned round; the first points at it. */
|
|
12
13
|
export function AiWords({ words, newestFirst, voice }) {
|
|
13
14
|
const list = useRef(null);
|
|
14
15
|
useOnlyWhole(list, words, newestFirst);
|
|
15
16
|
useSpokenNews(words, voice);
|
|
16
|
-
|
|
17
|
+
const read = useReading(voice);
|
|
18
|
+
return (_jsx("ol", { ref: list, class: "says", "aria-live": "polite", children: inOrder(words.map((word, index) => (_jsxs("li", { class: "say", style: aged(words.length - 1 - index), children: [_jsx(SkipReading, { at: wordKey(index, word), read: read, voice: voice }), _jsx(Marked, { text: word })] }, index))), newestFirst) }));
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* The AI's word on the move shown of a game in analysis (D-69), in a bubble under the Torretta beside the AI's name, pointing up at it: the move, its badge by name,
|
|
20
22
|
* and its note when it wrote one; read out as the player steps through the game. The Torretta and the name stay when the move has no word.
|
|
21
23
|
*/
|
|
22
|
-
export function AiNote({ ply, opening, closing, summary, reviewing, onComplete, name, by, mood, voice, text }) {
|
|
24
|
+
export function AiNote({ ply, opening, closing, summary, reviewing, count, onComplete, name, by, mood, voice, text }) {
|
|
23
25
|
// On a move of the book without the AI's note, the AI reports what the book says: the opening in the title, the variation under it.
|
|
24
26
|
const book = ply?.review?.note === undefined ? opening : undefined;
|
|
25
27
|
const shown = ply?.review ?? (book === undefined ? undefined : { badge: "book" });
|
|
26
28
|
const title = [shown?.badge === undefined || (book !== undefined && shown.badge === "book") ? undefined : text.badges[shown.badge], book?.family].filter((part) => part !== undefined).join(" · ");
|
|
27
29
|
const note = shown?.note ?? book?.variation;
|
|
28
|
-
|
|
30
|
+
// Each bubble read on its own, so the mark that skips one sits on the one read: the move's, the game's review, the end.
|
|
31
|
+
const moveWords = ply === undefined || shown === undefined ? "" : [title, note].filter((part) => part !== undefined && part !== "").join(". ");
|
|
32
|
+
const spoken = [
|
|
33
|
+
{ key: `move:${moveWords}`, words: moveWords },
|
|
34
|
+
{ key: `review:${summary ?? ""}`, words: summary?.replace(/\s*•\s+/g, ". ") ?? "" },
|
|
35
|
+
{ key: `closing:${closing ?? ""}`, words: closing ?? "" },
|
|
36
|
+
];
|
|
37
|
+
const keys = spoken.map((part) => part.key).join("\n");
|
|
38
|
+
const read = useReading(voice);
|
|
29
39
|
useEffect(() => {
|
|
30
40
|
if (voice !== undefined) {
|
|
31
41
|
speak(spoken, voice);
|
|
32
42
|
}
|
|
33
43
|
return hush;
|
|
34
|
-
}, [
|
|
44
|
+
}, [keys, voice]);
|
|
35
45
|
// The Torretta stays in sight through the whole analysis, over its bubbles, which show only where it wrote something.
|
|
36
|
-
return (_jsxs("div", { class: "ai-note", "aria-live": "polite", children: [_jsxs("p", { class: "who", children: [_jsx(Torretta, { size: 36, mood: mood }), _jsxs("span", { class: "signed", children: [name, by === undefined ? null : _jsx("small", { children: signature(by.annotator, by.effort) })] })] }), ply === undefined || shown === undefined ? null : (_jsxs("p", { class: `say move-note ${shown.badge ?? ""}`, children: [_jsxs("span", { class: "head", children: [shown.badge === undefined ? _jsx(NoteIcon, {}) : _jsx(BadgeIcon, { badge: shown.badge }), _jsxs("b", { children: [_jsxs("span", { class: "visually-hidden", children: [ply.number, ply.side === "white" ? "." : "...", ply.san] }), _jsxs("span", { "aria-hidden": "true", children: [_jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] })] }), " ", title] }), note === undefined ? null : (_jsx("span", { class: "note", children: _jsx(Marked, { text: note }) })), shown.arrows === undefined && shown.squares === undefined ? null : _jsx(Legend, { review: shown, words: text.drawn })] })), summary === undefined ? null : _jsx(GameReview, { by: name, summary: summary, text: text }), closing === undefined ? null : (
|
|
37
|
-
? (_jsxs("
|
|
38
|
-
: null, onComplete === undefined ? null : (_jsxs("
|
|
46
|
+
return (_jsxs("div", { class: "ai-note", "aria-live": "polite", children: [_jsxs("p", { class: "who", children: [_jsx(Torretta, { size: 36, mood: mood }), _jsxs("span", { class: "signed", children: [name, by === undefined ? null : _jsx("small", { children: signature(by.annotator, by.effort) })] })] }), ply === undefined || shown === undefined ? null : (_jsxs("p", { class: `say move-note ${shown.badge ?? ""}`, children: [_jsx(SkipReading, { at: spoken[0].key, read: read, voice: voice }), _jsxs("span", { class: "head", children: [shown.badge === undefined ? _jsx(NoteIcon, {}) : _jsx(BadgeIcon, { badge: shown.badge }), _jsxs("b", { children: [_jsxs("span", { class: "visually-hidden", children: [ply.number, ply.side === "white" ? "." : "...", ply.san] }), _jsxs("span", { "aria-hidden": "true", children: [_jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] })] }), " ", title] }), note === undefined ? null : (_jsx("span", { class: "note", children: _jsx(Marked, { text: note }) })), shown.arrows === undefined && shown.squares === undefined ? null : _jsx(Legend, { review: shown, words: text.drawn })] })), summary === undefined ? null : _jsx(GameReview, { by: name, summary: summary, skipping: _jsx(SkipReading, { at: spoken[1].key, read: read, voice: voice }), text: text }), closing === undefined ? null : (_jsxs("p", { class: "say closing", children: [_jsx(SkipReading, { at: spoken[2].key, read: read, voice: voice }), _jsx(Marked, { text: closing })] })), reviewing === true
|
|
47
|
+
? (_jsxs("div", { class: "say reviewing", children: [_jsx("p", { role: "status", children: text.reviewing(name) }), count === undefined ? null : _jsx(ReviewProgress, { count: count, label: text.reviewing(name) })] }))
|
|
48
|
+
: null, onComplete === undefined ? null : (_jsxs("div", { class: "say incomplete", children: [_jsxs("p", { children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: onComplete, children: text.complete })] }), count === undefined || count.done === 0 ? null : _jsx(ReviewProgress, { count: count, label: text.incomplete, stopped: true })] }))] }));
|
|
39
49
|
}
|
|
40
50
|
/** The squares and moves in the AI's words, such as e3, Cxe4, 7.Nd4 or O-O: in bold, as the player looks for them on the board. */
|
|
41
51
|
export function Marked({ text }) {
|
|
@@ -123,30 +133,58 @@ function squareIn(word) {
|
|
|
123
133
|
return /[a-h][1-8](?!.*[a-h][1-8])/.exec(word)?.[0];
|
|
124
134
|
}
|
|
125
135
|
/** The AI's word on the whole game (D-69), in the Torretta's bubble at the start of the game: a paragraph, and its points as a list. */
|
|
126
|
-
function GameReview({ by, summary, text }) {
|
|
136
|
+
function GameReview({ by, summary, skipping, text }) {
|
|
127
137
|
const [lead = "", ...points] = summary.split(/\s*•\s+/);
|
|
128
|
-
return (_jsxs("section", { class: "say review", "aria-label": text.reviewBy(by), children: [_jsxs("p", { class: "head", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }) }), text.reviewBy(by)] }), lead === "" ? null : (_jsx("p", { children: _jsx(Marked, { text: lead }) })), points.length === 0 ? null : (_jsx("ul", { children: points.map((point, index) => (_jsx("li", { children: _jsx(Marked, { text: point }) }, index))) }))] }));
|
|
138
|
+
return (_jsxs("section", { class: "say review", "aria-label": text.reviewBy(by), children: [skipping, _jsxs("p", { class: "head", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }) }), text.reviewBy(by)] }), lead === "" ? null : (_jsx("p", { children: _jsx(Marked, { text: lead }) })), points.length === 0 ? null : (_jsx("ul", { children: points.map((point, index) => (_jsx("li", { children: _jsx(Marked, { text: point }) }, index))) }))] }));
|
|
129
139
|
}
|
|
130
140
|
function RailPlayer({ place, picture, name, side, moves, unread = false }) {
|
|
131
141
|
return (_jsxs("div", { class: `rail-player ${place}${moves ? " moving" : ""}`, children: [_jsx("span", { class: `face${unread ? " unread" : ""}`, children: picture }), _jsxs("span", { class: "rail-name", children: [name, " ", side === undefined ? null : _jsxs("span", { class: "note", children: ["\u00B7 ", side] })] })] }));
|
|
132
142
|
}
|
|
133
|
-
/** Reads aloud each word the AI adds while the page is open, not those already there. */
|
|
143
|
+
/** Reads aloud each word the AI adds while the page is open, not those already there, a bubble after the other; the voice turned off stops it. */
|
|
134
144
|
function useSpokenNews(words, voice) {
|
|
135
145
|
const heard = useRef(words.length);
|
|
146
|
+
const speaking = voice !== undefined;
|
|
136
147
|
useEffect(() => {
|
|
137
148
|
if (voice !== undefined && words.length > heard.current) {
|
|
138
|
-
speak(words.slice(heard.current).
|
|
149
|
+
speak(words.slice(heard.current).map((word, index) => ({ key: wordKey(heard.current + index, word), words: word })), voice);
|
|
139
150
|
}
|
|
140
151
|
heard.current = words.length;
|
|
141
152
|
}, [words.length, voice]);
|
|
153
|
+
useEffect(() => speaking ? hush : undefined, [speaking]);
|
|
154
|
+
}
|
|
155
|
+
/** A word of the AI's by its place in the game and its text, so a word of another game in the same place is not taken for it. */
|
|
156
|
+
function wordKey(index, word) {
|
|
157
|
+
return `${index}:${word}`;
|
|
158
|
+
}
|
|
159
|
+
/** The key of the bubble the voice reads, none while it reads nothing or is off. */
|
|
160
|
+
function useReading(voice) {
|
|
161
|
+
const [key, setKey] = useState(reading);
|
|
162
|
+
useEffect(() => onReading(setKey), []);
|
|
163
|
+
return voice === undefined ? undefined : key;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* In the corner of a bubble the voice may read, a mark that stops reading it while it is read, and the next goes on; its room is kept while the voice is on,
|
|
167
|
+
* so the words never move when it comes and goes.
|
|
168
|
+
*/
|
|
169
|
+
function SkipReading({ at, read, voice }) {
|
|
170
|
+
if (voice === undefined || (read !== at && !hasVoice(voice.language))) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (read !== at) {
|
|
174
|
+
return _jsx("span", { class: "skip-reading", "aria-hidden": "true" });
|
|
175
|
+
}
|
|
176
|
+
return (_jsx("button", { type: "button", class: "skip-reading", title: voice.skipReading, "aria-label": voice.skipReading, onClick: () => skip(at), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 7L12 12L5 17ZM12 7L19 12L12 17ZM19 6.5V17.5" }) }) }));
|
|
142
177
|
}
|
|
143
178
|
/** A list's items, the newest first when it asks. */
|
|
144
179
|
function inOrder(items, newestFirst) {
|
|
145
180
|
return newestFirst ? items.reverse() : items;
|
|
146
181
|
}
|
|
147
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* A word fades with age: the newest whole, the older ones well back, so they never pass for the newest (Stefano), each fainter down to a floor where its text still
|
|
184
|
+
* keeps 3.5:1 on its bubble in the light theme.
|
|
185
|
+
*/
|
|
148
186
|
function aged(age) {
|
|
149
|
-
return { opacity: age === 0 ? 1 : Math.max(0.
|
|
187
|
+
return { opacity: age === 0 ? 1 : Math.max(0.6, 0.7 - (age - 1) * 0.05) };
|
|
150
188
|
}
|
|
151
189
|
/** 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
190
|
function reread(index, lastAnswer) {
|
|
@@ -170,9 +208,9 @@ function useOnlyWhole(list, lines, newestFirst = false) {
|
|
|
170
208
|
for (const item of element.children) {
|
|
171
209
|
const place = item.getBoundingClientRect();
|
|
172
210
|
const style = item.style;
|
|
173
|
-
// The newest never hides: one longer than the column, as a note with the helps on (D-78), scrolls in itself.
|
|
211
|
+
// The newest never hides: one longer than the column, as a note with the helps on (D-78), scrolls in itself with no scrollbar (talk.css).
|
|
174
212
|
style.maxHeight = item === newest ? room + "px" : "";
|
|
175
|
-
|
|
213
|
+
item.classList.toggle("scrolls", item === newest && item.scrollHeight > room);
|
|
176
214
|
style.visibility = item === newest || (place.top >= box.top - 1 && place.bottom <= box.bottom + 1) ? "" : "hidden";
|
|
177
215
|
}
|
|
178
216
|
};
|
|
@@ -182,8 +220,15 @@ function useOnlyWhole(list, lines, newestFirst = false) {
|
|
|
182
220
|
return () => observer.disconnect();
|
|
183
221
|
}, [list, lines.length, lines.at(-1)]);
|
|
184
222
|
}
|
|
185
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Under the player's last line, a mark with its words as title: one check once written, two once the AI has read it; once it has answered, its answer says it and
|
|
225
|
+
* the mark leaves, its room kept so the line does not move.
|
|
226
|
+
*/
|
|
186
227
|
function Status({ heard, name, text }) {
|
|
187
|
-
|
|
188
|
-
|
|
228
|
+
if (heard === "answered") {
|
|
229
|
+
return _jsx("span", { class: "status", "aria-hidden": "true" });
|
|
230
|
+
}
|
|
231
|
+
const read = heard === "read";
|
|
232
|
+
const label = read ? text.readBy(name) : text.sent;
|
|
233
|
+
return (_jsx("span", { class: read ? "status read" : "status", role: "img", title: label, "aria-label": label, children: _jsx("svg", { class: "icon", viewBox: "0 0 21 24", "aria-hidden": "true", children: read ? _jsx("path", { d: "M2 12.5L6 16.5L13.5 8M10 15.5L11 16.5L18.5 8" }) : _jsx("path", { d: "M3.5 12.5L7.5 16.5L15 8" }) }) }));
|
|
189
234
|
}
|