@7ots/cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -20
- package/cli/7ots.mjs +55 -5
- package/cli/lib/account.mjs +89 -0
- package/cli/lib/assistant.mjs +122 -16
- package/cli/lib/brain.mjs +41 -11
- package/cli/lib/card-shot.mjs +84 -0
- package/cli/lib/config.mjs +28 -5
- package/cli/lib/i18n.mjs +57 -0
- package/cli/lib/inferences.mjs +76 -0
- package/cli/lib/integrations.mjs +414 -0
- package/cli/lib/orquesta.mjs +53 -0
- package/cli/lib/pet-core.mjs +7 -10
- package/cli/lib/pet-server.mjs +365 -22
- package/cli/lib/screen.mjs +125 -0
- package/cli/lib/stt-local.py +14 -0
- package/cli/lib/stt.mjs +68 -0
- package/cli/lib/tts-local.mjs +110 -0
- package/cli/lib/tts-local.py +18 -0
- package/cli/lib/vm.mjs +309 -0
- package/cli/lib/watchers.mjs +151 -0
- package/cli/pet/card.html +294 -0
- package/cli/pet/electron/director.cjs +175 -0
- package/cli/pet/electron/main.cjs +354 -49
- package/cli/pet/pet.html +328 -15
- package/cli/pet/settings.html +991 -57
- package/dist/7ots.esm.js +3 -3
- package/dist/7ots.esm.js.map +3 -3
- package/dist/7ots.iife.js +3 -3
- package/dist/7ots.iife.js.map +3 -3
- package/llms.txt +31 -17
- package/package.json +1 -1
- package/server/llm.mjs +2 -0
- package/server/platform/connect.mjs +3 -1
- package/server/platform/db.mjs +77 -0
- package/server/platform/device.mjs +125 -0
- package/server/platform/orquesta.mjs +5 -0
- package/server/platform/routes.mjs +100 -1
- package/server/platform/sites.mjs +146 -0
- package/server/platform/store.mjs +3 -1
- package/server/platform/vm.mjs +386 -0
- package/src/character3d/index.js +2 -1
- package/src/character3d/stage.js +15 -4
- package/src/i18n/messages/character.js +3 -3
- package/src/i18n/messages/platform.js +42 -3
package/cli/pet/settings.html
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
7
|
<title>7ots settings</title>
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
10
|
+
/* Same look as the pet page (/pet) and 7ots.com: soft lilac background, white cards, violet accent. */
|
|
9
11
|
--ink: #17141f; --ink-2: #4a4458; --muted: #8a8399; --bg: #f3f1f8; --panel: #ffffff; --panel-2: #f8f7fb;
|
|
10
|
-
--line: #e8e4f0; --accent: #6d5dfc; --accent-ink: #fff; --ok: #12a150; --warn: #e08a00; --err: #e5484d;
|
|
12
|
+
--line: #e8e4f0; --accent: #6d5dfc; --accent-soft: color-mix(in srgb, var(--accent) 10%, var(--panel)); --accent-ink: #fff; --ok: #12a150; --warn: #e08a00; --err: #e5484d;
|
|
11
13
|
--shadow: 0 1px 2px rgb(20 16 40 / .06), 0 12px 40px -12px rgb(20 16 40 / .18);
|
|
12
14
|
--font: ui-sans-serif, -apple-system, 'Segoe UI', Inter, Roboto, system-ui, sans-serif;
|
|
15
|
+
--display: var(--font);
|
|
13
16
|
--mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
|
|
14
17
|
color-scheme: light;
|
|
15
18
|
}
|
|
@@ -18,22 +21,51 @@
|
|
|
18
21
|
}
|
|
19
22
|
* { box-sizing: border-box; }
|
|
20
23
|
html, body { margin: 0; font: 14px/1.5 var(--font); color: var(--ink); background: var(--bg); -webkit-font-smoothing: antialiased; }
|
|
21
|
-
body { padding:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
24
|
+
body { padding: 0; min-height: 100vh; }
|
|
25
|
+
.shell { display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: 24px; width: min(1080px, 100%); margin: 0 auto; padding: 24px 16px; min-height: 100vh; align-items: start; }
|
|
26
|
+
aside { position: sticky; top: 24px; padding: 0 0 12px; display: flex; flex-direction: column; gap: 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 24px; box-shadow: var(--shadow); overflow: hidden; }
|
|
27
|
+
aside nav { padding: 0 10px; }
|
|
28
|
+
.me { display: grid; justify-items: center; text-align: center; gap: 2px; padding: 26px 14px 16px; border-bottom: 1px solid var(--line); background: radial-gradient(120% 90% at 50% 30%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%); }
|
|
29
|
+
.me .av { width: 56px; height: 56px; margin-bottom: 8px; border-radius: 18px; box-shadow: var(--shadow); background: var(--accent); color: var(--accent-ink); display: grid; place-items: center; font: 800 18px/1 var(--display); }
|
|
30
|
+
.me .av.drawn { width: 96px; height: 100px; background: none; box-shadow: none; border-radius: 0; margin-bottom: 4px; }
|
|
31
|
+
@media (max-width: 760px) { .me .av.drawn { width: 36px; height: 38px; } }
|
|
32
|
+
.me b { font: 750 18px/1.15 var(--display); letter-spacing: -.025em; }
|
|
33
|
+
.me small { color: var(--muted); font-size: 12px; }
|
|
34
|
+
nav { display: grid; gap: 2px; }
|
|
35
|
+
nav a { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; color: var(--ink-2); font-weight: 500; text-decoration: none !important; }
|
|
36
|
+
nav a span { width: 20px; display: grid; place-items: center; opacity: .8; }
|
|
37
|
+
nav a[aria-current=page] span { opacity: 1; }
|
|
38
|
+
.ico { flex: none; vertical-align: -3px; }
|
|
39
|
+
.logo-claude { color: #d97757; } .logo-anthropic { color: var(--ink); } .logo-googlegemini { color: #4285f4; } .logo-ollama { color: var(--ink); }
|
|
40
|
+
.opt > label b, .chip { display: inline-flex; align-items: center; gap: 7px; }
|
|
41
|
+
nav a:hover { background: var(--panel-2); }
|
|
42
|
+
nav a[aria-current=page] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
43
|
+
aside .back { margin-top: 4px; font-size: 12.5px; padding: 10px 20px 0; border-top: 1px solid var(--line); }
|
|
44
|
+
main { width: 100%; padding: 6px 0 60px; display: grid; gap: 16px; align-content: start; }
|
|
45
|
+
.pageH { font: 750 26px/1.15 var(--display); letter-spacing: -.025em; margin: 0; }
|
|
46
|
+
.pageD { margin: 4px 0 6px; color: var(--muted); }
|
|
47
|
+
main > section[hidden] { display: none; }
|
|
48
|
+
main > section > h2:first-child { display: none; } /* the page title says it */
|
|
49
|
+
@media (max-width: 760px) {
|
|
50
|
+
.shell { grid-template-columns: 1fr; gap: 14px; padding: 12px; }
|
|
51
|
+
aside { top: 8px; z-index: 2; padding: 8px 10px; flex-direction: row; align-items: center; gap: 10px; border-radius: 18px; overflow-x: auto; }
|
|
52
|
+
aside nav { padding: 0; }
|
|
53
|
+
.me { padding: 0; background: none; border: 0; } .me small, .me b { display: none; } .me .av { width: 32px; height: 32px; margin: 0; border-radius: 10px; font-size: 15px; box-shadow: none; }
|
|
54
|
+
nav { display: flex; gap: 4px; } nav a { white-space: nowrap; padding: 6px 10px; } nav a b { display: none; }
|
|
55
|
+
aside .back { display: none; }
|
|
56
|
+
main { padding: 4px 4px 50px; }
|
|
57
|
+
}
|
|
26
58
|
.sp { flex: 1; }
|
|
27
59
|
a { color: var(--accent); text-decoration: none; }
|
|
28
60
|
a:hover { text-decoration: underline; }
|
|
29
|
-
section { background: var(--panel); border: 1px solid var(--line); border-radius:
|
|
30
|
-
h2 { margin: 0 0 2px; font
|
|
61
|
+
section { background: var(--panel); border: 1px solid var(--line); border-radius: 24px; box-shadow: var(--shadow); padding: 20px 22px; }
|
|
62
|
+
h2 { margin: 0 0 2px; font: 750 17px/1.3 var(--display); letter-spacing: -.02em; }
|
|
31
63
|
.hint { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
|
|
32
64
|
.now { display: inline-flex; gap: 6px; align-items: center; padding: 3px 10px; border-radius: 99px; background: var(--panel-2); border: 1px solid var(--line); font-size: 12px; color: var(--ink-2); }
|
|
33
65
|
.now b { color: var(--ink); }
|
|
34
66
|
|
|
35
67
|
.opts { display: grid; gap: 8px; }
|
|
36
|
-
.opt { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start; padding:
|
|
68
|
+
.opt { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start; padding: 12px 13px; border: 1px solid var(--line); border-radius: 14px; cursor: pointer; background: var(--panel-2); }
|
|
37
69
|
.opt:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
|
|
38
70
|
.opt.off { opacity: .5; cursor: not-allowed; }
|
|
39
71
|
.opt input { margin: 3px 0 0; accent-color: var(--accent); }
|
|
@@ -44,19 +76,24 @@
|
|
|
44
76
|
.more .chips { align-self: center; }
|
|
45
77
|
.opt:has(input:checked) .more { display: flex; }
|
|
46
78
|
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
79
|
+
.total { margin: 0 0 10px; color: var(--ink-2); font: 600 13px var(--mono); }
|
|
47
80
|
.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; }
|
|
48
81
|
.chip:has(input:checked) { border-color: var(--accent); color: var(--accent); font-weight: 600; }
|
|
49
82
|
.chip input { display: none; }
|
|
50
83
|
.chip.off { opacity: .45; cursor: not-allowed; }
|
|
51
84
|
label.f { display: grid; gap: 3px; font-size: 12px; color: var(--muted); flex: 1 1 180px; }
|
|
52
|
-
input[type=text], input[type=password], select { font: 13px var(--font); color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius:
|
|
85
|
+
input[type=text], input[type=password], input[type=search], select { font: 13px var(--font); color: var(--ink); background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px; padding: 0 12px; height: 36px; width: 100%; outline: none; }
|
|
86
|
+
input[type=text]:focus, input[type=password]:focus, input[type=search]:focus, select:focus { border-color: var(--accent); }
|
|
53
87
|
input:focus-visible, select:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
54
88
|
|
|
55
89
|
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
|
|
56
|
-
button { font:
|
|
90
|
+
button { font: 650 13px var(--font); color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0 14px; min-height: 36px; cursor: pointer; transition: background .15s, border-color .15s, transform .1s; }
|
|
91
|
+
button:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
|
|
92
|
+
button:active { transform: scale(.97); }
|
|
93
|
+
button.primary:hover { background: color-mix(in srgb, var(--accent) 88%, #000); }
|
|
57
94
|
button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
|
|
58
95
|
button:disabled { opacity: .55; cursor: wait; }
|
|
59
|
-
button.x { padding:
|
|
96
|
+
button.x { min-height: 26px; padding: 0 9px; border-radius: 9px; font-weight: 500; color: var(--muted); }
|
|
60
97
|
.msg { font-size: 13px; color: var(--muted); }
|
|
61
98
|
.msg.ok { color: var(--ok); } .msg.err { color: var(--err); }
|
|
62
99
|
.reply { margin-top: 10px; padding: 10px 12px; border-radius: 12px; background: var(--panel-2); border: 1px solid var(--line); font-size: 13.5px; display: none; }
|
|
@@ -84,17 +121,109 @@
|
|
|
84
121
|
ul.list li span { flex: 1; overflow-wrap: anywhere; }
|
|
85
122
|
ul.list time { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
86
123
|
.empty { color: var(--muted); font-size: 13px; }
|
|
124
|
+
.rows { display: grid; gap: 6px; }
|
|
125
|
+
.accHero { display: grid; grid-template-columns: auto 1fr; gap: 22px; align-items: center; padding: 22px; border: 1px solid var(--line); border-radius: 20px; background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%), var(--panel-2); }
|
|
126
|
+
.accHero .mark { width: 76px; height: 76px; border-radius: 22px; overflow: hidden; box-shadow: 0 10px 30px -10px color-mix(in srgb, var(--accent) 70%, transparent); }
|
|
127
|
+
.accHero .mark img { width: 100%; height: 100%; border-radius: 0 !important; }
|
|
128
|
+
.accHero h3 { margin: 0 0 4px; font: 750 19px/1.2 var(--display); letter-spacing: -.02em; }
|
|
129
|
+
.accHero p { margin: 0 0 14px; color: var(--ink-2); font-size: 13.5px; }
|
|
130
|
+
.perks { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 16px; padding: 0; list-style: none; }
|
|
131
|
+
.perks li { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); font-size: 12.5px; color: var(--ink-2); }
|
|
132
|
+
.accHero button.primary { min-height: 42px; padding: 0 20px; border-radius: 14px; font-size: 14px; display: inline-flex; align-items: center; gap: 8px; }
|
|
133
|
+
.wait { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-2); }
|
|
134
|
+
.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
|
+
@keyframes spin { to { transform: rotate(1turn); } }
|
|
136
|
+
.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
|
+
.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
|
+
.who div { flex: 1; min-width: 0; display: grid; line-height: 1.25; }
|
|
139
|
+
.who small { color: var(--muted); font-size: 11.5px; }
|
|
140
|
+
.who b { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
141
|
+
.otgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 12px; }
|
|
142
|
+
.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
|
+
.ot:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
|
|
144
|
+
.ot.cur { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
145
|
+
.ot .stage { height: 132px; margin-bottom: 8px; background: radial-gradient(80% 90% at 50% 75%, color-mix(in srgb, var(--c, var(--accent)) 30%, transparent), transparent 70%), color-mix(in srgb, var(--c, var(--accent)) 8%, var(--panel-2)); display: grid; place-items: center; }
|
|
146
|
+
.ot .stage > div { width: 112px; height: 120px; }
|
|
147
|
+
.ot .stage .init { font: 800 34px/1 var(--display); color: var(--c, var(--accent)); }
|
|
148
|
+
.ot b, .ot small { padding: 0 12px; }
|
|
149
|
+
.ot b { font: 700 15px/1.2 var(--display); }
|
|
150
|
+
.ot small { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
|
+
.ot .cardlink { position: absolute; right: 8px; top: 8px; display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 999px; font-size: 11px; font-weight: 600; text-decoration: none; color: var(--ink); background: color-mix(in srgb, var(--panel) 80%, transparent); border: 1px solid var(--line); }
|
|
152
|
+
.ot .visit { margin-top: 6px; font-size: 12px; }
|
|
153
|
+
.ot .cardlink:hover { border-color: var(--c, var(--accent)); }
|
|
154
|
+
.ot button { margin: auto 12px 0; } .ot small { margin-bottom: 10px; }
|
|
155
|
+
.ot .tag { position: absolute; top: 8px; left: 8px; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: var(--accent); color: var(--accent-ink); font-size: 11px; font-weight: 650; }
|
|
156
|
+
.ot.new { justify-content: center; align-items: center; text-align: center; padding: 16px; border-style: dashed; color: var(--muted); text-decoration: none !important; min-height: 220px; }
|
|
157
|
+
.ot.new .ico { width: 28px; height: 28px; }
|
|
158
|
+
.badge { font-size: 12px; font-weight: 650; color: var(--accent); text-align: center; padding: 6px; }
|
|
159
|
+
.logo-orquesta { display: inline-block; flex: none; vertical-align: -3px; background: currentColor; -webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABdCAQAAADwdavAAAAHsElEQVR42tWba5SNVRjHf+fMzVyYOTOMWxljYtRCtSYiWa0i8oGI0VCJopY0TaVaErJWF4010WJUylq0EJXbNCVLF0UShZLGrVzG3UgYjJkzvH1w5jhnv9f9ntfK++5v5+yz938/e7/P83/+zz5RuPNpwedksptKl+JnFLX4Wc+T7oTfjM0oKChs87oQvodcOgBQy1w32v86fgvYv4wsN9p/LBcDC5iMC09QG3YH4B/lFvfBj6YoAF9hDjHuW0Bnjgbgn+Ye98FP5NOg/UtIcN8CBnAuAL+KB9wHP50fg/ZfTYrb4Ht5mdoA/BqGuc/+HTkYtP9PpLkNfn2WBOH73UfhPDxJdXABG2nstgW0C0ZfV9o/kY+D8BU2ku624zOCqhD7P+E2+7fnrxD7r6ORu+A3CCEPCtUMddvxeZaakAV867b4e0dI8FI4T67buM83IfAVSqnvJvixFAZTRwWFU/R0l/3zOBNm/7nEuct57gyDf5iOboKfRkkYfIVCotx0+t8MMv/Lbbub9B8PDwun308+nmsVbpQKWhf2CcdnDQ2vTeiZDKKtsIAM1grwz9L/2jvjmTzER2zndUEaSWa+AF9hAfHXCvBo0unKcyzmb/wozBGYTSyv4RfgHyTn/7d3I9rRl3Es4ndOB6Eto4mgOoykUoB/kVfN5FuP4z4kmjgSSCaNJmSQSSsyaEIKsWH9VjGS8rBPejFXWBL8Qj8Om22r/SeGBJJIJoVUUvHhI5kUfKSSSgr1idcVX9eRL8DPYZoK/nmKzODLL8BLMk3JJIssWtCUVBqQQBwxEnu5mVHsCvukNcXcqOq3lFInj0YanXmKOWzkaIjEId82c5swdjO+0Oj3F+2cgt6cfszgV/6NAPYV+KJX8TFPo181o5x4P5PoznT+jMjixvATeUfgPXXieXLkjPARVgqsJLK2SQU/jlc1jXOQzpFafjA/OGb3OkH2VpUDeT6o94eTtxciKd556MTiECnJmfY9N6n82QhOafZdSWokMl4+5Q6DV1ih4vNeBnNCs+9R7rQPvzGzHD44CheZT3MV/DyOafauZYL93KsFyxy3fTXTVAfCQ64OfIWv7JcuGrPUcfinGauqJXrI5bBO/wP2vU8is7jkMPwDDFexIS8Pckh3t2ynjh5Gc8Fh+Fs0CtFehugeHoWF9pW3m1V5aKQv7jKV24RoHue47m/KaGsvS4I4Csiw/IsaKjjEAQ5wjFP4icVDUoD/N6URyfiZQxH/qKLuaCbqEoQzTGSHXft31vHIYrvAJoq4nzakaNLwGFLI4BbaC8nL5dj+mmbUrduxyfYvbkQxzQL4KkrJtV2dasjMMJ1f7TwjqLu0YJsp/DKGRCBtZ7LY0MPtEu79NJfTIQZy3gT+zxEpAx1ZYxIr8oSqQDHXyUww1QT+36oMSsY99xVUZjXznBR2+qOYxCGyZSb5znCCWsbYhp9AvoHXr/P94X6pJxVUysXjvYYT7KGVTfjNec80OG4QWGoGG1CokRMTjXOuEg2XaM01rzZ1DfsE4pzAh4FvCmSmMn6F37YBPp4RFiL7v+SFMR8vBcEdmyrDic4ZTjNJGn4Ws039mkI1Y4Vg2J0jwW+Xy4S1k4YTTZYCH8dAtlgIi5eYRWLYL7ODt6Evp/8SSaVxGFsgkR9lMVMlz2q35cKNHx+Lw74/okEFdZ8lhlNtV6WDejrGcP6wyFXXCb4nljcEbaiGvtYX8GJYOVkdaEZbYLTdWGI5n9ghFEw9DNPYtwnWX+MuBhz9ciS+3VDsbU8xFZYzhUP0FkboHnYf4sq1Astl7SRWmOZW3XRKFzlMZb9EolMhuE5oz1Yds7W0fohGaqqToa2ccbQNcW1eGtOf+cF7zFZT/JGC4na9cJ0j1M1afAs8QDO+tHCRvZyN/EkFl0ghm1vJlry7cI6JzMAf8kkqxQzW7T+FsShWF1FgugeRtirGC6QknmmG7mM9PuvWacL6qwq/mjeFJCWGl0z01zPcJbPBAyyGIDuthkIh7np5LKReqdfekGFE9Zh11eBPEeBDfxPXXSfI+2T2oFUYG3Gq+ZlCkjBTD5Mc5Molg3vleGQfE2Jn5+wXqcSAO9lu+ffFcoWOaCY56o2qeUt1eLpQJjHCbtl80Mdyx+Bf0ICfY4lshwpeBUg+N1nmlGbnd7xKWM+x8Zatkb/q6sSbcJKnVbl0azbZGOkcfcyERfHZQyzdIvqL3wleYja1qpmy6SBdPoohmlLVWKZvwmcRWH8/uTrLT+QZTepstps2in7ZOiTXvJXRw5B1dWWVIQPSah/YUa772noTfrQgQzZivOQ+HKOTncLHK6rrX2Yu7xMyLZfSF3JWYuz37exBMgul6PJUqQJpPP351rBmENqO08WON8my7PiOkU896fFTeZQ1Fgvr82yMD/S2lK5v5T7bbjeNQZRaoNan6WWv/DTGxEIXKYn4ZlUid1PMThMetoIG9gZ/16A8VEmhQ/9q9JLBUBawW9dgFxhub+h0FuksYRdDbYrv+r4vg34U8jV7NQTibVp32K0kbQ0pYJhQtzpOCdPZxtV5EkjnBrJpzfWk4SOBOKJQmE4Rl+QXAFHkkEd3WlKPKvawliX8TA1X/4mhHonEE48XD5WUiwv4D2U5zUDaj6UBAAAAAElFTkSuQmCC) center/contain no-repeat; mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABdCAQAAADwdavAAAAHsElEQVR42tWba5SNVRjHf+fMzVyYOTOMWxljYtRCtSYiWa0i8oGI0VCJopY0TaVaErJWF4010WJUylq0EJXbNCVLF0UShZLGrVzG3UgYjJkzvH1w5jhnv9f9ntfK++5v5+yz938/e7/P83/+zz5RuPNpwedksptKl+JnFLX4Wc+T7oTfjM0oKChs87oQvodcOgBQy1w32v86fgvYv4wsN9p/LBcDC5iMC09QG3YH4B/lFvfBj6YoAF9hDjHuW0Bnjgbgn+Ye98FP5NOg/UtIcN8CBnAuAL+KB9wHP50fg/ZfTYrb4Ht5mdoA/BqGuc/+HTkYtP9PpLkNfn2WBOH73UfhPDxJdXABG2nstgW0C0ZfV9o/kY+D8BU2ku624zOCqhD7P+E2+7fnrxD7r6ORu+A3CCEPCtUMddvxeZaakAV867b4e0dI8FI4T67buM83IfAVSqnvJvixFAZTRwWFU/R0l/3zOBNm/7nEuct57gyDf5iOboKfRkkYfIVCotx0+t8MMv/Lbbub9B8PDwun308+nmsVbpQKWhf2CcdnDQ2vTeiZDKKtsIAM1grwz9L/2jvjmTzER2zndUEaSWa+AF9hAfHXCvBo0unKcyzmb/wozBGYTSyv4RfgHyTn/7d3I9rRl3Es4ndOB6Eto4mgOoykUoB/kVfN5FuP4z4kmjgSSCaNJmSQSSsyaEIKsWH9VjGS8rBPejFXWBL8Qj8Om22r/SeGBJJIJoVUUvHhI5kUfKSSSgr1idcVX9eRL8DPYZoK/nmKzODLL8BLMk3JJIssWtCUVBqQQBwxEnu5mVHsCvukNcXcqOq3lFInj0YanXmKOWzkaIjEId82c5swdjO+0Oj3F+2cgt6cfszgV/6NAPYV+KJX8TFPo181o5x4P5PoznT+jMjixvATeUfgPXXieXLkjPARVgqsJLK2SQU/jlc1jXOQzpFafjA/OGb3OkH2VpUDeT6o94eTtxciKd556MTiECnJmfY9N6n82QhOafZdSWokMl4+5Q6DV1ih4vNeBnNCs+9R7rQPvzGzHD44CheZT3MV/DyOafauZYL93KsFyxy3fTXTVAfCQ64OfIWv7JcuGrPUcfinGauqJXrI5bBO/wP2vU8is7jkMPwDDFexIS8Pckh3t2ynjh5Gc8Fh+Fs0CtFehugeHoWF9pW3m1V5aKQv7jKV24RoHue47m/KaGsvS4I4Csiw/IsaKjjEAQ5wjFP4icVDUoD/N6URyfiZQxH/qKLuaCbqEoQzTGSHXft31vHIYrvAJoq4nzakaNLwGFLI4BbaC8nL5dj+mmbUrduxyfYvbkQxzQL4KkrJtV2dasjMMJ1f7TwjqLu0YJsp/DKGRCBtZ7LY0MPtEu79NJfTIQZy3gT+zxEpAx1ZYxIr8oSqQDHXyUww1QT+36oMSsY99xVUZjXznBR2+qOYxCGyZSb5znCCWsbYhp9AvoHXr/P94X6pJxVUysXjvYYT7KGVTfjNec80OG4QWGoGG1CokRMTjXOuEg2XaM01rzZ1DfsE4pzAh4FvCmSmMn6F37YBPp4RFiL7v+SFMR8vBcEdmyrDic4ZTjNJGn4Ws039mkI1Y4Vg2J0jwW+Xy4S1k4YTTZYCH8dAtlgIi5eYRWLYL7ODt6Evp/8SSaVxGFsgkR9lMVMlz2q35cKNHx+Lw74/okEFdZ8lhlNtV6WDejrGcP6wyFXXCb4nljcEbaiGvtYX8GJYOVkdaEZbYLTdWGI5n9ghFEw9DNPYtwnWX+MuBhz9ciS+3VDsbU8xFZYzhUP0FkboHnYf4sq1Astl7SRWmOZW3XRKFzlMZb9EolMhuE5oz1Yds7W0fohGaqqToa2ccbQNcW1eGtOf+cF7zFZT/JGC4na9cJ0j1M1afAs8QDO+tHCRvZyN/EkFl0ghm1vJlry7cI6JzMAf8kkqxQzW7T+FsShWF1FgugeRtirGC6QknmmG7mM9PuvWacL6qwq/mjeFJCWGl0z01zPcJbPBAyyGIDuthkIh7np5LKReqdfekGFE9Zh11eBPEeBDfxPXXSfI+2T2oFUYG3Gq+ZlCkjBTD5Mc5Molg3vleGQfE2Jn5+wXqcSAO9lu+ffFcoWOaCY56o2qeUt1eLpQJjHCbtl80Mdyx+Bf0ICfY4lshwpeBUg+N1nmlGbnd7xKWM+x8Zatkb/q6sSbcJKnVbl0azbZGOkcfcyERfHZQyzdIvqL3wleYja1qpmy6SBdPoohmlLVWKZvwmcRWH8/uTrLT+QZTepstps2in7ZOiTXvJXRw5B1dWWVIQPSah/YUa772noTfrQgQzZivOQ+HKOTncLHK6rrX2Yu7xMyLZfSF3JWYuz37exBMgul6PJUqQJpPP351rBmENqO08WON8my7PiOkU896fFTeZQ1Fgvr82yMD/S2lK5v5T7bbjeNQZRaoNan6WWv/DTGxEIXKYn4ZlUid1PMThMetoIG9gZ/16A8VEmhQ/9q9JLBUBawW9dgFxhub+h0FuksYRdDbYrv+r4vg34U8jV7NQTibVp32K0kbQ0pYJhQtzpOCdPZxtV5EkjnBrJpzfWk4SOBOKJQmE4Rl+QXAFHkkEd3WlKPKvawliX8TA1X/4mhHonEE48XD5WUiwv4D2U5zUDaj6UBAAAAAElFTkSuQmCC) center/contain no-repeat; }
|
|
160
|
+
details.r { background: var(--panel-2); border: 1px solid var(--line); border-radius: 14px; }
|
|
161
|
+
details.r > summary { list-style: none; cursor: pointer; display: grid; grid-template-columns: auto 1fr auto; gap: 2px 10px; padding: 9px 12px; align-items: center; }
|
|
162
|
+
details.r > summary::-webkit-details-marker { display: none; }
|
|
163
|
+
details.r > summary::before { content: '›'; color: var(--muted); transition: transform .15s; grid-row: 1 / span 2; }
|
|
164
|
+
details.r[open] > summary::before { transform: rotate(90deg); }
|
|
165
|
+
details.r .sum { font-size: 13.5px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere; }
|
|
166
|
+
details.r .meta { grid-column: 2 / span 2; color: var(--muted); font: 11.5px var(--mono); }
|
|
167
|
+
details.r time { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
168
|
+
details.r .body { border-top: 1px solid var(--line); padding: 10px 12px; display: grid; gap: 8px; }
|
|
169
|
+
details.r .body b { font-size: 11.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
|
170
|
+
pre { margin: 0; font: 12px/1.45 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 260px; overflow: auto; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; color: var(--ink-2); }
|
|
171
|
+
.bad { color: var(--err); }
|
|
172
|
+
.tabs { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
173
|
+
.sub { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; grid-column: 1 / span 2; margin-top: 6px; font-size: 12.5px; color: var(--muted); }
|
|
174
|
+
.sub select { width: auto; height: 30px; padding: 0 8px; }
|
|
175
|
+
.sub button { min-height: 30px; padding: 0 10px; font-size: 12.5px; }
|
|
176
|
+
.sh { display: flex; align-items: center; gap: 8px; margin: 18px 0 6px; font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
|
177
|
+
.sh:first-of-type { margin-top: 4px; }
|
|
178
|
+
.sh input { width: 150px; height: 28px; font-size: 12px; letter-spacing: 0; text-transform: none; }
|
|
179
|
+
ul.list li .kind { flex: none; display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; background: var(--panel-2); border: 1px solid var(--line); font-size: 13px; flex: none; }
|
|
180
|
+
ul.list li small { color: var(--muted); font-size: 12px; }
|
|
181
|
+
table.ps { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
182
|
+
table.ps th { text-align: left; font-weight: 600; color: var(--muted); font-size: 11.5px; padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
183
|
+
table.ps td { padding: 6px 8px; border-bottom: 1px dashed var(--line); vertical-align: top; }
|
|
184
|
+
table.ps td.n { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
|
185
|
+
table.ps .cmd { font: 11.5px var(--mono); color: var(--muted); overflow-wrap: anywhere; max-width: 520px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
|
186
|
+
table.ps tr:hover .cmd { -webkit-line-clamp: 6; }
|
|
187
|
+
table.ps tr.hot td.n:first-of-type { color: var(--warn); font-weight: 650; }
|
|
188
|
+
.bar { display: inline-block; width: 46px; height: 4px; border-radius: 9px; background: var(--line); vertical-align: middle; margin-left: 6px; overflow: hidden; }
|
|
189
|
+
.bar i { display: block; height: 100%; background: var(--accent); }
|
|
190
|
+
textarea { font: 13px/1.45 var(--font); color: var(--ink); background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px; padding: 8px 12px; width: 100%; min-height: 40px; resize: vertical; outline: none; }
|
|
191
|
+
textarea:focus { border-color: var(--accent); }
|
|
192
|
+
.more [hidden], .integ [hidden] { display: none !important; }
|
|
193
|
+
.integ { display: grid; gap: 8px; padding: 12px 13px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel-2); margin-bottom: 8px; }
|
|
194
|
+
.integ > header { display: flex; align-items: center; gap: 10px; }
|
|
195
|
+
.integ > header b { flex: 1; display: inline-flex; align-items: center; gap: 8px; font-weight: 650; }
|
|
196
|
+
.integ .tog { grid-row: auto; grid-column: auto; flex: none; }
|
|
197
|
+
.integ small { color: var(--muted); font-size: 12.5px; }
|
|
198
|
+
.integ details > summary { cursor: pointer; color: var(--accent); font-size: 12.5px; }
|
|
199
|
+
.integ details .rows { margin-top: 8px; }
|
|
200
|
+
.integ .vm { display: grid; gap: 2px; padding-top: 8px; border-top: 1px solid var(--line); }
|
|
201
|
+
.sw .kinds { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; font-size: 12px; }
|
|
202
|
+
.integ .vm b { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
|
|
203
|
+
.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; } }
|
|
87
206
|
footer { color: var(--muted); font-size: 12px; text-align: center; }
|
|
88
207
|
footer code { font: 11.5px var(--mono); }
|
|
89
208
|
</style>
|
|
90
209
|
</head>
|
|
91
210
|
<body>
|
|
211
|
+
<div class="shell">
|
|
212
|
+
<aside>
|
|
213
|
+
<div class="me"><span class="av" id="av">7</span><b id="title"></b><small id="petLine"></small></div>
|
|
214
|
+
<nav id="nav" aria-label="Settings"></nav>
|
|
215
|
+
<label class="lang"><select id="langSel"></select></label>
|
|
216
|
+
<a id="back" class="back"></a>
|
|
217
|
+
</aside>
|
|
92
218
|
<main>
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
219
|
+
<div><h1 class="pageH" id="pageH"></h1><p class="pageD" id="pageD"></p></div>
|
|
220
|
+
|
|
221
|
+
<section aria-labelledby="hAcc">
|
|
222
|
+
<h2 id="hAcc" data-t="accH"></h2>
|
|
223
|
+
<p class="hint" data-t="accHint"></p>
|
|
224
|
+
<div id="acc"></div>
|
|
225
|
+
<div class="row"><span class="msg" id="accMsg" role="status"></span></div>
|
|
226
|
+
</section>
|
|
98
227
|
|
|
99
228
|
<section aria-labelledby="hBrain">
|
|
100
229
|
<h2 id="hBrain" data-t="brainH"></h2>
|
|
@@ -109,10 +238,24 @@
|
|
|
109
238
|
<div class="reply" id="reply"></div>
|
|
110
239
|
</section>
|
|
111
240
|
|
|
241
|
+
<section aria-labelledby="hVoice">
|
|
242
|
+
<h2 id="hVoice" data-t="voiceH"></h2>
|
|
243
|
+
<p class="hint" data-t="voiceHint"></p>
|
|
244
|
+
<div class="opts" id="voices"></div>
|
|
245
|
+
<div class="row">
|
|
246
|
+
<button class="primary" id="saveVoice" data-t="save"></button>
|
|
247
|
+
<span class="now" id="stt"></span>
|
|
248
|
+
<span class="msg" id="voiceMsg" role="status"></span>
|
|
249
|
+
</div>
|
|
250
|
+
</section>
|
|
251
|
+
|
|
112
252
|
<section aria-labelledby="hAccess">
|
|
113
253
|
<h2 id="hAccess" data-t="accessH"></h2>
|
|
114
254
|
<p class="hint" data-t="accessHint"></p>
|
|
115
255
|
<div id="access"></div>
|
|
256
|
+
<h3 class="sh" data-t="integH"></h3>
|
|
257
|
+
<p class="hint" data-t="integHint"></p>
|
|
258
|
+
<div id="integ"></div>
|
|
116
259
|
</section>
|
|
117
260
|
|
|
118
261
|
<section aria-labelledby="hPet">
|
|
@@ -127,28 +270,62 @@
|
|
|
127
270
|
<div class="chips" id="view"></div>
|
|
128
271
|
<span class="msg" id="petMsg" role="status"></span>
|
|
129
272
|
</div>
|
|
273
|
+
<h3 class="sh" data-t="charH"></h3>
|
|
274
|
+
<p class="hint" data-t="charHint"></p>
|
|
275
|
+
<div id="char"></div>
|
|
130
276
|
</section>
|
|
131
277
|
|
|
132
278
|
<section aria-labelledby="hNotes">
|
|
133
279
|
<h2 id="hNotes" data-t="notesH"></h2>
|
|
134
280
|
<p class="hint" data-t="notesHint"></p>
|
|
135
|
-
<
|
|
136
|
-
<div
|
|
281
|
+
<div class="tabs chips" id="memTabs"></div>
|
|
282
|
+
<div id="memNotes">
|
|
283
|
+
<ul class="list" id="notes"></ul>
|
|
284
|
+
<div class="row"><button id="forgetAll" data-t="forgetAll"></button></div>
|
|
285
|
+
</div>
|
|
286
|
+
<div id="memSeen" hidden>
|
|
287
|
+
<div class="rows" id="seen"></div>
|
|
288
|
+
<div class="row"><button id="forgetSeen" data-t="forgetSeen"></button></div>
|
|
289
|
+
</div>
|
|
290
|
+
<div id="memSaid" hidden><ul class="list" id="said"></ul></div>
|
|
291
|
+
</section>
|
|
292
|
+
|
|
293
|
+
<section aria-labelledby="hInf">
|
|
294
|
+
<h2 id="hInf" data-t="infH"></h2>
|
|
295
|
+
<p class="hint" data-t="infHint"></p>
|
|
296
|
+
<p class="total" id="infTotal"></p>
|
|
297
|
+
<div class="rows" id="infs"></div>
|
|
298
|
+
<div class="row"><button id="clearInf" data-t="clearInf"></button><span class="msg" id="infMsg" role="status"></span></div>
|
|
137
299
|
</section>
|
|
138
300
|
|
|
139
301
|
<section aria-labelledby="hRem">
|
|
140
302
|
<h2 id="hRem" data-t="remH"></h2>
|
|
141
303
|
<p class="hint" data-t="remHint"></p>
|
|
304
|
+
<h3 class="sh" data-t="petRuns"></h3>
|
|
142
305
|
<ul class="list" id="rems"></ul>
|
|
306
|
+
<div id="ps"></div>
|
|
143
307
|
</section>
|
|
144
308
|
|
|
145
309
|
<footer id="foot"></footer>
|
|
146
310
|
</main>
|
|
311
|
+
</div>
|
|
147
312
|
|
|
148
313
|
<script>
|
|
149
314
|
const BOOT = __BOOT__;
|
|
150
315
|
const T = {
|
|
151
316
|
en: {
|
|
317
|
+
accH: 'Your 7ots account', accHint: 'Connect it to pick which of your ots lives on this computer, and bring its look, voice and personality from 7ots.com.',
|
|
318
|
+
connect: 'Connect with 7ots.com', connecting: 'Waiting for you in the browser…', openAgain: 'Didn’t open? Open the page', signedAs: 'Connected as {who}', logout: 'Disconnect', noOts: 'No ots in your account yet.', newOt: 'Create one on 7ots.com',
|
|
319
|
+
useThis: 'Use this one', current: 'On this computer', switching: 'Switching…', resync: 'Sync again', restarting: 'Restarting with {name}…', heroH: 'Bring your ots home', heroP: 'Sign in once and choose which of your ots lives on this computer.', perkLook: 'Look', perkVoice: 'Voice', perkMind: 'Personality', heroNote: 'Opens 7ots.com in your browser: press Connect there and come back.', cancel: 'Cancel', connectedAs: 'Connected as', yourOts: 'Your ots', slow: 'Taking a while… still restarting.', card: 'Card', invite: 'Also on this computer', dismiss: 'Turn off', visiting: 'Also active', maxOts: 'Two at a time for now',
|
|
320
|
+
voiceH: 'Voice', voiceHint: 'How it speaks out loud, and how it hears you (🎙️ in its menu or Ctrl+Alt+Space).',
|
|
321
|
+
vnone: 'Silent', vnoneD: 'Only speech bubbles.', vbrowser: 'System voice', vbrowserD: 'The browser’s speech synthesis. Often has no voices on Linux.',
|
|
322
|
+
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
|
+
velevenlabs: 'ElevenLabs', velevenlabsD: 'Natural cloud voices, with your key.', vopenai: 'OpenAI', vopenaiD: 'OpenAI text-to-speech, with your key.',
|
|
324
|
+
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.',
|
|
326
|
+
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',
|
|
328
|
+
fSystem: 'System prompt', fInput: 'What it sent', fFiles: 'Files', fOutput: 'Answer', fError: 'Error',
|
|
152
329
|
title: '{name} · settings', back: '← Back to the pet', save: 'Save', saved: 'Saved ✓', test: 'Try it', testing: 'Thinking…', now: 'Now:',
|
|
153
330
|
brainH: 'Brain', brainHint: 'Who thinks when it talks to you. Your own AI CLI is free with your subscription and runs on this computer.',
|
|
154
331
|
lines: 'No AI', linesD: 'Built-in phrases. Instant, but it can’t understand free text (reminders still work).',
|
|
@@ -162,14 +339,26 @@
|
|
|
162
339
|
memory: 'Learn about you', memoryD: 'Keeps notes (“remember that…”) and uses them when answering.',
|
|
163
340
|
open: 'Open sites', openD: 'Opens your mail, calendar… when you ask, or when a reminder fires.',
|
|
164
341
|
notify: 'Notifications', notifyD: 'A desktop notification when a reminder fires.',
|
|
165
|
-
soon: 'coming soon', screen: 'See your screen', screenD: 'Look at what’s open to help with it.', procs: 'Processes and apps', procsD: '
|
|
342
|
+
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.',
|
|
166
343
|
petH: 'How chatty', annoyHint: 'How often it comments on what you do.', a0: 'Quiet', a1: 'A little', a2: 'Normal', a3: 'Chatty',
|
|
167
|
-
view: 'Look:', v2d: '2D', v3d: '3D plush', viewNext: '
|
|
344
|
+
view: 'Look:', v2d: '2D', v3d: '3D plush', viewNext: 'Changed — the ot switches right away.',
|
|
168
345
|
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',
|
|
169
|
-
remH: '
|
|
346
|
+
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',
|
|
170
347
|
foot: 'Open this page with <code>7ots config</code>, or ⚙️ in the pet’s menu.', err: 'Couldn’t save: {e}',
|
|
171
348
|
},
|
|
172
349
|
es: {
|
|
350
|
+
accH: 'Tu cuenta 7ots', accHint: 'Conéctala para elegir cuál de tus ots vive en este computador, y traer su aspecto, voz y personalidad desde 7ots.com.',
|
|
351
|
+
connect: 'Conectar con 7ots.com', connecting: 'Esperándote en el navegador…', openAgain: '¿No se abrió? Abrir la página', signedAs: 'Conectado como {who}', logout: 'Desconectar', noOts: 'Todavía no hay ots en tu cuenta.', newOt: 'Crea uno en 7ots.com',
|
|
352
|
+
useThis: 'Usar este', current: 'En este computador', switching: 'Cambiando…', resync: 'Sincronizar', restarting: 'Reiniciando con {name}…', heroH: 'Trae a tus ots a casa', heroP: 'Inicia sesión una vez y elige cuál de tus ots vive en este computador.', perkLook: 'Aspecto', perkVoice: 'Voz', perkMind: 'Personalidad', heroNote: 'Abre 7ots.com en tu navegador: aprieta Conectar ahí y vuelve.', cancel: 'Cancelar', connectedAs: 'Conectado como', yourOts: 'Tus ots', slow: 'Está tardando… sigue reiniciando.', card: 'Tarjeta', invite: 'Activar también aquí', dismiss: 'Desactivar', visiting: 'También activo', maxOts: 'Por ahora, dos a la vez',
|
|
353
|
+
voiceH: 'Voz', voiceHint: 'Cómo habla en voz alta, y cómo te escucha (🎙️ en su menú o Ctrl+Alt+Espacio).',
|
|
354
|
+
vnone: 'Mudo', vnoneD: 'Solo globos de texto.', vbrowser: 'Voz del sistema', vbrowserD: 'La síntesis del navegador. En Linux suele no tener voces.',
|
|
355
|
+
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
|
+
velevenlabs: 'ElevenLabs', velevenlabsD: 'Voces naturales en la nube, con tu key.', vopenai: 'OpenAI', vopenaiD: 'Texto a voz de OpenAI, con tu key.',
|
|
357
|
+
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.',
|
|
359
|
+
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',
|
|
361
|
+
fSystem: 'System prompt', fInput: 'Lo que mandó', fFiles: 'Archivos', fOutput: 'Respuesta', fError: 'Error',
|
|
173
362
|
title: '{name} · ajustes', back: '← Volver a la mascota', save: 'Guardar', saved: 'Guardado ✓', test: 'Probar', testing: 'Pensando…', now: 'Ahora:',
|
|
174
363
|
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.',
|
|
175
364
|
lines: 'Sin IA', linesD: 'Frases incluidas. Instantáneo, pero no entiende texto libre (los recordatorios igual funcionan).',
|
|
@@ -183,14 +372,26 @@
|
|
|
183
372
|
memory: 'Aprender de ti', memoryD: 'Guarda notas (“anota que…”) y las usa al responder.',
|
|
184
373
|
open: 'Abrir sitios', openD: 'Abre tu correo, calendario… cuando se lo pides o cuando suena un recordatorio.',
|
|
185
374
|
notify: 'Notificaciones', notifyD: 'Una notificación de escritorio cuando suena un recordatorio.',
|
|
186
|
-
soon: '
|
|
375
|
+
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.',
|
|
187
376
|
petH: 'Cuánto habla', annoyHint: 'Qué tan seguido comenta lo que haces.', a0: 'Callado', a1: 'Poco', a2: 'Normal', a3: 'Charlatán',
|
|
188
|
-
view: 'Aspecto:', v2d: '2D', v3d: 'Peluche 3D', viewNext: '
|
|
377
|
+
view: 'Aspecto:', v2d: '2D', v3d: 'Peluche 3D', viewNext: 'Listo: el ot cambia al instante.',
|
|
189
378
|
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',
|
|
190
|
-
remH: '
|
|
379
|
+
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',
|
|
191
380
|
foot: 'Abre esta página con <code>7ots config</code>, o ⚙️ en el menú de la mascota.', err: 'No se pudo guardar: {e}',
|
|
192
381
|
},
|
|
193
382
|
pt: {
|
|
383
|
+
accH: 'Sua conta 7ots', accHint: 'Conecte para escolher qual dos seus ots vive neste computador, e trazer visual, voz e personalidade do 7ots.com.',
|
|
384
|
+
connect: 'Conectar com 7ots.com', connecting: 'Esperando você no navegador…', openAgain: 'Não abriu? Abrir a página', signedAs: 'Conectado como {who}', logout: 'Desconectar', noOts: 'Ainda não há ots na sua conta.', newOt: 'Crie um no 7ots.com',
|
|
385
|
+
useThis: 'Usar este', current: 'Neste computador', switching: 'Trocando…', resync: 'Sincronizar', restarting: 'Reiniciando com {name}…', heroH: 'Traga seus ots para casa', heroP: 'Entre uma vez e escolha qual dos seus ots vive neste computador.', perkLook: 'Visual', perkVoice: 'Voz', perkMind: 'Personalidade', heroNote: 'Abre o 7ots.com no navegador: aperte Conectar lá e volte.', cancel: 'Cancelar', connectedAs: 'Conectado como', yourOts: 'Seus ots', slow: 'Está demorando… ainda reiniciando.', card: 'Carta', invite: 'Ativar também aqui', dismiss: 'Desativar', visiting: 'Também ativo', maxOts: 'Por enquanto, dois de cada vez',
|
|
386
|
+
voiceH: 'Voz', voiceHint: 'Como ele fala em voz alta, e como te ouve (🎙️ no menu ou Ctrl+Alt+Espaço).',
|
|
387
|
+
vnone: 'Mudo', vnoneD: 'Só balões de texto.', vbrowser: 'Voz do sistema', vbrowserD: 'A síntese do navegador. No Linux costuma não ter vozes.',
|
|
388
|
+
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
|
+
velevenlabs: 'ElevenLabs', velevenlabsD: 'Vozes naturais na nuvem, com sua key.', vopenai: 'OpenAI', vopenaiD: 'Texto para voz da OpenAI, com sua key.',
|
|
390
|
+
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.',
|
|
392
|
+
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',
|
|
394
|
+
fSystem: 'System prompt', fInput: 'O que mandou', fFiles: 'Arquivos', fOutput: 'Resposta', fError: 'Erro',
|
|
194
395
|
title: '{name} · ajustes', back: '← Voltar ao bichinho', save: 'Salvar', saved: 'Salvo ✓', test: 'Testar', testing: 'Pensando…', now: 'Agora:',
|
|
195
396
|
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.',
|
|
196
397
|
lines: 'Sem IA', linesD: 'Frases embutidas. Instantâneo, mas não entende texto livre (lembretes funcionam).',
|
|
@@ -204,17 +405,78 @@
|
|
|
204
405
|
memory: 'Aprender sobre você', memoryD: 'Guarda notas (“anote que…”) e usa ao responder.',
|
|
205
406
|
open: 'Abrir sites', openD: 'Abre seu e-mail, agenda… quando você pede ou quando um lembrete toca.',
|
|
206
407
|
notify: 'Notificações', notifyD: 'Uma notificação na área de trabalho quando um lembrete toca.',
|
|
207
|
-
soon: 'em breve', screen: 'Ver sua tela', screenD: 'Olhar o que está aberto para ajudar.', procs: 'Processos e apps', procsD: '
|
|
408
|
+
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.',
|
|
208
409
|
petH: 'Quanto fala', annoyHint: 'Com que frequência comenta o que você faz.', a0: 'Calado', a1: 'Pouco', a2: 'Normal', a3: 'Tagarela',
|
|
209
|
-
view: 'Visual:', v2d: '2D', v3d: 'Pelúcia 3D', viewNext: '
|
|
410
|
+
view: 'Visual:', v2d: '2D', v3d: 'Pelúcia 3D', viewNext: 'Pronto: o ot muda na hora.',
|
|
210
411
|
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',
|
|
211
|
-
remH: '
|
|
412
|
+
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',
|
|
212
413
|
foot: 'Abra esta página com <code>7ots config</code>, ou ⚙️ no menu do bichinho.', err: 'Não deu para salvar: {e}',
|
|
213
414
|
},
|
|
214
415
|
};
|
|
215
|
-
//
|
|
416
|
+
// Strings for integrations, the character sheet and the newer providers (merged into T above).
|
|
417
|
+
const T2 = {
|
|
418
|
+
en: {
|
|
419
|
+
lv: 'Lv', oaiCompat: 'OpenAI-compatible', langLabel: 'Language', langAuto: 'Automatic (system)',
|
|
420
|
+
apiD: 'Anthropic, any OpenAI-compatible endpoint, or Batuta (Orquesta’s model router), with your key.',
|
|
421
|
+
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
|
+
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.',
|
|
423
|
+
inbox: 'Read your mail', inboxD: 'Read and summarize the inbox connected below (apumail) when you ask.',
|
|
424
|
+
integH: 'Integrations', integHint: 'Other places it can reach. What you turn on here is listed to its brain as a tool.',
|
|
425
|
+
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
|
+
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
|
+
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',
|
|
429
|
+
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
|
+
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
|
+
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
|
+
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).',
|
|
434
|
+
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
|
+
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
|
+
},
|
|
437
|
+
es: {
|
|
438
|
+
lv: 'Nv', oaiCompat: 'Compatible con OpenAI', langLabel: 'Idioma', langAuto: 'Automático (sistema)',
|
|
439
|
+
apiD: 'Anthropic, cualquier endpoint compatible con OpenAI, o Batuta (el router de modelos de Orquesta), con tu key.',
|
|
440
|
+
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
|
+
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.',
|
|
442
|
+
inbox: 'Leer tu correo', inboxD: 'Leer y resumir la bandeja conectada abajo (apumail) cuando se lo pides.',
|
|
443
|
+
integH: 'Integraciones', integHint: 'Otros lugares a los que puede llegar. Lo que actives acá se le presenta a su cerebro como herramienta.',
|
|
444
|
+
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
|
+
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
|
+
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',
|
|
448
|
+
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
|
+
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
|
+
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
|
+
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).',
|
|
453
|
+
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
|
+
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
|
+
},
|
|
456
|
+
pt: {
|
|
457
|
+
lv: 'Nv', oaiCompat: 'Compatível com OpenAI', langLabel: 'Idioma', langAuto: 'Automático (sistema)',
|
|
458
|
+
apiD: 'Anthropic, qualquer endpoint compatível com OpenAI, ou Batuta (o roteador de modelos da Orquesta), com sua key.',
|
|
459
|
+
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
|
+
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.',
|
|
461
|
+
inbox: 'Ler seu e-mail', inboxD: 'Ler e resumir a caixa conectada abaixo (apumail) quando você pede.',
|
|
462
|
+
integH: 'Integrações', integHint: 'Outros lugares que ele pode alcançar. O que você ativar aqui aparece para o cérebro como ferramenta.',
|
|
463
|
+
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
|
+
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
|
+
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',
|
|
467
|
+
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
|
+
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
|
+
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
|
+
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).',
|
|
472
|
+
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
|
+
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
|
+
},
|
|
475
|
+
};
|
|
476
|
+
for (const k of Object.keys(T2)) Object.assign(T[k], T2[k]);
|
|
477
|
+
// The system's language (LANG, or the one picked below) first, then the browser's, then the ot's.
|
|
216
478
|
const nav = (navigator.language || '').slice(0, 2);
|
|
217
|
-
const lang = T[nav] ? nav : T[BOOT.lang] ? BOOT.lang : 'en';
|
|
479
|
+
const lang = T[BOOT.uiLang] ? BOOT.uiLang : T[nav] ? nav : T[BOOT.lang] ? BOOT.lang : 'en';
|
|
218
480
|
document.documentElement.lang = lang;
|
|
219
481
|
const t = (k, v = {}) => (T[lang][k] ?? T.en[k] ?? k).replace(/\{(\w+)\}/g, (_, n) => v[n] ?? '');
|
|
220
482
|
const $ = (s) => document.querySelector(s);
|
|
@@ -239,13 +501,204 @@
|
|
|
239
501
|
for (const n of document.querySelectorAll('[data-t]')) n.textContent = t(n.dataset.t);
|
|
240
502
|
$('#back').textContent = t('back');
|
|
241
503
|
$('#back').href = `/pet?token=${encodeURIComponent(BOOT.token)}`;
|
|
504
|
+
// The ot itself (the same drawing as the pet page) instead of an initial, when the bundle loads.
|
|
505
|
+
import('/dist/7ots.esm.js')
|
|
506
|
+
.then(({ createCharacter }) => {
|
|
507
|
+
const av = $('#av');
|
|
508
|
+
av.textContent = '';
|
|
509
|
+
av.classList.add('drawn');
|
|
510
|
+
createCharacter(av, BOOT.identity?.look?.character || {}, { locale: lang, title: BOOT.identity?.name });
|
|
511
|
+
})
|
|
512
|
+
.catch(() => {});
|
|
242
513
|
$('#foot').innerHTML = t('foot'); // own strings only
|
|
243
514
|
$('#ticks').append(...[0, 1, 2, 3].map((i) => el('span', { textContent: t(`a${i}`) })));
|
|
244
515
|
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
// ── 7ots account ──
|
|
519
|
+
let accPoll = null;
|
|
520
|
+
let charLib = null;
|
|
521
|
+
const loadChars = () => (charLib ||= import('/dist/7ots.esm.js').catch(() => null));
|
|
522
|
+
async function loadAccount() {
|
|
523
|
+
let A;
|
|
524
|
+
try {
|
|
525
|
+
A = await api('/account');
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return flash($('#accMsg'), t('err', { e: e.message }), 'err');
|
|
528
|
+
}
|
|
529
|
+
const box = $('#acc');
|
|
530
|
+
$('#hAcc').nextElementSibling.hidden = !A.signedIn;
|
|
531
|
+
if (!A.signedIn) {
|
|
532
|
+
const b = el('button', { className: 'primary' }, t('connect'), ico('arrow-right', 16));
|
|
533
|
+
const body = el('div', {}, el('h3', { textContent: t('heroH') }), el('p', { textContent: t('heroP') }),
|
|
534
|
+
el('ul', { className: 'perks' }, el('li', {}, ico('palette', 14), t('perkLook')), el('li', {}, ico('mic', 14), t('perkVoice')), el('li', {}, ico('sparkles', 14), t('perkMind'))),
|
|
535
|
+
b, el('p', { className: 'hint', style: 'margin:10px 0 0', textContent: t('heroNote') }));
|
|
536
|
+
b.addEventListener('click', async () => {
|
|
537
|
+
b.disabled = true;
|
|
538
|
+
try {
|
|
539
|
+
const { url } = await api('/account/login', {});
|
|
540
|
+
const cancel = el('button', { className: 'x', textContent: t('cancel') });
|
|
541
|
+
cancel.addEventListener('click', () => (clearInterval(accPoll), loadAccount()));
|
|
542
|
+
b.replaceWith(el('div', { className: 'wait' }, el('span', { className: 'spin' }), t('connecting'), el('a', { href: url, target: '_blank', rel: 'noopener', textContent: t('openAgain') }), cancel));
|
|
543
|
+
clearInterval(accPoll);
|
|
544
|
+
let n = 0;
|
|
545
|
+
accPoll = setInterval(async () => {
|
|
546
|
+
const r = await api('/account').catch(() => null);
|
|
547
|
+
if (r?.signedIn || ++n > 300) clearInterval(accPoll), loadAccount();
|
|
548
|
+
}, 2000);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
b.disabled = false;
|
|
551
|
+
flash($('#accMsg'), t('err', { e: e.message }), 'err');
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
return box.replaceChildren(el('div', { className: 'accHero' }, el('div', { className: 'mark' }, logo('7ots', 76)), body));
|
|
555
|
+
}
|
|
556
|
+
const out = el('button', { className: 'x', textContent: t('logout') });
|
|
557
|
+
out.addEventListener('click', async () => (await api('/account/logout', {}), loadAccount()));
|
|
558
|
+
const email = A.account?.email || A.account?.name || '';
|
|
559
|
+
const who = el('div', { className: 'who' }, el('span', { className: 'av', textContent: (email || '?')[0].toUpperCase() }), el('div', {}, el('small', { textContent: t('connectedAs') }), el('b', { textContent: email })), out);
|
|
560
|
+
if (A.error) who.append(el('span', { className: 'msg err', textContent: A.error }));
|
|
561
|
+
const ots = A.ots || [];
|
|
562
|
+
const add = el('a', { className: 'ot new', href: `${A.url}/app/`, target: '_blank', rel: 'noopener' }, ico('plus', 28), el('span', { textContent: t('newOt') }));
|
|
563
|
+
if (!ots.length) return box.replaceChildren(who, el('p', { className: 'empty', textContent: t('noOts') }), el('div', { className: 'otgrid' }, add));
|
|
564
|
+
const curId = A.current?.otsId;
|
|
565
|
+
const visiting = new Set(((await api('/visitors').catch(() => [])) || []).map((v) => v.id));
|
|
566
|
+
const stages = [];
|
|
567
|
+
const cards = ots.map((o) => {
|
|
568
|
+
const name = o.identity?.name || o.name;
|
|
569
|
+
const look = o.identity?.look || {};
|
|
570
|
+
const cur = o.id === curId;
|
|
571
|
+
const b = el('button', { className: cur ? '' : 'primary', textContent: cur ? t('resync') : t('useThis') });
|
|
572
|
+
b.addEventListener('click', () => useOt(o.id, name, b));
|
|
573
|
+
const fig = el('div', {}, el('span', { className: 'init', textContent: (name || '?')[0].toUpperCase() }));
|
|
574
|
+
stages.push([fig, look.character, name]);
|
|
575
|
+
const card = el('div', { className: `ot${cur ? ' cur' : ''}` }, el('div', { className: 'stage' }, fig), el('b', { textContent: name }), el('small', { textContent: o.identity?.role || '' }), b);
|
|
576
|
+
if (look.color) card.style.setProperty('--c', look.color);
|
|
577
|
+
if (cur) card.append(el('span', { className: 'tag' }, ico('monitor', 12), t('current')));
|
|
578
|
+
else if (!BOOT.companion) {
|
|
579
|
+
// a second ot with everything (its own brain, voice, memory and settings) beside the one that lives here
|
|
580
|
+
const on = visiting.has(o.id);
|
|
581
|
+
if (on) card.append(el('span', { className: 'tag' }, ico('sparkles', 12), t('visiting')));
|
|
582
|
+
const v = el('button', { className: 'x visit', textContent: on ? t('dismiss') : t('invite') });
|
|
583
|
+
if (!on && visiting.size >= 1) (v.disabled = true), (v.title = t('maxOts'));
|
|
584
|
+
v.addEventListener('click', async () => {
|
|
585
|
+
v.disabled = true;
|
|
586
|
+
try {
|
|
587
|
+
await api('/account/visitor', { id: o.id, on: !on });
|
|
588
|
+
loadAccount();
|
|
589
|
+
} catch (e) {
|
|
590
|
+
v.disabled = false;
|
|
591
|
+
flash($('#accMsg'), t('err', { e: e.message }), 'err');
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
card.append(v);
|
|
595
|
+
}
|
|
596
|
+
// its trading card (holo foil, PNG in high resolution for an NFT)
|
|
597
|
+
card.append(el('a', { className: 'cardlink', href: `/card?id=${encodeURIComponent(o.id)}&token=${encodeURIComponent(BOOT.token)}`, target: '_blank', rel: 'noopener', title: t('card'), 'aria-label': t('card') }, ico('sparkles', 14)));
|
|
598
|
+
return card;
|
|
599
|
+
});
|
|
600
|
+
box.replaceChildren(who, el('div', { className: 'otgrid' }, ...cards, add));
|
|
601
|
+
// Draw each ot as it looks on 7ots.com (the initial stays if the library can't load).
|
|
602
|
+
const lib = await loadChars();
|
|
603
|
+
if (!lib?.createCharacter) return;
|
|
604
|
+
for (const [fig, spec, name] of stages) {
|
|
605
|
+
if (!spec) continue;
|
|
606
|
+
try {
|
|
607
|
+
fig.replaceChildren();
|
|
608
|
+
lib.createCharacter(fig, spec, { locale: lang, title: name, moodFx: false });
|
|
609
|
+
} catch {}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function useOt(id, name, b) {
|
|
613
|
+
b.disabled = true;
|
|
614
|
+
b.textContent = t('switching');
|
|
615
|
+
try {
|
|
616
|
+
await api('/account/use', { id });
|
|
617
|
+
flash($('#accMsg'), t('restarting', { name }));
|
|
618
|
+
// The pet restarts with the new identity: wait for it (it can take ~40 s) and reload this page.
|
|
619
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
620
|
+
for (let i = 0; i < 120; i++) {
|
|
621
|
+
const h = await fetch('/health', { cache: 'no-store' }).then((r) => (r.ok ? r.json() : null), () => null);
|
|
622
|
+
if (h && h.pid !== BOOT.pid) return location.reload(); // the new daemon, with the new ot
|
|
623
|
+
if (i === 30) flash($('#accMsg'), t('slow'));
|
|
624
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
625
|
+
}
|
|
626
|
+
} catch (e) {
|
|
627
|
+
b.disabled = false;
|
|
628
|
+
b.textContent = t('useThis');
|
|
629
|
+
flash($('#accMsg'), t('err', { e: e.message }), 'err');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── navigation: one section at a time, remembered in the URL hash ──
|
|
634
|
+
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' },
|
|
638
|
+
};
|
|
639
|
+
// moods and inference purposes arrive as English codes from the daemon
|
|
640
|
+
const WORDS = {
|
|
641
|
+
es: { happy: 'feliz', sad: 'triste', neutral: 'tranquilo', disgust: 'aburrido', sleep: 'durmiendo', hello: 'saludo', played: 'jugó', fed: 'comió', slept: 'durmió', stop: 'terminó', tool: 'herramienta', idle: 'ocio', bored: 'aburrido', hungry: 'hambre', screen: 'pantalla', assistant: 'asistente', test: 'prueba' },
|
|
642
|
+
pt: { happy: 'feliz', sad: 'triste', neutral: 'tranquilo', disgust: 'entediado', sleep: 'dormindo', hello: 'oi', played: 'brincou', fed: 'comeu', slept: 'dormiu', stop: 'terminou', tool: 'ferramenta', idle: 'ócio', bored: 'entediado', hungry: 'fome', screen: 'tela', assistant: 'assistente', test: 'teste' },
|
|
643
|
+
};
|
|
644
|
+
const word = (w) => WORDS[lang]?.[w] || w;
|
|
645
|
+
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\"/>"};
|
|
647
|
+
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
|
+
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
|
+
// Full-colour marks (their own squares): apuchat's rat, apumail. (Orquesta: the masked mark below.)
|
|
650
|
+
const BR = {"apuchat": {"vb": 32, "svg": "<rect width=\"32\" height=\"32\" rx=\"6\" fill=\"#1a1a1a\"/><path d=\"M9 7Q16 4 23 7\" stroke=\"#d6541f\" stroke-width=\"1.5\" fill=\"none\" stroke-linecap=\"round\"/><path d=\"M7 10Q16 5 25 10\" stroke=\"#d6541f\" stroke-width=\"1.5\" fill=\"none\" stroke-linecap=\"round\" opacity=\".5\"/><ellipse cx=\"11\" cy=\"14\" rx=\"2\" ry=\"3\" fill=\"#f4ede0\" transform=\"rotate(-15 11 14)\"/><ellipse cx=\"21\" cy=\"14\" rx=\"2\" ry=\"3\" fill=\"#f4ede0\" transform=\"rotate(15 21 14)\"/><ellipse cx=\"16\" cy=\"22\" rx=\"8\" ry=\"6.5\" fill=\"#f4ede0\"/><circle cx=\"13\" cy=\"21\" r=\"1.2\" fill=\"#1a1a1a\"/><circle cx=\"19\" cy=\"21\" r=\"1.2\" fill=\"#1a1a1a\"/><ellipse cx=\"16\" cy=\"25\" rx=\"1.5\" ry=\"1\" fill=\"#d6541f\"/>"}, "apumail": {"vb": 32, "svg": "<rect width=\"32\" height=\"32\" rx=\"6\" fill=\"#1d1813\"/><path fill=\"#e8521e\" d=\"M3 3h26v26H3Z\"/><g transform=\"translate(-.43 1.03) scale(.915)\"><g fill=\"#1d1813\"><path d=\"M9.6 30C10.5 23.6 11.6 18.6 12.9 15.2 13.8 12.7 15.4 11.3 17.6 10.9L19.2 10.7C22.2 10.3 24.8 11.1 26 12.5 26.9 13.5 26.6 14.9 25.3 15.5 24.2 16 22.6 16.3 21 16.3L19.6 16.3C17.7 18.3 16.9 20.8 16.7 23.6L16.4 30Z\"/><path d=\"M17 11.1C15.7 7.5 16 4.3 17.2 2.7 18.4 4.2 19 7.5 19.1 10.8Z\"/><path d=\"M19.8 10.7C19.6 7.3 20.6 4.3 21.9 3.2 22.7 5 22.7 7.9 22.1 10.8Z\"/></g><circle cx=\"21.9\" cy=\"13.3\" r=\"1.55\" fill=\"#e8521e\"/></g>"}};
|
|
651
|
+
// Line icons (Lucide) for the UI, brand marks (Simple Icons) for the providers.
|
|
652
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
653
|
+
const ico = (name, size = 18) => {
|
|
654
|
+
const s = document.createElementNS(svgNS, 'svg');
|
|
655
|
+
for (const [k, v] of Object.entries({ viewBox: '0 0 24 24', width: size, height: size, fill: 'none', stroke: 'currentColor', 'stroke-width': 1.8, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true' })) s.setAttribute(k, v);
|
|
656
|
+
s.innerHTML = LU[name] || '';
|
|
657
|
+
s.classList.add('ico');
|
|
658
|
+
return s;
|
|
659
|
+
};
|
|
660
|
+
const logo = (name, size = 16) => {
|
|
661
|
+
if (name === '7ots') return el('img', { className: 'ico', src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CjxzdHlsZT4uYmd7ZmlsbDojNWI0N2YwfS5mZ3tmaWxsOiNmZmZ9LmRvdHtmaWxsOiMyMmQzZWV9QG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTpkYXJrKXsuYmd7ZmlsbDojOGI3Y2ZmfS5mZ3tmaWxsOiMwYjBhMTR9fTwvc3R5bGU+CjxwYXRoIGNsYXNzPSJiZyIgZD0iTTY0IDMyTDYzLjk4IDQxLjgzTDYzLjkzIDQ0Ljk3TDYzLjg0IDQ3LjI0TDYzLjcyIDQ5LjA3TDYzLjU2IDUwLjY0TDYzLjM2IDUyLjAxTDYzLjEzIDUzLjIzTDYyLjg2IDU0LjMzTDYyLjU2IDU1LjMzTDYyLjIxIDU2LjI1TDYxLjgzIDU3LjFMNjEuNCA1Ny44N0w2MC45MyA1OC41OUw2MC40MiA1OS4yNUw1OS44NiA1OS44Nkw1OS4yNSA2MC40Mkw1OC41OSA2MC45M0w1Ny44NyA2MS40TDU3LjEgNjEuODNMNTYuMjUgNjIuMjFMNTUuMzMgNjIuNTZMNTQuMzMgNjIuODZMNTMuMjMgNjMuMTNMNTIuMDEgNjMuMzZMNTAuNjQgNjMuNTZMNDkuMDcgNjMuNzJMNDcuMjQgNjMuODRMNDQuOTcgNjMuOTNMNDEuODMgNjMuOThMMzIgNjRMMjIuMTcgNjMuOThMMTkuMDMgNjMuOTNMMTYuNzYgNjMuODRMMTQuOTMgNjMuNzJMMTMuMzYgNjMuNTZMMTEuOTkgNjMuMzZMMTAuNzcgNjMuMTNMOS42NyA2Mi44Nkw4LjY3IDYyLjU2TDcuNzUgNjIuMjFMNi45IDYxLjgzTDYuMTMgNjEuNEw1LjQxIDYwLjkzTDQuNzUgNjAuNDJMNC4xNCA1OS44NkwzLjU4IDU5LjI1TDMuMDcgNTguNTlMMi42IDU3Ljg3TDIuMTcgNTcuMUwxLjc5IDU2LjI1TDEuNDQgNTUuMzNMMS4xNCA1NC4zM0wwLjg3IDUzLjIzTDAuNjQgNTIuMDFMMC40NCA1MC42NEwwLjI4IDQ5LjA3TDAuMTYgNDcuMjRMMC4wNyA0NC45N0wwLjAyIDQxLjgzTDAgMzJMMC4wMiAyMi4xN0wwLjA3IDE5LjAzTDAuMTYgMTYuNzZMMC4yOCAxNC45M0wwLjQ0IDEzLjM2TDAuNjQgMTEuOTlMMC44NyAxMC43N0wxLjE0IDkuNjdMMS40NCA4LjY3TDEuNzkgNy43NUwyLjE3IDYuOUwyLjYgNi4xM0wzLjA3IDUuNDFMMy41OCA0Ljc1TDQuMTQgNC4xNEw0Ljc1IDMuNThMNS40MSAzLjA3TDYuMTMgMi42TDYuOSAyLjE3TDcuNzUgMS43OUw4LjY3IDEuNDRMOS42NyAxLjE0TDEwLjc3IDAuODdMMTEuOTkgMC42NEwxMy4zNiAwLjQ0TDE0LjkzIDAuMjhMMTYuNzYgMC4xNkwxOS4wMyAwLjA3TDIyLjE3IDAuMDJMMzIgMEw0MS44MyAwLjAyTDQ0Ljk3IDAuMDdMNDcuMjQgMC4xNkw0OS4wNyAwLjI4TDUwLjY0IDAuNDRMNTIuMDEgMC42NEw1My4yMyAwLjg3TDU0LjMzIDEuMTRMNTUuMzMgMS40NEw1Ni4yNSAxLjc5TDU3LjEgMi4xN0w1Ny44NyAyLjZMNTguNTkgMy4wN0w1OS4yNSAzLjU4TDU5Ljg2IDQuMTRMNjAuNDIgNC43NUw2MC45MyA1LjQxTDYxLjQgNi4xM0w2MS44MyA2LjlMNjIuMjEgNy43NUw2Mi41NiA4LjY3TDYyLjg2IDkuNjdMNjMuMTMgMTAuNzdMNjMuMzYgMTEuOTlMNjMuNTYgMTMuMzZMNjMuNzIgMTQuOTNMNjMuODQgMTYuNzZMNjMuOTMgMTkuMDNMNjMuOTggMjIuMTdaIi8+PHBhdGggY2xhc3M9ImZnIiBkPSJNMjEuMTYgNDlRMjEuMTYgNDUuNCAyMiA0MS44UTIyLjg1IDM4LjIgMjQuNDUgMzQuNzVRMjYuMDQgMzEuMzEgMjguMzMgMjguMTJRMzAuNjIgMjQuOTMgMzMuNTUgMjIuMVYyMS44OUgxNi43OFYxNUg0MS4yMlYyMS44OVEzOS4zMSAyMy45IDM3LjM4IDI2Ljg4UTM1LjQ2IDI5Ljg3IDMzLjg2IDMzLjQ3UTMyLjI3IDM3LjA3IDMxLjMxIDQxLjAzUTMwLjM2IDQ0Ljk5IDMwLjM2IDQ5WiIvPjxjaXJjbGUgY3g9IjQ0LjUiIGN5PSI0NC40IiByPSI0LjciIGNsYXNzPSJkb3QiLz4KPC9zdmc+Cg==', width: size, height: size, alt: '', style: 'border-radius:28%' });
|
|
662
|
+
if (BR[name]) {
|
|
663
|
+
const s = document.createElementNS(svgNS, 'svg');
|
|
664
|
+
for (const [k, v] of Object.entries({ viewBox: `0 0 ${BR[name].vb} ${BR[name].vb}`, width: size, height: size, 'aria-hidden': 'true' })) s.setAttribute(k, v);
|
|
665
|
+
s.innerHTML = BR[name].svg;
|
|
666
|
+
s.classList.add('ico', `logo-${name}`);
|
|
667
|
+
return s;
|
|
668
|
+
}
|
|
669
|
+
if (name === 'orquesta') return el('span', { className: 'ico logo-orquesta', style: `width:${size}px;height:${size}px`, ariaHidden: 'true' });
|
|
670
|
+
if (name === 'codex') name = 'openai';
|
|
671
|
+
if (!SI[name]) return ico('house', size);
|
|
672
|
+
const s = document.createElementNS(svgNS, 'svg');
|
|
673
|
+
for (const [k, v] of Object.entries({ viewBox: '0 0 24 24', width: size, height: size, fill: 'currentColor', 'aria-hidden': 'true' })) s.setAttribute(k, v);
|
|
674
|
+
const p = document.createElementNS(svgNS, 'path');
|
|
675
|
+
p.setAttribute('d', SI[name]);
|
|
676
|
+
s.append(p);
|
|
677
|
+
s.classList.add('ico', `logo-${name}`);
|
|
678
|
+
return s;
|
|
679
|
+
};
|
|
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' };
|
|
682
|
+
const navT = NAV[lang] || NAV.en;
|
|
683
|
+
$('#nav').replaceChildren(...Object.keys(SECS).map((k) => el('a', { href: `#${k}` }, el('span', {}, ico(ICON[k])), el('b', { textContent: navT[k] }))));
|
|
684
|
+
function go() {
|
|
685
|
+
const k = SECS[location.hash.slice(1)] ? location.hash.slice(1) : 'brain';
|
|
686
|
+
for (const [key, h] of Object.entries(SECS)) document.querySelector(`section[aria-labelledby=${h}]`).hidden = key !== k;
|
|
687
|
+
for (const a of document.querySelectorAll('#nav a')) a.getAttribute('href') === `#${k}` ? a.setAttribute('aria-current', 'page') : a.removeAttribute('aria-current');
|
|
688
|
+
$('#pageH').textContent = $(`#${SECS[k]}`).textContent;
|
|
689
|
+
$('#pageD').textContent = '';
|
|
690
|
+
if (k === 'inf') loadInferences();
|
|
691
|
+
if (k === 'acc') loadAccount();
|
|
692
|
+
if (k === 'rem') loadProcesses();
|
|
693
|
+
if (k === 'notes') loadMemory();
|
|
694
|
+
window.scrollTo(0, 0);
|
|
695
|
+
}
|
|
696
|
+
addEventListener('hashchange', go);
|
|
697
|
+
|
|
245
698
|
let S = null;
|
|
246
699
|
|
|
247
700
|
// ── brain ──
|
|
248
|
-
const CLI_MODELS = { claude: ['claude-haiku-4-5-20251001', 'claude-sonnet-5-5', 'claude-opus-5-5'], ollama: ['llama3.2', 'qwen2.5', 'gemma3'] };
|
|
701
|
+
const CLI_MODELS = { claude: ['claude-haiku-4-5-20251001', 'claude-sonnet-5-5', 'claude-opus-5-5'], ollama: ['llama3.2', 'qwen2.5', 'gemma3'], orquesta: ['batuta'] };
|
|
249
702
|
function keyField(name, label) {
|
|
250
703
|
const has = S.keys[name];
|
|
251
704
|
return el('label', { className: 'f' }, `${label} · ${has ? '✓' : t('keyNone')}`, el('input', { type: 'password', name, placeholder: has ? t('keySaved') : '', autocomplete: 'off' }));
|
|
@@ -254,11 +707,13 @@
|
|
|
254
707
|
const b = S.config.brain;
|
|
255
708
|
const box = $('#brains');
|
|
256
709
|
box.replaceChildren();
|
|
710
|
+
const CLI_LOGO = { claude: 'claude', codex: 'openai', gemini: 'googlegemini', ollama: 'ollama', orquesta: 'orquesta' };
|
|
711
|
+
const KIND_LOGOS = { lines: [ico('message-circle', 16)], cli: [logo('claude'), logo('openai'), logo('googlegemini'), logo('ollama'), logo('orquesta')], api: [logo('anthropic'), logo('openai'), logo('orquesta')], '7ots': [logo('7ots')] };
|
|
257
712
|
const opt = (kind, title, desc, more, off = '') => {
|
|
258
|
-
const radio = el('input', { type: 'radio', name: 'kind', value: kind, checked: b.kind === kind, disabled: !!off });
|
|
713
|
+
const radio = el('input', { type: 'radio', name: 'kind', value: kind, checked: (b.kind === 'batuta' ? 'api' : b.kind) === kind, disabled: !!off }); // Batuta is a provider under “Your API key”
|
|
259
714
|
radio.id = `k-${kind}`;
|
|
260
715
|
// A div, not a label: the extra fields inside have labels of their own (no nested labels).
|
|
261
|
-
const card = el('div', { className: `opt${off ? ' off' : ''}` }, radio, el('label', { htmlFor: radio.id }, el('b', {
|
|
716
|
+
const card = el('div', { className: `opt${off ? ' off' : ''}` }, radio, el('label', { htmlFor: radio.id }, el('b', {}, ...(KIND_LOGOS[kind] || []), title + (off ? ` — ${off}` : ''))), el('small', { textContent: desc }), more ? el('div', { className: 'more' }, ...more) : null);
|
|
262
717
|
card.addEventListener('click', (e) => {
|
|
263
718
|
if (!off && !e.target.closest('.more') && !radio.checked) radio.checked = true;
|
|
264
719
|
});
|
|
@@ -267,7 +722,7 @@
|
|
|
267
722
|
// CLI: one chip per known CLI, uninstalled ones greyed.
|
|
268
723
|
const chips = el('div', { className: 'chips' }, ...S.allClis.map((c) => {
|
|
269
724
|
const ok = S.clis.includes(c);
|
|
270
|
-
return el('label', { className: `chip${ok ? '' : ' off'}`, title: ok ? '' : t('notInstalled') }, el('input', { type: 'radio', name: 'cli', value: c, disabled: !ok, checked: b.kind === 'cli' ? b.cli === c : ok && c === S.clis[0] }), c);
|
|
725
|
+
return el('label', { className: `chip${ok ? '' : ' off'}`, title: ok ? '' : t('notInstalled') }, el('input', { type: 'radio', name: 'cli', value: c, disabled: !ok, checked: b.kind === 'cli' ? b.cli === c : ok && c === S.clis[0] }), logo(CLI_LOGO[c] || c, 14), c);
|
|
271
726
|
}));
|
|
272
727
|
const cliModel = el('input', { type: 'text', name: 'cliModel', value: b.kind === 'cli' ? b.model || '' : '', placeholder: 'claude-haiku-4-5-20251001', autocomplete: 'off' });
|
|
273
728
|
cliModel.setAttribute('list', 'models');
|
|
@@ -278,20 +733,33 @@
|
|
|
278
733
|
cliModel.placeholder = (CLI_MODELS[c] || [''])[0];
|
|
279
734
|
};
|
|
280
735
|
chips.addEventListener('change', fillModels);
|
|
736
|
+
// Your API key: Anthropic, OpenAI-compatible or Batuta (Orquesta), each with its own fields
|
|
737
|
+
const prov = b.kind === 'batuta' ? 'batuta' : b.provider || 'anthropic';
|
|
738
|
+
const provChips = el('div', { className: 'chips' }, ...[['anthropic', 'Anthropic', 'anthropic'], ['openai', t('oaiCompat'), 'openai'], ['batuta', 'Batuta (Orquesta)', 'orquesta']].map(([p, label, lg]) =>
|
|
739
|
+
el('label', { className: 'chip' }, el('input', { type: 'radio', name: 'provider', value: p, checked: prov === p }), logo(lg, 14), label)));
|
|
740
|
+
const only = (p, node) => ((node.dataset.p = p), node);
|
|
741
|
+
const apiModel = el('input', { type: 'text', name: 'apiModel', value: b.kind === 'api' || b.kind === 'batuta' ? b.model || '' : '', autocomplete: 'off' });
|
|
742
|
+
const apiFields = [
|
|
743
|
+
el('label', { className: 'f' }, t('model'), apiModel),
|
|
744
|
+
only('openai', el('label', { className: 'f' }, t('baseUrl'), el('input', { type: 'text', name: 'baseUrl', value: b.baseUrl || '', placeholder: 'https://…', autocomplete: 'off' }))),
|
|
745
|
+
only('anthropic', keyField('ANTHROPIC_API_KEY', 'Anthropic ' + t('key'))),
|
|
746
|
+
only('openai', keyField('OPENAI_API_KEY', 'OpenAI ' + t('key'))),
|
|
747
|
+
only('batuta', keyField('ORQUESTA_TOKEN', t('orqToken'))),
|
|
748
|
+
];
|
|
749
|
+
const showProv = () => {
|
|
750
|
+
const p = provChips.querySelector('input:checked')?.value || 'anthropic';
|
|
751
|
+
for (const f of apiFields) if (f.dataset.p) f.hidden = f.dataset.p !== p;
|
|
752
|
+
apiModel.placeholder = { anthropic: 'claude-haiku-4-5-20251001', openai: 'gpt-4o-mini', batuta: 'batuta-auto' }[p];
|
|
753
|
+
};
|
|
754
|
+
provChips.addEventListener('change', showProv);
|
|
281
755
|
box.append(
|
|
282
756
|
opt('lines', t('lines'), t('linesD')),
|
|
283
757
|
opt('cli', t('cli'), t('cliD'), [chips, el('label', { className: 'f' }, t('model'), cliModel, models)], S.clis.length ? '' : t('notInstalled')),
|
|
284
|
-
opt('api', t('api'), t('apiD'), [
|
|
285
|
-
el('label', { className: 'f' }, t('provider'), el('select', { name: 'provider' }, ...['anthropic', 'openai'].map((p) => el('option', { value: p, textContent: p === 'openai' ? 'OpenAI-compatible' : 'Anthropic', selected: b.provider === p })))),
|
|
286
|
-
el('label', { className: 'f' }, t('model'), el('input', { type: 'text', name: 'apiModel', value: b.kind === 'api' ? b.model || '' : '', autocomplete: 'off' })),
|
|
287
|
-
el('label', { className: 'f' }, t('baseUrl'), el('input', { type: 'text', name: 'baseUrl', value: b.baseUrl || '', placeholder: 'https://…', autocomplete: 'off' })),
|
|
288
|
-
keyField('ANTHROPIC_API_KEY', 'Anthropic ' + t('key')),
|
|
289
|
-
keyField('OPENAI_API_KEY', 'OpenAI ' + t('key')),
|
|
290
|
-
]),
|
|
291
|
-
opt('batuta', t('batuta'), t('batutaD'), [keyField('ORQUESTA_TOKEN', t('orqToken'))]),
|
|
758
|
+
opt('api', t('api'), t('apiD'), [provChips, ...apiFields]),
|
|
292
759
|
opt('7ots', t('home'), t('homeD'), null, S.home.url ? '' : t('noHome')),
|
|
293
760
|
);
|
|
294
761
|
fillModels();
|
|
762
|
+
showProv();
|
|
295
763
|
$('#now').replaceChildren(t('now'), ' ', el('b', { textContent: S.brain === 'lines' ? t('lines') : S.brain }));
|
|
296
764
|
}
|
|
297
765
|
function brainForm() {
|
|
@@ -300,7 +768,7 @@
|
|
|
300
768
|
const kind = box.querySelector('input[name=kind]:checked')?.value || 'lines';
|
|
301
769
|
const brain =
|
|
302
770
|
kind === 'cli' ? { kind, cli: box.querySelector('input[name=cli]:checked')?.value, model: v('cliModel') }
|
|
303
|
-
: kind === 'api' ? { kind,
|
|
771
|
+
: kind === 'api' ? ((p) => (p === 'batuta' ? { kind: 'batuta', model: v('apiModel') } : { kind, provider: p, model: v('apiModel'), baseUrl: p === 'openai' ? v('baseUrl') : '' }))(box.querySelector('input[name=provider]:checked')?.value || 'anthropic')
|
|
304
772
|
: { kind };
|
|
305
773
|
const keys = {};
|
|
306
774
|
for (const n of ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ORQUESTA_TOKEN']) if (v(n)) keys[n] = v(n);
|
|
@@ -346,14 +814,179 @@
|
|
|
346
814
|
const sw = (key, on, disabled, soon) => {
|
|
347
815
|
const input = el('input', { type: 'checkbox', checked: on, disabled });
|
|
348
816
|
input.setAttribute('role', 'switch');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
817
|
+
const lk = key === 'processes' ? 'procs' : key === 'vm' ? 'offload' : key;
|
|
818
|
+
input.setAttribute('aria-label', t(lk));
|
|
819
|
+
if (!disabled) input.addEventListener('change', () => save({ access: { [key]: input.checked } }, $('#accessMsg')).then(() => (key === 'screen' || key === 'vm') && renderAccess()));
|
|
820
|
+
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')));
|
|
352
821
|
};
|
|
353
822
|
for (const k of ['agent', 'memory', 'open', 'notify']) box.append(sw(k, S.config.access[k] !== false, false, false));
|
|
354
|
-
|
|
823
|
+
const scr = sw('screen', S.config.access.screen === true, false, false);
|
|
824
|
+
if (S.config.access.screen) scr.append(screenRow());
|
|
825
|
+
box.append(scr);
|
|
826
|
+
box.append(sw('processes', S.config.access.processes === true, false, false));
|
|
827
|
+
box.append(sw('logs', S.config.access.logs === true, false, false));
|
|
828
|
+
box.append(sw('inbox', S.config.access.inbox === true, false, false));
|
|
829
|
+
const vm = sw('vm', S.config.access.vm === true, false, false);
|
|
830
|
+
if (S.config.access.vm) vm.append(vmKindsRow());
|
|
831
|
+
box.append(vm);
|
|
355
832
|
box.append(el('span', { className: 'msg', id: 'accessMsg', role: 'status' }));
|
|
833
|
+
renderInteg();
|
|
834
|
+
}
|
|
835
|
+
// Under “Hand work to your server”: which kinds the pet may send (code/shell only if ticked here).
|
|
836
|
+
function vmKindsRow() {
|
|
837
|
+
const on = new Set(S.config.access.vmKinds || ['research', 'browse', 'long']);
|
|
838
|
+
const row = el('div', { className: 'kinds' }, el('span', { textContent: t('kindsL') }));
|
|
839
|
+
for (const k of ['research', 'browse', 'long', 'code', 'shell']) {
|
|
840
|
+
const box = el('input', { type: 'checkbox', checked: on.has(k) });
|
|
841
|
+
box.addEventListener('change', () => {
|
|
842
|
+
if (box.checked) on.add(k);
|
|
843
|
+
else on.delete(k);
|
|
844
|
+
save({ access: { vmKinds: [...on] } }, $('#accessMsg'));
|
|
845
|
+
});
|
|
846
|
+
row.append(el('label', {}, box, ` ${t(`k_${k}`)}`));
|
|
847
|
+
}
|
|
848
|
+
return row;
|
|
849
|
+
}
|
|
850
|
+
// Under “See your screen”: how often, a look-now button, and the last thing it saw.
|
|
851
|
+
function screenRow() {
|
|
852
|
+
const every = el('select', {}, ...[1, 2, 5, 10, 15, 30, 60].map((m) => el('option', { value: m, textContent: m, selected: (S.screen?.every ?? 5) === m })));
|
|
853
|
+
every.addEventListener('change', () => save({ screen: { every: Number(every.value) } }, $('#accessMsg')));
|
|
854
|
+
const last = el('span');
|
|
855
|
+
const showLast = (l) => last.replaceChildren(l ? `${t('lastSeen')} ${l.doing} · ${fmt(l.at)}` : '');
|
|
856
|
+
showLast(M?.seen?.looks?.at(-1));
|
|
857
|
+
const now = el('button', { textContent: t('lookNow') });
|
|
858
|
+
now.addEventListener('click', async () => {
|
|
859
|
+
now.disabled = true;
|
|
860
|
+
now.textContent = t('looking');
|
|
861
|
+
try {
|
|
862
|
+
const r = await api('/screen/look', {});
|
|
863
|
+
showLast(r.seen);
|
|
864
|
+
loadMemory();
|
|
865
|
+
} catch (e) {
|
|
866
|
+
flash($('#accessMsg'), t('err', { e: e.message }), 'err');
|
|
867
|
+
} finally {
|
|
868
|
+
now.disabled = false;
|
|
869
|
+
now.textContent = t('lookNow');
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
return el('div', { className: 'sub' }, t('screenEvery'), every, t('min'), now, last, S.vision ? null : el('div', { textContent: t('noVision') }));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ── voice ──
|
|
876
|
+
function renderVoices() {
|
|
877
|
+
const box = $('#voices');
|
|
878
|
+
const cur = S.config.voice?.kind || 'browser';
|
|
879
|
+
const why = { local: S.voices.local.length ? '' : t('vlocalNo') };
|
|
880
|
+
const VLOGO = { none: () => ico('volume-x', 16), browser: () => ico('globe', 16), local: () => ico('laptop', 16), elevenlabs: () => logo('elevenlabs'), openai: () => logo('openai'), apuchat: () => logo('apuchat'), grok: () => logo('grok'), fish: () => logo('fishaudio') };
|
|
881
|
+
const opt = (kind, more) => {
|
|
882
|
+
const off = why[kind] || '';
|
|
883
|
+
const radio = el('input', { type: 'radio', name: 'voice', value: kind, checked: cur === kind, disabled: !!off });
|
|
884
|
+
radio.id = `v-${kind}`;
|
|
885
|
+
const card = el('div', { className: `opt${off ? ' off' : ''}` }, radio, el('label', { htmlFor: radio.id }, el('b', {}, VLOGO[kind]?.(), t(`v${kind}`) + (off ? ` — ${off}` : ''))), el('small', { textContent: t(`v${kind}D`) + (kind === 'local' && S.voices.local.length ? ` · ${S.voices.local.join(', ')}` : '') }), more ? el('div', { className: 'more' }, ...more) : null);
|
|
886
|
+
card.addEventListener('click', (e) => {
|
|
887
|
+
if (!off && !e.target.closest('.more') && !radio.checked) radio.checked = true;
|
|
888
|
+
});
|
|
889
|
+
return card;
|
|
890
|
+
};
|
|
891
|
+
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
|
+
box.replaceChildren(
|
|
893
|
+
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)]),
|
|
899
|
+
);
|
|
900
|
+
const stt = { local: t('sttLocal'), openai: 'OpenAI', elevenlabs: 'ElevenLabs' }[S.stt] || t('sttNone');
|
|
901
|
+
$('#stt').replaceChildren(t('sttNow'), ' ', el('b', { textContent: stt }));
|
|
902
|
+
}
|
|
903
|
+
$('#saveVoice').addEventListener('click', async () => {
|
|
904
|
+
const box = $('#voices');
|
|
905
|
+
const kind = box.querySelector('input[name=voice]:checked')?.value || 'browser';
|
|
906
|
+
const keys = {};
|
|
907
|
+
for (const [field, name] of [['ELEVENLABS_API_KEY', 'ELEVENLABS_API_KEY'], ['OPENAI_API_KEY_V', 'OPENAI_API_KEY'], ['APUCHAT_VOICE_TOKEN', 'APUCHAT_VOICE_TOKEN'], ['XAI_API_KEY', 'XAI_API_KEY'], ['FISH_API_KEY', 'FISH_API_KEY']]) {
|
|
908
|
+
const v = box.querySelector(`[name=${field}]`)?.value.trim();
|
|
909
|
+
if (v) keys[name] = v;
|
|
910
|
+
}
|
|
911
|
+
await save({ voice: { kind }, ...(Object.keys(keys).length ? { keys } : {}) }, $('#voiceMsg'));
|
|
912
|
+
renderVoices();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// ── memory: what it saw, what it said ──
|
|
916
|
+
let M = null;
|
|
917
|
+
let memTab = 'notes';
|
|
918
|
+
function renderMemTabs() {
|
|
919
|
+
$('#memTabs').replaceChildren(...['notes', 'seen', 'said'].map((k) => {
|
|
920
|
+
const r = el('input', { type: 'radio', name: 'memtab', value: k, checked: memTab === k });
|
|
921
|
+
r.addEventListener('change', () => ((memTab = k), showMemTab()));
|
|
922
|
+
return el('label', { className: 'chip' }, r, t(`mem${k[0].toUpperCase()}${k.slice(1)}`));
|
|
923
|
+
}));
|
|
924
|
+
showMemTab();
|
|
925
|
+
}
|
|
926
|
+
function showMemTab() {
|
|
927
|
+
$('#memNotes').hidden = memTab !== 'notes';
|
|
928
|
+
$('#memSeen').hidden = memTab !== 'seen';
|
|
929
|
+
$('#memSaid').hidden = memTab !== 'said';
|
|
930
|
+
}
|
|
931
|
+
const row = (summary, meta, at, fields, bad = false) => {
|
|
932
|
+
const d = el('details', { className: 'r' });
|
|
933
|
+
d.append(el('summary', {}, el('span', { className: `sum${bad ? ' bad' : ''}`, textContent: summary }), at ? el('time', { textContent: fmt(at) }) : el('span'), meta ? el('span', { className: 'meta', textContent: meta }) : null));
|
|
934
|
+
// Built on first open: inference rows can carry long prompts.
|
|
935
|
+
d.addEventListener('toggle', () => {
|
|
936
|
+
if (!d.open || d.querySelector('.body')) return;
|
|
937
|
+
d.append(el('div', { className: 'body' }, ...fields().filter(([, v]) => v).flatMap(([k, v]) => [el('b', { textContent: k }), el('pre', { textContent: v })])));
|
|
938
|
+
}, { once: false });
|
|
939
|
+
return d;
|
|
940
|
+
};
|
|
941
|
+
async function loadMemory() {
|
|
942
|
+
try {
|
|
943
|
+
M = await api('/memory');
|
|
944
|
+
} catch {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
// What it saw: brain summaries interleaved with the window changes that led to them.
|
|
948
|
+
const items = [...M.seen.looks.map((l) => ({ ...l, kind: 'look' })), ...M.seen.windows.map((w) => ({ ...w, kind: 'win' }))].sort((a, b) => b.at - a.at).slice(0, 60);
|
|
949
|
+
$('#seen').replaceChildren(...(items.length ? items.map((i) => i.kind === 'look'
|
|
950
|
+
? row(`👁️ ${i.doing}`, i.how, i.at, () => [[t('fOutput'), [i.doing, i.say].filter(Boolean).join('\n')]])
|
|
951
|
+
: row(`🪟 ${i.title || i.app}`, i.app, i.at, () => [['', JSON.stringify(i, null, 2)]]))
|
|
952
|
+
: [el('p', { className: 'empty', textContent: t('noSeen') })]));
|
|
953
|
+
$('#forgetSeen').hidden = !items.length;
|
|
954
|
+
const said = M.said.slice(-40).reverse();
|
|
955
|
+
$('#said').replaceChildren(...(said.length ? said.map((x) => el('li', {}, el('span', { textContent: x }))) : [el('li', { className: 'empty', textContent: t('noSaid') })]));
|
|
956
|
+
}
|
|
957
|
+
$('#forgetSeen').addEventListener('click', async () => {
|
|
958
|
+
await api('/seen/forget', {});
|
|
959
|
+
loadMemory();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// ── inference history ──
|
|
963
|
+
const num = (n) => Number(n || 0).toLocaleString(lang);
|
|
964
|
+
const asText = (v) => (v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v, null, 2));
|
|
965
|
+
async function loadInferences() {
|
|
966
|
+
let L = [];
|
|
967
|
+
try {
|
|
968
|
+
L = (await api('/inferences?n=80')).inferences;
|
|
969
|
+
} catch {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
// Keep open rows open across refreshes.
|
|
973
|
+
const open = new Set([...document.querySelectorAll('#infs details[open]')].map((d) => d.dataset.at));
|
|
974
|
+
// Tokens: what each call cost, and the total of what's listed ("≈" when the brain doesn't report it).
|
|
975
|
+
const tok = (u) => (u ? `${u.approx ? '≈' : ''}${num(u.in + (u.cacheRead || 0) + (u.cacheWrite || 0))} → ${num(u.out)} tok${u.usd ? ` · $${u.usd.toFixed(4)}` : ''}` : '');
|
|
976
|
+
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
|
+
$('#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
|
+
$('#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;
|
|
982
|
+
return d;
|
|
983
|
+
}) : [el('p', { className: 'empty', textContent: t('noInf') })]));
|
|
356
984
|
}
|
|
985
|
+
$('#clearInf').addEventListener('click', async () => {
|
|
986
|
+
await api('/inferences/clear', {});
|
|
987
|
+
loadInferences();
|
|
988
|
+
flash($('#infMsg'), t('saved'));
|
|
989
|
+
});
|
|
357
990
|
|
|
358
991
|
// ── chattiness + look ──
|
|
359
992
|
function renderPet() {
|
|
@@ -367,12 +1000,269 @@
|
|
|
367
1000
|
}
|
|
368
1001
|
$('#annoy').addEventListener('change', (e) => save({ pet: { annoy: Number(e.target.value) } }, $('#petMsg')));
|
|
369
1002
|
|
|
1003
|
+
// ── integrations (Permissions → Integrations) ──
|
|
1004
|
+
const toggle = (on, onChange, label) => {
|
|
1005
|
+
const i = el('input', { type: 'checkbox', checked: !!on });
|
|
1006
|
+
i.setAttribute('role', 'switch');
|
|
1007
|
+
if (label) i.setAttribute('aria-label', label);
|
|
1008
|
+
i.addEventListener('change', () => onChange(i.checked));
|
|
1009
|
+
return el('span', { className: 'tog' }, i, el('span'));
|
|
1010
|
+
};
|
|
1011
|
+
const card = (lg, title, desc, tog, ...body) =>
|
|
1012
|
+
el('div', { className: 'integ' }, el('header', {}, el('b', {}, lg, title), tog), desc ? el('small', { textContent: desc }) : null, ...body);
|
|
1013
|
+
const field = (label, input) => el('label', { className: 'f' }, label, input);
|
|
1014
|
+
function renderInteg() {
|
|
1015
|
+
const box = $('#integ');
|
|
1016
|
+
const I = S.integrations || {};
|
|
1017
|
+
const msg = el('span', { className: 'msg', role: 'status' });
|
|
1018
|
+
|
|
1019
|
+
// 7ots cloud: the link with 7ots.com and the cloud assistant (its VM), read-only for now
|
|
1020
|
+
const cloudBody = el('div', { className: 'rows' }, el('small', { textContent: '…' }));
|
|
1021
|
+
api('/cloud').then((c) => {
|
|
1022
|
+
const who = c.signedIn
|
|
1023
|
+
? el('small', { textContent: [t('cloudAs', { who: c.user?.email || c.user?.name || '…' }), c.twin ? t('cloudTwin', { name: c.twin.name }) : t('cloudNoTwin')].join(' · ') })
|
|
1024
|
+
: el('small', {}, t('cloudOff'), ' ', el('a', { href: '#acc', textContent: t('goAcc') }));
|
|
1025
|
+
const st = c.vm?.status || 'soon';
|
|
1026
|
+
const word = { none: t('vmNone'), pending: t('vmPending'), online: t('vmOnline'), offline: t('vmOffline'), terminated: t('vmTerminated') }[st] || st;
|
|
1027
|
+
const vmLine = st === 'soon' ? t('vmD') : st === 'denied' ? t('vmDenied') : st === 'error' ? t('vmError') : [word, c.vm.region, c.vm.since && st !== 'none' ? t('vmSince', { d: fmt(new Date(c.vm.since).getTime()) }) : ''].filter(Boolean).join(' · ');
|
|
1028
|
+
const vm = el('div', { className: 'vm' }, el('b', {}, ico('cloud', 16), t('vmH'), st === 'soon' ? el('span', { className: 'tag', textContent: t('soon') }) : null), el('small', { textContent: vmLine }));
|
|
1029
|
+
cloudBody.replaceChildren(who, vm);
|
|
1030
|
+
}).catch((e) => cloudBody.replaceChildren(el('small', { className: 'bad', textContent: e.message })));
|
|
1031
|
+
|
|
1032
|
+
// Orquesta: its agents (projects), a default one, and a task box
|
|
1033
|
+
const oq = I.orquesta || {};
|
|
1034
|
+
const tokenIn = keyField('ORQUESTA_TOKEN', t('orqToken'));
|
|
1035
|
+
const saveTok = el('button', { textContent: t('save') });
|
|
1036
|
+
saveTok.addEventListener('click', async () => {
|
|
1037
|
+
const v = tokenIn.querySelector('input').value.trim();
|
|
1038
|
+
if (!v) return;
|
|
1039
|
+
await save({ keys: { ORQUESTA_TOKEN: v } }, msg);
|
|
1040
|
+
renderInteg();
|
|
1041
|
+
});
|
|
1042
|
+
const proj = el('div', { className: 'rows' }, el('small', { textContent: '…' }));
|
|
1043
|
+
const loadProjects = async () => {
|
|
1044
|
+
let r;
|
|
1045
|
+
try {
|
|
1046
|
+
r = await api('/orquesta/projects');
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
return proj.replaceChildren(el('small', { className: 'bad', textContent: e.message }));
|
|
1049
|
+
}
|
|
1050
|
+
if (!r.connected) return proj.replaceChildren(el('small', { textContent: r.error || t('orqNoToken') }));
|
|
1051
|
+
if (!r.projects?.length) return proj.replaceChildren(el('small', { textContent: r.error || t('orqNone') }));
|
|
1052
|
+
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
|
+
sel.addEventListener('change', () => save({ integrations: { orquesta: { projectId: sel.value, projectName: sel.selectedOptions[0]?.textContent.replace(/^[●○] /, '').replace(/ · .*$/, '') } } }, msg));
|
|
1054
|
+
const task = el('textarea', { rows: 2, placeholder: t('orqTask') });
|
|
1055
|
+
const send = el('button', { className: 'primary' }, ico('send', 15), ' ', t('orqSend'));
|
|
1056
|
+
const out = el('pre', { hidden: true });
|
|
1057
|
+
send.addEventListener('click', async () => {
|
|
1058
|
+
const text = task.value.trim();
|
|
1059
|
+
if (!text) return task.focus();
|
|
1060
|
+
send.disabled = true;
|
|
1061
|
+
try {
|
|
1062
|
+
const { id } = await api('/orquesta/send', { projectId: sel.value, text });
|
|
1063
|
+
task.value = '';
|
|
1064
|
+
out.hidden = false;
|
|
1065
|
+
out.textContent = t('orqSent', { status: 'pending' });
|
|
1066
|
+
// follow it until the agent finishes (or 30 min)
|
|
1067
|
+
for (let i = 0; i < 450; i++) {
|
|
1068
|
+
await new Promise((res) => setTimeout(res, 4000));
|
|
1069
|
+
const s = await api(`/orquesta/prompt?id=${encodeURIComponent(id)}`).catch((e) => ({ status: 'error', error: e.message }));
|
|
1070
|
+
out.textContent = [t('orqSent', { status: s.status }), s.result || s.error || ''].filter(Boolean).join('\n\n');
|
|
1071
|
+
if (s.result != null || s.error) break;
|
|
1072
|
+
}
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
flash(msg, t('err', { e: e.message }), 'err');
|
|
1075
|
+
} finally {
|
|
1076
|
+
send.disabled = false;
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
proj.replaceChildren(el('div', { className: 'fields' }, field(t('orqProject'), sel)), task, el('div', { className: 'fields' }, send), out);
|
|
1080
|
+
};
|
|
1081
|
+
loadProjects();
|
|
1082
|
+
const orq = card(logo('orquesta', 18), t('orqH'), t('orqD'), toggle(oq.on, (on) => save({ integrations: { orquesta: { on } } }, msg), t('orqH')),
|
|
1083
|
+
el('div', { className: 'fields' }, tokenIn, saveTok), el('small', { textContent: t('orqHint') }), proj);
|
|
1084
|
+
|
|
1085
|
+
// Email: an apumail inbox (Gmail later)
|
|
1086
|
+
const am = I.apumail || {};
|
|
1087
|
+
const inbox = el('input', { type: 'text', value: am.inbox || '', placeholder: 'ot@apumail.com', autocomplete: 'off' });
|
|
1088
|
+
const mailTok = keyField('APUMAIL_TOKEN', t('mailToken'));
|
|
1089
|
+
const saveMail = el('button', { textContent: t('save') });
|
|
1090
|
+
saveMail.addEventListener('click', async () => {
|
|
1091
|
+
const v = mailTok.querySelector('input').value.trim();
|
|
1092
|
+
await save({ integrations: { apumail: { inbox: inbox.value.trim() } }, ...(v ? { keys: { APUMAIL_TOKEN: v } } : {}) }, msg);
|
|
1093
|
+
renderInteg();
|
|
1094
|
+
});
|
|
1095
|
+
const mail = card(logo('apumail', 18), t('mailH'), t('mailD'), toggle(am.on, (on) => save({ integrations: { apumail: { on } } }, msg), t('mailH')),
|
|
1096
|
+
el('div', { className: 'fields' }, field(t('mailInbox'), inbox), mailTok, saveMail));
|
|
1097
|
+
const gmail = el('div', { className: 'integ soon' }, el('header', {}, el('b', {}, logo('gmail', 18), 'Gmail', el('span', { className: 'tag', textContent: t('soon') }))), el('small', { textContent: t('gmailD') }));
|
|
1098
|
+
|
|
1099
|
+
// Custom: name + instructions (+ a URL or a command), told to the brain as tools
|
|
1100
|
+
let custom = (I.custom || []).map((c) => ({ ...c }));
|
|
1101
|
+
const list = el('div');
|
|
1102
|
+
const saveCustom = async () => {
|
|
1103
|
+
await save({ integrations: { custom } }, msg);
|
|
1104
|
+
custom = (S.integrations?.custom || []).map((c) => ({ ...c }));
|
|
1105
|
+
draw();
|
|
1106
|
+
};
|
|
1107
|
+
const add = el('button', {}, ico('plus', 15), ' ', t('customAdd'));
|
|
1108
|
+
add.addEventListener('click', () => {
|
|
1109
|
+
custom.push({ name: '', instructions: '', on: true, _new: true });
|
|
1110
|
+
draw();
|
|
1111
|
+
});
|
|
1112
|
+
const draw = () => list.replaceChildren(...custom.map((c, i) => {
|
|
1113
|
+
const inp = (k, type = 'text', ph = '') => {
|
|
1114
|
+
const n = type === 'area' ? el('textarea', { rows: 3, value: c[k] || '', placeholder: ph }) : el('input', { type: 'text', value: c[k] || '', placeholder: ph, autocomplete: 'off' });
|
|
1115
|
+
n.addEventListener('input', () => (c[k] = n.value));
|
|
1116
|
+
return n;
|
|
1117
|
+
};
|
|
1118
|
+
const rm = el('button', { className: 'x', textContent: t('remove') });
|
|
1119
|
+
rm.addEventListener('click', () => (custom.splice(i, 1), saveCustom()));
|
|
1120
|
+
const ok = el('button', { className: 'primary', textContent: t('save') });
|
|
1121
|
+
ok.addEventListener('click', () => (custom.forEach((x) => delete x._new), saveCustom()));
|
|
1122
|
+
const det = el('details', { open: !!c._new }, el('summary', { textContent: t('edit') }),
|
|
1123
|
+
el('div', { className: 'rows' }, field(t('customName'), inp('name')), field(t('customInstr'), inp('instructions', 'area')),
|
|
1124
|
+
el('div', { className: 'fields' }, field(t('customUrl'), inp('url', 'text', 'https://…')), field(t('customCmd'), inp('command', 'text', 'curl -s …')), field(t('customEnv'), inp('envVar', 'text', 'MY_TOKEN'))),
|
|
1125
|
+
el('small', { textContent: t('customEnvHint') }), el('div', { className: 'fields' }, ok, rm)));
|
|
1126
|
+
return card(ico('plug', 18), c.name || t('untitled'), c.instructions ? c.instructions.slice(0, 140) : '', c._new ? null : toggle(c.on !== false, (on) => ((c.on = on), saveCustom()), c.name), det);
|
|
1127
|
+
}), add);
|
|
1128
|
+
draw();
|
|
1129
|
+
const customBox = el('div', {}, el('p', { className: 'hint', textContent: t('customD') }), list);
|
|
1130
|
+
|
|
1131
|
+
box.replaceChildren(
|
|
1132
|
+
card(logo('7ots', 18), t('cloudH'), t('cloudD'), null, cloudBody),
|
|
1133
|
+
orq, mail, gmail,
|
|
1134
|
+
el('h3', { className: 'sh', textContent: t('customH') }), customBox, msg,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ── character sheet (Personality) ──
|
|
1139
|
+
const CH_FIELDS = [['backstory', 4], ['style', 2], ['quirks', 2], ['scenario', 2], ['greeting', 1], ['examples', 5]];
|
|
1140
|
+
const chLabel = (k) => t(`ch${k[0].toUpperCase()}${k.slice(1)}`);
|
|
1141
|
+
// A chara_card_v2 (or v1/v3) from a JSON file or a PNG's "chara"/"ccv3" text chunk.
|
|
1142
|
+
async function readCard(file) {
|
|
1143
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
1144
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) {
|
|
1145
|
+
const found = {};
|
|
1146
|
+
for (let p = 8; p + 8 <= buf.length;) {
|
|
1147
|
+
const len = ((buf[p] << 24) | (buf[p + 1] << 16) | (buf[p + 2] << 8) | buf[p + 3]) >>> 0;
|
|
1148
|
+
const type = String.fromCharCode(...buf.subarray(p + 4, p + 8));
|
|
1149
|
+
if (type === 'tEXt') {
|
|
1150
|
+
const data = buf.subarray(p + 8, p + 8 + len);
|
|
1151
|
+
const z = data.indexOf(0);
|
|
1152
|
+
const kw = String.fromCharCode(...data.subarray(0, z));
|
|
1153
|
+
if (kw === 'chara' || kw === 'ccv3') {
|
|
1154
|
+
const b64 = Array.from(data.subarray(z + 1), (c) => String.fromCharCode(c)).join('');
|
|
1155
|
+
found[kw] = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (type === 'IEND') break;
|
|
1159
|
+
p += 12 + len;
|
|
1160
|
+
}
|
|
1161
|
+
if (!found.chara && !found.ccv3) throw new Error('card');
|
|
1162
|
+
return found.chara || found.ccv3;
|
|
1163
|
+
}
|
|
1164
|
+
return JSON.parse(new TextDecoder().decode(buf));
|
|
1165
|
+
}
|
|
1166
|
+
function cardToSheet(card) {
|
|
1167
|
+
const d = card?.data && typeof card.data === 'object' ? card.data : card;
|
|
1168
|
+
if (!d || typeof d !== 'object' || !(d.description || d.personality || d.first_mes || d.mes_example || d.scenario)) throw new Error('card');
|
|
1169
|
+
const sub = (s) => String(s || '').replace(/\{\{char\}\}|<BOT>/gi, S.name).replace(/\{\{user\}\}|<USER>/gi, 'Human').trim();
|
|
1170
|
+
const ext = d.extensions?.['7ots'] || {};
|
|
1171
|
+
return {
|
|
1172
|
+
backstory: sub(d.description).slice(0, 3000),
|
|
1173
|
+
style: sub(ext.style || d.personality).slice(0, 1500),
|
|
1174
|
+
quirks: sub(ext.quirks || '').slice(0, 1500),
|
|
1175
|
+
scenario: sub(d.scenario).slice(0, 1500),
|
|
1176
|
+
greeting: sub(d.first_mes).slice(0, 600),
|
|
1177
|
+
examples: sub(d.mes_example).replace(/<START>\s*/gi, '').slice(0, 4000),
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function sheetToCard(ch) {
|
|
1181
|
+
return {
|
|
1182
|
+
spec: 'chara_card_v2', spec_version: '2.0',
|
|
1183
|
+
data: {
|
|
1184
|
+
name: S.name, description: ch.backstory || '', personality: ch.style || '', scenario: ch.scenario || '', first_mes: ch.greeting || '', mes_example: ch.examples || '',
|
|
1185
|
+
creator_notes: 'Exported from 7ots (7ots.com)', system_prompt: '', post_history_instructions: '', alternate_greetings: [], tags: ['7ots'], creator: '', character_version: '1',
|
|
1186
|
+
extensions: { '7ots': { style: ch.style || '', quirks: ch.quirks || '' } },
|
|
1187
|
+
},
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function renderChar() {
|
|
1191
|
+
const ch = S.character || {};
|
|
1192
|
+
const msg = el('span', { className: 'msg', role: 'status' });
|
|
1193
|
+
const ins = {};
|
|
1194
|
+
const fields = CH_FIELDS.map(([k, rows]) => {
|
|
1195
|
+
ins[k] = rows > 1 ? el('textarea', { rows, value: ch[k] || '' }) : el('input', { type: 'text', value: ch[k] || '', autocomplete: 'off' });
|
|
1196
|
+
if (k === 'examples') ins[k].placeholder = t('chExamplesPh', { name: S.name });
|
|
1197
|
+
return field(chLabel(k), ins[k]);
|
|
1198
|
+
});
|
|
1199
|
+
const sheet = () => Object.fromEntries(CH_FIELDS.map(([k]) => [k, ins[k].value.trim()]));
|
|
1200
|
+
const ok = el('button', { className: 'primary', textContent: t('save') });
|
|
1201
|
+
ok.addEventListener('click', async () => (await save({ character: sheet() }, msg), renderChar()));
|
|
1202
|
+
const file = el('input', { type: 'file', accept: '.json,.png,application/json,image/png', hidden: true });
|
|
1203
|
+
file.addEventListener('change', async () => {
|
|
1204
|
+
const f = file.files[0];
|
|
1205
|
+
if (!f) return;
|
|
1206
|
+
try {
|
|
1207
|
+
await save({ character: cardToSheet(await readCard(f)) }, msg, t('imported'));
|
|
1208
|
+
renderChar();
|
|
1209
|
+
} catch {
|
|
1210
|
+
flash(msg, t('badCard'), 'err');
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
const imp = el('button', {}, ico('upload', 15), ' ', t('importCard'));
|
|
1214
|
+
imp.addEventListener('click', () => file.click());
|
|
1215
|
+
const exp = el('button', {}, ico('download', 15), ' ', t('exportCard'));
|
|
1216
|
+
exp.addEventListener('click', () => {
|
|
1217
|
+
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` });
|
|
1218
|
+
a.click();
|
|
1219
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Letta: the persona in a Letta agent's core memory (optional)
|
|
1223
|
+
const lt = ch.letta || {};
|
|
1224
|
+
const agent = el('input', { type: 'text', value: lt.agentId || '', placeholder: 'agent-…', autocomplete: 'off' });
|
|
1225
|
+
const lkey = keyField('LETTA_API_KEY', t('lettaKey'));
|
|
1226
|
+
const sync = el('button', {}, ico('refresh-cw', 15), ' ', t('lettaSync'));
|
|
1227
|
+
sync.addEventListener('click', async () => {
|
|
1228
|
+
sync.disabled = true;
|
|
1229
|
+
try {
|
|
1230
|
+
const k = lkey.querySelector('input').value.trim();
|
|
1231
|
+
S = await api('/config', { character: { ...sheet(), letta: { on: true, agentId: agent.value.trim() } }, ...(k ? { keys: { LETTA_API_KEY: k } } : {}) });
|
|
1232
|
+
const r = await api('/character/letta', {});
|
|
1233
|
+
S = await api('/config');
|
|
1234
|
+
renderChar();
|
|
1235
|
+
flash($('#charMsg'), r.created ? t('lettaNew') : t('lettaOk'));
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
flash(msg, t('err', { e: errText(e) }), 'err');
|
|
1238
|
+
} finally {
|
|
1239
|
+
sync.disabled = false;
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
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
|
+
el('div', { className: 'fields' }, field(t('lettaAgent'), agent), lkey, sync));
|
|
1244
|
+
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));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// ── language of this page (and of the ot's settings) ──
|
|
1249
|
+
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'));
|
|
1255
|
+
location.reload();
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const errText = (e) => (T[lang][`e_${e.message}`] || T.en[`e_${e.message}`] ? t(`e_${e.message}`) : e.message);
|
|
1259
|
+
|
|
370
1260
|
async function save(body, msg, okText = t('saved')) {
|
|
371
1261
|
try {
|
|
372
1262
|
S = await api('/config', body);
|
|
373
1263
|
flash(msg, okText);
|
|
374
1264
|
} catch (e) {
|
|
375
|
-
flash(msg, t('err', { e: e
|
|
1265
|
+
flash(msg, t('err', { e: errText(e) }), 'err');
|
|
376
1266
|
}
|
|
377
1267
|
}
|
|
378
1268
|
|
|
@@ -389,15 +1279,48 @@
|
|
|
389
1279
|
return el('li', {}, el('span', { textContent: n.text }), n.at ? el('time', { textContent: fmt(n.at) }) : null, x);
|
|
390
1280
|
}) : [el('li', { className: 'empty', textContent: t('noNotes') })]));
|
|
391
1281
|
$('#forgetAll').hidden = !notes.length;
|
|
392
|
-
|
|
1282
|
+
}
|
|
1283
|
+
// ── processes: what the pet runs (reminders, alerts, the screen) and what runs on this computer ──
|
|
1284
|
+
const ago = (ms) => {
|
|
1285
|
+
const m = Math.round((Date.now() - ms) / 60000);
|
|
1286
|
+
return m < 60 ? `${m}m` : m < 1440 ? `${Math.round(m / 60)}h` : `${Math.round(m / 1440)}d`;
|
|
1287
|
+
};
|
|
1288
|
+
const money = (n) => Number(n).toLocaleString(lang, { maximumFractionDigits: n < 1 ? 6 : 2 });
|
|
1289
|
+
let P = null;
|
|
1290
|
+
async function loadProcesses() {
|
|
1291
|
+
try {
|
|
1292
|
+
P = await api('/processes');
|
|
1293
|
+
} catch {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const cancel = (path, id) => {
|
|
393
1297
|
const x = el('button', { className: 'x', textContent: t('cancel') });
|
|
394
|
-
x.addEventListener('click', async () => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
1298
|
+
x.addEventListener('click', async () => (await api(path, { id }), loadProcesses()));
|
|
1299
|
+
return x;
|
|
1300
|
+
};
|
|
1301
|
+
const items = [
|
|
1302
|
+
...P.reminders.map((r) => el('li', {}, el('span', { className: 'kind' }, ico('alarm-clock', 16)), el('span', {}, r.text + (r.open ? ` → ${new URL(r.open).host}` : ''), el('br'), el('small', { textContent: `${t('remind')} · ${fmt(r.at)}` })), cancel('/reminders/cancel', r.id))),
|
|
1303
|
+
...P.watchers.map((w) => {
|
|
1304
|
+
const price = w.kind === 'price';
|
|
1305
|
+
const what = price ? t('priceW', { coin: w.coin, op: w.op, value: money(w.value), vs: w.vs.toUpperCase() }) : t('procW', { m: w.match, until: t(w.until === 'start' ? 'untilStart' : 'untilExit') });
|
|
1306
|
+
const now = price ? (w.last?.price != null ? t('priceNow', { p: money(w.last.price) }) : '…') : w.last ? t(w.last.present ? 'running' : 'notRunning') : '…';
|
|
1307
|
+
return el('li', {}, el('span', { className: 'kind' }, ico(price ? 'trending-up' : 'terminal', 16)), el('span', {}, what, el('br'), el('small', { textContent: `${now}${w.text ? ` · ${w.text}` : ''}` })), cancel('/watchers/cancel', w.id));
|
|
1308
|
+
}),
|
|
1309
|
+
];
|
|
1310
|
+
if (P.screen.on) items.push(el('li', {}, el('span', { className: 'kind' }, ico('eye', 16)), el('span', {}, t('screenEvery', { n: P.screen.every }), P.screen.last ? el('br') : null, P.screen.last ? el('small', { textContent: t('lastSeen', { what: P.screen.last.doing || '' }) }) : null)));
|
|
1311
|
+
$('#rems').replaceChildren(...(items.length ? items : [el('li', { className: 'empty', textContent: t('noRems') })]));
|
|
1312
|
+
renderPs();
|
|
400
1313
|
}
|
|
1314
|
+
// Only what the ot itself watches or set up. Without "Processes and apps" it can't wait for a program: say so
|
|
1315
|
+
// when it was asked to.
|
|
1316
|
+
function renderPs() {
|
|
1317
|
+
const box = $('#ps');
|
|
1318
|
+
if (!P || P.system || !P.watchers.some((w) => w.kind === 'process')) return box.replaceChildren();
|
|
1319
|
+
const on = el('button', { className: 'primary', textContent: t('psOn') });
|
|
1320
|
+
on.addEventListener('click', async () => (await save({ access: { processes: true } }, $('#accessMsg')), renderAccess(), loadProcesses()));
|
|
1321
|
+
box.replaceChildren(el('p', { className: 'empty' }, t('psOff', { name: S.name }), ' '), on);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
401
1324
|
let armed = false;
|
|
402
1325
|
$('#forgetAll').addEventListener('click', async (e) => {
|
|
403
1326
|
if (!armed) {
|
|
@@ -416,12 +1339,23 @@
|
|
|
416
1339
|
S = await api('/config');
|
|
417
1340
|
if (all) {
|
|
418
1341
|
document.title = t('title', { name: S.name });
|
|
419
|
-
$('#title').textContent =
|
|
1342
|
+
$('#title').textContent = S.name;
|
|
1343
|
+
await loadMemory();
|
|
420
1344
|
renderBrains();
|
|
1345
|
+
renderVoices();
|
|
421
1346
|
renderAccess();
|
|
422
1347
|
renderPet();
|
|
423
|
-
|
|
1348
|
+
renderChar();
|
|
1349
|
+
renderLang();
|
|
1350
|
+
renderMemTabs();
|
|
1351
|
+
go();
|
|
1352
|
+
} else loadMemory();
|
|
1353
|
+
const pv = M?.pet;
|
|
1354
|
+
if (pv) $('#petLine').textContent = `${t('lv')} ${pv.level} · ${word(pv.mood)}${pv.asleep ? ' · 💤' : ''}`;
|
|
1355
|
+
if (!$('#av svg')) $('#av').textContent = (S.name || '7')[0].toUpperCase();
|
|
424
1356
|
renderLists();
|
|
1357
|
+
loadInferences();
|
|
1358
|
+
if (location.hash === '#rem') loadProcesses();
|
|
425
1359
|
}
|
|
426
1360
|
refresh(true);
|
|
427
1361
|
setInterval(() => document.visibilityState === 'visible' && refresh(), 15000); // reminders fire, notes get added
|