clavarium 0.2.366-next.1 → 0.2.367-next.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/api.mjs +1 -0
- package/server/rooms.mjs +4 -3
- package/server/settingswords.mjs +12 -6
- package/web/app.js +200 -67
- package/web/index.html +39 -34
- package/web/kit.js +75 -3
- package/web/style.css +52 -29
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.367-next.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.367-next.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/package.json
CHANGED
package/server/api.mjs
CHANGED
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
* GET /api/rooms the rooms (#268): [{id, name, host, members: [{id, kind, name, ref, host?}], unread, last, thinking: [names]}]
|
|
95
95
|
* POST /api/rooms {name, conversations: [names], boxes: [ids], host?}: a new room
|
|
96
96
|
* POST /api/rooms/<id> {name?, host?, add?: {conversations, boxes}, remove?: [member ids]}
|
|
97
|
+
* quiet?: true/false (muted in the list: no dot, at the bottom)
|
|
97
98
|
* POST /api/rooms/<id>/remove the room and its thread
|
|
98
99
|
* POST /api/rooms/<id>/give {asset, rights, shared?}: the same right given to each AI of it, its own
|
|
99
100
|
* conversation's -> {asset, rights, given: [{conversation, ok, kept?, error?}]}
|
package/server/rooms.mjs
CHANGED
|
@@ -100,7 +100,7 @@ export class Rooms {
|
|
|
100
100
|
in its row of the list and on its bar, « Hugo réfléchit… » in its thread. */
|
|
101
101
|
#thinkingOf(id, members) { return [...(this.#thinking.get(id) ?? new Map()).entries()].filter(([, v]) => Date.now() - v.at < THINKING_MS).map(([mid]) => members.find((m) => m.id === mid)?.name).filter(Boolean); }
|
|
102
102
|
#doingOf(id, members) { return [...(this.#thinking.get(id) ?? new Map()).entries()].filter(([, v]) => v.text && Date.now() - v.at < THINKING_MS).map(([mid, v]) => ({ name: members.find((m) => m.id === mid)?.name, text: v.text })).filter((x) => x.name); }
|
|
103
|
-
list() { return this.#all().map((r) => { const members = this.#shown(r); return { id: r.id, name: r.name, host: r.host, members, at: r.at, unread: this.#unread(r), last: this.#last(r), thinking: this.#thinkingOf(r.id, members) }; }); }
|
|
103
|
+
list() { return this.#all().map((r) => { const members = this.#shown(r); return { id: r.id, name: r.name, host: r.host, members, at: r.at, unread: this.#unread(r), last: this.#last(r), thinking: this.#thinkingOf(r.id, members), ...(r.quiet ? { quiet: true } : {}) }; }); }
|
|
104
104
|
/* ITS AIs, as conversations (06-11): the names of the conversations of its AI members that are there now --
|
|
105
105
|
never a box, never the person, never one taken out of the room. What « give to all its AIs » gives to. */
|
|
106
106
|
ais(id) { return this.#shown(this.#find(id)).filter((m) => m.kind === 'ai').map((m) => m.ref); }
|
|
@@ -139,10 +139,11 @@ export class Rooms {
|
|
|
139
139
|
if (new Set(names).size !== names.length) throw new Refusal(409, 'Two members of a room cannot have the same name: rename one first.');
|
|
140
140
|
if (room.members.length > MEMBERS_MAX) throw new Refusal(409, `${MEMBERS_MAX} members at most.`);
|
|
141
141
|
}
|
|
142
|
-
/* Its name, its host, members added or taken away. */
|
|
143
|
-
change(id, { name, host, add = {}, remove = [] } = {}) {
|
|
142
|
+
/* Its name, its host, members added or taken away; `quiet` (07-11, the tidy-up): muted in the list, or not. */
|
|
143
|
+
change(id, { name, host, add = {}, remove = [], quiet } = {}) {
|
|
144
144
|
const list = this.#all(); const room = list.find((r) => r.id === id);
|
|
145
145
|
if (!room) throw new Refusal(404, 'No such room.');
|
|
146
|
+
if (quiet !== undefined) { if (quiet === true) room.quiet = true; else delete room.quiet; }
|
|
146
147
|
if (name !== undefined) {
|
|
147
148
|
const n = nameOf(name); if (!n) throw new Refusal(400, 'Give the room a name.');
|
|
148
149
|
if (list.some((r) => r.id !== id && r.name.toLowerCase() === n.toLowerCase())) throw new Refusal(409, 'A room has that name already.');
|
package/server/settingswords.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export const SETTINGS_WORDS_FR = {
|
|
|
33
33
|
'set-dict-backup': 'Secours en service', 'set-dict-expired': 'Clé expirée', 'set-dict-with-backup': 'avec secours', 'set-dict-server': 'réglée sur le serveur',
|
|
34
34
|
'set-attached': '{n} joints', 'set-secret': '1 secret', 'set-secrets': '{n} secrets', 'set-offline': 'hors ligne',
|
|
35
35
|
'set-device': '1 appareil', 'set-devices': '{n} appareils', 'set-no-device': 'Aucun appareil', 'set-chat': 'Chat',
|
|
36
|
-
'set-wakeups-on': '{on} sur {n} actifs', 'set-asleep': '
|
|
36
|
+
'set-wakeups-on': '{on} sur {n} actifs', 'set-asleep': 'En veille', 'set-not-looked': 'Pas encore regardée',
|
|
37
37
|
'set-running-well': 'En marche · rien d\'anormal', 'set-conv-advanced': 'Archiver · remettre à neuf · supprimer',
|
|
38
38
|
/* New asset, one box (05-11, step 3): one verb per gesture -- Créer, Attacher, Terminé. */
|
|
39
39
|
'set-na-new': 'Nouvel asset', 'set-na-attach': 'Attacher', 'set-na-there': 'Déjà là', 'set-na-or-new': 'ou un nouveau',
|
|
@@ -109,11 +109,15 @@ export const SETTINGS_WORDS_FR = {
|
|
|
109
109
|
'set-list-not-filed': 'Sans groupe', 'set-list-asks': 'vous demande', 'set-list-waits': 'vous attend',
|
|
110
110
|
'set-list-asks-for': 'le répondeur vous demande', 'set-list-answered': 'a répondu à votre place',
|
|
111
111
|
'set-list-wakeup': '1 réveil', 'set-list-wakeups': '{n} réveils', 'set-list-hidden': 'cachée',
|
|
112
|
-
'set-list-asleep': '
|
|
112
|
+
'set-list-asleep': 'en veille', 'set-list-starting': 'démarre', 'set-list-restarting': 'redémarre',
|
|
113
113
|
'set-list-moving': 'déplace son travail', 'set-list-failed': 'en échec', 'set-list-you': 'Vous : {text}',
|
|
114
114
|
'set-list-fewer': 'En montrer moins', 'set-list-more': 'En montrer {n} de plus',
|
|
115
115
|
'set-list-hide-hidden': 'Masquer les {n} cachées', 'set-list-n-hidden': '{n} cachées',
|
|
116
116
|
'set-list-tip': 'Rangez-les en groupes : Réglages, Groupe.',
|
|
117
|
+
'set-idle-h': 'rien depuis {n} h', 'set-idle-d': 'rien depuis {n} j', 'set-idle-w': 'rien depuis {n} sem.', 'set-idle-mo': 'rien depuis {n} mois',
|
|
118
|
+
'set-tidy-open': 'Faire le ménage ({n})', 'set-tidy-title': 'Mettre en veille ?', 'set-tidy-hint': 'Rien depuis 12 h ou plus.',
|
|
119
|
+
'set-tidy-go': 'Mettre en veille ({n})', 'set-close': 'Fermer',
|
|
120
|
+
'set-asleep-page': 'En veille. Elle reprend quand vous la réveillez.', 'set-wake-it': 'Réveiller',
|
|
117
121
|
'set-as-k-git': 'Dépôts git', 'set-as-k-folder': 'Dossiers', 'set-as-k-mailbox': 'Boîtes mail', 'set-as-k-agenda': 'Agendas',
|
|
118
122
|
'set-as-k-contacts': 'Contacts', 'set-as-k-secret': 'Secrets', 'set-as-k-list': 'Classeurs', 'set-as-k-acces': 'Accès',
|
|
119
123
|
'set-as-kind-folder': 'dossier', 'set-as-kind-git': 'dépôt', 'set-as-kind-mailbox': 'boîte mail',
|
|
@@ -134,12 +138,14 @@ export const SETTINGS_WORDS_FR = {
|
|
|
134
138
|
'set-files-read-only': 'Lecture seule : rien ne peut y être changé.',
|
|
135
139
|
'set-this-conv': 'Cette conversation · {name}', 'set-ask-first': 'Demander avant d\'agir',
|
|
136
140
|
'set-online': 'En ligne', 'set-offline-btn': 'Hors ligne', 'set-sleep': 'Endormir',
|
|
137
|
-
'set-room-name': 'Nom', 'set-room-
|
|
138
|
-
'set-room-
|
|
139
|
-
'set-room-in': '{n} dans le salon', 'set-room-find': 'Chercher une conversation', 'set-room-boxes': 'Boîtiers vocaux',
|
|
141
|
+
'set-room-name': 'Nom', 'set-room-welcomes': 'Accueille', 'set-room-ais': 'Ses IA',
|
|
142
|
+
'set-room-find': 'Chercher une conversation', 'set-room-boxes': 'Boîtiers vocaux',
|
|
140
143
|
'set-room-remove': 'Supprimer le salon', 'set-room-remove-what': 'Le fil part · les IA et boîtiers restent',
|
|
141
144
|
'set-room-remove-dots': 'Supprimer…', 'set-room-remove-yes': 'Supprimer {name}', 'set-room-keep': 'Garder',
|
|
142
|
-
'set-room-
|
|
145
|
+
'set-room-create': 'Créer', 'set-room-new': 'Nouveau salon',
|
|
146
|
+
/* A room's settings as every settings page (07-11): each change saved at once, said a moment in the bar. */
|
|
147
|
+
'set-room-saved': 'Enregistré', 'set-room-settings-of': '{name} · Réglages', 'set-room-of': '{n} sur {t}',
|
|
148
|
+
'set-room-host-how': 'Touchez une autre IA du salon pour la déplacer',
|
|
143
149
|
'set-room-people': 'Personnes', 'set-room-ais-of': 'IA', 'set-room-boxes-of': 'Boîtiers', 'set-room-host-mark': 'accueille',
|
|
144
150
|
'set-room-n-person': '1 personne', 'set-room-n-people': '{n} personnes', 'set-room-n-ai': '1 IA', 'set-room-n-ais': '{n} IA',
|
|
145
151
|
'set-room-n-box': '1 boîtier', 'set-room-n-boxes': '{n} boîtiers',
|
package/web/app.js
CHANGED
|
@@ -433,15 +433,16 @@
|
|
|
433
433
|
? KIT.TopBar({ variant: 'main', title: $('name'), actions: [...old.children].filter((n) => n.id !== 'name') })
|
|
434
434
|
: KIT.TopBar({ variant: 'main', fold: $('fold'), back: { element: $('back'), label: 'Conversations' }, state: $('state'), title: $('name'), actions: [$('views')] }));
|
|
435
435
|
}
|
|
436
|
-
/* A room (the device page has none): its dot, its name, under it who speaks now
|
|
437
|
-
in
|
|
436
|
+
/* A room (the device page has none): its dot, its name, under it who speaks now (only then); on its right who is
|
|
437
|
+
in it (07-11: small stacked avatars, a click opens the list) then its settings, in the same group of views a
|
|
438
|
+
conversation's settings sit in; one line, as every bar; its way back, "‹", as a conversation's (kit: 'main'). */
|
|
438
439
|
if (document.getElementById('salon')) {
|
|
439
440
|
const top = $('salon').querySelector(':scope > .panel-top');
|
|
440
441
|
const t = $('salon-title'); const name = document.createElement('span'); name.id = 'salon-name'; name.textContent = t.textContent; t.replaceChildren(name);
|
|
441
442
|
const live = document.createElement('small'); live.id = 'salon-live'; live.className = 'salon-live'; live.hidden = true;
|
|
442
443
|
const views = document.createElement('div'); views.className = 'views'; views.setAttribute('role', 'group'); views.setAttribute('aria-label', 'Room'); views.append($('salon-settings'));
|
|
443
444
|
const strip = $('salon').querySelector('.salon-bar');
|
|
444
|
-
topBar(top, { variant: 'main', state: { id: 'salon-state' }, sub: [live, $('salon-who')
|
|
445
|
+
topBar(top, { variant: 'main', state: { id: 'salon-state' }, sub: [live], actions: [$('salon-who'), views] });
|
|
445
446
|
strip?.remove();
|
|
446
447
|
}
|
|
447
448
|
for (const top of [...document.querySelectorAll('#places .sheet > .panel-top')]) topBar(top, top.parentElement.id === 'salon-new' ? { state: { id: 'salon-new-state' } } : {});
|
|
@@ -1068,7 +1069,7 @@
|
|
|
1068
1069
|
/* Given its chat and not its terminal: the chat is the whole page, and no terminal is ever opened. */
|
|
1069
1070
|
if (noTerminal()) { chatAlone(); return; }
|
|
1070
1071
|
/* Asleep because somebody asked: it stays asleep until somebody asks. */
|
|
1071
|
-
if (c && c.asleep === true && !socket) { say('asleep', 'waiting'); show(
|
|
1072
|
+
if (c && c.asleep === true && !socket) { say('asleep', 'waiting'); show(`${uiSaid('set-asleep-page', 'Asleep. It wakes only when you wake it.')}${c.sizeAtWake ? ` When it wakes, its machine is restarted with its new size (${laterWords(c.sizeAtWake)}), and the tool takes up its session.` : ''}`, uiSaid('set-wake-it', 'Wake it'), connect); mascot('asleep'); return; }
|
|
1072
1073
|
if (!socket) { terminal.reset(); connect(); }
|
|
1073
1074
|
void watchSignIn();
|
|
1074
1075
|
}
|
|
@@ -1271,7 +1272,7 @@
|
|
|
1271
1272
|
/* Under way: the step the panel shows, in its words (the tool named once). */
|
|
1272
1273
|
const under = c.progress && ['building', 'restarting', 'ready'].includes(c.phase) ? stepNow(c.progress, c.toolLabel) : '';
|
|
1273
1274
|
/* What it is doing, when it says something: its tool is not named on every row. */
|
|
1274
|
-
const now = under || (holds(c) && c.phase !== 'building' ? (c.progress.kind === 'build' ? uiSaid('set-list-starting', 'starting') : uiSaid('set-list-restarting', 'restarting')) : c.phase === 'ready' ? (c.asleep ? uiSaid('set-list-asleep', 'asleep
|
|
1275
|
+
const now = under || (holds(c) && c.phase !== 'building' ? (c.progress.kind === 'build' ? uiSaid('set-list-starting', 'starting') : uiSaid('set-list-restarting', 'restarting')) : c.phase === 'ready' ? (c.asleep ? uiSaid('set-list-asleep', 'asleep') : '') : c.phase === 'building' ? (c.step ?? 'building') : c.phase === 'moving' ? uiSaid('set-list-moving', 'moving its work') : c.phase === 'restarting' ? uiSaid('set-list-restarting', 'restarting') : uiSaid('set-list-failed', 'failed'));
|
|
1275
1276
|
/* The line of its tool only when it says something: under way, asleep, failed -- not "Claude Code" again and again. */
|
|
1276
1277
|
const plain = c.phase === 'ready' && !c.asleep && !under && !holds(c);
|
|
1277
1278
|
const chips = [];
|
|
@@ -1288,9 +1289,15 @@
|
|
|
1288
1289
|
for (const b of boxesNow()) if (b.conversation === c.name) chips.push({ text: b.name, icon: 'box', wide: true });
|
|
1289
1290
|
/* Its last message. The agent is the usual speaker, so its name is left off (just the words); only
|
|
1290
1291
|
the person's own last word is marked "You:", which tells you it is your move. */
|
|
1291
|
-
|
|
1292
|
+
let last = told && c.talk?.lastText ? (c.talk.lastFrom === 'person' ? uiSaid('set-list-you', 'You: {text}', { text: c.talk.lastText }) : c.talk.lastText) : '';
|
|
1292
1293
|
const state = rollup(c);
|
|
1293
|
-
|
|
1294
|
+
/* IDLE 12 H OR MORE (07-11): « rien depuis 14 h », in grey, in place of its last words; never on one that
|
|
1295
|
+
waits for you. Asleep: after « asleep ». */
|
|
1296
|
+
const idle = c.phase === 'ready' && !under && !holds(c) ? KIT.idleFor(activityOf(c), { waits: needsYou(c) }) : 0;
|
|
1297
|
+
let said = plain ? '' : now;
|
|
1298
|
+
if (idle && c.asleep) said = `${said} \u00b7 ${KIT.idleWords(idle)}`;
|
|
1299
|
+
else if (idle && !said) { said = KIT.idleWords(idle); last = ''; }
|
|
1300
|
+
return { title: shownName(c), state, stateLabel: STATE_WORDS[state], avatar: { name: shownName(c) }, when: told ? whenWords(activityOf(c)) : '', last, now: said, idle: Boolean(idle && !c.asleep && said), who: agentWho(c), chips };
|
|
1294
1301
|
}
|
|
1295
1302
|
/* `copy`: a row of Needs you or Recent, beside its place in its group: it is not moved by hand.
|
|
1296
1303
|
`told`: it says what was said last, and when (a copy always; the one list when there is no group). */
|
|
@@ -1336,9 +1343,18 @@
|
|
|
1336
1343
|
try { roomsOnly = localStorage.getItem('clavarium-list-show') === 'rooms'; } catch { /* a private window */ }
|
|
1337
1344
|
/* A ROOM, drawn as a conversation (kit.js ConvCard): its sign for an initial, its last words or who speaks,
|
|
1338
1345
|
when; its point: new words waiting (the colour of what waits for you), else at work while one speaks, else fine. */
|
|
1346
|
+
/* A ROOM MUTED by a tidy-up (07-11): no point, at the bottom of the rooms -- until new words come, or it is opened. */
|
|
1347
|
+
const roomMuted = (r) => r.quiet === true && !(r.unread > 0) && thinkingOf(r).length === 0;
|
|
1348
|
+
const roomAt = (r) => Date.parse(r.last?.at ?? r.at ?? '') || 0;
|
|
1339
1349
|
function roomModel(r) {
|
|
1340
1350
|
const who = thinkingOf(r);
|
|
1341
1351
|
const sign = document.createElement('span'); sign.className = 'room-sign';
|
|
1352
|
+
const muted = roomMuted(r);
|
|
1353
|
+
const idle = KIT.idleFor(roomAt(r), { waits: r.unread > 0 || who.length > 0 });
|
|
1354
|
+
if (muted || idle) {
|
|
1355
|
+
const words = [muted ? uiSaid('set-list-asleep', 'asleep') : '', idle ? KIT.idleWords(idle) : ''].filter(Boolean).join(' \u00b7 ');
|
|
1356
|
+
return { title: r.name, state: muted ? 'sleep' : 'ok', stateLabel: muted ? STATE_WORDS.sleep : 'Room', avatar: { name: r.name, glyph: sign }, last: '', when: r.last ? whenWords(Date.parse(r.last.at)) : '', now: words, idle: !muted, chips: [] };
|
|
1357
|
+
}
|
|
1342
1358
|
const state = r.unread > 0 ? 'wait' : who.length ? 'work' : 'ok';
|
|
1343
1359
|
const chips = [];
|
|
1344
1360
|
if (r.unread > 0) chips.push({ text: r.unread > 99 ? '99+' : String(r.unread), tone: 'count', title: `${r.unread} new` });
|
|
@@ -1356,9 +1372,14 @@
|
|
|
1356
1372
|
const b = KIT.ConvCard(roomModel(r), { size: 'row', cls: 'item room-item' }); b.dataset.room = r.id;
|
|
1357
1373
|
/* One of its AIs speaking now: said instead of its last message (a conversation at work: its dot, gold). */
|
|
1358
1374
|
if (thinkingOf(r).length) b.dataset.live = 'true';
|
|
1375
|
+
if (roomMuted(r)) b.dataset.asleep = 'true';
|
|
1359
1376
|
if (r.last) b.dataset.when = 'true';
|
|
1360
1377
|
if (r.id === roomOpenNow()) b.setAttribute('aria-current', 'true');
|
|
1361
|
-
b.addEventListener('click', () => {
|
|
1378
|
+
b.addEventListener('click', () => {
|
|
1379
|
+
/* Opened: a muted room is a room again (it has no machine to wake). */
|
|
1380
|
+
if (r.quiet) { r.quiet = false; void post(`${API}/rooms/${r.id}`, { quiet: false }).catch(() => {}); }
|
|
1381
|
+
void openSalon(r.id, true, { step: true });
|
|
1382
|
+
});
|
|
1362
1383
|
return b;
|
|
1363
1384
|
}
|
|
1364
1385
|
/* What was just unfolded comes in gently, once (the next paints show it as it is). */
|
|
@@ -1413,6 +1434,66 @@
|
|
|
1413
1434
|
paint();
|
|
1414
1435
|
});
|
|
1415
1436
|
}
|
|
1437
|
+
/* WHAT A TIDY-UP OFFERS: the conversations and rooms idle 12 h or more, neither waiting for you nor asleep
|
|
1438
|
+
already, nor hidden, nor under way; the longest idle first (kit.js tidyList). */
|
|
1439
|
+
const tidyNow = () => KIT.tidyList([
|
|
1440
|
+
...conversations.filter((c) => c.phase === 'ready' && !holds(c)).map((c) => ({ kind: 'conversation', id: c.name, name: shownName(c), at: activityOf(c), waits: needsYou(c), asleep: c.asleep === true, hidden: c.hidden === true })),
|
|
1441
|
+
...roomsNow().map((r) => ({ kind: 'room', id: r.id, name: r.name, at: roomAt(r), waits: r.unread > 0 || thinkingOf(r).length > 0, asleep: r.quiet === true })),
|
|
1442
|
+
]);
|
|
1443
|
+
/* « Mettre en veille ? » (07-11): one question, the list all ticked (untick to keep one), one button. A
|
|
1444
|
+
conversation is put to sleep (its machine too: it wakes only when you open it and touch « Wake it »);
|
|
1445
|
+
a room is muted in the list. No undo: a machine asleep takes a while to come back. */
|
|
1446
|
+
function openTidy() {
|
|
1447
|
+
document.getElementById('tidy')?.remove();
|
|
1448
|
+
const list = tidyNow(); if (!list.length) return;
|
|
1449
|
+
const wrap = document.createElement('div'); wrap.className = 'lk-sheet-wrap tidy-wrap'; wrap.id = 'tidy';
|
|
1450
|
+
const box = document.createElement('div'); box.className = 'lk-sheet tidy'; box.setAttribute('role', 'dialog'); box.setAttribute('aria-modal', 'true'); box.setAttribute('aria-labelledby', 'tidy-title');
|
|
1451
|
+
const top = document.createElement('div'); top.className = 'lk-sheet-top';
|
|
1452
|
+
const title = document.createElement('b'); title.id = 'tidy-title'; title.className = 'lk-tx'; title.textContent = uiSaid('set-tidy-title', 'Put to sleep?');
|
|
1453
|
+
const x = document.createElement('button'); x.type = 'button'; x.className = 'icon-btn'; x.textContent = '\u00d7'; x.setAttribute('aria-label', uiSaid('set-close', 'Close'));
|
|
1454
|
+
top.append(title, x);
|
|
1455
|
+
const hint = document.createElement('p'); hint.className = 'hint'; hint.textContent = uiSaid('set-tidy-hint', 'Nothing for 12 h or more.');
|
|
1456
|
+
const rows = document.createElement('div'); rows.className = 'tidy-list';
|
|
1457
|
+
const go = document.createElement('button'); go.type = 'button'; go.className = 'primary tidy-go';
|
|
1458
|
+
const said = document.createElement('p'); said.className = 'hint tidy-said'; said.hidden = true;
|
|
1459
|
+
const boxes = list.map((t) => {
|
|
1460
|
+
const row = document.createElement('label'); row.className = 'tidy-row'; row.dataset.kind = t.kind; row.dataset.id = t.id;
|
|
1461
|
+
const tick = document.createElement('input'); tick.type = 'checkbox'; tick.checked = true;
|
|
1462
|
+
const name = document.createElement('span'); name.className = 'tidy-name';
|
|
1463
|
+
name.append(Object.assign(document.createElement('b'), { textContent: t.name }), document.createTextNode(` \u2014 ${KIT.idleWords(t.idle)}`));
|
|
1464
|
+
row.append(tick, name); rows.append(row);
|
|
1465
|
+
tick.addEventListener('change', count);
|
|
1466
|
+
return { t, tick };
|
|
1467
|
+
});
|
|
1468
|
+
function count() { const n = boxes.filter((b) => b.tick.checked).length; go.disabled = n === 0; go.textContent = uiSaid('set-tidy-go', 'Put to sleep ({n})', { n }); }
|
|
1469
|
+
count();
|
|
1470
|
+
const close = () => { wrap.remove(); document.removeEventListener('keydown', esc, true); };
|
|
1471
|
+
const esc = (event) => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); shut(); } };
|
|
1472
|
+
const shut = () => { if (!nav.close('tidy')) close(); };
|
|
1473
|
+
x.addEventListener('click', shut);
|
|
1474
|
+
wrap.addEventListener('click', (event) => { if (event.target === wrap) shut(); });
|
|
1475
|
+
document.addEventListener('keydown', esc, true);
|
|
1476
|
+
go.addEventListener('click', async () => {
|
|
1477
|
+
const chosen = boxes.filter((b) => b.tick.checked).map((b) => b.t);
|
|
1478
|
+
go.disabled = true; for (const b of boxes) b.tick.disabled = true;
|
|
1479
|
+
if (chosen.some((t) => t.kind === 'conversation' && t.id === current)) disconnect();
|
|
1480
|
+
const done = await Promise.allSettled(chosen.map((t) => (t.kind === 'room'
|
|
1481
|
+
? post(`${API}/rooms/${t.id}`, { quiet: true })
|
|
1482
|
+
: post(`${API}/conversations/${encodeURIComponent(t.id)}/sleep`))));
|
|
1483
|
+
const failed = done.map((r, i) => (r.status === 'rejected' ? `${chosen[i].name}: ${r.reason?.message ?? ''}` : '')).filter(Boolean);
|
|
1484
|
+
await refresh().catch(() => {});
|
|
1485
|
+
try { await loadRooms(); } catch { /* read again later */ }
|
|
1486
|
+
if (chosen.some((t) => t.kind === 'conversation' && t.id === current)) enter();
|
|
1487
|
+
paint();
|
|
1488
|
+
if (failed.length) { said.hidden = false; said.textContent = failed.join(' \u00b7 '); count(); for (const b of boxes) b.tick.disabled = false; return; }
|
|
1489
|
+
shut();
|
|
1490
|
+
});
|
|
1491
|
+
box.append(top, hint, rows, go, said);
|
|
1492
|
+
wrap.append(box);
|
|
1493
|
+
document.body.append(wrap);
|
|
1494
|
+
nav.push('tidy', () => close());
|
|
1495
|
+
go.focus();
|
|
1496
|
+
}
|
|
1416
1497
|
function paint() {
|
|
1417
1498
|
if (listHeld) { paintLater = true; return; }
|
|
1418
1499
|
/* The groups, folded as a whole or not (a search shows them all). */
|
|
@@ -1464,7 +1545,7 @@
|
|
|
1464
1545
|
if (openOne && !inSight.includes(openOne)) inSight.push(openOne);
|
|
1465
1546
|
const few = inSight.length;
|
|
1466
1547
|
/* The rooms used in the last day, among them by when (03-11: « dans le récent, indifféremment »). */
|
|
1467
|
-
const recentRooms = roomsNow().filter((r) => r.last && Date.parse(r.last.at) >= dayAgo);
|
|
1548
|
+
const recentRooms = roomsNow().filter((r) => r.last && Date.parse(r.last.at) >= dayAgo && !roomMuted(r));
|
|
1468
1549
|
const rowsOfRecent = (list) => [...list.map((c) => ({ at: activityOf(c), el: () => itemButton(c, true) })), ...recentRooms.map((r) => ({ at: Date.parse(r.last.at), el: () => roomItem(r) }))].sort((a, b) => b.at - a.at).map((x) => x.el());
|
|
1469
1550
|
if ((recent.length || recentRooms.length) && groups.length && section(RECENT, uiSaid('set-list-recent', 'Recent'), recent)) {
|
|
1470
1551
|
top.append(...unfolding(RECENT, rowsOfRecent(showAllRecent ? recent : recent.filter((c) => inSight.includes(c)))));
|
|
@@ -1503,7 +1584,7 @@
|
|
|
1503
1584
|
}
|
|
1504
1585
|
if (groupsOpen && !onlyRooms) unfolding(GROUPS, [...items.children]);
|
|
1505
1586
|
/* ROOMS (03-11): their own list, after the groups of conversations; a search finds them by name. */
|
|
1506
|
-
const roomsShown = roomsNow().filter((r) => !query || r.name.toLowerCase().includes(String(query).toLowerCase()));
|
|
1587
|
+
const roomsShown = roomsNow().filter((r) => !query || r.name.toLowerCase().includes(String(query).toLowerCase())).sort((a, b) => Number(roomMuted(a)) - Number(roomMuted(b)));
|
|
1507
1588
|
if (!DEVICE && roomsShown.length) {
|
|
1508
1589
|
const b = document.createElement('button'); b.type = 'button'; b.className = 'list-head list-fold'; b.dataset.section = 'rooms';
|
|
1509
1590
|
const open = onlyRooms || !folded.has(ROOMS); b.setAttribute('aria-expanded', String(open));
|
|
@@ -1513,6 +1594,16 @@
|
|
|
1513
1594
|
items.append(b);
|
|
1514
1595
|
if (open) items.append(...roomsShown.map((r) => roomItem(r)));
|
|
1515
1596
|
}
|
|
1597
|
+
/* THE OPEN ONE, MARKED ONCE (07-11): at its first place in the list (À vous, Récent, then its group). */
|
|
1598
|
+
for (const extra of [...top.querySelectorAll('[aria-current="true"]'), ...items.querySelectorAll('[aria-current="true"]')].slice(1)) extra.removeAttribute('aria-current');
|
|
1599
|
+
/* FAIRE LE MÉNAGE (07-11): at the bottom, only when something has been idle 12 h or more. */
|
|
1600
|
+
const tidy = !DEVICE && !query ? tidyNow() : [];
|
|
1601
|
+
if (tidy.length) {
|
|
1602
|
+
const b = document.createElement('button'); b.type = 'button'; b.id = 'tidy-open'; b.className = 'tidy-open';
|
|
1603
|
+
b.textContent = uiSaid('set-tidy-open', 'Tidy up ({n})', { n: tidy.length });
|
|
1604
|
+
b.addEventListener('click', () => openTidy());
|
|
1605
|
+
items.append(b);
|
|
1606
|
+
}
|
|
1516
1607
|
/* The folders there are, offered where one is chosen. */
|
|
1517
1608
|
$('group-names').replaceChildren(...groupsInOrder().map((g) => { const o = document.createElement('option'); o.value = g; return o; }));
|
|
1518
1609
|
const hits = [...items.querySelectorAll('.item')];
|
|
@@ -2390,7 +2481,7 @@
|
|
|
2390
2481
|
/* All folded when the app is opened: what the person opens stays open while this tab lives (a reload
|
|
2391
2482
|
included), never from one day to the next -- a page that opens half-unfolded says nothing. What asks
|
|
2392
2483
|
for them is said on the folded chapter instead (markAttention). */
|
|
2393
|
-
const OPEN_AT_FIRST = { 's-features': true };
|
|
2484
|
+
const OPEN_AT_FIRST = { 's-features': true, 'room-ais': true };
|
|
2394
2485
|
let chapters = {};
|
|
2395
2486
|
try { chapters = JSON.parse(sessionStorage.getItem('clavarium-chapters') ?? '{}') ?? {}; } catch { /* a private window */ }
|
|
2396
2487
|
try { localStorage.removeItem('clavarium-chapters'); } catch { /* as above */ }
|
|
@@ -5412,11 +5503,11 @@
|
|
|
5412
5503
|
if (line) { line.hidden = who.length === 0; line.textContent = who.length ? speaking(who) : ''; }
|
|
5413
5504
|
}
|
|
5414
5505
|
let salonAsked = 0;
|
|
5415
|
-
/* A room's bar: its name, its state, its members (
|
|
5506
|
+
/* A room's bar: its name, its state, its members (up to 3 avatars; a click shows the list under the bar). */
|
|
5416
5507
|
function salonHead(r) {
|
|
5417
5508
|
$('salon-name').textContent = r.name;
|
|
5418
5509
|
salonLive(thinkingOf(r));
|
|
5419
|
-
|
|
5510
|
+
KIT.MemberStack(r.members, { max: 3, button: $('salon-who'), label: countOf(r.members) });
|
|
5420
5511
|
$('salon-who').setAttribute('aria-expanded', 'false'); $('salon-members').hidden = true;
|
|
5421
5512
|
const rows = ['person', 'ai', 'box'].map((k) => { const of = r.members.filter((m) => m.kind === k); if (!of.length) return null; const row = el('div', 'salon-members-row'); row.append(el('span', 'hint', k === 'person' ? uiSaid('set-room-people', 'People') : k === 'ai' ? uiSaid('set-room-ais-of', 'AIs') : uiSaid('set-room-boxes-of', 'Boxes')), ...of.map((m) => { const x = el('span', 'salon-member'); x.append(memberChips([m], 1), document.createTextNode(m.name)); if (m.host) x.append(el('span', 'salon-host', uiSaid('set-room-host-mark', 'host'))); return x; })); return row; }).filter(Boolean);
|
|
5422
5513
|
$('salon-members').replaceChildren(...rows);
|
|
@@ -5503,37 +5594,92 @@
|
|
|
5503
5594
|
}
|
|
5504
5595
|
$('salon-close').addEventListener('click', () => leavePlace());
|
|
5505
5596
|
$('salon-who').addEventListener('click', () => { const open = $('salon-members').hidden; $('salon-members').hidden = !open; $('salon-who').setAttribute('aria-expanded', String(open)); });
|
|
5506
|
-
/* A NEW ROOM, or a room
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5597
|
+
/* A NEW ROOM, or a room's settings (07-11, charte-reglages: as every settings page): folding chapters -- General
|
|
5598
|
+
(its name), Its AIs (every conversation on a row, a switch: in the room or not; the one that welcomes wears
|
|
5599
|
+
« Welcomes », a tap on another of the room moves it), Their assets, Chat & voice (its boxes), Advanced (remove).
|
|
5600
|
+
A room's settings save each change at once (roomPatch), « ✓ Saved » in the bar; a new room keeps its choices
|
|
5601
|
+
here until « Create ». `salonEditing`: the room, or null for a new one. */
|
|
5510
5602
|
let salonIn = new Set(); let salonHost = ''; let salonBoxes = new Set(); let salonEditing = null; let salonBoxList = [];
|
|
5511
|
-
/*
|
|
5603
|
+
/* What a room has, read again from it (after each change saved: what the server kept). */
|
|
5604
|
+
const salonTake = (r) => {
|
|
5605
|
+
salonIn = new Set(r ? r.members.filter((m) => m.kind === 'ai').map((m) => m.ref) : []);
|
|
5606
|
+
salonHost = r?.members.find((m) => m.kind === 'ai' && m.host)?.ref ?? '';
|
|
5607
|
+
salonBoxes = new Set(r ? r.members.filter((m) => m.kind === 'box').map((m) => m.ref) : []);
|
|
5608
|
+
};
|
|
5609
|
+
let salonSavedTimer;
|
|
5610
|
+
/* ONE CHANGE, SAVED AT ONCE (a room's settings): its POST, then the room as the server keeps it; « ✓ Saved » a
|
|
5611
|
+
moment, as Settings say it; refused, said under the bar and drawn back as it was. */
|
|
5612
|
+
async function salonSave(change) {
|
|
5613
|
+
const r = salonEditing; if (!r) return;
|
|
5614
|
+
const patch = KIT.roomPatch(r, change);
|
|
5615
|
+
if (!patch) { salonTake(r); paintSalonMembers(); return; }
|
|
5616
|
+
$('salon-new-error').hidden = true;
|
|
5617
|
+
try {
|
|
5618
|
+
const now = await post(`${API}/rooms/${r.id}`, patch);
|
|
5619
|
+
if (salonEditing !== r) return;
|
|
5620
|
+
salonEditing = now && Array.isArray(now.members) ? now : r;
|
|
5621
|
+
const said = $('salon-new-status'); said.textContent = `✓ ${uiSaid('set-room-saved', 'Saved')}`;
|
|
5622
|
+
clearTimeout(salonSavedTimer); salonSavedTimer = setTimeout(() => { said.textContent = ''; }, 2500);
|
|
5623
|
+
if ('name' in patch) { $('salon-new-title').textContent = uiSaid('set-room-settings-of', '{name} · Settings', { name: salonEditing.name }); }
|
|
5624
|
+
void loadRooms().then(() => paint());
|
|
5625
|
+
if ('add' in patch || 'remove' in patch) void paintSalonAssets();
|
|
5626
|
+
} catch (error) { if (salonEditing !== r) return; $('salon-new-error').textContent = error.message; $('salon-new-error').hidden = false; }
|
|
5627
|
+
salonTake(salonEditing); paintSalonMembers();
|
|
5628
|
+
if ('name' in change) $('salon-new-name').value = salonEditing.name;
|
|
5629
|
+
}
|
|
5630
|
+
/* Every conversation as a member: the kit's card, its size `member`, its switch at its end; the one that welcomes,
|
|
5631
|
+
its pill « Welcomes » (the only place it is said). */
|
|
5512
5632
|
function paintSalonMembers() {
|
|
5513
|
-
const
|
|
5514
|
-
const box = $('salon-new-host'); box.replaceChildren();
|
|
5515
|
-
if (hostC) box.append(KIT.ConvCard({ ...convModel(hostC), now: uiSaid('set-room-host-what', 'answers, or hands the message on') }, { size: 'member', cls: 'salon-host-what' }));
|
|
5516
|
-
else box.append(el('small', 'hint', uiSaid('set-room-host-pick', 'Pick one below: « Welcomes »')));
|
|
5633
|
+
const editing = salonEditing;
|
|
5517
5634
|
const q = $('salon-new-search').value.trim().toLowerCase();
|
|
5518
5635
|
const rows = conversations.filter((c) => !q || `${shownName(c)} ${c.chatName ?? ''}`.toLowerCase().includes(q)).sort((a, b) => Number(salonIn.has(b.name)) - Number(salonIn.has(a.name)));
|
|
5636
|
+
const makeHost = (c) => { if (c.name === salonHost || !salonIn.has(c.name)) return; if (editing) { void salonSave({ host: c.name }); return; } salonHost = c.name; paintSalonMembers(); };
|
|
5519
5637
|
$('salon-new-ais').replaceChildren(...rows.map((c) => {
|
|
5520
|
-
const
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5638
|
+
const isIn = salonIn.has(c.name); const isHost = c.name === salonHost;
|
|
5639
|
+
const actions = [];
|
|
5640
|
+
if (isHost) { const pill = el('span', 'salon-welcome-pill', uiSaid('set-room-welcomes', 'Welcomes')); pill.setAttribute('aria-label', `${shownName(c)} welcomes the room`); actions.push(pill); }
|
|
5641
|
+
actions.push(KIT.Switch({ on: isIn, label: `${shownName(c)} in the room`, onChange: (on) => {
|
|
5642
|
+
if (editing) { void salonSave({ ai: c.name, on }); return; }
|
|
5643
|
+
if (!on) { salonIn.delete(c.name); if (salonHost === c.name) salonHost = [...salonIn][0] ?? ''; } else { salonIn.add(c.name); if (!salonHost) salonHost = c.name; }
|
|
5644
|
+
paintSalonMembers();
|
|
5645
|
+
} }));
|
|
5646
|
+
const row = KIT.ConvCard(convModel(c), { size: 'member', cls: 'salon-member-row', actions });
|
|
5647
|
+
row.setAttribute('role', 'listitem'); row.dataset.name = c.name;
|
|
5648
|
+
if (isHost) row.dataset.host = 'true';
|
|
5649
|
+
/* Another AI of the room: a tap (Enter) on its row, and it welcomes. */
|
|
5650
|
+
if (isIn && !isHost) {
|
|
5651
|
+
row.tabIndex = 0; row.dataset.pick = 'true'; row.title = uiSaid('set-room-host-how', 'Tap another AI of the room to move it');
|
|
5652
|
+
row.addEventListener('click', (e) => { if (!e.target.closest?.('button')) makeHost(c); });
|
|
5653
|
+
row.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target === row) { e.preventDefault(); makeHost(c); } });
|
|
5654
|
+
}
|
|
5525
5655
|
return row;
|
|
5526
5656
|
}));
|
|
5527
|
-
|
|
5657
|
+
const hostC = conversations.find((c) => c.name === salonHost);
|
|
5658
|
+
$('salon-new-count').textContent = [uiSaid('set-room-of', '{n} of {t}', { n: salonIn.size, t: conversations.length }), hostC ? shownName(hostC) : ''].filter(Boolean).join(' · ');
|
|
5659
|
+
$('salon-sum-general').textContent = editing?.name ?? '';
|
|
5660
|
+
const nb = salonBoxes.size;
|
|
5661
|
+
$('salon-sum-voice').textContent = nb ? uiSaid(nb > 1 ? 'set-room-n-boxes' : 'set-room-n-box', nb > 1 ? '{n} boxes' : '1 box', { n: nb }) : uiSaid('set-none', 'None');
|
|
5528
5662
|
$('salon-new-boxes').replaceChildren(...salonBoxList.map((b) => {
|
|
5529
5663
|
const row = el('div', 'salon-member-row'); row.setAttribute('role', 'listitem');
|
|
5530
5664
|
const what = el('span', 'salon-member-what'); what.append(el('b', '', b.name), el('small', '', b.place || uiSaid('set-na-k-box', 'Voice box')));
|
|
5531
|
-
const sw = KIT.Switch({ on: salonBoxes.has(b.id), label: `${b.name} in the room`, onChange: (on) => {
|
|
5665
|
+
const sw = KIT.Switch({ on: salonBoxes.has(b.id), label: `${b.name} in the room`, onChange: (on) => {
|
|
5666
|
+
if (editing) { void salonSave({ box: b.id, on }); return; }
|
|
5667
|
+
if (on) salonBoxes.add(b.id); else salonBoxes.delete(b.id); paintSalonMembers();
|
|
5668
|
+
} });
|
|
5532
5669
|
row.append(what, sw);
|
|
5533
5670
|
return row;
|
|
5534
5671
|
}));
|
|
5535
5672
|
}
|
|
5536
5673
|
$('salon-new-search').addEventListener('input', paintSalonMembers);
|
|
5674
|
+
/* Its name: saved at Enter, or on leaving the field (a room's settings); Escape gives it back. */
|
|
5675
|
+
$('salon-new-name').addEventListener('keydown', (e) => {
|
|
5676
|
+
if (!salonEditing) return;
|
|
5677
|
+
if (e.key === 'Enter') { e.preventDefault(); $('salon-new-name').blur(); }
|
|
5678
|
+
if (e.key === 'Escape') { e.preventDefault(); $('salon-new-name').value = salonEditing.name; $('salon-new-name').blur(); }
|
|
5679
|
+
});
|
|
5680
|
+
$('salon-new-name').addEventListener('change', () => { if (salonEditing) void salonSave({ name: $('salon-new-name').value }); });
|
|
5681
|
+
/* « + … » on a chapter's title: its gesture, never the chapter folded by it -- opened instead. */
|
|
5682
|
+
for (const id of ['salon-assets-all', 'salon-new-box-add']) $(id).addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const ch = $(id).closest('details'); if (ch) ch.open = true; }, true);
|
|
5537
5683
|
/* EACH AI'S ASSETS (06-11), in a room's settings: what each of its AIs has, as its own conversation has it --
|
|
5538
5684
|
there is no room asset. « + Give… » opens that conversation's own give; « Give to all its AIs… » gives one
|
|
5539
5685
|
asset at one right to each (POST /rooms/<id>/give): ordinary rights of each conversation, taken back one by one. */
|
|
@@ -5551,6 +5697,8 @@
|
|
|
5551
5697
|
/* What its own Assets tab lists, row for row (06-11 showed a part: no calendar, contacts, binder or access). */
|
|
5552
5698
|
const assetsOf = (name) => givenItems(name, { all, secrets: keys, boxes, tether, lists }).map((a) => ({ key: a.key, kind: a.kind, name: a.name, live: a.live, right: a.right }))
|
|
5553
5699
|
.sort((a, b) => ASSET_KINDS.findIndex((k) => k[0] === a.kind) - ASSET_KINDS.findIndex((k) => k[0] === b.kind));
|
|
5700
|
+
/* Folded, its chapter says how many each has (« Kitchen helper 9 · Homework 1 »). */
|
|
5701
|
+
$('salon-sum-assets').textContent = ais.map((m) => `${shownName(conversations.find((x) => x.name === m.ref)) || m.name} ${assetsOf(m.ref).length}`).join(' \u00b7 ');
|
|
5554
5702
|
$('salon-assets').replaceChildren(...ais.map((m) => {
|
|
5555
5703
|
const c = conversations.find((x) => x.name === m.ref);
|
|
5556
5704
|
const give = el('button', 'salon-give-btn', uiSaid('set-room-give', '+ Give\u2026')); give.type = 'button';
|
|
@@ -5597,7 +5745,7 @@
|
|
|
5597
5745
|
}
|
|
5598
5746
|
$('salon-assets-all').addEventListener('click', () => { salonGive = salonGive ? null : { asset: '', level: '', said: [] }; void paintSalonAssets(); });
|
|
5599
5747
|
/* A ROOM'S "+ Voice box": the New asset box, its kind a voice box, speaking in this room; once it answers, it is
|
|
5600
|
-
among the room's boxes, switched on (
|
|
5748
|
+
among the room's boxes, switched on (saved at once). */
|
|
5601
5749
|
function paintSalonBoxNew() {
|
|
5602
5750
|
const holder = $('salon-box-newasset');
|
|
5603
5751
|
if (holder.contains($('box-add'))) $('assets-forms').append($('box-add'));
|
|
@@ -5614,71 +5762,56 @@
|
|
|
5614
5762
|
void getJson('/boxes', null).then((list) => {
|
|
5615
5763
|
if (!Array.isArray(list)) return;
|
|
5616
5764
|
salonBoxList = list; boxesKnown = list;
|
|
5617
|
-
if (list.some((b) => b.id === boxFlow?.made?.id))
|
|
5618
|
-
paintSalonMembers();
|
|
5765
|
+
if (list.some((b) => b.id === boxFlow?.made?.id)) void salonSave({ box: boxFlow.made.id, on: true });
|
|
5766
|
+
else paintSalonMembers();
|
|
5619
5767
|
});
|
|
5620
5768
|
} });
|
|
5621
5769
|
paintSalonBoxNew();
|
|
5622
5770
|
focusFirst($('salon-box-newasset'));
|
|
5623
5771
|
});
|
|
5624
|
-
let salonDragged = '';
|
|
5625
|
-
$('salon-new-ais').addEventListener('dragstart', (e) => { salonDragged = e.target.closest?.('.salon-member-row')?.dataset.name ?? ''; });
|
|
5626
|
-
$('salon-new-host').addEventListener('dragover', (e) => { e.preventDefault(); $('salon-new-host').classList.add('over'); });
|
|
5627
|
-
$('salon-new-host').addEventListener('dragleave', () => $('salon-new-host').classList.remove('over'));
|
|
5628
|
-
$('salon-new-host').addEventListener('drop', (e) => { e.preventDefault(); $('salon-new-host').classList.remove('over'); if (salonDragged) { salonIn.add(salonDragged); salonHost = salonDragged; paintSalonMembers(); } });
|
|
5629
5772
|
async function openSalonNew(push = true, editing = null) {
|
|
5630
5773
|
if (DEVICE) return;
|
|
5631
5774
|
salonEditing = editing;
|
|
5632
5775
|
enterPlace('salon-new', push);
|
|
5633
|
-
|
|
5776
|
+
/* « <name> · Settings », as a conversation's; a room to be made: « New room ». */
|
|
5777
|
+
$('salon-new-title').textContent = editing ? uiSaid('set-room-settings-of', '{name} · Settings', { name: editing.name }) : uiSaid('set-room-new', 'New room');
|
|
5634
5778
|
/* Its settings say its state as the room's bar does; a room to be made has none yet. */
|
|
5635
5779
|
const dot = $('salon-new-state'); if (dot) { dot.hidden = !editing; dot.dataset.state = thinkingOf(editing).length ? 'work' : 'ok'; }
|
|
5636
|
-
|
|
5637
|
-
$('salon-new-
|
|
5638
|
-
$('salon-new-
|
|
5780
|
+
/* Saved at once: no button at its end; a new room, « Create » (nothing to save before it is there). */
|
|
5781
|
+
$('salon-new-actions').hidden = Boolean(editing);
|
|
5782
|
+
$('salon-new-status').textContent = ''; clearTimeout(salonSavedTimer);
|
|
5783
|
+
$('salon-new-remove-card').hidden = !editing; salonRemoveAsk?.close(); salonRemoveAsk = null;
|
|
5639
5784
|
$('salon-new-error').hidden = true; $('salon-new-name').value = editing?.name ?? ''; $('salon-new-search').value = '';
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5785
|
+
salonTake(editing);
|
|
5786
|
+
/* A new room: its name and its AIs open, the rest as the person left it. */
|
|
5787
|
+
if (!editing) for (const id of ['salon-ch-general', 'salon-ch-ais']) $(id).open = true;
|
|
5643
5788
|
try { const x = await fetch(`${API}/boxes`); salonBoxList = x.ok ? await x.json() : []; } catch { salonBoxList = []; }
|
|
5644
5789
|
if (Array.isArray(salonBoxList)) boxesKnown = salonBoxList; else salonBoxList = [];
|
|
5645
5790
|
/* A room that is there already adds a voice box of its own: "+ Voice box". */
|
|
5646
|
-
$('salon-new-boxes-card').hidden =
|
|
5791
|
+
$('salon-new-boxes-card').hidden = salonBoxList.length === 0 && !editing;
|
|
5647
5792
|
$('salon-new-box-add').hidden = !editing; closeSalonBoxNew();
|
|
5648
5793
|
paintSalonMembers();
|
|
5649
5794
|
salonGive = null; void paintSalonAssets();
|
|
5650
|
-
$('salon-new-name').focus();
|
|
5795
|
+
if (!editing) $('salon-new-name').focus();
|
|
5651
5796
|
}
|
|
5652
5797
|
$('salon-settings').addEventListener('click', async () => { await loadRooms(); const r = rooms.find((x) => x.id === salonId); if (r) await openSalonNew(true, r); });
|
|
5653
|
-
|
|
5798
|
+
/* Remove the room: confirmed in place (what goes, what stays), as every settings page does. */
|
|
5799
|
+
let salonRemoveAsk = null;
|
|
5800
|
+
const salonRemove = $('salon-new-remove');
|
|
5801
|
+
salonRemove.addEventListener('click', () => {
|
|
5654
5802
|
const r = salonEditing; if (!r) return;
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
const ask = el('span', 'callout bad confirm'); ask.setAttribute('role', 'alertdialog'); ask.append(keep, sure);
|
|
5658
|
-
keep.addEventListener('click', () => { ask.remove(); $('salon-new-remove').hidden = false; });
|
|
5659
|
-
sure.addEventListener('click', async () => { try { await post(`${API}/rooms/${r.id}/remove`, {}); salonEditing = null; await loadRooms(); paint(); openDeck(false); } catch (error) { $('salon-new-error').textContent = error.message; $('salon-new-error').hidden = false; } });
|
|
5660
|
-
$('salon-new-remove').hidden = true; $('salon-new-remove').after(ask); sure.focus();
|
|
5803
|
+
salonRemoveAsk = KIT.InlineConfirm(salonRemove, { question: uiSaid('set-room-remove-what', 'The thread goes · AIs and boxes stay'), yes: uiSaid('set-room-remove-yes', 'Remove {name}', { name: r.name }), keep: uiSaid('set-room-keep', 'Keep'),
|
|
5804
|
+
onYes: async () => { try { await post(`${API}/rooms/${r.id}/remove`, {}); salonEditing = null; await loadRooms(); paint(); openDeck(false); } catch (error) { $('salon-new-error').textContent = error.message; $('salon-new-error').hidden = false; } } });
|
|
5661
5805
|
});
|
|
5662
5806
|
/* Back from a room's settings: to the room; from a new one: where it was opened from. */
|
|
5663
5807
|
const salonNewBack = () => { const r = salonEditing; salonEditing = null; if (r) void openSalon(r.id, false); else leavePlace(); };
|
|
5664
5808
|
$('salon-new-close').addEventListener('click', salonNewBack);
|
|
5665
|
-
|
|
5809
|
+
/* A NEW ROOM: made at « Create », with what was chosen above (its settings, once made, save at once). */
|
|
5666
5810
|
$('salon-new-create').addEventListener('click', async () => {
|
|
5811
|
+
if (salonEditing) return;
|
|
5667
5812
|
const picked = [salonHost, ...[...salonIn].filter((c) => c !== salonHost)].filter(Boolean);
|
|
5668
|
-
const boxes = [...salonBoxes];
|
|
5669
5813
|
try {
|
|
5670
|
-
|
|
5671
|
-
if (salonEditing) {
|
|
5672
|
-
const r = salonEditing;
|
|
5673
|
-
const ais = r.members.filter((m) => m.kind === 'ai'); const had = r.members.filter((m) => m.kind === 'box');
|
|
5674
|
-
const add = { conversations: picked.filter((c) => !ais.some((m) => m.ref === c)), boxes: boxes.filter((b) => !had.some((m) => m.ref === b)) };
|
|
5675
|
-
const remove = [...ais.filter((m) => !salonIn.has(m.ref)), ...had.filter((m) => !salonBoxes.has(m.ref))].map((m) => m.id);
|
|
5676
|
-
let now = await post(`${API}/rooms/${r.id}`, { name: $('salon-new-name').value, add, remove });
|
|
5677
|
-
const host = now.members.find((m) => m.kind === 'ai' && m.ref === salonHost);
|
|
5678
|
-
if (host && !host.host) now = await post(`${API}/rooms/${r.id}`, { host: host.id });
|
|
5679
|
-
salonEditing = null; await openSalon(r.id, false); return;
|
|
5680
|
-
}
|
|
5681
|
-
const made = await post(`${API}/rooms`, { name: $('salon-new-name').value, conversations: picked, boxes, host: salonHost });
|
|
5814
|
+
const made = await post(`${API}/rooms`, { name: $('salon-new-name').value, conversations: picked, boxes: [...salonBoxes], host: salonHost });
|
|
5682
5815
|
await openSalon(made.id, false);
|
|
5683
5816
|
} catch (error) { $('salon-new-error').textContent = error.message; $('salon-new-error').hidden = false; }
|
|
5684
5817
|
});
|
package/web/index.html
CHANGED
|
@@ -715,44 +715,49 @@
|
|
|
715
715
|
<div id="salon-thread" class="salon-thread"></div>
|
|
716
716
|
<p id="salon-thinking" class="salon-thinking" hidden></p>
|
|
717
717
|
</div>
|
|
718
|
+
<!-- A ROOM'S SETTINGS (07-11, charte-reglages): the chapters of every settings page, folding; a row = its name and
|
|
719
|
+
a short line on the left, one control on the right; each change saved at once (a new room: « Create » at the end). -->
|
|
718
720
|
<div id="salon-new" class="sheet as-place" hidden>
|
|
719
|
-
<div class="panel-top"><b id="salon-new-title">New room</b><button type="button" id="salon-new-close" class="as-link place-back">‹ Back</button></div>
|
|
721
|
+
<div class="panel-top"><b id="salon-new-title">New room</b><span class="hint salon-saved" id="salon-new-status" role="status"></span><button type="button" id="salon-new-close" class="as-link place-back">‹ Back</button></div>
|
|
720
722
|
<div class="nc-body">
|
|
721
|
-
<!-- The same names as every settings page (General, Access, Chat & voice, Advanced): a room has no machine. -->
|
|
722
|
-
<h3 class="room-group" data-said="set-tab-general">General</h3>
|
|
723
|
-
<div class="nc-card">
|
|
724
|
-
<div class="set-row"><div class="set-what"><label for="salon-new-name" data-said="set-room-name">Name</label></div><div class="set-control"><input id="salon-new-name" maxlength="60" placeholder="Home" spellcheck="false" autocomplete="off"></div></div>
|
|
725
|
-
</div>
|
|
726
|
-
<h3 class="room-group" data-said="set-tab-access">Access</h3>
|
|
727
|
-
<div class="nc-card">
|
|
728
|
-
<span class="sub-head" data-said="set-room-host">Welcomes the room</span>
|
|
729
|
-
<div id="salon-new-host" class="salon-host-box" aria-live="polite"></div>
|
|
730
|
-
</div>
|
|
731
|
-
<div class="nc-card">
|
|
732
|
-
<span class="sub-head"><span data-said="set-room-ais">Its AIs</span> <span id="salon-new-count" class="hint"></span></span>
|
|
733
|
-
<small class="tip" hidden>The switch puts it in the room or takes it out; « Welcomes » makes it the one that answers first. On a computer, a row can also be dragged into the box above.</small>
|
|
734
|
-
<div class="salon-members-tools"><input id="salon-new-search" type="search" placeholder="Find a conversation" data-said-placeholder="set-room-find" aria-label="Find a conversation" autocomplete="off"></div>
|
|
735
|
-
<div id="salon-new-ais" class="salon-member-list" role="list" aria-label="AIs"></div>
|
|
736
|
-
</div>
|
|
737
|
-
<!-- Each AI's assets (06-11): its own, as its conversation has them; no room asset. -->
|
|
738
|
-
<div class="nc-card" id="salon-assets-card" hidden>
|
|
739
|
-
<div class="row"><span class="sub-head" data-said="set-room-assets">Their assets</span><span class="sp"></span><button type="button" id="salon-assets-all" data-said="set-room-give-all">Give to all its AIs…</button></div>
|
|
740
|
-
<div id="salon-assets-give" class="salon-give" hidden></div>
|
|
741
|
-
<div id="salon-assets" class="salon-member-list salon-assets-list" role="list" aria-label="Their assets"></div>
|
|
742
|
-
</div>
|
|
743
|
-
<h3 class="room-group" id="salon-new-voice-head" data-said="set-tab-chat">Chat & voice</h3>
|
|
744
|
-
<div class="nc-card" id="salon-new-boxes-card">
|
|
745
|
-
<div class="row"><span class="sub-head" data-said="set-room-boxes">Voice boxes</span><span class="sp"></span><button type="button" id="salon-new-box-add" data-said="set-na-plus-box">+ Voice box</button></div>
|
|
746
|
-
<div id="salon-box-newasset" hidden></div>
|
|
747
|
-
<div id="salon-new-boxes" class="salon-member-list" role="list" aria-label="Voice boxes"></div>
|
|
748
|
-
</div>
|
|
749
|
-
<h3 class="room-group" id="salon-new-advanced-head" data-said="set-tab-advanced" hidden>Advanced</h3>
|
|
750
|
-
<div class="nc-card" id="salon-new-remove-card" hidden>
|
|
751
|
-
<div class="set-row"><div class="set-what"><label data-said="set-room-remove">Remove the room</label><small data-said="set-room-remove-what">The thread goes · AIs and boxes stay</small></div><div class="set-control"><button type="button" id="salon-new-remove" class="danger" data-said="set-room-remove-dots">Remove…</button></div></div>
|
|
752
|
-
</div>
|
|
753
723
|
<span id="salon-new-error" class="err" role="alert" hidden></span>
|
|
724
|
+
<details class="chapter" id="salon-ch-general" data-chapter="room-general">
|
|
725
|
+
<summary><span class="chapter-title" data-said="set-tab-general">General</span><span class="chapter-about" id="salon-sum-general"></span></summary>
|
|
726
|
+
<div class="chapter-body">
|
|
727
|
+
<div class="set-row"><div class="set-what"><label for="salon-new-name" data-said="set-room-name">Name</label></div><div class="set-control"><input id="salon-new-name" maxlength="60" placeholder="Home" spellcheck="false" autocomplete="off" enterkeyhint="done"></div></div>
|
|
728
|
+
</div>
|
|
729
|
+
</details>
|
|
730
|
+
<details class="chapter" id="salon-ch-ais" data-chapter="room-ais">
|
|
731
|
+
<summary><span class="chapter-title" data-said="set-room-ais">Its AIs</span><span class="chapter-about" id="salon-new-count"></span></summary>
|
|
732
|
+
<div class="chapter-body">
|
|
733
|
+
<div class="set-row salon-members-tools"><div class="set-what"><small data-said="set-room-host-how">Tap another AI of the room to move it</small></div><div class="set-control"><input id="salon-new-search" type="search" placeholder="Find a conversation" data-said-placeholder="set-room-find" aria-label="Find a conversation" autocomplete="off"></div></div>
|
|
734
|
+
<div id="salon-new-ais" class="salon-member-list" role="list" aria-label="AIs"></div>
|
|
735
|
+
</div>
|
|
736
|
+
</details>
|
|
737
|
+
<!-- Each AI's assets (06-11): its own, as its conversation has them; no room asset. -->
|
|
738
|
+
<details class="chapter" id="salon-assets-card" data-chapter="room-assets" hidden>
|
|
739
|
+
<summary><span class="chapter-title" data-said="set-room-assets">Their assets</span><span class="chapter-about" id="salon-sum-assets"></span><button type="button" id="salon-assets-all" class="chapter-add" data-said="set-room-give-all">Give to all its AIs…</button></summary>
|
|
740
|
+
<div class="chapter-body">
|
|
741
|
+
<div id="salon-assets-give" class="salon-give" hidden></div>
|
|
742
|
+
<div id="salon-assets" class="salon-member-list salon-assets-list" role="list" aria-label="Their assets"></div>
|
|
743
|
+
</div>
|
|
744
|
+
</details>
|
|
745
|
+
<details class="chapter" id="salon-new-boxes-card" data-chapter="room-voice">
|
|
746
|
+
<summary><span class="chapter-title" data-said="set-tab-chat">Chat & voice</span><span class="chapter-about" id="salon-sum-voice"></span><button type="button" id="salon-new-box-add" class="chapter-add" data-said="set-na-plus-box">+ Voice box</button></summary>
|
|
747
|
+
<div class="chapter-body">
|
|
748
|
+
<div id="salon-box-newasset" hidden></div>
|
|
749
|
+
<div id="salon-new-boxes" class="salon-member-list" role="list" aria-label="Voice boxes"></div>
|
|
750
|
+
</div>
|
|
751
|
+
</details>
|
|
752
|
+
<details class="chapter" id="salon-new-remove-card" data-chapter="room-advanced" hidden>
|
|
753
|
+
<summary><span class="chapter-title" data-said="set-tab-advanced">Advanced</span><span class="chapter-about"></span></summary>
|
|
754
|
+
<div class="chapter-body">
|
|
755
|
+
<div class="set-row"><div class="set-what"><span data-said="set-room-remove">Remove the room</span><small data-said="set-room-remove-what">The thread goes · AIs and boxes stay</small></div><div class="set-control"><button type="button" id="salon-new-remove" class="danger" data-said="set-room-remove-dots">Remove…</button></div></div>
|
|
756
|
+
</div>
|
|
757
|
+
</details>
|
|
754
758
|
</div>
|
|
755
|
-
|
|
759
|
+
<!-- A new room only: nothing is saved before it exists. -->
|
|
760
|
+
<div class="action-row" id="salon-new-actions"><button type="button" id="salon-new-create" class="primary" data-said="set-room-create">Create</button></div>
|
|
756
761
|
</div>
|
|
757
762
|
<div id="assets" class="sheet as-place" hidden>
|
|
758
763
|
<div class="panel-top"><b>Assets</b></div>
|
package/web/kit.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Switch yes / no (role=switch, aria-checked)
|
|
8
8
|
* Segmented one of a few (pressed buttons, aria-pressed)
|
|
9
9
|
* InlineConfirm confirm in place of the button: Keep, then the action
|
|
10
|
+
* MemberStack who is in a room: up to 3 small stacked avatars, "+n" (its bar's right); roomPatch, one
|
|
11
|
+
* change of a room's settings as it is saved at once
|
|
10
12
|
* ConvCard ONE CONVERSATION, three sizes: `row` (the list, Recent), `card` (the home deck),
|
|
11
13
|
* `member` (a room's members, and the one that welcomes it). Same dot, name, line
|
|
12
14
|
* of what it does now, chips; each place adds its own gestures (`actions`).
|
|
@@ -106,6 +108,25 @@
|
|
|
106
108
|
return g;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/* IDLE (07-11, the tidy-up): nothing for 12 h or more. Never one that waits for you, never one asleep.
|
|
112
|
+
`idleFor` is how long (0: not idle); `idleWords` says it: « rien depuis 14 h », « … 2 j », « … 3 sem. ». */
|
|
113
|
+
const IDLE_MS = 12 * 3600_000;
|
|
114
|
+
const idleFor = (at, { now = Date.now(), waits = false, asleep = false } = {}) => {
|
|
115
|
+
const t = typeof at === 'number' ? at : Date.parse(at ?? '');
|
|
116
|
+
if (waits || asleep || !Number.isFinite(t) || t <= 0) return 0;
|
|
117
|
+
return now - t >= IDLE_MS ? now - t : 0;
|
|
118
|
+
};
|
|
119
|
+
const idleWords = (ms) => {
|
|
120
|
+
const h = Math.floor(ms / 3600_000); const d = Math.floor(h / 24);
|
|
121
|
+
const [key, en, n] = h < 48 ? ['set-idle-h', 'idle for {n} h', h] : d < 14 ? ['set-idle-d', 'idle for {n} d', d] : d < 60 ? ['set-idle-w', 'idle for {n} wk', Math.floor(d / 7)] : ['set-idle-mo', 'idle for {n} mo', Math.floor(d / 30)];
|
|
122
|
+
return say(key, en).replace('{n}', String(n));
|
|
123
|
+
};
|
|
124
|
+
/* WHAT A TIDY-UP OFFERS: of `items` ({ at, waits, asleep, hidden, ... }), those idle 12 h or more, the
|
|
125
|
+
longest idle first, each with its `idle` (ms). Hidden ones are left alone. */
|
|
126
|
+
const tidyList = (items, now = Date.now()) => items.filter((x) => x && !x.hidden)
|
|
127
|
+
.map((x) => ({ ...x, idle: idleFor(x.at, { now, waits: x.waits, asleep: x.asleep }) }))
|
|
128
|
+
.filter((x) => x.idle > 0).sort((a, b) => b.idle - a.idle);
|
|
129
|
+
|
|
109
130
|
/* Confirm in place of `button`: the question, Keep (focused), then the action. Escape keeps.
|
|
110
131
|
Returns { close }. `onYes()` may be async; the action stays pressed till it is done. */
|
|
111
132
|
function InlineConfirm(button, { question = '', yes = 'Remove', keep, danger = true, onYes = () => {} } = {}) {
|
|
@@ -152,7 +173,7 @@
|
|
|
152
173
|
const lines = [];
|
|
153
174
|
if (size === 'row') {
|
|
154
175
|
const marks = Chips((m.chips ?? []).filter((c) => c && !c.wide && c.tone !== 'count'));
|
|
155
|
-
const sub = [m.last ? node('small', 'last', m.last) : null, m.now ? node('small',
|
|
176
|
+
const sub = [m.last ? node('small', 'last', m.last) : null, m.now ? node('small', `kit-conv-now${m.idle ? ' kit-conv-idle' : ''}`, m.now) : null].filter(Boolean);
|
|
156
177
|
if (marks && sub.length) { const line = node('span', 'kit-conv-sub'); line.append(sub[0], marks); lines.push(line, ...sub.slice(1)); }
|
|
157
178
|
else if (marks) lines.push(marks);
|
|
158
179
|
else lines.push(...sub);
|
|
@@ -534,6 +555,57 @@
|
|
|
534
555
|
return box;
|
|
535
556
|
}
|
|
536
557
|
|
|
558
|
+
/* WHO IS IN A ROOM, AT A GLANCE (07-11): its members as up to `max` small stacked avatars, on its bar's right --
|
|
559
|
+
the one that welcomes first, then its other AIs, its people, its boxes. More than `max`: `max - 1` of them and
|
|
560
|
+
"+n" (never more than `max` circles). `members` as the server lists them [{ name, kind, host? }]. */
|
|
561
|
+
const STACK_ORDER = { ai: 1, person: 2, box: 3 };
|
|
562
|
+
function memberStackModel(members = [], { max = 3 } = {}) {
|
|
563
|
+
const all = (members ?? []).filter((m) => m && m.name).map((m, i) => ({ m, i }))
|
|
564
|
+
.sort((a, b) => (Number(Boolean(b.m.host)) - Number(Boolean(a.m.host))) || ((STACK_ORDER[a.m.kind] ?? 4) - (STACK_ORDER[b.m.kind] ?? 4)) || a.i - b.i)
|
|
565
|
+
.map((x) => x.m);
|
|
566
|
+
const most = Math.max(1, Math.floor(max) || 1);
|
|
567
|
+
const shown = all.length > most ? all.slice(0, most - 1) : all;
|
|
568
|
+
return { shown: shown.map((m) => ({ name: m.name, kind: m.kind ?? 'ai', ...(m.host ? { host: true } : {}) })), more: all.length - shown.length, names: all.map((m) => m.name) };
|
|
569
|
+
}
|
|
570
|
+
/* The stack, drawn in `button` (the page's own, its id and listeners kept) or a new one; `onOpen` on a click.
|
|
571
|
+
`label`: what a click opens (« Members »), said with the names. */
|
|
572
|
+
function MemberStack(members = [], { max = 3, label = '', button = null, onOpen = null } = {}) {
|
|
573
|
+
const model = memberStackModel(members, { max });
|
|
574
|
+
const b = button ?? node('button'); b.type = 'button';
|
|
575
|
+
addClass(b, 'kit-stack');
|
|
576
|
+
b.replaceChildren(...model.shown.map((m) => {
|
|
577
|
+
const a = Avatar({ name: m.name, glyph: m.kind === 'box' ? node('span', 'kit-stack-box') : null });
|
|
578
|
+
a.dataset.kind = m.kind;
|
|
579
|
+
return a;
|
|
580
|
+
}));
|
|
581
|
+
if (model.more) { const more = node('span', 'kit-av kit-stack-more', `+${model.more}`); more.setAttribute('aria-hidden', 'true'); b.append(more); }
|
|
582
|
+
const names = model.names.join(', ');
|
|
583
|
+
b.title = names; b.setAttribute('aria-label', label ? `${label}: ${names}` : names);
|
|
584
|
+
if (onOpen) b.addEventListener('click', () => onOpen());
|
|
585
|
+
return b;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* ONE CHANGE OF A ROOM, SAVED AT ONCE (07-11: its settings as every settings page, no Save): what POST
|
|
589
|
+
/rooms/<id> is given for it, or null when it changes nothing. `room` as the server lists it; `change` one of
|
|
590
|
+
{ name } its new name (trimmed; empty: nothing -- the field shows the name again)
|
|
591
|
+
{ ai, on } a conversation (its name) put in the room or taken out
|
|
592
|
+
{ box, on } a voice box (its id) put in or taken out
|
|
593
|
+
{ host } a conversation of the room (its name) that welcomes it from now on */
|
|
594
|
+
function roomPatch(room, change = {}) {
|
|
595
|
+
const members = room?.members ?? [];
|
|
596
|
+
const of = (kind, ref) => members.find((m) => m.kind === kind && m.ref === ref);
|
|
597
|
+
if ('name' in change) { const name = String(change.name ?? '').trim(); return name && name !== room?.name ? { name } : null; }
|
|
598
|
+
for (const kind of ['ai', 'box']) {
|
|
599
|
+
if (!(kind in change)) continue;
|
|
600
|
+
const ref = String(change[kind] ?? ''); if (!ref) return null;
|
|
601
|
+
const m = of(kind, ref);
|
|
602
|
+
if (change.on) return m ? null : { add: kind === 'ai' ? { conversations: [ref] } : { boxes: [ref] } };
|
|
603
|
+
return m ? { remove: [m.id] } : null;
|
|
604
|
+
}
|
|
605
|
+
if ('host' in change) { const m = of('ai', String(change.host ?? '')); return m && !m.host ? { host: m.id } : null; }
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
537
609
|
/* ONE ASSET. `a`, what the page knows of it, said once:
|
|
538
610
|
key, kind 'folder:reports', its kind (ASSET_ICONS) name its name
|
|
539
611
|
live a live link (a connexion): its rights are a connexion's
|
|
@@ -803,10 +875,10 @@
|
|
|
803
875
|
|
|
804
876
|
window.clavariumKit = {
|
|
805
877
|
collectModel,
|
|
806
|
-
AssetIcon, AssetCard, Holders, RightsBadge, RightsPicker, AssetList, assetListModel, AccessList, accessModel, AssetPanel, AssetDelete, RIGHT_SAME,
|
|
878
|
+
AssetIcon, AssetCard, Holders, MemberStack, memberStackModel, roomPatch, RightsBadge, RightsPicker, AssetList, assetListModel, AccessList, accessModel, AssetPanel, AssetDelete, RIGHT_SAME,
|
|
807
879
|
ASSET_ICONS, ASSET_KINDS, RIGHTS, RIGHT_LEVELS, rightsKey, rightsFor, rightOf, storedOf, lightest, rightWord,
|
|
808
880
|
shareSlices, ShareDonut, ShareKey,
|
|
809
|
-
STATES, STATE_WORDS, stateOf, hueOf, initialOf,
|
|
881
|
+
STATES, STATE_WORDS, stateOf, hueOf, initialOf, IDLE_MS, idleFor, idleWords, tidyList,
|
|
810
882
|
setWords(fn) { if (typeof fn === 'function') words = fn; },
|
|
811
883
|
StatusDot, Avatar, Chips, Switch, Segmented, InlineConfirm, ConvCard, StepFlow, SecretPicker, NewAsset, TopBar,
|
|
812
884
|
};
|
package/web/style.css
CHANGED
|
@@ -2009,12 +2009,13 @@ span.callout.confirm { display: inline-flex; justify-content: flex-end; padding:
|
|
|
2009
2009
|
:is(#newconv, #salon-new) .set-row > .set-control > :is(input, select) { width: 18rem; }
|
|
2010
2010
|
#newconv .set-what > .err { font-size: .75rem; color: var(--danger); }
|
|
2011
2011
|
/* A card of rows, as an open chapter without its fold. */
|
|
2012
|
-
|
|
2013
|
-
.nc-card
|
|
2014
|
-
.nc-card >
|
|
2012
|
+
/* A room's settings (07-11): the chapters of a new conversation's, of Settings. */
|
|
2013
|
+
.nc-card, :is(#newconv, #salon-new) details.chapter { flex-shrink: 0; border: 1px solid var(--line); border-radius: 10px; background: var(--bar); }
|
|
2014
|
+
.nc-card > *, :is(#newconv, #salon-new) details.chapter > .chapter-body > * { margin: 0; padding: 12px 16px; }
|
|
2015
|
+
.nc-card > * + *, :is(#newconv, #salon-new) details.chapter > .chapter-body > :where(:not([hidden])) ~ :where(:not([hidden])) { border-top: 1px solid var(--line); }
|
|
2015
2016
|
.nc-card > .sub-head { display: block; padding-bottom: 0; }
|
|
2016
2017
|
.nc-card > .sub-head + * { border-top: 0; }
|
|
2017
|
-
#newconv details.chapter > .chapter-body { gap: 0; padding: 0; background: var(--bar); border-radius: 0 0 10px 10px; }
|
|
2018
|
+
:is(#newconv, #salon-new) details.chapter > .chapter-body { gap: 0; padding: 0; background: var(--bar); border-radius: 0 0 10px 10px; }
|
|
2018
2019
|
/* A choice of a few, each a card: its name and one short line. */
|
|
2019
2020
|
.pick { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
|
|
2020
2021
|
.pick.two { grid-template-columns: 1fr 1fr; }
|
|
@@ -2342,7 +2343,6 @@ body.as-dragging #assets-rows .as-row { touch-action: none; }
|
|
|
2342
2343
|
.salon-chip-box::before { content: ""; width: 9px; height: 9px; background: currentColor; -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H3v6h3l5 4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H3v6h3l5 4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3C/svg%3E") center / contain no-repeat; }
|
|
2343
2344
|
#salon { display: flex; flex-direction: column; min-height: 0; }
|
|
2344
2345
|
#salon[hidden] { display: none; }
|
|
2345
|
-
.salon-who { display: inline-flex; align-items: center; gap: 8px; border: 0; background: transparent; padding: 4px 14px; min-height: 44px; }
|
|
2346
2346
|
.salon-members { display: flex; flex-direction: column; gap: 8px; padding: 0 14px 10px; border-bottom: 1px solid var(--line); }
|
|
2347
2347
|
.salon-members-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
|
|
2348
2348
|
.salon-members-row > .hint { width: 4.5rem; }
|
|
@@ -2353,8 +2353,6 @@ body.as-dragging #assets-rows .as-row { touch-action: none; }
|
|
|
2353
2353
|
.salon-thinking { display: flex; align-items: center; gap: 6px; margin: 0; padding: 0 14px 8px; font-size: .75rem; color: var(--ink2); order: 2; }
|
|
2354
2354
|
#salon-new { display: flex; flex-direction: column; }
|
|
2355
2355
|
#salon-new[hidden] { display: none; }
|
|
2356
|
-
#salon-new .nc-card > .hint { display: block; padding-top: 0; border-top: 0; }
|
|
2357
|
-
#salon-new .pick .salon-welcome { color: var(--brass-ink); }
|
|
2358
2356
|
|
|
2359
2357
|
/* THE PLACES OF THE WHOLE (03-11): settings, access, assets, the deck, rooms, a new conversation are not a
|
|
2360
2358
|
conversation's -- in a part of the page of their own, over the conversation (which stays as it was, its
|
|
@@ -2363,34 +2361,39 @@ body.as-dragging #assets-rows .as-row { touch-action: none; }
|
|
|
2363
2361
|
#places[hidden] { display: none; }
|
|
2364
2362
|
@media (max-width: 700px) { #places { grid-column: 1; grid-row: 1; } }
|
|
2365
2363
|
#app:has(> #list[hidden]) #places { grid-column: 1 / -1; }
|
|
2366
|
-
.salon-bar { display: flex; align-items: center; gap: 8px; padding: 0 8px 0 0; }
|
|
2367
|
-
.salon-bar .salon-who { flex: 1; }
|
|
2368
|
-
#salon-new-remove-card .confirm { display: inline-flex; gap: 6px; flex-wrap: wrap; }
|
|
2369
2364
|
|
|
2370
2365
|
/* A ROOM'S MEMBERS (03-11): the one that welcomes in a box of its own; every conversation on a row, its switch. */
|
|
2371
|
-
.salon-host-box { display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 8px 12px; border: 1px solid var(--brass); border-radius: 10px; background: color-mix(in srgb, var(--brass) 10%, var(--panel)); transition: background .15s; }
|
|
2372
|
-
.salon-host-box.over { background: color-mix(in srgb, var(--brass) 22%, var(--panel)); }
|
|
2373
2366
|
.salon-member-what { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
2374
2367
|
.salon-member-what b { font-weight: 600; font-size: .8125rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2375
2368
|
.salon-member-what small { color: var(--ink2); font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2376
|
-
|
|
2369
|
+
/* In a chapter, its rows are the chapter's (07-11): no box in a box. */
|
|
2370
|
+
#salon-new .chapter-body > .salon-member-list { border: 0; border-radius: 0; background: transparent; max-height: none; }
|
|
2371
|
+
#salon-new .chapter-body .salon-member-row { padding: 8px 16px; }
|
|
2372
|
+
#salon-new .chapter-body > .salon-members-tools { padding-top: 10px; padding-bottom: 10px; }
|
|
2373
|
+
details.chapter > summary > .chapter-add { flex: none; align-self: center; min-height: 32px; padding: 4px 12px; font-size: .8125rem; }
|
|
2374
|
+
details.chapter > summary > .chapter-add[hidden] { display: none; }
|
|
2375
|
+
@media (max-width: 560px) {
|
|
2376
|
+
details.chapter > summary > .chapter-add { padding: 4px 8px; font-size: .75rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; }
|
|
2377
|
+
details.chapter > summary:has(> .chapter-add:not([hidden])) > .chapter-about { display: none; }
|
|
2378
|
+
}
|
|
2379
|
+
.salon-saved { flex: none; font-size: .8125rem; color: var(--ok, var(--ink2)); white-space: nowrap; }
|
|
2380
|
+
.salon-saved:empty { display: none; }
|
|
2381
|
+
#salon-new-actions[hidden] { display: none; }
|
|
2377
2382
|
.salon-member-list { display: flex; flex-direction: column; max-height: 22rem; overflow-y: auto; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 0 !important; }
|
|
2378
2383
|
.salon-member-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; min-height: 48px; border-top: 1px solid var(--line); }
|
|
2379
2384
|
.salon-member-row:first-child { border-top: 0; }
|
|
2380
2385
|
.salon-member-row[draggable="true"] { cursor: grab; }
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
.
|
|
2384
|
-
.
|
|
2385
|
-
.nc-card > .salon-members-tools { padding-bottom: 4px; }
|
|
2386
|
-
.nc-card > .salon-host-box + *, .nc-card > .salon-member-list + * { border-top: 0; }
|
|
2386
|
+
.salon-welcome-pill { flex: none; border: 1px solid var(--brass); border-radius: 999px; padding: 3px 10px; font-size: .75rem; font-weight: 600; color: var(--brass-ink); background: color-mix(in srgb, var(--brass) 14%, transparent); white-space: nowrap; }
|
|
2387
|
+
.salon-member-row[data-pick="true"] { cursor: pointer; }
|
|
2388
|
+
.salon-member-row[data-pick="true"]:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); }
|
|
2389
|
+
.salon-member-row[data-pick="true"]:focus-visible { outline: 2px solid var(--brass); outline-offset: -2px; }
|
|
2387
2390
|
/* Each AI's assets in a room's settings (06-11): its member card, its assets as chips below; give to all its AIs. */
|
|
2388
2391
|
.salon-assets-list { max-height: none; }
|
|
2389
2392
|
.salon-asset-ai { display: flex; flex-direction: column; gap: 2px; padding-bottom: 10px; border-top: 1px solid var(--line); }
|
|
2390
2393
|
.salon-asset-ai:first-child { border-top: 0; }
|
|
2391
2394
|
.salon-asset-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px; }
|
|
2392
2395
|
button.salon-give-btn { border-radius: 999px; padding: 4px 10px; font-size: .75rem; min-height: 32px; background: transparent; color: var(--ink2); white-space: nowrap; }
|
|
2393
|
-
.
|
|
2396
|
+
#salon-new .chapter-body > .salon-give { margin: 4px 16px 12px; padding: 10px 12px; border: 1px solid var(--brass); border-radius: 10px; background: color-mix(in srgb, var(--brass) 6%, var(--panel)); display: flex; flex-direction: column; gap: 8px; }
|
|
2394
2397
|
.salon-give-assets { display: flex; flex-wrap: wrap; gap: 6px; max-height: 12rem; overflow-y: auto; }
|
|
2395
2398
|
.salon-give-said { display: flex; flex-direction: column; gap: 2px; }
|
|
2396
2399
|
.salon-give .action-row { padding: 0; border: 0; }
|
|
@@ -2431,14 +2434,19 @@ html[data-door="device"] header.topbar { height: auto; }
|
|
|
2431
2434
|
.sheet.as-place > header.topbar { margin: 0; }
|
|
2432
2435
|
:is(#newconv, #salon-new) > header.topbar { margin: 0 -20px 4px; }
|
|
2433
2436
|
@media (max-width: 700px) { #newconv > header.topbar { margin: 0 -16px 4px; } }
|
|
2434
|
-
/* A room: its name, who speaks now
|
|
2437
|
+
/* A room: its name, who speaks now on the line under it (only then); who is in it on the right, before its gear
|
|
2438
|
+
(07-11): up to 3 small stacked avatars, a button that opens the list under the bar. */
|
|
2439
|
+
header.topbar .kit-stack { display: inline-flex; align-items: center; flex: none; min-height: 34px; padding: 0 4px; border: 0; border-radius: 999px; background: transparent; cursor: pointer; }
|
|
2440
|
+
header.topbar .kit-stack:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
|
|
2441
|
+
header.topbar .kit-stack:focus-visible { outline: 2px solid var(--brass); outline-offset: 1px; }
|
|
2442
|
+
.kit-stack > .kit-av { --kc-av: 24px; border-radius: 50%; box-shadow: 0 0 0 2px var(--bar); }
|
|
2443
|
+
.kit-stack > .kit-av + .kit-av { margin-left: -7px; }
|
|
2444
|
+
.kit-stack > .kit-av[data-kind="box"] { background: var(--panel); color: var(--ink2); box-shadow: 0 0 0 2px var(--bar), inset 0 0 0 1px var(--line); }
|
|
2445
|
+
.kit-stack-box { width: 12px; height: 12px; background: currentColor; -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H3v6h3l5 4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H3v6h3l5 4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3C/svg%3E") center / contain no-repeat; }
|
|
2446
|
+
.kit-stack > .kit-stack-more { background: var(--panel); color: var(--ink2); box-shadow: 0 0 0 2px var(--bar), inset 0 0 0 1px var(--line); font-size: .6875rem; }
|
|
2435
2447
|
#salon-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2436
2448
|
.salon-live { font-size: .75rem; font-weight: 400; color: var(--ink2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2437
2449
|
.salon-live[hidden] { display: none; }
|
|
2438
|
-
header.topbar .topbar-sub .salon-who { display: inline-flex; align-items: center; gap: 6px; min-height: 0; padding: 0; border: 0; background: transparent; color: var(--ink2); font: inherit; cursor: pointer; }
|
|
2439
|
-
header.topbar .topbar-sub .salon-who:hover { color: var(--ink); }
|
|
2440
|
-
header.topbar .topbar-sub .salon-chip { width: 15px; height: 15px; margin-left: -4px; border-width: 1px; font-size: .5625rem; }
|
|
2441
|
-
header.topbar .topbar-sub .salon-chip:first-child { margin-left: 0; }
|
|
2442
2450
|
/* A room's row in the list (03-11): a conversation's row, with a small sign of a room before its name (Lucide « users »). */
|
|
2443
2451
|
.room-sign { display: inline-block; width: 14px; height: 14px; margin-right: 6px; vertical-align: -2px; background: var(--ink2);
|
|
2444
2452
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='9' cy='7' r='4'/%3E%3Cpath d='M22 21v-2a4 4 0 0 0-3-3.87'/%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
@@ -2698,9 +2706,6 @@ header.topbar .topbar-sub .salon-chip:first-child { margin-left: 0; }
|
|
|
2698
2706
|
/* The search atop a settings page (05-11). */
|
|
2699
2707
|
:is(header.topbar, .panel-top) .settings-search { margin-left: auto; width: min(240px, 42vw); min-height: 32px; padding: 4px 10px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); color: var(--ink); font-size: .875rem; }
|
|
2700
2708
|
.set-row.search-out { display: none; }
|
|
2701
|
-
/* A room's settings: the same group names as every settings page (05-11). */
|
|
2702
|
-
#salon-new .room-group { margin: 6px 4px -6px; font-size: .6875rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink2); }
|
|
2703
|
-
#salon-new .room-group[hidden] { display: none; }
|
|
2704
2709
|
|
|
2705
2710
|
/* ======================================================================================================
|
|
2706
2711
|
KIT (05-11, web/kit.js): the pieces every screen shares, one style each. A screen places them; it
|
|
@@ -2784,7 +2789,6 @@ button.kit-switch[aria-checked="true"]::after { left: 18px; background: var(--ok
|
|
|
2784
2789
|
.kit-conv-member { --kc-ring: var(--panel); align-items: center; padding: 8px 12px; }
|
|
2785
2790
|
.kit-conv-member .kit-conv-head > b { font-size: .8125rem; }
|
|
2786
2791
|
.kit-conv-member .kit-conv-main > small { font-size: .75rem; }
|
|
2787
|
-
.salon-host-box > .kit-conv-member { flex: 1; padding: 0; --kc-ring: color-mix(in srgb, var(--brass) 10%, var(--panel)); }
|
|
2788
2792
|
@media (max-width: 520px) { .kit-conv-card { --kc-av: 30px; column-gap: 8px; padding: 10px; } }
|
|
2789
2793
|
|
|
2790
2794
|
/* THE ROW, compact (05-11, designers' review): a smaller initial, a bigger ringed point, the marks as small
|
|
@@ -2897,3 +2901,22 @@ button.kit-switch[aria-checked="true"]::after { left: 18px; background: var(--ok
|
|
|
2897
2901
|
.kit-rights-pick > button > .kit-rights-same { display: block; font-size: .6875rem; font-weight: 500; opacity: .8; }
|
|
2898
2902
|
.kit-asdelete { display: inline; }
|
|
2899
2903
|
.as-files-menu #files.embedded > .panel-top { display: none; }
|
|
2904
|
+
/* IDLE 12 H OR MORE (07-11): « rien depuis 14 h », in grey, never bold. */
|
|
2905
|
+
.item small.kit-conv-idle { color: var(--ink2); opacity: .8; }
|
|
2906
|
+
/* FAIRE LE MÉNAGE (07-11): a clear button at the bottom of the list, only when something is idle. */
|
|
2907
|
+
.tidy-open { display: block; width: calc(100% - 1.5rem); margin: .75rem .75rem .25rem; padding: .55rem .75rem; background: transparent; border: 1px dashed var(--line); border-radius: 10px; color: var(--ink2); font-size: .8125rem; font-weight: 600; text-align: center; }
|
|
2908
|
+
.tidy-open:hover { color: var(--ink); border-color: var(--ink2); }
|
|
2909
|
+
/* « Mettre en veille ? »: the app's pop-up (a sheet from the bottom on a phone, centred on a wider screen). */
|
|
2910
|
+
.tidy-list { display: flex; flex-direction: column; gap: 2px; max-height: 50dvh; overflow-y: auto; }
|
|
2911
|
+
.tidy-row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 6px; border-radius: 8px; cursor: pointer; }
|
|
2912
|
+
.tidy-row:hover { background: var(--bar); }
|
|
2913
|
+
.tidy-row input { width: 18px; height: 18px; accent-color: var(--brass); flex: none; }
|
|
2914
|
+
.tidy-name { color: var(--ink2); font-size: .875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2915
|
+
.tidy-name b { color: var(--ink); font-weight: 600; }
|
|
2916
|
+
.tidy-go { min-height: 44px; }
|
|
2917
|
+
.tidy-said { color: var(--danger); }
|
|
2918
|
+
@media (min-width: 700px) {
|
|
2919
|
+
.tidy-wrap { align-items: center; justify-content: center; }
|
|
2920
|
+
.tidy-wrap .lk-sheet { width: min(440px, 92vw); border: 1px solid var(--line); border-radius: 14px; padding: 16px; }
|
|
2921
|
+
.tidy-wrap .lk-sheet::before { display: none; }
|
|
2922
|
+
}
|