@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.
Files changed (61) hide show
  1. package/README.md +3 -3
  2. package/cli/7ots.mjs +13 -1
  3. package/cli/lib/account.mjs +152 -2
  4. package/cli/lib/assistant.mjs +107 -38
  5. package/cli/lib/brain.mjs +13 -2
  6. package/cli/lib/config.mjs +5 -1
  7. package/cli/lib/inferences.mjs +30 -6
  8. package/cli/lib/integrations.mjs +69 -5
  9. package/cli/lib/orquesta.mjs +44 -1
  10. package/cli/lib/pet-server.mjs +352 -11
  11. package/cli/lib/prompts.mjs +31 -17
  12. package/cli/lib/prowl.mjs +110 -0
  13. package/cli/pet/electron/director.cjs +37 -5
  14. package/cli/pet/electron/main.cjs +92 -10
  15. package/cli/pet/pet.html +149 -10
  16. package/cli/pet/settings.html +549 -43
  17. package/dist/7ots.esm.js +216 -137
  18. package/dist/7ots.esm.js.map +4 -4
  19. package/dist/7ots.iife.js +216 -137
  20. package/dist/7ots.iife.js.map +4 -4
  21. package/llms.txt +1 -1
  22. package/package.json +17 -14
  23. package/server/channels/agent.mjs +7 -1
  24. package/server/channels/index.mjs +3 -2
  25. package/server/platform/connect.mjs +4 -1
  26. package/server/platform/db.mjs +14 -1
  27. package/server/platform/device.mjs +101 -4
  28. package/server/platform/mods.mjs +222 -0
  29. package/server/platform/orquesta.mjs +78 -14
  30. package/server/platform/profile.mjs +161 -0
  31. package/server/platform/routes.mjs +39 -33
  32. package/server/platform/runtime.mjs +5 -3
  33. package/server/platform/store.mjs +1 -1
  34. package/server/platform/vm.mjs +688 -17
  35. package/server/server.mjs +12 -2
  36. package/server/tts.mjs +17 -4
  37. package/src/avatar/three.js +3 -2
  38. package/src/character/AvatarEditor.js +95 -7
  39. package/src/character/Character.js +116 -17
  40. package/src/character/TradingCard.js +226 -0
  41. package/src/character/mods/index.js +4 -0
  42. package/src/character/mods/orc.js +64 -0
  43. package/src/character/mods.js +251 -0
  44. package/src/character/more/acc-head.js +213 -0
  45. package/src/character/more/acc-rest.js +342 -0
  46. package/src/character/more/brows.js +39 -0
  47. package/src/character/more/cheeks.js +78 -0
  48. package/src/character/more/eyes.js +288 -0
  49. package/src/character/more/mouths.js +259 -0
  50. package/src/character/more/patterns.js +120 -0
  51. package/src/character/parts.js +100 -0
  52. package/src/character/skin.js +166 -1
  53. package/src/character/styles.js +1285 -9
  54. package/src/character3d/Character3D.js +253 -37
  55. package/src/character3d/index.js +7 -1
  56. package/src/character3d/model.js +367 -0
  57. package/src/i18n/messages/character.js +37 -22
  58. package/src/i18n/messages/platform.js +33 -0
  59. package/src/identity/random.js +17 -3
  60. package/src/identity/schema.js +10 -0
  61. package/src/index.js +1 -0
@@ -75,6 +75,11 @@
75
75
  .more { grid-column: 2; display: none; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: flex-end; }
76
76
  .more .chips { align-self: center; }
77
77
  .opt:has(input:checked) .more { display: flex; }
78
+ .vconf { flex-basis: 100%; }
79
+ .vconf > summary { cursor: pointer; color: var(--accent); font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
80
+ .vconf .rows { margin-top: 8px; display: grid; gap: 8px; }
81
+ .vconf .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
82
+ .vconf a.ext { color: var(--accent); font-size: 13px; display: inline-flex; align-items: center; gap: 4px; }
78
83
  .chips { display: flex; gap: 6px; flex-wrap: wrap; }
79
84
  .total { margin: 0 0 10px; color: var(--ink-2); font: 600 13px var(--mono); }
80
85
  .chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 99px; border: 1px solid var(--line); background: var(--panel); font-size: 13px; cursor: pointer; }
@@ -133,11 +138,25 @@
133
138
  .wait { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-2); }
134
139
  .spin { width: 16px; height: 16px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); animation: spin .8s linear infinite; }
135
140
  @keyframes spin { to { transform: rotate(1turn); } }
141
+ .ai-busy { display: flex; align-items: center; gap: 10px; margin: 10px 0; padding: 10px 14px; border-radius: 10px; background: color-mix(in srgb, var(--accent) 14%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); }
142
+ .ai-busy[hidden] { display: none; }
143
+ .ai-busy .spin { width: 20px; height: 20px; }
144
+ .ai-wait textarea, .ai-wait input[type=text] { background-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent) 22%, transparent), transparent); background-size: 200% 100%; animation: shimmer 1.4s linear infinite; opacity: .7; }
145
+ @keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
136
146
  .who { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 16px; background: var(--panel-2); }
137
147
  .who .av { width: 36px; height: 36px; border-radius: 50%; background: var(--accent); color: var(--accent-ink); display: grid; place-items: center; font: 750 15px/1 var(--display); flex: none; }
138
148
  .who div { flex: 1; min-width: 0; display: grid; line-height: 1.25; }
139
149
  .who small { color: var(--muted); font-size: 11.5px; }
140
150
  .who b { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
151
+ .com-meta { display: flex; gap: 8px; padding: 0 12px; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
152
+ .com-meta .rt { color: var(--warn); font-weight: 650; }
153
+ .stars { display: flex; gap: 2px; padding: 0 12px; margin-bottom: 6px; }
154
+ .ot .stars button { margin: 0; padding: 0 2px; border: 0; background: none; font-size: 18px; line-height: 1; color: var(--line); cursor: pointer; box-shadow: none; }
155
+ .ot .stars button.on { color: var(--warn); }
156
+ .ot .acts { display: flex; gap: 6px; margin: auto 12px 0; } .ot .acts button { margin: 0; flex: 1; }
157
+ .ot .acts button.icon { flex: 0 0 auto; padding: 6px 8px; }
158
+ .ot .rep { display: flex; gap: 6px; padding: 6px 12px 0; } .ot .rep input { flex: 1; min-width: 0; } .ot .rep button { margin: 0; }
159
+ .ot .st { position: absolute; top: 8px; right: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 650; background: var(--panel-2); border: 1px solid var(--line); }
141
160
  .otgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 12px; }
142
161
  .ot { position: relative; display: flex; flex-direction: column; gap: 2px; padding: 0 0 12px; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); overflow: hidden; transition: transform .15s, box-shadow .15s, border-color .15s; }
143
162
  .ot:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
@@ -201,8 +220,6 @@
201
220
  .sw .kinds { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; font-size: 12px; }
