@7ots/cli 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/cli/7ots.mjs +13 -1
- package/cli/lib/account.mjs +152 -2
- package/cli/lib/assistant.mjs +107 -38
- package/cli/lib/brain.mjs +13 -2
- package/cli/lib/config.mjs +5 -1
- package/cli/lib/inferences.mjs +30 -6
- package/cli/lib/integrations.mjs +69 -5
- package/cli/lib/orquesta.mjs +44 -1
- package/cli/lib/pet-server.mjs +352 -11
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/pet/electron/director.cjs +37 -5
- package/cli/pet/electron/main.cjs +92 -10
- package/cli/pet/pet.html +149 -10
- package/cli/pet/settings.html +549 -43
- package/dist/7ots.esm.js +216 -137
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +216 -137
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +1 -1
- package/package.json +17 -14
- package/server/channels/agent.mjs +7 -1
- package/server/channels/index.mjs +3 -2
- package/server/platform/connect.mjs +4 -1
- package/server/platform/db.mjs +14 -1
- package/server/platform/device.mjs +101 -4
- package/server/platform/mods.mjs +222 -0
- package/server/platform/orquesta.mjs +78 -14
- package/server/platform/profile.mjs +161 -0
- package/server/platform/routes.mjs +39 -33
- package/server/platform/runtime.mjs +5 -3
- package/server/platform/store.mjs +1 -1
- package/server/platform/vm.mjs +688 -17
- package/server/server.mjs +12 -2
- package/server/tts.mjs +17 -4
- package/src/avatar/three.js +3 -2
- package/src/character/AvatarEditor.js +95 -7
- package/src/character/Character.js +116 -17
- package/src/character/TradingCard.js +226 -0
- package/src/character/mods/index.js +4 -0
- package/src/character/mods/orc.js +64 -0
- package/src/character/mods.js +251 -0
- package/src/character/more/acc-head.js +213 -0
- package/src/character/more/acc-rest.js +342 -0
- package/src/character/more/brows.js +39 -0
- package/src/character/more/cheeks.js +78 -0
- package/src/character/more/eyes.js +288 -0
- package/src/character/more/mouths.js +259 -0
- package/src/character/more/patterns.js +120 -0
- package/src/character/parts.js +100 -0
- package/src/character/skin.js +166 -1
- package/src/character/styles.js +1285 -9
- package/src/character3d/Character3D.js +253 -37
- package/src/character3d/index.js +7 -1
- package/src/character3d/model.js +367 -0
- package/src/i18n/messages/character.js +37 -22
- package/src/i18n/messages/platform.js +33 -0
- package/src/identity/random.js +17 -3
- package/src/identity/schema.js +10 -0
- package/src/index.js +1 -0
package/cli/pet/pet.html
CHANGED
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
opacity: 0; transform: translate(-50%, 6px) scale(.97); transform-origin: 50% 100%; transition: opacity .22s, transform .22s; pointer-events: none; }
|
|
46
46
|
.bubble.on { opacity: 1; transform: translate(-50%, 0) scale(1); }
|
|
47
47
|
.bubble::after { content: ''; position: absolute; left: 50%; top: 100%; width: 12px; height: 12px; margin: -6px 0 0 -6px; background: inherit; border: inherit; border-width: 0 1px 1px 0; transform: rotate(45deg); }
|
|
48
|
+
.bubble.asking { pointer-events: auto; -webkit-app-region: no-drag; border-color: var(--accent); }
|
|
49
|
+
.bubble .q { display: block; margin-bottom: 8px; }
|
|
50
|
+
.bubble .yn { display: flex; gap: 6px; justify-content: flex-end; }
|
|
51
|
+
.bubble .yn button { font: inherit; font-size: 12.5px; font-weight: 650; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink); cursor: pointer; }
|
|
52
|
+
.bubble .yn button.ok { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
|
53
|
+
.bubble .yn button:hover { filter: brightness(1.08); }
|
|
48
54
|
.dots { display: inline-flex; gap: 4px; padding: 2px 0; }
|
|
49
55
|
.dots b { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: dot 1s infinite ease-in-out; }
|
|
50
56
|
.dots b:nth-child(2) { animation-delay: .15s; } .dots b:nth-child(3) { animation-delay: .3s; }
|
|
@@ -119,8 +125,11 @@
|
|
|
119
125
|
body.desktop .app { width: 100%; background: transparent; border: 0; box-shadow: none; border-radius: 0; overflow: visible; }
|
|
120
126
|
body.desktop .top, body.desktop .who, body.desktop .level, body.desktop .stats, body.desktop .feed, body.desktop .foot { display: none; }
|
|
121
127
|
body.desktop .hero { background: none; padding: 0 0 6px; }
|
|
122
|
-
body.desktop .stage { width: 140px; height: 148px; }
|
|
128
|
+
body.desktop .stage { width: calc(140px * var(--pet-scale, 1)); height: calc(148px * var(--pet-scale, 1)); } /* stretched from a corner (see 'resize' below) */
|
|
123
129
|
body.desktop .stage.flip .face { transform: scaleX(-1); }
|
|
130
|
+
/* the 3D ot is framed with room under its feet (~34px at 1x) that grows with the stretch: the figure is drawn
|
|
131
|
+
lower by what that room grew, so a bigger ot still stands where it stood; the stage (hit area) stays put */
|
|
132
|
+
body.desktop.ot3d .face { top: calc(34px * (var(--pet-scale, 1) - 1)); }
|
|
124
133
|
/* scenes with other ots (electron/director.cjs): the acts move .act; props (fireballs, beams, sabers, bullets) */
|
|
125
134
|
.actor { position: absolute; inset: 0; z-index: 1; transform-origin: 50% 92%; }
|
|
126
135
|
.props { position: absolute; inset: 0; z-index: 3; pointer-events: none; overflow: visible; }
|
|
@@ -137,7 +146,7 @@
|
|
|
137
146
|
.bye { display: none; }
|
|
138
147
|
body.desktop.guest .bye { display: flex; }
|
|
139
148
|
body.desktop .bubble { max-width: 220px; font-size: 12.5px; padding: 8px 12px; }
|
|
140
|
-
body.desktop .actions { position: absolute; right: auto; left: calc(50% +
|
|
149
|
+
body.desktop .actions { position: absolute; right: auto; left: calc(50% + 70px * var(--pet-scale, 1) - 4px); /* beside the ot at any size: over it, it hid the corners that stretch it */ bottom: 14px; transform: translateX(6px); display: flex; flex-direction: column; gap: 4px; padding: 4px; border-radius: 99px;
|
|
141
150
|
background: color-mix(in srgb, var(--panel) 86%, transparent); border: 1px solid var(--line); box-shadow: var(--shadow); backdrop-filter: blur(10px);
|
|
142
151
|
opacity: 0; transition: none; /* a transition here never finished painting in the transparent window: the menu stayed invisible */ -webkit-app-region: no-drag; z-index: 5; } /* above the 3D stage canvas (z 3) */
|
|
143
152
|
body.desktop:hover .actions, body.desktop.hover .actions, body.desktop.menu .actions { opacity: 1; transform: none; } /* .hover: set by the shell, which sees the cursor even when no mouse event reaches the page */
|
|
@@ -213,17 +222,17 @@
|
|
|
213
222
|
<script type="module">
|
|
214
223
|
const BOOT = __BOOT__;
|
|
215
224
|
const T = {
|
|
216
|
-
en: { settings: 'Settings', dismiss: 'Send home', listen: 'Listen', listening: 'Listening… 🎙️', noStt: 'Voice control needs an OpenAI or ElevenLabs key (Settings).', noMic: 'I can’t reach the microphone.', sttErr: 'I didn’t catch that.', talk: 'Talk', askPh: 'Remind me to check email in 10 min…', feed: 'Treat', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…',
|
|
225
|
+
en: { settings: 'Settings', dismiss: 'Send home', listen: 'Listen', listening: 'Listening… 🎙️', noStt: 'Voice control needs an OpenAI or ElevenLabs key (Settings).', noMic: 'I can’t reach the microphone.', sttErr: 'I didn’t catch that.', talk: 'Talk', askPh: 'Remind me to check email in 10 min…', feed: 'Treat', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…', approve: 'Approve', reject: 'Reject',
|
|
217
226
|
activity: 'Activity', empty: 'Nothing yet. Run a command or ask your coding agent something.', tally: '{ok} ok · {fail} failed · turns {turns}',
|
|
218
227
|
brain: 'Brain: {b}', age: 'Born {d}', lines: 'built-in phrases', now: 'now', m: '{n}m', h: '{n}h',
|
|
219
228
|
moods: { happy: 'Happy', neutral: 'Calm', sad: 'Sad', disgust: 'Bored', sleep: 'Asleep', thinking: 'Thinking' },
|
|
220
229
|
kind: { tool: 'Tool', shell: 'Shell', prompt: 'You asked', stop: 'Turn finished', notify: 'Needs you', start: 'Session started' } },
|
|
221
|
-
es: { settings: 'Ajustes', dismiss: 'Despedir', listen: 'Escuchar', listening: 'Te escucho… 🎙️', noStt: 'Para hablarme por voz falta una key de OpenAI o ElevenLabs (Ajustes).', noMic: 'No puedo usar el micrófono.', sttErr: 'No te entendí.', talk: 'Hablar', askPh: 'Recuérdame ver el correo en 10 min…', feed: 'Mimar', play: 'Jugar', sleep: 'Dormir', wake: 'Despertar', food: 'Comida', energy: 'Energía', fun: 'Diversión', level: 'Nivel {l}', xp: '{x} / {n} xp', live: 'En vivo', off: 'Reconectando…',
|
|
230
|
+
es: { settings: 'Ajustes', dismiss: 'Despedir', listen: 'Escuchar', listening: 'Te escucho… 🎙️', noStt: 'Para hablarme por voz falta una key de OpenAI o ElevenLabs (Ajustes).', noMic: 'No puedo usar el micrófono.', sttErr: 'No te entendí.', talk: 'Hablar', askPh: 'Recuérdame ver el correo en 10 min…', feed: 'Mimar', play: 'Jugar', sleep: 'Dormir', wake: 'Despertar', food: 'Comida', energy: 'Energía', fun: 'Diversión', level: 'Nivel {l}', xp: '{x} / {n} xp', live: 'En vivo', off: 'Reconectando…', approve: 'Aprobar', reject: 'Rechazar',
|
|
222
231
|
activity: 'Actividad', empty: 'Nada todavía. Ejecuta un comando o pregúntale algo a tu agente.', tally: '{ok} ok · {fail} fallos · turnos {turns}',
|
|
223
232
|
brain: 'Cerebro: {b}', age: 'Nació {d}', lines: 'frases propias', now: 'ahora', m: '{n} min', h: '{n} h',
|
|
224
233
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Aburrido', sleep: 'Dormido', thinking: 'Pensando' },
|
|
225
234
|
kind: { tool: 'Herramienta', shell: 'Terminal', prompt: 'Preguntaste', stop: 'Turno terminado', notify: 'Te necesita', start: 'Sesión iniciada' } },
|
|
226
|
-
pt: { settings: 'Ajustes', dismiss: 'Despedir', listen: 'Ouvir', listening: 'Estou ouvindo… 🎙️', noStt: 'Para falar por voz falta uma key da OpenAI ou ElevenLabs (Ajustes).', noMic: 'Não consigo usar o microfone.', sttErr: 'Não entendi.', talk: 'Falar', askPh: 'Me lembre de ver o e-mail em 10 min…', feed: 'Mimar', play: 'Brincar', sleep: 'Dormir', wake: 'Acordar', food: 'Comida', energy: 'Energia', fun: 'Diversão', level: 'Nível {l}', xp: '{x} / {n} xp', live: 'Ao vivo', off: 'Reconectando…',
|
|
235
|
+
pt: { settings: 'Ajustes', dismiss: 'Despedir', listen: 'Ouvir', listening: 'Estou ouvindo… 🎙️', noStt: 'Para falar por voz falta uma key da OpenAI ou ElevenLabs (Ajustes).', noMic: 'Não consigo usar o microfone.', sttErr: 'Não entendi.', talk: 'Falar', askPh: 'Me lembre de ver o e-mail em 10 min…', feed: 'Mimar', play: 'Brincar', sleep: 'Dormir', wake: 'Acordar', food: 'Comida', energy: 'Energia', fun: 'Diversão', level: 'Nível {l}', xp: '{x} / {n} xp', live: 'Ao vivo', off: 'Reconectando…', approve: 'Aprovar', reject: 'Recusar',
|
|
227
236
|
activity: 'Atividade', empty: 'Nada ainda. Rode um comando ou pergunte algo ao seu agente.', tally: '{ok} ok · {fail} falhas · turnos {turns}',
|
|
228
237
|
brain: 'Cérebro: {b}', age: 'Nasceu {d}', lines: 'frases próprias', now: 'agora', m: '{n} min', h: '{n} h',
|
|
229
238
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Entediado', sleep: 'Dormindo', thinking: 'Pensando' },
|
|
@@ -272,6 +281,7 @@
|
|
|
272
281
|
}
|
|
273
282
|
ch ||= createCharacter($('#face'), spec, { locale: lang, title: id.name });
|
|
274
283
|
const is3d = !!ch.character;
|
|
284
|
+
document.body.classList.toggle('ot3d', is3d);
|
|
275
285
|
// While a gesture/effect/morph plays the ot needs room around it (wings, hearts, confetti): the desktop
|
|
276
286
|
// shell reads window.__need() and sizes the window to what is really on screen.
|
|
277
287
|
let fxUntil = 0;
|
|
@@ -319,6 +329,7 @@
|
|
|
319
329
|
}
|
|
320
330
|
if (document.body.classList.contains('talking')) (w = Math.max(w, 240)), (h = Math.max(h, st.height + 60));
|
|
321
331
|
if (Date.now() < actUntil) (w = Math.max(w, actRoom * 2)), (h = Math.max(h, st.height + 70));
|
|
332
|
+
w = Math.max(w, st.width + 2 * 58); // the menu beside it (shown on hover)
|
|
322
333
|
return { w: Math.ceil(w), h: Math.ceil(h) };
|
|
323
334
|
};
|
|
324
335
|
// The shell polls __need every 250 ms; when an effect starts it's told at once (a console line it listens
|
|
@@ -341,9 +352,53 @@
|
|
|
341
352
|
const r = $('#stage').getBoundingClientRect();
|
|
342
353
|
return e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
|
343
354
|
};
|
|
355
|
+
// Size: grabbing the ot by a corner stretches it (bigger or smaller) instead of moving it; remembered per ot.
|
|
356
|
+
const SCALE_KEY = `7ots.petScale${guest ? `.${guest}` : ''}`;
|
|
357
|
+
let scale = 1;
|
|
358
|
+
try {
|
|
359
|
+
scale = Math.max(0.6, Math.min(2, Number(localStorage.getItem(SCALE_KEY)) || 1));
|
|
360
|
+
} catch {}
|
|
361
|
+
const setScale = (k) => ((scale = k), document.documentElement.style.setProperty('--pet-scale', k.toFixed(3)), tellNeed());
|
|
362
|
+
if (desktop && scale !== 1) setScale(scale);
|
|
363
|
+
const CORNER = 22;
|
|
364
|
+
// which corner of the ot the pointer is on ('nw' | 'ne' | 'sw' | 'se'), or ''
|
|
365
|
+
const cornerAt = (e) => {
|
|
366
|
+
if (!desktop || !onOt(e)) return '';
|
|
367
|
+
const r = $('#stage').getBoundingClientRect();
|
|
368
|
+
const v = e.clientY - r.top < CORNER ? 'n' : r.bottom - e.clientY < CORNER ? 's' : '';
|
|
369
|
+
const h = e.clientX - r.left < CORNER ? 'w' : r.right - e.clientX < CORNER ? 'e' : '';
|
|
370
|
+
return v && h ? v + h : '';
|
|
371
|
+
};
|
|
344
372
|
if (desktop) {
|
|
345
373
|
let down = false;
|
|
346
|
-
|
|
374
|
+
let sizing = null;
|
|
375
|
+
let pulled = null; // { m, on }: a drag that started off the corners
|
|
376
|
+
const up = () => {
|
|
377
|
+
if (pulled?.on) {
|
|
378
|
+
ch.unmorph?.(pulled.m); // let go: back into shape
|
|
379
|
+
fxUntil = Date.now() + 600; // room while it eases back
|
|
380
|
+
}
|
|
381
|
+
pulled = null;
|
|
382
|
+
if (sizing) {
|
|
383
|
+
sizing = null;
|
|
384
|
+
try {
|
|
385
|
+
localStorage.setItem(SCALE_KEY, String(scale));
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
if (down) (down = false), console.debug('\u00a77drag' + JSON.stringify({ on: false }));
|
|
389
|
+
};
|
|
390
|
+
addEventListener('pointermove', (e) => {
|
|
391
|
+
if (sizing) {
|
|
392
|
+
// the ot stays anchored at its feet, centred: sideways it grows on both sides, so a corner moves half as much
|
|
393
|
+
const dx = (e.screenX - sizing.x) * (sizing.c[1] === 'e' ? 1 : -1) * 2;
|
|
394
|
+
const dy = (e.screenY - sizing.y) * (sizing.c[0] === 's' ? 1 : -1);
|
|
395
|
+
const k = sizing.k * ((140 * sizing.k + dx) / (140 * sizing.k) + (148 * sizing.k + dy) / (148 * sizing.k)) / 2;
|
|
396
|
+
return setScale(Math.max(0.6, Math.min(2, k)));
|
|
397
|
+
}
|
|
398
|
+
if (down) return;
|
|
399
|
+
const c = cornerAt(e);
|
|
400
|
+
document.body.style.cursor = c ? (c === 'nw' || c === 'se' ? 'nwse-resize' : 'nesw-resize') : '';
|
|
401
|
+
});
|
|
347
402
|
// on the document: in 3D the ot is drawn on a canvas above the page, so events don't target #stage
|
|
348
403
|
addEventListener('pointerdown', (e) => {
|
|
349
404
|
// on the ot itself, or on the move handle in the menu (a sure grip while the ot is mid-animation)
|
|
@@ -351,14 +406,50 @@
|
|
|
351
406
|
if (e.button !== 0 || (!grip && (e.target.closest?.('.actions, .ask, a, button') || !onOt(e)))) return;
|
|
352
407
|
down = true;
|
|
353
408
|
document.documentElement.setPointerCapture?.(e.pointerId);
|
|
354
|
-
|
|
409
|
+
const c = !grip && cornerAt(e);
|
|
410
|
+
if (c) sizing = { c, x: e.screenX, y: e.screenY, k: scale };
|
|
411
|
+
else if (!grip) {
|
|
412
|
+
const r = $('#stage').getBoundingClientRect();
|
|
413
|
+
const side = Math.abs(e.clientX - (r.left + r.width / 2)) / r.width > Math.abs(e.clientY - (r.top + r.height / 2)) / r.height;
|
|
414
|
+
pulled = { m: side ? 'squish' : 'stretch', on: false };
|
|
415
|
+
}
|
|
416
|
+
// resizing: the shell keeps the mouse but leaves the window where it is
|
|
417
|
+
console.debug('\u00a77drag' + JSON.stringify({ on: true, resize: !!c }));
|
|
418
|
+
});
|
|
419
|
+
// pulled by a side or the top: once the shell says the window is really being carried (the page sees
|
|
420
|
+
// almost no pointer motion then: the window moves with the hand), it stretches toward the pull — wider
|
|
421
|
+
// from a side, taller from above or below — until it's let go
|
|
422
|
+
addEventListener('message', (e) => {
|
|
423
|
+
if (!e.data?.carried || !pulled || pulled.on) return;
|
|
424
|
+
pulled.on = true;
|
|
425
|
+
fxUntil = Infinity; // room around it for as long as it's held stretched
|
|
426
|
+
ch.morph?.(pulled.m, { hold: true });
|
|
427
|
+
tellNeed();
|
|
355
428
|
});
|
|
356
429
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) addEventListener(ev, up);
|
|
357
430
|
addEventListener('blur', up);
|
|
358
431
|
}
|
|
359
432
|
let lookFree = 0; // while > now, the eyes wander on their own (a glance) instead of following the cursor
|
|
360
433
|
addEventListener('pointermove', (e) => Date.now() > lookFree && ch.lookAtPoint(e.clientX, e.clientY));
|
|
361
|
-
|
|
434
|
+
// A click on the ot pokes it: it gets squished and, poked again and again, more and more annoyed.
|
|
435
|
+
// Waits a beat so a double click (the menu) doesn't count as two pokes; a drag or a stretch isn't a poke.
|
|
436
|
+
let pressAt = null, pokeT = 0, pokes = 0, pokedAt = 0;
|
|
437
|
+
addEventListener('pointerdown', (e) => (pressAt = { x: e.screenX, y: e.screenY }));
|
|
438
|
+
addEventListener('click', (e) => {
|
|
439
|
+
clearTimeout(pokeT);
|
|
440
|
+
if (e.detail > 1 || !onOt(e) || e.target.closest?.('.actions, .ask, .bubble.asking, a, button')) return;
|
|
441
|
+
if (pressAt && Math.hypot(e.screenX - pressAt.x, e.screenY - pressAt.y) > 4) return;
|
|
442
|
+
pokeT = setTimeout(poke, 240);
|
|
443
|
+
});
|
|
444
|
+
function poke() {
|
|
445
|
+
pokes = Date.now() - pokedAt < 2500 ? pokes + 1 : 1;
|
|
446
|
+
pokedAt = Date.now();
|
|
447
|
+
lookFree = 0;
|
|
448
|
+
ch.morph?.('squish', { ms: 420 });
|
|
449
|
+
if (pokes === 1) ch.gesture('bounce');
|
|
450
|
+
else if (pokes <= 3) (ch.gesture('surprise'), ch.effect?.('exclaim'));
|
|
451
|
+
else (ch.gesture(pokes % 2 ? 'stomp' : 'shake'), ch.effect?.('anger'), pokes >= 6 && ch.effect?.('steam'));
|
|
452
|
+
}
|
|
362
453
|
|
|
363
454
|
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': BOOT.token }, body: JSON.stringify(body) });
|
|
364
455
|
let asleep = false;
|
|
@@ -501,6 +592,8 @@
|
|
|
501
592
|
speechSynthesis.speak(u);
|
|
502
593
|
}
|
|
503
594
|
function showBubble(content, ms) {
|
|
595
|
+
if (asking && !content.classList?.contains('askq')) return; // a question waiting for you stays until answered
|
|
596
|
+
bubble.classList.remove('asking');
|
|
504
597
|
bubble.replaceChildren(content);
|
|
505
598
|
bubble.classList.add('on');
|
|
506
599
|
clearTimeout(hideT);
|
|
@@ -537,6 +630,42 @@
|
|
|
537
630
|
if (withFace) react(text);
|
|
538
631
|
speak(text);
|
|
539
632
|
}
|
|
633
|
+
// It asks before acting for you (Orquesta…): Approve / Reject in the bubble, answered on POST /approve.
|
|
634
|
+
let asking = null;
|
|
635
|
+
function ask4(a) {
|
|
636
|
+
if (a.done) {
|
|
637
|
+
if (asking !== a.id) return;
|
|
638
|
+
asking = null;
|
|
639
|
+
bubble.classList.remove('on', 'asking');
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
asking = null;
|
|
643
|
+
const box = document.createElement('div');
|
|
644
|
+
box.className = 'askq';
|
|
645
|
+
const q = document.createElement('span');
|
|
646
|
+
q.className = 'q';
|
|
647
|
+
q.textContent = a.text;
|
|
648
|
+
const yn = document.createElement('div');
|
|
649
|
+
yn.className = 'yn';
|
|
650
|
+
const btn = (ok) => {
|
|
651
|
+
const b = document.createElement('button');
|
|
652
|
+
b.type = 'button';
|
|
653
|
+
b.textContent = t(ok ? 'approve' : 'reject');
|
|
654
|
+
if (ok) b.className = 'ok';
|
|
655
|
+
b.addEventListener('click', () => {
|
|
656
|
+
yn.querySelectorAll('button').forEach((x) => (x.disabled = true));
|
|
657
|
+
post('/approve', { id: a.id, ok }).catch(() => {});
|
|
658
|
+
});
|
|
659
|
+
return b;
|
|
660
|
+
};
|
|
661
|
+
yn.append(btn(false), btn(true));
|
|
662
|
+
box.append(q, yn);
|
|
663
|
+
showBubble(box, 0);
|
|
664
|
+
bubble.classList.add('asking');
|
|
665
|
+
asking = a.id;
|
|
666
|
+
ch.gesture('think');
|
|
667
|
+
speak(a.text);
|
|
668
|
+
}
|
|
540
669
|
function setThinking(on) {
|
|
541
670
|
if (on === thinking) return;
|
|
542
671
|
thinking = on;
|
|
@@ -546,7 +675,7 @@
|
|
|
546
675
|
d.className = 'dots';
|
|
547
676
|
d.innerHTML = '<b></b><b></b><b></b>';
|
|
548
677
|
showBubble(d, 0);
|
|
549
|
-
} else if (bubble.querySelector('.dots')) bubble.classList.remove('on');
|
|
678
|
+
} else if (!asking && bubble.querySelector('.dots')) bubble.classList.remove('on');
|
|
550
679
|
}
|
|
551
680
|
|
|
552
681
|
// ── live updates ──
|
|
@@ -582,6 +711,7 @@
|
|
|
582
711
|
if (guest) return; // the host's events are the host's
|
|
583
712
|
if (m.stir || (m.say && asleep && Date.now() < stirUntil)) stir(); // awake until it has said it
|
|
584
713
|
if (m.state) render(m.state);
|
|
714
|
+
if (m.ask) ask4(m.ask); // before thinking: an approved ask hands the bubble back to the dots
|
|
585
715
|
if ('thinking' in m) setThinking(!!m.thinking);
|
|
586
716
|
if (m.activity) addActivity(m.activity);
|
|
587
717
|
if (m.gesture) ch.gesture(m.gesture);
|
|
@@ -600,6 +730,7 @@
|
|
|
600
730
|
askIn.placeholder = t('askPh');
|
|
601
731
|
$('#setBtn').href = `/settings?token=${encodeURIComponent(BOOT.token)}`;
|
|
602
732
|
$('#setBtn').title = t('settings');
|
|
733
|
+
if (!desktop) $('#setBtn').removeAttribute('target'); // in a browser tab, Settings opens in place (its Back returns here); the desktop window opens a tab
|
|
603
734
|
$('#byeBtn').title = t('dismiss');
|
|
604
735
|
$('#byeBtn').addEventListener('click', () => console.debug('\u00a77bye')); // the shell sends the visitor home
|
|
605
736
|
const talking = (on) => {
|
|
@@ -617,6 +748,14 @@
|
|
|
617
748
|
clearTimeout(menuT);
|
|
618
749
|
if (on) menuT = setTimeout(() => document.body.classList.remove('menu'), 8000);
|
|
619
750
|
});
|
|
751
|
+
// right click: the shell's menu (settings, talk, sleep, hide…) at the cursor. Asked for by name: Electron's own
|
|
752
|
+
// 'context-menu' event didn't reach the shell from this transparent click-through window on Linux.
|
|
753
|
+
if (desktop)
|
|
754
|
+
addEventListener('contextmenu', (e) => {
|
|
755
|
+
if (e.target.closest?.('.ask input')) return; // the talk box keeps copy/paste
|
|
756
|
+
e.preventDefault();
|
|
757
|
+
console.debug('\u00a77ctx');
|
|
758
|
+
});
|
|
620
759
|
askIn.addEventListener('keydown', (e) => e.key === 'Escape' && talking(false));
|
|
621
760
|
ask.addEventListener('submit', async (e) => {
|
|
622
761
|
e.preventDefault();
|
|
@@ -834,7 +973,7 @@
|
|
|
834
973
|
[hungry ? 1.2 : 0, () => (ch.gesture('point'), ch.effect?.('thought'))],
|
|
835
974
|
[0.6, () => ch.effect?.('blush')],
|
|
836
975
|
[0.3, () => ch.gesture('shiver')],
|
|
837
|
-
[1.2, () => ch.morph?.(pick(['puff', 'squish', 'stretch'
|
|
976
|
+
[1.2, () => ch.morph?.(pick(['puff', 'squish', 'stretch']), { ms: 1600 + Math.random() * 1600 })],
|
|
838
977
|
[0.7, () => ch.morph?.(pick(['wings', 'horns', 'spikes', 'hands']), { ms: 2500 + Math.random() * 2500 })],
|
|
839
978
|
[0.8, () => ch.morph?.(`shape:${pick(SHAPES)}`, { ms: 2500 + Math.random() * 3000 })],
|
|
840
979
|
])?.();
|