privateboard 0.1.9 → 0.1.11
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/dist/cli.js +3732 -2160
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.js +59 -36
- package/public/agent-profile.css +392 -100
- package/public/agent-profile.js +551 -59
- package/public/app.js +1341 -681
- package/public/i18n.js +1990 -0
- package/public/index.html +251 -94
- package/public/keys-store.js +63 -0
- package/public/new-agent.css +60 -0
- package/public/new-agent.js +121 -53
- package/public/room-settings.js +2 -1
- package/public/user-settings.css +68 -0
- package/public/user-settings.js +241 -87
- package/public/voice-replay.css +607 -0
- package/public/voice-replay.js +533 -0
package/public/user-settings.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
// /api/prefs is async; cache the latest value at module bootstrap so the
|
|
20
20
|
// synchronous render code below stays simple. saveUser writes through.
|
|
21
|
-
let _prefsCache = { name: "You", intro: "", avatarSeed: null };
|
|
21
|
+
let _prefsCache = { name: "You", intro: "", avatarSeed: null, webSearchProvider: "brave", minimaxRegion: "cn" };
|
|
22
22
|
|
|
23
23
|
const THEMES = [
|
|
24
24
|
{ slug: "regent", name: "Regent", desc: "warm gold on dark · default · the boardroom premium",
|
|
@@ -49,9 +49,13 @@
|
|
|
49
49
|
{ id: "openai", label: "OpenAI", hint: "GPT · gpt-5, gpt-5 mini, gpt-4o", placeholder: "sk-…", group: "llm" },
|
|
50
50
|
{ id: "google", label: "Google", hint: "Gemini · 2.5 Pro, 2.5 Flash", placeholder: "AIza…", group: "llm" },
|
|
51
51
|
{ id: "xai", label: "xAI", hint: "Grok · grok-4.3, grok-4.1 fast", placeholder: "xai-…", group: "llm" },
|
|
52
|
+
{ id: "minimax", label: "MiniMax", hint: "speech · T2A voices, cloning, streaming audio", placeholder: "mm-…", group: "voice" },
|
|
53
|
+
{ id: "elevenlabs", label: "ElevenLabs", hint: "text-to-speech · pricing & docs at elevenlabs.io", placeholder: "xi-…", group: "voice" },
|
|
52
54
|
// ── Skill Services (not LLM providers, but the same encrypted key store) ──
|
|
53
55
|
{ id: "brave", label: "Brave Search", hint: "powers the Web Search system skill · ≈ $5 / 1000 queries · privacy-respecting",
|
|
54
56
|
placeholder: "BSA…", group: "skill" },
|
|
57
|
+
{ id: "tavily", label: "Tavily Search", hint: "alternate Web Search backend · billed per Tavily credits · LLM-focused results",
|
|
58
|
+
placeholder: "tvly-…", group: "skill" },
|
|
55
59
|
];
|
|
56
60
|
|
|
57
61
|
function escape(s) {
|
|
@@ -60,6 +64,10 @@
|
|
|
60
64
|
}[c]));
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
function tr(key, vars) {
|
|
68
|
+
return (window.I18n && window.I18n.t(key, vars)) || key;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
/* ── Storage helpers ───────────────────────────────────────── */
|
|
64
72
|
function getTheme() { try { return localStorage.getItem(THEME_KEY) || "regent"; } catch (e) { return "regent"; } }
|
|
65
73
|
function setTheme(slug) {
|
|
@@ -76,7 +84,9 @@
|
|
|
76
84
|
_prefsCache = {
|
|
77
85
|
name: typeof data.name === "string" ? data.name : "You",
|
|
78
86
|
intro: typeof data.intro === "string" ? data.intro : "",
|
|
79
|
-
avatarSeed: data.avatarSeed ?? null
|
|
87
|
+
avatarSeed: data.avatarSeed ?? null,
|
|
88
|
+
webSearchProvider: data.webSearchProvider === "tavily" ? "tavily" : "brave",
|
|
89
|
+
minimaxRegion: data.minimaxRegion === "intl" ? "intl" : "cn",
|
|
80
90
|
};
|
|
81
91
|
} catch (e) {
|
|
82
92
|
// Network or server hiccup — fall back to whatever's already cached.
|
|
@@ -101,31 +111,28 @@
|
|
|
101
111
|
}).catch(() => { /* offline → cache stays, retry on next edit */ });
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
// Provider keys
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
// Provider keys · canonical state lives in keys-store.js (loaded as a
|
|
115
|
+
// module script before this file). All reads/writes go through that store;
|
|
116
|
+
// _keysMeta is a live accessor so the rest of this file needs no changes.
|
|
117
|
+
const _keysMeta = new Proxy({}, {
|
|
118
|
+
get(_, k) { return window.keysStore ? window.keysStore.keysMeta[k] : undefined; },
|
|
119
|
+
set(_, k, v) { if (window.keysStore) window.keysStore.keysMeta[k] = v; return true; },
|
|
120
|
+
ownKeys() { return window.keysStore ? Object.keys(window.keysStore.keysMeta) : []; },
|
|
121
|
+
has(_, k) { return window.keysStore ? k in window.keysStore.keysMeta : false; },
|
|
122
|
+
getOwnPropertyDescriptor(_, k) {
|
|
123
|
+
const v = window.keysStore ? window.keysStore.keysMeta[k] : undefined;
|
|
124
|
+
return v !== undefined ? { value: v, writable: true, enumerable: true, configurable: true } : undefined;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function fetchKeyMeta() {
|
|
129
|
+
return window.keysStore ? window.keysStore.fetchKeyMeta() : Promise.resolve();
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
// Sync read used by new-agent.js: returns a map { provider: truthy } where
|
|
122
133
|
// 'truthy' is the meta object so existence-check (`if (keys[p])`) still works.
|
|
123
134
|
function getKeys() {
|
|
124
|
-
|
|
125
|
-
for (const [p, m] of Object.entries(_keysMeta)) {
|
|
126
|
-
if (m && m.configured) out[p] = m;
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
135
|
+
return window.keysStore ? window.keysStore.getConfiguredKeys() : {};
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
// Available-models snapshot · shape from /api/models. We don't keep
|
|
@@ -163,21 +170,9 @@
|
|
|
163
170
|
// were removed in favour of the dedicated "Default Model" sidebar
|
|
164
171
|
// pane (single source of truth). Helpers deleted as dead code.
|
|
165
172
|
|
|
166
|
-
// Set / clear a single provider key
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const trimmed = (value || "").trim();
|
|
171
|
-
const r = await fetch("/api/keys/" + encodeURIComponent(provider), {
|
|
172
|
-
method: trimmed ? "PUT" : "DELETE",
|
|
173
|
-
headers: trimmed ? { "content-type": "application/json" } : undefined,
|
|
174
|
-
body: trimmed ? JSON.stringify({ key: trimmed }) : undefined
|
|
175
|
-
});
|
|
176
|
-
if (!r.ok) return null;
|
|
177
|
-
const meta = await r.json();
|
|
178
|
-
_keysMeta[provider] = meta;
|
|
179
|
-
return meta;
|
|
180
|
-
} catch (e) { return null; }
|
|
173
|
+
// Set / clear a single provider key — delegated to keys-store.js.
|
|
174
|
+
function setProviderKey(provider, value) {
|
|
175
|
+
return window.keysStore ? window.keysStore.setProviderKey(provider, value) : Promise.resolve(null);
|
|
181
176
|
}
|
|
182
177
|
|
|
183
178
|
// Public — other modules read provider configuration via this
|
|
@@ -191,24 +186,24 @@
|
|
|
191
186
|
const u = getUser();
|
|
192
187
|
return `
|
|
193
188
|
<div class="us-pane-head">
|
|
194
|
-
<div class="us-pane-tag"
|
|
195
|
-
<div class="us-pane-deck"
|
|
189
|
+
<div class="us-pane-tag">${tr("us_user_tag")}</div>
|
|
190
|
+
<div class="us-pane-deck">${tr("us_user_deck")}</div>
|
|
196
191
|
</div>
|
|
197
192
|
|
|
198
193
|
<div class="us-pane-body">
|
|
199
194
|
<div class="us-row">
|
|
200
|
-
<div class="us-row-label"
|
|
195
|
+
<div class="us-row-label">${tr("us_avatar")}</div>
|
|
201
196
|
<div class="us-row-field us-avatar-row">
|
|
202
197
|
<div class="us-avatar-frame" data-us-avatar></div>
|
|
203
198
|
<button type="button" class="us-mini-btn" data-us-regen-avatar>
|
|
204
199
|
<span class="us-mini-btn-mark">◆</span>
|
|
205
|
-
<span
|
|
200
|
+
<span>${tr("us_regen_avatar")}</span>
|
|
206
201
|
</button>
|
|
207
202
|
</div>
|
|
208
203
|
</div>
|
|
209
204
|
|
|
210
205
|
<div class="us-row">
|
|
211
|
-
<div class="us-row-label"
|
|
206
|
+
<div class="us-row-label">${tr("us_name")}</div>
|
|
212
207
|
<div class="us-row-field">
|
|
213
208
|
<div class="us-input-wrap">
|
|
214
209
|
<input type="text" class="us-input" data-us-name placeholder="Kay" maxlength="32" value="${escape(u.name || "")}">
|
|
@@ -217,12 +212,12 @@
|
|
|
217
212
|
</div>
|
|
218
213
|
|
|
219
214
|
<div class="us-row">
|
|
220
|
-
<div class="us-row-label"
|
|
215
|
+
<div class="us-row-label">${tr("us_about")}</div>
|
|
221
216
|
<div class="us-row-field">
|
|
222
217
|
<div class="us-input-wrap tall">
|
|
223
|
-
<textarea class="us-input" data-us-intro maxlength="320" placeholder="
|
|
218
|
+
<textarea class="us-input" data-us-intro maxlength="320" placeholder="${escape(tr("us_intro_ph"))}">${escape(u.intro || "")}</textarea>
|
|
224
219
|
</div>
|
|
225
|
-
<div class="us-row-meta"><span data-us-intro-count>0</span
|
|
220
|
+
<div class="us-row-meta"><span data-us-intro-count>0</span><span>${tr("us_intro_meta_rest")}</span></div>
|
|
226
221
|
</div>
|
|
227
222
|
</div>
|
|
228
223
|
|
|
@@ -235,8 +230,8 @@
|
|
|
235
230
|
const swatchSpans = (cs) => cs.map((c) => `<span style="background:${c}"></span>`).join("");
|
|
236
231
|
return `
|
|
237
232
|
<div class="us-pane-head">
|
|
238
|
-
<div class="us-pane-tag"
|
|
239
|
-
<div class="us-pane-deck"
|
|
233
|
+
<div class="us-pane-tag">${tr("us_theme_tag")}</div>
|
|
234
|
+
<div class="us-pane-deck">${tr("us_theme_deck")}</div>
|
|
240
235
|
</div>
|
|
241
236
|
|
|
242
237
|
<div class="us-pane-body">
|
|
@@ -257,9 +252,8 @@
|
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
/* ── Other settings · misc per-user toggles that don't fit a
|
|
260
|
-
dedicated section.
|
|
261
|
-
natural home for future small ambient / UX preferences
|
|
262
|
-
cues, animations, etc.) without growing the nav for each one. */
|
|
255
|
+
dedicated section. Interface language · typing-sound effect;
|
|
256
|
+
natural home for future small ambient / UX preferences. */
|
|
263
257
|
function otherSettingsSectionHTML() {
|
|
264
258
|
return `
|
|
265
259
|
<div class="us-pane-head">
|
|
@@ -268,6 +262,18 @@
|
|
|
268
262
|
</div>
|
|
269
263
|
|
|
270
264
|
<div class="us-pane-body">
|
|
265
|
+
<div class="us-row">
|
|
266
|
+
<div class="us-row-label">${tr("us_locale_label")}</div>
|
|
267
|
+
<div class="us-row-field">
|
|
268
|
+
<button type="button" class="cmp-dd" data-cmp-dropdown="locale" title="${escape(tr("us_locale_label"))}" data-i18n-aria="aria_language" aria-label="">
|
|
269
|
+
<span class="cmp-dd-label" data-i18n="us_locale_label">${escape(tr("us_locale_label"))}</span>
|
|
270
|
+
<span class="cmp-dd-value" data-cmp-dd-value="locale">${escape(tr(window.I18n && window.I18n.getLocale && window.I18n.getLocale() === "zh" ? "locale_zh" : "locale_en"))}</span>
|
|
271
|
+
<span class="cmp-dd-chevron">▾</span>
|
|
272
|
+
</button>
|
|
273
|
+
<p class="us-locale-deck">${escape(tr("us_locale_deck"))}</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
271
277
|
<div class="us-row">
|
|
272
278
|
<div class="us-row-label">Typing sound</div>
|
|
273
279
|
<div class="us-row-field">
|
|
@@ -288,6 +294,12 @@
|
|
|
288
294
|
|
|
289
295
|
function wireOtherSettingsSection() {
|
|
290
296
|
if (!paneEl) return;
|
|
297
|
+
if (window.I18n && typeof window.I18n.applyDom === "function") {
|
|
298
|
+
window.I18n.applyDom(paneEl);
|
|
299
|
+
}
|
|
300
|
+
if (window.I18n && typeof window.I18n.syncLocaleControls === "function") {
|
|
301
|
+
window.I18n.syncLocaleControls();
|
|
302
|
+
}
|
|
291
303
|
// Typing-sound toggle · the persistence + audio context lives in
|
|
292
304
|
// window.boardroomTypingSfx (typing-sfx.js); this row only mirrors
|
|
293
305
|
// the current state and proxies clicks. Reading inside wire-up
|
|
@@ -339,13 +351,13 @@
|
|
|
339
351
|
function usageSectionHTML() {
|
|
340
352
|
return `
|
|
341
353
|
<div class="us-pane-head">
|
|
342
|
-
<div class="us-pane-tag"
|
|
343
|
-
<div class="us-pane-deck"
|
|
354
|
+
<div class="us-pane-tag">${tr("us_usage_tag")}</div>
|
|
355
|
+
<div class="us-pane-deck">${tr("us_usage_deck")}</div>
|
|
344
356
|
</div>
|
|
345
357
|
|
|
346
358
|
<div class="us-pane-body">
|
|
347
359
|
<div class="us-usage" data-usage-pane>
|
|
348
|
-
<div class="us-usage-loading"
|
|
360
|
+
<div class="us-usage-loading">${tr("us_usage_loading")}</div>
|
|
349
361
|
</div>
|
|
350
362
|
</div>
|
|
351
363
|
`;
|
|
@@ -378,7 +390,7 @@
|
|
|
378
390
|
pane.innerHTML = `
|
|
379
391
|
<div class="us-usage-empty">
|
|
380
392
|
<div class="us-usage-empty-num">0</div>
|
|
381
|
-
<div class="us-usage-empty-text"
|
|
393
|
+
<div class="us-usage-empty-text">${tr("us_usage_empty")}</div>
|
|
382
394
|
</div>
|
|
383
395
|
`;
|
|
384
396
|
return;
|
|
@@ -539,7 +551,7 @@
|
|
|
539
551
|
<div class="us-model-stats">
|
|
540
552
|
<span class="us-model-tokens">${fmtTokens(m.tokens)}</span>
|
|
541
553
|
<span class="us-model-pct">${pct.toFixed(1)}%</span>
|
|
542
|
-
<span class="us-model-agents">${
|
|
554
|
+
<span class="us-model-agents">${tr("us_usage_agents", { n: m.agents })}</span>
|
|
543
555
|
</div>
|
|
544
556
|
</div>
|
|
545
557
|
`;
|
|
@@ -549,7 +561,7 @@
|
|
|
549
561
|
const agentRows = topAgents.map((a) => {
|
|
550
562
|
const pct = (a.tokens / total) * 100;
|
|
551
563
|
const color = PROVIDER_COLOR_VAR[a.provider] || PROVIDER_COLOR_VAR.unknown;
|
|
552
|
-
const role = a.roleKind === "moderator" ? "
|
|
564
|
+
const role = a.roleKind === "moderator" ? tr("us_usage_chair") : tr("us_usage_director");
|
|
553
565
|
return `
|
|
554
566
|
<div class="us-agent-row">
|
|
555
567
|
<div class="us-agent-name-col">
|
|
@@ -567,7 +579,7 @@
|
|
|
567
579
|
|
|
568
580
|
const silentCount = byAgent.length - topAgents.length;
|
|
569
581
|
const silentNote = silentCount > 0
|
|
570
|
-
? `<div class="us-agent-silent"
|
|
582
|
+
? `<div class="us-agent-silent">${tr("us_usage_silent", { n: silentCount })}</div>`
|
|
571
583
|
: "";
|
|
572
584
|
|
|
573
585
|
const retiredNote = retired.tokens > 0
|
|
@@ -660,6 +672,7 @@
|
|
|
660
672
|
// "+ add provider" flow).
|
|
661
673
|
const LLM_PROVIDER_IDS = PROVIDERS.filter((p) => p.group === "llm").map((p) => p.id);
|
|
662
674
|
const SKILL_PROVIDER_IDS = PROVIDERS.filter((p) => p.group === "skill").map((p) => p.id);
|
|
675
|
+
const VOICE_PROVIDER_IDS = PROVIDERS.filter((p) => p.group === "voice").map((p) => p.id);
|
|
663
676
|
|
|
664
677
|
function ensureActiveProviders() {
|
|
665
678
|
if (activeProviders === null) {
|
|
@@ -700,7 +713,23 @@
|
|
|
700
713
|
<div class="us-key-head">
|
|
701
714
|
<div class="us-key-label">${escape(p.label)}</div>
|
|
702
715
|
<div class="us-key-status ${has ? "on" : "off"}" data-status>${has ? "● configured" : "○ not set"}</div>
|
|
703
|
-
${
|
|
716
|
+
${(() => {
|
|
717
|
+
if (!removable) return "";
|
|
718
|
+
// Last-LLM guardrail · matches the server's DELETE check.
|
|
719
|
+
// Block removal of the ONE configured LLM key so the boardroom
|
|
720
|
+
// never lands in "no usable carrier" state. Unconfigured rows
|
|
721
|
+
// (or non-LLM providers) bypass the lock — removing them
|
|
722
|
+
// doesn't reduce working-key count.
|
|
723
|
+
const isLLM = p.group === "llm";
|
|
724
|
+
const isConfigured = !!(_keysMeta[p.id] && _keysMeta[p.id].configured);
|
|
725
|
+
const llmConfiguredCount = LLM_PROVIDER_IDS.filter(
|
|
726
|
+
(id) => _keysMeta[id] && _keysMeta[id].configured,
|
|
727
|
+
).length;
|
|
728
|
+
const lock = isLLM && isConfigured && llmConfiguredCount <= 1;
|
|
729
|
+
return lock
|
|
730
|
+
? `<button type="button" class="us-key-remove is-locked" disabled title="Add another LLM key first — at least one must remain configured.">✕</button>`
|
|
731
|
+
: `<button type="button" class="us-key-remove" data-remove-provider="${p.id}" title="Remove">✕</button>`;
|
|
732
|
+
})()}
|
|
704
733
|
</div>
|
|
705
734
|
<div class="us-key-hint">${escape(p.hint)}</div>
|
|
706
735
|
<div class="us-input-wrap">
|
|
@@ -722,6 +751,52 @@
|
|
|
722
751
|
`;
|
|
723
752
|
}
|
|
724
753
|
|
|
754
|
+
/** Shown only when BOTH Brave Search and Tavily keys are configured. */
|
|
755
|
+
function webSearchBackendPrefHTML() {
|
|
756
|
+
const braveOk = !!(_keysMeta.brave && _keysMeta.brave.configured);
|
|
757
|
+
const tavilyOk = !!(_keysMeta.tavily && _keysMeta.tavily.configured);
|
|
758
|
+
const visible = braveOk && tavilyOk;
|
|
759
|
+
const pref = _prefsCache.webSearchProvider === "tavily" ? "tavily" : "brave";
|
|
760
|
+
return `
|
|
761
|
+
<div class="us-key-group us-key-group-ws-backend" data-us-ws-backend-wrap ${visible ? "" : "hidden"}>
|
|
762
|
+
<div class="us-key-group-tag">${tr("us_ws_backend_tag")}</div>
|
|
763
|
+
<div class="us-key-group-deck">${tr("us_ws_backend_deck")}</div>
|
|
764
|
+
<div class="us-ws-backend-radios">
|
|
765
|
+
<label class="us-ws-backend-label">
|
|
766
|
+
<input type="radio" name="us-ws-backend" value="brave" ${pref === "brave" ? "checked" : ""}>
|
|
767
|
+
<span>${tr("us_ws_backend_brave")}</span>
|
|
768
|
+
</label>
|
|
769
|
+
<label class="us-ws-backend-label">
|
|
770
|
+
<input type="radio" name="us-ws-backend" value="tavily" ${pref === "tavily" ? "checked" : ""}>
|
|
771
|
+
<span>${tr("us_ws_backend_tavily")}</span>
|
|
772
|
+
</label>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function minimaxRegionPrefHTML() {
|
|
779
|
+
const minimaxOk = !!(_keysMeta.minimax && _keysMeta.minimax.configured);
|
|
780
|
+
if (!minimaxOk) return "";
|
|
781
|
+
const region = (_prefsCache && _prefsCache.minimaxRegion) || "cn";
|
|
782
|
+
return `
|
|
783
|
+
<div class="us-key-group us-key-group-minimax-region" data-us-minimax-region-wrap>
|
|
784
|
+
<div class="us-key-group-tag">MiniMax API region</div>
|
|
785
|
+
<div class="us-key-group-deck">Select the region matching your API key source.</div>
|
|
786
|
+
<div class="us-ws-backend-radios">
|
|
787
|
+
<label class="us-ws-backend-label">
|
|
788
|
+
<input type="radio" name="us-minimax-region" value="cn" ${region === "cn" ? "checked" : ""}>
|
|
789
|
+
<span>China (api.minimaxi.com)</span>
|
|
790
|
+
</label>
|
|
791
|
+
<label class="us-ws-backend-label">
|
|
792
|
+
<input type="radio" name="us-minimax-region" value="intl" ${region === "intl" ? "checked" : ""}>
|
|
793
|
+
<span>International (api.minimax.io)</span>
|
|
794
|
+
</label>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
`;
|
|
798
|
+
}
|
|
799
|
+
|
|
725
800
|
function keysSectionHTML() {
|
|
726
801
|
ensureActiveProviders();
|
|
727
802
|
// Anthropic is temporarily excluded from the "+ add provider"
|
|
@@ -736,17 +811,18 @@
|
|
|
736
811
|
(p) => p.group === "llm" && !HIDDEN_FROM_ADD.has(p.id) && !activeProviders.includes(p.id),
|
|
737
812
|
);
|
|
738
813
|
const skillProviders = PROVIDERS.filter((p) => p.group === "skill");
|
|
814
|
+
const voiceProviders = PROVIDERS.filter((p) => p.group === "voice");
|
|
739
815
|
|
|
740
816
|
return `
|
|
741
817
|
<div class="us-pane-head">
|
|
742
|
-
<div class="us-pane-tag"
|
|
743
|
-
<div class="us-pane-deck"
|
|
818
|
+
<div class="us-pane-tag">${tr("us_keys_tag")}</div>
|
|
819
|
+
<div class="us-pane-deck">${tr("us_keys_deck")}</div>
|
|
744
820
|
</div>
|
|
745
821
|
|
|
746
822
|
<div class="us-pane-body">
|
|
747
823
|
|
|
748
824
|
<div class="us-key-group">
|
|
749
|
-
<div class="us-key-group-tag"
|
|
825
|
+
<div class="us-key-group-tag">${tr("us_keys_group_llm")}</div>
|
|
750
826
|
${activeProviders.map((id) => {
|
|
751
827
|
const p = PROVIDERS.find((x) => x.id === id);
|
|
752
828
|
if (!p) return "";
|
|
@@ -754,7 +830,7 @@
|
|
|
754
830
|
}).join("")}
|
|
755
831
|
${addable.length > 0 ? `
|
|
756
832
|
<div class="us-key-add">
|
|
757
|
-
<span class="us-key-add-label"
|
|
833
|
+
<span class="us-key-add-label">${tr("us_keys_add_label")}</span>
|
|
758
834
|
<div class="us-key-add-chips">
|
|
759
835
|
${addable.map((p) => `
|
|
760
836
|
<button type="button" class="us-key-add-chip" data-add-provider="${p.id}">${escape(p.label)}</button>
|
|
@@ -766,12 +842,24 @@
|
|
|
766
842
|
|
|
767
843
|
${skillProviders.length > 0 ? `
|
|
768
844
|
<div class="us-key-group us-key-group-skill">
|
|
769
|
-
<div class="us-key-group-tag"
|
|
770
|
-
<div class="us-key-group-deck"
|
|
845
|
+
<div class="us-key-group-tag">${tr("us_keys_group_skill")}</div>
|
|
846
|
+
<div class="us-key-group-deck">${tr("us_keys_skill_deck")}</div>
|
|
771
847
|
${skillProviders.map((p) => renderKeyRow(p, !!(_keysMeta[p.id] && _keysMeta[p.id].configured))).join("")}
|
|
772
848
|
</div>
|
|
773
849
|
` : ""}
|
|
774
850
|
|
|
851
|
+
${webSearchBackendPrefHTML()}
|
|
852
|
+
|
|
853
|
+
${voiceProviders.length > 0 ? `
|
|
854
|
+
<div class="us-key-group us-key-group-voice">
|
|
855
|
+
<div class="us-key-group-tag">Voice providers</div>
|
|
856
|
+
<div class="us-key-group-deck">Used for voice meetings and per-director speech synthesis.</div>
|
|
857
|
+
${voiceProviders.map((p) => renderKeyRow(p, !!(_keysMeta[p.id] && _keysMeta[p.id].configured))).join("")}
|
|
858
|
+
</div>
|
|
859
|
+
` : ""}
|
|
860
|
+
|
|
861
|
+
${minimaxRegionPrefHTML()}
|
|
862
|
+
|
|
775
863
|
<div data-models-summary>${modelsSummaryHTML()}</div>
|
|
776
864
|
|
|
777
865
|
</div>
|
|
@@ -922,8 +1010,8 @@
|
|
|
922
1010
|
|
|
923
1011
|
return `
|
|
924
1012
|
<div class="us-pane-head">
|
|
925
|
-
<div class="us-pane-tag"
|
|
926
|
-
<div class="us-pane-deck"
|
|
1013
|
+
<div class="us-pane-tag">${tr("us_default_tag")}</div>
|
|
1014
|
+
<div class="us-pane-deck">${tr("us_default_deck_long")}</div>
|
|
927
1015
|
</div>
|
|
928
1016
|
|
|
929
1017
|
<div class="us-pane-body">
|
|
@@ -984,20 +1072,20 @@
|
|
|
984
1072
|
<div class="user-settings-modal" role="document">
|
|
985
1073
|
|
|
986
1074
|
<div class="us-classification">
|
|
987
|
-
<span><span class="dot">●</span>
|
|
988
|
-
<span class="right"
|
|
1075
|
+
<span><span class="dot">●</span> <span data-i18n="us_modal_kicker_left"></span></span>
|
|
1076
|
+
<span class="right" data-i18n="us_modal_kicker_right"></span>
|
|
989
1077
|
</div>
|
|
990
1078
|
|
|
991
|
-
<button type="button" class="us-close" aria-label="Close">✕</button>
|
|
1079
|
+
<button type="button" class="us-close" data-i18n-aria="us_close" aria-label="Close">✕</button>
|
|
992
1080
|
|
|
993
1081
|
<div class="us-frame">
|
|
994
1082
|
<nav class="us-nav" role="tablist">
|
|
995
|
-
<a href="#" class="us-nav-item active" data-section="user" role="tab" aria-selected="true"
|
|
996
|
-
<a href="#" class="us-nav-item" data-section="theme" role="tab" aria-selected="false"
|
|
997
|
-
<a href="#" class="us-nav-item" data-section="usage" role="tab" aria-selected="false"
|
|
998
|
-
<a href="#" class="us-nav-item" data-section="keys" role="tab" aria-selected="false"
|
|
999
|
-
<a href="#" class="us-nav-item" data-section="default" role="tab" aria-selected="false"
|
|
1000
|
-
<a href="#" class="us-nav-item" data-section="other" role="tab" aria-selected="false"
|
|
1083
|
+
<a href="#" class="us-nav-item active" data-section="user" role="tab" aria-selected="true" data-i18n="us_nav_user"></a>
|
|
1084
|
+
<a href="#" class="us-nav-item" data-section="theme" role="tab" aria-selected="false" data-i18n="us_nav_theme"></a>
|
|
1085
|
+
<a href="#" class="us-nav-item" data-section="usage" role="tab" aria-selected="false" data-i18n="us_nav_usage"></a>
|
|
1086
|
+
<a href="#" class="us-nav-item" data-section="keys" role="tab" aria-selected="false" data-i18n="us_nav_api_key"></a>
|
|
1087
|
+
<a href="#" class="us-nav-item" data-section="default" role="tab" aria-selected="false" data-i18n="us_default_model_title"></a>
|
|
1088
|
+
<a href="#" class="us-nav-item" data-section="other" role="tab" aria-selected="false" data-i18n="us_nav_other_settings"></a>
|
|
1001
1089
|
<div class="us-nav-foot" data-us-version aria-label="App version">
|
|
1002
1090
|
<span class="us-nav-foot-label">version</span>
|
|
1003
1091
|
<span class="us-nav-foot-value" data-us-version-value>·</span>
|
|
@@ -1008,10 +1096,10 @@
|
|
|
1008
1096
|
</div>
|
|
1009
1097
|
|
|
1010
1098
|
<footer class="us-foot">
|
|
1011
|
-
<span class="saved"
|
|
1099
|
+
<span class="saved" data-i18n="us_foot_saved"></span>
|
|
1012
1100
|
<div class="us-foot-right">
|
|
1013
|
-
<a class="us-website" href="/home.html" target="_blank" rel="noopener"
|
|
1014
|
-
<button type="button" class="us-done"
|
|
1101
|
+
<a class="us-website" href="/home.html" target="_blank" rel="noopener" data-i18n="us_foot_website"></a>
|
|
1102
|
+
<button type="button" class="us-done" data-i18n="us_done"></button>
|
|
1015
1103
|
</div>
|
|
1016
1104
|
</footer>
|
|
1017
1105
|
|
|
@@ -1194,6 +1282,7 @@
|
|
|
1194
1282
|
await setProviderKey(provider, v);
|
|
1195
1283
|
await refreshModels();
|
|
1196
1284
|
refreshModelsSummary();
|
|
1285
|
+
syncWsBackendPicker();
|
|
1197
1286
|
}, 220);
|
|
1198
1287
|
debounceMap.set(row, timer);
|
|
1199
1288
|
}
|
|
@@ -1210,6 +1299,50 @@
|
|
|
1210
1299
|
});
|
|
1211
1300
|
});
|
|
1212
1301
|
|
|
1302
|
+
function syncWsBackendPicker() {
|
|
1303
|
+
const wrap = paneEl.querySelector("[data-us-ws-backend-wrap]");
|
|
1304
|
+
if (!wrap) return;
|
|
1305
|
+
const braveOk = !!(_keysMeta.brave && _keysMeta.brave.configured);
|
|
1306
|
+
const tavilyOk = !!(_keysMeta.tavily && _keysMeta.tavily.configured);
|
|
1307
|
+
wrap.hidden = !(braveOk && tavilyOk);
|
|
1308
|
+
const pref = _prefsCache.webSearchProvider === "tavily" ? "tavily" : "brave";
|
|
1309
|
+
wrap.querySelectorAll("input[name=\"us-ws-backend\"]").forEach((inp) => {
|
|
1310
|
+
inp.checked = inp.value === pref;
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
paneEl.querySelectorAll("input[name=us-ws-backend]").forEach((input) => {
|
|
1315
|
+
input.addEventListener("change", async () => {
|
|
1316
|
+
if (!input.checked) return;
|
|
1317
|
+
const v = input.value === "tavily" ? "tavily" : "brave";
|
|
1318
|
+
_prefsCache = { ..._prefsCache, webSearchProvider: v };
|
|
1319
|
+
try {
|
|
1320
|
+
await fetch("/api/prefs", {
|
|
1321
|
+
method: "PUT",
|
|
1322
|
+
headers: { "content-type": "application/json" },
|
|
1323
|
+
body: JSON.stringify({ webSearchProvider: v }),
|
|
1324
|
+
});
|
|
1325
|
+
} catch (_) { /* offline · prefs stay in _prefsCache */ }
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
paneEl.querySelectorAll("input[name=us-minimax-region]").forEach((input) => {
|
|
1330
|
+
input.addEventListener("change", async () => {
|
|
1331
|
+
if (!input.checked) return;
|
|
1332
|
+
const v = input.value === "intl" ? "intl" : "cn";
|
|
1333
|
+
_prefsCache = { ..._prefsCache, minimaxRegion: v };
|
|
1334
|
+
try {
|
|
1335
|
+
await fetch("/api/prefs", {
|
|
1336
|
+
method: "PUT",
|
|
1337
|
+
headers: { "content-type": "application/json" },
|
|
1338
|
+
body: JSON.stringify({ minimaxRegion: v }),
|
|
1339
|
+
});
|
|
1340
|
+
} catch (_) { /* offline */ }
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
syncWsBackendPicker();
|
|
1345
|
+
|
|
1213
1346
|
// First render of the section · the shared cache may already
|
|
1214
1347
|
// have a snapshot (models-cache.js fetches at module load). If
|
|
1215
1348
|
// not, kick off a refresh and update the summary when it lands.
|
|
@@ -1282,6 +1415,7 @@
|
|
|
1282
1415
|
input.classList.toggle("has-preview", has);
|
|
1283
1416
|
}
|
|
1284
1417
|
});
|
|
1418
|
+
syncWsBackendPicker();
|
|
1285
1419
|
});
|
|
1286
1420
|
}
|
|
1287
1421
|
|
|
@@ -1322,21 +1456,33 @@
|
|
|
1322
1456
|
}
|
|
1323
1457
|
} catch { /* swallow · the foot just stays at "·" if offline */ }
|
|
1324
1458
|
}
|
|
1325
|
-
function close() {
|
|
1459
|
+
async function close() {
|
|
1326
1460
|
if (!overlay) return;
|
|
1327
1461
|
overlay.classList.remove("open");
|
|
1328
1462
|
overlay.setAttribute("aria-hidden", "true");
|
|
1329
1463
|
document.body.style.overflow = "";
|
|
1330
|
-
// Sync app.keys with whatever the user just
|
|
1331
|
-
// requireModelKey gate
|
|
1332
|
-
//
|
|
1464
|
+
// Sync app.keys + app.agentsById with whatever the user just
|
|
1465
|
+
// configured · the requireModelKey gate, hasAnyVoiceKey, and the
|
|
1466
|
+
// agent profile's voice block all read from these caches. Both
|
|
1467
|
+
// refreshes must complete BEFORE we fire refreshAgentProfileSkills
|
|
1468
|
+
// — otherwise the re-render reads stale state and the locked
|
|
1469
|
+
// voice card persists until the next page reload, and any voices
|
|
1470
|
+
// auto-assigned server-side on a 0→1 voice-key transition won't
|
|
1471
|
+
// surface until then either. Run the two fetches in parallel.
|
|
1472
|
+
const ops = [];
|
|
1333
1473
|
if (window.app && typeof window.app.refreshKeys === "function") {
|
|
1334
|
-
window.app.refreshKeys();
|
|
1474
|
+
ops.push(window.app.refreshKeys());
|
|
1475
|
+
}
|
|
1476
|
+
if (window.app && typeof window.app.refreshAgents === "function") {
|
|
1477
|
+
ops.push(window.app.refreshAgents());
|
|
1335
1478
|
}
|
|
1479
|
+
try { await Promise.all(ops); } catch { /* swallow · stale state is recoverable */ }
|
|
1480
|
+
|
|
1336
1481
|
// If an agent profile is open, its skill rows have data-key-
|
|
1337
|
-
// configured cached from first paint
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1482
|
+
// configured cached from first paint and the voice section may
|
|
1483
|
+
// be showing the locked card. Re-fetch / re-render so both the
|
|
1484
|
+
// web-search toggle and the voice picker reflect the new state
|
|
1485
|
+
// immediately, no manual refresh required.
|
|
1340
1486
|
if (typeof window.refreshAgentProfileSkills === "function") {
|
|
1341
1487
|
window.refreshAgentProfileSkills();
|
|
1342
1488
|
}
|
|
@@ -1350,8 +1496,16 @@
|
|
|
1350
1496
|
overlay = document.getElementById("user-settings-overlay");
|
|
1351
1497
|
modal = overlay.querySelector(".user-settings-modal");
|
|
1352
1498
|
paneEl = modal.querySelector("[data-us-pane]");
|
|
1499
|
+
if (window.I18n && typeof window.I18n.applyDom === "function") {
|
|
1500
|
+
window.I18n.applyDom(overlay);
|
|
1501
|
+
}
|
|
1353
1502
|
|
|
1354
|
-
|
|
1503
|
+
document.addEventListener("boardroom:locale", () => {
|
|
1504
|
+
if (overlay && window.I18n && typeof window.I18n.applyDom === "function") {
|
|
1505
|
+
window.I18n.applyDom(overlay);
|
|
1506
|
+
}
|
|
1507
|
+
if (overlay && overlay.classList.contains("open")) renderSection(currentSection);
|
|
1508
|
+
});
|
|
1355
1509
|
overlay.querySelector(".us-close").addEventListener("click", close);
|
|
1356
1510
|
modal.querySelector(".us-done").addEventListener("click", (e) => { e.preventDefault(); close(); });
|
|
1357
1511
|
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|