202
221
  .integ .vm b { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
203
222
  .fields { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; }
204
- .lang { padding: 0 14px; } .lang select { height: 30px; font-size: 12.5px; }
205
- @media (max-width: 760px) { .lang { padding: 0; } .lang select { width: auto; } }
206
223
  footer { color: var(--muted); font-size: 12px; text-align: center; }
207
224
  footer code { font: 11.5px var(--mono); }
208
225
  </style>
@@ -212,7 +229,6 @@
212
229
  <aside>
213
230
  <div class="me"><span class="av" id="av">7</span><b id="title"></b><small id="petLine"></small></div>
214
231
  <nav id="nav" aria-label="Settings"></nav>
215
- <label class="lang"><select id="langSel"></select></label>
216
232
  <a id="back" class="back"></a>
217
233
  </aside>
218
234
  <main>
@@ -222,6 +238,7 @@
222
238
  <h2 id="hAcc" data-t="accH"></h2>
223
239
  <p class="hint" data-t="accHint"></p>
224
240
  <div id="acc"></div>
241
+ <div class="fields" id="langs" style="margin-top:14px"></div>
225
242
  <div class="row"><span class="msg" id="accMsg" role="status"></span></div>
226
243
  </section>
227
244
 
@@ -270,11 +287,36 @@
270
287
  <div class="chips" id="view"></div>
271
288
  <span class="msg" id="petMsg" role="status"></span>
272
289
  </div>
290
+ <h3 class="sh" data-t="scH"></h3>
291
+ <p class="hint" data-t="scHint"></p>
292
+ <p class="hint" id="scVisitor" data-t="scVisitor" hidden></p>
293
+ <div class="row" id="scConf">
294
+ <label class="chip"><input type="checkbox" id="scOn"> <span data-t="scOn"></span></label>
295
+ <div class="chips" id="scEvery"></div>
296
+ </div>
297
+ <div class="chips" id="scList"></div>
298
+ <div class="row">
299
+ <button type="button" id="scPlay" data-t="scPlay"></button>
300
+ <span class="msg" id="scMsg" role="status"></span>
301
+ </div>
302
+ <h3 class="sh" data-t="modsH"></h3>
303
+ <p class="hint" data-t="modsHint"></p>
304
+ <div id="mods"></div>
273
305
  <h3 class="sh" data-t="charH"></h3>
274
306
  <p class="hint" data-t="charHint"></p>
275
307
  <div id="char"></div>
276
308
  </section>
277
309
 
310
+ <section aria-labelledby="hCom">
311
+ <h2 id="hCom" data-t="comH"></h2>
312
+ <p class="hint" data-t="comHint"></p>
313
+ <div class="row"><div class="chips" id="comSort"></div><input id="comQ" type="search" style="flex:1;min-width:140px"><span class="msg" id="comMsg" role="status"></span></div>
314
+ <p class="hint" id="comLogin" hidden data-t="comLogin"></p>
315
+ <div id="community"></div>
316
+ <h3 class="sh" data-t="comMine"></h3>
317
+ <div id="comMine"></div>
318
+ </section>
319
+
278
320
  <section aria-labelledby="hNotes">
279
321
  <h2 id="hNotes" data-t="notesH"></h2>
280
322
  <p class="hint" data-t="notesHint"></p>
@@ -293,7 +335,7 @@
293
335
  <section aria-labelledby="hInf">
294
336
  <h2 id="hInf" data-t="infH"></h2>
295
337
  <p class="hint" data-t="infHint"></p>
296
- <p class="total" id="infTotal"></p>
338
+ <div class="row"><select id="infOt" aria-label="ot" hidden></select><p class="total" id="infTotal" style="margin:0"></p></div>
297
339
  <div class="rows" id="infs"></div>
298
340
  <div class="row"><button id="clearInf" data-t="clearInf"></button><span class="msg" id="infMsg" role="status"></span></div>
299
341
  </section>
@@ -322,9 +364,9 @@
322
364
  vlocal: 'Local voice (Piper)', vlocalD: 'Runs on this computer, free and private. Picks Spanish or English per line.', vlocalNo: 'not installed (7ots voice install)',
323
365
  velevenlabs: 'ElevenLabs', velevenlabsD: 'Natural cloud voices, with your key.', vopenai: 'OpenAI', vopenaiD: 'OpenAI text-to-speech, with your key.',
324
366
  sttNow: 'Hears with:', sttNone: 'nothing (no speech-to-text)', sttLocal: 'Whisper on this computer',
325
- screenEvery: 'Look every', min: 'min', lookNow: 'Look now', looking: 'Looking…', lastSeen: 'Last:', noVision: 'This brain can’t see images: it will only read window titles.',
367
+ convL: 'Remembers the conversation up to', convOff: 'off', convTok: 'tokens', convUse: 'in use: {n}', convForget: 'Forget conversation', convGone: 'Conversation forgotten ✓', screenEvery: 'Look every', min: 'min', lookNow: 'Look now', looking: 'Looking…', lastSeen: 'Last:', noVision: 'This brain can’t see images: it will only read window titles.',
326
368
  memNotes: 'Notes', memSeen: 'What it saw', memSaid: 'What it said', noSeen: 'Nothing yet. Turn on “See your screen”.', noSaid: 'Nothing yet.', forgetSeen: 'Forget what it saw',
327
- infH: 'Inference history', infHint: 'Every time it asked its brain something: why, what it sent, and what came back. Stays on this computer.', noInf: 'No calls yet.', clearInf: 'Clear history',
369
+ infH: 'Inference history', infHint: 'Every time it asked its brain something: why, what it sent, and what came back. Stays on this computer.', noInf: 'No calls yet.', infAll: 'All ots', clearInf: 'Clear history',
328
370
  fSystem: 'System prompt', fInput: 'What it sent', fFiles: 'Files', fOutput: 'Answer', fError: 'Error',
329
371
  title: '{name} · settings', back: '← Back to the pet', save: 'Save', saved: 'Saved ✓', test: 'Try it', testing: 'Thinking…', now: 'Now:',
330
372
  brainH: 'Brain', brainHint: 'Who thinks when it talks to you. Your own AI CLI is free with your subscription and runs on this computer.',
@@ -341,6 +383,7 @@
341
383
  notify: 'Notifications', notifyD: 'A desktop notification when a reminder fires.',
342
384
  soon: 'coming soon', screen: 'See your screen', screenD: 'Look at what’s open to help with it.', procs: 'Processes and apps', procsD: 'See which programs run (name, CPU, memory) and wait for one to finish. Command arguments that look like keys are hidden.', inbox: 'Read mail and calendar', inboxD: 'Summaries and replies, not just opening the tab.',
343
385
  petH: 'How chatty', annoyHint: 'How often it comments on what you do.', a0: 'Quiet', a1: 'A little', a2: 'Normal', a3: 'Chatty',
386
+ scH: 'Scenes between ots', scHint: 'When two ots share the desktop they meet now and then and act out a famous fight.', scVisitor: 'How often and which ones is set in the main ot\'s settings.', scOn: 'On', scEvery: { rare: 'Rarely', normal: 'Normal', often: 'Often' }, scPlay: '▶ Play one now', scPlayed: 'Done — it starts as soon as two ots are free.', scNames: { rocky: 'Rocky', sf: 'Street Fighter', dbz: 'Dragon Ball', matrix: 'Matrix', duel: 'Western duel', sw: 'Star Wars', fight: 'Fight & hug', twist: 'Twist' },
344
387
  view: 'Look:', v2d: '2D', v3d: '3D plush', viewNext: 'Changed — the ot switches right away.',
345
388
  notesH: 'What it knows about you', notesHint: 'Its notes. Tell it “remember that…” to add one.', noNotes: 'Nothing yet.', forget: 'Forget', forgetAll: 'Forget everything', sure: 'Click again to forget everything',
346
389
  remH: 'Processes', remHint: 'What {name} is doing for you: reminders, alerts and the programs it’s waiting on. Ask it “remind me…”, “let me know if BTC reaches 85k” or “tell me when the build finishes”.', infTotal: 'Last {n} calls: {in} in → {out} out · {all} tokens', petRuns: 'What {name} is running', pcRuns: 'On this computer', refreshPs: 'Refresh', noRems: 'Nothing pending: no reminders or alerts.', psOff: '{name} can’t see your programs. Turn on “Processes and apps” to list them and let it wait for one to finish.', psOn: 'Turn on', screenEvery: 'Looks at your screen every {n} min', lastSeen: 'last: {what}', priceW: '{coin} {op} {value} {vs}', priceNow: 'now {p}', procW: '“{m}” {until}', untilExit: 'when it ends', untilStart: 'when it starts', running: 'running', notRunning: 'not running', remind: 'Reminder', cpu: 'CPU', mem: 'Mem', up: 'Up', prog: 'Program', cancel: 'Cancel',
@@ -355,9 +398,9 @@
355
398
  vlocal: 'Voz local (Piper)', vlocalD: 'Corre en este computador, gratis y privada. Elige español o inglés por frase.', vlocalNo: 'no instalada (7ots voice install)',
356
399
  velevenlabs: 'ElevenLabs', velevenlabsD: 'Voces naturales en la nube, con tu key.', vopenai: 'OpenAI', vopenaiD: 'Texto a voz de OpenAI, con tu key.',
357
400
  sttNow: 'Escucha con:', sttNone: 'nada (sin voz a texto)', sttLocal: 'Whisper en este computador',
358
- screenEvery: 'Mirar cada', min: 'min', lookNow: 'Mirar ahora', looking: 'Mirando…', lastSeen: 'Último:', noVision: 'Este cerebro no ve imágenes: solo leerá los títulos de ventana.',
401
+ convL: 'Recuerda la conversación hasta', convOff: 'no', convTok: 'tokens', convUse: 'en uso: {n}', convForget: 'Olvidar conversación', convGone: 'Conversación olvidada ✓', screenEvery: 'Mirar cada', min: 'min', lookNow: 'Mirar ahora', looking: 'Mirando…', lastSeen: 'Último:', noVision: 'Este cerebro no ve imágenes: solo leerá los títulos de ventana.',
359
402
  memNotes: 'Notas', memSeen: 'Lo que vio', memSaid: 'Lo que dijo', noSeen: 'Nada todavía. Activa “Ver tu pantalla”.', noSaid: 'Nada todavía.', forgetSeen: 'Olvidar lo que vio',
360
- infH: 'Historial de inferencias', infHint: 'Cada vez que le preguntó algo a su cerebro: para qué, qué mandó y qué volvió. Queda en este computador.', noInf: 'Sin llamadas todavía.', clearInf: 'Borrar historial',
403
+ infH: 'Historial de inferencias', infHint: 'Cada vez que le preguntó algo a su cerebro: para qué, qué mandó y qué volvió. Queda en este computador.', noInf: 'Sin llamadas todavía.', infAll: 'Todos los ots', clearInf: 'Borrar historial',
361
404
  fSystem: 'System prompt', fInput: 'Lo que mandó', fFiles: 'Archivos', fOutput: 'Respuesta', fError: 'Error',
362
405
  title: '{name} · ajustes', back: '← Volver a la mascota', save: 'Guardar', saved: 'Guardado ✓', test: 'Probar', testing: 'Pensando…', now: 'Ahora:',
363
406
  brainH: 'Cerebro', brainHint: 'Quién piensa cuando te habla. Tu propio CLI de IA es gratis con tu suscripción y corre en este computador.',
@@ -374,6 +417,7 @@
374
417
  notify: 'Notificaciones', notifyD: 'Una notificación de escritorio cuando suena un recordatorio.',
375
418
  soon: 'próximamente', screen: 'Ver tu pantalla', screenD: 'Mirar lo que tienes abierto para ayudarte.', procs: 'Procesos y apps', procsD: 'Ver qué programas corren (nombre, CPU, memoria) y esperar a que uno termine. Los argumentos que parecen claves se ocultan.', inbox: 'Leer correo y calendario', inboxD: 'Resúmenes y respuestas, no solo abrir la pestaña.',
376
419
  petH: 'Cuánto habla', annoyHint: 'Qué tan seguido comenta lo que haces.', a0: 'Callado', a1: 'Poco', a2: 'Normal', a3: 'Charlatán',
420
+ scH: 'Escenas entre ots', scHint: 'Cuando dos ots comparten el escritorio, cada tanto se juntan y actúan una pelea famosa.', scVisitor: 'Cada cuánto y cuáles se ajusta en los ajustes del ot principal.', scOn: 'Activadas', scEvery: { rare: 'Poco', normal: 'Normal', often: 'Seguido' }, scPlay: '▶ Jugar una ahora', scPlayed: 'Listo: empieza apenas haya dos ots libres.', scNames: { rocky: 'Rocky', sf: 'Street Fighter', dbz: 'Dragon Ball', matrix: 'Matrix', duel: 'Duelo', sw: 'Star Wars', fight: 'Pelea y abrazo', twist: 'Twist' },
377
421
  view: 'Aspecto:', v2d: '2D', v3d: 'Peluche 3D', viewNext: 'Listo: el ot cambia al instante.',
378
422
  notesH: 'Lo que sabe de ti', notesHint: 'Sus notas. Dile “anota que…” para agregar una.', noNotes: 'Nada todavía.', forget: 'Olvidar', forgetAll: 'Olvidar todo', sure: 'Haz clic otra vez para olvidar todo',
379
423
  remH: 'Procesos', remHint: 'Lo que {name} está haciendo por ti: recordatorios, alertas y los programas que está esperando. Pídele “recuérdame…”, “avísame si BTC llega a 85 mil” o “avísame cuando termine el build”.', infTotal: 'Últimas {n} llamadas: {in} entrada → {out} salida · {all} tokens', petRuns: 'Lo que {name} tiene corriendo', pcRuns: 'En este computador', refreshPs: 'Actualizar', noRems: 'Nada pendiente: sin recordatorios ni alertas.', psOff: '{name} no ve tus programas. Activa “Procesos y apps” para listarlos y que pueda esperar a que uno termine.', psOn: 'Activar', screenEvery: 'Mira tu pantalla cada {n} min', lastSeen: 'última: {what}', priceW: '{coin} {op} {value} {vs}', priceNow: 'ahora {p}', procW: '“{m}” {until}', untilExit: 'cuando termine', untilStart: 'cuando empiece', running: 'corriendo', notRunning: 'no está corriendo', remind: 'Recordatorio', cpu: 'CPU', mem: 'Mem', up: 'Hace', prog: 'Programa', cancel: 'Cancelar',
@@ -388,9 +432,9 @@
388
432
  vlocal: 'Voz local (Piper)', vlocalD: 'Roda neste computador, grátis e privada. Escolhe espanhol ou inglês por frase.', vlocalNo: 'não instalada (7ots voice install)',
389
433
  velevenlabs: 'ElevenLabs', velevenlabsD: 'Vozes naturais na nuvem, com sua key.', vopenai: 'OpenAI', vopenaiD: 'Texto para voz da OpenAI, com sua key.',
390
434
  sttNow: 'Ouve com:', sttNone: 'nada (sem voz para texto)', sttLocal: 'Whisper neste computador',
391
- screenEvery: 'Olhar a cada', min: 'min', lookNow: 'Olhar agora', looking: 'Olhando…', lastSeen: 'Último:', noVision: 'Este cérebro não vê imagens: só vai ler os títulos das janelas.',
435
+ convL: 'Lembra a conversa até', convOff: 'não', convTok: 'tokens', convUse: 'em uso: {n}', convForget: 'Esquecer conversa', convGone: 'Conversa esquecida ✓', screenEvery: 'Olhar a cada', min: 'min', lookNow: 'Olhar agora', looking: 'Olhando…', lastSeen: 'Último:', noVision: 'Este cérebro não vê imagens: só vai ler os títulos das janelas.',
392
436
  memNotes: 'Notas', memSeen: 'O que viu', memSaid: 'O que disse', noSeen: 'Nada ainda. Ative “Ver sua tela”.', noSaid: 'Nada ainda.', forgetSeen: 'Esquecer o que viu',
393
- infH: 'Histórico de inferências', infHint: 'Cada vez que perguntou algo ao cérebro: para quê, o que mandou e o que voltou. Fica neste computador.', noInf: 'Nenhuma chamada ainda.', clearInf: 'Apagar histórico',
437
+ infH: 'Histórico de inferências', infHint: 'Cada vez que perguntou algo ao cérebro: para quê, o que mandou e o que voltou. Fica neste computador.', noInf: 'Nenhuma chamada ainda.', infAll: 'Todos os ots', clearInf: 'Apagar histórico',
394
438
  fSystem: 'System prompt', fInput: 'O que mandou', fFiles: 'Arquivos', fOutput: 'Resposta', fError: 'Erro',
395
439
  title: '{name} · ajustes', back: '← Voltar ao bichinho', save: 'Salvar', saved: 'Salvo ✓', test: 'Testar', testing: 'Pensando…', now: 'Agora:',
396
440
  brainH: 'Cérebro', brainHint: 'Quem pensa quando ele fala com você. Seu próprio CLI de IA é grátis com sua assinatura e roda neste computador.',
@@ -407,6 +451,7 @@
407
451
  notify: 'Notificações', notifyD: 'Uma notificação na área de trabalho quando um lembrete toca.',
408
452
  soon: 'em breve', screen: 'Ver sua tela', screenD: 'Olhar o que está aberto para ajudar.', procs: 'Processos e apps', procsD: 'Ver quais programas rodam (nome, CPU, memória) e esperar um terminar. Argumentos que parecem chaves ficam ocultos.', inbox: 'Ler e-mail e agenda', inboxD: 'Resumos e respostas, não só abrir a aba.',
409
453
  petH: 'Quanto fala', annoyHint: 'Com que frequência comenta o que você faz.', a0: 'Calado', a1: 'Pouco', a2: 'Normal', a3: 'Tagarela',
454
+ scH: 'Cenas entre ots', scHint: 'Quando dois ots dividem a área de trabalho, de vez em quando se encontram e encenam uma luta famosa.', scVisitor: 'Frequência e quais se ajustam nas configurações do ot principal.', scOn: 'Ativadas', scEvery: { rare: 'Pouco', normal: 'Normal', often: 'Frequente' }, scPlay: '▶ Jogar uma agora', scPlayed: 'Pronto: começa assim que houver dois ots livres.', scNames: { rocky: 'Rocky', sf: 'Street Fighter', dbz: 'Dragon Ball', matrix: 'Matrix', duel: 'Duelo', sw: 'Star Wars', fight: 'Briga e abraço', twist: 'Twist' },
410
455
  view: 'Visual:', v2d: '2D', v3d: 'Pelúcia 3D', viewNext: 'Pronto: o ot muda na hora.',
411
456
  notesH: 'O que ele sabe de você', notesHint: 'As notas dele. Diga “anote que…” para adicionar.', noNotes: 'Nada ainda.', forget: 'Esquecer', forgetAll: 'Esquecer tudo', sure: 'Clique de novo para esquecer tudo',
412
457
  remH: 'Processos', remHint: 'O que {name} está fazendo por você: lembretes, alertas e os programas que está esperando. Peça “me lembre…”, “me avise se o BTC chegar a 85 mil” ou “me avise quando o build terminar”.', infTotal: 'Últimas {n} chamadas: {in} entrada → {out} saída · {all} tokens', petRuns: 'O que {name} está rodando', pcRuns: 'Neste computador', refreshPs: 'Atualizar', noRems: 'Nada pendente: sem lembretes nem alertas.', psOff: '{name} não vê seus programas. Ative “Processos e apps” para listá-los e esperar um terminar.', psOn: 'Ativar', screenEvery: 'Olha sua tela a cada {n} min', lastSeen: 'última: {what}', priceW: '{coin} {op} {value} {vs}', priceNow: 'agora {p}', procW: '“{m}” {until}', untilExit: 'quando terminar', untilStart: 'quando começar', running: 'rodando', notRunning: 'não está rodando', remind: 'Lembrete', cpu: 'CPU', mem: 'Mem', up: 'Há', prog: 'Programa', cancel: 'Cancelar',
@@ -416,59 +461,65 @@
416
461
  // Strings for integrations, the character sheet and the newer providers (merged into T above).
417
462
  const T2 = {
418
463
  en: {
419
- lv: 'Lv', oaiCompat: 'OpenAI-compatible', langLabel: 'Language', langAuto: 'Automatic (system)',
464
+ lv: 'Lv', oaiCompat: 'OpenAI-compatible', comH: 'Community', comHint: 'Mods people shared on 7ots.com. Install one to wear it, then rate it. Share yours from Personality → Mods.', comTop: 'Top rated', comNew: 'New', comPop: 'Popular', comSearch: 'Search mods…', comInstall: 'Install', comWear: 'Wear again', comEmpty: 'Nothing here yet.', comLogin: 'Sign in to 7ots.com (Account) to share, rate and report mods.', comMine: 'Shared by you', comMineEmpty: 'You have not shared any mod yet: use Share on one of yours in Personality → Mods.', comShare: 'Share', comShared: 'Sent: it shows in the gallery once approved.', comReport: 'Report', comReportPh: 'What is wrong with it?', comReportSend: 'Send', comReported: 'Reported, thanks.', comRated: 'Thanks for rating.', comDel: 'Delete', comInstalls: '{n} installs', comNoRating: 'no ratings', comRateHint: 'Install it to rate it', st_pending: 'In review', st_approved: 'Published', st_rejected: 'Rejected', st_hidden: 'Hidden', e_login: 'Sign in to 7ots.com first (Account).', modsH: 'Mods', modsHint: 'Packs that change its look (an orc, a robot…) in 2D and 3D. Take one off and it goes back to how it was.', modPut: 'Put it on', modOff: 'Take off', modOn: 'On', modImport: 'Import mod (.json)', modModel: 'New 3D body (.vrm / .glb)', modModelHint: 'Uploading…', modModelBy: '3D body', e_model: 'That file is not a .vrm or .glb model.', modBad: 'That file is not a valid mod.', e_mod: 'That file is not a valid mod.', e_maxMods: 'At most 4 mods at once.',
465
+ langLabel: 'Interface language', langAuto: 'Automatic (system)', otLang: 'Language {name} speaks',
420
466
  apiD: 'Anthropic, any OpenAI-compatible endpoint, or Batuta (Orquesta’s model router), with your key.',
421
467
  vapuchat: 'apuchat audio', vapuchatD: 'apuchat’s voices, with your apuchat voice token.', vgrok: 'Grok AI', vgrokD: 'xAI’s text-to-speech, with your xAI key.', vfish: 'Fish Audio', vfishD: 'fish.audio voices (also cloned ones), with your key.',
422
468
  logs: 'Process logs', logsD: 'Read the last lines of a service’s logs (journal, pm2, docker, a .log file or a running program) when you ask. Keys and tokens are hidden.',
469
+ sites: 'Site guides', sitesD: 'When you mention a site, read its usage guide on Prowl.world (only the domain is sent).',
423
470
  inbox: 'Read your mail', inboxD: 'Read and summarize the inbox connected below (apumail) when you ask.',
424
471
  integH: 'Integrations', integHint: 'Other places it can reach. What you turn on here is listed to its brain as a tool.',
425
472
  cloudH: '7ots cloud', cloudD: 'Your account on 7ots.com and the ot twinned with this one.', cloudOff: 'Not connected.', cloudAs: 'Connected as {who}', cloudTwin: 'twin: {name}', cloudNoTwin: 'no ot twinned yet', goAcc: 'Connect in Account',
426
473
  offload: 'Hand work to your server', offloadD: 'Send long tasks (research, reading pages…) to your ot’s cloud worker. It only gets the request and, with Memory on, your notes.', kindsL: 'Kinds', k_research: 'research', k_browse: 'read pages', k_long: 'long tasks', k_code: 'code', k_shell: 'shell commands', vmNone: 'no server yet: add yours at 7ots.com → your ot → Assistant', vmPending: 'waiting for the server to enroll', vmOnline: 'online', vmOffline: 'offline', vmTerminated: 'revoked',
427
474
  vmH: 'Cloud assistant', vmD: 'Your ot running 24/7 in the cloud, even with this computer off.', vmDenied: 'no access', vmError: 'couldn’t check', vmSince: 'since {d}',
428
- orqH: 'Orquesta', orqD: 'See your Orquesta agents and send them tasks; it tells you when they finish.', orqHint: 'Or run 7ots login orquesta in a terminal.', orqNoToken: 'Add your Orquesta token to list your agents.', orqProject: 'Agent (default project)', orqOnline: 'online', orqOffline: 'offline', orqNone: 'No projects.', orqSend: 'Send', orqTask: 'Task for the agent', orqSent: 'Sent · {status}', orqRefresh: 'Reload',
475
+ orqH: 'Orquesta', orqD: 'See your Orquesta agents and send them tasks; it tells you when they finish.', orqHint: 'Or run 7ots login orquesta in a terminal.', orqNoToken: 'Not connected: it can’t see your agents yet.', orqConnect: 'Connect Orquesta', orqWaiting: 'Finish signing in in your browser…', orqProject: 'Agent (default project)', orqOnline: 'online', orqOffline: 'offline', orqNone: 'No projects.', orqSend: 'Send', orqTask: 'Task for the agent', orqSent: 'Sent · {status}', orqRefresh: 'Reload',
429
476
  mailH: 'Email (apumail)', mailD: 'An apumail inbox it can read when you ask (turn on “Read your mail” above).', mailInbox: 'Inbox address', mailToken: 'apumail token', gmailD: 'Gmail and Outlook through their own login.',
430
477
  customH: 'Custom integrations', customD: 'Anything else: tell it what it is and how to use it. Optional: a URL it can GET or a command it can run (with a token from an environment variable).', customAdd: 'Add integration', customName: 'Name', customInstr: 'Instructions for the brain', customUrl: 'URL (optional)', customCmd: 'Command (optional)', customEnv: 'Token env var (optional)', customEnvHint: 'Read from this computer’s environment or ~/.7ots/keys.json, never shown.', edit: 'Edit', remove: 'Remove', untitled: 'New integration',
431
478
  charH: 'Character sheet', charHint: 'Who it is. This weighs more than the traits from 7ots.com: backstory, how it talks, its quirks and example dialogues.',
432
479
  chBackstory: 'Backstory', chStyle: 'How it talks', chQuirks: 'Quirks', chScenario: 'Scenario (optional)', chGreeting: 'Greeting (optional)', chExamples: 'Example dialogues',
433
- chExamplesPh: 'Human: how are you?\n{name}: …', importCard: 'Import card', exportCard: 'Export card', imported: 'Imported ✓', badCard: 'That file has no character card (chara_card_v2 JSON or PNG).',
480
+ vcConf: 'Configure voice', vcNow: 'Voice: {v} · change', vcId: 'Voice id', vcIdPh: 'Empty = provider default', vcPick: 'Pick one of your {n} voices…', vcNoList: 'No voices in your account yet', vcModel: 'Model', vcDefault: 'Default', vcStyle: 'Style (how it speaks)', vcStylePh: 'E.g.: warm, a bit sarcastic, fast', vcTest: 'Try', vcTesting: 'Generating…', vcSave: 'Use this voice', vcPage: 'Browse voices on the site', vcKeyFirst: 'Save the key first to list your voices.', chExamplesPh: 'Human: how are you?\n{name}: …', chIdeaPh: 'Idea (optional): a grumpy retired wizard who loves trains…', chAi: 'Fill with AI', chAiWait: 'The AI is writing the sheet in {lang}… (up to a minute)', chAiLang: 'Language of the sheet', chAiPartial: 'Draft ready, but these came back empty: {f}. Try again.', chAiDone: 'Draft ready: review it and Save', chAiBrain: 'Pick an AI brain first (Brain section)', importCard: 'Import card', exportCard: 'Export card', imported: 'Imported ✓', badCard: 'That file has no character card (chara_card_v2 JSON or PNG).',
434
481
  lettaH: 'Letta memory (optional)', lettaD: 'Keeps the persona in a Letta agent’s core memory, and reads it back into its prompt.', lettaAgent: 'Letta agent ID (empty: create one)', lettaKey: 'Letta API key', lettaSync: 'Sync with Letta', lettaOk: 'Synced ✓', lettaNew: 'Agent created ✓',
435
482
  e_customName: 'every integration needs a name', e_customUrl: 'the URL must start with http:// or https://', e_customEnv: 'the env var must look like MY_TOKEN', e_inbox: 'that inbox address isn’t valid', e_lettaAgent: 'invalid Letta agent ID', e_custom: 'up to 20 integrations', e_project: 'invalid project',
436
483
  },
437
484
  es: {
438
- lv: 'Nv', oaiCompat: 'Compatible con OpenAI', langLabel: 'Idioma', langAuto: 'Automático (sistema)',
485
+ lv: 'Nv', oaiCompat: 'Compatible con OpenAI', comH: 'Comunidad', comHint: 'Mods que la gente compartió en 7ots.com. Instala uno para ponértelo y después califícalo. Comparte los tuyos desde Personalidad → Mods.', comTop: 'Mejor calificados', comNew: 'Nuevos', comPop: 'Populares', comSearch: 'Buscar mods…', comInstall: 'Instalar', comWear: 'Volver a ponerlo', comEmpty: 'Todavía no hay nada.', comLogin: 'Inicia sesión en 7ots.com (Cuenta) para compartir, calificar y reportar mods.', comMine: 'Compartidos por ti', comMineEmpty: 'Aún no compartes ningún mod: usa Compartir en uno tuyo en Personalidad → Mods.', comShare: 'Compartir', comShared: 'Enviado: aparece en la galería cuando lo aprueben.', comReport: 'Reportar', comReportPh: '¿Qué tiene de malo?', comReportSend: 'Enviar', comReported: 'Reportado, gracias.', comRated: 'Gracias por calificar.', comDel: 'Borrar', comInstalls: '{n} instalaciones', comNoRating: 'sin calificaciones', comRateHint: 'Instálalo para calificarlo', st_pending: 'En revisión', st_approved: 'Publicado', st_rejected: 'Rechazado', st_hidden: 'Oculto', e_login: 'Primero inicia sesión en 7ots.com (Cuenta).', modsH: 'Mods', modsHint: 'Paquetes que le cambian el aspecto (un orco, un robot…) en 2D y 3D. Si se lo quitas, vuelve a como era.', modPut: 'Ponérselo', modOff: 'Quitar', modOn: 'Puesto', modImport: 'Importar mod (.json)', modModel: 'Muñeco 3D nuevo (.vrm / .glb)', modModelHint: 'Subiendo…', modModelBy: 'cuerpo 3D', e_model: 'Ese archivo no es un modelo .vrm ni .glb.', modBad: 'Ese archivo no es un mod válido.', e_mod: 'Ese archivo no es un mod válido.', e_maxMods: 'Como mucho 4 mods a la vez.',
486
+ langLabel: 'Idioma de la interfaz', langAuto: 'Automático (sistema)', otLang: 'Idioma en que habla {name}',
439
487
  apiD: 'Anthropic, cualquier endpoint compatible con OpenAI, o Batuta (el router de modelos de Orquesta), con tu key.',
440
488
  vapuchat: 'apuchat audio', vapuchatD: 'Las voces de apuchat, con tu token de voz de apuchat.', vgrok: 'Grok AI', vgrokD: 'Texto a voz de xAI, con tu key de xAI.', vfish: 'Fish Audio', vfishD: 'Voces de fish.audio (también clonadas), con tu key.',
441
489
  logs: 'Logs de procesos', logsD: 'Leer las últimas líneas de los logs de un servicio (journal, pm2, docker, un archivo .log o un programa corriendo) cuando se lo pides. Claves y tokens se ocultan.',
490
+ sites: 'Guías de sitios', sitesD: 'Cuando nombras un sitio, leer su guía de uso en Prowl.world (solo se envía el dominio).',
442
491
  inbox: 'Leer tu correo', inboxD: 'Leer y resumir la bandeja conectada abajo (apumail) cuando se lo pides.',
443
492
  integH: 'Integraciones', integHint: 'Otros lugares a los que puede llegar. Lo que actives acá se le presenta a su cerebro como herramienta.',
444
493
  cloudH: '7ots nube', cloudD: 'Tu cuenta en 7ots.com y el ot gemelo de este.', cloudOff: 'Sin conectar.', cloudAs: 'Conectado como {who}', cloudTwin: 'gemelo: {name}', cloudNoTwin: 'todavía sin ot gemelo', goAcc: 'Conectar en Cuenta',
445
494
  offload: 'Mandar trabajo a tu servidor', offloadD: 'Envía tareas largas (investigar, leer páginas…) al worker en la nube de tu ot. Solo recibe el pedido y, con Memoria activa, tus notas.', kindsL: 'Tipos', k_research: 'investigar', k_browse: 'leer páginas', k_long: 'tareas largas', k_code: 'código', k_shell: 'comandos shell', vmNone: 'aún sin servidor: agrega el tuyo en 7ots.com → tu ot → Asistente', vmPending: 'esperando que el servidor se enrole', vmOnline: 'en línea', vmOffline: 'desconectado', vmTerminated: 'revocado',
446
495
  vmH: 'Asistente en la nube', vmD: 'Tu ot funcionando 24/7 en la nube, aunque este computador esté apagado.', vmDenied: 'sin acceso', vmError: 'no se pudo revisar', vmSince: 'desde {d}',
447
- orqH: 'Orquesta', orqD: 'Ver tus agentes de Orquesta y mandarles tareas; te avisa cuando terminan.', orqHint: 'O ejecuta 7ots login orquesta en una terminal.', orqNoToken: 'Agrega tu token de Orquesta para listar tus agentes.', orqProject: 'Agente (proyecto por defecto)', orqOnline: 'en línea', orqOffline: 'desconectado', orqNone: 'Sin proyectos.', orqSend: 'Enviar', orqTask: 'Tarea para el agente', orqSent: 'Enviado · {status}', orqRefresh: 'Recargar',
496
+ orqH: 'Orquesta', orqD: 'Ver tus agentes de Orquesta y mandarles tareas; te avisa cuando terminan.', orqHint: 'O ejecuta 7ots login orquesta en una terminal.', orqNoToken: 'Sin conectar: todavía no ve tus agentes.', orqConnect: 'Conectar Orquesta', orqWaiting: 'Termina de iniciar sesión en tu navegador…', orqProject: 'Agente (proyecto por defecto)', orqOnline: 'en línea', orqOffline: 'desconectado', orqNone: 'Sin proyectos.', orqSend: 'Enviar', orqTask: 'Tarea para el agente', orqSent: 'Enviado · {status}', orqRefresh: 'Recargar',
448
497
  mailH: 'Correo (apumail)', mailD: 'Una bandeja de apumail que puede leer cuando se lo pides (activa “Leer tu correo” arriba).', mailInbox: 'Dirección de la bandeja', mailToken: 'Token de apumail', gmailD: 'Gmail y Outlook con su propio inicio de sesión.',
449
498
  customH: 'Integraciones propias', customD: 'Cualquier otra cosa: dile qué es y cómo usarla. Opcional: una URL a la que puede hacer GET o un comando que puede ejecutar (con un token de una variable de entorno).', customAdd: 'Agregar integración', customName: 'Nombre', customInstr: 'Instrucciones para el cerebro', customUrl: 'URL (opcional)', customCmd: 'Comando (opcional)', customEnv: 'Variable con el token (opcional)', customEnvHint: 'Se lee del entorno de este computador o de ~/.7ots/keys.json, nunca se muestra.', edit: 'Editar', remove: 'Quitar', untitled: 'Nueva integración',
450
499
  charH: 'Ficha del personaje', charHint: 'Quién es. Pesa más que los rasgos de 7ots.com: historia, cómo habla, sus manías y diálogos de ejemplo.',
451
500
  chBackstory: 'Historia', chStyle: 'Cómo habla', chQuirks: 'Manías', chScenario: 'Escenario (opcional)', chGreeting: 'Saludo (opcional)', chExamples: 'Diálogos de ejemplo',
452
- chExamplesPh: 'Human: ¿cómo estás?\n{name}: …', importCard: 'Importar ficha', exportCard: 'Exportar ficha', imported: 'Importada ✓', badCard: 'Ese archivo no trae una ficha de personaje (JSON o PNG chara_card_v2).',
501
+ vcConf: 'Configurar voz', vcNow: 'Voz: {v} · cambiar', vcId: 'Id de la voz', vcIdPh: 'Vacío = la del proveedor', vcPick: 'Elige una de tus {n} voces…', vcNoList: 'Aún no hay voces en tu cuenta', vcModel: 'Modelo', vcDefault: 'Por defecto', vcStyle: 'Estilo (cómo habla)', vcStylePh: 'Ej.: cálido, algo sarcástico, rápido', vcTest: 'Probar', vcTesting: 'Generando…', vcSave: 'Usar esta voz', vcPage: 'Ver voces en su sitio', vcKeyFirst: 'Guarda primero la key para listar tus voces.', chExamplesPh: 'Human: ¿cómo estás?\n{name}: …', chIdeaPh: 'Idea (opcional): un mago jubilado gruñón que ama los trenes…', chAi: 'Rellenar con IA', chAiWait: 'La IA está escribiendo la ficha en {lang}… (hasta un minuto)', chAiLang: 'Idioma de la ficha', chAiPartial: 'Borrador listo, pero estos vinieron vacíos: {f}. Prueba de nuevo.', chAiDone: 'Borrador listo: revísalo y Guarda', chAiBrain: 'Elige primero un cerebro con IA (sección Cerebro)', importCard: 'Importar ficha', exportCard: 'Exportar ficha', imported: 'Importada ✓', badCard: 'Ese archivo no trae una ficha de personaje (JSON o PNG chara_card_v2).',
453
502
  lettaH: 'Memoria Letta (opcional)', lettaD: 'Guarda la persona en la memoria central de un agente Letta y la vuelve a leer en su prompt.', lettaAgent: 'ID del agente Letta (vacío: crear uno)', lettaKey: 'API key de Letta', lettaSync: 'Sincronizar con Letta', lettaOk: 'Sincronizado ✓', lettaNew: 'Agente creado ✓',
454
503
  e_customName: 'cada integración necesita un nombre', e_customUrl: 'la URL debe empezar con http:// o https://', e_customEnv: 'la variable debe verse como MI_TOKEN', e_inbox: 'esa dirección de bandeja no es válida', e_lettaAgent: 'ID de agente Letta inválido', e_custom: 'máximo 20 integraciones', e_project: 'proyecto inválido',
455
504
  },
456
505
  pt: {
457
- lv: 'Nv', oaiCompat: 'Compatível com OpenAI', langLabel: 'Idioma', langAuto: 'Automático (sistema)',
506
+ lv: 'Nv', oaiCompat: 'Compatível com OpenAI', comH: 'Comunidade', comHint: 'Mods que as pessoas compartilharam no 7ots.com. Instale um para usar e depois avalie. Compartilhe os seus em Personalidade → Mods.', comTop: 'Mais bem avaliados', comNew: 'Novos', comPop: 'Populares', comSearch: 'Buscar mods…', comInstall: 'Instalar', comWear: 'Usar de novo', comEmpty: 'Ainda não há nada.', comLogin: 'Entre no 7ots.com (Conta) para compartilhar, avaliar e denunciar mods.', comMine: 'Compartilhados por você', comMineEmpty: 'Você ainda não compartilhou nenhum mod: use Compartilhar em um seu em Personalidade → Mods.', comShare: 'Compartilhar', comShared: 'Enviado: aparece na galeria quando for aprovado.', comReport: 'Denunciar', comReportPh: 'O que há de errado?', comReportSend: 'Enviar', comReported: 'Denunciado, obrigado.', comRated: 'Obrigado por avaliar.', comDel: 'Apagar', comInstalls: '{n} instalações', comNoRating: 'sem avaliações', comRateHint: 'Instale para avaliar', st_pending: 'Em revisão', st_approved: 'Publicado', st_rejected: 'Rejeitado', st_hidden: 'Oculto', e_login: 'Primeiro entre no 7ots.com (Conta).', modsH: 'Mods', modsHint: 'Pacotes que mudam o visual (um orc, um robô…) em 2D e 3D. Se tirar, volta a ser como era.', modPut: 'Colocar', modOff: 'Tirar', modOn: 'Colocado', modImport: 'Importar mod (.json)', modModel: 'Boneco 3D novo (.vrm / .glb)', modModelHint: 'Enviando…', modModelBy: 'corpo 3D', e_model: 'Esse arquivo não é um modelo .vrm nem .glb.', modBad: 'Esse arquivo não é um mod válido.', e_mod: 'Esse arquivo não é um mod válido.', e_maxMods: 'No máximo 4 mods de uma vez.',
507
+ langLabel: 'Idioma da interface', langAuto: 'Automático (sistema)', otLang: 'Idioma que {name} fala',
458
508
  apiD: 'Anthropic, qualquer endpoint compatível com OpenAI, ou Batuta (o roteador de modelos da Orquesta), com sua key.',
459
509
  vapuchat: 'apuchat audio', vapuchatD: 'As vozes do apuchat, com seu token de voz do apuchat.', vgrok: 'Grok AI', vgrokD: 'Texto para voz da xAI, com sua key da xAI.', vfish: 'Fish Audio', vfishD: 'Vozes do fish.audio (também clonadas), com sua key.',
460
510
  logs: 'Logs de processos', logsD: 'Ler as últimas linhas dos logs de um serviço (journal, pm2, docker, um arquivo .log ou um programa rodando) quando você pede. Chaves e tokens ficam ocultos.',
511
+ sites: 'Guias de sites', sitesD: 'Quando você cita um site, ler o guia de uso dele no Prowl.world (só o domínio é enviado).',
461
512
  inbox: 'Ler seu e-mail', inboxD: 'Ler e resumir a caixa conectada abaixo (apumail) quando você pede.',
462
513
  integH: 'Integrações', integHint: 'Outros lugares que ele pode alcançar. O que você ativar aqui aparece para o cérebro como ferramenta.',
463
514
  cloudH: '7ots nuvem', cloudD: 'Sua conta no 7ots.com e o ot gêmeo deste.', cloudOff: 'Não conectado.', cloudAs: 'Conectado como {who}', cloudTwin: 'gêmeo: {name}', cloudNoTwin: 'ainda sem ot gêmeo', goAcc: 'Conectar em Conta',
464
515
  offload: 'Mandar trabalho ao seu servidor', offloadD: 'Envia tarefas longas (pesquisar, ler páginas…) ao worker na nuvem do seu ot. Ele só recebe o pedido e, com Memória ativa, suas notas.', kindsL: 'Tipos', k_research: 'pesquisar', k_browse: 'ler páginas', k_long: 'tarefas longas', k_code: 'código', k_shell: 'comandos shell', vmNone: 'ainda sem servidor: adicione o seu em 7ots.com → seu ot → Assistente', vmPending: 'esperando o servidor se registrar', vmOnline: 'online', vmOffline: 'offline', vmTerminated: 'revogado',
465
516
  vmH: 'Assistente na nuvem', vmD: 'Seu ot rodando 24/7 na nuvem, mesmo com este computador desligado.', vmDenied: 'sem acesso', vmError: 'não deu para verificar', vmSince: 'desde {d}',
466
- orqH: 'Orquesta', orqD: 'Ver seus agentes da Orquesta e mandar tarefas; ele avisa quando terminam.', orqHint: 'Ou rode 7ots login orquesta num terminal.', orqNoToken: 'Adicione seu token da Orquesta para listar seus agentes.', orqProject: 'Agente (projeto padrão)', orqOnline: 'online', orqOffline: 'offline', orqNone: 'Sem projetos.', orqSend: 'Enviar', orqTask: 'Tarefa para o agente', orqSent: 'Enviado · {status}', orqRefresh: 'Recarregar',
517
+ orqH: 'Orquesta', orqD: 'Ver seus agentes da Orquesta e mandar tarefas; ele avisa quando terminam.', orqHint: 'Ou rode 7ots login orquesta num terminal.', orqNoToken: 'Sem conexão: ainda não vê seus agentes.', orqConnect: 'Conectar Orquesta', orqWaiting: 'Termine o login no seu navegador…', orqProject: 'Agente (projeto padrão)', orqOnline: 'online', orqOffline: 'offline', orqNone: 'Sem projetos.', orqSend: 'Enviar', orqTask: 'Tarefa para o agente', orqSent: 'Enviado · {status}', orqRefresh: 'Recarregar',
467
518
  mailH: 'E-mail (apumail)', mailD: 'Uma caixa do apumail que ele pode ler quando você pede (ative “Ler seu e-mail” acima).', mailInbox: 'Endereço da caixa', mailToken: 'Token do apumail', gmailD: 'Gmail e Outlook com o próprio login.',
468
519
  customH: 'Integrações próprias', customD: 'Qualquer outra coisa: diga o que é e como usar. Opcional: uma URL para fazer GET ou um comando para rodar (com um token de uma variável de ambiente).', customAdd: 'Adicionar integração', customName: 'Nome', customInstr: 'Instruções para o cérebro', customUrl: 'URL (opcional)', customCmd: 'Comando (opcional)', customEnv: 'Variável com o token (opcional)', customEnvHint: 'Lida do ambiente deste computador ou de ~/.7ots/keys.json, nunca mostrada.', edit: 'Editar', remove: 'Remover', untitled: 'Nova integração',
469
520
  charH: 'Ficha do personagem', charHint: 'Quem ele é. Pesa mais que os traços do 7ots.com: história, jeito de falar, manias e diálogos de exemplo.',
470
521
  chBackstory: 'História', chStyle: 'Jeito de falar', chQuirks: 'Manias', chScenario: 'Cenário (opcional)', chGreeting: 'Saudação (opcional)', chExamples: 'Diálogos de exemplo',
471
- chExamplesPh: 'Human: tudo bem?\n{name}: …', importCard: 'Importar ficha', exportCard: 'Exportar ficha', imported: 'Importada ✓', badCard: 'Esse arquivo não tem uma ficha de personagem (JSON ou PNG chara_card_v2).',
522
+ vcConf: 'Configurar voz', vcNow: 'Voz: {v} · trocar', vcId: 'Id da voz', vcIdPh: 'Vazio = a do provedor', vcPick: 'Escolha uma das suas {n} vozes…', vcNoList: 'Ainda não há vozes na sua conta', vcModel: 'Modelo', vcDefault: 'Padrão', vcStyle: 'Estilo (como fala)', vcStylePh: 'Ex.: caloroso, meio sarcástico, rápido', vcTest: 'Testar', vcTesting: 'Gerando…', vcSave: 'Usar esta voz', vcPage: 'Ver vozes no site', vcKeyFirst: 'Salve a key primeiro para listar suas vozes.', chExamplesPh: 'Human: tudo bem?\n{name}: …', chIdeaPh: 'Ideia (opcional): um mago aposentado ranzinza que ama trens…', chAi: 'Preencher com IA', chAiWait: 'A IA está escrevendo a ficha em {lang}… (até um minuto)', chAiLang: 'Idioma da ficha', chAiPartial: 'Rascunho pronto, mas estes vieram vazios: {f}. Tente de novo.', chAiDone: 'Rascunho pronto: revise e Salve', chAiBrain: 'Escolha antes um cérebro com IA (seção Cérebro)', importCard: 'Importar ficha', exportCard: 'Exportar ficha', imported: 'Importada ✓', badCard: 'Esse arquivo não tem uma ficha de personagem (JSON ou PNG chara_card_v2).',
472
523
  lettaH: 'Memória Letta (opcional)', lettaD: 'Guarda a persona na memória central de um agente Letta e a lê de volta no prompt.', lettaAgent: 'ID do agente Letta (vazio: criar um)', lettaKey: 'API key da Letta', lettaSync: 'Sincronizar com Letta', lettaOk: 'Sincronizado ✓', lettaNew: 'Agente criado ✓',
473
524
  e_customName: 'cada integração precisa de um nome', e_customUrl: 'a URL deve começar com http:// ou https://', e_customEnv: 'a variável deve parecer MEU_TOKEN', e_inbox: 'esse endereço de caixa não é válido', e_lettaAgent: 'ID de agente Letta inválido', e_custom: 'no máximo 20 integrações', e_project: 'projeto inválido',
474
525
  },
@@ -605,7 +656,7 @@
605
656
  if (!spec) continue;
606
657
  try {
607
658
  fig.replaceChildren();
608
- lib.createCharacter(fig, spec, { locale: lang, title: name, moodFx: false });
659
+ lib.createCharacter(fig, spec, { locale: lang, title: name, moodFx: false, backdrop: false });
609
660
  } catch {}
610
661
  }
611
662
  }
