chesspal 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/context.d.ts +2 -0
- package/dist/page/app/app.js +27 -11
- package/dist/page/dialogs/profiles.js +2 -2
- package/dist/page/layout/layout.css +0 -26
- package/dist/page/style.css +1 -1
- package/dist/page/table/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +13 -2
- package/dist/page/talk/talk.js +2 -2
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +52 -4
- package/dist/server/page/chat.d.ts +5 -0
- package/dist/server/page/chat.js +9 -1
- package/dist/server/page/page.d.ts +4 -0
- package/dist/server/page/page.js +7 -2
- package/dist/server/page/table.d.ts +34 -15
- package/dist/server/page/table.js +126 -18
- package/dist/server/table/hosts/chats.js +6 -3
- package/dist/server/table/hosts/play.d.ts +8 -4
- package/dist/server/table/hosts/play.js +36 -8
- package/dist/server/table/hosts/seat.d.ts +13 -5
- package/dist/server/table/hosts/seat.js +21 -15
- package/dist/server/table/link.js +1 -0
- package/package.json +1 -1
- package/dist/page/table/opener.css +0 -65
- package/dist/page/table/opener.js +0 -59
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
/** The play process in the seat: its process id and
|
|
1
|
+
/** The play process in the seat: its process id, its host, and the model and effort it started that host with, none for the host's own. */
|
|
2
2
|
export interface Seat {
|
|
3
3
|
readonly pid: number;
|
|
4
4
|
readonly host: string;
|
|
5
|
+
readonly model?: string | undefined;
|
|
6
|
+
readonly effort?: string | undefined;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
|
|
@@ -14,10 +16,16 @@ export declare function refreshSeat(folder: string, pid: number, now?: Date): vo
|
|
|
14
16
|
export declare function leaveSeat(folder: string, pid: number): void;
|
|
15
17
|
/** Whether a play process holds the seat. */
|
|
16
18
|
export declare function seatTaken(folder: string, now?: Date): boolean;
|
|
17
|
-
/**
|
|
18
|
-
export declare function
|
|
19
|
-
/**
|
|
20
|
-
export declare function
|
|
19
|
+
/** The play process that holds the seat, with the model and effort it started its host with; none when no one holds it. */
|
|
20
|
+
export declare function seatHeld(folder: string, now?: Date): Seat | undefined;
|
|
21
|
+
/** Asks the play process in the seat to start its host again, or `again.host` in its place: whether one holds it, and was asked. */
|
|
22
|
+
export declare function askAgain(folder: string, now?: Date, again?: Again): boolean;
|
|
23
|
+
/** What the table asks the play process in the seat: to start its host again, or the host named in its place (D-107). */
|
|
24
|
+
export interface Again {
|
|
25
|
+
readonly host?: string;
|
|
26
|
+
}
|
|
27
|
+
/** What the table asked this play process, when it asked it to start its host again; the request is taken. */
|
|
28
|
+
export declare function askedAgain(folder: string, pid: number): Again | undefined;
|
|
21
29
|
/** How often a play process refreshes its seat, and how long the seat stays held without it. */
|
|
22
30
|
export declare const seatTimes: {
|
|
23
31
|
refresh: number;
|
|
@@ -33,36 +33,42 @@ export function leaveSeat(folder, pid) {
|
|
|
33
33
|
export function seatTaken(folder, now = new Date()) {
|
|
34
34
|
return seatHeld(folder, now) !== undefined;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
export function
|
|
36
|
+
/** The play process that holds the seat, with the model and effort it started its host with; none when no one holds it. */
|
|
37
|
+
export function seatHeld(folder, now = new Date()) {
|
|
38
|
+
const seat = seatIn(folder);
|
|
39
|
+
// A file's time is finer than a Date's: it is compared to the millisecond, as the Date is.
|
|
40
|
+
const refreshed = statSync(join(folder, seatFile), { throwIfNoEntry: false })?.mtimeMs;
|
|
41
|
+
return seat !== undefined && refreshed !== undefined && now.getTime() - Math.floor(refreshed) < seatTimes.heldFor ? seat : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Asks the play process in the seat to start its host again, or `again.host` in its place: whether one holds it, and was asked. */
|
|
44
|
+
export function askAgain(folder, now = new Date(), again = {}) {
|
|
38
45
|
const held = seatHeld(folder, now);
|
|
39
46
|
if (held === undefined) {
|
|
40
47
|
return false;
|
|
41
48
|
}
|
|
42
|
-
writeAtomically(join(folder, againFile), `${JSON.stringify({ pid: held.pid })}\n`);
|
|
49
|
+
writeAtomically(join(folder, againFile), `${JSON.stringify({ pid: held.pid, ...again })}\n`);
|
|
43
50
|
return true;
|
|
44
51
|
}
|
|
45
|
-
/**
|
|
52
|
+
/** What the table asked this play process, when it asked it to start its host again; the request is taken. */
|
|
46
53
|
export function askedAgain(folder, pid) {
|
|
47
|
-
const asked = jsonIn(join(folder, againFile))
|
|
48
|
-
if (asked) {
|
|
49
|
-
|
|
54
|
+
const asked = jsonIn(join(folder, againFile));
|
|
55
|
+
if (asked?.pid !== pid) {
|
|
56
|
+
return undefined;
|
|
50
57
|
}
|
|
51
|
-
|
|
58
|
+
rmSync(join(folder, againFile), { force: true });
|
|
59
|
+
return typeof asked.host === "string" ? { host: asked.host } : {};
|
|
52
60
|
}
|
|
53
61
|
/** How often a play process refreshes its seat, and how long the seat stays held without it. */
|
|
54
62
|
export const seatTimes = { refresh: 5_000, heldFor: 15_000 };
|
|
55
63
|
const seatFile = "play.json";
|
|
56
64
|
const againFile = "play-again.json";
|
|
57
|
-
function seatHeld(folder, now) {
|
|
58
|
-
const seat = seatIn(folder);
|
|
59
|
-
// A file's time is finer than a Date's: it is compared to the millisecond, as the Date is.
|
|
60
|
-
const refreshed = statSync(join(folder, seatFile), { throwIfNoEntry: false })?.mtimeMs;
|
|
61
|
-
return seat !== undefined && refreshed !== undefined && now.getTime() - Math.floor(refreshed) < seatTimes.heldFor ? seat : undefined;
|
|
62
|
-
}
|
|
63
65
|
function seatIn(folder) {
|
|
64
66
|
const seat = jsonIn(join(folder, seatFile));
|
|
65
|
-
|
|
67
|
+
if (typeof seat?.pid !== "number" || typeof seat.host !== "string") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
71
|
+
return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort) };
|
|
66
72
|
}
|
|
67
73
|
function jsonIn(path) {
|
|
68
74
|
try {
|
|
@@ -214,6 +214,7 @@ function seenBy(context) {
|
|
|
214
214
|
return {
|
|
215
215
|
seen: context.seen,
|
|
216
216
|
positionSeen: context.positionSeen,
|
|
217
|
+
positionFirstSeen: context.positionFirstSeen,
|
|
217
218
|
given: [...context.given],
|
|
218
219
|
host: context.host?.(),
|
|
219
220
|
aiModel: folder === undefined ? undefined : aiModelOf(folder),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/* The table with no chat at it (D-104): the asleep note's box, with a row for each host, its button and its choices, and the command to type instead */
|
|
2
|
-
.opener {
|
|
3
|
-
display: grid;
|
|
4
|
-
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
5
|
-
gap: 8px 6px;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.opener > p {
|
|
9
|
-
grid-column: 1 / -1;
|
|
10
|
-
margin: 0;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* One row a host, its columns shared with the other's through the subgrid: the choices as wide as their longest name, the button takes the rest, all as tall */
|
|
14
|
-
.opener-host {
|
|
15
|
-
display: grid;
|
|
16
|
-
grid-column: 1 / -1;
|
|
17
|
-
grid-template-columns: subgrid;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.opener-host > .command {
|
|
21
|
-
justify-content: center;
|
|
22
|
-
padding: 6px 8px;
|
|
23
|
-
overflow: hidden;
|
|
24
|
-
white-space: nowrap;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.opener-host > .command:disabled,
|
|
28
|
-
.opener-host > .command:disabled:hover {
|
|
29
|
-
cursor: default;
|
|
30
|
-
opacity: 0.5;
|
|
31
|
-
background: var(--accent);
|
|
32
|
-
border-color: var(--accent);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.opener-host select {
|
|
36
|
-
padding: 4px;
|
|
37
|
-
border: 1px solid var(--line);
|
|
38
|
-
border-radius: 6px;
|
|
39
|
-
background: var(--card);
|
|
40
|
-
color: var(--ink);
|
|
41
|
-
font: inherit;
|
|
42
|
-
font-size: 13px;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Empty, the state takes no room: its margin gives back the row's gap */
|
|
46
|
-
.opener-state:empty {
|
|
47
|
-
margin-bottom: -8px;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.opener-command {
|
|
51
|
-
color: var(--soft);
|
|
52
|
-
font-size: 12.5px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Each command whole on its line; two families, so the browser does not shrink the monospace */
|
|
56
|
-
.opener-command code {
|
|
57
|
-
font-family: ui-monospace, monospace;
|
|
58
|
-
white-space: nowrap;
|
|
59
|
-
user-select: all;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* Over the board, where the note lets clicks through to the squares, only its buttons, choices and commands take them */
|
|
63
|
-
.over-board > .opener :is(button, select, code) {
|
|
64
|
-
pointer-events: auto;
|
|
65
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
-
// The table with no chat at it (D-104): a button for each host installed, which opens a terminal with a chat at the table, the model and effort it plays
|
|
3
|
-
// with beside it, kept in the profile, and the command to type in a terminal instead.
|
|
4
|
-
import { useState } from "preact/hooks";
|
|
5
|
-
/** Where the asleep note stands while no chat is at the table: the hosts that open one, their choices, and the command to type. */
|
|
6
|
-
export function ChatOpener({ table, settings, onChange, name, text }) {
|
|
7
|
-
const [sending, setSending] = useState(false);
|
|
8
|
-
const [failed, setFailed] = useState(false);
|
|
9
|
-
const words = text.table;
|
|
10
|
-
const installed = table.hosts.filter((host) => host.installed && host.name in choiceKeys);
|
|
11
|
-
const busy = sending || table.opening !== undefined;
|
|
12
|
-
const open = async (host) => {
|
|
13
|
-
setSending(true);
|
|
14
|
-
setFailed(false);
|
|
15
|
-
const opened = await openChat(host);
|
|
16
|
-
setSending(false);
|
|
17
|
-
setFailed(!opened);
|
|
18
|
-
};
|
|
19
|
-
// The chat started again for a new game needs nothing of the player.
|
|
20
|
-
if (table.opening === "again") {
|
|
21
|
-
return _jsx("p", { class: "asleep", role: "status", children: words.again(name) });
|
|
22
|
-
}
|
|
23
|
-
if (installed.length === 0) {
|
|
24
|
-
return _jsx("p", { class: "asleep", role: "status", children: words.install(name) });
|
|
25
|
-
}
|
|
26
|
-
// The state stays in the page, empty when there is none, so a screen reader hears it change.
|
|
27
|
-
return (_jsxs("section", { class: "asleep opener", "aria-label": words.label, children: [_jsx("p", { children: words.noChat(name) }), installed.map((host) => (_jsxs("div", { class: "opener-host", children: [_jsx("button", { type: "button", class: "command primary", disabled: busy, onClick: () => void open(host.name), children: hostTitles[host.name] ?? host.name }), _jsx(Choice, { host: host, kind: "model", settings: settings, onChange: onChange, text: text }), _jsx(Choice, { host: host, kind: "effort", settings: settings, onChange: onChange, text: text })] }, host.name))), _jsx("p", { class: "opener-state", role: "status", children: busy ? words.opening : failed ? words.failed : null }), _jsxs("p", { class: "opener-command", children: [words.typeIt, " ", installed.map((host, index) => (_jsxs("span", { children: [index === 0 ? "" : " · ", _jsx("code", { children: host.command })] }, host.name)))] })] }));
|
|
28
|
-
}
|
|
29
|
-
const hostTitles = { claude: "Claude Code", codex: "Codex" };
|
|
30
|
-
// The settings that keep each host's choice.
|
|
31
|
-
const choiceKeys = {
|
|
32
|
-
claude: { model: "claudeModel", effort: "claudeEffort" },
|
|
33
|
-
codex: { model: "codexModel", effort: "codexEffort" },
|
|
34
|
-
};
|
|
35
|
-
/** A host's model or effort: a dash keeps the host's own; a choice it no longer offers shows as the dash. Its title says what it stands at, the dash too. */
|
|
36
|
-
function Choice({ host, kind, settings, onChange, text }) {
|
|
37
|
-
const key = choiceKeys[host.name][kind];
|
|
38
|
-
const offered = kind === "model" ? host.models : host.efforts;
|
|
39
|
-
const chosen = offered.includes(settings[key]) ? settings[key] : "";
|
|
40
|
-
const title = hostTitles[host.name] ?? host.name;
|
|
41
|
-
if (offered.length === 0) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
return (_jsxs("select", { "aria-label": `${text.table[kind]}, ${title}`, title: `${text.table[kind]}: ${chosen === "" ? text.table.asHost(title) : chosen}`, value: chosen, onChange: (event) => onChange({ ...settings, [key]: event.currentTarget.value }), children: [_jsx("option", { value: "", children: "\u2014" }), offered.map((value) => _jsx("option", { value: value, children: shortName(value) }, value))] }));
|
|
45
|
-
}
|
|
46
|
-
// Codex's models without the "gpt-" they all start with, so the choice fits beside the button in the column; the title keeps the whole name.
|
|
47
|
-
function shortName(value) {
|
|
48
|
-
return value.replace(/^gpt-/, "");
|
|
49
|
-
}
|
|
50
|
-
/** Asks the table to open a terminal with a chat in the host: whether it did. */
|
|
51
|
-
async function openChat(host) {
|
|
52
|
-
try {
|
|
53
|
-
const response = await fetch("/api/table/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ host }) });
|
|
54
|
-
return response.ok && (await response.json()).opened === true;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|