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/agent-profile.js
CHANGED
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
*/
|
|
12
12
|
(function () {
|
|
13
13
|
|
|
14
|
+
function uiT(key, vars) {
|
|
15
|
+
return (window.I18n && window.I18n.t(key, vars)) || key;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function profileRoleLabel(p) {
|
|
19
|
+
const r = (p.role || "").trim();
|
|
20
|
+
if (!r) return uiT("ap_role_director_upper");
|
|
21
|
+
const low = r.toLowerCase();
|
|
22
|
+
if (low === "moderator") return uiT("agent_role_tag_moderator");
|
|
23
|
+
if (low === "director") return uiT("ap_role_director_upper");
|
|
24
|
+
return r;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function profileStatusLabel(p) {
|
|
28
|
+
if (p.status === "intern") return uiT("ap_status_intern");
|
|
29
|
+
if (p.status === "active" || !p.status) return uiT("ap_status_active");
|
|
30
|
+
return String(p.status).toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
const PROFILES = {
|
|
15
34
|
|
|
16
35
|
/* ════════════════════════════════════ SOCRATES ════════════════════════════════════ */
|
|
@@ -585,6 +604,7 @@
|
|
|
585
604
|
// /api/agents record (via window.app.agentsById) and resolves it here.
|
|
586
605
|
const MODEL_LABELS = {
|
|
587
606
|
"sonnet-4-6": { name: "Sonnet 4.6", deck: "balanced · default" },
|
|
607
|
+
"opus-4-6": { name: "Opus 4.6", deck: "deep reasoning · 1M ctx" },
|
|
588
608
|
"opus-4-7": { name: "Opus 4.7", deck: "deep reasoning" },
|
|
589
609
|
"opus-4-6": { name: "Opus 4.6", deck: "prior-gen flagship" },
|
|
590
610
|
"opus-4-6-fast": { name: "Opus 4.6 Fast", deck: "faster 4.6 · same intelligence" },
|
|
@@ -597,6 +617,11 @@
|
|
|
597
617
|
"gemini-3-1-flash": { name: "Gemini 3.1 Flash Lite", deck: "fast · 1M ctx" },
|
|
598
618
|
"grok-4-3": { name: "Grok 4.3", deck: "flagship · 1M ctx" },
|
|
599
619
|
"grok-4-1-fast": { name: "Grok 4.1 Fast", deck: "fast · 256k ctx" },
|
|
620
|
+
"grok-4-20": { name: "Grok 4.20", deck: "2M ctx · big context" },
|
|
621
|
+
"gpt-5-5-pro": { name: "GPT-5.5 Pro", deck: "deep reasoning · 1M ctx" },
|
|
622
|
+
"codex-5-4": { name: "ChatGPT Codex 5.4", deck: "code · agents" },
|
|
623
|
+
"deepseek-v4-pro": { name: "DeepSeek V4 Pro", deck: "reasoning · open weights" },
|
|
624
|
+
"deepseek-v4-flash": { name: "DeepSeek Lite", deck: "V4 Flash · fast · 1M ctx" },
|
|
600
625
|
};
|
|
601
626
|
|
|
602
627
|
function liveModelFor(slug) {
|
|
@@ -901,7 +926,7 @@
|
|
|
901
926
|
const bio = bioFor(slug, p);
|
|
902
927
|
block.innerHTML = `
|
|
903
928
|
<div class="ap-intel-view" data-ap-intel-view>${
|
|
904
|
-
escape(bio) || `<span class="ap-empty"
|
|
929
|
+
escape(bio) || `<span class="ap-empty">${escape(uiT("ap_intel_empty"))}</span>`
|
|
905
930
|
}</div>
|
|
906
931
|
`;
|
|
907
932
|
}
|
|
@@ -912,12 +937,12 @@
|
|
|
912
937
|
const bio = bioFor(slug, p);
|
|
913
938
|
block.innerHTML = `
|
|
914
939
|
<div class="ap-intel-edit">
|
|
915
|
-
<textarea class="ap-intel-textarea" data-ap-intel-textarea spellcheck="false" maxlength="${BIO_MAX}" placeholder="
|
|
940
|
+
<textarea class="ap-intel-textarea" data-ap-intel-textarea spellcheck="false" maxlength="${BIO_MAX}" placeholder="${escape(uiT("ap_intel_placeholder", { min: BIO_MIN, max: BIO_MAX }))}">${escape(bio)}</textarea>
|
|
916
941
|
<div class="ap-intel-edit-foot">
|
|
917
|
-
<span class="ap-intel-edit-hint" data-ap-intel-hint>${
|
|
942
|
+
<span class="ap-intel-edit-hint" data-ap-intel-hint>${escape(uiT("ap_intel_hint", { min: BIO_MIN, max: BIO_MAX }))}</span>
|
|
918
943
|
<div class="ap-intel-edit-actions">
|
|
919
|
-
<button type="button" class="ap-instr-cancel" data-ap-intel-cancel
|
|
920
|
-
<button type="button" class="ap-instr-save" data-ap-intel-save
|
|
944
|
+
<button type="button" class="ap-instr-cancel" data-ap-intel-cancel>${escape(uiT("ap_cancel"))}</button>
|
|
945
|
+
<button type="button" class="ap-instr-save" data-ap-intel-save>${escape(uiT("ap_save"))}</button>
|
|
921
946
|
</div>
|
|
922
947
|
</div>
|
|
923
948
|
</div>
|
|
@@ -965,9 +990,9 @@
|
|
|
965
990
|
return `
|
|
966
991
|
<div class="ap-instr" data-ap-instr data-slug="${escape(slug)}">
|
|
967
992
|
<div class="ap-instr-view" data-ap-instr-view>
|
|
968
|
-
${rendered || `<div class="ap-empty"
|
|
993
|
+
${rendered || `<div class="ap-empty">${escape(uiT("ap_instr_empty"))}</div>`}
|
|
969
994
|
</div>
|
|
970
|
-
<button type="button" class="ap-instr-toggle" data-ap-instr-toggle aria-expanded="false"
|
|
995
|
+
<button type="button" class="ap-instr-toggle" data-ap-instr-toggle aria-expanded="false">${escape(uiT("ap_show_more"))}</button>
|
|
971
996
|
</div>
|
|
972
997
|
`;
|
|
973
998
|
}
|
|
@@ -978,9 +1003,9 @@
|
|
|
978
1003
|
block.classList.remove("overflowing");
|
|
979
1004
|
block.innerHTML = `
|
|
980
1005
|
<div class="ap-instr-view" data-ap-instr-view>
|
|
981
|
-
${renderMarkdown(instructionFor(slug, p)) || `<div class="ap-empty"
|
|
1006
|
+
${renderMarkdown(instructionFor(slug, p)) || `<div class="ap-empty">${escape(uiT("ap_instr_empty"))}</div>`}
|
|
982
1007
|
</div>
|
|
983
|
-
<button type="button" class="ap-instr-toggle" data-ap-instr-toggle aria-expanded="false"
|
|
1008
|
+
<button type="button" class="ap-instr-toggle" data-ap-instr-toggle aria-expanded="false">${escape(uiT("ap_show_more"))}</button>
|
|
984
1009
|
`;
|
|
985
1010
|
evaluateInstructionOverflow(slug);
|
|
986
1011
|
}
|
|
@@ -1000,7 +1025,7 @@
|
|
|
1000
1025
|
// 'expanded' state from a prior interaction shadowing the check.
|
|
1001
1026
|
view.classList.remove("expanded");
|
|
1002
1027
|
toggle.setAttribute("aria-expanded", "false");
|
|
1003
|
-
toggle.textContent = "
|
|
1028
|
+
toggle.textContent = uiT("ap_show_more");
|
|
1004
1029
|
// scrollHeight is the full content; clientHeight is the rendered
|
|
1005
1030
|
// (capped) height. A few-pixel epsilon avoids flagging content
|
|
1006
1031
|
// that fits exactly at the cap as "overflowing".
|
|
@@ -1029,12 +1054,12 @@
|
|
|
1029
1054
|
const md = instructionFor(slug, p);
|
|
1030
1055
|
block.innerHTML = `
|
|
1031
1056
|
<div class="ap-instr-edit">
|
|
1032
|
-
<textarea class="ap-instr-textarea" data-ap-instr-textarea spellcheck="false" placeholder="
|
|
1057
|
+
<textarea class="ap-instr-textarea" data-ap-instr-textarea spellcheck="false" placeholder="${escape(uiT("ap_instr_placeholder_editor"))}">${escape(md)}</textarea>
|
|
1033
1058
|
<div class="ap-instr-edit-foot">
|
|
1034
|
-
<span class="ap-instr-edit-hint"
|
|
1059
|
+
<span class="ap-instr-edit-hint">${escape(uiT("ap_instr_edit_hint"))}</span>
|
|
1035
1060
|
<div class="ap-instr-edit-actions">
|
|
1036
|
-
<button type="button" class="ap-instr-cancel" data-ap-instr-cancel
|
|
1037
|
-
<button type="button" class="ap-instr-save" data-ap-instr-save
|
|
1061
|
+
<button type="button" class="ap-instr-cancel" data-ap-instr-cancel>${escape(uiT("ap_cancel"))}</button>
|
|
1062
|
+
<button type="button" class="ap-instr-save" data-ap-instr-save>${escape(uiT("ap_save"))}</button>
|
|
1038
1063
|
</div>
|
|
1039
1064
|
</div>
|
|
1040
1065
|
</div>
|
|
@@ -1057,7 +1082,7 @@
|
|
|
1057
1082
|
function renderRulesInner(slug) {
|
|
1058
1083
|
const rules = rulesForAgent(slug);
|
|
1059
1084
|
const list = rules.length === 0
|
|
1060
|
-
? `<li class="ap-rule-empty"
|
|
1085
|
+
? `<li class="ap-rule-empty">${escape(uiT("ap_rules_empty_list"))}</li>`
|
|
1061
1086
|
: rules.map((body, i) => `
|
|
1062
1087
|
<li class="ap-rule" data-rule-idx="${i}">
|
|
1063
1088
|
<span class="ap-rule-num">${i + 1}</span>
|
|
@@ -1477,7 +1502,7 @@
|
|
|
1477
1502
|
.join(" ");
|
|
1478
1503
|
const titleText = keyOk
|
|
1479
1504
|
? (enabled ? "Disable Web Search for this director" : "Enable Web Search for this director")
|
|
1480
|
-
: "
|
|
1505
|
+
: uiT("ag_ws_title_needs");
|
|
1481
1506
|
// When the global key is missing, omit the text label entirely
|
|
1482
1507
|
// so the row stays compact. The dotted toggle track + hover
|
|
1483
1508
|
// tooltip communicate the state on its own; the just-in-time
|
|
@@ -1542,7 +1567,7 @@
|
|
|
1542
1567
|
const r = await fetch("/api/agents/" + encodeURIComponent(slug) + "/skills");
|
|
1543
1568
|
if (!r.ok) {
|
|
1544
1569
|
const j = await r.json().catch(() => ({}));
|
|
1545
|
-
list.innerHTML = `<div class="ap-empty"
|
|
1570
|
+
list.innerHTML = `<div class="ap-empty">${escape(uiT("ap_skills_load_fail", { detail: String(j.error || ("HTTP " + r.status)) }))}</div>`;
|
|
1546
1571
|
return;
|
|
1547
1572
|
}
|
|
1548
1573
|
const { skills } = await r.json();
|
|
@@ -1553,7 +1578,7 @@
|
|
|
1553
1578
|
// Header count tag (e.g. "3 / 5 installed").
|
|
1554
1579
|
const card = block.closest(".ap-block");
|
|
1555
1580
|
const countTag = card?.querySelector("[data-ap-skills-count]");
|
|
1556
|
-
if (countTag) countTag.textContent =
|
|
1581
|
+
if (countTag) countTag.textContent = uiT("ap_skills_installed", { current: userCount, cap });
|
|
1557
1582
|
// Radar reflects user skills only — the system skill has no
|
|
1558
1583
|
// turn-time ability deltas (it runs at brief time).
|
|
1559
1584
|
if (radarWrap) radarWrap.innerHTML = renderRadar(slug, userSkills);
|
|
@@ -1569,16 +1594,16 @@
|
|
|
1569
1594
|
drop.classList.add("disabled");
|
|
1570
1595
|
drop.setAttribute("aria-disabled", "true");
|
|
1571
1596
|
const txt = drop.querySelector(".ap-skills-drop-text");
|
|
1572
|
-
if (txt) txt.
|
|
1597
|
+
if (txt) txt.textContent = uiT("ap_skills_cap_reached", { current: userCount, cap });
|
|
1573
1598
|
} else {
|
|
1574
1599
|
drop.classList.remove("disabled");
|
|
1575
1600
|
drop.removeAttribute("aria-disabled");
|
|
1576
1601
|
const txt = drop.querySelector(".ap-skills-drop-text");
|
|
1577
|
-
if (txt) txt.innerHTML =
|
|
1602
|
+
if (txt) txt.innerHTML = uiT("ap_skills_drop_hint");
|
|
1578
1603
|
}
|
|
1579
1604
|
}
|
|
1580
1605
|
} catch (e) {
|
|
1581
|
-
list.innerHTML = `<div class="ap-empty"
|
|
1606
|
+
list.innerHTML = `<div class="ap-empty">${escape(uiT("ap_skills_load_fail", { detail: String(e && e.message ? e.message : e) }))}</div>`;
|
|
1582
1607
|
}
|
|
1583
1608
|
}
|
|
1584
1609
|
|
|
@@ -1645,7 +1670,7 @@
|
|
|
1645
1670
|
<div class="ap-skill-info-actions">
|
|
1646
1671
|
<button type="button" class="ap-skill-info-configure" data-ap-ws-configure data-provider="${escape(wsProvider)}">
|
|
1647
1672
|
<span class="ap-skill-info-configure-mark">↗</span>
|
|
1648
|
-
<span
|
|
1673
|
+
<span>${escape(uiT("ag_ws_configure_key"))}</span>
|
|
1649
1674
|
</button>
|
|
1650
1675
|
</div>
|
|
1651
1676
|
` : ""}
|
|
@@ -1919,7 +1944,8 @@
|
|
|
1919
1944
|
{ v: "grok-4-3", name: "Grok 4.3", provider: "xAI", deck: "1M ctx" },
|
|
1920
1945
|
{ v: "grok-4-20", name: "Grok 4.20", provider: "xAI", deck: "2M ctx · big context" },
|
|
1921
1946
|
// DeepSeek
|
|
1922
|
-
{ v: "deepseek-v4-pro", name: "DeepSeek V4 Pro", provider: "DeepSeek", deck: "reasoning · open weights" }
|
|
1947
|
+
{ v: "deepseek-v4-pro", name: "DeepSeek V4 Pro", provider: "DeepSeek", deck: "reasoning · open weights" },
|
|
1948
|
+
{ v: "deepseek-v4-flash", name: "DeepSeek Lite", provider: "DeepSeek", deck: "V4 Flash · fast · 1M ctx" }
|
|
1923
1949
|
];
|
|
1924
1950
|
function modelKey(slug) { return "boardroom.agent.model." + slug; }
|
|
1925
1951
|
|
|
@@ -2176,9 +2202,9 @@
|
|
|
2176
2202
|
// `effectiveDefaultModel()` does the actual fallback.
|
|
2177
2203
|
const warning = reachable
|
|
2178
2204
|
? ""
|
|
2179
|
-
: `<div class="ap-model-stale" title="
|
|
2205
|
+
: `<div class="ap-model-stale" title="${escape(uiT("ap_model_stale_title"))}">
|
|
2180
2206
|
<span class="ap-model-stale-mark">⚠</span>
|
|
2181
|
-
<span class="ap-model-stale-text"
|
|
2207
|
+
<span class="ap-model-stale-text">${escape(uiT("ap_model_stale_text"))}</span>
|
|
2182
2208
|
</div>`;
|
|
2183
2209
|
// Trigger meta line · show the route when present, fall back to
|
|
2184
2210
|
// provider otherwise. We DROP the leading provider when the route
|
|
@@ -2204,6 +2230,7 @@
|
|
|
2204
2230
|
|
|
2205
2231
|
function openModelPicker(triggerEl) {
|
|
2206
2232
|
closeModelPicker();
|
|
2233
|
+
closeEmotionPicker();
|
|
2207
2234
|
const row = triggerEl.closest("[data-ap-model-row]");
|
|
2208
2235
|
const slug = row?.getAttribute("data-slug");
|
|
2209
2236
|
if (!slug) return;
|
|
@@ -2279,6 +2306,348 @@
|
|
|
2279
2306
|
if (prov) prov.textContent = formatTriggerMeta(m);
|
|
2280
2307
|
}
|
|
2281
2308
|
|
|
2309
|
+
function voiceEmotionOptionLabel(emotionSlug) {
|
|
2310
|
+
const s = emotionSlug === undefined || emotionSlug === null ? "" : String(emotionSlug);
|
|
2311
|
+
const key = !s ? "ap_voice_emotion_auto" : `ap_voice_emotion_${s}`;
|
|
2312
|
+
const txt = uiT(key);
|
|
2313
|
+
return txt === key ? s || "auto" : txt;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/** API emotion slugs mirrored in PATCH body `voice.emotion`. */
|
|
2317
|
+
const VOICE_EMOTION_VALUES = ["", "happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm", "fluent"];
|
|
2318
|
+
|
|
2319
|
+
let voiceOptionsCache = null;
|
|
2320
|
+
async function ensureVoiceOptions() {
|
|
2321
|
+
if (voiceOptionsCache) return voiceOptionsCache;
|
|
2322
|
+
try {
|
|
2323
|
+
const r = await fetch("/api/voices");
|
|
2324
|
+
const j = r.ok ? await r.json() : {};
|
|
2325
|
+
voiceOptionsCache = Array.isArray(j.voices) ? j.voices : [];
|
|
2326
|
+
} catch {
|
|
2327
|
+
voiceOptionsCache = [];
|
|
2328
|
+
}
|
|
2329
|
+
return voiceOptionsCache;
|
|
2330
|
+
}
|
|
2331
|
+
function voiceForAgent(slug) {
|
|
2332
|
+
const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
|
|
2333
|
+
return live && live.voice ? live.voice : null;
|
|
2334
|
+
}
|
|
2335
|
+
/** Format a voice-tune slider value for display. Speed gets an
|
|
2336
|
+
* `×` suffix; centered ranges (pitch / modify-*) prefix non-zero
|
|
2337
|
+
* values with `+` so the sign is unambiguous. Tabular numerals
|
|
2338
|
+
* in CSS keep the value's box width stable as the user drags. */
|
|
2339
|
+
function formatVoiceVal(param, val) {
|
|
2340
|
+
if (param === "speed") return `${Number(val).toFixed(1)}×`;
|
|
2341
|
+
const n = Number(val);
|
|
2342
|
+
if (n > 0) return `+${n}`;
|
|
2343
|
+
return String(n);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/** Compute the lime-fill segment for a given range as two
|
|
2347
|
+
* percentages (`lo` and `hi`) that the CSS gradient consumes.
|
|
2348
|
+
* Non-centered ranges fill from 0 to value%; centered ranges
|
|
2349
|
+
* (min < 0 < max) fill the band between zero and value, so a
|
|
2350
|
+
* positive value lights up the right half and a negative value
|
|
2351
|
+
* the left half. */
|
|
2352
|
+
function rangeFillPositions(min, max, value) {
|
|
2353
|
+
const span = max - min;
|
|
2354
|
+
if (span <= 0) return { lo: "0%", hi: "0%" };
|
|
2355
|
+
const pct = ((value - min) / span) * 100;
|
|
2356
|
+
if (min < 0 && max > 0) {
|
|
2357
|
+
const zero = ((0 - min) / span) * 100;
|
|
2358
|
+
return {
|
|
2359
|
+
lo: `${Math.min(pct, zero)}%`,
|
|
2360
|
+
hi: `${Math.max(pct, zero)}%`,
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
return { lo: "0%", hi: `${pct}%` };
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/** One slider row inside Advanced tuning · header (label left,
|
|
2367
|
+
* value right) on top, hairline track + square thumb beneath.
|
|
2368
|
+
* `centered` ranges (pitch / modify-*) get a mid-point tick
|
|
2369
|
+
* AND a band-style lime fill (zero ↔ value); non-centered
|
|
2370
|
+
* ranges (speed) get a "left-of-thumb" lime fill. */
|
|
2371
|
+
function renderVoiceTuneRow(slug, param, label, value, min, max, step) {
|
|
2372
|
+
const centered = min < 0 && max > 0;
|
|
2373
|
+
const { lo, hi } = rangeFillPositions(min, max, value);
|
|
2374
|
+
return `
|
|
2375
|
+
<div class="ap-voice-tune${centered ? " ap-voice-tune-centered" : ""}">
|
|
2376
|
+
<div class="ap-voice-tune-head">
|
|
2377
|
+
<span class="ap-voice-tune-label">${escape(label)}</span>
|
|
2378
|
+
<span class="ap-voice-tune-value" data-ap-voice-val="${escape(param)}">${escape(formatVoiceVal(param, value))}</span>
|
|
2379
|
+
</div>
|
|
2380
|
+
<input type="range" class="ap-voice-tune-range" min="${min}" max="${max}" step="${step}" value="${value}" data-ap-voice-range="${escape(param)}" data-slug="${escape(slug)}" style="--fill-lo: ${lo}; --fill-hi: ${hi};">
|
|
2381
|
+
</div>`;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function renderVoiceBlock(slug) {
|
|
2385
|
+
// No voice provider configured · render the gamified "locked"
|
|
2386
|
+
// card instead of the picker + sliders. Removing the chrome
|
|
2387
|
+
// makes the missing-key state read as a feature gate ("set
|
|
2388
|
+
// this up to unlock") rather than a broken control panel.
|
|
2389
|
+
const hasVoiceKey = !!(window.app && typeof window.app.hasAnyVoiceKey === "function" && window.app.hasAnyVoiceKey());
|
|
2390
|
+
if (!hasVoiceKey) {
|
|
2391
|
+
return `
|
|
2392
|
+
<div class="ap-voice-locked" data-ap-voice-row data-slug="${escape(slug)}">
|
|
2393
|
+
<div class="ap-voice-locked-glyph" aria-hidden="true">
|
|
2394
|
+
<i></i><i></i><i></i><i></i><i></i>
|
|
2395
|
+
</div>
|
|
2396
|
+
<div class="ap-voice-locked-title">${uiT("ap_voice_locked_title")}</div>
|
|
2397
|
+
<button type="button" class="ap-voice-locked-cta" data-ap-voice-unlock>
|
|
2398
|
+
<span>${escape(uiT("ap_voice_locked_cta"))}</span>
|
|
2399
|
+
<span class="ap-voice-locked-cta-arrow" aria-hidden="true">→</span>
|
|
2400
|
+
</button>
|
|
2401
|
+
</div>
|
|
2402
|
+
`;
|
|
2403
|
+
}
|
|
2404
|
+
// Fire-and-forget prefetch · warms voiceOptionsCache so the picker
|
|
2405
|
+
// pops instantly when the user clicks. Without it the first click
|
|
2406
|
+
// pays the /api/voices round-trip (hundreds of voices on MiniMax)
|
|
2407
|
+
// and the dropdown lags visibly. Idempotent · cache-hit no-ops.
|
|
2408
|
+
ensureVoiceOptions();
|
|
2409
|
+
const v = voiceForAgent(slug);
|
|
2410
|
+
const label = v ? `${v.provider} · ${v.voiceId}` : uiT("ap_voice_browser_default");
|
|
2411
|
+
const deck = v ? v.model : uiT("ap_voice_engine_browser");
|
|
2412
|
+
const speed = v?.speed ?? 1;
|
|
2413
|
+
const pitch = v?.pitch ?? 0;
|
|
2414
|
+
const emotion = v?.emotion || "";
|
|
2415
|
+
const modPitch = v?.modifyPitch ?? 0;
|
|
2416
|
+
const modIntensity = v?.modifyIntensity ?? 0;
|
|
2417
|
+
const modTimbre = v?.modifyTimbre ?? 0;
|
|
2418
|
+
|
|
2419
|
+
const emotionLabel = voiceEmotionOptionLabel(emotion);
|
|
2420
|
+
|
|
2421
|
+
return `
|
|
2422
|
+
<div class="ap-voice-config" data-ap-voice-row data-slug="${escape(slug)}">
|
|
2423
|
+
<div class="ap-voice-picker-row">
|
|
2424
|
+
<button type="button" class="ap-model-trigger" data-ap-voice-trigger>
|
|
2425
|
+
<span class="ap-model-trigger-text">
|
|
2426
|
+
<span class="ap-model-trigger-name" data-ap-voice-name>${escape(label)}</span>
|
|
2427
|
+
<span class="ap-model-trigger-provider" data-ap-voice-provider>${escape(deck)}</span>
|
|
2428
|
+
</span>
|
|
2429
|
+
<span class="ap-model-trigger-caret">▾</span>
|
|
2430
|
+
</button>
|
|
2431
|
+
<button type="button" class="ap-voice-preview-btn" data-ap-voice-preview data-slug="${escape(slug)}" title="${escape(uiT("ap_voice_preview_btn_title"))}" aria-label="${escape(uiT("ap_voice_preview_btn_title"))}"><span class="ap-voice-preview-glyph">▶</span><span class="ap-voice-preview-dots" aria-hidden="true"><i></i><i></i><i></i></span></button>
|
|
2432
|
+
</div>
|
|
2433
|
+
<div class="ap-voice-emotion-row">
|
|
2434
|
+
<button type="button" class="ap-model-trigger ap-voice-emotion-trigger" data-ap-emotion-trigger data-slug="${escape(slug)}">
|
|
2435
|
+
<span class="ap-model-trigger-text">
|
|
2436
|
+
<span class="ap-model-trigger-name" data-ap-voice-emotion-label>${escape(emotionLabel)}</span>
|
|
2437
|
+
</span>
|
|
2438
|
+
<span class="ap-model-trigger-caret">▾</span>
|
|
2439
|
+
</button>
|
|
2440
|
+
<div class="ap-voice-emotion-hint">${escape(uiT("ap_voice_emotion_hint"))}</div>
|
|
2441
|
+
</div>
|
|
2442
|
+
<details class="ap-voice-advanced">
|
|
2443
|
+
<summary>${escape(uiT("ap_voice_advanced"))}</summary>
|
|
2444
|
+
<div class="ap-voice-tune-grid">
|
|
2445
|
+
${renderVoiceTuneRow(slug, "speed", uiT("ap_voice_speed"), speed, 0.5, 2, 0.1)}
|
|
2446
|
+
${renderVoiceTuneRow(slug, "pitch", uiT("ap_voice_pitch"), pitch, -12, 12, 1)}
|
|
2447
|
+
${renderVoiceTuneRow(slug, "modifyPitch", uiT("ap_voice_modify_pitch"), modPitch, -100, 100, 5)}
|
|
2448
|
+
${renderVoiceTuneRow(slug, "modifyIntensity", uiT("ap_voice_modify_intensity"), modIntensity, -100, 100, 5)}
|
|
2449
|
+
${renderVoiceTuneRow(slug, "modifyTimbre", uiT("ap_voice_modify_timbre"), modTimbre, -100, 100, 5)}
|
|
2450
|
+
</div>
|
|
2451
|
+
</details>
|
|
2452
|
+
</div>
|
|
2453
|
+
`;
|
|
2454
|
+
}
|
|
2455
|
+
/** Position a `.ap-model-picker` popover under (or above, if there
|
|
2456
|
+
* isn't enough room below) a trigger element, clamping the height
|
|
2457
|
+
* to fit the available viewport. The picker uses `position: fixed`
|
|
2458
|
+
* so we work in viewport coords throughout. Returns nothing —
|
|
2459
|
+
* mutates `pop.style` in place.
|
|
2460
|
+
*
|
|
2461
|
+
* Preference: open BELOW the trigger. Flip ABOVE only when the
|
|
2462
|
+
* below-space is < 180px AND the above-space is bigger. Either
|
|
2463
|
+
* way, set `max-height` to whatever fits, so a long voice list
|
|
2464
|
+
* scrolls inside the picker rather than disappearing off-screen. */
|
|
2465
|
+
function placePickerNearTrigger(pop, triggerEl, popW) {
|
|
2466
|
+
const margin = 8;
|
|
2467
|
+
const minBelow = 180; // below this many px, prefer flipping above
|
|
2468
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
2469
|
+
const left = Math.round(Math.min(rect.left, window.innerWidth - popW - margin));
|
|
2470
|
+
pop.style.left = `${left}px`;
|
|
2471
|
+
pop.style.width = `${popW}px`;
|
|
2472
|
+
|
|
2473
|
+
const spaceBelow = Math.max(0, window.innerHeight - rect.bottom - margin);
|
|
2474
|
+
const spaceAbove = Math.max(0, rect.top - margin);
|
|
2475
|
+
|
|
2476
|
+
const flipAbove = spaceBelow < minBelow && spaceAbove > spaceBelow;
|
|
2477
|
+
if (flipAbove) {
|
|
2478
|
+
// Anchor the bottom edge of the picker just above the trigger.
|
|
2479
|
+
// The picker grows upward · we use `bottom` rather than `top +
|
|
2480
|
+
// translateY` so max-height clipping stays predictable.
|
|
2481
|
+
pop.style.top = "auto";
|
|
2482
|
+
pop.style.bottom = `${Math.round(window.innerHeight - rect.top + 4)}px`;
|
|
2483
|
+
pop.style.maxHeight = `${Math.round(spaceAbove - 4)}px`;
|
|
2484
|
+
} else {
|
|
2485
|
+
pop.style.top = `${Math.round(rect.bottom + 4)}px`;
|
|
2486
|
+
pop.style.bottom = "auto";
|
|
2487
|
+
pop.style.maxHeight = `${Math.round(spaceBelow - 4)}px`;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
async function openVoicePicker(triggerEl) {
|
|
2492
|
+
closeVoicePicker();
|
|
2493
|
+
closeEmotionPicker();
|
|
2494
|
+
const row = triggerEl.closest("[data-ap-voice-row]");
|
|
2495
|
+
const slug = row?.getAttribute("data-slug");
|
|
2496
|
+
if (!slug) return;
|
|
2497
|
+
const current = voiceForAgent(slug);
|
|
2498
|
+
|
|
2499
|
+
// Mount the popover shell IMMEDIATELY · the user gets visual
|
|
2500
|
+
// confirmation of their click in the same frame. If the cache
|
|
2501
|
+
// is cold, the skeleton holds the place while /api/voices
|
|
2502
|
+
// round-trips; once it lands we swap content in. Without this,
|
|
2503
|
+
// a cold cache produced "click → nothing → eventually picker"
|
|
2504
|
+
// which felt unresponsive on the first open of every session.
|
|
2505
|
+
const pop = document.createElement("div");
|
|
2506
|
+
pop.id = "ap-voice-picker";
|
|
2507
|
+
pop.className = "ap-model-picker";
|
|
2508
|
+
pop.dataset.slug = slug;
|
|
2509
|
+
// Loading row · animated dot trio + label. Visible feedback so
|
|
2510
|
+
// users don't read a static "loading" line as a frozen popover.
|
|
2511
|
+
pop.innerHTML = `
|
|
2512
|
+
<div class="ap-model-picker-loading">
|
|
2513
|
+
<span class="ap-loading-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
|
2514
|
+
<span>${escape(uiT("ap_voice_loading"))}</span>
|
|
2515
|
+
</div>`;
|
|
2516
|
+
document.body.appendChild(pop);
|
|
2517
|
+
placePickerNearTrigger(pop, triggerEl, 280);
|
|
2518
|
+
|
|
2519
|
+
const voices = await ensureVoiceOptions();
|
|
2520
|
+
// The user (or a sibling open) may have closed this picker
|
|
2521
|
+
// during the await · don't write into a detached node.
|
|
2522
|
+
if (!document.body.contains(pop)) return;
|
|
2523
|
+
|
|
2524
|
+
if (voices.length === 0) {
|
|
2525
|
+
pop.innerHTML = `<div class="ap-model-group">${escape(uiT("ap_voice_no_provider"))}</div>`;
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const groups = [];
|
|
2529
|
+
let last = null;
|
|
2530
|
+
for (const v of voices) {
|
|
2531
|
+
const provider = String(v.provider || "browser");
|
|
2532
|
+
if (provider !== last) {
|
|
2533
|
+
groups.push(`<div class="ap-model-group">${escape(provider)}</div>`);
|
|
2534
|
+
last = provider;
|
|
2535
|
+
}
|
|
2536
|
+
const id = [provider, v.model || "", v.voiceId || ""].join("|");
|
|
2537
|
+
const active = current && current.provider === provider && current.model === v.model && current.voiceId === v.voiceId;
|
|
2538
|
+
groups.push(`
|
|
2539
|
+
<button type="button" class="ap-model-opt${active ? " active" : ""}" data-ap-voice-pick="${escape(id)}">
|
|
2540
|
+
<span class="ap-model-opt-label">${escape(v.label || v.voiceId || uiT("ap_voice_fallback_voice"))}</span>
|
|
2541
|
+
<span class="ap-model-opt-hint">${escape((v.model || "") + (v.language ? " · " + v.language : ""))}</span>
|
|
2542
|
+
</button>
|
|
2543
|
+
`);
|
|
2544
|
+
}
|
|
2545
|
+
pop.innerHTML = groups.join("");
|
|
2546
|
+
}
|
|
2547
|
+
function closeVoicePicker() {
|
|
2548
|
+
const el = document.getElementById("ap-voice-picker");
|
|
2549
|
+
if (el) el.remove();
|
|
2550
|
+
}
|
|
2551
|
+
function closeEmotionPicker() {
|
|
2552
|
+
const el = document.getElementById("ap-emotion-picker");
|
|
2553
|
+
if (el) el.remove();
|
|
2554
|
+
}
|
|
2555
|
+
/** Custom popover for voice emotion · matches `.ap-model-picker` chrome (no native `<select>` menu). */
|
|
2556
|
+
function openEmotionPicker(triggerEl) {
|
|
2557
|
+
closeEmotionPicker();
|
|
2558
|
+
closeVoicePicker();
|
|
2559
|
+
closeModelPicker();
|
|
2560
|
+
const row = triggerEl.closest("[data-ap-voice-row]");
|
|
2561
|
+
const slug = row?.getAttribute("data-slug");
|
|
2562
|
+
if (!slug) return;
|
|
2563
|
+
const curVoice = voiceForAgent(slug);
|
|
2564
|
+
const raw = curVoice && curVoice.emotion != null && curVoice.emotion !== ""
|
|
2565
|
+
? String(curVoice.emotion)
|
|
2566
|
+
: "";
|
|
2567
|
+
const parts = [`<div class="ap-model-group">${escape(uiT("ap_voice_emotion_label"))}</div>`];
|
|
2568
|
+
for (const e of VOICE_EMOTION_VALUES) {
|
|
2569
|
+
const active = raw === e;
|
|
2570
|
+
parts.push(`
|
|
2571
|
+
<button type="button" class="ap-model-opt${active ? " active" : ""}" data-ap-emotion-pick="${escape(e)}">
|
|
2572
|
+
<span class="ap-model-opt-label">${escape(voiceEmotionOptionLabel(e))}</span>
|
|
2573
|
+
</button>`);
|
|
2574
|
+
}
|
|
2575
|
+
const pop = document.createElement("div");
|
|
2576
|
+
pop.id = "ap-emotion-picker";
|
|
2577
|
+
pop.className = "ap-model-picker";
|
|
2578
|
+
pop.dataset.slug = slug;
|
|
2579
|
+
pop.innerHTML = parts.join("");
|
|
2580
|
+
document.body.appendChild(pop);
|
|
2581
|
+
// Width tracks the trigger so the popover hugs whichever
|
|
2582
|
+
// emotion-picker variant is in use (compact vs full row).
|
|
2583
|
+
const triggerWidth = triggerEl.getBoundingClientRect().width;
|
|
2584
|
+
placePickerNearTrigger(pop, triggerEl, Math.round(Math.min(280, Math.max(220, triggerWidth))));
|
|
2585
|
+
}
|
|
2586
|
+
function setVoiceFor(slug, voice) {
|
|
2587
|
+
const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
|
|
2588
|
+
if (!live) return;
|
|
2589
|
+
fetch("/api/agents/" + encodeURIComponent(slug), {
|
|
2590
|
+
method: "PATCH",
|
|
2591
|
+
headers: { "content-type": "application/json" },
|
|
2592
|
+
body: JSON.stringify({ voice }),
|
|
2593
|
+
})
|
|
2594
|
+
.then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || `HTTP ${r.status}`))))
|
|
2595
|
+
.then((updated) => {
|
|
2596
|
+
const nv = updated.voice != null ? updated.voice : voice;
|
|
2597
|
+
live.voice = nv;
|
|
2598
|
+
const row = document.querySelector(`[data-ap-voice-row][data-slug="${slug}"]`);
|
|
2599
|
+
const name = row?.querySelector("[data-ap-voice-name]");
|
|
2600
|
+
const prov = row?.querySelector("[data-ap-voice-provider]");
|
|
2601
|
+
if (name && nv && nv.provider && nv.voiceId) name.textContent = `${nv.provider} · ${nv.voiceId}`;
|
|
2602
|
+
if (prov && nv && nv.model != null) prov.textContent = nv.model;
|
|
2603
|
+
const emLb = row?.querySelector("[data-ap-voice-emotion-label]");
|
|
2604
|
+
if (emLb && nv) emLb.textContent = voiceEmotionOptionLabel(nv.emotion ?? "");
|
|
2605
|
+
})
|
|
2606
|
+
.catch((e) => alert(uiT("ap_voice_save_err", { msg: e && e.message ? e.message : String(e) })));
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
async function previewVoice(slug) {
|
|
2610
|
+
const v = voiceForAgent(slug);
|
|
2611
|
+
if (!v || !v.voiceId) {
|
|
2612
|
+
alert(uiT("ap_voice_preview_need_voice"));
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
const btn = document.querySelector(`[data-ap-voice-preview][data-slug="${slug}"]`);
|
|
2616
|
+
// Loading state · class-toggle so the inner glyph hides and the
|
|
2617
|
+
// animated 3-dot indicator takes its place. Avoids the previous
|
|
2618
|
+
// `⏳` emoji which renders inconsistently across platforms and
|
|
2619
|
+
// doesn't match the system's mono register.
|
|
2620
|
+
if (btn) { btn.disabled = true; btn.classList.add("is-loading"); }
|
|
2621
|
+
try {
|
|
2622
|
+
const r = await fetch("/api/voices/preview", {
|
|
2623
|
+
method: "POST",
|
|
2624
|
+
headers: { "content-type": "application/json" },
|
|
2625
|
+
body: JSON.stringify({
|
|
2626
|
+
provider: v.provider,
|
|
2627
|
+
model: v.model,
|
|
2628
|
+
voiceId: v.voiceId,
|
|
2629
|
+
speed: v.speed,
|
|
2630
|
+
pitch: v.pitch,
|
|
2631
|
+
emotion: v.emotion,
|
|
2632
|
+
modifyPitch: v.modifyPitch,
|
|
2633
|
+
modifyIntensity: v.modifyIntensity,
|
|
2634
|
+
modifyTimbre: v.modifyTimbre,
|
|
2635
|
+
}),
|
|
2636
|
+
});
|
|
2637
|
+
const data = await r.json();
|
|
2638
|
+
if (!r.ok || !data.audioBase64) {
|
|
2639
|
+
alert(uiT("ap_voice_preview_failed", { msg: data.error || "no audio" }));
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
const audio = new Audio(`data:${data.mimeType};base64,${data.audioBase64}`);
|
|
2643
|
+
audio.play().catch((e) => alert(uiT("ap_voice_preview_playback_blocked", { msg: e.message || String(e) })));
|
|
2644
|
+
} catch (e) {
|
|
2645
|
+
alert(uiT("ap_voice_preview_err", { msg: e.message || String(e) }));
|
|
2646
|
+
} finally {
|
|
2647
|
+
if (btn) { btn.disabled = false; btn.classList.remove("is-loading"); }
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2282
2651
|
function pageHTML(p, slug) {
|
|
2283
2652
|
const skills = skillsForAgent(slug);
|
|
2284
2653
|
const liveModel = liveModelFor(slug) || (p.metrics && p.metrics.model) || { name: "—", deck: "" };
|
|
@@ -2302,7 +2671,7 @@
|
|
|
2302
2671
|
}).join("");
|
|
2303
2672
|
|
|
2304
2673
|
const bioBody = (Array.isArray(p.bio) ? p.bio.join("\n\n") : (p.bio || "")).trim();
|
|
2305
|
-
const statusLabel =
|
|
2674
|
+
const statusLabel = profileStatusLabel(p);
|
|
2306
2675
|
|
|
2307
2676
|
return `
|
|
2308
2677
|
<section class="ap-profile-card ap-profile-card-full" data-ap-card-slug="${escape(slug)}">
|
|
@@ -2316,12 +2685,12 @@
|
|
|
2316
2685
|
<div class="ap-id-text">
|
|
2317
2686
|
<h1 class="ap-id-name">${escape(p.name)}</h1>
|
|
2318
2687
|
<div class="ap-id-meta">
|
|
2319
|
-
<span class="ap-id-role">${escape(p
|
|
2688
|
+
<span class="ap-id-role">${escape(profileRoleLabel(p))}</span>
|
|
2320
2689
|
${p.handle ? `<span class="ap-id-dot">·</span><span class="ap-id-handle">${escape(p.handle)}</span>` : ""}
|
|
2321
2690
|
<span class="ap-status-pill">${escape(statusLabel)}</span>
|
|
2322
2691
|
</div>
|
|
2323
2692
|
</div>
|
|
2324
|
-
<button type="button" class="ap-id-menu" data-ap-id-menu data-slug="${escape(slug)}" aria-label="
|
|
2693
|
+
<button type="button" class="ap-id-menu" data-ap-id-menu data-slug="${escape(slug)}" aria-label="${escape(uiT("ap_aria_id_menu"))}">⋯</button>
|
|
2325
2694
|
</div>
|
|
2326
2695
|
</section>
|
|
2327
2696
|
|
|
@@ -2332,27 +2701,27 @@
|
|
|
2332
2701
|
|
|
2333
2702
|
<section class="ap-block">
|
|
2334
2703
|
<header class="ap-block-h">
|
|
2335
|
-
<span class="ap-block-h-title"
|
|
2336
|
-
<button type="button" class="ap-block-h-action" data-ap-intel-edit
|
|
2704
|
+
<span class="ap-block-h-title">${escape(uiT("ap_intel"))}</span>
|
|
2705
|
+
<button type="button" class="ap-block-h-action" data-ap-intel-edit>${escape(uiT("ap_edit"))}</button>
|
|
2337
2706
|
</header>
|
|
2338
2707
|
<div class="ap-intel" data-ap-intel data-slug="${escape(slug)}">
|
|
2339
|
-
<div class="ap-intel-view" data-ap-intel-view>${escape(bioBody) || `<span class="ap-empty"
|
|
2708
|
+
<div class="ap-intel-view" data-ap-intel-view>${escape(bioBody) || `<span class="ap-empty">${escape(uiT("ap_intel_empty"))}</span>`}</div>
|
|
2340
2709
|
</div>
|
|
2341
2710
|
</section>
|
|
2342
2711
|
|
|
2343
2712
|
<section class="ap-block">
|
|
2344
2713
|
<header class="ap-block-h">
|
|
2345
|
-
<span class="ap-block-h-title"
|
|
2346
|
-
<button type="button" class="ap-block-h-action" data-ap-instr-edit
|
|
2714
|
+
<span class="ap-block-h-title">${escape(uiT("ap_instruction"))}</span>
|
|
2715
|
+
<button type="button" class="ap-block-h-action" data-ap-instr-edit>${escape(uiT("ap_edit"))}</button>
|
|
2347
2716
|
</header>
|
|
2348
2717
|
${renderInstructionBlock(p, slug)}
|
|
2349
2718
|
</section>
|
|
2350
2719
|
|
|
2351
2720
|
<section class="ap-block">
|
|
2352
2721
|
<header class="ap-block-h">
|
|
2353
|
-
<span class="ap-block-h-title"
|
|
2722
|
+
<span class="ap-block-h-title">${escape(uiT("ap_rules"))}</span>
|
|
2354
2723
|
<button type="button" class="ap-block-h-action" data-ap-rule-add data-slug="${escape(slug)}" ${rulesForAgent(slug).length >= RULES_MAX ? "disabled" : ""}>
|
|
2355
|
-
${rulesForAgent(slug).length >= RULES_MAX ?
|
|
2724
|
+
${rulesForAgent(slug).length >= RULES_MAX ? escape(uiT("ap_rules_max", { n: RULES_MAX })) : escape(uiT("ap_rules_add"))}
|
|
2356
2725
|
</button>
|
|
2357
2726
|
</header>
|
|
2358
2727
|
${renderRulesBlock(slug)}
|
|
@@ -2361,10 +2730,10 @@
|
|
|
2361
2730
|
${isChair ? `
|
|
2362
2731
|
<section class="ap-block">
|
|
2363
2732
|
<header class="ap-block-h">
|
|
2364
|
-
<span class="ap-block-h-title"
|
|
2733
|
+
<span class="ap-block-h-title">${escape(uiT("ap_memory"))}</span>
|
|
2365
2734
|
<div class="ap-block-h-actions">
|
|
2366
|
-
<button type="button" class="ap-block-h-action ap-dream-trigger" data-ap-dream-trigger data-slug="${escape(slug)}" title="
|
|
2367
|
-
<button type="button" class="ap-block-h-action" data-ap-memory-add-toggle data-slug="${escape(slug)}"
|
|
2735
|
+
<button type="button" class="ap-block-h-action ap-dream-trigger" data-ap-dream-trigger data-slug="${escape(slug)}" title="${escape(uiT("ap_dream_consolidate_title"))}">${escape(uiT("ap_dream_consolidate_btn"))}</button>
|
|
2736
|
+
<button type="button" class="ap-block-h-action" data-ap-memory-add-toggle data-slug="${escape(slug)}">${escape(uiT("ap_memory_add"))}</button>
|
|
2368
2737
|
</div>
|
|
2369
2738
|
</header>
|
|
2370
2739
|
${renderMemoryBlock(slug)}
|
|
@@ -2377,23 +2746,23 @@
|
|
|
2377
2746
|
|
|
2378
2747
|
<section class="ap-block">
|
|
2379
2748
|
<header class="ap-block-h">
|
|
2380
|
-
<span class="ap-block-h-title"
|
|
2381
|
-
<span class="ap-block-h-tag"
|
|
2749
|
+
<span class="ap-block-h-title">${escape(uiT("ap_track_record"))}</span>
|
|
2750
|
+
<span class="ap-block-h-tag">${escape(uiT("ap_track_tag_model_usage"))}</span>
|
|
2382
2751
|
</header>
|
|
2383
2752
|
<div class="ap-block-body">
|
|
2384
2753
|
${renderModelBlock(slug, liveModel)}
|
|
2385
2754
|
<div class="ap-stats-grid" data-ap-stats data-slug="${escape(slug)}">
|
|
2386
2755
|
<div class="ap-stat">
|
|
2387
2756
|
<div class="ap-stat-v" data-ap-stat-rooms>—</div>
|
|
2388
|
-
<div class="ap-stat-l"
|
|
2757
|
+
<div class="ap-stat-l">${escape(uiT("ap_stat_rooms"))}</div>
|
|
2389
2758
|
</div>
|
|
2390
2759
|
<div class="ap-stat">
|
|
2391
2760
|
<div class="ap-stat-v" data-ap-stat-rounds>—</div>
|
|
2392
|
-
<div class="ap-stat-l"
|
|
2761
|
+
<div class="ap-stat-l">${escape(uiT("ap_stat_rounds"))}</div>
|
|
2393
2762
|
</div>
|
|
2394
2763
|
<div class="ap-stat">
|
|
2395
2764
|
<div class="ap-stat-v" data-ap-stat-tokens>—</div>
|
|
2396
|
-
<div class="ap-stat-l"
|
|
2765
|
+
<div class="ap-stat-l">${escape(uiT("ap_stat_tokens"))}</div>
|
|
2397
2766
|
</div>
|
|
2398
2767
|
</div>
|
|
2399
2768
|
</div>
|
|
@@ -2401,22 +2770,19 @@
|
|
|
2401
2770
|
|
|
2402
2771
|
<section class="ap-block">
|
|
2403
2772
|
<header class="ap-block-h">
|
|
2404
|
-
<span class="ap-block-h-title"
|
|
2405
|
-
<span class="ap-block-h-tag" data-ap-skills-count
|
|
2773
|
+
<span class="ap-block-h-title">${escape(uiT("ap_skills"))}</span>
|
|
2774
|
+
<span class="ap-block-h-tag" data-ap-skills-count>${escape(uiT("ap_skills_installed", { current: 0, cap: isChair ? SKILL_CAP.chair : SKILL_CAP.director }))}</span>
|
|
2406
2775
|
</header>
|
|
2407
2776
|
${renderSkillsBlockV2(slug, isChair)}
|
|
2408
2777
|
</section>
|
|
2409
2778
|
|
|
2410
2779
|
<section class="ap-block">
|
|
2411
2780
|
<header class="ap-block-h">
|
|
2412
|
-
<span class="ap-block-h-title"
|
|
2413
|
-
<span class="ap-block-h-tag"
|
|
2781
|
+
<span class="ap-block-h-title">${escape(uiT("ap_voice_section"))}</span>
|
|
2782
|
+
<span class="ap-block-h-tag">${escape(uiT("ap_voice_section_tag"))}</span>
|
|
2414
2783
|
</header>
|
|
2415
|
-
<div class="ap-
|
|
2416
|
-
|
|
2417
|
-
<div class="ap-coming-soon-title">Knowledge docs</div>
|
|
2418
|
-
<p class="ap-coming-soon-body">Attach PDFs, links, and reference notes to ground this agent's reasoning. They'll be cited inline during rooms and available for the agent to recall by name.</p>
|
|
2419
|
-
<div class="ap-coming-soon-tag">in development</div>
|
|
2784
|
+
<div class="ap-block-body">
|
|
2785
|
+
${renderVoiceBlock(slug)}
|
|
2420
2786
|
</div>
|
|
2421
2787
|
</section>
|
|
2422
2788
|
|
|
@@ -3242,9 +3608,10 @@
|
|
|
3242
3608
|
const provider = wsToggle.getAttribute("data-provider") || "brave";
|
|
3243
3609
|
if (!keyConfigured) {
|
|
3244
3610
|
const ok = confirm(
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3611
|
+
(window.I18n && typeof window.I18n.t === "function")
|
|
3612
|
+
? uiT("ag_ws_need_key_confirm")
|
|
3613
|
+
:
|
|
3614
|
+
("Web Search needs Brave Search or Tavily API credentials.\n\nBrave Search · ≈ $5 per 1000 queries. Tavily · per Tavily API credits.\n\nOpen Preferences now?"),
|
|
3248
3615
|
);
|
|
3249
3616
|
if (ok && typeof window.openUserSettings === "function") {
|
|
3250
3617
|
window.openUserSettings({ section: "keys", focusProvider: provider });
|
|
@@ -3394,7 +3761,7 @@
|
|
|
3394
3761
|
if (!view) return;
|
|
3395
3762
|
const expanded = view.classList.toggle("expanded");
|
|
3396
3763
|
instrToggle.setAttribute("aria-expanded", String(expanded));
|
|
3397
|
-
instrToggle.textContent = expanded ? "
|
|
3764
|
+
instrToggle.textContent = expanded ? uiT("ap_show_less") : uiT("ap_show_more");
|
|
3398
3765
|
return;
|
|
3399
3766
|
}
|
|
3400
3767
|
|
|
@@ -3550,6 +3917,72 @@
|
|
|
3550
3917
|
closeModelPicker();
|
|
3551
3918
|
return;
|
|
3552
3919
|
}
|
|
3920
|
+
const voiceTrigger = e.target.closest("[data-ap-voice-trigger]");
|
|
3921
|
+
if (voiceTrigger) {
|
|
3922
|
+
e.preventDefault();
|
|
3923
|
+
if (document.getElementById("ap-voice-picker")) closeVoicePicker();
|
|
3924
|
+
else openVoicePicker(voiceTrigger);
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
// Locked card CTA · deep-links to user-settings keys panel
|
|
3928
|
+
// and scrolls the MiniMax row into view (the same deep-link
|
|
3929
|
+
// the composer's voice toggle uses when no key is set).
|
|
3930
|
+
// After the user closes the modal, refreshAgentProfileSkills
|
|
3931
|
+
// already fires (window-level wrapper); we also refresh the
|
|
3932
|
+
// visible voice block so a configured key flips the locked
|
|
3933
|
+
// card into the picker without a page reload.
|
|
3934
|
+
const unlockBtn = e.target.closest("[data-ap-voice-unlock]");
|
|
3935
|
+
if (unlockBtn) {
|
|
3936
|
+
e.preventDefault();
|
|
3937
|
+
if (typeof window.openUserSettings === "function") {
|
|
3938
|
+
window.openUserSettings({ section: "keys", focusProvider: "minimax" });
|
|
3939
|
+
}
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
const emoTrig = e.target.closest("[data-ap-emotion-trigger]");
|
|
3943
|
+
if (emoTrig) {
|
|
3944
|
+
e.preventDefault();
|
|
3945
|
+
if (document.getElementById("ap-emotion-picker")) closeEmotionPicker();
|
|
3946
|
+
else openEmotionPicker(emoTrig);
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
const emoOpt = e.target.closest("[data-ap-emotion-pick]");
|
|
3950
|
+
if (emoOpt) {
|
|
3951
|
+
e.preventDefault();
|
|
3952
|
+
const pop = document.getElementById("ap-emotion-picker");
|
|
3953
|
+
const slug = pop?.dataset.slug;
|
|
3954
|
+
if (!slug) return;
|
|
3955
|
+
let rawPick = emoOpt.getAttribute("data-ap-emotion-pick");
|
|
3956
|
+
if (rawPick === null) rawPick = "";
|
|
3957
|
+
const existing = voiceForAgent(slug) || { provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-qingse" };
|
|
3958
|
+
setVoiceFor(slug, {
|
|
3959
|
+
...existing,
|
|
3960
|
+
emotion: rawPick === "" ? undefined : rawPick,
|
|
3961
|
+
});
|
|
3962
|
+
closeEmotionPicker();
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
const voiceOpt = e.target.closest("[data-ap-voice-pick]");
|
|
3966
|
+
if (voiceOpt) {
|
|
3967
|
+
e.preventDefault();
|
|
3968
|
+
const raw = voiceOpt.getAttribute("data-ap-voice-pick") || "";
|
|
3969
|
+
const pop = document.getElementById("ap-voice-picker");
|
|
3970
|
+
const slug = pop?.dataset.slug;
|
|
3971
|
+
const [provider, model, voiceId] = raw.split("|");
|
|
3972
|
+
if (!slug || !provider || !model || !voiceId) return;
|
|
3973
|
+
const existing = voiceForAgent(slug) || {};
|
|
3974
|
+
setVoiceFor(slug, { ...existing, provider, model, voiceId });
|
|
3975
|
+
closeVoicePicker();
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
// Preview button
|
|
3979
|
+
const previewBtn = e.target.closest("[data-ap-voice-preview]");
|
|
3980
|
+
if (previewBtn) {
|
|
3981
|
+
e.preventDefault();
|
|
3982
|
+
const slug = previewBtn.getAttribute("data-slug");
|
|
3983
|
+
if (slug) previewVoice(slug);
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3553
3986
|
});
|
|
3554
3987
|
document.addEventListener("click", (e) => {
|
|
3555
3988
|
const pop = document.getElementById("ap-model-picker");
|
|
@@ -3558,6 +3991,51 @@
|
|
|
3558
3991
|
if (e.target.closest("[data-ap-model-trigger]")) return;
|
|
3559
3992
|
closeModelPicker();
|
|
3560
3993
|
}, true);
|
|
3994
|
+
document.addEventListener("click", (e) => {
|
|
3995
|
+
const pop = document.getElementById("ap-voice-picker");
|
|
3996
|
+
if (!pop) return;
|
|
3997
|
+
if (e.target.closest("#ap-voice-picker")) return;
|
|
3998
|
+
if (e.target.closest("[data-ap-voice-trigger]")) return;
|
|
3999
|
+
closeVoicePicker();
|
|
4000
|
+
}, true);
|
|
4001
|
+
document.addEventListener("click", (e) => {
|
|
4002
|
+
const pop = document.getElementById("ap-emotion-picker");
|
|
4003
|
+
if (!pop) return;
|
|
4004
|
+
if (e.target.closest("#ap-emotion-picker")) return;
|
|
4005
|
+
if (e.target.closest("[data-ap-emotion-trigger]")) return;
|
|
4006
|
+
closeEmotionPicker();
|
|
4007
|
+
}, true);
|
|
4008
|
+
|
|
4009
|
+
// Voice config sliders + emotion
|
|
4010
|
+
document.addEventListener("input", (e) => {
|
|
4011
|
+
const range = e.target.closest("[data-ap-voice-range]");
|
|
4012
|
+
if (range) {
|
|
4013
|
+
const param = range.getAttribute("data-ap-voice-range");
|
|
4014
|
+
const val = parseFloat(range.value);
|
|
4015
|
+
// Update display value · same formatter that initial render
|
|
4016
|
+
// uses, so dragging matches the static "1.0×" / "+5" reading.
|
|
4017
|
+
const container = range.closest(".ap-voice-config");
|
|
4018
|
+
const display = container?.querySelector(`[data-ap-voice-val="${param}"]`);
|
|
4019
|
+
if (display) display.textContent = formatVoiceVal(param, val);
|
|
4020
|
+
// Repaint the lime-fill segment as the thumb moves.
|
|
4021
|
+
const { lo, hi } = rangeFillPositions(parseFloat(range.min), parseFloat(range.max), val);
|
|
4022
|
+
range.style.setProperty("--fill-lo", lo);
|
|
4023
|
+
range.style.setProperty("--fill-hi", hi);
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
});
|
|
4027
|
+
document.addEventListener("change", (e) => {
|
|
4028
|
+
const range = e.target.closest("[data-ap-voice-range]");
|
|
4029
|
+
if (range) {
|
|
4030
|
+
const param = range.getAttribute("data-ap-voice-range");
|
|
4031
|
+
const slug = range.getAttribute("data-slug");
|
|
4032
|
+
if (!param || !slug) return;
|
|
4033
|
+
const val = parseFloat(range.value);
|
|
4034
|
+
const existing = voiceForAgent(slug) || { provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-qingse" };
|
|
4035
|
+
setVoiceFor(slug, { ...existing, [param]: val });
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
});
|
|
3561
4039
|
|
|
3562
4040
|
// Back-to-room paths now that the explicit Back button is gone:
|
|
3563
4041
|
// • clicking any sidebar room row → switch back to room view
|
|
@@ -3636,12 +4114,26 @@
|
|
|
3636
4114
|
window.closeAgentProfile = showRoom;
|
|
3637
4115
|
// Re-fetch the open profile's skills (incl. per-skill keyConfigured
|
|
3638
4116
|
// flags) so the web-search toggle row's cached `data-key-configured`
|
|
3639
|
-
// refreshes after the user adds a key in Preferences.
|
|
3640
|
-
//
|
|
4117
|
+
// refreshes after the user adds a key in Preferences. Also re-renders
|
|
4118
|
+
// the Voice Setup block so the gamified locked card flips into the
|
|
4119
|
+
// picker the moment the user adds a MiniMax / ElevenLabs key without
|
|
4120
|
+
// forcing a page reload. No-op when no profile is currently open.
|
|
3641
4121
|
window.refreshAgentProfileSkills = function () {
|
|
3642
|
-
if (currentlyOpenSlug)
|
|
4122
|
+
if (!currentlyOpenSlug) return;
|
|
4123
|
+
loadSkillsForV2(currentlyOpenSlug);
|
|
4124
|
+
const voiceRow = document.querySelector(`[data-ap-voice-row][data-slug="${currentlyOpenSlug}"]`);
|
|
4125
|
+
if (voiceRow) {
|
|
4126
|
+
const wrap = document.createElement("div");
|
|
4127
|
+
wrap.innerHTML = renderVoiceBlock(currentlyOpenSlug).trim();
|
|
4128
|
+
const fresh = wrap.firstElementChild;
|
|
4129
|
+
if (fresh) voiceRow.replaceWith(fresh);
|
|
4130
|
+
}
|
|
3643
4131
|
};
|
|
3644
4132
|
|
|
4133
|
+
document.addEventListener("boardroom:locale", () => {
|
|
4134
|
+
if (currentlyOpenSlug && typeof open === "function") open(currentlyOpenSlug);
|
|
4135
|
+
});
|
|
4136
|
+
|
|
3645
4137
|
if (document.readyState === "loading") {
|
|
3646
4138
|
document.addEventListener("DOMContentLoaded", init);
|
|
3647
4139
|
} else {
|