@@ -632,9 +683,9 @@
632
683
 
633
684
  // ── navigation: one section at a time, remembered in the URL hash ──
634
685
  const NAV = {
635
- en: { acc: 'Account', brain: 'Brain', voice: 'Voice', access: 'Permissions', pet: 'Personality', notes: 'Memory', rem: 'Processes', inf: 'Activity' },
636
- es: { acc: 'Cuenta', brain: 'Cerebro', voice: 'Voz', access: 'Permisos', pet: 'Personalidad', notes: 'Memoria', rem: 'Procesos', inf: 'Actividad' },
637
- pt: { acc: 'Conta', brain: 'Cérebro', voice: 'Voz', access: 'Permissões', pet: 'Personalidade', notes: 'Memória', rem: 'Processos', inf: 'Atividade' },
686
+ en: { acc: 'Account', brain: 'Brain', voice: 'Voice', access: 'Permissions', pet: 'Personality', com: 'Community', notes: 'Memory', rem: 'Processes', inf: 'Activity' },
687
+ es: { acc: 'Cuenta', brain: 'Cerebro', voice: 'Voz', access: 'Permisos', pet: 'Personalidad', com: 'Comunidad', notes: 'Memoria', rem: 'Procesos', inf: 'Actividad' },
688
+ pt: { acc: 'Conta', brain: 'Cérebro', voice: 'Voz', access: 'Permissões', pet: 'Personalidade', com: 'Comunidade', notes: 'Memória', rem: 'Processos', inf: 'Atividade' },
638
689
  };
