chesspal 0.3.2 → 0.3.3
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/dist/commands/answers/agents.d.ts +1 -1
- package/dist/commands/answers/agents.js +8 -4
- package/dist/page/app/app.js +37 -13
- package/dist/page/app/served.js +8 -3
- package/dist/page/game/column.js +1 -1
- package/dist/page/game/game.css +25 -2
- package/dist/page/helps/helps.css +74 -5
- package/dist/page/helps/helps.js +5 -5
- package/dist/page/table/window.js +6 -5
- package/dist/page/talk/talk.js +2 -2
- package/dist/page/text.js +12 -2
- package/dist/server/bell/bell.js +14 -1
- package/dist/server/page/attach.js +8 -3
- package/dist/server/page/game.js +5 -2
- package/dist/server/page/page.js +13 -1
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ export declare function agentsLine(agents: readonly string[]): string;
|
|
|
38
38
|
export declare function goneOver(context: Context, game: string): boolean;
|
|
39
39
|
/**
|
|
40
40
|
* The run of the analyst agent at work on a game: the one that took it last, unless its chat has started again since, not only summarized, and took the
|
|
41
|
-
* agent with it.
|
|
41
|
+
* agent with it, or it has written nothing for half an hour, as one long dead (D-109): a quiet minute proves nothing, since an analyst may think long.
|
|
42
42
|
*/
|
|
43
43
|
export declare function analystOf(context: Context, game: string): string | undefined;
|
|
44
44
|
/** What stops an analyst agent on a game another analyst has taken: the chat hears of the review from that one (Priya). */
|
|
@@ -90,14 +90,18 @@ export function goneOver(context, game) {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* The run of the analyst agent at work on a game: the one that took it last, unless its chat has started again since, not only summarized, and took the
|
|
93
|
-
* agent with it.
|
|
93
|
+
* agent with it, or it has written nothing for half an hour, as one long dead (D-109): a quiet minute proves nothing, since an analyst may think long.
|
|
94
94
|
*/
|
|
95
95
|
export function analystOf(context, game) {
|
|
96
|
-
const
|
|
96
|
+
const analysis = gameInAnalysis(gameFolder(context.home()));
|
|
97
|
+
const taken = analysis?.analyst;
|
|
97
98
|
const start = lastChatStart(context.folder);
|
|
98
|
-
const
|
|
99
|
-
|
|
99
|
+
const restarted = taken !== undefined && start !== undefined && start.source !== "compact" && Date.parse(start.at) > Date.parse(taken.since);
|
|
100
|
+
const quiet = analysis !== undefined && context.now().getTime() - Date.parse(analysis.at) > analystQuiet;
|
|
101
|
+
return taken?.game === game && !restarted && !quiet ? taken.run : undefined;
|
|
100
102
|
}
|
|
103
|
+
/** How long an analyst may write nothing before it counts as gone. */
|
|
104
|
+
const analystQuiet = 30 * 60 * 1000;
|
|
101
105
|
/** What stops an analyst agent on a game another analyst has taken: the chat hears of the review from that one (Priya). */
|
|
102
106
|
export const anotherAnalyst = "Stop now: another analyst is reviewing this game. End with one line for the AI that started you: its review is on its way to the "
|
|
103
107
|
+ "board page, and its end comes to you by itself; meanwhile talk the game over with the player whenever they ask. Should it never come, the player has "
|
package/dist/page/app/app.js
CHANGED
|
@@ -43,7 +43,7 @@ import { textFor } from "../text.js";
|
|
|
43
43
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
44
44
|
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
45
45
|
import { phaseOf, verdictOf } from "./phase.js";
|
|
46
|
-
import { post, sendMessage, sendMove, useServed } from "./served.js";
|
|
46
|
+
import { ask, post, sendMessage, sendMove, useServed } from "./served.js";
|
|
47
47
|
import { tabTitle, useTabTitle } from "./title.js";
|
|
48
48
|
/**
|
|
49
49
|
* The page: the board of the game in the player's folder, where the player moves on their turn, the talk with the AI beside it, and the game's column; in the
|
|
@@ -71,12 +71,23 @@ export function App({ start }) {
|
|
|
71
71
|
const { data: served, problem, offline, reload } = useServed(() => setDialog("new-game"));
|
|
72
72
|
// Nuova partita with no chat at the table asks for one first (D-107): the chat's window, then the new game's, however the first closes.
|
|
73
73
|
const [chatFirst, setChatFirst] = useState(false);
|
|
74
|
-
|
|
74
|
+
// At the table with no chat, and none opening: what asks for the AI opens the chat's window first, to choose its host and model (D-107, D-109).
|
|
75
|
+
const chatMissing = () => {
|
|
75
76
|
const table = offline ? undefined : served?.table;
|
|
76
|
-
|
|
77
|
+
return table !== undefined && !table.chat && table.opening === undefined;
|
|
78
|
+
};
|
|
79
|
+
// The chat's window opened for an analysis asks who reviews the game, not whom the player plays (Rosa, Zoe).
|
|
80
|
+
const [chatToReview, setChatToReview] = useState(false);
|
|
81
|
+
const askNewGame = () => {
|
|
82
|
+
const first = chatMissing();
|
|
77
83
|
setChatFirst(first);
|
|
84
|
+
setChatToReview(false);
|
|
78
85
|
setDialog(first ? "chat" : "new-game");
|
|
79
86
|
};
|
|
87
|
+
const askChat = () => {
|
|
88
|
+
setChatToReview(false);
|
|
89
|
+
setDialog("chat");
|
|
90
|
+
};
|
|
80
91
|
const closeChat = () => {
|
|
81
92
|
setDialog(chatFirst ? "new-game" : undefined);
|
|
82
93
|
setChatFirst(false);
|
|
@@ -302,6 +313,8 @@ export function App({ start }) {
|
|
|
302
313
|
const summary = studied === undefined ? undefined : gameReviewOf(live.record);
|
|
303
314
|
// An AI is going over the game only when the server says so: with no chat at it, nobody is (Stefano).
|
|
304
315
|
const reviewing = summary === undefined && studied?.reviewing === true && !offline;
|
|
316
|
+
// With nobody at it, a review not finished says so, and the player brings the game back to the AI (D-109).
|
|
317
|
+
const completeIt = summary === undefined && studied?.incomplete === true && !offline ? () => void studyGame({ game: studied.name }) : undefined;
|
|
305
318
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
306
319
|
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
307
320
|
const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
|
|
@@ -320,7 +333,7 @@ export function App({ start }) {
|
|
|
320
333
|
// Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
|
|
321
334
|
// On the table, what its chat is doing stands there, and in a game with no chat at it a line that opens the chat's window (D-107).
|
|
322
335
|
const table = offline ? undefined : served.table;
|
|
323
|
-
const atTable = table === undefined ? null : tableNote({ table, name, inGame: going, onOpen:
|
|
336
|
+
const atTable = table === undefined ? null : tableNote({ table, name, inGame: going, onOpen: askChat, text });
|
|
324
337
|
const asleepNote = atTable !== null
|
|
325
338
|
? atTable
|
|
326
339
|
: asleep && table?.chat !== false
|
|
@@ -402,15 +415,19 @@ export function App({ start }) {
|
|
|
402
415
|
};
|
|
403
416
|
// In analysis the AI's note goes with the other side's card and the talk with the player's, turned round with the board as in a game (D-101, Zoe).
|
|
404
417
|
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place, picture: pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
|
|
405
|
-
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
|
|
418
|
+
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog. One the AI has to go over, with no
|
|
419
|
+
// chat at the table, opens the chat's window: the analyst works with the chat's model (D-109).
|
|
406
420
|
const studyGame = async (studied) => {
|
|
407
|
-
const
|
|
408
|
-
if (refused
|
|
409
|
-
|
|
421
|
+
const sent = await ask("/api/study", studied, text, name);
|
|
422
|
+
if ("refused" in sent) {
|
|
423
|
+
return sent.refused.text;
|
|
410
424
|
}
|
|
411
|
-
|
|
425
|
+
const toReview = sent.answer.reviewed === false && chatMissing();
|
|
426
|
+
setChatToReview(toReview);
|
|
427
|
+
setDialog(toReview ? "chat" : undefined);
|
|
428
|
+
return undefined;
|
|
412
429
|
};
|
|
413
|
-
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
430
|
+
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
414
431
|
? null
|
|
415
432
|
: (_jsx(UpdateDialog, { open: dialog === "update", version: served.update.version, current: served.version, installing: served.update.installing === true, installed: served.update.installed === true, going: going, chat: served.table.chat ? (served.table.held ? "held" : "byHand") : undefined, name: name, onClose: () => setDialog(undefined), text: text })), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true
|
|
416
433
|
? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) })
|
|
@@ -419,11 +436,11 @@ export function App({ start }) {
|
|
|
419
436
|
: null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
420
437
|
? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
|
|
421
438
|
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
|
|
422
|
-
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
439
|
+
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
423
440
|
setDraft(words);
|
|
424
441
|
setUnsent(undefined);
|
|
425
442
|
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
|
|
426
|
-
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen:
|
|
443
|
+
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
427
444
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
428
445
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
429
446
|
setDraft(words);
|
|
@@ -434,7 +451,14 @@ export function App({ start }) {
|
|
|
434
451
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
435
452
|
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleepNote !== null || clockWaits)
|
|
436
453
|
? (_jsxs("div", { class: "over-board", children: [asleepNote, startClocks] }))
|
|
437
|
-
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : {
|
|
454
|
+
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : {
|
|
455
|
+
analysis: {
|
|
456
|
+
by: analyst,
|
|
457
|
+
...(summary === undefined ? {} : { summary }),
|
|
458
|
+
...(reviewing ? { reviewing: true } : {}),
|
|
459
|
+
...(completeIt === undefined ? {} : { onComplete: completeIt }),
|
|
460
|
+
},
|
|
461
|
+
}), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
|
|
438
462
|
}
|
|
439
463
|
/** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
|
|
440
464
|
function pictureOf({ who }, profile, size, mood) {
|
package/dist/page/app/served.js
CHANGED
|
@@ -56,17 +56,22 @@ export function useServed(onNewGame) {
|
|
|
56
56
|
* it sends the player, or undefined.
|
|
57
57
|
*/
|
|
58
58
|
export async function post(path, body, text, name) {
|
|
59
|
+
const sent = await ask(path, body, text, name);
|
|
60
|
+
return "refused" in sent ? sent.refused : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Sends a request to the server as `post` does: its answer, or the reason it was refused. */
|
|
63
|
+
export async function ask(path, body, text, name) {
|
|
59
64
|
try {
|
|
60
65
|
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
61
66
|
if (response.ok) {
|
|
62
|
-
return
|
|
67
|
+
return { answer: await response.json().catch(() => ({})) };
|
|
63
68
|
}
|
|
64
69
|
const refused = (await response.json());
|
|
65
70
|
const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
|
|
66
|
-
return { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true };
|
|
71
|
+
return { refused: { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true } };
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
69
|
-
return { text: String(error), words: false };
|
|
74
|
+
return { refused: { text: String(error), words: false } };
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
/** Sends the player's move to the server, which plays it as the chat would, with the words written for the AI: the reason it was refused, or undefined. */
|
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: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis
|
|
17
|
+
: (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsx("p", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })] })) }));
|
|
18
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 }) {
|
package/dist/page/game/game.css
CHANGED
|
@@ -320,6 +320,27 @@
|
|
|
320
320
|
color: var(--soft);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/* A review nobody is finishing: the note, and beside it the button that brings the game back to the AI (D-109); neutral, as no error (Zoe) */
|
|
324
|
+
.review.incomplete,
|
|
325
|
+
.say.incomplete {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: space-between;
|
|
329
|
+
gap: 8px;
|
|
330
|
+
margin: 0;
|
|
331
|
+
color: var(--soft);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.review.incomplete button,
|
|
335
|
+
.say.incomplete button {
|
|
336
|
+
flex: none;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* On the bubble's tint the button keeps the card's ground, to read as one */
|
|
340
|
+
.say.incomplete button {
|
|
341
|
+
background: var(--card);
|
|
342
|
+
}
|
|
343
|
+
|
|
323
344
|
.review ul {
|
|
324
345
|
margin: 0;
|
|
325
346
|
padding-left: 18px;
|
|
@@ -497,7 +518,8 @@
|
|
|
497
518
|
padding: 8px;
|
|
498
519
|
}
|
|
499
520
|
|
|
500
|
-
.analysis-actions button
|
|
521
|
+
.analysis-actions button,
|
|
522
|
+
.incomplete button {
|
|
501
523
|
flex: 1;
|
|
502
524
|
display: inline-flex;
|
|
503
525
|
align-items: center;
|
|
@@ -513,7 +535,8 @@
|
|
|
513
535
|
cursor: pointer;
|
|
514
536
|
}
|
|
515
537
|
|
|
516
|
-
.analysis-actions button:hover
|
|
538
|
+
.analysis-actions button:hover,
|
|
539
|
+
.incomplete button:hover {
|
|
517
540
|
border-color: var(--accent);
|
|
518
541
|
}
|
|
519
542
|
|
|
@@ -238,12 +238,19 @@
|
|
|
238
238
|
padding: 3px 6px;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/* The coach's words at the top of the card (D-88)
|
|
241
|
+
/* The coach's words at the top of the card (D-88), tinted rows as the tactics': each judgment in its badge's colour, the hint in the accent behind a bulb */
|
|
242
242
|
.helps-card .coach {
|
|
243
243
|
display: flex;
|
|
244
244
|
flex-direction: column;
|
|
245
|
-
gap:
|
|
246
|
-
font-size:
|
|
245
|
+
gap: 3px;
|
|
246
|
+
font-size: 13px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.helps-card .coach .judged,
|
|
250
|
+
.helps-card .coach .coach-hint {
|
|
251
|
+
--row: var(--accent);
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
background: color-mix(in srgb, var(--row) 10%, transparent);
|
|
247
254
|
}
|
|
248
255
|
|
|
249
256
|
.coach-hint summary {
|
|
@@ -257,6 +264,48 @@
|
|
|
257
264
|
display: none;
|
|
258
265
|
}
|
|
259
266
|
|
|
267
|
+
/* A row's head: its icon, its words in ink, the sign of a line that opens on the right */
|
|
268
|
+
.helps-card .coach .head,
|
|
269
|
+
.helps-card .coach summary {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 6px;
|
|
273
|
+
padding: 3px 8px 3px 6px;
|
|
274
|
+
border-radius: 6px;
|
|
275
|
+
color: var(--ink);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.helps-card .coach summary {
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
list-style: none;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.helps-card .coach summary:hover,
|
|
284
|
+
.helps-card .coach details[open] > summary {
|
|
285
|
+
background: color-mix(in srgb, var(--row) 16%, transparent);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.helps-card .coach .row-icon {
|
|
289
|
+
display: grid;
|
|
290
|
+
place-items: center;
|
|
291
|
+
flex: none;
|
|
292
|
+
width: 16px;
|
|
293
|
+
height: 16px;
|
|
294
|
+
color: var(--row);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.board-map .coach-hint .row-icon {
|
|
298
|
+
display: none;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* A finger needs a taller line (Zoe) */
|
|
302
|
+
@media (pointer: coarse) {
|
|
303
|
+
.helps-card .coach .head,
|
|
304
|
+
.helps-card .coach summary {
|
|
305
|
+
padding-block: 7px;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
260
309
|
/* The same sign on both, so that on a phone, with no pointer, they read as lines that open (Zoe) */
|
|
261
310
|
.helps-card .coach summary::after,
|
|
262
311
|
.board-map .coach-hint summary::after {
|
|
@@ -265,12 +314,31 @@
|
|
|
265
314
|
color: var(--soft);
|
|
266
315
|
}
|
|
267
316
|
|
|
317
|
+
.helps-card .coach summary::after {
|
|
318
|
+
margin-left: auto;
|
|
319
|
+
}
|
|
320
|
+
|
|
268
321
|
.helps-card .coach details[open] > summary::after,
|
|
269
322
|
.board-map .coach-hint[open] > summary::after {
|
|
270
323
|
content: "▾";
|
|
271
324
|
}
|
|
272
325
|
|
|
273
|
-
|
|
326
|
+
/* The words opened, under the head's text */
|
|
327
|
+
.helps-card .coach .note {
|
|
328
|
+
display: block;
|
|
329
|
+
margin: 0;
|
|
330
|
+
padding: 2px 8px 6px 28px;
|
|
331
|
+
color: var(--say-ink);
|
|
332
|
+
font-size: 13px;
|
|
333
|
+
line-height: 1.4;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.helps-card .coach .legend {
|
|
337
|
+
margin: 0;
|
|
338
|
+
padding: 0 8px 6px 28px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.board-map .coach-hint .note {
|
|
274
342
|
display: block;
|
|
275
343
|
margin-top: 4px;
|
|
276
344
|
color: var(--say-ink);
|
|
@@ -472,7 +540,8 @@
|
|
|
472
540
|
color: var(--row);
|
|
473
541
|
}
|
|
474
542
|
|
|
475
|
-
.help-rows .row-icon svg
|
|
543
|
+
.help-rows .row-icon svg,
|
|
544
|
+
.coach-hint .row-icon svg {
|
|
476
545
|
width: 14px;
|
|
477
546
|
height: 14px;
|
|
478
547
|
fill: none;
|
package/dist/page/helps/helps.js
CHANGED
|
@@ -41,12 +41,12 @@ export function BareButton({ bare, onBare, text }) {
|
|
|
41
41
|
return (_jsx(HoldButton, { on: bare, onHold: onBare, title: text.helpsCard.bareHint, label: text.helpsCard.bare, kind: "bare-button", children: _jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: [_jsx("path", { d: "M1.5 8S4 3.5 8 3.5 14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8Z" }), _jsx("circle", { cx: "8", cy: "8", r: "2" }), _jsx("path", { d: "M2.5 2.5l11 11" })] }) }));
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* The coach's hint, a line that opens on its words and the legend of what it draws (D-88): in the helps' card
|
|
45
|
-
* map, where its drawings are in sight (Zoe).
|
|
44
|
+
* The coach's hint, a line that opens on its words and the legend of what it draws (D-88): in the helps' card a tinted row behind a bulb, as the tactics',
|
|
45
|
+
* and on a phone under the board, beside the map, where its drawings are in sight (Zoe).
|
|
46
46
|
*/
|
|
47
47
|
export function HintLine({ hint, open, onOpen, text }) {
|
|
48
48
|
const words = text.helpsCard;
|
|
49
|
-
return (_jsxs("details", { class: "coach-hint", open: open, onToggle: (event) => onOpen(event.currentTarget.open), children: [
|
|
49
|
+
return (_jsxs("details", { class: "coach-hint", open: open, onToggle: (event) => onOpen(event.currentTarget.open), children: [_jsxs("summary", { children: [_jsxs(RowIcon, { children: [_jsx("path", { d: "M5.5 10.5C4.3 9.7 3.5 8.4 3.5 7a4.5 4.5 0 0 1 9 0c0 1.4-.8 2.7-2 3.5V12h-5Z" }), _jsx("path", { d: "M6.5 14.2h3" })] }), open ? words.hideHint : words.showHint] }), _jsx("span", { class: "note", children: _jsx(Marked, { text: hint.text }) }), hint.arrows === undefined && hint.squares === undefined ? null : _jsx(Legend, { review: hint, words: words.drawn })] }));
|
|
50
50
|
}
|
|
51
51
|
/** An icon the mouse holds, while what it shows lasts; a finger, or the keyboard, turns it on and off (D-78). */
|
|
52
52
|
function HoldButton({ on, onHold, title, label, kind, children }) {
|
|
@@ -54,11 +54,11 @@ function HoldButton({ on, onHold, title, label, kind, children }) {
|
|
|
54
54
|
const mouse = (event) => event instanceof PointerEvent && event.pointerType === "mouse";
|
|
55
55
|
return (_jsx("button", { type: "button", class: kind === undefined ? "hold-button" : `hold-button ${kind}`, "aria-pressed": on, "aria-label": label, title: title, onPointerDown: (event) => mouse(event) && onHold(true), onPointerUp: (event) => mouse(event) && onHold(false), onPointerLeave: (event) => mouse(event) && onHold(false), onClick: (event) => !mouse(event) && onHold(!on), children: children }));
|
|
56
56
|
}
|
|
57
|
-
/** The coach's judgment of the player's move: its badge and the move in a
|
|
57
|
+
/** The coach's judgment of the player's move: its badge and the move in a row tinted in the badge's colour, which opens on its note (D-78, D-88). */
|
|
58
58
|
function Judged({ ply, text }) {
|
|
59
59
|
const { badge, note } = ply.review ?? {};
|
|
60
60
|
const head = (_jsxs(_Fragment, { children: [badge === undefined ? _jsx(NoteIcon, {}) : _jsx(BadgeIcon, { badge: badge }), _jsxs("b", { children: [_jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] }), " ", badge === undefined ? null : text.badges[badge]] }));
|
|
61
|
-
return (_jsx("div", { class: `move-note judged ${badge ?? ""}`, children: note === undefined ? _jsx("span", { class: "head", children: head }) : (_jsxs("details", { children: [_jsx("summary", { class: "head", children: head }), _jsx("span", { class: "note", children: _jsx(Marked, { text: note }) })] })) }));
|
|
61
|
+
return (_jsx("div", { class: `move-note judged ${badge ?? ""}`, style: badge === undefined ? undefined : { "--row": `var(--badge-${badge})` }, children: note === undefined ? _jsx("span", { class: "head", children: head }) : (_jsxs("details", { children: [_jsx("summary", { class: "head", children: head }), _jsx("span", { class: "note", children: _jsx(Marked, { text: note }) })] })) }));
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* The tactics in lines (D-91): one a kind of fact and a side, its count, red when it threatens the player and green when it is their chance; a click opens
|
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } 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
|
-
/** The chat's window: closed once a chat opens or starts again, the page then saying where it stands. */
|
|
6
|
-
export function ChatDialog({ open, table, settings, onChange, name, onClose, text }) {
|
|
5
|
+
/** 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
|
+
export function ChatDialog({ open, review = false, table, settings, onChange, name, onClose, text }) {
|
|
7
7
|
const [sending, setSending] = useState(false);
|
|
8
8
|
const [failed, setFailed] = useState(false);
|
|
9
9
|
useEffect(() => setFailed(false), [open]);
|
|
@@ -25,20 +25,21 @@ export function ChatDialog({ open, table, settings, onChange, name, onClose, tex
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
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;
|
|
28
29
|
const note = installed.length === 0
|
|
29
30
|
? words.install(name)
|
|
30
31
|
: busy
|
|
31
|
-
? chatState(table, name, text) ??
|
|
32
|
+
? chatState(table, name, text) ?? chooseNote
|
|
32
33
|
: byHand
|
|
33
34
|
? words.byHand(name)
|
|
34
35
|
: table.chat
|
|
35
36
|
? words.restartHint
|
|
36
|
-
:
|
|
37
|
+
: chooseNote;
|
|
37
38
|
const offersChoices = installed.some((host) => host.models.length > 0 || host.efforts.length > 0);
|
|
38
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.
|
|
39
40
|
const started = table.held ? table.started : undefined;
|
|
40
41
|
const changed = (host) => started !== undefined && (chosen(host, "model", settings) !== (started.model ?? "") || chosen(host, "effort", settings) !== (started.effort ?? ""));
|
|
41
|
-
return (_jsxs(Dialog, { open: open, title: words.title, onClose: onClose, text: text, children: [_jsx("p", { class: "note", children: note }), _jsxs("div", { class: "chat-hosts", role: "group", "aria-label": words.title, children: [offersChoices
|
|
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
|
|
42
43
|
? (_jsxs("div", { class: "chat-head", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: words.model }), _jsx("span", { children: words.effort })] }))
|
|
43
44
|
: null, installed.map((host) => {
|
|
44
45
|
const atTable = table.chat && table.host === host.name;
|
package/dist/page/talk/talk.js
CHANGED
|
@@ -19,7 +19,7 @@ export function AiWords({ words, newestFirst, voice }) {
|
|
|
19
19
|
* 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
20
|
* 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
21
|
*/
|
|
22
|
-
export function AiNote({ ply, opening, closing, summary, reviewing, name, by, mood, voice, text }) {
|
|
22
|
+
export function AiNote({ ply, opening, closing, summary, reviewing, onComplete, name, by, mood, voice, text }) {
|
|
23
23
|
// 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
24
|
const book = ply?.review?.note === undefined ? opening : undefined;
|
|
25
25
|
const shown = ply?.review ?? (book === undefined ? undefined : { badge: "book" });
|
|
@@ -35,7 +35,7 @@ export function AiNote({ ply, opening, closing, summary, reviewing, name, by, mo
|
|
|
35
35
|
// The Torretta stays in sight through the whole analysis, over its bubbles, which show only where it wrote something.
|
|
36
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 : (_jsx("p", { class: "say closing", children: _jsx(Marked, { text: closing }) })), reviewing === true
|
|
37
37
|
? (_jsxs("p", { class: "say reviewing", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(name)] }))
|
|
38
|
-
: null] }));
|
|
38
|
+
: null, onComplete === undefined ? null : (_jsxs("p", { class: "say incomplete", children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: onComplete, children: text.complete })] }))] }));
|
|
39
39
|
}
|
|
40
40
|
/** 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
41
|
export function Marked({ text }) {
|
package/dist/page/text.js
CHANGED
|
@@ -87,7 +87,7 @@ const english = {
|
|
|
87
87
|
saveMoves: "PGN",
|
|
88
88
|
saveMovesHint: "The moves alone, to open on Lichess or chess.com",
|
|
89
89
|
noGameNow: "No game in progress",
|
|
90
|
-
analysing: "Analysis
|
|
90
|
+
analysing: "Analysis",
|
|
91
91
|
badges: {
|
|
92
92
|
brilliant: "Brilliant",
|
|
93
93
|
great: "Great move",
|
|
@@ -115,6 +115,9 @@ const english = {
|
|
|
115
115
|
mated: (side) => `${side === "white" ? "White" : "Black"} is mated`,
|
|
116
116
|
},
|
|
117
117
|
reviewing: (name) => `${name} is going over the game…`,
|
|
118
|
+
incomplete: "Analysis not complete",
|
|
119
|
+
complete: "Complete it",
|
|
120
|
+
completeHint: "Complete the analysis",
|
|
118
121
|
you: "You",
|
|
119
122
|
ai: "AI",
|
|
120
123
|
sides: { white: "White", black: "Black" },
|
|
@@ -170,6 +173,8 @@ const english = {
|
|
|
170
173
|
table: {
|
|
171
174
|
title: "Who are you playing?",
|
|
172
175
|
choose: "Choose the chat to play with: it opens in a window of its own, to keep open.",
|
|
176
|
+
reviewTitle: "Who reviews the game?",
|
|
177
|
+
reviewChoose: "Choose the chat that reviews the game: it opens in a window of its own, to keep open.",
|
|
173
178
|
restartHint: "Choose the model and effort, then press Claude Code or Codex: the chat starts again, clean, in the same window, once it has ended its answer. The game stays as it is.",
|
|
174
179
|
byHand: (name) => `You opened this chat yourself: change ${name}'s model with /model in its window, or close it and open one from here, with the choices below.`,
|
|
175
180
|
noChat: "No chat at the table: choose one",
|
|
@@ -494,7 +499,7 @@ const italian = {
|
|
|
494
499
|
saveMoves: "PGN",
|
|
495
500
|
saveMovesHint: "Solo le mosse, da aprire su Lichess o chess.com",
|
|
496
501
|
noGameNow: "Nessuna partita in corso",
|
|
497
|
-
analysing: "Analisi
|
|
502
|
+
analysing: "Analisi",
|
|
498
503
|
badges: {
|
|
499
504
|
brilliant: "Geniale",
|
|
500
505
|
great: "Ottima mossa",
|
|
@@ -522,6 +527,9 @@ const italian = {
|
|
|
522
527
|
mated: (side) => `Matto al ${side === "white" ? "Bianco" : "Nero"}`,
|
|
523
528
|
},
|
|
524
529
|
reviewing: (name) => `${name} sta analizzando la partita…`,
|
|
530
|
+
incomplete: "Analisi non completa",
|
|
531
|
+
complete: "Completa",
|
|
532
|
+
completeHint: "Completa l'analisi",
|
|
525
533
|
you: "Tu",
|
|
526
534
|
ai: "AI",
|
|
527
535
|
sides: { white: "Bianco", black: "Nero" },
|
|
@@ -577,6 +585,8 @@ const italian = {
|
|
|
577
585
|
table: {
|
|
578
586
|
title: "Contro chi giochi?",
|
|
579
587
|
choose: "Scegli la chat con cui giocare: parte in una finestra a parte, da tenere aperta.",
|
|
588
|
+
reviewTitle: "Chi analizza la partita?",
|
|
589
|
+
reviewChoose: "Scegli la chat che analizza la partita: parte in una finestra a parte, da tenere aperta.",
|
|
580
590
|
restartHint: "Scegli modello e ragionamento, poi premi Claude Code o Codex: la chat riparte pulita nella stessa finestra, appena ha finito di rispondere. La partita resta com'è.",
|
|
581
591
|
byHand: (name) => `Questa chat l'hai aperta tu: cambia il modello di ${name} con /model nella sua finestra, oppure chiudila e aprine una da qui, con le scelte qui sotto.`,
|
|
582
592
|
noChat: "Nessuna chat al tavolo: scegline una",
|
package/dist/server/bell/bell.js
CHANGED
|
@@ -6,6 +6,7 @@ import { get } from "node:http";
|
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { writeAtomically } from "../../games/index.js";
|
|
8
8
|
import { dataFolder, tableFolder } from "../../home/index.js";
|
|
9
|
+
import { analystAgent } from "../../host-files/index.js";
|
|
9
10
|
import { version } from "../../version.js";
|
|
10
11
|
/** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
|
|
11
12
|
export const bellLine = "The player acted on the board page: call status to see it.";
|
|
@@ -171,7 +172,8 @@ export async function bellHeard(input, ports) {
|
|
|
171
172
|
const pages = ours.length > 1 ? `${ours.length} pages of it, none of this chat's session, ${open} open in a browser` : "no page of it";
|
|
172
173
|
return { folder, rang: false, why: `${pages} at ports ${tried.join(", ")}${others.length > 0 ? `; ${others.join(", ")}` : ""}` };
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
+
// The page lets the analyst go as the hook starts waiting, when none runs any more.
|
|
176
|
+
const said = await body(port, hook.analystGone === true ? "/api/bell?analyst=gone" : "/api/bell");
|
|
175
177
|
return said === "ring" ? { folder, rang: true } : { folder, rang: false, why: said === undefined ? `the page at port ${port} did not answer` : `the page at port ${port} said "${said}"` };
|
|
176
178
|
}
|
|
177
179
|
// How long a port may take to say whose page it is: one of this computer answers at once.
|
|
@@ -244,12 +246,23 @@ function hookInput(input) {
|
|
|
244
246
|
return {
|
|
245
247
|
...(typeof parsed.session_id === "string" ? { session: parsed.session_id } : {}),
|
|
246
248
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
249
|
+
...(analystGone(parsed.background_tasks) ? { analystGone: true } : {}),
|
|
247
250
|
};
|
|
248
251
|
}
|
|
249
252
|
catch {
|
|
250
253
|
return {};
|
|
251
254
|
}
|
|
252
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Whether Claude Code's Stop hook says no analyst runs in the background: after it ends, is stopped or fails, the chat takes a turn, and its Stop hook
|
|
258
|
+
* lists the tasks still running (D-109, Priya's test). A host that lists none says nothing.
|
|
259
|
+
*/
|
|
260
|
+
function analystGone(tasks) {
|
|
261
|
+
return Array.isArray(tasks) && !tasks.some((task) => {
|
|
262
|
+
const { agent_type: agent, status } = (task ?? {});
|
|
263
|
+
return agent === analystAgent && status === "running";
|
|
264
|
+
});
|
|
265
|
+
}
|
|
253
266
|
/**
|
|
254
267
|
* The chat's page among those of its folder: the one of its session; else the only one, or the only one a browser has open, where the player plays. Codex
|
|
255
268
|
* gives the page no session, and a chat closed may leave its page behind for a while.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The table's end of the chat attached to it (D-104): the chat attaches and stays while its process lives, says when it calls a tool, sends a copy of what
|
|
2
2
|
// it saw, and asks for the New game window. One chat at a time, of this version alone; a page a chat serves itself has none of these routes.
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { forgetTheAi, noteAiHost, noteAiModel } from "../../games/index.js";
|
|
4
|
+
import { forgetAnalyst, forgetTheAi, noteAiHost, noteAiModel } from "../../games/index.js";
|
|
5
5
|
import { gameFolder } from "../../profiles/index.js";
|
|
6
6
|
import { version } from "../../version.js";
|
|
7
7
|
import { chatAttach, chatCopy } from "./chat.js";
|
|
@@ -67,10 +67,15 @@ function attach(body, response, served) {
|
|
|
67
67
|
});
|
|
68
68
|
tellWatchers(served.watchers);
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The next chat may play in another host, with another model: the AI's card shows its own from its first move (Federico). The analyst ended with its chat
|
|
72
|
+
* (D-109): the game it held is free for the next one.
|
|
73
|
+
*/
|
|
71
74
|
function forgetChat(served) {
|
|
72
75
|
try {
|
|
73
|
-
|
|
76
|
+
const folder = gameFolder(served.context.home());
|
|
77
|
+
forgetTheAi(folder);
|
|
78
|
+
forgetAnalyst(folder);
|
|
74
79
|
}
|
|
75
80
|
catch {
|
|
76
81
|
// A data folder that cannot be read keeps no notes to forget.
|
package/dist/server/page/game.js
CHANGED
|
@@ -356,9 +356,12 @@ function analysisShown(served) {
|
|
|
356
356
|
// started again since is gone (Priya). On the table the chat's start is in the chat's own folder.
|
|
357
357
|
const { bell } = served;
|
|
358
358
|
const chat = { ...served.context, folder: served.attached?.chat.folder ?? served.folder };
|
|
359
|
-
const
|
|
359
|
+
const reviewed = isReviewed(record);
|
|
360
|
+
const reviewing = !reviewed && (bell.inTurn || (bell.listening && analystOf(chat, analysis.game) !== undefined));
|
|
361
|
+
// Without a review and with nobody at it, the analysis is incomplete: the page offers to bring it back to the AI (D-109).
|
|
362
|
+
const state = reviewed ? {} : reviewing ? { reviewing: true } : { incomplete: true };
|
|
360
363
|
// How far the book goes, which the page cannot tell without the book's moves: its card names the opening on those moves.
|
|
361
|
-
return { name, pgn: writePgn(record), book: bookPlies(record), ...
|
|
364
|
+
return { name, pgn: writePgn(record), book: bookPlies(record), ...state };
|
|
362
365
|
}
|
|
363
366
|
catch {
|
|
364
367
|
// A game gone from the archive is no longer shown.
|
package/dist/server/page/page.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from "node:http";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { positionIn, samePosition } from "../../commands/index.js";
|
|
7
7
|
import { aiNameOf } from "../../core/index.js";
|
|
8
|
-
import { gameStamp, tableTalkStamp } from "../../games/index.js";
|
|
8
|
+
import { forgetAnalyst, gameStamp, tableTalkStamp } from "../../games/index.js";
|
|
9
9
|
import { outdatedHostFiles } from "../../host-files/index.js";
|
|
10
10
|
import { gameFolder } from "../../profiles/index.js";
|
|
11
11
|
import { Bell, bellProbe, pagePorts } from "../bell/index.js";
|
|
@@ -141,6 +141,9 @@ function answer(request, response, served) {
|
|
|
141
141
|
send(response, 200, json, JSON.stringify(bellProbe(served.folder, served.watchers.size > 0, tableChat(served))));
|
|
142
142
|
}
|
|
143
143
|
else {
|
|
144
|
+
if (url.searchParams.get("analyst") === "gone") {
|
|
145
|
+
forgetGoneAnalyst(served);
|
|
146
|
+
}
|
|
144
147
|
served.bell.listen(request, response);
|
|
145
148
|
}
|
|
146
149
|
}
|
|
@@ -148,6 +151,15 @@ function answer(request, response, served) {
|
|
|
148
151
|
sendFile(response, path);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
154
|
+
/** The chat's hook says no analyst runs any more (D-109): the game it held is free, and its review, unless finished, incomplete. */
|
|
155
|
+
function forgetGoneAnalyst(served) {
|
|
156
|
+
try {
|
|
157
|
+
forgetAnalyst(gameFolder(served.context.home()));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// A data folder that cannot be written keeps the analyst until half an hour has gone by.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
151
163
|
/** The chat at the table, as its probe says it: none on a page a chat serves itself. */
|
|
152
164
|
function tableChat(served) {
|
|
153
165
|
if (served.table === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|