@7ots/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -1
- package/cli/7ots.mjs +133 -36
- package/cli/lib/assistant.mjs +337 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/i18n.mjs +78 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +30 -0
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +79 -28
- package/cli/pet/pet.html +55 -6
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- package/src/index.js +1 -0
package/cli/lib/terminal.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal mode: the pet drawn with ANSI in the current terminal, fed by the daemon's SSE.
|
|
3
3
|
* Keys: f feed · p play · s sleep/wake · q quit (the daemon keeps running if --detach started it).
|
|
4
|
+
* Only redraws when the frame changes, line by line (no full clears: no flicker, fine inside tmux).
|
|
5
|
+
* compact: a few narrow lines for a small tmux pane.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { PET_PORT } from './paths.mjs';
|
|
@@ -46,7 +48,7 @@ function bar(v, width = 16) {
|
|
|
46
48
|
return `${color}${'█'.repeat(n)}${DIM}${'░'.repeat(width - n)}${RESET} ${String(v).padStart(3)}`;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export async function runTerminal({ identity, lang, port = PET_PORT, bell = false }) {
|
|
51
|
+
export async function runTerminal({ identity, lang, port = PET_PORT, bell = false, compact = false }) {
|
|
50
52
|
const out = process.stdout;
|
|
51
53
|
const color = identity.look.color;
|
|
52
54
|
let state = null;
|
|
@@ -55,15 +57,26 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
55
57
|
let thinking = false;
|
|
56
58
|
let blink = false;
|
|
57
59
|
let hop = 0;
|
|
60
|
+
let last = '';
|
|
58
61
|
|
|
59
|
-
const
|
|
60
|
-
if (!state) return;
|
|
62
|
+
const frame = () => {
|
|
61
63
|
const [l, m, r] = FACES[state.mood] || FACES.neutral;
|
|
62
64
|
const eyesL = blink && !state.asleep ? '-' : l;
|
|
63
65
|
const eyesR = blink && !state.asleep ? '-' : r;
|
|
64
66
|
const c = fg(color);
|
|
65
67
|
const lines = [];
|
|
66
68
|
const now = Date.now();
|
|
69
|
+
if (compact) {
|
|
70
|
+
const say = bubble && now < bubbleUntil ? bubble : thinking ? '…' : '';
|
|
71
|
+
const w = Math.max(16, (out.columns || 28) - 2);
|
|
72
|
+
lines.push(`${c}(${RESET}${BOLD}${eyesL}${RESET}${m}${BOLD}${eyesR}${RESET}${c})${RESET} ${BOLD}${identity.name}${RESET}${state.asleep ? `${DIM} zz${RESET}` : ''}`);
|
|
73
|
+
lines.push(`${t('pet.food', lang).slice(0, 3)} ${bar(state.food, 6)}`);
|
|
74
|
+
lines.push(`${t('pet.energy', lang).slice(0, 3)} ${bar(state.energy, 6)}`);
|
|
75
|
+
lines.push(`${t('pet.fun', lang).slice(0, 3)} ${bar(state.fun, 6)}`);
|
|
76
|
+
for (const x of wrap(say, w).slice(0, 4)) lines.push(`${DIM}${x}${RESET}`);
|
|
77
|
+
lines.push(`${DIM}f p s q${RESET}`);
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
67
80
|
if (bubble && now < bubbleUntil) {
|
|
68
81
|
const w = wrap(bubble, 38);
|
|
69
82
|
const width = Math.max(...w.map((x) => x.length));
|
|
@@ -85,7 +98,15 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
85
98
|
lines.push(` ${DIM}${t('pet.level', lang, { level: state.level, xp: state.xp, next: state.next })}${RESET}`);
|
|
86
99
|
lines.push('');
|
|
87
100
|
lines.push(` ${DIM}${t('pet.keys', lang)}${RESET}`);
|
|
88
|
-
|
|
101
|
+
return lines;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const draw = (force = false) => {
|
|
105
|
+
if (!state) return;
|
|
106
|
+
const f = frame().map((x) => x + '\x1b[K').join('\r\n');
|
|
107
|
+
if (f === last && !force) return;
|
|
108
|
+
last = f;
|
|
109
|
+
out.write('\x1b[H' + f + '\x1b[J');
|
|
89
110
|
};
|
|
90
111
|
|
|
91
112
|
out.write('\x1b[?1049h\x1b[?25l');
|
|
@@ -103,11 +124,11 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
103
124
|
if (k === 's') postPet('/event', { type: state?.asleep ? 'wake' : 'sleep' }, port);
|
|
104
125
|
});
|
|
105
126
|
}
|
|
106
|
-
out.on('resize', draw);
|
|
127
|
+
out.on('resize', () => draw(true));
|
|
107
128
|
setInterval(() => {
|
|
108
129
|
blink = Math.random() < 0.08;
|
|
109
130
|
if (hop) hop--;
|
|
110
|
-
draw();
|
|
131
|
+
draw(); // no-op unless something visible changed (blink, hop, bubble expiring)
|
|
111
132
|
}, 500);
|
|
112
133
|
|
|
113
134
|
// SSE over fetch (Node has no EventSource without flags).
|
|
@@ -1,63 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Desktop mode: the pet page in a transparent, frameless, always-on-top window that strolls
|
|
3
3
|
* along the bottom of the screen. Launched by `7ots pet --mode desktop` with
|
|
4
|
-
*
|
|
5
|
-
* and SEVENOTS_PET_URL / SEVENOTS_PET_TOKEN in the environment.
|
|
4
|
+
* ~/.7ots/electron/…/electron cli/pet/electron/main.cjs (installed once by the CLI)
|
|
5
|
+
* and SEVENOTS_PET_URL / SEVENOTS_PET_TOKEN in the environment.
|
|
6
|
+
* Menu: right click on the pet, or the tray icon (Show / Hide / care / Quit).
|
|
7
|
+
* SEVENOTS_PET_OPAQUE=1 paints a solid card instead of transparency (Linux setups without a compositor).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
const
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { app, BrowserWindow, Menu, Tray, nativeImage, screen } = require('electron');
|
|
9
12
|
|
|
10
13
|
const URL_ = process.env.SEVENOTS_PET_URL;
|
|
11
14
|
const TOKEN = process.env.SEVENOTS_PET_TOKEN;
|
|
15
|
+
const OPAQUE = process.env.SEVENOTS_PET_OPAQUE === '1';
|
|
16
|
+
const VIEW3D = process.env.SEVENOTS_PET_3D === '1';
|
|
17
|
+
const LANG = String(process.env.SEVENOTS_PET_LANG || 'en').slice(0, 2);
|
|
18
|
+
const ICON = path.join(__dirname, '..', '..', '..', 'brand', 'icon-512.png');
|
|
12
19
|
const W = 240;
|
|
13
20
|
const H = 270;
|
|
21
|
+
const linux = process.platform === 'linux';
|
|
22
|
+
|
|
23
|
+
const L = {
|
|
24
|
+
en: { talk: '💬 Talk', show: 'Show pet', hide: 'Hide (keeps living)', feed: '🍎 Feed', play: '🎾 Play', sleep: '💤 Sleep', wake: '☀️ Wake up', still: 'Stay still', quit: 'Quit pet' },
|
|
25
|
+
es: { talk: '💬 Hablar', show: 'Mostrar mascota', hide: 'Ocultar (sigue viva)', feed: '🍎 Dar comida', play: '🎾 Jugar', sleep: '💤 Dormir', wake: '☀️ Despertar', still: 'Quedarse quieta', quit: 'Cerrar mascota' },
|
|
26
|
+
pt: { talk: '💬 Falar', show: 'Mostrar bichinho', hide: 'Esconder (segue vivo)', feed: '🍎 Dar comida', play: '🎾 Brincar', sleep: '💤 Dormir', wake: '☀️ Acordar', still: 'Ficar parado', quit: 'Fechar bichinho' },
|
|
27
|
+
}[LANG] || {};
|
|
28
|
+
const tr = (k) => L[k] || { show: 'Show pet', hide: 'Hide', talk: 'Talk', feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake up', still: 'Stay still', quit: 'Quit pet' }[k];
|
|
14
29
|
|
|
15
|
-
app.commandLine.appendSwitch('enable-transparent-visuals'); // Linux compositors
|
|
16
30
|
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); // it talks without a click
|
|
31
|
+
if (linux && !OPAQUE) {
|
|
32
|
+
// Transparent windows on Linux: these avoid the black/grey box on most X11 setups.
|
|
33
|
+
app.commandLine.appendSwitch('enable-transparent-visuals');
|
|
34
|
+
if (!VIEW3D) app.disableHardwareAcceleration(); // the 3D ot needs the GPU (WebGL)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const post = (p, body) =>
|
|
38
|
+
fetch(new URL(p, URL_), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': TOKEN }, body: JSON.stringify(body || {}) }).catch(() => {});
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
fetch(new URL(path, URL_), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': TOKEN }, body: JSON.stringify(body || {}) }).catch(() => {});
|
|
40
|
+
app.whenReady().then(() => setTimeout(start, linux ? 400 : 0)); // Linux: the compositor needs a moment
|
|
20
41
|
|
|
21
|
-
|
|
42
|
+
function start() {
|
|
22
43
|
const area = screen.getPrimaryDisplay().workArea;
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
const home = () => [area.x + area.width - W - 24, area.y + area.height - H];
|
|
45
|
+
let [x, y] = home();
|
|
46
|
+
|
|
47
|
+
// Tray first: if there is one, the window can stay out of the taskbar; if not, it shows there so it can be found.
|
|
48
|
+
let tray = null;
|
|
49
|
+
try {
|
|
50
|
+
const img = nativeImage.createFromPath(ICON);
|
|
51
|
+
if (!img.isEmpty()) {
|
|
52
|
+
tray = new Tray(img.resize({ width: 22, height: 22 }));
|
|
53
|
+
tray.setToolTip('7ots pet');
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
25
57
|
const win = new BrowserWindow({
|
|
26
58
|
width: W,
|
|
27
59
|
height: H,
|
|
28
60
|
x,
|
|
29
61
|
y,
|
|
30
|
-
transparent:
|
|
62
|
+
transparent: !OPAQUE,
|
|
31
63
|
frame: false,
|
|
32
64
|
resizable: false,
|
|
33
|
-
hasShadow:
|
|
65
|
+
hasShadow: OPAQUE,
|
|
34
66
|
alwaysOnTop: true,
|
|
35
|
-
skipTaskbar:
|
|
36
|
-
|
|
67
|
+
skipTaskbar: !!tray,
|
|
68
|
+
title: '7ots pet',
|
|
69
|
+
icon: ICON,
|
|
70
|
+
backgroundColor: OPAQUE ? '#ffffff' : '#00000000',
|
|
37
71
|
webPreferences: { contextIsolation: true, sandbox: true },
|
|
38
72
|
});
|
|
39
73
|
win.setAlwaysOnTop(true, 'floating');
|
|
40
74
|
win.setVisibleOnAllWorkspaces?.(true);
|
|
41
|
-
win.loadURL(`${URL_}/pet?desktop=1&token=${encodeURIComponent(TOKEN)}`);
|
|
75
|
+
win.loadURL(`${URL_}/pet?desktop=1${OPAQUE ? '&opaque=1' : ''}${VIEW3D ? '&3d=1' : ''}&token=${encodeURIComponent(TOKEN)}`);
|
|
42
76
|
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
43
77
|
win.webContents.on('will-navigate', (e) => e.preventDefault());
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
let still = false;
|
|
80
|
+
const show = () => {
|
|
81
|
+
[x, y] = home();
|
|
82
|
+
win.setPosition(x, y);
|
|
83
|
+
win.showInactive();
|
|
84
|
+
win.moveTop();
|
|
85
|
+
};
|
|
86
|
+
const menu = () =>
|
|
87
|
+
Menu.buildFromTemplate([
|
|
88
|
+
win.isVisible() ? { label: tr('hide'), click: () => win.hide() } : { label: tr('show'), click: show },
|
|
89
|
+
{ type: 'separator' },
|
|
90
|
+
{ label: tr('talk'), click: () => { show(); win.focus(); win.webContents.executeJavaScript('postMessage({ talk: true })').catch(() => {}); } },
|
|
91
|
+
{ label: tr('feed'), click: () => post('/event', { type: 'feed' }) },
|
|
92
|
+
{ label: tr('play'), click: () => post('/event', { type: 'play' }) },
|
|
93
|
+
{ label: tr('sleep'), click: () => post('/event', { type: 'sleep' }) },
|
|
94
|
+
{ label: tr('wake'), click: () => post('/event', { type: 'wake' }) },
|
|
95
|
+
{ type: 'separator' },
|
|
96
|
+
{ label: tr('still'), type: 'checkbox', checked: still, click: (i) => (still = i.checked) },
|
|
97
|
+
{ label: tr('quit'), click: () => post('/quit').finally(() => app.quit()) },
|
|
98
|
+
]);
|
|
99
|
+
win.webContents.on('context-menu', () => menu().popup({ window: win }));
|
|
100
|
+
if (tray) {
|
|
101
|
+
const refresh = () => tray.setContextMenu(menu()); // Linux trays only show the context menu
|
|
102
|
+
refresh();
|
|
103
|
+
win.on('show', refresh);
|
|
104
|
+
win.on('hide', refresh);
|
|
105
|
+
tray.on('click', () => (win.isVisible() ? win.hide() : show()));
|
|
106
|
+
}
|
|
56
107
|
|
|
57
108
|
// Walk: a few seconds walking, a few standing, turn at the edges.
|
|
58
109
|
let dir = -1;
|
|
59
110
|
let walking = false;
|
|
60
|
-
let still = false;
|
|
61
111
|
let dragged = 0;
|
|
62
112
|
let lastSet = x;
|
|
63
113
|
win.on('moved', () => {
|
|
@@ -70,7 +120,7 @@ app.whenReady().then(() => {
|
|
|
70
120
|
tell();
|
|
71
121
|
}, 4000);
|
|
72
122
|
setInterval(() => {
|
|
73
|
-
if (!walking || still || Date.now() - dragged < 8000 || win.isDestroyed()) return;
|
|
123
|
+
if (!walking || still || !win.isVisible() || Date.now() - dragged < 8000 || win.isDestroyed()) return;
|
|
74
124
|
const [cx, cy] = win.getPosition();
|
|
75
125
|
x = cx + dir * 2;
|
|
76
126
|
if (x < area.x || x > area.x + area.width - W) {
|
|
@@ -81,6 +131,7 @@ app.whenReady().then(() => {
|
|
|
81
131
|
lastSet = x;
|
|
82
132
|
win.setPosition(x, cy);
|
|
83
133
|
}, 40);
|
|
84
|
-
}
|
|
134
|
+
}
|
|
85
135
|
|
|
136
|
+
// Hidden is not closed: the pet lives on in the tray until Quit.
|
|
86
137
|
app.on('window-all-closed', () => app.quit());
|
package/cli/pet/pet.html
CHANGED
|
@@ -108,7 +108,11 @@
|
|
|
108
108
|
.foot { display: flex; gap: 6px 12px; flex-wrap: wrap; justify-content: space-between; padding: 10px 18px 14px; color: var(--muted); font-size: 11.5px; border-top: 1px solid var(--line); background: var(--panel-2); }
|
|
109
109
|
.foot b { color: var(--ink-2); font-weight: 600; }
|
|
110
110
|
|
|
111
|
+
.ask { display: flex; width: 100%; margin: 4px 0 2px; }
|
|
112
|
+
.ask input { flex: 1; min-width: 0; height: 36px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--panel-2); color: var(--ink); font: inherit; font-size: 13px; outline: none; }
|
|
113
|
+
.ask input:focus { border-color: var(--accent); }
|
|
111
114
|
/* ── desktop: transparent frameless window walking along the bottom of the screen ── */
|
|
115
|
+
html:has(body.desktop:not(.opaque)) { background: transparent; } /* html also paints --bg: without this the window is a white box */
|
|
112
116
|
body.desktop { background: transparent; padding: 0; place-items: end center; height: 100vh; overflow: hidden; -webkit-app-region: drag; user-select: none; }
|
|
113
117
|
body.desktop .app { width: 100%; background: transparent; border: 0; box-shadow: none; border-radius: 0; overflow: visible; }
|
|
114
118
|
body.desktop .top, body.desktop .who, body.desktop .level, body.desktop .stats, body.desktop .feed, body.desktop .foot { display: none; }
|
|
@@ -124,9 +128,13 @@
|
|
|
124
128
|
body.desktop .act span { display: none; }
|
|
125
129
|
body.desktop .act.primary { background: var(--accent); }
|
|
126
130
|
body.desktop .badge { display: flex; }
|
|
131
|
+
body.desktop .ask { display: none; position: absolute; left: 6px; right: 6px; top: 6px; margin: 0; z-index: 4; -webkit-app-region: no-drag; }
|
|
132
|
+
body.desktop.talking .ask { display: flex; }
|
|
127
133
|
.badge { display: none; position: absolute; z-index: 2; left: -6px; bottom: 18px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 99px; align-items: center; justify-content: center;
|
|
128
134
|
background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow); font: 750 11px/1 var(--font); color: var(--ink); }
|
|
129
135
|
body.desktop .floor { bottom: 0; }
|
|
136
|
+
/* --opaque: a solid card instead of transparency (for Linux setups that paint a box behind the window) */
|
|
137
|
+
body.desktop.opaque { background: var(--panel); }
|
|
130
138
|
|
|
131
139
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
|
132
140
|
</style>
|
|
@@ -147,6 +155,7 @@
|
|
|
147
155
|
<div class="floor" aria-hidden="true"></div>
|
|
148
156
|
<span class="badge" id="badge"></span>
|
|
149
157
|
</div>
|
|
158
|
+
<form class="ask" id="ask" autocomplete="off"><input id="askIn" maxlength="800" enterkeyhint="send"></form>
|
|
150
159
|
<div class="who">
|
|
151
160
|
<h1 id="name"></h1>
|
|
152
161
|
<div class="sub"><span id="role"></span><span class="mood" id="mood"></span></div>
|
|
@@ -162,6 +171,7 @@
|
|
|
162
171
|
|
|
163
172
|
<nav class="actions">
|
|
164
173
|
<button class="act primary" data-act="feed"><svg viewBox="0 0 24 24"><path d="M12 7c-1.5-2.5-6-2.5-7 1.5S7 20 9.5 20c1.2 0 1.6-.6 2.5-.6s1.3.6 2.5.6C17 20 20 12.5 19 8.5S13.5 4.5 12 7z"/><path d="M12 7c0-2 1-3.5 3-4"/></svg><span data-t="feed"></span></button>
|
|
174
|
+
<button class="act" id="talkBtn" type="button"><svg viewBox="0 0 24 24"><path d="M5 6h14v9H9l-4 3z"/></svg><span data-t="talk"></span></button>
|
|
165
175
|
<button class="act" data-act="play"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M5 8.5c3 1.5 11 1.5 14 0M5 15.5c3-1.5 11-1.5 14 0"/></svg><span data-t="play"></span></button>
|
|
166
176
|
<button class="act" data-act="sleep" id="sleepBtn"><svg viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg><span data-t="sleep"></span></button>
|
|
167
177
|
</nav>
|
|
@@ -177,17 +187,17 @@
|
|
|
177
187
|
<script type="module">
|
|
178
188
|
const BOOT = __BOOT__;
|
|
179
189
|
const T = {
|
|
180
|
-
en: { feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…',
|
|
190
|
+
en: { talk: 'Talk', askPh: 'Remind me to check email in 10 min…', feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…',
|
|
181
191
|
activity: 'Activity', empty: 'Nothing yet. Run a command or ask your coding agent something.', tally: '{ok} ok · {fail} failed · turns {turns}',
|
|
182
192
|
brain: 'Brain: {b}', age: 'Born {d}', lines: 'built-in phrases', now: 'now', m: '{n}m', h: '{n}h',
|
|
183
193
|
moods: { happy: 'Happy', neutral: 'Calm', sad: 'Sad', disgust: 'Bored', sleep: 'Asleep', thinking: 'Thinking' },
|
|
184
194
|
kind: { tool: 'Tool', shell: 'Shell', prompt: 'You asked', stop: 'Turn finished', notify: 'Needs you', start: 'Session started' } },
|
|
185
|
-
es: { feed: 'Comida', 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…',
|
|
195
|
+
es: { talk: 'Hablar', askPh: 'Recuérdame ver el correo en 10 min…', feed: 'Comida', 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…',
|
|
186
196
|
activity: 'Actividad', empty: 'Nada todavía. Ejecuta un comando o pregúntale algo a tu agente.', tally: '{ok} ok · {fail} fallos · turnos {turns}',
|
|
187
197
|
brain: 'Cerebro: {b}', age: 'Nació {d}', lines: 'frases propias', now: 'ahora', m: '{n} min', h: '{n} h',
|
|
188
198
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Aburrido', sleep: 'Dormido', thinking: 'Pensando' },
|
|
189
199
|
kind: { tool: 'Herramienta', shell: 'Terminal', prompt: 'Preguntaste', stop: 'Turno terminado', notify: 'Te necesita', start: 'Sesión iniciada' } },
|
|
190
|
-
pt: { feed: 'Comida', 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…',
|
|
200
|
+
pt: { talk: 'Falar', askPh: 'Me lembre de ver o e-mail em 10 min…', feed: 'Comida', 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…',
|
|
191
201
|
activity: 'Atividade', empty: 'Nada ainda. Rode um comando ou pergunte algo ao seu agente.', tally: '{ok} ok · {fail} falhas · turnos {turns}',
|
|
192
202
|
brain: 'Cérebro: {b}', age: 'Nasceu {d}', lines: 'frases próprias', now: 'agora', m: '{n} min', h: '{n} h',
|
|
193
203
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Entediado', sleep: 'Dormindo', thinking: 'Pensando' },
|
|
@@ -202,6 +212,7 @@
|
|
|
202
212
|
|
|
203
213
|
document.documentElement.lang = lang;
|
|
204
214
|
document.body.classList.toggle('desktop', desktop);
|
|
215
|
+
document.body.classList.toggle('opaque', desktop && new URLSearchParams(location.search).has('opaque'));
|
|
205
216
|
document.title = `${id.name} · 7ots`;
|
|
206
217
|
$('#name').textContent = id.name;
|
|
207
218
|
$('#role').textContent = id.role || '';
|
|
@@ -217,8 +228,22 @@
|
|
|
217
228
|
$('#brain').innerHTML = '';
|
|
218
229
|
$('#brain').append(t('brain', { b: '' }), Object.assign(document.createElement('b'), { textContent: BOOT.brain === 'lines' ? t('lines') : BOOT.brain }));
|
|
219
230
|
|
|
220
|
-
const { createCharacter } = await import('/dist/7ots.esm.js');
|
|
221
|
-
const
|
|
231
|
+
const { createCharacter, loadCharacter3D } = await import('/dist/7ots.esm.js');
|
|
232
|
+
const spec = id.look?.character || {};
|
|
233
|
+
let ch = null;
|
|
234
|
+
if (new URLSearchParams(location.search).has('3d')) {
|
|
235
|
+
// three comes from the CDN; without WebGL or network the 2D ot takes over
|
|
236
|
+
try {
|
|
237
|
+
const m = await loadCharacter3D();
|
|
238
|
+
ch = m.createCharacter3D($('#face'), spec, { material: 'plush', backdrop: !desktop, orbit: !desktop, stage: { key: 'pet', zIndex: 3, maxDpr: 2 } });
|
|
239
|
+
if (ch.fallback) console.warn('[7ots pet] no WebGL: 2D ot');
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn('[7ots pet] 3D ot unavailable, using 2D:', err?.message || err);
|
|
242
|
+
ch = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
ch ||= createCharacter($('#face'), spec, { locale: lang, title: id.name });
|
|
246
|
+
const is3d = !!ch.character;
|
|
222
247
|
addEventListener('pointermove', (e) => ch.lookAtPoint(e.clientX, e.clientY));
|
|
223
248
|
$('#face').addEventListener('click', () => ch.gesture('bounce'));
|
|
224
249
|
|
|
@@ -395,9 +420,33 @@
|
|
|
395
420
|
live.textContent = t('off');
|
|
396
421
|
connect();
|
|
397
422
|
|
|
423
|
+
// ── talk: reminders, notes, open a site (POST /ask) ──
|
|
424
|
+
const ask = $('#ask');
|
|
425
|
+
const askIn = $('#askIn');
|
|
426
|
+
askIn.placeholder = t('askPh');
|
|
427
|
+
const talking = (on) => {
|
|
428
|
+
document.body.classList.toggle('talking', on);
|
|
429
|
+
if (on) askIn.focus();
|
|
430
|
+
};
|
|
431
|
+
$('#talkBtn').addEventListener('click', () => talking(!document.body.classList.contains('talking') || !desktop));
|
|
432
|
+
$('#face').addEventListener('dblclick', () => talking(true));
|
|
433
|
+
askIn.addEventListener('keydown', (e) => e.key === 'Escape' && talking(false));
|
|
434
|
+
ask.addEventListener('submit', async (e) => {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
const text = askIn.value.trim();
|
|
437
|
+
if (!text) return;
|
|
438
|
+
askIn.value = '';
|
|
439
|
+
if (desktop) talking(false);
|
|
440
|
+
const r = await post('/ask', { text }).then((x) => x.json()).catch(() => null);
|
|
441
|
+
if (!r) say('…');
|
|
442
|
+
});
|
|
443
|
+
addEventListener('message', (e) => e.data?.talk && talking(true));
|
|
444
|
+
|
|
398
445
|
// The desktop shell tells us which way we walk.
|
|
399
446
|
addEventListener('message', (e) => {
|
|
400
|
-
if (e.data?.walk
|
|
447
|
+
if (e.data?.walk == null) return;
|
|
448
|
+
if (is3d) ch.character.yaw = e.data.walk ? -e.data.walk * 0.9 : 0; // turns toward where it walks
|
|
449
|
+
else $('#stage').classList.toggle('flip', e.data.walk < 0);
|
|
401
450
|
});
|
|
402
451
|
</script>
|
|
403
452
|
</body>
|