639
690
  // moods and inference purposes arrive as English codes from the daemon
640
691
  const WORDS = {
@@ -643,7 +694,7 @@
643
694
  };
644
695
  const word = (w) => WORDS[lang]?.[w] || w;
645
696
  const SI = {"anthropic": "M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z", "claude": "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z", "googlegemini": "M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81", "ollama": "M16.361 10.26a.894.894 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.752.752 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.06 1.06 0 0 0-.466 0zm-9.203.005c-.305.096-.533.32-.65.639a1.187 1.187 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.894.894 0 0 0-.565-.472 1.02 1.02 0 0 0-.474.007Zm4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.394.394 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.396.396 0 0 0-.193.032zm.39-2.195c-.373.036-.475.05-.654.086-.291.06-.68.195-.951.328-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a6.873 6.873 0 0 0-.855-.031zm.645.937a3.283 3.283 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3.115 3.115 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042-.57-.107-1.022-.334-1.35-.68-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002zm-5.503-11a1.653 1.653 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208a8.12 8.12 0 0 1-.187.152 5.324 5.324 0 0 0-.949 1.02 5.49 5.49 0 0 0-.94 2.339 6.625 6.625 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.405 7.405 0 0 0-.548 1.873c-.062.417-.071.552-.071.991 0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.915.915 0 0 0-.194-.25 1.74 1.74 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.787.787 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.136 3.136 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04.319 0 .398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2.096 2.096 0 0 1-.224.258.916.916 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a.845.845 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516.029-.217.029-1.016 0-1.258-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141.008-.005.057-.074.108-.152.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6.083 6.083 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.625 6.625 0 0 0-.024-1.358 5.512 5.512 0 0 0-.939-2.339 5.325 5.325 0 0 0-.95-1.02 8.097 8.097 0 0 1-.186-.152.692.692 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a6.805 6.805 0 0 0-.097.726c0 .036-.007.066-.015.066a.96.96 0 0 1-.149-.078A4.857 4.857 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a.958.958 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a6.71 6.71 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2.096 2.096 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.438 8.438 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013zm9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.693 3.693 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012z"};
646
- const LU = {"user-round": "<circle cx=\"12\" cy=\"8\" r=\"5\" /> <path d=\"M20 21a8 8 0 0 0-16 0\" />", "cpu": "<path d=\"M12 20v2\" /> <path d=\"M12 2v2\" /> <path d=\"M17 20v2\" /> <path d=\"M17 2v2\" /> <path d=\"M2 12h2\" /> <path d=\"M2 17h2\" /> <path d=\"M2 7h2\" /> <path d=\"M20 12h2\" /> <path d=\"M20 17h2\" /> <path d=\"M20 7h2\" /> <path d=\"M7 20v2\" /> <path d=\"M7 2v2\" /> <rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"2\" /> <rect x=\"8\" y=\"8\" width=\"8\" height=\"8\" rx=\"1\" />", "mic": "<path d=\"M12 19v3\" /> <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" /> <rect x=\"9\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\" />", "shield-check": "<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\" /> <path d=\"m9 12 2 2 4-4\" />", "message-circle": "<path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" />", "notebook-pen": "<path d=\"M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4\" /> <path d=\"M2 6h4\" /> <path d=\"M2 10h4\" /> <path d=\"M2 14h4\" /> <path d=\"M2 18h4\" /> <path d=\"M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z\" />", "activity": "<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\" />", "scroll-text": "<path d=\"M15 12h-5\" /> <path d=\"M15 8h-5\" /> <path d=\"M19 17V5a2 2 0 0 0-2-2H4\" /> <path d=\"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3\" />", "terminal": "<path d=\"M12 19h8\" /> <path d=\"m4 17 6-6-6-6\" />", "sparkles": "<path d=\"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z\" /> <path d=\"M20 2v4\" /> <path d=\"M22 4h-4\" /> <circle cx=\"4\" cy=\"20\" r=\"2\" />", "workflow": "<rect width=\"8\" height=\"8\" x=\"3\" y=\"3\" rx=\"2\" /> <path d=\"M7 11v4a2 2 0 0 0 2 2h4\" /> <rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\" />", "alarm-clock": "<circle cx=\"12\" cy=\"13\" r=\"8\" /> <path d=\"M12 9v4l2 2\" /> <path d=\"M5 3 2 6\" /> <path d=\"m22 6-3-3\" /> <path d=\"M6.38 18.7 4 21\" /> <path d=\"M17.64 18.67 20 21\" />", "trending-up": "<path d=\"M16 7h6v6\" /> <path d=\"m22 7-8.5 8.5-5-5L2 17\" />", "eye": "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /> <circle cx=\"12\" cy=\"12\" r=\"3\" />", "house": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /> <path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />", "plus": "<path d=\"M5 12h14\"/><path d=\"M12 5v14\"/>", "monitor": "<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\"/><line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\"/>", "palette": "<path d=\"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z\"/><circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/>", "arrow-right": "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>"};
697
+ const LU = {"users":"<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /> <circle cx=\"9\" cy=\"7\" r=\"4\" /> <path d=\"M22 21v-2a4 4 0 0 0-3-3.87\" /> <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />","flag":"<path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" /> <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />","share-2":"<circle cx=\"18\" cy=\"5\" r=\"3\" /> <circle cx=\"6\" cy=\"12\" r=\"3\" /> <circle cx=\"18\" cy=\"19\" r=\"3\" /> <line x1=\"8.59\" x2=\"15.42\" y1=\"13.51\" y2=\"17.49\" /> <line x1=\"15.41\" x2=\"8.59\" y1=\"6.51\" y2=\"10.49\" />","trash-2":"<path d=\"M3 6h18\" /> <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\" /> <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\" /> <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\" /> <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\" />","user-round":"<circle cx=\"12\" cy=\"8\" r=\"5\" /> <path d=\"M20 21a8 8 0 0 0-16 0\" />","cpu":"<path d=\"M12 20v2\" /> <path d=\"M12 2v2\" /> <path d=\"M17 20v2\" /> <path d=\"M17 2v2\" /> <path d=\"M2 12h2\" /> <path d=\"M2 17h2\" /> <path d=\"M2 7h2\" /> <path d=\"M20 12h2\" /> <path d=\"M20 17h2\" /> <path d=\"M20 7h2\" /> <path d=\"M7 20v2\" /> <path d=\"M7 2v2\" /> <rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"2\" /> <rect x=\"8\" y=\"8\" width=\"8\" height=\"8\" rx=\"1\" />","mic":"<path d=\"M12 19v3\" /> <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" /> <rect x=\"9\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\" />","shield-check":"<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\" /> <path d=\"m9 12 2 2 4-4\" />","message-circle":"<path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" />","notebook-pen":"<path d=\"M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4\" /> <path d=\"M2 6h4\" /> <path d=\"M2 10h4\" /> <path d=\"M2 14h4\" /> <path d=\"M2 18h4\" /> <path d=\"M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z\" />","activity":"<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\" />","scroll-text":"<path d=\"M15 12h-5\" /> <path d=\"M15 8h-5\" /> <path d=\"M19 17V5a2 2 0 0 0-2-2H4\" /> <path d=\"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3\" />","terminal":"<path d=\"M12 19h8\" /> <path d=\"m4 17 6-6-6-6\" />","sparkles":"<path d=\"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z\" /> <path d=\"M20 2v4\" /> <path d=\"M22 4h-4\" /> <circle cx=\"4\" cy=\"20\" r=\"2\" />","workflow":"<rect width=\"8\" height=\"8\" x=\"3\" y=\"3\" rx=\"2\" /> <path d=\"M7 11v4a2 2 0 0 0 2 2h4\" /> <rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\" />","alarm-clock":"<circle cx=\"12\" cy=\"13\" r=\"8\" /> <path d=\"M12 9v4l2 2\" /> <path d=\"M5 3 2 6\" /> <path d=\"m22 6-3-3\" /> <path d=\"M6.38 18.7 4 21\" /> <path d=\"M17.64 18.67 20 21\" />","trending-up":"<path d=\"M16 7h6v6\" /> <path d=\"m22 7-8.5 8.5-5-5L2 17\" />","eye":"<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /> <circle cx=\"12\" cy=\"12\" r=\"3\" />","house":"<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /> <path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />","box":"<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>","plus":"<path d=\"M5 12h14\"/><path d=\"M12 5v14\"/>","monitor":"<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\"/><line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\"/>","palette":"<path d=\"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z\"/><circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/>","arrow-right":"<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>","volume-2":"<path d=\"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z\"/><path d=\"M16 9a5 5 0 0 1 0 6\"/><path d=\"M19.364 18.364a9 9 0 0 0 0-12.728\"/>","external-link":"<path d=\"M15 3h6v6\"/><path d=\"M10 14 21 3\"/><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>","sliders-horizontal":"<path d=\"M21 4h-7\"/><path d=\"M10 4H3\"/><path d=\"M21 12h-9\"/><path d=\"M8 12H3\"/><path d=\"M21 20h-5\"/><path d=\"M12 20H3\"/><path d=\"M14 2v4\"/><path d=\"M8 10v4\"/><path d=\"M16 18v4\"/>"};
647
698
  Object.assign(SI, {"elevenlabs": "M4.6035 0v24h4.9317V0zm9.8613 0v24h4.9317V0z", "fishaudio": "M23.12 12.13v.62c0 .24.19.44.44.44.24 0 .44-.2.44-.44v-.62c0-.24-.2-.44-.44-.44-.25 0-.44.2-.44.44m-1.77.37v1.74c0 .24.2.44.44.44.25 0 .44-.2.44-.44V12.5c0-.24-.19-.44-.44-.44-.24 0-.44.2-.44.44m-1.76.43v1.99q0 .09.03.17.04.08.1.14.07.07.15.1t.17.03c.24 0 .44-.19.44-.44v-1.99c0-.25-.2-.44-.44-.44q-.09 0-.17.03t-.15.1q-.06.06-.1.14-.03.08-.03.17m-1.76.35v1.97c0 .24.19.44.44.44.24 0 .44-.2.44-.44v-1.97c0-.24-.2-.44-.44-.44-.25 0-.44.2-.44.44m-1.77.32v1.65c0 .25.2.45.44.45s.44-.2.44-.45V13.6c0-.25-.2-.45-.44-.45s-.44.2-.44.45m-1.76 1.26v.3q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3v-.3q-.01-.17-.14-.3-.13-.11-.31-.12-.17.01-.3.12-.13.13-.14.3M0 9.38v.19c0 .24.2.44.44.44.25 0 .44-.2.44-.44v-.19c0-.25-.19-.44-.44-.44-.24 0-.44.19-.44.44m1.82.15v.81c0 .24.19.44.44.44.24 0 .44-.2.44-.44v-.81q0-.18-.13-.31t-.31-.13-.31.13-.13.31m1.81-.24v3.39q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3V9.29q-.01-.17-.14-.3-.13-.11-.31-.12-.17.01-.3.12-.13.13-.14.3m1.83-.35v.22c0 .24.19.44.44.44.24 0 .44-.2.44-.44v-.22q0-.18-.13-.31T5.9 8.5t-.31.13-.13.31m0 2.94v2.34c0 .25.19.44.44.44.24 0 .44-.19.44-.44v-2.34c0-.24-.2-.44-.44-.44-.25 0-.44.2-.44.44m15.89-3.32V11c0 .25.2.44.44.44.25 0 .44-.19.44-.44V8.56c0-.24-.19-.44-.44-.44-.24 0-.44.2-.44.44m-1.76-.39v3.28q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3V8.17c0-.25-.2-.44-.44-.44q-.09 0-.17.03t-.15.1q-.06.06-.1.14-.03.08-.03.17M17.83 8v3.9c0 .24.19.44.44.44.24 0 .44-.2.44-.44V8c0-.25-.2-.44-.44-.44-.25 0-.44.19-.44.44m-1.77.11v3.96c0 .25.2.44.44.44s.44-.19.44-.44V8.11c0-.25-.2-.44-.44-.44s-.44.19-.44.44m-1.76.27v5.03q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3V8.38c0-.25-.2-.44-.45-.44-.24 0-.44.19-.44.44m-1.8.46v5.75q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3V8.84c0-.25-.2-.44-.45-.44-.24 0-.44.19-.44.44m-1.76.76v5.52c0 .24.2.44.44.44.25 0 .44-.2.44-.44V9.6c0-.24-.19-.44-.44-.44-.24 0-.44.2-.44.44m-1.77 1.01v4.77q.01.17.14.3.13.11.3.12.18-.01.31-.12.13-.13.14-.3v-4.77q-.01-.17-.14-.3-.13-.11-.31-.12-.17.01-.3.12-.13.13-.14.3M23.12 9.6v1.22c0 .24.19.44.44.44.24 0 .44-.2.44-.44V9.6c0-.24-.2-.44-.44-.44-.25 0-.44.2-.44.44M7.21 11.32V16c0 .24.19.44.44.44.24 0 .44-.2.44-.44v-4.68q0-.18-.13-.31t-.31-.13-.31.13-.13.31", "gmail": "M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z", "grok": "M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815", "openai": "M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z", "x": "M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"});
648
699
  Object.assign(LU, {"plug": "<path d=\"M12 22v-5\"/><path d=\"M9 8V2\"/><path d=\"M15 8V2\"/><path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\"/>", "cloud": "<path d=\"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z\"/>", "send": "<path d=\"m22 2-7 20-4-9-9-4Z\"/><path d=\"M22 2 11 13\"/>", "upload": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>", "download": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\"/>", "volume-2": "<path d=\"M11 5 6 9H2v6h4l5 4V5Z\"/><path d=\"M15.54 8.46a5 5 0 0 1 0 7.07\"/><path d=\"M19.07 4.93a10 10 0 0 1 0 14.14\"/>", "volume-x": "<path d=\"M11 5 6 9H2v6h4l5 4V5Z\"/><line x1=\"22\" x2=\"16\" y1=\"9\" y2=\"15\"/><line x1=\"16\" x2=\"22\" y1=\"9\" y2=\"15\"/>", "laptop": "<path d=\"M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16\"/>", "globe": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\"/><path d=\"M2 12h20\"/>", "refresh-cw": "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/>"});
649
700
  // Full-colour marks (their own squares): apuchat's rat, apumail. (Orquesta: the masked mark below.)
@@ -677,8 +728,8 @@
677
728
  s.classList.add('ico', `logo-${name}`);
678
729
  return s;
679
730
  };
680
- const ICON = { acc: 'user-round', brain: 'cpu', voice: 'mic', access: 'shield-check', pet: 'message-circle', notes: 'notebook-pen', rem: 'activity', inf: 'scroll-text' };
681
- const SECS = { acc: 'hAcc', brain: 'hBrain', voice: 'hVoice', access: 'hAccess', pet: 'hPet', notes: 'hNotes', rem: 'hRem', inf: 'hInf' };
731
+ const ICON = { acc: 'user-round', brain: 'cpu', voice: 'mic', access: 'shield-check', pet: 'message-circle', com: 'users', notes: 'notebook-pen', rem: 'activity', inf: 'scroll-text' };
732
+ const SECS = { acc: 'hAcc', brain: 'hBrain', voice: 'hVoice', access: 'hAccess', pet: 'hPet', com: 'hCom', notes: 'hNotes', rem: 'hRem', inf: 'hInf' };
682
733
  const navT = NAV[lang] || NAV.en;
683
734
  $('#nav').replaceChildren(...Object.keys(SECS).map((k) => el('a', { href: `#${k}` }, el('span', {}, ico(ICON[k])), el('b', { textContent: navT[k] }))));
684
735
  function go() {
@@ -691,6 +742,7 @@
691
742
  if (k === 'acc') loadAccount();
692
743
  if (k === 'rem') loadProcesses();
693
744
  if (k === 'notes') loadMemory();
745
+ if (k === 'com') setTimeout(renderCommunity); // after the script ran (com is declared further down)
694
746
  window.scrollTo(0, 0);
695
747
  }
696
748
  addEventListener('hashchange', go);
@@ -819,7 +871,8 @@
819
871
  if (!disabled) input.addEventListener('change', () => save({ access: { [key]: input.checked } }, $('#accessMsg')).then(() => (key === 'screen' || key === 'vm') && renderAccess()));
820
872
  return el('div', { className: `sw${soon ? ' soon' : ''}` }, el('b', {}, t(lk), soon ? el('span', { className: 'tag', textContent: t('soon') }) : null), el('small', { textContent: t(`${lk}D`) }), el('span', { className: 'tog' }, input, el('span')));
821
873
  };
822
- for (const k of ['agent', 'memory', 'open', 'notify']) box.append(sw(k, S.config.access[k] !== false, false, false));
874
+ for (const k of ['agent', 'memory', 'open', 'notify', 'sites']) box.append(sw(k, S.config.access[k] !== false, false, false));
875
+ box.children[1].append(convRow());
823
876
  const scr = sw('screen', S.config.access.screen === true, false, false);
824
877
  if (S.config.access.screen) scr.append(screenRow());
825
878
  box.append(scr);
@@ -832,6 +885,22 @@
832
885
  box.append(el('span', { className: 'msg', id: 'accessMsg', role: 'status' }));
833
886
  renderInteg();
834
887
  }
888
+ // Under “Learn about you”: how much of the conversation it keeps (older turns get summarized), 0 = off.
889
+ function convRow() {
890
+ const cur = S.config.memory?.tokens ?? 20000;
891
+ const opts = [0, 5000, 10000, 20000, 50000, 100000, 200000];
892
+ if (!opts.includes(cur)) opts.push(cur), opts.sort((a, b) => a - b);
893
+ const pick = el('select', {}, ...opts.map((n) => el('option', { value: n, textContent: n ? `${n / 1000}k ${t('convTok')}` : t('convOff'), selected: cur === n })));
894
+ pick.addEventListener('change', () => save({ memory: { tokens: Number(pick.value) } }, $('#accessMsg')));
895
+ const used = M?.conversation?.tokens;
896
+ const forget = el('button', { textContent: t('convForget') });
897
+ forget.addEventListener('click', async () => {
898
+ await api('/conversation/forget', {});
899
+ flash($('#accessMsg'), t('convGone'));
900
+ loadMemory();
901
+ });
902
+ return el('div', { className: 'sub' }, t('convL'), pick, used ? el('span', { textContent: t('convUse', { n: used }) }) : null, forget);
903
+ }
835
904
  // Under “Hand work to your server”: which kinds the pet may send (code/shell only if ticked here).
836
905
  function vmKindsRow() {
837
906
  const on = new Set(S.config.access.vmKinds || ['research', 'browse', 'long']);
@@ -872,6 +941,78 @@
872
941
  return el('div', { className: 'sub' }, t('screenEvery'), every, t('min'), now, last, S.vision ? null : el('div', { textContent: t('noVision') }));
873
942
  }
874
943
 
944
+ // ── a provider's own voice: pick it (listed with your key when the provider allows), its model and style, try it ──
945
+ const VPROV = {
946
+ elevenlabs: { list: true, models: ['eleven_multilingual_v2', 'eleven_flash_v2_5', 'eleven_turbo_v2_5', 'eleven_v3'], page: 'https://elevenlabs.io/app/voice-library' },
947
+ openai: { list: true, models: ['gpt-4o-mini-tts', 'tts-1', 'tts-1-hd'], style: true, page: 'https://www.openai.fm/' },
948
+ grok: { list: true, models: [], page: 'https://console.x.ai/' },
949
+ fish: { list: true, models: ['s1', 'speech-1.6', 'speech-1.5'], page: 'https://fish.audio/app/discovery/' },
950
+ apuchat: { list: false, models: [], page: 'https://voice.apuchat.com/' },
951
+ };
952
+ function vconf(kind) {
953
+ const P = VPROV[kind];
954
+ const vfield = (label, input) => el('label', { className: 'f' }, label, input);
955
+ const cur = S.config.voice?.providers?.[kind] || {};
956
+ const has = S.voices.keys[kind];
957
+ const msg = el('span', { className: 'msg', role: 'status' });
958
+ const idIn = el('input', { type: 'text', value: cur.voiceId || '', placeholder: t('vcIdPh'), autocomplete: 'off' });
959
+ const pick = el('select', { hidden: true });
960
+ const model = el('input', { type: 'text', value: cur.model || '', placeholder: P.models[0] || t('vcDefault'), autocomplete: 'off' });
961
+ if (P.models.length) model.setAttribute('list', `vm-${kind}`);
962
+ const dl = el('datalist', { id: `vm-${kind}` }, ...P.models.map((m) => el('option', { value: m })));
963
+ const style = el('textarea', { rows: 2, value: cur.style || '', placeholder: t('vcStylePh') });
964
+ const loadList = async () => {
965
+ if (!P.list || !has) return;
966
+ pick.hidden = false;
967
+ pick.replaceChildren(el('option', { value: '', textContent: '…' }));
968
+ try {
969
+ const { voices } = await api('/voice/list', { kind });
970
+ pick.replaceChildren(el('option', { value: '', textContent: voices.length ? t('vcPick', { n: voices.length }) : t('vcNoList') }), ...voices.map((v) => el('option', { value: v.id, textContent: v.note ? `${v.name} · ${v.note}` : v.name, selected: v.id === idIn.value })));
971
+ } catch (e) {
972
+ pick.hidden = true;
973
+ flash(msg, t('err', { e: errText(e) }), 'err');
974
+ }
975
+ };
976
+ pick.addEventListener('change', () => pick.value && (idIn.value = pick.value));
977
+ const test = el('button', {}, ico('volume-2', 15), ' ', t('vcTest'));
978
+ test.addEventListener('click', async (e) => {
979
+ e.preventDefault();
980
+ test.disabled = true;
981
+ flash(msg, t('vcTesting'), 'ok');
982
+ try {
983
+ const r = await fetch('/voice/test', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': BOOT.token }, body: JSON.stringify({ kind, voiceId: idIn.value, model: model.value, style: style.value }) });
984
+ if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || `HTTP ${r.status}`);
985
+ const a = new Audio(URL.createObjectURL(await r.blob()));
986
+ await a.play();
987
+ msg.textContent = '';
988
+ } catch (x) {
989
+ flash(msg, t('err', { e: errText(x) }), 'err');
990
+ } finally {
991
+ test.disabled = false;
992
+ }
993
+ });
994
+ const ok = el('button', { className: 'primary', textContent: t('vcSave') });
995
+ ok.addEventListener('click', async (e) => {
996
+ e.preventDefault();
997
+ await save({ voice: { kind, providers: { [kind]: { voiceId: idIn.value, model: model.value, style: style.value, name: pick.value === idIn.value ? pick.selectedOptions[0]?.textContent || '' : '' } } } }, msg);
998
+ S.config.voice = { ...S.config.voice, kind, providers: { ...(S.config.voice?.providers || {}), [kind]: { voiceId: idIn.value, model: model.value, style: style.value } } };
999
+ const r = box.querySelector(`#v-${kind}`);
1000
+ if (r) r.checked = true;
1001
+ });
1002
+ const page = el('a', { href: P.page, target: '_blank', rel: 'noopener', className: 'ext' }, ico('external-link', 14), ' ', t('vcPage'));
1003
+ const sumTxt = cur.voiceId ? t('vcNow', { v: cur.name || cur.voiceId }) : t('vcConf');
1004
+ const box = el('details', { className: 'vconf' }, el('summary', {}, ico('sliders-horizontal', 14), ' ', sumTxt),
1005
+ el('div', { className: 'rows' },
1006
+ has || !P.list ? null : el('small', { textContent: t('vcKeyFirst') }),
1007
+ pick,
1008
+ vfield(t('vcId'), idIn),
1009
+ vfield(t('vcModel'), model), dl,
1010
+ P.style ? vfield(t('vcStyle'), style) : null,
1011
+ el('div', { className: 'row' }, test, ok, page, msg)));
1012
+ box.addEventListener('toggle', () => box.open && !pick.options.length && loadList(), { once: false });
1013
+ return box;
1014
+ }
1015
+
875
1016
  // ── voice ──
876
1017
  function renderVoices() {
877
1018
  const box = $('#voices');
@@ -891,11 +1032,11 @@
891
1032
  const vkey = (name, label, has) => el('label', { className: 'f' }, `${label} · ${has ? '✓' : t('keyNone')}`, el('input', { type: 'password', name, placeholder: has ? t('keySaved') : '', autocomplete: 'off' }));
892
1033
  box.replaceChildren(
893
1034
  opt('none'), opt('browser'), opt('local'),
894
- opt('elevenlabs', [vkey('ELEVENLABS_API_KEY', 'ElevenLabs ' + t('key'), S.voices.keys.elevenlabs)]),
895
- opt('openai', [vkey('OPENAI_API_KEY_V', 'OpenAI ' + t('key'), S.voices.keys.openai)]),
896
- opt('apuchat', [vkey('APUCHAT_VOICE_TOKEN', 'apuchat ' + t('key'), S.voices.keys.apuchat)]),
897
- opt('grok', [vkey('XAI_API_KEY', 'xAI ' + t('key'), S.voices.keys.grok)]),
898
- opt('fish', [vkey('FISH_API_KEY', 'Fish Audio ' + t('key'), S.voices.keys.fish)]),
1035
+ opt('elevenlabs', [vkey('ELEVENLABS_API_KEY', 'ElevenLabs ' + t('key'), S.voices.keys.elevenlabs), vconf('elevenlabs')]),
1036
+ opt('openai', [vkey('OPENAI_API_KEY_V', 'OpenAI ' + t('key'), S.voices.keys.openai), vconf('openai')]),
1037
+ opt('apuchat', [vkey('APUCHAT_VOICE_TOKEN', 'apuchat ' + t('key'), S.voices.keys.apuchat), vconf('apuchat')]),
1038
+ opt('grok', [vkey('XAI_API_KEY', 'xAI ' + t('key'), S.voices.keys.grok), vconf('grok')]),
1039
+ opt('fish', [vkey('FISH_API_KEY', 'Fish Audio ' + t('key'), S.voices.keys.fish), vconf('fish')]),
899
1040
  );
900
1041
  const stt = { local: t('sttLocal'), openai: 'OpenAI', elevenlabs: 'ElevenLabs' }[S.stt] || t('sttNone');
901
1042
  $('#stt').replaceChildren(t('sttNow'), ' ', el('b', { textContent: stt }));
@@ -964,11 +1105,21 @@
964
1105
  const asText = (v) => (v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v, null, 2));
965
1106
  async function loadInferences() {
966
1107
  let L = [];
1108
+ let ots = [];
1109
+ const sel = $('#infOt');
967
1110
  try {
968
- L = (await api('/inferences?n=80')).inferences;
1111
+ ({ inferences: L, ots = [] } = await api(`/inferences?n=80${sel.value ? `&ot=${encodeURIComponent(sel.value)}` : ''}`));
969
1112
  } catch {
970
1113
  return;
971
1114
  }
1115
+ // which ot made each call (the host also sees its visitors'): a filter when there is more than one
1116
+ const many = ots.length > 1;
1117
+ sel.hidden = !many;
1118
+ if (many && sel.options.length !== ots.length + 1) {
1119
+ const cur = sel.value;
1120
+ sel.replaceChildren(el('option', { value: '', textContent: t('infAll') }), ...ots.map((o) => el('option', { value: o.id, textContent: o.name })));
1121
+ sel.value = ots.some((o) => o.id === cur) ? cur : '';
1122
+ }
972
1123
  // Keep open rows open across refreshes.
973
1124
  const open = new Set([...document.querySelectorAll('#infs details[open]')].map((d) => d.dataset.at));
974
1125
  // Tokens: what each call cost, and the total of what's listed ("≈" when the brain doesn't report it).
@@ -976,12 +1127,13 @@
976
1127
  const sum = L.reduce((a, r) => (r.usage ? { in: a.in + r.usage.in + (r.usage.cacheRead || 0) + (r.usage.cacheWrite || 0), out: a.out + r.usage.out, usd: a.usd + (r.usage.usd || 0), n: a.n + 1, approx: a.approx || r.usage.approx } : a), { in: 0, out: 0, usd: 0, n: 0, approx: false });
977
1128
  $('#infTotal').textContent = sum.n ? t('infTotal', { n: sum.n, in: num(sum.in), out: num(sum.out), all: `${sum.approx ? '≈' : ''}${num(sum.in + sum.out)}` }) + (sum.usd ? ` · $${sum.usd.toFixed(3)}` : '') : '';
978
1129
  $('#infs').replaceChildren(...(L.length ? L.map((r) => {
979
- const d = row(`${word(r.purpose || '?')} · ${r.ok ? asText(r.output).slice(0, 160) : '✗ ' + r.error}`, [r.brain, `${(r.ms / 1000).toFixed(1)} s`, tok(r.usage)].filter(Boolean).join(' · '), r.at, () => [[t('fSystem'), asText(r.system)], [t('fInput'), asText(r.input)], [t('fFiles'), (r.files || []).join('\n')], [r.ok ? t('fOutput') : t('fError'), r.ok ? asText(r.output) : r.error]], !r.ok);
980
- d.dataset.at = r.at;
981
- if (open.has(String(r.at))) d.open = true;
1130
+ const d = row(`${word(r.purpose || '?')} · ${r.ok ? asText(r.output).slice(0, 160) : '✗ ' + r.error}`, [many && r.ot ? `◉ ${r.ot.name}` : '', r.brain, `${(r.ms / 1000).toFixed(1)} s`, tok(r.usage)].filter(Boolean).join(' · '), r.at, () => [[t('fSystem'), asText(r.system)], [t('fInput'), asText(r.input)], [t('fFiles'), (r.files || []).join('\n')], [r.ok ? t('fOutput') : t('fError'), r.ok ? asText(r.output) : r.error]], !r.ok);
1131
+ d.dataset.at = `${r.ot?.id || ''}:${r.at}`;
1132
+ if (open.has(d.dataset.at)) d.open = true;
982
1133
  return d;
983
1134
  }) : [el('p', { className: 'empty', textContent: t('noInf') })]));
984
1135
  }
1136
+ $('#infOt').addEventListener('change', () => loadInferences());
985
1137
  $('#clearInf').addEventListener('click', async () => {
986
1138
  await api('/inferences/clear', {});
987
1139
  loadInferences();
@@ -998,6 +1150,291 @@
998
1150
  return el('label', { className: 'chip' }, r, t(`v${k}`));
999
1151
  }));
1000
1152
  }
1153
+
1154
+ // ── scenes between ots (pet/electron/director.cjs reads them from GET /scenes) ──
1155
+ const SCENES = ['rocky', 'sf', 'dbz', 'matrix', 'duel', 'sw', 'fight', 'twist'];
1156
+ function renderScenes() {
1157
+ const sc = S.config.pet.scenes || { on: true, every: 'normal', list: SCENES };
1158
+ $('#scVisitor').hidden = !S.visitor;
1159
+ $('#scConf').hidden = $('#scList').hidden = !!S.visitor;
1160
+ const put = (scenes) => save({ pet: { scenes } }, $('#scMsg')).then(() => renderScenes());
1161
+ $('#scOn').checked = sc.on !== false;
1162
+ $('#scOn').onchange = (e) => put({ on: e.target.checked });
1163
+ $('#scEvery').replaceChildren(...['rare', 'normal', 'often'].map((k) => {
1164
+ const r = el('input', { type: 'radio', name: 'scEvery', value: k, checked: sc.every === k });
1165
+ r.addEventListener('change', () => put({ every: k }));
1166
+ return el('label', { className: 'chip' }, r, (T[lang].scEvery || T.en.scEvery)[k]);
1167
+ }));
1168
+ $('#scList').replaceChildren(...SCENES.map((k) => {
1169
+ const c = el('input', { type: 'checkbox', checked: sc.list.includes(k) });
1170
+ c.addEventListener('change', () => put({ list: SCENES.filter((n) => (n === k ? c.checked : sc.list.includes(n))) }));
1171
+ return el('label', { className: 'chip' }, c, (T[lang].scNames || T.en.scNames)[k]);
1172
+ }));
1173
+ }
1174
+ $('#scPlay').addEventListener('click', async () => {
1175
+ try {
1176
+ await api('/scenes/play', {});
1177
+ flash($('#scMsg'), t('scPlayed'));
1178
+ } catch (e) {
1179
+ flash($('#scMsg'), t('err', { e: e.message }), 'err');
1180
+ }
1181
+ });
1182
+
1183
+ // ── mods of its look (src/character/mods.js): previews drawn with the 7ots library ──
1184
+ const modName = (m) => (m.name && typeof m.name === 'object' ? m.name[lang] || m.name.en || Object.values(m.name)[0] : m.name) || m.id;
1185
+ const modViews = []; // previews 3D vivos (se destruyen al redibujar)
1186
+ async function renderMods() {
1187
+ const base = S.otLook || {};
1188
+ const on = base.mods || [];
1189
+ const catalog = S.modCatalog || [];
1190
+ const known = [...catalog, ...(S.modLibrary || [])]; // el catálogo + los importados en este pet
1191
+ const list = [...known, ...on.filter((m) => !known.some((c) => c.id === m.id))].filter((m, i, a) => a.findIndex((x) => x.id === m.id) === i);
1192
+ const msg = el('span', { className: 'msg', role: 'status' });
1193
+ const stages = [];
1194
+ const models = []; // mods de cuerpo completo: preview 3D en vivo (un solo renderer compartido)
1195
+ const cards = list.map((m) => {
1196
+ const isOn = on.some((x) => x.id === m.id);
1197
+ const b = el('button', { className: isOn ? '' : 'primary', textContent: isOn ? t('modOff') : t('modPut') });
1198
+ b.addEventListener('click', () => setMod(isOn ? { remove: m.id } : { add: m.id }, b, msg));
1199
+ // your own mods (imported or made here, not the catalog's nor the gallery's) can go to the community
1200
+ let acts = b;
1201
+ if (!catalog.some((c) => c.id === m.id) && !com.fromGallery.has(m.id)) {
1202
+ const sh = el('button', { className: 'icon', title: t('comShare'), ariaLabel: t('comShare') }, ico('share-2', 15));
1203
+ sh.addEventListener('click', () => shareMod(m.id, sh, msg));
1204
+ acts = el('div', { className: 'acts' }, b, sh);
1205
+ }
1206
+ const fig = el('div', {}, m.model ? ico('box', 56) : el('span', { className: 'init', textContent: modName(m)[0].toUpperCase() }));
1207
+ if (m.model) models.push([fig, { ...base, mods: [m] }]);
1208
+ else stages.push([fig, { ...base, mods: [...on.filter((x) => x.id !== m.id), m] }]);
1209
+ const card = el('div', { className: `ot${isOn ? ' cur' : ''}` }, el('div', { className: 'stage' }, fig), el('b', { textContent: modName(m) }),
1210
+ el('small', { textContent: m.model ? t('modModelBy') : [m.by, m.parts.some((p) => p.glb) ? '2D + 3D' : '2D'].filter(Boolean).join(' · ') }), acts);
1211
+ if (isOn) card.append(el('span', { className: 'tag' }, ico('sparkles', 12), t('modOn')));
1212
+ return card;
1213
+ });
1214
+ const file = el('input', { type: 'file', accept: '.json,application/json', hidden: true });
1215
+ file.addEventListener('change', async () => {
1216
+ const f = file.files[0];
1217
+ if (!f) return;
1218
+ let mod;
1219
+ try {
1220
+ mod = JSON.parse(await f.text());
1221
+ } catch {
1222
+ return flash(msg, t('modBad'), 'err');
1223
+ }
1224
+ setMod({ mod }, null, msg);
1225
+ });
1226
+ const imp = el('button', { className: 'ot new' }, ico('upload', 28), el('span', { textContent: t('modImport') }));
1227
+ imp.addEventListener('click', () => file.click());
1228
+ // muñeco 3D completo: el .vrm/.glb se guarda en el pet y el mod lo apunta
1229
+ const mfile = el('input', { type: 'file', accept: '.vrm,.glb', hidden: true });
1230
+ mfile.addEventListener('change', async () => {
1231
+ const f = mfile.files[0];
1232
+ if (!f) return;
1233
+ const ext = /\.vrm$/i.test(f.name) ? 'vrm' : 'glb';
1234
+ flash(msg, t('modModelHint'), 'ok');
1235
+ try {
1236
+ const r = await fetch(`/mods/upload?ext=${ext}`, { method: 'POST', headers: { 'X-7ots-Token': BOOT.token }, body: f });
1237
+ const d = await r.json().catch(() => ({}));
1238
+ if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
1239
+ const name = f.name.replace(/\.(vrm|glb)$/i, '').replace(/[<>]/g, '').slice(0, 40) || 'model';
1240
+ const id = `model-${d.url.slice(6, 14)}`;
1241
+ await setMod({ mod: { id, name, model: { url: d.url } } }, null, msg);
1242
+ } catch (e) {
1243
+ flash(msg, t('err', { e: errText(e) }), 'err');
1244
+ }
1245
+ mfile.value = '';
1246
+ });
1247
+ const impModel = el('button', { className: 'ot new' }, ico('box', 28), el('span', { textContent: t('modModel') }));
1248
+ impModel.addEventListener('click', () => mfile.click());
1249
+ $('#mods').replaceChildren(el('div', { className: 'otgrid' }, ...cards, impModel, imp, file, mfile), el('div', { className: 'row' }, msg));
1250
+ const lib = await loadChars();
1251
+ if (!lib?.createCharacter) return;
1252
+ for (const [fig, spec] of stages) {
1253
+ try {
1254
+ fig.replaceChildren();
1255
+ lib.createCharacter(fig, spec, { locale: lang, moodFx: false, backdrop: false });
1256
+ } catch {}
1257
+ }
1258
+ for (const v of modViews.splice(0)) v.destroy?.();
1259
+ const m3 = models.length && (await lib.loadCharacter3D?.().catch(() => null));
1260
+ if (!m3?.webglAvailable?.()) return;
1261
+ for (const [fig, spec] of models) {
1262
+ try {
1263
+ fig.replaceChildren();
1264
+ modViews.push(m3.createCharacter3D(fig, spec, { backdrop: false, moodFx: false }));
1265
+ } catch {}
1266
+ }
1267
+ }
1268
+ async function setMod(body, b, msg) {
1269
+ if (b) b.disabled = true;
1270
+ try {
1271
+ await api('/identity/mods', body);
1272
+ S = await api('/config');
1273
+ await renderMods();
1274
+ } catch (e) {
1275
+ if (b) b.disabled = false;
1276
+ flash(msg, t('err', { e: errText(e) }), 'err');
1277
+ }
1278
+ }
1279
+ // ── community: the 7ots.com gallery (server/platform/mods.mjs), through the pet (/community/*) ──
1280
+ const com = { sort: 'top', q: '', signedIn: false, fromGallery: new Set(), views: [] };
1281
+ async function shareMod(id, b, msg) {
1282
+ b.disabled = true;
1283
+ try {
1284
+ await api('/community/share', { id });
1285
+ flash(msg, t('comShared'));
1286
+ if (com.loaded) renderCommunity();
1287
+ } catch (e) {
1288
+ flash(msg, t('err', { e: errText(e) }), 'err');
1289
+ }
1290
+ b.disabled = false;
1291
+ }
1292
+ function comCard(v, { mineList = false } = {}) {
1293
+ const m = v.mod;
1294
+ const msg = $('#comMsg');
1295
+ const fig = el('div', {}, m.model ? ico('box', 56) : el('span', { className: 'init', textContent: modName(m)[0].toUpperCase() }));
1296
+ const meta = el('div', { className: 'com-meta' },
1297
+ el('span', { className: 'rt', textContent: v.rating ? `★ ${v.rating} (${v.ratings})` : t('comNoRating') }),
1298
+ el('span', { textContent: t('comInstalls', { n: v.installs }) }));
1299
+ const card = el('div', { className: 'ot' }, el('div', { className: 'stage' }, fig), el('b', { textContent: modName(m) }), el('small', { textContent: [v.author, m.model ? t('modModelBy') : '2D'].filter(Boolean).join(' · ') }), meta);
1300
+ if (mineList || v.mine) card.append(el('span', { className: 'st', textContent: t(`st_${v.status}`) }));
1301
+ // stars: once installed, one rating per account (not on your own)
1302
+ if (!mineList && com.signedIn && !v.mine) {
1303
+ const stars = el('div', { className: 'stars', title: v.installed ? '' : t('comRateHint') });
1304
+ for (let n = 1; n <= 5; n++) {
1305
+ const sb = el('button', { className: n <= (v.myStars || 0) ? 'on' : '', textContent: '★', disabled: !v.installed, ariaLabel: `${n}/5` });
1306
+ sb.addEventListener('click', async () => {
1307
+ try {
1308
+ const r = await api('/community/rate', { id: v.id, stars: n });
1309
+ Object.assign(v, r.mod);
1310
+ flash(msg, t('comRated'));
1311
+ card.replaceWith(comCard(v));
1312
+ } catch (e) {
1313
+ flash(msg, t('err', { e: errText(e) }), 'err');
1314
+ }
1315
+ });
1316
+ stars.append(sb);
1317
+ }
1318
+ card.append(stars);
1319
+ }
1320
+ const acts = el('div', { className: 'acts' });
1321
+ if (mineList) {
1322
+ const del = el('button', {}, ico('trash-2', 15), ' ', t('comDel'));
1323
+ del.addEventListener('click', async () => {
1324
+ del.disabled = true;
1325
+ try {
1326
+ await api('/community/remove', { id: v.id });
1327
+ renderCommunity();
1328
+ } catch (e) {
1329
+ del.disabled = false;
1330
+ flash(msg, t('err', { e: errText(e) }), 'err');
1331
+ }
1332
+ });
1333
+ acts.append(del);
1334
+ } else {
1335
+ const wearing = (S.otLook?.mods || []).some((x) => x.id === v.id);
1336
+ const inst = el('button', { className: wearing ? '' : 'primary', disabled: wearing }, ico('download', 15), ' ', wearing ? t('modOn') : v.installed ? t('comWear') : t('comInstall'));
1337
+ inst.addEventListener('click', async () => {
1338
+ inst.disabled = true;
1339
+ flash(msg, t('modModelHint'));
1340
+ try {
1341
+ await api('/community/install', { id: v.id });
1342
+ com.fromGallery.add(v.id);
1343
+ S = await api('/config');
1344
+ flash(msg, t('saved'));
1345
+ renderMods();
1346
+ renderCommunity();
1347
+ } catch (e) {
1348
+ inst.disabled = false;
1349
+ flash(msg, t('err', { e: errText(e) }), 'err');
1350
+ }
1351
+ });
1352
+ acts.append(inst);
1353
+ if (com.signedIn && !v.mine && !v.reported) {
1354
+ const rep = el('button', { className: 'icon', title: t('comReport'), ariaLabel: t('comReport') }, ico('flag', 15));
1355
+ rep.addEventListener('click', () => {
1356
+ const why = el('input', { placeholder: t('comReportPh'), maxLength: 300 });
1357
+ const send = el('button', { textContent: t('comReportSend') });
1358
+ send.addEventListener('click', async () => {
1359
+ send.disabled = true;
1360
+ try {
1361
+ await api('/community/report', { id: v.id, reason: why.value });
1362
+ v.reported = true;
1363
+ flash(msg, t('comReported'));
1364
+ card.replaceWith(comCard(v));
1365
+ } catch (e) {
1366
+ send.disabled = false;
1367
+ flash(msg, t('err', { e: errText(e) }), 'err');
1368
+ }
1369
+ });
1370
+ rep.replaceWith();
1371
+ card.append(el('div', { className: 'rep' }, why, send));
1372
+ why.focus();
1373
+ });
1374
+ acts.append(rep);
1375
+ }
1376
+ }
1377
+ card.append(acts);
1378
+ card._preview = [fig, m];
1379
+ return card;
1380
+ }
1381
+ async function comPreviews(cards) {
1382
+ for (const v of com.views.splice(0)) v.destroy?.();
1383
+ const lib = await loadChars();
1384
+ if (!lib?.createCharacter) return;
1385
+ const base = S.otLook || {};
1386
+ const m3 = cards.some((c) => c._preview[1].model) && (await lib.loadCharacter3D?.().catch(() => null));
1387
+ for (const c of cards) {
1388
+ const [fig, m] = c._preview;
1389
+ try {
1390
+ if (m.model) {
1391
+ if (!m3?.webglAvailable?.() || com.views.length >= 12) continue; // each 3D preview is a WebGL context
1392
+ fig.replaceChildren();
1393
+ com.views.push(m3.createCharacter3D(fig, { ...base, mods: [m] }, { backdrop: false, moodFx: false }));
1394
+ } else {
1395
+ fig.replaceChildren();
1396
+ lib.createCharacter(fig, { ...base, mods: [m] }, { locale: lang, moodFx: false, backdrop: false });
1397
+ }
1398
+ } catch {}
1399
+ }
1400
+ }
1401
+ let comSeq = 0;
1402
+ async function renderCommunity() {
1403
+ com.loaded = true;
1404
+ const seq = ++comSeq;
1405
+ $('#comSort').replaceChildren(...['top', 'new', 'popular'].map((k) => {
1406
+ const r = el('input', { type: 'radio', name: 'comSort', value: k, checked: com.sort === k });
1407
+ r.addEventListener('change', () => { com.sort = k; renderCommunity(); });
1408
+ return el('label', { className: 'chip' }, r, t({ top: 'comTop', new: 'comNew', popular: 'comPop' }[k]));
1409
+ }));
1410
+ $('#comQ').placeholder = t('comSearch');
1411
+ let list;
1412
+ try {
1413
+ list = await api('/community/list', { sort: com.sort, q: com.q });
1414
+ } catch (e) {
1415
+ return $('#community').replaceChildren(el('p', { className: 'empty', textContent: t('err', { e: errText(e) }) }));
1416
+ }
1417
+ if (seq !== comSeq) return;
1418
+ com.signedIn = !!list.signedIn;
1419
+ for (const v of list.mods) if (v.installed) com.fromGallery.add(v.id);
1420
+ $('#comLogin').hidden = com.signedIn;
1421
+ const cards = list.mods.map((v) => comCard(v));
1422
+ $('#community').replaceChildren(cards.length ? el('div', { className: 'otgrid' }, ...cards) : el('p', { className: 'empty', textContent: t('comEmpty') }));
1423
+ let mineCards = [];
1424
+ if (com.signedIn) {
1425
+ const mine = await api('/community/mine', {}).catch(() => ({ mods: [] }));
1426
+ for (const v of mine.mods) com.fromGallery.add(v.id);
1427
+ mineCards = mine.mods.map((v) => comCard(v, { mineList: true }));
1428
+ $('#comMine').replaceChildren(mineCards.length ? el('div', { className: 'otgrid' }, ...mineCards) : el('p', { className: 'empty', textContent: t('comMineEmpty') }));
1429
+ }
1430
+ $('#comMine').previousElementSibling.hidden = $('#comMine').hidden = !com.signedIn;
1431
+ comPreviews([...cards, ...mineCards]);
1432
+ }
1433
+ let comQT;
1434
+ $('#comQ').addEventListener('input', (e) => {
1435
+ clearTimeout(comQT);
1436
+ comQT = setTimeout(() => { com.q = e.target.value.trim(); renderCommunity(); }, 300);
1437
+ });
1001
1438
  $('#annoy').addEventListener('change', (e) => save({ pet: { annoy: Number(e.target.value) } }, $('#petMsg')));
1002
1439
 
1003
1440
  // ── integrations (Permissions → Integrations) ──
@@ -1047,7 +1484,28 @@
1047
1484
  } catch (e) {
1048
1485
  return proj.replaceChildren(el('small', { className: 'bad', textContent: e.message }));
1049
1486
  }
1050
- if (!r.connected) return proj.replaceChildren(el('small', { textContent: r.error || t('orqNoToken') }));
1487
+ if (!r.connected) {
1488
+ // sign in in the browser (the same flow as 7ots login orquesta), then the agents show up here
1489
+ const go = el('button', { className: 'primary', textContent: t('orqConnect') });
1490
+ const note = el('small', { textContent: r.error || t('orqNoToken') });
1491
+ go.addEventListener('click', async () => {
1492
+ go.disabled = true;
1493
+ note.textContent = t('orqWaiting');
1494
+ try {
1495
+ await api('/orquesta/connect', {});
1496
+ } catch (e) {
1497
+ go.disabled = false;
1498
+ return (note.textContent = e.message);
1499
+ }
1500
+ for (let i = 0; i < 100; i++) {
1501
+ await new Promise((res) => setTimeout(res, 3000));
1502
+ if ((await api('/orquesta/projects').catch(() => ({}))).connected) return renderInteg();
1503
+ }
1504
+ go.disabled = false;
1505
+ note.textContent = t('orqNoToken');
1506
+ });
1507
+ return proj.replaceChildren(note, el('div', { className: 'fields' }, go));
1508
+ }
1051
1509
  if (!r.projects?.length) return proj.replaceChildren(el('small', { textContent: r.error || t('orqNone') }));
1052
1510
  const sel = el('select', {}, ...r.projects.map((p) => el('option', { value: p.id, textContent: `${p.online === true ? '● ' : p.online === false ? '○ ' : ''}${p.name}${p.online == null ? '' : ` · ${t(p.online ? 'orqOnline' : 'orqOffline')}`}`, selected: p.id === oq.projectId })));
1053
1511
  sel.addEventListener('change', () => save({ integrations: { orquesta: { projectId: sel.value, projectName: sel.selectedOptions[0]?.textContent.replace(/^[●○] /, '').replace(/ · .*$/, '') } } }, msg));
@@ -1212,6 +1670,39 @@
1212
1670
  });
1213
1671
  const imp = el('button', {}, ico('upload', 15), ' ', t('importCard'));
1214
1672
  imp.addEventListener('click', () => file.click());
1673
+ // the brain drafts it (from the profile + an optional idea); it fills the fields, saving is yours
1674
+ const idea = el('input', { type: 'text', placeholder: t('chIdeaPh'), autocomplete: 'off' });
1675
+ // in the ot's language unless picked otherwise (shown, so it is never a surprise)
1676
+ const aiLang = el('select', { title: t('chAiLang') }, ...[['es', 'Español'], ['en', 'English'], ['pt', 'Português']].map(([v, n]) => el('option', { value: v, textContent: n, selected: (S.lang || 'en') === v })));
1677
+ const ai = el('button', { className: 'primary' }, ico('sparkles', 15), ' ', t('chAi'));
1678
+ const busy = el('div', { className: 'ai-busy', hidden: true }, el('span', { className: 'spin' }), el('b', {}), el('span', { className: 'muted' }));
1679
+ ai.addEventListener('click', async () => {
1680
+ ai.disabled = aiLang.disabled = idea.disabled = true;
1681
+ const box = $('#char');
1682
+ box.classList.add('ai-wait');
1683
+ busy.hidden = false;
1684
+ const t0 = Date.now();
1685
+ const tick = () => {
1686
+ busy.children[1].textContent = t('chAiWait', { lang: aiLang.selectedOptions[0].textContent });
1687
+ busy.children[2].textContent = `${Math.round((Date.now() - t0) / 1000)} s`;
1688
+ };
1689
+ tick();
1690
+ const iv = setInterval(tick, 1000);
1691
+ msg.textContent = '';
1692
+ try {
1693
+ const { sheet: d } = await api('/character/draft', { idea: idea.value.trim(), lang: aiLang.value });
1694
+ for (const [k] of CH_FIELDS) if (d[k]) ins[k].value = d[k];
1695
+ const empty = CH_FIELDS.filter(([k]) => !d[k]).map(([k]) => chLabel(k));
1696
+ flash(msg, empty.length ? t('chAiPartial', { f: empty.join(', ') }) : t('chAiDone'), empty.length ? 'err' : 'ok');
1697
+ } catch (e) {
1698
+ flash(msg, e.message === 'brain' ? t('chAiBrain') : t('err', { e: errText(e) }), 'err');
1699
+ } finally {
1700
+ clearInterval(iv);
1701
+ busy.hidden = true;
1702
+ box.classList.remove('ai-wait');
1703
+ ai.disabled = aiLang.disabled = idea.disabled = false;
1704
+ }
1705
+ });
1215
1706
  const exp = el('button', {}, ico('download', 15), ' ', t('exportCard'));
1216
1707
  exp.addEventListener('click', () => {
1217
1708
  const a = el('a', { href: URL.createObjectURL(new Blob([JSON.stringify(sheetToCard(sheet()), null, 2)], { type: 'application/json' })), download: `${(S.name || 'ot').replace(/[^\w-]+/g, '_')}.chara_card_v2.json` });
@@ -1242,18 +1733,31 @@
1242
1733
  const letta = card(ico('notebook-pen', 18), t('lettaH'), t('lettaD'), toggle(lt.on, (on) => save({ character: { letta: { on, agentId: agent.value.trim() } } }, msg), t('lettaH')),
1243
1734
  el('div', { className: 'fields' }, field(t('lettaAgent'), agent), lkey, sync));
1244
1735
  msg.id = 'charMsg';
1245
- $('#char').replaceChildren(el('div', { className: 'rows' }, ...fields), el('div', { className: 'row' }, ok, imp, exp, file, msg), el('div', { style: 'margin-top:14px' }, letta));
1736
+ $('#char').replaceChildren(el('div', { className: 'rows' }, ...fields), el('div', { className: 'row' }, idea, aiLang, ai), busy, el('div', { className: 'row' }, ok, imp, exp, file, msg), el('div', { style: 'margin-top:14px' }, letta));
1246
1737
  }
1247
1738
 
1248
1739
  // ── language of this page (and of the ot's settings) ──
1249
1740
  function renderLang() {
1250
- const sel = $('#langSel');
1251
- sel.setAttribute('aria-label', t('langLabel'));
1252
- sel.replaceChildren(...[['', t('langAuto')], ['en', 'English'], ['es', 'Español'], ['pt', 'Português']].map(([v, n]) => el('option', { value: v, textContent: n, selected: (S.configLang || '') === v })));
1253
- sel.onchange = async () => {
1254
- await save({ lang: sel.value }, $('#accMsg'));
1741
+ const opts = (cur, list) => list.map(([v, n]) => el('option', { value: v, textContent: n, selected: (cur || '') === v }));
1742
+ const NAMES = [['en', 'English'], ['es', 'Español'], ['pt', 'Português']];
1743
+ const ui = el('select', {}, ...opts(S.configLang, [['', t('langAuto')], ...NAMES]));
1744
+ ui.onchange = async () => {
1745
+ await save({ lang: ui.value }, $('#accMsg'));
1255
1746
  location.reload();
1256
1747
  };
1748
+ const ot = el('select', {}, ...opts(S.lang, NAMES));
1749
+ ot.onchange = async () => {
1750
+ ot.disabled = true;
1751
+ try {
1752
+ await api('/identity/lang', { lang: ot.value });
1753
+ flash($('#accMsg'), t('saved'));
1754
+ } catch (e) {
1755
+ flash($('#accMsg'), t('err', { e: errText(e) }), 'err');
1756
+ } finally {
1757
+ ot.disabled = false;
1758
+ }
1759
+ };
1760
+ $('#langs').replaceChildren(field(t('langLabel'), ui), field(t('otLang', { name: S.name || 'ot' }), ot));
1257
1761
  }
1258
1762
  const errText = (e) => (T[lang][`e_${e.message}`] || T.en[`e_${e.message}`] ? t(`e_${e.message}`) : e.message);
1259
1763
 
@@ -1345,6 +1849,8 @@
1345
1849
  renderVoices();
1346
1850
  renderAccess();
1347
1851
  renderPet();
1852
+ renderScenes();
1853
+ renderMods();
1348
1854
  renderChar();
1349
1855
  renderLang();
1350
1856
  renderMemTabs();