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/app.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"grok-4-1-fast": "Grok 4.1 Fast",
|
|
35
35
|
"grok-4-20": "Grok 4.20",
|
|
36
36
|
"deepseek-v4-pro": "DeepSeek V4 Pro",
|
|
37
|
+
"deepseek-v4-flash": "DeepSeek Lite",
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/** Full model catalog for the new-agent composer dropdown. Mirrors
|
|
@@ -47,8 +48,7 @@
|
|
|
47
48
|
{ v: "sonnet-4-6", label: "Claude Sonnet 4.6", provider: "Anthropic", deck: "balanced · default" },
|
|
48
49
|
{ v: "opus-4-6", label: "Claude Opus 4.6", provider: "Anthropic", deck: "prior-gen flagship" },
|
|
49
50
|
{ v: "opus-4-6-fast", label: "Claude Opus 4.6 Fast", provider: "Anthropic", deck: "faster 4.6 · same intelligence" },
|
|
50
|
-
{ v: "haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic", deck: "fast · low-cost" },
|
|
51
|
-
{ v: "gpt-5-5-pro", label: "GPT-5.5 Pro", provider: "OpenAI", deck: "flagship · 1M ctx" },
|
|
51
|
+
{ v: "haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic", deck: "fast · low-cost" }, { v: "gpt-5-5-pro", label: "GPT-5.5 Pro", provider: "OpenAI", deck: "flagship · 1M ctx" },
|
|
52
52
|
{ v: "gpt-5-5", label: "GPT-5.5", provider: "OpenAI", deck: "1M ctx" },
|
|
53
53
|
{ v: "gpt-5-4", label: "GPT-5.4", provider: "OpenAI", deck: "general · 1M ctx" },
|
|
54
54
|
{ v: "gpt-5-4-mini", label: "GPT-5.4 Mini", provider: "OpenAI", deck: "fast · 400k ctx" },
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
{ v: "grok-4-1-fast", label: "Grok 4.1 Fast", provider: "xAI", deck: "fast · 256k ctx" },
|
|
61
61
|
{ v: "grok-4-20", label: "Grok 4.20", provider: "xAI", deck: "2M ctx · big context" },
|
|
62
62
|
{ v: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "DeepSeek", deck: "reasoning · open weights" },
|
|
63
|
+
{ v: "deepseek-v4-flash", label: "DeepSeek Lite", provider: "DeepSeek", deck: "V4 Flash · fast · 1M ctx" },
|
|
63
64
|
];
|
|
64
65
|
|
|
65
66
|
/** Tone tooltips · short, user-readable summary of how each tone
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
currentMembers: [], // directors only (chair excluded)
|
|
94
95
|
currentChair: null, // chair agent for the current room
|
|
95
96
|
currentQueue: [],
|
|
97
|
+
voiceQueues: {},
|
|
96
98
|
/** Round progress from the orchestrator: how many directors have
|
|
97
99
|
* spoken in the current round vs. the cap (= cast size). */
|
|
98
100
|
currentRound: { spoken: 0, total: 0 },
|
|
@@ -181,10 +183,20 @@
|
|
|
181
183
|
if (!card) return;
|
|
182
184
|
const expanded = card.classList.toggle("expanded");
|
|
183
185
|
const label = expanded
|
|
184
|
-
? (btn.getAttribute("data-less") || "
|
|
185
|
-
: (btn.getAttribute("data-more") || "
|
|
186
|
+
? (btn.getAttribute("data-less") || this._t("convene_show_less"))
|
|
187
|
+
: (btn.getAttribute("data-more") || this._t("convene_show_more"));
|
|
186
188
|
btn.textContent = label;
|
|
187
189
|
});
|
|
190
|
+
document.addEventListener("boardroom:locale", () => {
|
|
191
|
+
this.renderSidebarRooms();
|
|
192
|
+
this.renderSidebarAgents();
|
|
193
|
+
this.renderSidebarCounts();
|
|
194
|
+
this.renderUserBlock();
|
|
195
|
+
if (this.currentRoomId) this.renderRoom();
|
|
196
|
+
else this.renderEmptyState();
|
|
197
|
+
if (Array.isArray(this._reportsCache)) this.renderReportsPage(this._reportsCache);
|
|
198
|
+
if (Array.isArray(this._notesCache)) this.renderNotesPage(this._notesCache);
|
|
199
|
+
});
|
|
188
200
|
window.addEventListener("hashchange", () => this.handleRoute());
|
|
189
201
|
this.handleRoute();
|
|
190
202
|
|
|
@@ -294,11 +306,10 @@
|
|
|
294
306
|
|
|
295
307
|
const count = fresh.length;
|
|
296
308
|
const names = fresh.map((m) => m.name).join(", ");
|
|
297
|
-
|
|
309
|
+
const bodyKey = count === 1 ? "migrate_body_one" : "migrate_body";
|
|
298
310
|
const copy = {
|
|
299
|
-
head:
|
|
300
|
-
body:
|
|
301
|
-
tooltip: names,
|
|
311
|
+
head: this._t("migrate_head"),
|
|
312
|
+
body: this._t(bodyKey, { count }), tooltip: names,
|
|
302
313
|
};
|
|
303
314
|
textEl.innerHTML =
|
|
304
315
|
`<span class="sys-notice-strong">${this.escape(copy.head)}</span> · ${this.escape(copy.body)}`;
|
|
@@ -806,12 +817,32 @@
|
|
|
806
817
|
|
|
807
818
|
this.sse.addEventListener("message-removed", (e) => {
|
|
808
819
|
const data = JSON.parse(e.data);
|
|
820
|
+
// Stop any voice playback for this message (e.g. READY control token)
|
|
821
|
+
const vq = this.voiceQueues[data.messageId];
|
|
822
|
+
if (vq) {
|
|
823
|
+
if (vq.audio) { try { vq.audio.pause(); } catch(_) {} }
|
|
824
|
+
delete this.voiceQueues[data.messageId];
|
|
825
|
+
}
|
|
809
826
|
// Drop the empty placeholder bubble.
|
|
810
827
|
this.currentMessages = this.currentMessages.filter((m) => m.id !== data.messageId);
|
|
811
828
|
const article = document.querySelector(`[data-message-id="${data.messageId}"]`);
|
|
812
829
|
if (article) article.remove();
|
|
813
830
|
});
|
|
814
831
|
|
|
832
|
+
this.sse.addEventListener("voice-chunk", (e) => {
|
|
833
|
+
const data = JSON.parse(e.data);
|
|
834
|
+
this.enqueueVoiceChunk(roomId, data);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
this.sse.addEventListener("voice-final", (e) => {
|
|
838
|
+
const data = JSON.parse(e.data);
|
|
839
|
+
const q = this.voiceQueues[data.messageId] || (this.voiceQueues[data.messageId] = { chunks: [], final: false, scheduled: false, roomId, messageId: data.messageId });
|
|
840
|
+
q.final = true;
|
|
841
|
+
q.messageId = data.messageId;
|
|
842
|
+
q.roomId = roomId;
|
|
843
|
+
this.drainVoiceQueue(roomId, data.messageId);
|
|
844
|
+
});
|
|
845
|
+
|
|
815
846
|
// Full body+meta replacement · used by tool-use rows whose
|
|
816
847
|
// status flips running → done|failed once the side-effect
|
|
817
848
|
// (URL fetch) completes. We re-render the affected message in
|
|
@@ -854,6 +885,11 @@
|
|
|
854
885
|
this.currentRoom.status = "paused";
|
|
855
886
|
this.currentRoom.pausedAt = ts;
|
|
856
887
|
}
|
|
888
|
+
// Hard pause: stop audio immediately (speaker was aborted mid-stream).
|
|
889
|
+
// Soft pause: let current audio finish naturally (speaker completed their turn).
|
|
890
|
+
if (payload.mode === "hard") {
|
|
891
|
+
this.stopVoicePlayback();
|
|
892
|
+
}
|
|
857
893
|
document.documentElement.classList.remove("pause-pending");
|
|
858
894
|
document.documentElement.setAttribute("data-status", "paused");
|
|
859
895
|
this.renderHeader();
|
|
@@ -955,13 +991,9 @@
|
|
|
955
991
|
language: payload.language === "zh" ? "zh" : "en",
|
|
956
992
|
pipelineStartedAt: Date.now(),
|
|
957
993
|
createdAt: Date.now(),
|
|
958
|
-
// Stage checklist · seeded according to the brief mode (see
|
|
959
|
-
// briefMode resolution above). brief-stage events flip
|
|
960
|
-
// them active → done as the pipeline progresses.
|
|
961
|
-
// startedAt is captured when each stage first becomes
|
|
962
|
-
// active so the UI can display elapsed time alongside the
|
|
963
|
-
// ETA range.
|
|
964
994
|
stages: seededStages,
|
|
995
|
+
llmLogs: [],
|
|
996
|
+
llmLogOpen: false,
|
|
965
997
|
};
|
|
966
998
|
this.currentBrief = newBrief;
|
|
967
999
|
// Insert the in-progress brief into currentBriefs so the tab
|
|
@@ -1067,6 +1099,51 @@
|
|
|
1067
1099
|
this.renderBrief();
|
|
1068
1100
|
}
|
|
1069
1101
|
}
|
|
1102
|
+
} else if (kind === "brief-llm-start") {
|
|
1103
|
+
this.markBriefEvent();
|
|
1104
|
+
const target = this._briefById(payload.briefId);
|
|
1105
|
+
if (target && payload.log) {
|
|
1106
|
+
const logs = target.llmLogs || (target.llmLogs = []);
|
|
1107
|
+
const idx = logs.findIndex((l) => l.id === payload.log.id);
|
|
1108
|
+
const log = { ...payload.log, text: payload.log.text || "" };
|
|
1109
|
+
if (idx >= 0) logs[idx] = log;
|
|
1110
|
+
else logs.push(log);
|
|
1111
|
+
if (this.currentBrief && this.currentBrief.id === target.id) this.renderBrief();
|
|
1112
|
+
}
|
|
1113
|
+
} else if (kind === "brief-llm-token") {
|
|
1114
|
+
this.markBriefEvent();
|
|
1115
|
+
const target = this._briefById(payload.briefId);
|
|
1116
|
+
if (target && payload.logId) {
|
|
1117
|
+
const logs = target.llmLogs || (target.llmLogs = []);
|
|
1118
|
+
const log = logs.find((l) => l.id === payload.logId);
|
|
1119
|
+
if (log) {
|
|
1120
|
+
const limit = 6000;
|
|
1121
|
+
if ((log.text || "").length < limit) {
|
|
1122
|
+
log.text = ((log.text || "") + (payload.delta || "")).slice(0, limit);
|
|
1123
|
+
}
|
|
1124
|
+
if (this.currentBrief && this.currentBrief.id === target.id && target.llmLogOpen) {
|
|
1125
|
+
const now = Date.now();
|
|
1126
|
+
if (!this._briefLlmLastRender || (now - this._briefLlmLastRender) > 500) {
|
|
1127
|
+
this._briefLlmLastRender = now;
|
|
1128
|
+
this.renderBrief();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
} else if (kind === "brief-llm-end") {
|
|
1134
|
+
this.markBriefEvent();
|
|
1135
|
+
const target = this._briefById(payload.briefId);
|
|
1136
|
+
if (target && payload.logId) {
|
|
1137
|
+
const logs = target.llmLogs || (target.llmLogs = []);
|
|
1138
|
+
const log = logs.find((l) => l.id === payload.logId);
|
|
1139
|
+
if (log) {
|
|
1140
|
+
log.status = payload.status || log.status;
|
|
1141
|
+
log.finishedAt = typeof payload.finishedAt === "number" ? payload.finishedAt : Date.now();
|
|
1142
|
+
if (typeof payload.totalTokens === "number") log.totalTokens = payload.totalTokens;
|
|
1143
|
+
if (typeof payload.error === "string") log.error = payload.error;
|
|
1144
|
+
if (this.currentBrief && this.currentBrief.id === target.id) this.renderBrief();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1070
1147
|
} else if (kind === "brief-token") {
|
|
1071
1148
|
this.markBriefEvent();
|
|
1072
1149
|
// Append tokens to the brief identified by payload.briefId,
|
|
@@ -1198,6 +1275,7 @@
|
|
|
1198
1275
|
if (ch.mode) this.currentRoom.mode = ch.mode.to;
|
|
1199
1276
|
if (ch.intensity) this.currentRoom.intensity = ch.intensity.to;
|
|
1200
1277
|
if (ch.briefStyle) this.currentRoom.briefStyle = ch.briefStyle.to;
|
|
1278
|
+
if (ch.deliveryMode) this.currentRoom.deliveryMode = ch.deliveryMode.to;
|
|
1201
1279
|
}
|
|
1202
1280
|
this.renderHeader();
|
|
1203
1281
|
syncSidebar({
|
|
@@ -1361,7 +1439,7 @@
|
|
|
1361
1439
|
},
|
|
1362
1440
|
|
|
1363
1441
|
// ── Actions ───────────────────────────────────────────────
|
|
1364
|
-
async createRoom({ subject, agentIds, mode, intensity, briefStyle, autoPick }) {
|
|
1442
|
+
async createRoom({ subject, agentIds, mode, intensity, briefStyle, autoPick, deliveryMode }) {
|
|
1365
1443
|
const r = await fetch("/api/rooms", {
|
|
1366
1444
|
method: "POST",
|
|
1367
1445
|
headers: { "content-type": "application/json" },
|
|
@@ -1371,6 +1449,7 @@
|
|
|
1371
1449
|
mode: mode || "constructive",
|
|
1372
1450
|
intensity: intensity || "sharp",
|
|
1373
1451
|
briefStyle: briefStyle || "auto",
|
|
1452
|
+
deliveryMode: deliveryMode === "voice" ? "voice" : "text",
|
|
1374
1453
|
...(autoPick ? { autoPick: true } : {}),
|
|
1375
1454
|
}),
|
|
1376
1455
|
});
|
|
@@ -1704,27 +1783,55 @@
|
|
|
1704
1783
|
return MODEL_PROVIDERS.some((p) => keys[p] && keys[p].configured);
|
|
1705
1784
|
},
|
|
1706
1785
|
|
|
1786
|
+
/** Pure cache read · true when at least one voice provider
|
|
1787
|
+
* (MiniMax / ElevenLabs) is configured. The composer's voice
|
|
1788
|
+
* toggle calls this before flipping ON; if false, we redirect
|
|
1789
|
+
* the user to the keys panel rather than silently enabling a
|
|
1790
|
+
* feature that has no provider behind it. */
|
|
1791
|
+
hasAnyVoiceKey() {
|
|
1792
|
+
const VOICE_PROVIDERS = ["minimax", "elevenlabs"];
|
|
1793
|
+
const keys = this.keys || {};
|
|
1794
|
+
return VOICE_PROVIDERS.some((p) => keys[p] && keys[p].configured);
|
|
1795
|
+
},
|
|
1796
|
+
|
|
1797
|
+
/** Secondary hint under a failed brief · the legacy copy always blamed
|
|
1798
|
+
* missing keys even when stage-2 scaffolding failed after successful
|
|
1799
|
+
* LLM calls. Route hints by substring of the server's `brief-error`
|
|
1800
|
+
* message (`NoKeyError` vs scaffold failures vs everything else). */
|
|
1801
|
+
_briefSecondaryHintHtml(errorText) {
|
|
1802
|
+
const s = String(errorText || "").toLowerCase();
|
|
1803
|
+
if (/no key configured|no openrouter fallback|add a key in preference/i.test(s)) {
|
|
1804
|
+
return this._t("brief_err_hint_failed_html");
|
|
1805
|
+
}
|
|
1806
|
+
if (
|
|
1807
|
+
/couldn't structure|report writer couldn't|json scaffold|repeat(ed)? attempts|retries failed|parse scaffold|\[brief\.stage2\]|stage 2/i.test(s)
|
|
1808
|
+
) {
|
|
1809
|
+
return this._t("brief_err_hint_scaffold_html");
|
|
1810
|
+
}
|
|
1811
|
+
return this._t("brief_err_hint_generic_html");
|
|
1812
|
+
},
|
|
1813
|
+
|
|
1707
1814
|
/** Modal that fires when an AI action is attempted but no model
|
|
1708
1815
|
* provider key is configured. Two CTAs · open settings (preferred)
|
|
1709
1816
|
* or dismiss. Same chrome family as openSendChoiceModal so the
|
|
1710
1817
|
* visual treatment stays consistent. */
|
|
1711
1818
|
openNoKeyModal() {
|
|
1712
1819
|
this.closeNoKeyModal();
|
|
1713
|
-
// System UI · always English. No-key modal is app chrome.
|
|
1714
1820
|
const t = {
|
|
1715
|
-
title: "
|
|
1716
|
-
deck:
|
|
1717
|
-
primary: "
|
|
1718
|
-
dismiss: "
|
|
1719
|
-
classification: "
|
|
1720
|
-
tag: "
|
|
1721
|
-
|
|
1821
|
+
title: this._t("nk_title"),
|
|
1822
|
+
deck: this._t("nk_deck"),
|
|
1823
|
+
primary: this._t("nk_primary"),
|
|
1824
|
+
dismiss: this._t("nk_dismiss"),
|
|
1825
|
+
classification: this._t("nk_classification"),
|
|
1826
|
+
tag: this._t("nk_tag"),
|
|
1827
|
+
primaryDeck: this._t("nk_primary_deck"),
|
|
1828
|
+
gate: this._t("nk_frontend_gate"), };
|
|
1722
1829
|
const html = `
|
|
1723
1830
|
<div id="no-key-overlay" class="pc-overlay" data-no-key-overlay>
|
|
1724
1831
|
<div class="pc-modal">
|
|
1725
1832
|
<div class="pc-classification">
|
|
1726
|
-
<span><span class="dot">●</span> ${this.escape(t.classification
|
|
1727
|
-
<span class="right"
|
|
1833
|
+
<span><span class="dot">●</span> ${this.escape(t.classification)}</span>
|
|
1834
|
+
<span class="right">${this.escape(t.gate)}</span>
|
|
1728
1835
|
</div>
|
|
1729
1836
|
<div class="pc-head">
|
|
1730
1837
|
<div class="pc-tag">${this.escape(t.tag)}</div>
|
|
@@ -1734,8 +1841,7 @@
|
|
|
1734
1841
|
<div class="pc-body">
|
|
1735
1842
|
<button type="button" class="pc-choice primary" data-no-key-open-settings>
|
|
1736
1843
|
<div class="pc-choice-mark">${this.escape(t.primary)}</div>
|
|
1737
|
-
<div class="pc-choice-deck"
|
|
1738
|
-
</button>
|
|
1844
|
+
<div class="pc-choice-deck">${this.escape(t.primaryDeck)}</div> </button>
|
|
1739
1845
|
<button type="button" class="pc-choice ghost" data-no-key-dismiss>
|
|
1740
1846
|
<div class="pc-choice-mark">${this.escape(t.dismiss)}</div>
|
|
1741
1847
|
</button>
|
|
@@ -1760,31 +1866,32 @@
|
|
|
1760
1866
|
const speaker = this.currentQueue[0]
|
|
1761
1867
|
? this.agentsById[this.currentQueue[0].agentId]
|
|
1762
1868
|
: null;
|
|
1763
|
-
const
|
|
1869
|
+
const speakerName = speaker ? speaker.name : this._t("sc_speaker_fallback");
|
|
1870
|
+
const speakerLabel = this.escape(speakerName);
|
|
1764
1871
|
const html = `
|
|
1765
1872
|
<div id="send-choice-overlay" class="pc-overlay">
|
|
1766
1873
|
<div class="pc-modal">
|
|
1767
1874
|
<div class="pc-classification">
|
|
1768
|
-
<span><span class="dot">●</span>
|
|
1769
|
-
<span class="right"
|
|
1875
|
+
<span><span class="dot">●</span> ${this.escape(this._t("sc_send_class"))}</span>
|
|
1876
|
+
<span class="right">${this.escape(this._t("sc_send_right", { name: speakerName }))}</span>
|
|
1770
1877
|
</div>
|
|
1771
1878
|
<div class="pc-head">
|
|
1772
|
-
<div class="pc-tag"
|
|
1773
|
-
<h2 class="pc-title">${
|
|
1774
|
-
<p class="pc-deck"
|
|
1879
|
+
<div class="pc-tag">${this.escape(this._t("sc_send_tag"))}</div>
|
|
1880
|
+
<h2 class="pc-title">${this.escape(this._t("sc_send_title", { name: speakerName }))}</h2>
|
|
1881
|
+
<p class="pc-deck">${this.escape(this._t("sc_send_deck"))}</p>
|
|
1775
1882
|
</div>
|
|
1776
1883
|
<div class="pc-body">
|
|
1777
1884
|
<button type="button" class="pc-choice" data-send-choice="interrupt">
|
|
1778
|
-
<div class="pc-choice-mark"
|
|
1779
|
-
<div class="pc-choice-deck"
|
|
1885
|
+
<div class="pc-choice-mark">${this.escape(this._t("sc_interrupt_mark"))}</div>
|
|
1886
|
+
<div class="pc-choice-deck">${this.escape(this._t("sc_interrupt_deck", { name: speakerName }))}</div>
|
|
1780
1887
|
</button>
|
|
1781
1888
|
<button type="button" class="pc-choice primary" data-send-choice="queue">
|
|
1782
|
-
<div class="pc-choice-mark"
|
|
1783
|
-
<div class="pc-choice-deck"
|
|
1889
|
+
<div class="pc-choice-mark">${this.escape(this._t("sc_queue_mark", { name: speakerName }))}</div>
|
|
1890
|
+
<div class="pc-choice-deck">${this.escape(this._t("sc_queue_deck"))}</div>
|
|
1784
1891
|
</button>
|
|
1785
1892
|
<button type="button" class="pc-choice ghost" data-send-choice="cancel">
|
|
1786
|
-
<div class="pc-choice-mark"
|
|
1787
|
-
<div class="pc-choice-deck"
|
|
1893
|
+
<div class="pc-choice-mark">${this.escape(this._t("sc_cancel_mark"))}</div>
|
|
1894
|
+
<div class="pc-choice-deck">${this.escape(this._t("sc_cancel_deck"))}</div>
|
|
1788
1895
|
</button>
|
|
1789
1896
|
</div>
|
|
1790
1897
|
</div>
|
|
@@ -1898,15 +2005,21 @@
|
|
|
1898
2005
|
const room = this.currentRoom || {};
|
|
1899
2006
|
const turns = (this.currentMessages || []).filter((m) => m.body && m.body.trim()).length;
|
|
1900
2007
|
const status = room.status || "live";
|
|
1901
|
-
const titleTxt = isGen ? "
|
|
1902
|
-
const classifyTxt = isGen ? "
|
|
1903
|
-
const classifyRight = isGen ? "
|
|
1904
|
-
const confirmTxt = isGen ? "
|
|
2008
|
+
const titleTxt = isGen ? this._t("adj_title_generate") : this._t("adj_title_file");
|
|
2009
|
+
const classifyTxt = isGen ? this._t("adj_classify_generate") : this._t("adj_classify_adjourn");
|
|
2010
|
+
const classifyRight = isGen ? this._t("adj_classify_right_posthoc") : this._t("adj_classify_right_terminal");
|
|
2011
|
+
const confirmTxt = isGen ? this._t("adj_confirm_generate") : this._t("adj_confirm_file");
|
|
1905
2012
|
const subjectTxt = room.subject || room.name || "—";
|
|
1906
2013
|
const memberCount = (this.currentMembers || []).length;
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2014
|
+
const statusLabel = this._t(
|
|
2015
|
+
status === "paused" ? "adj_meta_status_paused"
|
|
2016
|
+
: status === "adjourned" ? "adj_meta_status_adjourned"
|
|
2017
|
+
: "adj_meta_status_live",
|
|
2018
|
+
);
|
|
2019
|
+
const roomKicker = this._t("adj_meta_room_kicker");
|
|
2020
|
+
const metaSep = this._t("adj_meta_sep");
|
|
2021
|
+
const metaTurns = this._t("adj_meta_turns", { n: turns });
|
|
2022
|
+
const noteTxt = isGen ? this._t("adj_note_generate") : this._t("adj_note_adjourn");
|
|
1910
2023
|
const defaultMode = this.lastBriefMode();
|
|
1911
2024
|
const html = `
|
|
1912
2025
|
<div class="adjourn-overlay" id="adjourn-overlay" role="dialog" aria-modal="true" data-adjourn-mode="${this.escape(mode)}">
|
|
@@ -1920,34 +2033,32 @@
|
|
|
1920
2033
|
|
|
1921
2034
|
<header class="adjourn-head">
|
|
1922
2035
|
<div>
|
|
1923
|
-
<div class="meta"
|
|
2036
|
+
<div class="meta">${this.escape(roomKicker)}<span>${this.escape(String(room.number ?? "—"))}</span>${this.escape(metaSep)}<span class="${status === "live" ? "live" : "status"}">${this.escape(statusLabel)}</span>${this.escape(metaSep)}${this.escape(metaTurns)}</div>
|
|
1924
2037
|
<div class="title">${this.escape(titleTxt)}</div>
|
|
1925
2038
|
</div>
|
|
1926
|
-
<button type="button" class="adjourn-close" data-adjourn-close aria-label="
|
|
2039
|
+
<button type="button" class="adjourn-close" data-adjourn-close aria-label="${this.escape(this._t("adj_close_aria"))}">✕</button>
|
|
1927
2040
|
</header>
|
|
1928
2041
|
|
|
1929
2042
|
<div class="adjourn-body">
|
|
1930
2043
|
<div class="adjourn-summary">
|
|
1931
|
-
|
|
1932
|
-
<span class="adjourn-summary-key"
|
|
2044
|
+
<div class="adjourn-summary-row adjourn-summary-row-subject">
|
|
2045
|
+
<span class="adjourn-summary-key">${this.escape(this._t("adj_key_subject"))}</span>
|
|
1933
2046
|
<div class="adjourn-summary-val adjourn-subject-wrap">
|
|
1934
2047
|
<span class="adjourn-subject-text is-clamped" data-adjourn-subject>${this.escape(subjectTxt)}</span>
|
|
1935
2048
|
<button type="button" class="adjourn-subject-toggle" data-adjourn-subject-toggle hidden>Show more</button>
|
|
1936
2049
|
</div>
|
|
1937
2050
|
</div>
|
|
1938
2051
|
<div class="adjourn-summary-row">
|
|
1939
|
-
<span class="adjourn-summary-key"
|
|
1940
|
-
<span class="adjourn-summary-val">${memberCount}
|
|
2052
|
+
<span class="adjourn-summary-key">${this.escape(this._t("adj_key_authors"))}</span>
|
|
2053
|
+
<span class="adjourn-summary-val">${this.escape(this._t("adj_agents_count", { n: memberCount }))}</span>
|
|
1941
2054
|
</div>
|
|
1942
2055
|
<div class="adjourn-summary-row">
|
|
1943
|
-
<span class="adjourn-summary-key"
|
|
2056
|
+
<span class="adjourn-summary-key">${this.escape(this._t("adj_key_turns"))}</span>
|
|
1944
2057
|
<span class="adjourn-summary-val">${turns}</span>
|
|
1945
2058
|
</div>
|
|
1946
2059
|
</div>
|
|
1947
2060
|
<p class="adjourn-summary-note">
|
|
1948
|
-
|
|
1949
|
-
format below — the room is marked adjourned and the report is
|
|
1950
|
-
filed in the chat once it's ready.
|
|
2061
|
+
${this.escape(noteTxt)}
|
|
1951
2062
|
</p>
|
|
1952
2063
|
|
|
1953
2064
|
${this.renderBriefModePicker(defaultMode)}
|
|
@@ -1957,10 +2068,10 @@
|
|
|
1957
2068
|
${isGen ? `<span class="adjourn-skip-spacer"></span>` : `
|
|
1958
2069
|
<button type="button" class="adjourn-skip-btn" data-adjourn-skip>
|
|
1959
2070
|
<span class="adjourn-skip-mark">⊘</span>
|
|
1960
|
-
<span
|
|
2071
|
+
<span>${this.escape(this._t("adj_skip"))}</span>
|
|
1961
2072
|
</button>`}
|
|
1962
2073
|
<div class="adjourn-foot-actions">
|
|
1963
|
-
<button type="button" class="adjourn-cancel" data-adjourn-close
|
|
2074
|
+
<button type="button" class="adjourn-cancel" data-adjourn-close>${this.escape(this._t("adj_cancel"))}</button>
|
|
1964
2075
|
<button type="button" class="adjourn-confirm" data-adjourn-confirm>${this.escape(confirmTxt)}</button>
|
|
1965
2076
|
</div>
|
|
1966
2077
|
</footer>
|
|
@@ -2230,31 +2341,50 @@
|
|
|
2230
2341
|
if (this.currentRoom.status !== "adjourned") return;
|
|
2231
2342
|
this.closeFollowUpOverlay();
|
|
2232
2343
|
|
|
2233
|
-
|
|
2234
|
-
const t =
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2344
|
+
const lang = this.composerLanguage();
|
|
2345
|
+
const t = lang === "zh"
|
|
2346
|
+
? {
|
|
2347
|
+
classify: "follow-up · 跟进会议",
|
|
2348
|
+
classifyRight: "// continuing",
|
|
2349
|
+
title: "开一场跟进会议",
|
|
2350
|
+
metaPrefix: "// following up",
|
|
2351
|
+
placeholder: "在上一场判断之上,下一个要追问的问题是什么?",
|
|
2352
|
+
contextNote: "上一场的议题、最终判断(brief)和每位 director 的关键观察会作为这场 follow-up 房间的上下文交给新一组 director —— 他们可以直接在已成型的判断上推进,不会从零开始。",
|
|
2353
|
+
castLabel: "Directors",
|
|
2354
|
+
castHint: "建议 2-4 位",
|
|
2355
|
+
castSame: "沿用上一场的 cast",
|
|
2356
|
+
pickerLabel: "选择董事",
|
|
2357
|
+
autoLabel: "directors",
|
|
2358
|
+
autoVal: "自动挑选",
|
|
2359
|
+
countersDirectors: (n) => `${n} 位董事`,
|
|
2360
|
+
cancel: "[ Cancel ]",
|
|
2361
|
+
confirm: "[ Convene → ]",
|
|
2362
|
+
confirmBusy: "[ Convening… ]",
|
|
2363
|
+
adjournedAtPrefix: "adjourned",
|
|
2364
|
+
briefsCount: (n) => `${n} ${n === 1 ? "brief" : "briefs"} filed`,
|
|
2365
|
+
noBrief: "no brief filed",
|
|
2366
|
+
}
|
|
2367
|
+
: {
|
|
2368
|
+
classify: "follow-up · continuation room",
|
|
2369
|
+
classifyRight: "// continuing",
|
|
2370
|
+
title: "Convene a follow-up",
|
|
2371
|
+
metaPrefix: "// following up",
|
|
2372
|
+
placeholder: "What's the next question to chase, given what the prior session settled?",
|
|
2373
|
+
contextNote: "The prior subject, the filed brief (room's settled judgement), and each director's load-bearing observations are bundled as context for this follow-up — the new cast picks up where the prior session left off rather than starting from scratch.",
|
|
2374
|
+
castLabel: "Directors",
|
|
2375
|
+
castHint: "2–4 recommended",
|
|
2376
|
+
castSame: "Same cast as last session",
|
|
2377
|
+
pickerLabel: "Pick directors",
|
|
2378
|
+
autoLabel: "directors",
|
|
2379
|
+
autoVal: "auto-pick",
|
|
2380
|
+
countersDirectors: (n) => `${n} director${n === 1 ? "" : "s"}`,
|
|
2381
|
+
cancel: "[ Cancel ]",
|
|
2382
|
+
confirm: "[ Convene → ]",
|
|
2383
|
+
confirmBusy: "[ Convening… ]",
|
|
2384
|
+
adjournedAtPrefix: "adjourned",
|
|
2385
|
+
briefsCount: (n) => `${n} ${n === 1 ? "brief" : "briefs"} filed`,
|
|
2386
|
+
noBrief: "no brief filed",
|
|
2387
|
+
};
|
|
2258
2388
|
const room = this.currentRoom;
|
|
2259
2389
|
const briefCount = Array.isArray(this.currentBriefs) ? this.currentBriefs.length : 0;
|
|
2260
2390
|
const briefLine = briefCount > 0 ? t.briefsCount(briefCount) : t.noBrief;
|
|
@@ -2345,13 +2475,13 @@
|
|
|
2345
2475
|
</span>
|
|
2346
2476
|
</button>
|
|
2347
2477
|
<span class="followup-cast-row-sep" aria-hidden="true"></span>
|
|
2348
|
-
<button type="button" class="cmp-dd" data-cmp-dropdown="tone" title="${this.escape(
|
|
2349
|
-
<span class="cmp-dd-label"
|
|
2478
|
+
<button type="button" class="cmp-dd" data-cmp-dropdown="tone" title="${this.escape(this._t("cmp_tone_label"))}">
|
|
2479
|
+
<span class="cmp-dd-label">${this.escape(this._t("cmp_tone_label"))}</span>
|
|
2350
2480
|
<span class="cmp-dd-value" data-cmp-dd-value="tone">${this.escape(inheritedMode)}</span>
|
|
2351
2481
|
<span class="cmp-dd-chevron">▾</span>
|
|
2352
2482
|
</button>
|
|
2353
|
-
<button type="button" class="cmp-dd" data-cmp-dropdown="intensity" title="${this.escape(
|
|
2354
|
-
<span class="cmp-dd-label"
|
|
2483
|
+
<button type="button" class="cmp-dd" data-cmp-dropdown="intensity" title="${this.escape(this._t("cmp_intensity_label"))}">
|
|
2484
|
+
<span class="cmp-dd-label">${this.escape(this._t("cmp_intensity_label"))}</span>
|
|
2355
2485
|
<span class="cmp-dd-value" data-cmp-dd-value="intensity">${this.escape(inheritedIntensity)}</span>
|
|
2356
2486
|
<span class="cmp-dd-chevron">▾</span>
|
|
2357
2487
|
</button>
|
|
@@ -2570,7 +2700,7 @@
|
|
|
2570
2700
|
<span class="composer-pick-title">${this.escape(t.title)}</span>
|
|
2571
2701
|
<span class="composer-pick-hint">${this.escape(t.hint)}</span>
|
|
2572
2702
|
</div>
|
|
2573
|
-
<div class="composer-pick-list">${rows || `<div class="composer-pick-empty"
|
|
2703
|
+
<div class="composer-pick-list">${rows || `<div class="composer-pick-empty">${this.escape(this._t("picker_no_directors"))}</div>`}</div>
|
|
2574
2704
|
`;
|
|
2575
2705
|
document.body.appendChild(pop);
|
|
2576
2706
|
const r = anchorBtn.getBoundingClientRect();
|
|
@@ -2884,6 +3014,20 @@
|
|
|
2884
3014
|
target.progress = incoming.progress || target.progress;
|
|
2885
3015
|
target.etaSec = (incoming.etaSec && typeof incoming.etaSec.lo === "number") ? incoming.etaSec : target.etaSec;
|
|
2886
3016
|
}
|
|
3017
|
+
if (Array.isArray(state.llmLogs)) {
|
|
3018
|
+
brief.llmLogs = state.llmLogs.map((l) => ({
|
|
3019
|
+
id: String(l.id || ""),
|
|
3020
|
+
stage: String(l.stage || ""),
|
|
3021
|
+
label: String(l.label || ""),
|
|
3022
|
+
modelV: String(l.modelV || ""),
|
|
3023
|
+
status: l.status === "done" || l.status === "failed" ? l.status : "running",
|
|
3024
|
+
startedAt: typeof l.startedAt === "number" ? l.startedAt : Date.now(),
|
|
3025
|
+
finishedAt: typeof l.finishedAt === "number" ? l.finishedAt : null,
|
|
3026
|
+
text: String(l.text || ""),
|
|
3027
|
+
totalTokens: typeof l.totalTokens === "number" ? l.totalTokens : null,
|
|
3028
|
+
error: typeof l.error === "string" ? l.error : null,
|
|
3029
|
+
})).filter((l) => l.id);
|
|
3030
|
+
}
|
|
2887
3031
|
},
|
|
2888
3032
|
|
|
2889
3033
|
/** Delete a single brief from this room's history. Asks for
|
|
@@ -2956,12 +3100,25 @@
|
|
|
2956
3100
|
}
|
|
2957
3101
|
} catch { /* swallow */ }
|
|
2958
3102
|
}
|
|
3103
|
+
// Preserve the failed brief's mode on retry · without this
|
|
3104
|
+
// the POST defaulted back to `research-note` regardless of
|
|
3105
|
+
// whether the user originally picked Magazine / Newspaper /
|
|
3106
|
+
// Slides. Same goes for the supplement string (regenerate-
|
|
3107
|
+
// with-perspective flow). Anything else (style etc) is
|
|
3108
|
+
// re-derived server-side from the room's stored config.
|
|
3109
|
+
const retryBody = {};
|
|
3110
|
+
if (failed && this.isStructuredBriefMode(failed.mode)) {
|
|
3111
|
+
retryBody.mode = failed.mode;
|
|
3112
|
+
}
|
|
3113
|
+
if (failed && typeof failed.supplement === "string" && failed.supplement.trim()) {
|
|
3114
|
+
retryBody.supplement = failed.supplement;
|
|
3115
|
+
}
|
|
2959
3116
|
const r = await fetch(
|
|
2960
3117
|
"/api/rooms/" + encodeURIComponent(this.currentRoomId) + "/brief",
|
|
2961
3118
|
{
|
|
2962
3119
|
method: "POST",
|
|
2963
3120
|
headers: { "content-type": "application/json" },
|
|
2964
|
-
body:
|
|
3121
|
+
body: JSON.stringify(retryBody),
|
|
2965
3122
|
},
|
|
2966
3123
|
);
|
|
2967
3124
|
if (!r.ok) {
|
|
@@ -3004,8 +3161,8 @@
|
|
|
3004
3161
|
const briefMode = this.isStructuredBriefMode(v) ? v : "research-note";
|
|
3005
3162
|
this.saveLastBriefMode(briefMode);
|
|
3006
3163
|
const btn = overlay.querySelector("[data-adjourn-confirm]");
|
|
3007
|
-
const origLabel = isGen ? "
|
|
3008
|
-
const busyLabel = isGen ? "
|
|
3164
|
+
const origLabel = isGen ? this._t("adj_confirm_generate") : this._t("adj_confirm_file");
|
|
3165
|
+
const busyLabel = isGen ? this._t("adj_busy_generate") : this._t("adj_busy_adjourn");
|
|
3009
3166
|
if (btn) { btn.disabled = true; btn.textContent = busyLabel; }
|
|
3010
3167
|
try {
|
|
3011
3168
|
if (isGen) {
|
|
@@ -3018,7 +3175,7 @@
|
|
|
3018
3175
|
this.closeAdjournOverlay();
|
|
3019
3176
|
} catch (e) {
|
|
3020
3177
|
if (btn) { btn.disabled = false; btn.textContent = origLabel; }
|
|
3021
|
-
alert((isGen ? "
|
|
3178
|
+
alert((isGen ? this._t("adj_err_generate") : this._t("adj_err_adjourn")) + (e && e.message ? e.message : e));
|
|
3022
3179
|
}
|
|
3023
3180
|
},
|
|
3024
3181
|
|
|
@@ -3046,6 +3203,27 @@
|
|
|
3046
3203
|
}
|
|
3047
3204
|
},
|
|
3048
3205
|
|
|
3206
|
+
async toggleDeliveryMode() {
|
|
3207
|
+
if (!this.currentRoomId || !this.currentRoom) return;
|
|
3208
|
+
const next = this.currentRoom.deliveryMode === "voice" ? "text" : "voice";
|
|
3209
|
+
// Unlock audio if switching to voice
|
|
3210
|
+
if (next === "voice") this.unlockAudioPlayback();
|
|
3211
|
+
try {
|
|
3212
|
+
const r = await fetch(
|
|
3213
|
+
"/api/rooms/" + encodeURIComponent(this.currentRoomId),
|
|
3214
|
+
{
|
|
3215
|
+
method: "PATCH",
|
|
3216
|
+
headers: { "content-type": "application/json" },
|
|
3217
|
+
body: JSON.stringify({ deliveryMode: next }),
|
|
3218
|
+
},
|
|
3219
|
+
);
|
|
3220
|
+
if (r.ok) {
|
|
3221
|
+
this.currentRoom.deliveryMode = next;
|
|
3222
|
+
this.renderHeader();
|
|
3223
|
+
}
|
|
3224
|
+
} catch (_) { /* offline */ }
|
|
3225
|
+
},
|
|
3226
|
+
|
|
3049
3227
|
async pauseRoom(mode) {
|
|
3050
3228
|
if (!this.currentRoomId) return;
|
|
3051
3229
|
// Soft pause: flip the input bar to a "pausing after current turn"
|
|
@@ -3110,31 +3288,32 @@
|
|
|
3110
3288
|
const speaker = this.currentQueue[0]
|
|
3111
3289
|
? this.agentsById[this.currentQueue[0].agentId]
|
|
3112
3290
|
: null;
|
|
3113
|
-
const
|
|
3291
|
+
const speakerName = speaker ? speaker.name : this._t("sc_speaker_fallback");
|
|
3292
|
+
const speakerLabel = this.escape(speakerName);
|
|
3114
3293
|
const html = `
|
|
3115
3294
|
<div id="pause-choice-overlay" class="pc-overlay">
|
|
3116
3295
|
<div class="pc-modal">
|
|
3117
3296
|
<div class="pc-classification">
|
|
3118
|
-
<span><span class="dot">●</span>
|
|
3119
|
-
<span class="right"
|
|
3297
|
+
<span><span class="dot">●</span> ${this.escape(this._t("pause_class"))}</span>
|
|
3298
|
+
<span class="right">${this.escape(this._t("pause_right"))}</span>
|
|
3120
3299
|
</div>
|
|
3121
3300
|
<div class="pc-head">
|
|
3122
|
-
<div class="pc-tag"
|
|
3123
|
-
<h2 class="pc-title">${
|
|
3124
|
-
<p class="pc-deck"
|
|
3301
|
+
<div class="pc-tag">${this.escape(this._t("pause_tag"))}</div>
|
|
3302
|
+
<h2 class="pc-title">${this.escape(this._t("pause_title", { name: speakerName }))}</h2>
|
|
3303
|
+
<p class="pc-deck">${this.escape(this._t("pause_deck"))}</p>
|
|
3125
3304
|
</div>
|
|
3126
3305
|
<div class="pc-body">
|
|
3127
3306
|
<button type="button" class="pc-choice danger" data-pause-choice="hard">
|
|
3128
|
-
<div class="pc-choice-mark"
|
|
3129
|
-
<div class="pc-choice-deck"
|
|
3307
|
+
<div class="pc-choice-mark">${this.escape(this._t("pause_hard_mark"))}</div>
|
|
3308
|
+
<div class="pc-choice-deck">${this.escape(this._t("pause_hard_deck"))}</div>
|
|
3130
3309
|
</button>
|
|
3131
3310
|
<button type="button" class="pc-choice primary" data-pause-choice="soft">
|
|
3132
|
-
<div class="pc-choice-mark"
|
|
3133
|
-
<div class="pc-choice-deck"
|
|
3311
|
+
<div class="pc-choice-mark">${this.escape(this._t("pause_soft_mark"))}</div>
|
|
3312
|
+
<div class="pc-choice-deck">${this.escape(this._t("pause_soft_deck", { name: speakerName }))}</div>
|
|
3134
3313
|
</button>
|
|
3135
3314
|
<button type="button" class="pc-choice ghost" data-pause-choice="cancel">
|
|
3136
|
-
<div class="pc-choice-mark"
|
|
3137
|
-
<div class="pc-choice-deck"
|
|
3315
|
+
<div class="pc-choice-mark">${this.escape(this._t("pause_cancel_mark"))}</div>
|
|
3316
|
+
<div class="pc-choice-deck">${this.escape(this._t("pause_cancel_deck"))}</div>
|
|
3138
3317
|
</button>
|
|
3139
3318
|
</div>
|
|
3140
3319
|
</div>
|
|
@@ -3773,9 +3952,10 @@
|
|
|
3773
3952
|
const adj = this.rooms.filter((r) => r.status === "adjourned");
|
|
3774
3953
|
|
|
3775
3954
|
const renderRow = (r) => {
|
|
3955
|
+
const pausedLbl = this._t("sidebar_paused");
|
|
3776
3956
|
const status =
|
|
3777
3957
|
r.status === "paused"
|
|
3778
|
-
?
|
|
3958
|
+
? `<span class="row-status paused">❚❚ ${this.escape(pausedLbl)}</span>`
|
|
3779
3959
|
: "";
|
|
3780
3960
|
const time =
|
|
3781
3961
|
r.status === "paused"
|
|
@@ -3799,20 +3979,19 @@
|
|
|
3799
3979
|
<div class="row-content">
|
|
3800
3980
|
<div class="row-top-line">
|
|
3801
3981
|
<span class="row-title">${this.escape(fullTitle)}</span>
|
|
3802
|
-
<span class="row-time">${this.escape(time)}</span>
|
|
3803
3982
|
</div>
|
|
3804
3983
|
<div class="row-subtitle">${status}${this.escape(r.subject || "")}</div>
|
|
3805
3984
|
</div>
|
|
3806
3985
|
</a>
|
|
3807
|
-
<button type="button" class="row-delete" data-room-delete title="
|
|
3986
|
+
<button type="button" class="row-delete" data-room-delete title="${this.escape(this._t("sidebar_delete_room"))}">✕</button>
|
|
3808
3987
|
</div>
|
|
3809
3988
|
`;
|
|
3810
3989
|
};
|
|
3811
3990
|
|
|
3812
|
-
|
|
3991
|
+
const html = `
|
|
3813
3992
|
${live.length > 0 ? `
|
|
3814
3993
|
<div class="section-header live">
|
|
3815
|
-
<span
|
|
3994
|
+
<span>${this.escape(this._t("sidebar_section_live"))}</span>
|
|
3816
3995
|
<span class="line"></span>
|
|
3817
3996
|
<span class="badge">${live.length}</span>
|
|
3818
3997
|
</div>
|
|
@@ -3821,7 +4000,7 @@
|
|
|
3821
4000
|
|
|
3822
4001
|
${paused.length > 0 ? `
|
|
3823
4002
|
<div class="section-header paused">
|
|
3824
|
-
<span
|
|
4003
|
+
<span>${this.escape(this._t("sidebar_section_paused"))}</span>
|
|
3825
4004
|
<span class="line"></span>
|
|
3826
4005
|
<span class="badge">${paused.length}</span>
|
|
3827
4006
|
</div>
|
|
@@ -3829,7 +4008,7 @@
|
|
|
3829
4008
|
` : ""}
|
|
3830
4009
|
|
|
3831
4010
|
<div class="section-header adjourned">
|
|
3832
|
-
<span
|
|
4011
|
+
<span>${this.escape(this._t("sidebar_section_adjourned"))}</span>
|
|
3833
4012
|
<span class="line"></span>
|
|
3834
4013
|
<span class="badge" data-adjourned-count>${adj.length}</span>
|
|
3835
4014
|
</div>
|
|
@@ -3838,11 +4017,24 @@
|
|
|
3838
4017
|
</div>
|
|
3839
4018
|
<div class="adjourned-empty" data-adjourned-empty ${adj.length > 0 ? "hidden" : ""}>
|
|
3840
4019
|
<div class="adjourned-empty-mark">○</div>
|
|
3841
|
-
<div class="adjourned-empty-title"
|
|
3842
|
-
<div class="adjourned-empty-deck"
|
|
4020
|
+
<div class="adjourned-empty-title">${this.escape(this._t("sidebar_no_adjourned_title"))}</div>
|
|
4021
|
+
<div class="adjourned-empty-deck">${this.escape(this._t("sidebar_no_adjourned_deck"))}</div>
|
|
3843
4022
|
</div>
|
|
3844
4023
|
`;
|
|
3845
4024
|
|
|
4025
|
+
// SSE handlers (room.updated / message.appended / brief.appended)
|
|
4026
|
+
// call renderSidebarRooms unconditionally; many of those events
|
|
4027
|
+
// don't actually change anything visible in the sidebar. A no-op
|
|
4028
|
+
// innerHTML rewrite still tears down + recreates every anchor,
|
|
4029
|
+
// which kills any in-flight click whose mousedown/mouseup
|
|
4030
|
+
// straddle the wipe — symptom: "I clicked a room and nothing
|
|
4031
|
+
// happened, had to refresh." Skip the write when the rendered
|
|
4032
|
+
// string is identical to last time.
|
|
4033
|
+
if (this._sidebarRoomsHtml !== html) {
|
|
4034
|
+
this._sidebarRoomsHtml = html;
|
|
4035
|
+
list.innerHTML = html;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
3846
4038
|
this.markActiveRoom(this.currentRoomId);
|
|
3847
4039
|
},
|
|
3848
4040
|
|
|
@@ -3968,7 +4160,7 @@
|
|
|
3968
4160
|
const status = "active"; // every persisted director is active in v1
|
|
3969
4161
|
const time = this.relTime(a.createdAt) || "—";
|
|
3970
4162
|
const pinBtn = `
|
|
3971
|
-
<button type="button" class="pin-toggle" title="${a.isPinned ? "
|
|
4163
|
+
<button type="button" class="pin-toggle" title="${this.escape(a.isPinned ? this._t("sidebar_unpin") : this._t("sidebar_pin"))}" data-pin-toggle>${PIN_GLYPH}</button>
|
|
3972
4164
|
`;
|
|
3973
4165
|
// Delete moved off the sidebar row · it now lives inside the
|
|
3974
4166
|
// agent profile's ⋯ overflow menu where it's protected by the
|
|
@@ -3981,13 +4173,12 @@
|
|
|
3981
4173
|
<div class="agent-row-content">
|
|
3982
4174
|
<div class="agent-row-top-line">
|
|
3983
4175
|
<span class="agent-row-title">${this.escape(a.name)}</span>
|
|
3984
|
-
<span class="agent-row-time">${this.escape(time)}</span>
|
|
3985
4176
|
${pinBtn}
|
|
3986
4177
|
</div>
|
|
3987
4178
|
<div class="agent-row-subtitle">
|
|
3988
|
-
<span>${this.escape(a.roleTag || "
|
|
4179
|
+
<span>${this.escape(a.roleTag || this._t("sidebar_role_director"))}</span>
|
|
3989
4180
|
<span class="agent-row-sep">·</span>
|
|
3990
|
-
<span class="agent-row-status">${this.escape(
|
|
4181
|
+
<span class="agent-row-status">${this.escape(this._t("sidebar_status_active"))}</span>
|
|
3991
4182
|
</div>
|
|
3992
4183
|
</div>
|
|
3993
4184
|
</a>
|
|
@@ -4006,12 +4197,10 @@
|
|
|
4006
4197
|
<div class="agent-row-content">
|
|
4007
4198
|
<div class="agent-row-top-line">
|
|
4008
4199
|
<span class="agent-row-title">${this.escape(a.name)}</span>
|
|
4009
|
-
<span class="agent-row-chair-badge" title="
|
|
4200
|
+
<span class="agent-row-chair-badge" title="${this.escape(this._t("sidebar_chair_badge_title"))}">${this.escape(this._t("sidebar_chair_badge"))}</span>
|
|
4010
4201
|
</div>
|
|
4011
4202
|
<div class="agent-row-subtitle">
|
|
4012
|
-
<span class="agent-row-chair-role">${this.escape(a.roleTag
|
|
4013
|
-
<span class="agent-row-sep">·</span>
|
|
4014
|
-
<span class="agent-row-chair-note">in every room</span>
|
|
4203
|
+
<span class="agent-row-chair-role">${this.escape((a.roleTag && String(a.roleTag).toLowerCase() === "moderator") ? this._t("agent_role_tag_moderator") : (a.roleTag || this._t("sidebar_chair_role_fallback")))}</span>
|
|
4015
4204
|
</div>
|
|
4016
4205
|
</div>
|
|
4017
4206
|
</a>
|
|
@@ -4040,22 +4229,30 @@
|
|
|
4040
4229
|
// user immediately sees the orchestrator, and so it can't get
|
|
4041
4230
|
// grouped with directors they pin or create.
|
|
4042
4231
|
if (this.currentChair) {
|
|
4043
|
-
parts.push(sectionHeader("
|
|
4232
|
+
parts.push(sectionHeader(this._t("sidebar_sec_chair"), 1, "chair"));
|
|
4044
4233
|
parts.push(renderChairRow(this.currentChair));
|
|
4045
4234
|
}
|
|
4046
4235
|
if (pinned.length) {
|
|
4047
|
-
parts.push(sectionHeader("
|
|
4236
|
+
parts.push(sectionHeader(this._t("sidebar_sec_pinned"), pinned.length, "pinned"));
|
|
4048
4237
|
parts.push(pinned.map((a) => renderRow(a)).join(""));
|
|
4049
4238
|
}
|
|
4050
4239
|
if (custom.length) {
|
|
4051
|
-
parts.push(sectionHeader("
|
|
4240
|
+
parts.push(sectionHeader(this._t("sidebar_sec_custom"), custom.length));
|
|
4052
4241
|
parts.push(custom.map((a) => renderRow(a)).join(""));
|
|
4053
4242
|
}
|
|
4054
4243
|
if (core.length) {
|
|
4055
|
-
parts.push(sectionHeader("
|
|
4244
|
+
parts.push(sectionHeader(this._t("sidebar_sec_core"), core.length));
|
|
4056
4245
|
parts.push(core.map((a) => renderRow(a)).join(""));
|
|
4057
4246
|
}
|
|
4058
|
-
|
|
4247
|
+
// Same idempotency guard as renderSidebarRooms · skip the
|
|
4248
|
+
// innerHTML wipe when nothing has actually changed, so a
|
|
4249
|
+
// background SSE re-render doesn't drop in-flight clicks on
|
|
4250
|
+
// agent rows.
|
|
4251
|
+
const html = parts.join("");
|
|
4252
|
+
if (this._sidebarAgentsHtml !== html) {
|
|
4253
|
+
this._sidebarAgentsHtml = html;
|
|
4254
|
+
list.innerHTML = html;
|
|
4255
|
+
}
|
|
4059
4256
|
},
|
|
4060
4257
|
|
|
4061
4258
|
renderUserBlock() {
|
|
@@ -4068,7 +4265,7 @@
|
|
|
4068
4265
|
const firstLine = intro.split(/[\n.·]/)[0].trim();
|
|
4069
4266
|
meta = firstLine.length > 32 ? firstLine.slice(0, 30) + "…" : firstLine;
|
|
4070
4267
|
} else {
|
|
4071
|
-
meta = "
|
|
4268
|
+
meta = this._t("sidebar_host");
|
|
4072
4269
|
}
|
|
4073
4270
|
|
|
4074
4271
|
// Avatar source-of-truth · prefs.avatarSeed (set by the
|
|
@@ -4281,23 +4478,19 @@
|
|
|
4281
4478
|
const briefCard = document.querySelector("[data-brief-card]");
|
|
4282
4479
|
if (!briefCard) return;
|
|
4283
4480
|
|
|
4284
|
-
// System UI · always English. Session-analytics tile chrome
|
|
4285
|
-
// (banner, metric labels, section heads, chip text) is part of
|
|
4286
|
-
// the app shell and doesn't follow the brief language.
|
|
4287
4481
|
const t = {
|
|
4288
|
-
head: "
|
|
4289
|
-
stamp: "
|
|
4290
|
-
tokens: "
|
|
4291
|
-
messages: "
|
|
4292
|
-
rounds: "
|
|
4293
|
-
minutes: "
|
|
4294
|
-
modelHead: "
|
|
4295
|
-
valueHead: "
|
|
4296
|
-
valueEmpty:
|
|
4297
|
-
voted: "
|
|
4298
|
-
seconded: "
|
|
4299
|
-
probed: "
|
|
4300
|
-
};
|
|
4482
|
+
head: this._t("sa_head"),
|
|
4483
|
+
stamp: this._t("sa_stamp"),
|
|
4484
|
+
tokens: this._t("sa_tokens"),
|
|
4485
|
+
messages: this._t("sa_messages"),
|
|
4486
|
+
rounds: this._t("sa_rounds"),
|
|
4487
|
+
minutes: this._t("sa_minutes"),
|
|
4488
|
+
modelHead: this._t("sa_model_head"),
|
|
4489
|
+
valueHead: this._t("sa_value_head"),
|
|
4490
|
+
valueEmpty: this._t("sa_value_empty"),
|
|
4491
|
+
voted: this._t("sa_voted"),
|
|
4492
|
+
seconded: this._t("sa_seconded"),
|
|
4493
|
+
probed: this._t("sa_probed"), };
|
|
4301
4494
|
|
|
4302
4495
|
const fmtTokens = (n) => {
|
|
4303
4496
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
@@ -4336,11 +4529,11 @@
|
|
|
4336
4529
|
// base provider colour. Append-only — adding a new variant just
|
|
4337
4530
|
// requires its modelV at the appropriate position.
|
|
4338
4531
|
const MODEL_SHADE_ORDER = {
|
|
4339
|
-
anthropic: ["opus-4-7", "sonnet-4-6", "haiku-4-5"],
|
|
4532
|
+
anthropic: ["opus-4-7", "opus-4-6", "sonnet-4-6", "haiku-4-5"],
|
|
4340
4533
|
openai: ["gpt-5-5", "gpt-5-4-mini", "codex-5-4"],
|
|
4341
4534
|
google: ["gemini-3-1", "gemini-3-flash", "gemini-3-1-flash"],
|
|
4342
4535
|
xai: ["grok-4", "grok-4-3", "grok-4-mini"],
|
|
4343
|
-
deepseek: ["deepseek-v4-pro", "deepseek-v4"],
|
|
4536
|
+
deepseek: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4"],
|
|
4344
4537
|
};
|
|
4345
4538
|
const providerOf = (modelV) => {
|
|
4346
4539
|
const hit = reachable.find((m) => m.modelV === modelV);
|
|
@@ -4371,7 +4564,7 @@
|
|
|
4371
4564
|
const barSegments = stats.modelBreakdown.map((row) => {
|
|
4372
4565
|
const color = colorForModel(row.modelV);
|
|
4373
4566
|
const widthPct = (row.pct * 100).toFixed(2);
|
|
4374
|
-
return `<span class="sa-bar-seg" style="width: ${widthPct}%; background: ${color};" title="${this.escape(modelLabel(row.modelV)
|
|
4567
|
+
return `<span class="sa-bar-seg" style="width: ${widthPct}%; background: ${color};" title="${this.escape(this._t("sa_seg_title", { model: modelLabel(row.modelV), tokens: fmtTokens(row.tokens) }))}"></span>`;
|
|
4375
4568
|
}).join("");
|
|
4376
4569
|
const barHtml = stats.modelBreakdown.length > 0
|
|
4377
4570
|
? `<div class="sa-bar" role="img" aria-label="${this.escape(t.modelHead)}">${barSegments}</div>`
|
|
@@ -4520,13 +4713,11 @@
|
|
|
4520
4713
|
? this.escape(nextSpeaker.handle.replace(/^\//, ""))
|
|
4521
4714
|
: "";
|
|
4522
4715
|
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
const
|
|
4526
|
-
const
|
|
4527
|
-
const
|
|
4528
|
-
const pausedLabel = "paused";
|
|
4529
|
-
const nextLabel = "next";
|
|
4716
|
+
const addInputLabel = this._t("pause_bar_add_input");
|
|
4717
|
+
const adjournLabel = this._t("pause_bar_adjourn");
|
|
4718
|
+
const resumeLabel = this._t("pause_bar_resume");
|
|
4719
|
+
const pausedLabel = this._t("pause_bar_paused");
|
|
4720
|
+
const nextLabel = this._t("pause_bar_next");
|
|
4530
4721
|
const nextChunk = nextHandle
|
|
4531
4722
|
? ` · ${nextLabel} → <span class="lime">${nextHandle}</span>`
|
|
4532
4723
|
: "";
|
|
@@ -4551,6 +4742,7 @@
|
|
|
4551
4742
|
directorIds: [], // populated lazily from the chair's roster
|
|
4552
4743
|
mode: "constructive",
|
|
4553
4744
|
intensity: "sharp",
|
|
4745
|
+
deliveryMode: "text",
|
|
4554
4746
|
},
|
|
4555
4747
|
|
|
4556
4748
|
loadComposerState() {
|
|
@@ -4571,10 +4763,10 @@
|
|
|
4571
4763
|
saveComposerState() {
|
|
4572
4764
|
if (!this.composerState) return;
|
|
4573
4765
|
try {
|
|
4574
|
-
const { directorIds, mode, intensity, autoPickDirectors, subject } = this.composerState;
|
|
4766
|
+
const { directorIds, mode, intensity, deliveryMode, autoPickDirectors, subject } = this.composerState;
|
|
4575
4767
|
localStorage.setItem(
|
|
4576
4768
|
"boardroom.composer",
|
|
4577
|
-
JSON.stringify({ directorIds, mode, intensity, autoPickDirectors, subject }),
|
|
4769
|
+
JSON.stringify({ directorIds, mode, intensity, deliveryMode, autoPickDirectors, subject }),
|
|
4578
4770
|
);
|
|
4579
4771
|
} catch { /* ignore */ }
|
|
4580
4772
|
},
|
|
@@ -4635,6 +4827,13 @@
|
|
|
4635
4827
|
setTimeout(() => {
|
|
4636
4828
|
const ta = chat.querySelector("[data-agent-composer-desc]");
|
|
4637
4829
|
if (ta) {
|
|
4830
|
+
// Set the multi-line placeholder via DOM property · the
|
|
4831
|
+
// HTML attribute path strips newlines in some parsers,
|
|
4832
|
+
// so the second line of the pitch wouldn't render. The
|
|
4833
|
+
// DOM `placeholder` property reflects `\n` as a literal
|
|
4834
|
+
// line break in the textarea's empty state, which is
|
|
4835
|
+
// exactly what we want.
|
|
4836
|
+
ta.placeholder = this._t("ag_cmp_placeholder");
|
|
4638
4837
|
ta.focus();
|
|
4639
4838
|
this.autosizeAgentComposerTextarea();
|
|
4640
4839
|
}
|
|
@@ -4656,7 +4855,13 @@
|
|
|
4656
4855
|
chat.innerHTML = this.renderComposerHtml(state);
|
|
4657
4856
|
setTimeout(() => {
|
|
4658
4857
|
const ta = chat.querySelector("[data-composer-subject]");
|
|
4659
|
-
if (ta)
|
|
4858
|
+
if (ta) {
|
|
4859
|
+
// Same multi-line placeholder pattern as the agent
|
|
4860
|
+
// composer above · DOM property preserves the `\n` the
|
|
4861
|
+
// HTML attribute path can drop.
|
|
4862
|
+
ta.placeholder = this._composerSubjectPlaceholder();
|
|
4863
|
+
ta.focus();
|
|
4864
|
+
}
|
|
4660
4865
|
this.autosizeComposerTextarea();
|
|
4661
4866
|
}, 30);
|
|
4662
4867
|
}
|
|
@@ -4821,7 +5026,12 @@
|
|
|
4821
5026
|
// to All.
|
|
4822
5027
|
this._reportsCache = briefs;
|
|
4823
5028
|
const activeFilter = this._reportsFilter || "all";
|
|
4824
|
-
const filterLabels = {
|
|
5029
|
+
const filterLabels = {
|
|
5030
|
+
all: this._t("rep_filter_archive_label"),
|
|
5031
|
+
today: this._t("rep_filter_today"),
|
|
5032
|
+
week: this._t("rep_filter_week"),
|
|
5033
|
+
earlier: this._t("rep_filter_earlier"),
|
|
5034
|
+
};
|
|
4825
5035
|
|
|
4826
5036
|
const emptyChip = (key, label) => {
|
|
4827
5037
|
const on = key === activeFilter ? " on" : "";
|
|
@@ -4834,39 +5044,39 @@
|
|
|
4834
5044
|
};
|
|
4835
5045
|
|
|
4836
5046
|
const isAll = activeFilter === "all";
|
|
4837
|
-
const cardKicker = isAll ? "
|
|
5047
|
+
const cardKicker = isAll ? this._t("rep_empty_kicker_archive") : this._t("rep_empty_kicker_window");
|
|
4838
5048
|
const cardTitle = isAll
|
|
4839
|
-
? "
|
|
4840
|
-
:
|
|
5049
|
+
? this._t("rep_empty_title_none")
|
|
5050
|
+
: this._t("rep_empty_title_window", { window: filterLabels[activeFilter] });
|
|
4841
5051
|
const cardDeck = isAll
|
|
4842
|
-
?
|
|
4843
|
-
:
|
|
5052
|
+
? this._t("rep_empty_deck_all")
|
|
5053
|
+
: this._t("rep_empty_deck_window");
|
|
4844
5054
|
const cardCtaHtml = isAll
|
|
4845
5055
|
? `
|
|
4846
5056
|
<button type="button" class="reports-list-empty-cta" data-convene-trigger>
|
|
4847
5057
|
<span class="reports-list-empty-cta-arrow">→</span>
|
|
4848
|
-
<span
|
|
5058
|
+
<span>${this.escape(this._t("rep_cta_convene"))}</span>
|
|
4849
5059
|
</button>`
|
|
4850
5060
|
: `
|
|
4851
5061
|
<button type="button" class="reports-list-empty-cta" data-reports-filter="all">
|
|
4852
5062
|
<span class="reports-list-empty-cta-arrow">←</span>
|
|
4853
|
-
<span
|
|
5063
|
+
<span>${this.escape(this._t("rep_cta_show_all"))}</span>
|
|
4854
5064
|
</button>`;
|
|
4855
5065
|
|
|
4856
5066
|
page.innerHTML = `
|
|
4857
5067
|
<div class="reports-page-head">
|
|
4858
5068
|
<div>
|
|
4859
|
-
<div class="reports-page-kicker"
|
|
4860
|
-
<h1 class="reports-page-title"
|
|
5069
|
+
<div class="reports-page-kicker">${this.escape(this._t("rep_kicker"))}</div>
|
|
5070
|
+
<h1 class="reports-page-title">${this.escape(this._t("rep_title"))}</h1>
|
|
4861
5071
|
</div>
|
|
4862
|
-
<div class="reports-page-meta"
|
|
5072
|
+
<div class="reports-page-meta">${this.escape(this._t("rep_meta", { n: 0 }))}</div>
|
|
4863
5073
|
</div>
|
|
4864
5074
|
|
|
4865
|
-
<div class="reports-filters" role="tablist" aria-label="
|
|
4866
|
-
${emptyChip("all", "
|
|
4867
|
-
${emptyChip("today", "
|
|
4868
|
-
${emptyChip("week", "
|
|
4869
|
-
${emptyChip("earlier", "
|
|
5075
|
+
<div class="reports-filters" role="tablist" aria-label="${this.escape(this._t("rep_aria_filters"))}">
|
|
5076
|
+
${emptyChip("all", this._t("rep_filter_all"))}
|
|
5077
|
+
${emptyChip("today", this._t("rep_filter_today"))}
|
|
5078
|
+
${emptyChip("week", this._t("rep_filter_week"))}
|
|
5079
|
+
${emptyChip("earlier", this._t("rep_filter_earlier"))}
|
|
4870
5080
|
</div>
|
|
4871
5081
|
|
|
4872
5082
|
<div class="reports-list-wrap">
|
|
@@ -4933,21 +5143,43 @@
|
|
|
4933
5143
|
`;
|
|
4934
5144
|
};
|
|
4935
5145
|
|
|
4936
|
-
//
|
|
4937
|
-
//
|
|
4938
|
-
//
|
|
4939
|
-
//
|
|
4940
|
-
//
|
|
4941
|
-
const
|
|
4942
|
-
const
|
|
4943
|
-
|
|
4944
|
-
|
|
5146
|
+
// Group filtered items by date label (Today / Yesterday / This
|
|
5147
|
+
// week / Earlier) so the list still has rhythm without splitting
|
|
5148
|
+
// into multiple sections each with its own header chrome.
|
|
5149
|
+
// Iterates the *visible* slice; the load-more sentinel below
|
|
5150
|
+
// tops the list up by 20 each time it crosses the viewport.
|
|
5151
|
+
const groups = [];
|
|
5152
|
+
const yesterdayStart = todayStart - 86400_000;
|
|
5153
|
+
let currentGroup = null;
|
|
5154
|
+
const groupLabelFor = (ts) => {
|
|
5155
|
+
if (ts >= todayStart) return this._t("rep_group_today");
|
|
5156
|
+
if (ts >= yesterdayStart) return this._t("rep_group_yesterday");
|
|
5157
|
+
if (ts >= weekStart) return this._t("rep_group_week");
|
|
5158
|
+
return this._t("rep_group_earlier");
|
|
5159
|
+
};
|
|
5160
|
+
for (const b of visibleFiltered) {
|
|
5161
|
+
const label = groupLabelFor(b.createdAt);
|
|
5162
|
+
if (!currentGroup || currentGroup.label !== label) {
|
|
5163
|
+
currentGroup = { label, items: [] };
|
|
5164
|
+
groups.push(currentGroup);
|
|
5165
|
+
}
|
|
5166
|
+
currentGroup.items.push(b);
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
const filterLabels = {
|
|
5170
|
+
all: this._t("rep_filter_archive_label"),
|
|
5171
|
+
today: this._t("rep_filter_today"),
|
|
5172
|
+
week: this._t("rep_filter_week"),
|
|
5173
|
+
earlier: this._t("rep_filter_earlier"),
|
|
5174
|
+
};
|
|
5175
|
+
const filterCopyTitle = filterLabels[activeFilter] || filterLabels.all;
|
|
5176
|
+
const groupsHtml = groups.length === 0 ? `
|
|
4945
5177
|
<div class="reports-list-empty">
|
|
4946
5178
|
<!-- Notice text · explains the empty window. -->
|
|
4947
5179
|
<div class="reports-list-empty-text">
|
|
4948
|
-
<div class="reports-list-empty-kicker"
|
|
4949
|
-
<h3 class="reports-list-empty-title"
|
|
4950
|
-
<p class="reports-list-empty-deck"
|
|
5180
|
+
<div class="reports-list-empty-kicker">${this.escape(this._t("rep_empty_kicker_window"))}</div>
|
|
5181
|
+
<h3 class="reports-list-empty-title">${this.escape(this._t("rep_empty_title_window", { window: filterCopyTitle }))}</h3>
|
|
5182
|
+
<p class="reports-list-empty-deck">${this.escape(this._t("rep_empty_deck_filter"))}</p>
|
|
4951
5183
|
</div>
|
|
4952
5184
|
<!-- Static skeleton silhouette · three quiet bars
|
|
4953
5185
|
suggesting "title · judgement · meta," paired with
|
|
@@ -4960,7 +5192,7 @@
|
|
|
4960
5192
|
${activeFilter !== "all" ? `
|
|
4961
5193
|
<button type="button" class="reports-list-empty-cta" data-reports-filter="all">
|
|
4962
5194
|
<span class="reports-list-empty-cta-arrow">←</span>
|
|
4963
|
-
<span
|
|
5195
|
+
<span>${this.escape(this._t("rep_cta_show_all"))}</span>
|
|
4964
5196
|
</button>
|
|
4965
5197
|
` : ""}
|
|
4966
5198
|
</div>
|
|
@@ -4975,12 +5207,21 @@
|
|
|
4975
5207
|
// when there's more to load. The "+ N more" hint doubles as a
|
|
4976
5208
|
// click target if the user prefers explicit paging over scroll.
|
|
4977
5209
|
const remaining = filtered.length - visibleCount;
|
|
5210
|
+
const loadN = Math.min(20, remaining);
|
|
5211
|
+
const reportsMetaLine =
|
|
5212
|
+
(total === 1 ? this._t("rep_total_one") : this._t("rep_total_n", { n: total })) +
|
|
5213
|
+
(distinctRooms > 0
|
|
5214
|
+
? (distinctRooms === 1
|
|
5215
|
+
? this._t("rep_meta_room_one")
|
|
5216
|
+
: this._t("rep_meta_room_n", { n: distinctRooms }))
|
|
5217
|
+
: "") +
|
|
5218
|
+
(hasMore ? this._t("rep_showing", { n: visibleCount }) : "");
|
|
4978
5219
|
const sentinelHtml = hasMore
|
|
4979
5220
|
? `
|
|
4980
5221
|
<div class="reports-load-sentinel" data-reports-load-sentinel>
|
|
4981
5222
|
<button type="button" class="reports-load-more" data-reports-load-more>
|
|
4982
5223
|
<span class="reports-load-more-arrow">▾</span>
|
|
4983
|
-
<span class="reports-load-more-text"
|
|
5224
|
+
<span class="reports-load-more-text">${this.escape(this._t("rep_load_more", { load: loadN, remaining }))}</span>
|
|
4984
5225
|
</button>
|
|
4985
5226
|
</div>
|
|
4986
5227
|
`
|
|
@@ -4989,17 +5230,17 @@
|
|
|
4989
5230
|
page.innerHTML = `
|
|
4990
5231
|
<div class="reports-page-head">
|
|
4991
5232
|
<div>
|
|
4992
|
-
<div class="reports-page-kicker"
|
|
4993
|
-
<h1 class="reports-page-title"
|
|
5233
|
+
<div class="reports-page-kicker">${this.escape(this._t("rep_kicker"))}</div>
|
|
5234
|
+
<h1 class="reports-page-title">${this.escape(this._t("rep_title"))}</h1>
|
|
4994
5235
|
</div>
|
|
4995
|
-
<div class="reports-page-meta">${
|
|
5236
|
+
<div class="reports-page-meta">${this.escape(reportsMetaLine)}</div>
|
|
4996
5237
|
</div>
|
|
4997
5238
|
|
|
4998
|
-
<div class="reports-filters" role="tablist" aria-label="
|
|
4999
|
-
${filterChip("all", "
|
|
5000
|
-
${filterChip("today", "
|
|
5001
|
-
${filterChip("week", "
|
|
5002
|
-
${filterChip("earlier", "
|
|
5239
|
+
<div class="reports-filters" role="tablist" aria-label="${this.escape(this._t("rep_aria_filters"))}">
|
|
5240
|
+
${filterChip("all", this._t("rep_filter_all"), total)}
|
|
5241
|
+
${filterChip("today", this._t("rep_filter_today"), todayCount)}
|
|
5242
|
+
${filterChip("week", this._t("rep_filter_week"), weekCount)}
|
|
5243
|
+
${filterChip("earlier", this._t("rep_filter_earlier"), earlierCount)}
|
|
5003
5244
|
</div>
|
|
5004
5245
|
|
|
5005
5246
|
<div class="reports-list-wrap">${groupsHtml}${sentinelHtml}</div>
|
|
@@ -5050,11 +5291,16 @@
|
|
|
5050
5291
|
}
|
|
5051
5292
|
|
|
5052
5293
|
// Scroll path · IntersectionObserver fires when the sentinel
|
|
5053
|
-
// crosses the viewport.
|
|
5054
|
-
//
|
|
5055
|
-
//
|
|
5056
|
-
//
|
|
5294
|
+
// crosses the viewport. The All Reports view uses an inner
|
|
5295
|
+
// scroll container (`.main-view[data-main-view="reports"]` has
|
|
5296
|
+
// `overflow-y: auto`), so the document viewport itself never
|
|
5297
|
+
// scrolls. The observer's `root` MUST point at the inner
|
|
5298
|
+
// scroller — otherwise the sentinel never intersects and
|
|
5299
|
+
// infinite scroll silently does nothing. `rootMargin: 200px`
|
|
5300
|
+
// triggers slightly before the actual edge so the next batch
|
|
5301
|
+
// is rendered before the user reaches the bottom.
|
|
5057
5302
|
try {
|
|
5303
|
+
const scrollRoot = document.querySelector('.main-view[data-main-view="reports"]') || null;
|
|
5058
5304
|
this._reportsLoadObserver = new IntersectionObserver(
|
|
5059
5305
|
(entries) => {
|
|
5060
5306
|
for (const entry of entries) {
|
|
@@ -5064,7 +5310,7 @@
|
|
|
5064
5310
|
}
|
|
5065
5311
|
}
|
|
5066
5312
|
},
|
|
5067
|
-
{ rootMargin: "200px 0px 200px 0px", threshold: 0.01 },
|
|
5313
|
+
{ root: scrollRoot, rootMargin: "200px 0px 200px 0px", threshold: 0.01 },
|
|
5068
5314
|
);
|
|
5069
5315
|
this._reportsLoadObserver.observe(sentinel);
|
|
5070
5316
|
} catch { /* IntersectionObserver unavailable · click path remains */ }
|
|
@@ -5277,11 +5523,11 @@
|
|
|
5277
5523
|
`;
|
|
5278
5524
|
};
|
|
5279
5525
|
const filtersHtml = `
|
|
5280
|
-
<div class="notes-filters" role="tablist" aria-label="
|
|
5281
|
-
${filterChip("all", "
|
|
5282
|
-
${filterChip("today", "
|
|
5283
|
-
${filterChip("week", "
|
|
5284
|
-
${filterChip("earlier", "
|
|
5526
|
+
<div class="notes-filters" role="tablist" aria-label="${this.escape(this._t("notes_aria_filters"))}">
|
|
5527
|
+
${filterChip("all", this._t("notes_filter_all"), total)}
|
|
5528
|
+
${filterChip("today", this._t("rep_filter_today"), todayCount)}
|
|
5529
|
+
${filterChip("week", this._t("rep_filter_week"), weekCount)}
|
|
5530
|
+
${filterChip("earlier", this._t("rep_filter_earlier"), earlierCount)}
|
|
5285
5531
|
</div>
|
|
5286
5532
|
`;
|
|
5287
5533
|
|
|
@@ -5292,41 +5538,17 @@
|
|
|
5292
5538
|
page.innerHTML = `
|
|
5293
5539
|
<div class="notes-page-head">
|
|
5294
5540
|
<div>
|
|
5295
|
-
<div class="notes-page-kicker"
|
|
5296
|
-
<h1 class="notes-page-title"
|
|
5541
|
+
<div class="notes-page-kicker">${this.escape(this._t("notes_kicker"))}</div>
|
|
5542
|
+
<h1 class="notes-page-title">${this.escape(this._t("notes_title"))}</h1>
|
|
5297
5543
|
</div>
|
|
5298
|
-
<div class="notes-page-meta"
|
|
5544
|
+
<div class="notes-page-meta">${this.escape(this._t("notes_meta", { n: 0 }))}</div>
|
|
5299
5545
|
</div>
|
|
5300
5546
|
${filtersHtml}
|
|
5301
5547
|
<div class="notes-list-empty">
|
|
5302
5548
|
<div class="notes-empty-mark">○</div>
|
|
5303
|
-
<div class="notes-empty-title"
|
|
5549
|
+
<div class="notes-empty-title">${this.escape(this._t("notes_empty_title"))}</div>
|
|
5304
5550
|
<div class="notes-empty-deck">
|
|
5305
|
-
|
|
5306
|
-
</div>
|
|
5307
|
-
<ol class="notes-empty-steps">
|
|
5308
|
-
<li>
|
|
5309
|
-
<span class="notes-empty-step-num">1</span>
|
|
5310
|
-
<span class="notes-empty-step-text">
|
|
5311
|
-
<strong>Open a room.</strong> Any director's reply can be saved.
|
|
5312
|
-
</span>
|
|
5313
|
-
</li>
|
|
5314
|
-
<li>
|
|
5315
|
-
<span class="notes-empty-step-num">2</span>
|
|
5316
|
-
<span class="notes-empty-step-text">
|
|
5317
|
-
<strong>Select a passage.</strong> Drag-highlight the sentence or paragraph you want to keep.
|
|
5318
|
-
</span>
|
|
5319
|
-
</li>
|
|
5320
|
-
<li>
|
|
5321
|
-
<span class="notes-empty-step-num">3</span>
|
|
5322
|
-
<span class="notes-empty-step-text">
|
|
5323
|
-
<strong>Click <span class="kbd">⌖ Save</span></strong> on the floating bar that appears, or press <span class="kbd">S</span>. The excerpt lands here, indexed across all rooms.
|
|
5324
|
-
</span>
|
|
5325
|
-
</li>
|
|
5326
|
-
</ol>
|
|
5327
|
-
<div class="notes-empty-foot">
|
|
5328
|
-
Each saved excerpt links back to the original room + speaker, so you can jump back any time.
|
|
5329
|
-
</div>
|
|
5551
|
+
${this._t("notes_empty_deck")} </div>
|
|
5330
5552
|
</div>
|
|
5331
5553
|
`;
|
|
5332
5554
|
return;
|
|
@@ -5340,21 +5562,40 @@
|
|
|
5340
5562
|
return true;
|
|
5341
5563
|
});
|
|
5342
5564
|
|
|
5343
|
-
//
|
|
5344
|
-
//
|
|
5345
|
-
//
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5565
|
+
// Group filtered items by date label so even an "All" view has
|
|
5566
|
+
// visual rhythm. When a recency filter narrows to a single
|
|
5567
|
+
// bucket, only that bucket's section renders — no empty headers.
|
|
5568
|
+
const groupLabelFor = (ts) => {
|
|
5569
|
+
if (ts >= todayStart) return this._t("rep_group_today");
|
|
5570
|
+
if (ts >= weekStart) return this._t("rep_group_week");
|
|
5571
|
+
return this._t("rep_group_earlier");
|
|
5572
|
+
};
|
|
5573
|
+
const groups = [];
|
|
5574
|
+
let currentGroup = null;
|
|
5575
|
+
for (const n of filtered) {
|
|
5576
|
+
const label = groupLabelFor(n.createdAt || 0);
|
|
5577
|
+
if (!currentGroup || currentGroup.label !== label) {
|
|
5578
|
+
currentGroup = { label, items: [] };
|
|
5579
|
+
groups.push(currentGroup);
|
|
5580
|
+
}
|
|
5581
|
+
currentGroup.items.push(n);
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
const filterLabels = {
|
|
5585
|
+
all: this._t("rep_filter_archive_label"),
|
|
5586
|
+
today: this._t("rep_filter_today"),
|
|
5587
|
+
week: this._t("rep_filter_week"),
|
|
5588
|
+
earlier: this._t("rep_filter_earlier"),
|
|
5589
|
+
};
|
|
5590
|
+
const groupsHtml = groups.length === 0 ? `
|
|
5350
5591
|
<div class="notes-list-empty">
|
|
5351
5592
|
<div class="notes-empty-mark">○</div>
|
|
5352
|
-
<div class="notes-empty-title"
|
|
5353
|
-
<div class="notes-empty-deck"
|
|
5593
|
+
<div class="notes-empty-title">${this.escape(this._t("notes_empty_window", { window: filterLabels[activeFilter] || filterLabels.all }))}</div>
|
|
5594
|
+
<div class="notes-empty-deck">${this.escape(this._t("notes_empty_filter_deck"))}</div>
|
|
5354
5595
|
${activeFilter !== "all" ? `
|
|
5355
5596
|
<button type="button" class="notes-empty-cta" data-notes-filter="all">
|
|
5356
5597
|
<span class="notes-empty-cta-arrow">←</span>
|
|
5357
|
-
<span
|
|
5598
|
+
<span>${this.escape(this._t("notes_cta_show_all"))}</span>
|
|
5358
5599
|
</button>
|
|
5359
5600
|
` : ""}
|
|
5360
5601
|
</div>
|
|
@@ -5365,18 +5606,19 @@
|
|
|
5365
5606
|
</ul>
|
|
5366
5607
|
`;
|
|
5367
5608
|
|
|
5368
|
-
const totalLabel =
|
|
5609
|
+
const totalLabel =
|
|
5610
|
+
total === 1 ? this._t("notes_one") : this._t("notes_many", { n: total });
|
|
5369
5611
|
const roomLabel = distinctRooms > 0
|
|
5370
|
-
?
|
|
5612
|
+
? (distinctRooms === 1 ? this._t("rooms_one") : this._t("rooms_many", { n: distinctRooms }))
|
|
5371
5613
|
: "";
|
|
5372
5614
|
|
|
5373
5615
|
page.innerHTML = `
|
|
5374
5616
|
<div class="notes-page-head">
|
|
5375
5617
|
<div>
|
|
5376
|
-
<div class="notes-page-kicker"
|
|
5377
|
-
<h1 class="notes-page-title"
|
|
5618
|
+
<div class="notes-page-kicker">${this.escape(this._t("notes_kicker"))}</div>
|
|
5619
|
+
<h1 class="notes-page-title">${this.escape(this._t("notes_title"))}</h1>
|
|
5378
5620
|
</div>
|
|
5379
|
-
<div class="notes-page-meta">${this.escape(totalLabel)}${this.escape(roomLabel)}</div>
|
|
5621
|
+
<div class="notes-page-meta">${this.escape(totalLabel)}${roomLabel ? this.escape(" · " + roomLabel) : ""}</div>
|
|
5380
5622
|
</div>
|
|
5381
5623
|
${filtersHtml}
|
|
5382
5624
|
<div class="notes-list-wrap">${groupsHtml}</div>
|
|
@@ -5400,7 +5642,7 @@
|
|
|
5400
5642
|
const time = this.relTime(n.createdAt) || "";
|
|
5401
5643
|
const roomNum = n.roomNumber != null ? `#${String(n.roomNumber).padStart(3, "0")}` : "";
|
|
5402
5644
|
const roomSubject = (n.roomSubject || "").slice(0, 100);
|
|
5403
|
-
const author = n.authorName || "
|
|
5645
|
+
const author = n.authorName || this._t("notes_director_fallback");
|
|
5404
5646
|
// The jump link uses the room's hash route + the note id as a
|
|
5405
5647
|
// fragment-style query so openRoom can scroll to + flash the
|
|
5406
5648
|
// matching span. The delete button sits as a SIBLING of the
|
|
@@ -5413,7 +5655,7 @@
|
|
|
5413
5655
|
<li class="notes-item" data-note-id="${this.escape(n.id)}">
|
|
5414
5656
|
<a class="notes-item-link" href="${href}" data-note-jump="${this.escape(n.id)}" data-note-room="${this.escape(n.roomId)}">
|
|
5415
5657
|
<div class="notes-item-meta">
|
|
5416
|
-
<span class="notes-item-room"
|
|
5658
|
+
<span class="notes-item-room">${this.escape(this._t("notes_item_room"))} ${this.escape(roomNum)}</span>
|
|
5417
5659
|
${roomSubject ? `<span class="notes-item-sep">·</span><span class="notes-item-subject">${this.escape(roomSubject)}</span>` : ""}
|
|
5418
5660
|
<span class="notes-item-sep">·</span>
|
|
5419
5661
|
<span class="notes-item-director">${this.escape(author)}</span>
|
|
@@ -5660,7 +5902,7 @@
|
|
|
5660
5902
|
// tooltip's 1–2s delay competing with the custom `.note-tip`
|
|
5661
5903
|
// popover wired up in init(); aria-label still surfaces the
|
|
5662
5904
|
// affordance for screen readers.
|
|
5663
|
-
span.setAttribute("aria-label", "
|
|
5905
|
+
span.setAttribute("aria-label", this._t("note_saved"));
|
|
5664
5906
|
// Text-only ranges always satisfy surroundContents' "no
|
|
5665
5907
|
// partial non-Text node" precondition · the catch is just
|
|
5666
5908
|
// a safety belt for browsers that surprise us.
|
|
@@ -5701,7 +5943,7 @@
|
|
|
5701
5943
|
const time = note ? this.relTime(note.createdAt) : "";
|
|
5702
5944
|
tip.innerHTML = `
|
|
5703
5945
|
<span class="note-tip-mark">✓</span>
|
|
5704
|
-
<span class="note-tip-label"
|
|
5946
|
+
<span class="note-tip-label">${this.escape(this._t("note_saved"))}</span>
|
|
5705
5947
|
${time ? `<span class="note-tip-sep">·</span><span class="note-tip-meta">${this.escape(time)}</span>` : ""}
|
|
5706
5948
|
`;
|
|
5707
5949
|
const rect = span.getBoundingClientRect();
|
|
@@ -5871,6 +6113,19 @@
|
|
|
5871
6113
|
}
|
|
5872
6114
|
},
|
|
5873
6115
|
|
|
6116
|
+
/** Pitch copy for the new-room composer's textarea placeholder.
|
|
6117
|
+
* Centralised so renderComposerHtml AND the post-render DOM
|
|
6118
|
+
* `.placeholder` set in renderEmptyState share one source. The
|
|
6119
|
+
* DOM property path is the load-bearing one — setting it via
|
|
6120
|
+
* the HTML attribute strips the `\n` in some parsers, so a
|
|
6121
|
+
* multi-line pitch never renders the second line. The earlier
|
|
6122
|
+
* trailing "Drop in what's on your mind in a sentence or two."
|
|
6123
|
+
* was retired at the user's request — it was a CTA-style nudge
|
|
6124
|
+
* redundant with the heading question above the textarea. */
|
|
6125
|
+
_composerSubjectPlaceholder() {
|
|
6126
|
+
return "Convening a room can stress-test a thesis, force a decision you've been avoiding, or surface the angle nobody on your real team brings.";
|
|
6127
|
+
},
|
|
6128
|
+
|
|
5874
6129
|
/** Build the composer HTML. Centred hero composition · Claude /
|
|
5875
6130
|
* ChatGPT-style new-chat landing, but tuned to our boardroom
|
|
5876
6131
|
* language. The single input block IS the focal point — cast +
|
|
@@ -5889,7 +6144,7 @@
|
|
|
5889
6144
|
const t = {
|
|
5890
6145
|
greet: greeting,
|
|
5891
6146
|
prompt: "What's on your mind today?",
|
|
5892
|
-
placeholder:
|
|
6147
|
+
placeholder: this._composerSubjectPlaceholder(),
|
|
5893
6148
|
convene: "Convene",
|
|
5894
6149
|
tuneLabel: "tune",
|
|
5895
6150
|
starterLabel: "starter",
|
|
@@ -5949,10 +6204,8 @@
|
|
|
5949
6204
|
// Tune dropdowns · two trigger buttons that open option popovers
|
|
5950
6205
|
// on click. Discoverable (label + value + chevron pattern is
|
|
5951
6206
|
// unmistakeably a select) without taking up visual real estate.
|
|
5952
|
-
|
|
5953
|
-
const
|
|
5954
|
-
const intensityLbl = "intensity";
|
|
5955
|
-
|
|
6207
|
+
const toneLbl = this._t("cmp_tone_label");
|
|
6208
|
+
const intensityLbl = this._t("cmp_intensity_label");
|
|
5956
6209
|
// Starter grid · 2-col responsive cards.
|
|
5957
6210
|
const starters = Array.isArray(window.BOARDROOM_STARTERS) ? window.BOARDROOM_STARTERS : [];
|
|
5958
6211
|
const starterCards = starters.map((q, idx) => {
|
|
@@ -5994,10 +6247,47 @@
|
|
|
5994
6247
|
<span class="cmp-dd-value" data-cmp-dd-value="intensity">${this.escape(state.intensity)}</span>
|
|
5995
6248
|
<span class="cmp-dd-chevron">▾</span>
|
|
5996
6249
|
</button>
|
|
6250
|
+
${(() => {
|
|
6251
|
+
// Reuse the same toggle vocabulary as the new-agent
|
|
6252
|
+
// composer's web-search toggle: `.ap-skill-row-toggle`
|
|
6253
|
+
// (track + knob + text) plus `.cmp-ws-toggle` for
|
|
6254
|
+
// toolbar fitting. `needs-key` modifier renders the
|
|
6255
|
+
// dashed "configure first" treatment when no voice
|
|
6256
|
+
// provider is set, matching the WS toggle pattern
|
|
6257
|
+
// exactly. Class set: `on` / `off` / `needs-key`.
|
|
6258
|
+
const voiceConfigured = this.hasAnyVoiceKey();
|
|
6259
|
+
const voiceOn = voiceConfigured && state.deliveryMode === "voice";
|
|
6260
|
+
const voiceLabel = this._t("cmp_voice_label");
|
|
6261
|
+
const voiceTitle = !voiceConfigured
|
|
6262
|
+
? "Configure a voice provider to enable voice mode"
|
|
6263
|
+
: (voiceOn
|
|
6264
|
+
? "Voice mode on · directors speak aloud during the room"
|
|
6265
|
+
: "Voice mode off · click to enable");
|
|
6266
|
+
const voiceCls = [
|
|
6267
|
+
"ap-skill-row-toggle",
|
|
6268
|
+
"cmp-ws-toggle",
|
|
6269
|
+
voiceOn ? "on" : "off",
|
|
6270
|
+
voiceConfigured ? "" : "needs-key",
|
|
6271
|
+
].filter(Boolean).join(" ");
|
|
6272
|
+
return `
|
|
6273
|
+
<button type="button" class="${voiceCls}"
|
|
6274
|
+
data-composer-voice-toggle
|
|
6275
|
+
data-configured="${voiceConfigured ? "1" : "0"}"
|
|
6276
|
+
data-on="${voiceOn ? "1" : "0"}"
|
|
6277
|
+
aria-pressed="${voiceOn ? "true" : "false"}"
|
|
6278
|
+
title="${this.escape(voiceTitle)}">
|
|
6279
|
+
<span class="ap-skill-row-toggle-track"><span class="ap-skill-row-toggle-knob"></span></span>
|
|
6280
|
+
<span class="ap-skill-row-toggle-text">${this.escape(voiceLabel)}</span>
|
|
6281
|
+
</button>
|
|
6282
|
+
`;
|
|
6283
|
+
})()}
|
|
5997
6284
|
</div>
|
|
5998
6285
|
|
|
5999
6286
|
<button type="button" class="cmp-go" data-composer-go title="${this.escape(t.convene)} (⏎)">
|
|
6000
|
-
<
|
|
6287
|
+
<svg class="cmp-go-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
6288
|
+
<path d="M5 12h14"/>
|
|
6289
|
+
<path d="m13 5 7 7-7 7"/>
|
|
6290
|
+
</svg>
|
|
6001
6291
|
</button>
|
|
6002
6292
|
</div>
|
|
6003
6293
|
</div>
|
|
@@ -6017,24 +6307,44 @@
|
|
|
6017
6307
|
},
|
|
6018
6308
|
|
|
6019
6309
|
/** Time-of-day greeting like "// good evening, Kay" / "// 晚上好,Kay". */
|
|
6020
|
-
composerGreeting(
|
|
6021
|
-
//
|
|
6022
|
-
// chrome; the brief language doesn't change the app's voice.
|
|
6310
|
+
composerGreeting(lang, name) {
|
|
6311
|
+
// Follows composer chrome locale (`lang` · en or zh via I18n).
|
|
6023
6312
|
const h = new Date().getHours();
|
|
6024
|
-
const
|
|
6025
|
-
|
|
6026
|
-
|
|
6313
|
+
const isZh = lang === "zh";
|
|
6314
|
+
let key;
|
|
6315
|
+
if (isZh) {
|
|
6316
|
+
if (h < 5) key = "greet_zh_0";
|
|
6317
|
+
else if (h < 12) key = "greet_zh_1";
|
|
6318
|
+
else if (h < 14) key = "greet_zh_2";
|
|
6319
|
+
else if (h < 18) key = "greet_zh_3";
|
|
6320
|
+
else if (h < 23) key = "greet_zh_4";
|
|
6321
|
+
else key = "greet_zh_5";
|
|
6322
|
+
} else if (h < 5) key = "greet_en_0";
|
|
6323
|
+
else if (h < 12) key = "greet_en_1";
|
|
6324
|
+
else if (h < 18) key = "greet_en_2";
|
|
6325
|
+
else key = "greet_en_3";
|
|
6326
|
+
return this._t(key, { name }); },
|
|
6027
6327
|
|
|
6028
6328
|
composerLanguage() {
|
|
6029
|
-
// Match interface language to user prefs / browser locale; CJK
|
|
6030
|
-
// browsers see Chinese composer copy.
|
|
6031
6329
|
try {
|
|
6032
|
-
|
|
6033
|
-
|
|
6330
|
+
if (window.I18n && typeof window.I18n.getLocale === "function") {
|
|
6331
|
+
return window.I18n.getLocale() === "zh" ? "zh" : "en";
|
|
6332
|
+
}
|
|
6333
|
+
} catch { /* ignore */ }
|
|
6334
|
+
try {
|
|
6335
|
+
const lang = (navigator.language || "").toLowerCase();
|
|
6336
|
+
if (lang.startsWith("zh") || lang.includes("cn") || lang.includes("hans") || lang.includes("hant")) {
|
|
6337
|
+
return "zh";
|
|
6338
|
+
}
|
|
6034
6339
|
} catch { /* ignore */ }
|
|
6035
6340
|
return "en";
|
|
6036
6341
|
},
|
|
6037
6342
|
|
|
6343
|
+
/** UI copy from i18n.js · falls back to key if I18n not loaded. */
|
|
6344
|
+
_t(key, vars) {
|
|
6345
|
+
return (window.I18n && window.I18n.t(key, vars)) || key;
|
|
6346
|
+
},
|
|
6347
|
+
|
|
6038
6348
|
autosizeComposerTextarea() {
|
|
6039
6349
|
const ta = document.querySelector("[data-composer-subject]");
|
|
6040
6350
|
if (!ta) return;
|
|
@@ -6135,15 +6445,14 @@
|
|
|
6135
6445
|
} catch { /* ignore */ }
|
|
6136
6446
|
},
|
|
6137
6447
|
|
|
6138
|
-
/** True when
|
|
6139
|
-
* websearch toggle's "on" state
|
|
6140
|
-
*
|
|
6141
|
-
|
|
6142
|
-
agentComposerBraveConfigured() {
|
|
6448
|
+
/** True when a Web Search backend key is configured · gates the
|
|
6449
|
+
* websearch toggle's "on" state (Brave Search and/or Tavily). Reads
|
|
6450
|
+
* through window.boardroomKeys · refetched after /api/keys mutations. */
|
|
6451
|
+
agentComposerWebSearchConfigured() {
|
|
6143
6452
|
try {
|
|
6144
6453
|
if (typeof window.boardroomKeys !== "function") return false;
|
|
6145
6454
|
const k = window.boardroomKeys();
|
|
6146
|
-
return !!(k && k.brave);
|
|
6455
|
+
return !!(k && (k.brave || k.tavily));
|
|
6147
6456
|
} catch { return false; }
|
|
6148
6457
|
},
|
|
6149
6458
|
|
|
@@ -6152,7 +6461,7 @@
|
|
|
6152
6461
|
* configuring the key, no reason to default OFF) and FALSE when
|
|
6153
6462
|
* not configured (no point pretending it's on). */
|
|
6154
6463
|
loadAgentComposerWebSearch() {
|
|
6155
|
-
const configured = this.
|
|
6464
|
+
const configured = this.agentComposerWebSearchConfigured();
|
|
6156
6465
|
if (!configured) return false;
|
|
6157
6466
|
try {
|
|
6158
6467
|
const raw = localStorage.getItem("boardroom.composer.agent.websearch");
|
|
@@ -6168,6 +6477,15 @@
|
|
|
6168
6477
|
} catch { /* ignore */ }
|
|
6169
6478
|
},
|
|
6170
6479
|
|
|
6480
|
+
/** Public lookup for the human-friendly label of a model id.
|
|
6481
|
+
* Mirrors `MODEL_LABELS[modelV] || modelV`, exposed so callers
|
|
6482
|
+
* outside this IIFE (e.g. new-agent.js's overlay) don't have to
|
|
6483
|
+
* reach into module-scoped state. */
|
|
6484
|
+
modelLabel(modelV) {
|
|
6485
|
+
if (!modelV) return "";
|
|
6486
|
+
return MODEL_LABELS[modelV] || modelV;
|
|
6487
|
+
},
|
|
6488
|
+
|
|
6171
6489
|
setAgentComposerModel(modelV) {
|
|
6172
6490
|
if (!modelV) return;
|
|
6173
6491
|
// Accept any modelV the registry knows about (MODEL_LABELS) so
|
|
@@ -6177,9 +6495,16 @@
|
|
|
6177
6495
|
if (!MODEL_LABELS[modelV]) return;
|
|
6178
6496
|
this.agentComposerModel = modelV;
|
|
6179
6497
|
this.saveAgentComposerModel();
|
|
6180
|
-
// Update
|
|
6181
|
-
|
|
6182
|
-
|
|
6498
|
+
// Update every dropdown trigger label in place. There can be
|
|
6499
|
+
// more than one mounted at once (the AI composer's toolbar
|
|
6500
|
+
// chip + the manual new-agent overlay's foot chip), and they
|
|
6501
|
+
// share the same model state, so all instances should reflect
|
|
6502
|
+
// the new pick. Picking one out via querySelector() updated
|
|
6503
|
+
// only the first; querySelectorAll keeps them in sync.
|
|
6504
|
+
const label = MODEL_LABELS[modelV] || modelV;
|
|
6505
|
+
document.querySelectorAll('[data-cmp-dd-value="agent-model"]').forEach((v) => {
|
|
6506
|
+
v.textContent = label;
|
|
6507
|
+
});
|
|
6183
6508
|
},
|
|
6184
6509
|
|
|
6185
6510
|
/** Click handler for the agent-composer starter list. Drops the
|
|
@@ -6285,8 +6610,8 @@
|
|
|
6285
6610
|
const stages = this.AGENT_GEN_STAGES_EN;
|
|
6286
6611
|
const active = this.agentGenStageIndex;
|
|
6287
6612
|
const elapsed = Math.max(0, (Date.now() - this.agentGenStartedAt) / 1000);
|
|
6288
|
-
const elapsedLabel =
|
|
6289
|
-
const headerLabel = "
|
|
6613
|
+
const elapsedLabel = this._t("ag_gen_elapsed", { n: Math.round(elapsed) });
|
|
6614
|
+
const headerLabel = this._t("ag_gen_header");
|
|
6290
6615
|
const sigilSvg = this.renderAgentGenSigilSvg(stages, active, elapsed);
|
|
6291
6616
|
// The active stage's headline pulse — lifted out from the list so
|
|
6292
6617
|
// it reads as the focal "what's happening RIGHT NOW" line.
|
|
@@ -6300,11 +6625,12 @@
|
|
|
6300
6625
|
<span class="ag-gen-mark"><span class="ag-gen-pulse"></span></span>
|
|
6301
6626
|
<span class="ag-gen-title">${this.escape(headerLabel)}</span>
|
|
6302
6627
|
<span class="ag-gen-elapsed">${this.escape(elapsedLabel)}</span>
|
|
6628
|
+
<button type="button" class="ag-gen-stop" data-agent-spec-stop title="Stop generating">[ Stop ]</button>
|
|
6303
6629
|
</div>
|
|
6304
6630
|
<div class="ag-gen-stage-area">
|
|
6305
6631
|
<div class="ag-gen-sigil" aria-hidden="true">${sigilSvg}</div>
|
|
6306
6632
|
<div class="ag-gen-active-block">
|
|
6307
|
-
<div class="ag-gen-active-kicker">${this.escape(
|
|
6633
|
+
<div class="ag-gen-active-kicker">${this.escape(this._t("ag_gen_step", { current: active + 1, total: stages.length }))}</div>
|
|
6308
6634
|
<div class="ag-gen-active-label">${this.escape(activeStage ? activeStage.label : "")}</div>
|
|
6309
6635
|
${activeSubText ? `<div class="ag-gen-active-sub">${this.escape(activeSubText)}</div>` : ""}
|
|
6310
6636
|
</div>
|
|
@@ -6430,21 +6756,18 @@
|
|
|
6430
6756
|
const userName = (this.prefs?.name || "you").trim() || "you";
|
|
6431
6757
|
const lang = this.composerLanguage();
|
|
6432
6758
|
const greeting = this.composerGreeting(lang, userName);
|
|
6433
|
-
// System UI · always English (new-agent composer chrome).
|
|
6434
6759
|
const t = {
|
|
6435
6760
|
greet: greeting,
|
|
6436
|
-
prompt: "
|
|
6437
|
-
placeholder:
|
|
6438
|
-
cta: "
|
|
6439
|
-
ctaHint: "
|
|
6440
|
-
manual: "
|
|
6441
|
-
generating: "
|
|
6442
|
-
|
|
6443
|
-
starterCaption: "or start from an archetype",
|
|
6444
|
-
};
|
|
6761
|
+
prompt: this._t("ag_cmp_prompt"),
|
|
6762
|
+
placeholder: this._t("ag_cmp_placeholder"),
|
|
6763
|
+
cta: this._t("ag_cmp_cta"),
|
|
6764
|
+
ctaHint: this._t("ag_cmp_cta_hint"),
|
|
6765
|
+
manual: this._t("ag_cmp_manual"),
|
|
6766
|
+
generating: this._t("ag_cmp_generating"),
|
|
6767
|
+
starterCaption: this._t("ag_cmp_starter_caption"), };
|
|
6445
6768
|
// If we already have a spec preview, render that instead of the input.
|
|
6446
6769
|
if (this.agentSpec) {
|
|
6447
|
-
return this.renderAgentSpecPreviewHtml(this.agentSpec
|
|
6770
|
+
return this.renderAgentSpecPreviewHtml(this.agentSpec);
|
|
6448
6771
|
}
|
|
6449
6772
|
// If the last attempt failed (timeout or other), render the
|
|
6450
6773
|
// recovery card with [Retry] / [Discard] · keeps the description
|
|
@@ -6453,8 +6776,6 @@
|
|
|
6453
6776
|
return this.renderAgentSpecErrorHtml(this.agentSpecError, lang);
|
|
6454
6777
|
}
|
|
6455
6778
|
const generating = this.agentSpecGenerating;
|
|
6456
|
-
const currentModel = this.loadAgentComposerModel();
|
|
6457
|
-
const modelDisplay = MODEL_LABELS[currentModel] || currentModel;
|
|
6458
6779
|
const starters = this.AGENT_STARTERS_EN;
|
|
6459
6780
|
const starterCards = starters.map((q, idx) => `
|
|
6460
6781
|
<button type="button" class="cmp-starter" data-agent-starter="${idx}">
|
|
@@ -6474,11 +6795,13 @@
|
|
|
6474
6795
|
<textarea class="cmp-input" data-agent-composer-desc rows="1" placeholder="${this.escape(t.placeholder)}" ${generating ? "disabled" : ""}>${this.escape(this.loadAgentComposerDraft())}</textarea>
|
|
6475
6796
|
|
|
6476
6797
|
<div class="cmp-toolbar">
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6798
|
+
<!-- Model selection lives downstream now: the post-
|
|
6799
|
+
generation spec preview card has a model <select>
|
|
6800
|
+
(renderAgentSpecPreviewHtml), the manual new-agent
|
|
6801
|
+
overlay has its own model picker (new-agent.js), and
|
|
6802
|
+
the agent profile page can change the model at any
|
|
6803
|
+
time. Putting it in the composer toolbar too was
|
|
6804
|
+
redundant and made the bar visually noisy. -->
|
|
6482
6805
|
<button type="button" class="ag-cmp-manual" data-agent-composer-manual>
|
|
6483
6806
|
<span class="ag-cmp-manual-mark">⚙</span>
|
|
6484
6807
|
<span class="ag-cmp-manual-label">${this.escape(t.manual)}</span>
|
|
@@ -6489,16 +6812,19 @@
|
|
|
6489
6812
|
// same control. Class set: `ap-skill-row-toggle` +
|
|
6490
6813
|
// `on` / `off` / `needs-key` modifiers, mirroring the
|
|
6491
6814
|
// skill row at agent-profile.js:1411.
|
|
6492
|
-
const configured = this.
|
|
6815
|
+
const configured = this.agentComposerWebSearchConfigured();
|
|
6493
6816
|
const on = configured && this.loadAgentComposerWebSearch();
|
|
6494
|
-
|
|
6495
|
-
|
|
6817
|
+
const stateLabel = !configured
|
|
6818
|
+
? this._t("ag_ws_needs_key")
|
|
6819
|
+
: on
|
|
6820
|
+
? this._t("ag_ws_enabled")
|
|
6821
|
+
: this._t("ag_ws_disabled");
|
|
6496
6822
|
const titleText = !configured
|
|
6497
|
-
? "
|
|
6823
|
+
? this._t("ag_ws_title_needs")
|
|
6498
6824
|
: on
|
|
6499
|
-
? "
|
|
6500
|
-
: "
|
|
6501
|
-
const wsLabel = "
|
|
6825
|
+
? this._t("ag_ws_title_on")
|
|
6826
|
+
: this._t("ag_ws_title_off");
|
|
6827
|
+
const wsLabel = this._t("ag_ws_label");
|
|
6502
6828
|
const cls = [
|
|
6503
6829
|
"ap-skill-row-toggle",
|
|
6504
6830
|
"cmp-ws-toggle",
|
|
@@ -6518,7 +6844,12 @@
|
|
|
6518
6844
|
`;
|
|
6519
6845
|
})()}
|
|
6520
6846
|
<button type="button" class="cmp-go ${generating ? "busy" : ""}" data-agent-composer-go title="${this.escape(t.cta)} (⏎)" ${generating ? "disabled" : ""}>
|
|
6521
|
-
|
|
6847
|
+
${generating
|
|
6848
|
+
? `<span class="cmp-go-arrow">…</span>`
|
|
6849
|
+
: `<svg class="cmp-go-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
6850
|
+
<path d="M5 12h14"/>
|
|
6851
|
+
<path d="m13 5 7 7-7 7"/>
|
|
6852
|
+
</svg>`}
|
|
6522
6853
|
</button>
|
|
6523
6854
|
</div>
|
|
6524
6855
|
</div>
|
|
@@ -6544,24 +6875,7 @@
|
|
|
6544
6875
|
},
|
|
6545
6876
|
|
|
6546
6877
|
/** Preview card · all generated fields editable inline. */
|
|
6547
|
-
renderAgentSpecPreviewHtml(spec
|
|
6548
|
-
// System UI · always English (agent-spec preview card chrome).
|
|
6549
|
-
const t = {
|
|
6550
|
-
kicker: "// generated director · edit and save",
|
|
6551
|
-
avatar: "Avatar",
|
|
6552
|
-
reroll: "Reroll",
|
|
6553
|
-
name: "Name",
|
|
6554
|
-
handle: "Handle",
|
|
6555
|
-
role: "Role tag",
|
|
6556
|
-
bio: "Bio",
|
|
6557
|
-
quote: "Cover quote",
|
|
6558
|
-
instruction: "Instruction",
|
|
6559
|
-
model: "Model",
|
|
6560
|
-
save: "Save director",
|
|
6561
|
-
discard: "Discard",
|
|
6562
|
-
redo: "Regenerate",
|
|
6563
|
-
};
|
|
6564
|
-
const seed = this.agentSpecAvatarSeed;
|
|
6878
|
+
renderAgentSpecPreviewHtml(spec) { const seed = this.agentSpecAvatarSeed;
|
|
6565
6879
|
const avatarSvg = (window.AvatarSkill && seed)
|
|
6566
6880
|
? window.AvatarSkill.generate(seed, { size: 96 })
|
|
6567
6881
|
: `<div class="ag-prev-av-empty">—</div>`;
|
|
@@ -6590,19 +6904,19 @@
|
|
|
6590
6904
|
const abilitySvg = this.renderAgentSpecRadarSvg(spec.ability || {});
|
|
6591
6905
|
return `
|
|
6592
6906
|
<section class="cmp ag-cmp ag-prev-mode">
|
|
6593
|
-
<div class="ag-prev-kicker">${this.escape(
|
|
6907
|
+
<div class="ag-prev-kicker">${this.escape(this._t("ag_preview_kicker"))}</div>
|
|
6594
6908
|
|
|
6595
6909
|
<div class="ag-prev-card">
|
|
6596
6910
|
<header class="ag-prev-head">
|
|
6597
6911
|
<div class="ag-prev-identity">
|
|
6598
|
-
<button type="button" class="ag-prev-av" data-agent-spec-reroll title="${this.escape(
|
|
6912
|
+
<button type="button" class="ag-prev-av" data-agent-spec-reroll title="${this.escape(this._t("ag_preview_reroll"))}">
|
|
6599
6913
|
<div class="ag-prev-av-frame">${avatarSvg}</div>
|
|
6600
6914
|
<span class="ag-prev-av-reroll-mark">↻</span>
|
|
6601
6915
|
</button>
|
|
6602
6916
|
<div class="ag-prev-id-fields">
|
|
6603
|
-
<input type="text" class="ag-prev-name" data-agent-spec-field="name" maxlength="32" value="${this.escape(spec.name)}" placeholder="${this.escape(
|
|
6917
|
+
<input type="text" class="ag-prev-name" data-agent-spec-field="name" maxlength="32" value="${this.escape(spec.name)}" placeholder="${this.escape(this._t("ag_preview_name"))}">
|
|
6604
6918
|
<div class="ag-prev-id-meta">
|
|
6605
|
-
<input type="text" class="ag-prev-roletag" data-agent-spec-field="roleTag" maxlength="32" value="${this.escape(spec.roleTag)}" placeholder="${this.escape(
|
|
6919
|
+
<input type="text" class="ag-prev-roletag" data-agent-spec-field="roleTag" maxlength="32" value="${this.escape(spec.roleTag)}" placeholder="${this.escape(this._t("ag_preview_role"))}">
|
|
6606
6920
|
<span class="ag-prev-meta-sep">·</span>
|
|
6607
6921
|
<select class="ag-prev-model" data-agent-spec-field="modelV">${modelOpts}</select>
|
|
6608
6922
|
</div>
|
|
@@ -6613,27 +6927,27 @@
|
|
|
6613
6927
|
|
|
6614
6928
|
<div class="ag-prev-body">
|
|
6615
6929
|
<label class="ag-prev-field">
|
|
6616
|
-
<span class="ag-prev-label">${this.escape(
|
|
6930
|
+
<span class="ag-prev-label">${this.escape(this._t("ag_preview_bio"))}</span>
|
|
6617
6931
|
<textarea class="ag-prev-input ag-prev-textarea" data-agent-spec-field="bio" maxlength="280" rows="2">${this.escape(spec.bio)}</textarea>
|
|
6618
6932
|
</label>
|
|
6619
6933
|
|
|
6620
6934
|
<label class="ag-prev-field">
|
|
6621
|
-
<span class="ag-prev-label">${this.escape(
|
|
6935
|
+
<span class="ag-prev-label">${this.escape(this._t("ag_preview_quote"))}</span>
|
|
6622
6936
|
<textarea class="ag-prev-input ag-prev-textarea" data-agent-spec-field="coverQuote" maxlength="200" rows="2">${this.escape(spec.coverQuote || "")}</textarea>
|
|
6623
6937
|
</label>
|
|
6624
6938
|
|
|
6625
6939
|
<label class="ag-prev-field">
|
|
6626
|
-
<span class="ag-prev-label">${this.escape(
|
|
6940
|
+
<span class="ag-prev-label">${this.escape(this._t("ag_preview_instruction"))}</span>
|
|
6627
6941
|
<textarea class="ag-prev-input ag-prev-textarea ag-prev-instr" data-agent-spec-field="instruction" maxlength="4000" rows="10">${this.escape(spec.instruction)}</textarea>
|
|
6628
6942
|
</label>
|
|
6629
6943
|
</div>
|
|
6630
6944
|
|
|
6631
6945
|
<footer class="ag-prev-foot">
|
|
6632
|
-
<button type="button" class="ag-prev-discard" data-agent-spec-discard>${this.escape(
|
|
6633
|
-
<button type="button" class="ag-prev-redo" data-agent-spec-redo>↻ ${this.escape(
|
|
6946
|
+
<button type="button" class="ag-prev-discard" data-agent-spec-discard>${this.escape(this._t("ag_preview_discard"))}</button>
|
|
6947
|
+
<button type="button" class="ag-prev-redo" data-agent-spec-redo>↻ ${this.escape(this._t("ag_preview_redo"))}</button>
|
|
6634
6948
|
<button type="button" class="ag-prev-save" data-agent-spec-save>
|
|
6635
6949
|
<span class="ag-prev-save-mark">◆</span>
|
|
6636
|
-
<span>${this.escape(
|
|
6950
|
+
<span>${this.escape(this._t("ag_preview_save"))}</span>
|
|
6637
6951
|
</button>
|
|
6638
6952
|
</footer>
|
|
6639
6953
|
</div>
|
|
@@ -6647,43 +6961,35 @@
|
|
|
6647
6961
|
* composer back to its input state). The description text is
|
|
6648
6962
|
* surfaced read-only so the user can copy it before discard. */
|
|
6649
6963
|
renderAgentSpecErrorHtml(err, lang) {
|
|
6650
|
-
|
|
6651
|
-
const
|
|
6652
|
-
|
|
6653
|
-
title: err.kind === "timeout" ? "Generation didn't complete after 5 minutes" : "Generation failed",
|
|
6654
|
-
hintTimeout: "The model may be slow, the network flaky, or the backend pipeline stalled. Click retry to start a fresh run.",
|
|
6655
|
-
hintFailed: "Check your API key configuration and model reachability. Retry often clears transient failures.",
|
|
6656
|
-
descLabel: "Your description (re-used on retry)",
|
|
6657
|
-
retry: "Retry",
|
|
6658
|
-
discard: "Discard",
|
|
6659
|
-
};
|
|
6964
|
+
const kicker = err.kind === "timeout" ? this._t("ag_err_kicker_timeout") : this._t("ag_err_kicker_fail");
|
|
6965
|
+
const title = err.kind === "timeout" ? this._t("ag_err_title_timeout") : this._t("ag_err_title_fail");
|
|
6966
|
+
const hint = err.kind === "timeout" ? this._t("ag_err_hint_timeout") : this._t("ag_err_hint_fail");
|
|
6660
6967
|
const desc = this._agentComposerLastDesc || "";
|
|
6661
|
-
const hint = err.kind === "timeout" ? t.hintTimeout : t.hintFailed;
|
|
6662
6968
|
const detail = err.message ? `<div class="ag-gen-error-detail">${this.escape(err.message)}</div>` : "";
|
|
6663
6969
|
return `
|
|
6664
6970
|
<section class="cmp ag-cmp">
|
|
6665
6971
|
<header class="cmp-hero">
|
|
6666
6972
|
<div class="cmp-greet">${this.escape(this.composerGreeting(lang, (this.prefs?.name || "you").trim() || "you"))}</div>
|
|
6667
|
-
<h1 class="cmp-prompt">${this.escape("
|
|
6973
|
+
<h1 class="cmp-prompt">${this.escape(this._t("ag_err_prompt"))}</h1>
|
|
6668
6974
|
</header>
|
|
6669
6975
|
<div class="ag-gen-error-card">
|
|
6670
|
-
<div class="ag-gen-error-kicker">${this.escape(
|
|
6671
|
-
<h2 class="ag-gen-error-title">${this.escape(
|
|
6976
|
+
<div class="ag-gen-error-kicker">${this.escape(kicker)}</div>
|
|
6977
|
+
<h2 class="ag-gen-error-title">${this.escape(title)}</h2>
|
|
6672
6978
|
<p class="ag-gen-error-hint">${this.escape(hint)}</p>
|
|
6673
6979
|
${detail}
|
|
6674
6980
|
${desc ? `
|
|
6675
6981
|
<div class="ag-gen-error-desc">
|
|
6676
|
-
<div class="ag-gen-error-desc-label">${this.escape(
|
|
6982
|
+
<div class="ag-gen-error-desc-label">${this.escape(this._t("ag_err_desc"))}</div>
|
|
6677
6983
|
<div class="ag-gen-error-desc-body">${this.escape(desc)}</div>
|
|
6678
6984
|
</div>
|
|
6679
6985
|
` : ""}
|
|
6680
6986
|
<div class="ag-gen-error-actions">
|
|
6681
6987
|
<button type="button" class="ag-gen-error-retry" data-agent-spec-retry>
|
|
6682
6988
|
<span class="ag-gen-error-retry-mark">↻</span>
|
|
6683
|
-
<span>${this.escape(
|
|
6989
|
+
<span>${this.escape(this._t("ag_err_retry"))}</span>
|
|
6684
6990
|
</button>
|
|
6685
6991
|
<button type="button" class="ag-gen-error-discard" data-agent-spec-error-discard>
|
|
6686
|
-
${this.escape(
|
|
6992
|
+
${this.escape(this._t("ag_err_discard"))}
|
|
6687
6993
|
</button>
|
|
6688
6994
|
</div>
|
|
6689
6995
|
</div>
|
|
@@ -6726,7 +7032,7 @@
|
|
|
6726
7032
|
return `<text x="${lx.toFixed(1)}" y="${(ly + 2.5).toFixed(1)}" text-anchor="${anchor}" class="ag-prev-radar-axis-label">${labels[a]}</text>`;
|
|
6727
7033
|
}).join("");
|
|
6728
7034
|
return `
|
|
6729
|
-
<svg class="ag-prev-radar-svg" viewBox="0 0 ${vbW} ${vbH}" xmlns="http://www.w3.org/2000/svg" aria-label="
|
|
7035
|
+
<svg class="ag-prev-radar-svg" viewBox="0 0 ${vbW} ${vbH}" xmlns="http://www.w3.org/2000/svg" aria-label="${this.escape(this._t("ag_preview_radar_aria"))}">
|
|
6730
7036
|
${grid}
|
|
6731
7037
|
${spokes}
|
|
6732
7038
|
<polygon points="${curPoly}" class="ag-prev-radar-current"/>
|
|
@@ -7136,7 +7442,7 @@
|
|
|
7136
7442
|
<span class="composer-pick-title">${this.escape(t.title)}</span>
|
|
7137
7443
|
<span class="composer-pick-hint">${this.escape(t.hint)}</span>
|
|
7138
7444
|
</div>
|
|
7139
|
-
<div class="composer-pick-list">${rows || `<div class="composer-pick-empty"
|
|
7445
|
+
<div class="composer-pick-list">${rows || `<div class="composer-pick-empty">${this.escape(this._t("picker_no_directors"))}</div>`}</div>
|
|
7140
7446
|
<div class="composer-pick-foot">
|
|
7141
7447
|
<button type="button" class="composer-pick-done" data-composer-pick-done>${this.escape(t.done)}</button>
|
|
7142
7448
|
</div>
|
|
@@ -7294,6 +7600,26 @@
|
|
|
7294
7600
|
if (v) v.textContent = intensity;
|
|
7295
7601
|
},
|
|
7296
7602
|
|
|
7603
|
+
setComposerDeliveryMode(deliveryMode) {
|
|
7604
|
+
const state = this.loadComposerState();
|
|
7605
|
+
state.deliveryMode = deliveryMode === "voice" ? "voice" : "text";
|
|
7606
|
+
this.saveComposerState();
|
|
7607
|
+
// Sync the button-style toggle's classes / aria / data attrs /
|
|
7608
|
+
// visible text. Was a checkbox previously (cb.checked = ...);
|
|
7609
|
+
// the new register uses .ap-skill-row-toggle so we mirror what
|
|
7610
|
+
// the click handler does for the in-place mutation case.
|
|
7611
|
+
const btn = document.querySelector("[data-composer-voice-toggle]");
|
|
7612
|
+
if (btn) {
|
|
7613
|
+
const on = state.deliveryMode === "voice";
|
|
7614
|
+
btn.classList.toggle("on", on);
|
|
7615
|
+
btn.classList.toggle("off", !on);
|
|
7616
|
+
btn.setAttribute("data-on", on ? "1" : "0");
|
|
7617
|
+
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
7618
|
+
const txt = btn.querySelector(".ap-skill-row-toggle-text");
|
|
7619
|
+
if (txt) txt.textContent = this._t("cmp_voice_label");
|
|
7620
|
+
}
|
|
7621
|
+
},
|
|
7622
|
+
|
|
7297
7623
|
/** Generic option-list dropdown anchored under a tune trigger
|
|
7298
7624
|
* button. Used for both tone and intensity. Each option is a
|
|
7299
7625
|
* full-text row with a short hint (current/calmer/etc). Click an
|
|
@@ -7345,6 +7671,28 @@
|
|
|
7345
7671
|
} else {
|
|
7346
7672
|
current = state.intensity;
|
|
7347
7673
|
}
|
|
7674
|
+
} else if (kind === "delivery") {
|
|
7675
|
+
opts = lang === "zh"
|
|
7676
|
+
? [
|
|
7677
|
+
{ v: "text", label: "Text", hint: "最快的文字会议" },
|
|
7678
|
+
{ v: "voice", label: "Voice", hint: "口语化 · 逐位播放" },
|
|
7679
|
+
]
|
|
7680
|
+
: [
|
|
7681
|
+
{ v: "text", label: "Text", hint: "fast written meeting" },
|
|
7682
|
+
{ v: "voice", label: "Voice", hint: "spoken · paced turns" },
|
|
7683
|
+
];
|
|
7684
|
+
current = state.deliveryMode === "voice" ? "voice" : "text";
|
|
7685
|
+
} else if (kind === "locale") {
|
|
7686
|
+
// Interface language picker · routes through the shared
|
|
7687
|
+
// .cmp-dd dropdown vocabulary so the User-settings pane
|
|
7688
|
+
// doesn't ship a separate two-button toggle. Hints describe
|
|
7689
|
+
// what the locale switch affects so the user knows it's the
|
|
7690
|
+
// chrome language, not the brief language.
|
|
7691
|
+
opts = [
|
|
7692
|
+
{ v: "en", label: "EN", hint: "interface in english" },
|
|
7693
|
+
{ v: "zh", label: "中文", hint: "界面语言切到中文" },
|
|
7694
|
+
];
|
|
7695
|
+
current = (window.I18n && window.I18n.getLocale && window.I18n.getLocale()) || "en";
|
|
7348
7696
|
} else if (kind === "agent-model") {
|
|
7349
7697
|
// Reachable-only model catalog · pulls from the shared
|
|
7350
7698
|
// /api/models cache so the picker reflects the user's
|
|
@@ -7471,6 +7819,11 @@
|
|
|
7471
7819
|
if (ta) ta.focus();
|
|
7472
7820
|
return;
|
|
7473
7821
|
}
|
|
7822
|
+
// Unlock audio playback on this user gesture — required by
|
|
7823
|
+
// browser autoplay policies before we can play TTS audio later.
|
|
7824
|
+
if (state.deliveryMode === "voice") {
|
|
7825
|
+
this.unlockAudioPlayback();
|
|
7826
|
+
}
|
|
7474
7827
|
// Pre-flight · convening triggers chair convening + auto-pick +
|
|
7475
7828
|
// clarify, all of which need a model key. Bail early and prompt
|
|
7476
7829
|
// the user to configure one if missing.
|
|
@@ -7492,6 +7845,7 @@
|
|
|
7492
7845
|
agentIds: useAutoPick ? [] : state.directorIds.slice(),
|
|
7493
7846
|
mode: state.mode,
|
|
7494
7847
|
intensity: state.intensity,
|
|
7848
|
+
deliveryMode: state.deliveryMode,
|
|
7495
7849
|
autoPick: useAutoPick,
|
|
7496
7850
|
});
|
|
7497
7851
|
// Clear the saved draft now that the room is convened — next
|
|
@@ -7612,6 +7966,15 @@
|
|
|
7612
7966
|
}
|
|
7613
7967
|
},
|
|
7614
7968
|
|
|
7969
|
+
/** Tone hover copy · i18n keyed by mode, falls back to TONE_TIPS. */
|
|
7970
|
+
toneTipFor(mode) {
|
|
7971
|
+
const m = mode || "";
|
|
7972
|
+
const k = "tone_tip_" + m;
|
|
7973
|
+
const tr = this._t(k);
|
|
7974
|
+
if (tr !== k) return tr;
|
|
7975
|
+
return TONE_TIPS[m] || "";
|
|
7976
|
+
},
|
|
7977
|
+
|
|
7615
7978
|
renderHeader() {
|
|
7616
7979
|
const head = document.querySelector("[data-room-head]");
|
|
7617
7980
|
if (!head || !this.currentRoom) return;
|
|
@@ -7627,23 +7990,33 @@
|
|
|
7627
7990
|
.map((a) => `<img data-agent="${this.escape(a.id)}" src="${this.escape(a.avatarPath)}" alt="${this.escape(a.name)}" title="${this.escape(a.name)}">`)
|
|
7628
7991
|
.join("");
|
|
7629
7992
|
const castCount = this.currentMembers.length;
|
|
7993
|
+
const castTitle =
|
|
7994
|
+
castCount <= 1
|
|
7995
|
+
? this._t("room_cast_title_1")
|
|
7996
|
+
: this._t("room_cast_n", { n: castCount });
|
|
7630
7997
|
const castHtml = castImgs +
|
|
7631
7998
|
(castCount > 0
|
|
7632
|
-
? `<span class="cast-count" title="${
|
|
7999
|
+
? `<span class="cast-count" title="${this.escape(castTitle)}">${castCount}</span>`
|
|
7633
8000
|
: "");
|
|
7634
8001
|
|
|
7635
8002
|
const tone = r.mode || "constructive";
|
|
7636
8003
|
const intensity = r.intensity || "sharp";
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
8004
|
+
const briefStyle =
|
|
8005
|
+
typeof r.briefStyle === "string" && r.briefStyle.trim()
|
|
8006
|
+
? r.briefStyle.trim()
|
|
8007
|
+
: "auto";
|
|
8008
|
+
|
|
8009
|
+
// Status timestamp — what was on the right (paused-stamp / stamp) now
|
|
8010
|
+
// lives inline in the meta row.
|
|
8011
|
+
let stamp = "";
|
|
8012
|
+
if (r.status === "paused" && r.pausedAt) {
|
|
8013
|
+
stamp = this._t("room_stamp_paused", { ago: this.relTime(r.pausedAt) });
|
|
8014
|
+
} else if (r.status === "adjourned" && r.adjournedAt) {
|
|
8015
|
+
stamp = this._t("room_stamp_adjourned", { ago: this.relTime(r.adjournedAt) });
|
|
8016
|
+
} else if (r.status === "live" && r.createdAt) {
|
|
8017
|
+
stamp = this._t("room_stamp_opened", { ago: this.relTime(r.createdAt) });
|
|
8018
|
+
}
|
|
8019
|
+
const toneTip = this.toneTipFor(tone);
|
|
7647
8020
|
|
|
7648
8021
|
// Three primary actions, one per state — CSS hides the wrong ones based
|
|
7649
8022
|
// on html[data-status]. Per PRD §5.2.3:
|
|
@@ -7662,13 +8035,23 @@
|
|
|
7662
8035
|
// tagged-meta-pills) — net height reduction ~150 → ~85px.
|
|
7663
8036
|
// Tone / intensity / brief-style remain editable in the room
|
|
7664
8037
|
// settings overlay; only their on-header surface is collapsed.
|
|
8038
|
+
// Compact two-row layout · kicker carries all the meta (room
|
|
8039
|
+
// number / tone / intensity / status) on a single mono line
|
|
8040
|
+
// above the subject. Replaces the prior three-row stack of
|
|
8041
|
+
// `.room-id` + subject + `.room-meta` tagged pills which the
|
|
8042
|
+
// CSS in index.html no longer styles. Tone / intensity stay
|
|
8043
|
+
// editable in the room-settings overlay.
|
|
8044
|
+
// System chrome is English-only per the project rule (only
|
|
8045
|
+
// user / LLM content follows query language), so the kicker
|
|
8046
|
+
// tokens are not routed through `_t()`.
|
|
8047
|
+
const statusWord = r.status !== "live" ? String(r.status).toUpperCase() : "";
|
|
7665
8048
|
head.innerHTML = `
|
|
7666
|
-
<button type="button" class="room-head-expand" data-sidebar-expand title="
|
|
8049
|
+
<button type="button" class="room-head-expand" data-sidebar-expand title="${this.escape(this._t("sidebar_expand"))}" aria-label="${this.escape(this._t("sidebar_expand"))}"></button>
|
|
7667
8050
|
<div class="room-info">
|
|
7668
8051
|
<div class="room-kicker">
|
|
7669
8052
|
<span class="kicker-num">// ROOM #${r.number}</span>
|
|
7670
8053
|
<span class="kicker-sep">·</span>
|
|
7671
|
-
<span class="kicker-tone" data-tone-tip="${this.escape(
|
|
8054
|
+
<span class="kicker-tone" data-tone-tip="${this.escape(toneTip)}">${this.escape(tone.toUpperCase())}</span>
|
|
7672
8055
|
<span class="kicker-sep">·</span>
|
|
7673
8056
|
<span class="kicker-intensity">${this.escape(intensity.toUpperCase())}</span>
|
|
7674
8057
|
${statusWord ? `<span class="kicker-sep">·</span><span class="kicker-status status-${this.escape(r.status)}">${statusWord}</span>` : ""}
|
|
@@ -7677,9 +8060,9 @@
|
|
|
7677
8060
|
</div>
|
|
7678
8061
|
<div class="head-actions">
|
|
7679
8062
|
<div class="head-cast">${castHtml}</div>
|
|
7680
|
-
<a href="#" class="room-settings-trigger" data-room-settings-trigger title="
|
|
7681
|
-
<a href="#" class="pause-btn" data-pause>[ <span class="pause-icon">❚❚</span>
|
|
7682
|
-
<a href="#" class="resume-btn" data-resume>[ ▶
|
|
8063
|
+
<a href="#" class="room-settings-trigger" data-room-settings-trigger title="${this.escape(this._t("room_settings"))}" aria-label="${this.escape(this._t("room_settings"))}">⚙</a>
|
|
8064
|
+
<a href="#" class="pause-btn" data-pause>[ <span class="pause-icon">❚❚</span> ${this.escape(this._t("room_pause_verb"))} ]</a>
|
|
8065
|
+
<a href="#" class="resume-btn" data-resume>[ ▶ ${this.escape(this._t("room_resume_verb"))} ]</a>
|
|
7683
8066
|
${this.currentBrief
|
|
7684
8067
|
? (() => {
|
|
7685
8068
|
// Multiple briefs · render the View Report button as a
|
|
@@ -7692,13 +8075,12 @@
|
|
|
7692
8075
|
const multi = briefs.length > 1;
|
|
7693
8076
|
const directHref = this.briefViewerHref(this.currentBrief, r.id) || `/report.html?r=${this.escape(r.id)}`;
|
|
7694
8077
|
if (!multi) {
|
|
7695
|
-
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report
|
|
8078
|
+
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report>${this.escape(this._t("room_view_report"))}</a>`;
|
|
7696
8079
|
}
|
|
7697
|
-
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report data-view-report-trigger title="${this.escape(
|
|
8080
|
+
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report data-view-report-trigger title="${this.escape(this._t("room_view_report_title_multi", { n: briefs.length }))}">${this.escape(this._t("room_view_report_multi", { n: briefs.length }))}</a>`;
|
|
7698
8081
|
})()
|
|
7699
8082
|
: (r.status === "adjourned"
|
|
7700
|
-
? `<a href="#" class="view-report-btn generate-report" data-generate-brief title="${this.escape(
|
|
7701
|
-
: "")}
|
|
8083
|
+
? `<a href="#" class="view-report-btn generate-report" data-generate-brief title="${this.escape(this._t("room_generate_report_title"))}"><span class="vr-mark">▸</span> ${this.escape(this._t("room_generate_report"))}</a>` : "")}
|
|
7702
8084
|
</div>
|
|
7703
8085
|
`;
|
|
7704
8086
|
// Wire the tone-tag hover tip. Pure-CSS ::after tooltips were
|
|
@@ -7763,8 +8145,13 @@
|
|
|
7763
8145
|
}
|
|
7764
8146
|
const messages = this.currentMessages.slice();
|
|
7765
8147
|
const r = this.currentRoom;
|
|
8148
|
+
const tBanner = this._t("chat_banner", {
|
|
8149
|
+
when: new Date(r.createdAt).toLocaleString(),
|
|
8150
|
+
n: this.currentMembers.length,
|
|
8151
|
+
mode: this.escape(r.mode),
|
|
8152
|
+
});
|
|
7766
8153
|
const banner = r
|
|
7767
|
-
? `<div class="chat-banner"><span class="chat-banner-chip"><span class="cb-mark">▸</span><span class="cb-text"
|
|
8154
|
+
? `<div class="chat-banner"><span class="chat-banner-chip"><span class="cb-mark">▸</span><span class="cb-text">${this.escape(tBanner)}</span></span></div>`
|
|
7768
8155
|
: "";
|
|
7769
8156
|
const openerId = this.firstUserMessageId();
|
|
7770
8157
|
// Convening card · appended at the tail of the chat while the
|
|
@@ -7795,9 +8182,18 @@
|
|
|
7795
8182
|
? ["analyzing", "seating", "preparing"]
|
|
7796
8183
|
: ["preparing"];
|
|
7797
8184
|
const STAGE_LABELS = {
|
|
7798
|
-
analyzing: {
|
|
7799
|
-
|
|
7800
|
-
|
|
8185
|
+
analyzing: {
|
|
8186
|
+
title: this._t("conv_stage_analyzing_title"),
|
|
8187
|
+
deck: this._t("conv_stage_analyzing_deck"),
|
|
8188
|
+
},
|
|
8189
|
+
seating: {
|
|
8190
|
+
title: this._t("conv_stage_seating_title"),
|
|
8191
|
+
deck: this._t("conv_stage_seating_deck"),
|
|
8192
|
+
},
|
|
8193
|
+
preparing: {
|
|
8194
|
+
title: this._t("conv_stage_preparing_title"),
|
|
8195
|
+
deck: this._t("conv_stage_preparing_deck"),
|
|
8196
|
+
},
|
|
7801
8197
|
};
|
|
7802
8198
|
|
|
7803
8199
|
const currentIdx = stageOrder.indexOf(s.stage);
|
|
@@ -7829,8 +8225,7 @@
|
|
|
7829
8225
|
// already in currentMembers; rendering it here would be redundant).
|
|
7830
8226
|
const seatedRow = (s.autoPicked && s.seated.length > 0) ? `
|
|
7831
8227
|
<div class="conv-seated">
|
|
7832
|
-
<div class="conv-seated-label"
|
|
7833
|
-
<div class="conv-seated-list">
|
|
8228
|
+
<div class="conv-seated-label">${this.escape(this._t("conv_seated_label"))} · ${s.seated.length}</div> <div class="conv-seated-list">
|
|
7834
8229
|
${s.seated.map((a) => `
|
|
7835
8230
|
<div class="conv-seated-item" data-agent-id="${this.escape(a.id)}">
|
|
7836
8231
|
<img class="conv-seated-av" src="${this.escape(a.avatarPath)}" alt="${this.escape(a.name)}">
|
|
@@ -7846,8 +8241,7 @@
|
|
|
7846
8241
|
|
|
7847
8242
|
return `
|
|
7848
8243
|
<article class="convening-card" data-convene-card>
|
|
7849
|
-
<div class="conv-eyebrow"
|
|
7850
|
-
${s.subject ? `<blockquote class="conv-subject">${this.escape(s.subject)}</blockquote>` : ""}
|
|
8244
|
+
<div class="conv-eyebrow">${this.escape(this._t("conv_banner_convening"))}</div> ${s.subject ? `<blockquote class="conv-subject">${this.escape(s.subject)}</blockquote>` : ""}
|
|
7851
8245
|
<ul class="conv-stages">${stagesHtml}</ul>
|
|
7852
8246
|
${seatedRow}
|
|
7853
8247
|
</article>
|
|
@@ -7883,11 +8277,16 @@
|
|
|
7883
8277
|
const chat = document.querySelector("[data-chat-messages]");
|
|
7884
8278
|
if (!chat) return;
|
|
7885
8279
|
const existing = chat.querySelector("[data-chair-pending]");
|
|
7886
|
-
|
|
7887
|
-
const
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
8280
|
+
const chairName = (this.currentChair?.name) || this._t("msg_chair_display_fallback");
|
|
8281
|
+
const phaseKeys = {
|
|
8282
|
+
clarify: "chair_pend_clarify",
|
|
8283
|
+
"chair-direct": "chair_pend_chair_direct",
|
|
8284
|
+
"round-end": "chair_pend_round_end",
|
|
8285
|
+
convening: "chair_pend_convening",
|
|
8286
|
+
"next-speaker": "chair_pend_next_speaker",
|
|
8287
|
+
};
|
|
8288
|
+
const pendKey = phaseKeys[phase] || "chair_pend_default";
|
|
8289
|
+
const phrase = this._t(pendKey, { name: chairName });
|
|
7891
8290
|
if (existing) {
|
|
7892
8291
|
const span = existing.querySelector(".cp-text");
|
|
7893
8292
|
if (span) span.textContent = phrase;
|
|
@@ -7898,7 +8297,7 @@
|
|
|
7898
8297
|
node.setAttribute("data-chair-pending", "");
|
|
7899
8298
|
node.innerHTML = `
|
|
7900
8299
|
<div class="cp-rule" aria-hidden="true"></div>
|
|
7901
|
-
<div class="cp-kicker"
|
|
8300
|
+
<div class="cp-kicker">${this.escape(this._t("chair_pend_banner"))}</div>
|
|
7902
8301
|
<div class="cp-body">
|
|
7903
8302
|
<span class="cp-text">${this.escape(phrase)}</span>
|
|
7904
8303
|
<span class="thinking-dots"><span></span><span></span><span></span></span>
|
|
@@ -7925,6 +8324,145 @@
|
|
|
7925
8324
|
if (existing) existing.remove();
|
|
7926
8325
|
},
|
|
7927
8326
|
|
|
8327
|
+
enqueueVoiceChunk(roomId, chunk) {
|
|
8328
|
+
if (!chunk || !chunk.messageId || !chunk.audioBase64 || !chunk.mimeType) return;
|
|
8329
|
+
let q = this.voiceQueues[chunk.messageId];
|
|
8330
|
+
if (!q) {
|
|
8331
|
+
q = this._createVoiceStream(roomId, chunk.messageId);
|
|
8332
|
+
this.voiceQueues[chunk.messageId] = q;
|
|
8333
|
+
}
|
|
8334
|
+
// Convert base64 to Uint8Array and append to the MSE SourceBuffer
|
|
8335
|
+
const binary = atob(chunk.audioBase64);
|
|
8336
|
+
const bytes = new Uint8Array(binary.length);
|
|
8337
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
8338
|
+
q.pendingBuffers.push(bytes);
|
|
8339
|
+
this._flushVoiceBuffer(q);
|
|
8340
|
+
},
|
|
8341
|
+
|
|
8342
|
+
/** Create a MediaSource-backed audio stream for one speaker's turn. */
|
|
8343
|
+
_createVoiceStream(roomId, messageId) {
|
|
8344
|
+
const audio = new Audio();
|
|
8345
|
+
const ms = new MediaSource();
|
|
8346
|
+
audio.src = URL.createObjectURL(ms);
|
|
8347
|
+
|
|
8348
|
+
const q = {
|
|
8349
|
+
roomId,
|
|
8350
|
+
messageId,
|
|
8351
|
+
audio,
|
|
8352
|
+
mediaSource: ms,
|
|
8353
|
+
sourceBuffer: null,
|
|
8354
|
+
pendingBuffers: [],
|
|
8355
|
+
final: false,
|
|
8356
|
+
doneSent: false,
|
|
8357
|
+
ready: false,
|
|
8358
|
+
};
|
|
8359
|
+
|
|
8360
|
+
ms.addEventListener("sourceopen", () => {
|
|
8361
|
+
try {
|
|
8362
|
+
q.sourceBuffer = ms.addSourceBuffer("audio/mpeg");
|
|
8363
|
+
q.sourceBuffer.addEventListener("updateend", () => this._flushVoiceBuffer(q));
|
|
8364
|
+
q.ready = true;
|
|
8365
|
+
this._flushVoiceBuffer(q);
|
|
8366
|
+
} catch (e) {
|
|
8367
|
+
console.warn("[voice] addSourceBuffer failed:", e);
|
|
8368
|
+
}
|
|
8369
|
+
});
|
|
8370
|
+
|
|
8371
|
+
// Start playing as soon as we have some data
|
|
8372
|
+
audio.play().catch(() => {});
|
|
8373
|
+
|
|
8374
|
+
return q;
|
|
8375
|
+
},
|
|
8376
|
+
|
|
8377
|
+
/** Flush pending buffers into the SourceBuffer one at a time. */
|
|
8378
|
+
_flushVoiceBuffer(q) {
|
|
8379
|
+
if (!q.ready || !q.sourceBuffer || q.sourceBuffer.updating) return;
|
|
8380
|
+
if (q.pendingBuffers.length > 0) {
|
|
8381
|
+
const buf = q.pendingBuffers.shift();
|
|
8382
|
+
try {
|
|
8383
|
+
q.sourceBuffer.appendBuffer(buf);
|
|
8384
|
+
} catch (e) {
|
|
8385
|
+
console.warn("[voice] appendBuffer failed:", e);
|
|
8386
|
+
}
|
|
8387
|
+
return;
|
|
8388
|
+
}
|
|
8389
|
+
// No more pending buffers. If final, signal end of stream.
|
|
8390
|
+
if (q.final && !q.doneSent) {
|
|
8391
|
+
q.doneSent = true;
|
|
8392
|
+
try {
|
|
8393
|
+
if (q.mediaSource.readyState === "open") {
|
|
8394
|
+
q.mediaSource.endOfStream();
|
|
8395
|
+
}
|
|
8396
|
+
} catch (_) {}
|
|
8397
|
+
// Wait for audio to finish playing, then fire voice-done
|
|
8398
|
+
q.audio.addEventListener("ended", () => this._fireVoiceDone(q));
|
|
8399
|
+
// Safety timeout in case 'ended' doesn't fire (e.g. empty stream)
|
|
8400
|
+
const duration = q.audio.duration;
|
|
8401
|
+
const remaining = isFinite(duration) ? (duration - q.audio.currentTime) : 0;
|
|
8402
|
+
setTimeout(() => {
|
|
8403
|
+
if (!this.voiceQueues[q.messageId]) return; // already fired
|
|
8404
|
+
this._fireVoiceDone(q);
|
|
8405
|
+
}, (remaining * 1000) + 2000);
|
|
8406
|
+
}
|
|
8407
|
+
},
|
|
8408
|
+
|
|
8409
|
+
_scheduleVoiceDone(q) {
|
|
8410
|
+
// Called from voice-final handler — trigger flush check which will
|
|
8411
|
+
// endOfStream + wait for playback to finish.
|
|
8412
|
+
this._flushVoiceBuffer(q);
|
|
8413
|
+
},
|
|
8414
|
+
|
|
8415
|
+
_fireVoiceDone(q) {
|
|
8416
|
+
if (!this.voiceQueues[q.messageId]) return; // already fired
|
|
8417
|
+
delete this.voiceQueues[q.messageId];
|
|
8418
|
+
// Clean up audio element
|
|
8419
|
+
try { q.audio.pause(); } catch (_) {}
|
|
8420
|
+
try { URL.revokeObjectURL(q.audio.src); } catch (_) {}
|
|
8421
|
+
fetch(`/api/rooms/${encodeURIComponent(q.roomId)}/messages/${encodeURIComponent(q.messageId)}/voice-done`, {
|
|
8422
|
+
method: "POST",
|
|
8423
|
+
}).catch(() => {});
|
|
8424
|
+
},
|
|
8425
|
+
|
|
8426
|
+
async drainVoiceQueue(roomId, messageId) {
|
|
8427
|
+
// Called from voice-final handler — mark final and flush
|
|
8428
|
+
const q = this.voiceQueues[messageId];
|
|
8429
|
+
if (!q) return;
|
|
8430
|
+
if (q.final && !q.doneSent) {
|
|
8431
|
+
this._flushVoiceBuffer(q);
|
|
8432
|
+
}
|
|
8433
|
+
},
|
|
8434
|
+
|
|
8435
|
+
/** Immediately stop all voice playback and discard pending queues.
|
|
8436
|
+
* Used on hard-pause to silence mid-stream audio. */
|
|
8437
|
+
stopVoicePlayback() {
|
|
8438
|
+
for (const messageId of Object.keys(this.voiceQueues)) {
|
|
8439
|
+
const q = this.voiceQueues[messageId];
|
|
8440
|
+
if (q && q.audio) {
|
|
8441
|
+
try { q.audio.pause(); } catch (_) {}
|
|
8442
|
+
try { URL.revokeObjectURL(q.audio.src); } catch (_) {}
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
this.voiceQueues = {};
|
|
8446
|
+
this._voiceCurrentMessageId = null;
|
|
8447
|
+
},
|
|
8448
|
+
|
|
8449
|
+
/** Unlock HTMLAudioElement autoplay policy.
|
|
8450
|
+
* Must be called inside a user-gesture handler (click/keydown). */
|
|
8451
|
+
unlockAudioPlayback() {
|
|
8452
|
+
if (this._audioUnlocked) return;
|
|
8453
|
+
try {
|
|
8454
|
+
// Play a silent audio to satisfy autoplay policy
|
|
8455
|
+
const silence = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=");
|
|
8456
|
+
silence.volume = 0;
|
|
8457
|
+
const p = silence.play();
|
|
8458
|
+
if (p) p.then(() => silence.pause()).catch(() => {});
|
|
8459
|
+
this._audioUnlocked = true;
|
|
8460
|
+
console.log("[voice] audio playback unlocked via user gesture");
|
|
8461
|
+
} catch (e) {
|
|
8462
|
+
console.warn("[voice] unlock failed:", e);
|
|
8463
|
+
}
|
|
8464
|
+
},
|
|
8465
|
+
|
|
7928
8466
|
updateMessageBodyDom(messageId, body, streaming) {
|
|
7929
8467
|
const article = document.querySelector(`[data-message-id="${messageId}"]`);
|
|
7930
8468
|
if (!article) return;
|
|
@@ -8050,14 +8588,14 @@
|
|
|
8050
8588
|
const m = this.currentMessages.find((x) => x.id === messageId);
|
|
8051
8589
|
const n = (m && m.meta && typeof m.meta.roundNum === "number") ? m.meta.roundNum : null;
|
|
8052
8590
|
const prefix = n != null
|
|
8053
|
-
? `<span class="rp-spent-round"
|
|
8591
|
+
? `<span class="rp-spent-round">${this.escape(this._t("rp_spent_round", { n }))}</span><span class="rp-spent-sep">·</span>`
|
|
8054
8592
|
: "";
|
|
8055
8593
|
return `
|
|
8056
8594
|
<div class="round-prompt-card spent" data-round-prompt-card="${this.escape(messageId)}">
|
|
8057
8595
|
<span class="rp-spent-chip">
|
|
8058
8596
|
<span class="rp-spent-mark">◇</span>
|
|
8059
8597
|
${prefix}
|
|
8060
|
-
<span class="rp-spent-label"
|
|
8598
|
+
<span class="rp-spent-label">${this.escape(this._t("rp_spent_label"))}</span>
|
|
8061
8599
|
</span>
|
|
8062
8600
|
</div>
|
|
8063
8601
|
`;
|
|
@@ -8076,16 +8614,11 @@
|
|
|
8076
8614
|
const recKind = rec && (rec.kind === "end" || rec.kind === "continue") ? rec.kind : null;
|
|
8077
8615
|
const endClass = recKind === "end" ? " recommended" : "";
|
|
8078
8616
|
const contClass = recKind === "continue" ? " recommended" : "";
|
|
8079
|
-
// Detect language for the indicator label · Chinese rooms get
|
|
8080
|
-
// 中文 copy. Reads from the chair's prompt body which lands in
|
|
8081
|
-
// the message body when a recommendation is present.
|
|
8082
|
-
const lang = (promptMsg && promptMsg.body && /[一-鿿]/.test(promptMsg.body)) ? "zh" : "en";
|
|
8083
|
-
const recLabel = "chair recommends";
|
|
8084
8617
|
const recIndicator = recKind
|
|
8085
8618
|
? `
|
|
8086
8619
|
<div class="rp-rec-line rp-rec-line-${this.escape(recKind)}">
|
|
8087
8620
|
<span class="rp-rec-arrow" aria-hidden="true">↑</span>
|
|
8088
|
-
<span class="rp-rec-text">${this.escape(
|
|
8621
|
+
<span class="rp-rec-text">${this.escape(this._t("rp_chair_recommends"))}</span>
|
|
8089
8622
|
</div>
|
|
8090
8623
|
`
|
|
8091
8624
|
: "";
|
|
@@ -8094,17 +8627,17 @@
|
|
|
8094
8627
|
<div class="rp-primary">
|
|
8095
8628
|
<button type="button" class="rp-btn vote${endClass}" data-round-end>
|
|
8096
8629
|
<span class="rp-mark">▣</span>
|
|
8097
|
-
<span class="rp-label"
|
|
8630
|
+
<span class="rp-label">${this.escape(this._t("round_end_open_vote"))}</span>
|
|
8098
8631
|
</button>
|
|
8099
8632
|
<button type="button" class="rp-btn continue${contClass}" data-continue-auto>
|
|
8100
8633
|
<span class="rp-mark">▶</span>
|
|
8101
|
-
<span class="rp-label"
|
|
8634
|
+
<span class="rp-label">${this.escape(this._t("rp_continue_next"))}</span>
|
|
8102
8635
|
<span class="rp-timer" data-continue-timer></span>
|
|
8103
8636
|
</button>
|
|
8104
8637
|
</div>
|
|
8105
8638
|
${recIndicator}
|
|
8106
8639
|
<button type="button" class="rp-adjourn" data-adjourn-from-chair>
|
|
8107
|
-
|
|
8640
|
+
${this.escape(this._t("rp_adjourn_room_file"))}
|
|
8108
8641
|
</button>
|
|
8109
8642
|
</div>
|
|
8110
8643
|
`;
|
|
@@ -8176,7 +8709,7 @@
|
|
|
8176
8709
|
if (isStreaming) {
|
|
8177
8710
|
return `
|
|
8178
8711
|
<div class="round-end-card pending" data-round-end-card="${this.escape(messageId)}">
|
|
8179
|
-
<div class="kp-eyebrow kp-eyebrow-pending"
|
|
8712
|
+
<div class="kp-eyebrow kp-eyebrow-pending">${this.escape(this._t("kp_eyebrow_drafting"))}</div>
|
|
8180
8713
|
<div class="kp-list">
|
|
8181
8714
|
<div class="kp-row kp-skeleton" aria-hidden="true">
|
|
8182
8715
|
<div class="kp-skeleton-bar"></div>
|
|
@@ -8193,7 +8726,7 @@
|
|
|
8193
8726
|
</div>
|
|
8194
8727
|
<div class="kp-ctas-pending">
|
|
8195
8728
|
<span class="kp-pending-dot"></span>
|
|
8196
|
-
<span class="kp-pending-text"
|
|
8729
|
+
<span class="kp-pending-text">${this.escape(this._t("kp_pending_drafting"))}</span>
|
|
8197
8730
|
</div>
|
|
8198
8731
|
</div>
|
|
8199
8732
|
`;
|
|
@@ -8203,14 +8736,14 @@
|
|
|
8203
8736
|
const ctasDegraded = awaiting
|
|
8204
8737
|
? `
|
|
8205
8738
|
<div class="kp-ctas">
|
|
8206
|
-
<button type="button" class="kp-cta primary" data-continue
|
|
8207
|
-
<button type="button" class="kp-cta ghost" data-adjourn-from-chair
|
|
8739
|
+
<button type="button" class="kp-cta primary" data-continue>${this.escape(this._t("kp_btn_continue_next"))}</button>
|
|
8740
|
+
<button type="button" class="kp-cta ghost" data-adjourn-from-chair>${this.escape(this._t("kp_btn_adjourn_brief"))}</button>
|
|
8208
8741
|
</div>
|
|
8209
8742
|
`
|
|
8210
|
-
: `<div class="kp-ctas-spent"
|
|
8743
|
+
: `<div class="kp-ctas-spent">${this.escape(this._t("kp_ctas_spent"))}</div>`;
|
|
8211
8744
|
return `
|
|
8212
8745
|
<div class="round-end-card" data-round-end-card="${this.escape(messageId)}">
|
|
8213
|
-
<div class="kp-eyebrow kp-eyebrow-degraded"
|
|
8746
|
+
<div class="kp-eyebrow kp-eyebrow-degraded">${this.escape(this._t("kp_eyebrow_degraded"))}</div>
|
|
8214
8747
|
${ctasDegraded}
|
|
8215
8748
|
</div>
|
|
8216
8749
|
`;
|
|
@@ -8219,11 +8752,11 @@
|
|
|
8219
8752
|
<div class="kp-row" data-kp-id="${this.escape(p.id)}">
|
|
8220
8753
|
<div class="kp-body">${this.escape(p.body)}</div>
|
|
8221
8754
|
<div class="kp-actions">
|
|
8222
|
-
<button type="button" class="kp-vote up ${p.vote === "up" ? "active" : ""}" data-kp-vote="up" data-kp-id="${this.escape(p.id)}" aria-label="
|
|
8223
|
-
<span>▲</span><span
|
|
8755
|
+
<button type="button" class="kp-vote up ${p.vote === "up" ? "active" : ""}" data-kp-vote="up" data-kp-id="${this.escape(p.id)}" aria-label="${this.escape(this._t("kp_vote_aria_up"))}">
|
|
8756
|
+
<span>▲</span><span>${this.escape(this._t("kp_vote_more"))}</span>
|
|
8224
8757
|
</button>
|
|
8225
|
-
<button type="button" class="kp-vote down ${p.vote === "down" ? "active" : ""}" data-kp-vote="down" data-kp-id="${this.escape(p.id)}" aria-label="
|
|
8226
|
-
<span>▼</span><span
|
|
8758
|
+
<button type="button" class="kp-vote down ${p.vote === "down" ? "active" : ""}" data-kp-vote="down" data-kp-id="${this.escape(p.id)}" aria-label="${this.escape(this._t("kp_vote_aria_down"))}">
|
|
8759
|
+
<span>▼</span><span>${this.escape(this._t("kp_vote_drop"))}</span>
|
|
8227
8760
|
</button>
|
|
8228
8761
|
</div>
|
|
8229
8762
|
</div>
|
|
@@ -8237,14 +8770,16 @@
|
|
|
8237
8770
|
const shiftCallout = (shift && awaiting)
|
|
8238
8771
|
? `
|
|
8239
8772
|
<div class="kp-mode-shift">
|
|
8240
|
-
<div class="kp-shift-eyebrow"
|
|
8773
|
+
<div class="kp-shift-eyebrow">${this.escape(this._t("kp_shift_eyebrow_prefix"))}<strong>${this.escape(shift.to)}</strong></div>
|
|
8241
8774
|
<div class="kp-shift-because">${this.escape(shift.because)}</div>
|
|
8242
8775
|
</div>
|
|
8243
8776
|
`
|
|
8244
8777
|
: "";
|
|
8245
8778
|
let ctas;
|
|
8779
|
+
const modeRaw = (this.currentRoom?.mode || "").toLowerCase();
|
|
8780
|
+
const modeKeep = modeRaw || this._t("kp_mode_current_fallback");
|
|
8246
8781
|
if (!awaiting) {
|
|
8247
|
-
ctas = `<div class="kp-ctas-spent"
|
|
8782
|
+
ctas = `<div class="kp-ctas-spent">${this.escape(this._t("kp_ctas_spent"))}</div>`;
|
|
8248
8783
|
} else if (shift) {
|
|
8249
8784
|
// 3-button layout · primary takes ~50% of the row, the two
|
|
8250
8785
|
// secondaries split the rest. Without `kp-ctas-shift`, three
|
|
@@ -8252,25 +8787,24 @@
|
|
|
8252
8787
|
// longer "switch to constructive" label wraps to two lines.
|
|
8253
8788
|
// Labels stripped of "& continue" — the action is implicit
|
|
8254
8789
|
// in this round-end context.
|
|
8255
|
-
const currentMode = (this.currentRoom?.mode || "").toLowerCase();
|
|
8256
8790
|
ctas = `
|
|
8257
8791
|
<div class="kp-ctas kp-ctas-shift">
|
|
8258
|
-
<button type="button" class="kp-cta primary" data-shift-accept data-shift-to="${this.escape(shift.to)}"
|
|
8259
|
-
<button type="button" class="kp-cta ghost" data-continue
|
|
8260
|
-
<button type="button" class="kp-cta ghost" data-adjourn-from-chair
|
|
8792
|
+
<button type="button" class="kp-cta primary" data-shift-accept data-shift-to="${this.escape(shift.to)}">${this.escape(this._t("kp_switch_to", { mode: shift.to }))}</button>
|
|
8793
|
+
<button type="button" class="kp-cta ghost" data-continue>${this.escape(this._t("kp_keep_mode", { mode: modeKeep }))}</button>
|
|
8794
|
+
<button type="button" class="kp-cta ghost" data-adjourn-from-chair>${this.escape(this._t("kp_btn_adjourn"))}</button>
|
|
8261
8795
|
</div>
|
|
8262
8796
|
`;
|
|
8263
8797
|
} else {
|
|
8264
8798
|
ctas = `
|
|
8265
8799
|
<div class="kp-ctas">
|
|
8266
|
-
<button type="button" class="kp-cta primary" data-continue
|
|
8267
|
-
<button type="button" class="kp-cta ghost" data-adjourn-from-chair
|
|
8800
|
+
<button type="button" class="kp-cta primary" data-continue>${this.escape(this._t("kp_btn_continue_next"))}</button>
|
|
8801
|
+
<button type="button" class="kp-cta ghost" data-adjourn-from-chair>${this.escape(this._t("kp_btn_adjourn_brief"))}</button>
|
|
8268
8802
|
</div>
|
|
8269
8803
|
`;
|
|
8270
8804
|
}
|
|
8271
8805
|
return `
|
|
8272
8806
|
<div class="round-end-card" data-round-end-card="${this.escape(messageId)}">
|
|
8273
|
-
<div class="kp-eyebrow"
|
|
8807
|
+
<div class="kp-eyebrow">${this.escape(this._t("kp_eyebrow_vote"))}</div>
|
|
8274
8808
|
<div class="kp-list">${items}</div>
|
|
8275
8809
|
${shiftCallout}
|
|
8276
8810
|
${ctas}
|
|
@@ -8308,14 +8842,14 @@
|
|
|
8308
8842
|
const truncated = parentSubject.length > 70
|
|
8309
8843
|
? parentSubject.slice(0, 70) + "…"
|
|
8310
8844
|
: parentSubject;
|
|
8311
|
-
|
|
8312
|
-
const
|
|
8313
|
-
const
|
|
8845
|
+
const label = this._t("convene_followup_label");
|
|
8846
|
+
const roomTag = this._t("convene_room_label");
|
|
8847
|
+
const parentTitle = this._t("convene_parent_session_title", { subject: parentSubject || parentId });
|
|
8314
8848
|
const subjectChunk = truncated
|
|
8315
8849
|
? `<span class="convene-origin-sep">·</span><span class="convene-origin-subject">${this.escape(truncated)}</span>`
|
|
8316
8850
|
: "";
|
|
8317
8851
|
originHtml = `
|
|
8318
|
-
<a class="convene-origin" href="#/r/${this.escape(parentId)}" data-parent-room-id="${this.escape(parentId)}" title="${this.escape(
|
|
8852
|
+
<a class="convene-origin" href="#/r/${this.escape(parentId)}" data-parent-room-id="${this.escape(parentId)}" title="${this.escape(parentTitle)}">
|
|
8319
8853
|
<span class="convene-origin-arrow">↩</span>
|
|
8320
8854
|
<span class="convene-origin-label">${this.escape(label)}</span>
|
|
8321
8855
|
<span class="convene-origin-room">${this.escape(roomTag)}${this.escape(String(parentNum))}</span>
|
|
@@ -8328,24 +8862,21 @@
|
|
|
8328
8862
|
parentId ? "convene-opener-followup" : "",
|
|
8329
8863
|
isLongOpener ? "convene-opener-clamped" : "",
|
|
8330
8864
|
].filter(Boolean).join(" ");
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
// English regardless of the user's query language.
|
|
8334
|
-
const moreLabel = "Show more ↓";
|
|
8335
|
-
const lessLabel = "Show less ↑";
|
|
8865
|
+
const moreLabel = this._t("convene_show_more");
|
|
8866
|
+
const lessLabel = this._t("convene_show_less");
|
|
8336
8867
|
const toggleHtml = isLongOpener
|
|
8337
8868
|
? `<button type="button" class="convene-toggle" data-convene-toggle data-more="${this.escape(moreLabel)}" data-less="${this.escape(lessLabel)}">${moreLabel}</button>`
|
|
8338
8869
|
: "";
|
|
8339
8870
|
return `
|
|
8340
8871
|
<article class="${articleCls}" data-message-id="${this.escape(m.id)}">
|
|
8341
|
-
<div class="convene-eyebrow"
|
|
8872
|
+
<div class="convene-eyebrow">${this.escape(this._t("convene_eyebrow"))}</div>
|
|
8342
8873
|
${originHtml}
|
|
8343
8874
|
<h2 class="convene-body">${this.renderBody(m.body)}</h2>
|
|
8344
8875
|
${toggleHtml}
|
|
8345
8876
|
<div class="convene-meta">
|
|
8346
8877
|
<span class="convene-by">${who}</span>
|
|
8347
8878
|
<span class="convene-time">· ${this.timeFmt(m.createdAt)}</span>
|
|
8348
|
-
<span class="convene-cast">·
|
|
8879
|
+
<span class="convene-cast">· ${this.escape(this._t("convene_meta_to"))} ${this.currentMembers.map((a) => this.escape(a.handle)).join(" ")}</span>
|
|
8349
8880
|
</div>
|
|
8350
8881
|
</article>
|
|
8351
8882
|
`;
|
|
@@ -8373,9 +8904,10 @@
|
|
|
8373
8904
|
const status = meta.status === "done" || meta.status === "failed" ? meta.status : "running";
|
|
8374
8905
|
const tool = (meta.tool || "tool").toString();
|
|
8375
8906
|
const target = meta.target ? String(meta.target) : "";
|
|
8376
|
-
const
|
|
8377
|
-
?
|
|
8907
|
+
const elapsedSecs = typeof meta.elapsedMs === "number" && status !== "running"
|
|
8908
|
+
? this._t("msg_ws_secs", { n: (meta.elapsedMs / 1000).toFixed(1) })
|
|
8378
8909
|
: "";
|
|
8910
|
+
const elapsedTail = elapsedSecs ? ` · ${elapsedSecs}` : "";
|
|
8379
8911
|
const sources = Array.isArray(meta.sources) ? meta.sources : [];
|
|
8380
8912
|
|
|
8381
8913
|
// web-search renders as a full-width card mirroring `.brief-card`
|
|
@@ -8386,10 +8918,16 @@
|
|
|
8386
8918
|
if (tool === "web-search") {
|
|
8387
8919
|
const hasSources = status === "done" && sources.length > 0;
|
|
8388
8920
|
let stamp;
|
|
8389
|
-
if (status === "running") stamp = "
|
|
8390
|
-
else if (status === "done")
|
|
8391
|
-
stamp =
|
|
8392
|
-
|
|
8921
|
+
if (status === "running") stamp = this._t("msg_ws_searching");
|
|
8922
|
+
else if (status === "done") {
|
|
8923
|
+
stamp = sources.length === 1
|
|
8924
|
+
? this._t("msg_ws_done_one", { tail: elapsedTail })
|
|
8925
|
+
: this._t("msg_ws_done", { n: sources.length, tail: elapsedTail });
|
|
8926
|
+
} else {
|
|
8927
|
+
stamp = elapsedSecs
|
|
8928
|
+
? this._t("msg_ws_failed_elapsed", { elapsed: elapsedSecs })
|
|
8929
|
+
: this._t("msg_ws_failed");
|
|
8930
|
+
}
|
|
8393
8931
|
|
|
8394
8932
|
const mark = status === "running"
|
|
8395
8933
|
? `<span class="msg-tool-pulse"></span>`
|
|
@@ -8417,22 +8955,22 @@
|
|
|
8417
8955
|
`;
|
|
8418
8956
|
}).join("")}
|
|
8419
8957
|
</ol>
|
|
8420
|
-
<button type="button" class="msg-tool-sources-expand" data-msg-ws-toggle data-message-id="${this.escape(m.id)}" aria-label="
|
|
8958
|
+
<button type="button" class="msg-tool-sources-expand" data-msg-ws-toggle data-message-id="${this.escape(m.id)}" aria-label="${this.escape(this._t("msg_ws_toggle"))}">
|
|
8421
8959
|
<span class="msg-tool-sources-expand-icon" aria-hidden="true">▾</span>
|
|
8422
|
-
<span class="msg-tool-sources-expand-show"
|
|
8423
|
-
<span class="msg-tool-sources-expand-hide"
|
|
8960
|
+
<span class="msg-tool-sources-expand-show">${this.escape(this._t("msg_ws_expand_show", { n: sources.length }))}</span>
|
|
8961
|
+
<span class="msg-tool-sources-expand-hide">${this.escape(this._t("msg_ws_expand_hide"))}</span>
|
|
8424
8962
|
</button>`
|
|
8425
8963
|
: "";
|
|
8426
8964
|
|
|
8427
8965
|
const bodyToggleAttrs = hasSources
|
|
8428
|
-
? ` data-msg-ws-toggle data-message-id="${this.escape(m.id)}" role="button" tabindex="0" aria-label="
|
|
8966
|
+
? ` data-msg-ws-toggle data-message-id="${this.escape(m.id)}" role="button" tabindex="0" aria-label="${this.escape(this._t("msg_ws_toggle"))}"`
|
|
8429
8967
|
: "";
|
|
8430
8968
|
const caret = hasSources ? `<span class="msg-tool-caret" aria-hidden="true">▸</span>` : "";
|
|
8431
8969
|
|
|
8432
8970
|
return `
|
|
8433
8971
|
<div class="msg-tool-card status-${this.escape(status)}" data-message-id="${this.escape(m.id)}">
|
|
8434
8972
|
<div class="msg-tool-banner">
|
|
8435
|
-
<span class="msg-tool-banner-tag"
|
|
8973
|
+
<span class="msg-tool-banner-tag">${this.escape(this._t("msg_ws_banner"))}</span>
|
|
8436
8974
|
<span class="msg-tool-banner-stamp">${this.escape(stamp)}</span>
|
|
8437
8975
|
</div>
|
|
8438
8976
|
<div class="msg-tool-card-body${hasSources ? " is-toggle" : ""}"${bodyToggleAttrs}>
|
|
@@ -8458,7 +8996,7 @@
|
|
|
8458
8996
|
<span class="msg-tool-name">${this.escape(tool)}</span>
|
|
8459
8997
|
<span class="msg-tool-sep">·</span>
|
|
8460
8998
|
<span class="msg-tool-body">${this.escape(m.body || "")}</span>
|
|
8461
|
-
${
|
|
8999
|
+
${elapsedSecs ? `<span class="msg-tool-elapsed">${this.escape(elapsedSecs)}</span>` : ""}
|
|
8462
9000
|
${isUrlTarget ? `<a href="${this.escape(target)}" target="_blank" rel="noopener noreferrer" class="msg-tool-link" title="${this.escape(target)}">↗</a>` : ""}
|
|
8463
9001
|
</div>
|
|
8464
9002
|
</div>
|
|
@@ -8481,10 +9019,10 @@
|
|
|
8481
9019
|
return `
|
|
8482
9020
|
<div class="chair-direct${streaming ? " is-streaming" : ""}" data-message-id="${this.escape(m.id)}">
|
|
8483
9021
|
<div class="cd-rule" aria-hidden="true"></div>
|
|
8484
|
-
<div class="cd-kicker"
|
|
9022
|
+
<div class="cd-kicker">${this.escape(this._t("msg_chair_direct_kicker"))}</div>
|
|
8485
9023
|
<div class="cd-body">${bodyHtml}</div>
|
|
8486
9024
|
<div class="cd-meta">
|
|
8487
|
-
<span class="cd-author">${this.escape(this.currentChair?.name || "
|
|
9025
|
+
<span class="cd-author">${this.escape(this.currentChair?.name || this._t("msg_chair_display_fallback"))}</span>
|
|
8488
9026
|
<span class="cd-time">· ${this.timeFmt(m.createdAt)}</span>
|
|
8489
9027
|
</div>
|
|
8490
9028
|
</div>
|
|
@@ -8504,7 +9042,7 @@
|
|
|
8504
9042
|
return `
|
|
8505
9043
|
<div class="chair-intervention" data-message-id="${this.escape(m.id)}">
|
|
8506
9044
|
<div class="ci-rule" aria-hidden="true"></div>
|
|
8507
|
-
<div class="ci-kicker"
|
|
9045
|
+
<div class="ci-kicker">${this.escape(this._t("msg_chair_intervention_kicker"))}</div>
|
|
8508
9046
|
<div class="ci-body">${this.renderBody(m.body)}</div>
|
|
8509
9047
|
</div>
|
|
8510
9048
|
`;
|
|
@@ -8519,7 +9057,7 @@
|
|
|
8519
9057
|
return `
|
|
8520
9058
|
<div class="chair-billing-notice" data-message-id="${this.escape(m.id)}">
|
|
8521
9059
|
<div class="cb-rule" aria-hidden="true"></div>
|
|
8522
|
-
<div class="cb-kicker"
|
|
9060
|
+
<div class="cb-kicker">${this.escape(this._t("msg_chair_billing_kicker"))}</div>
|
|
8523
9061
|
<div class="cb-body">${this.renderBody(m.body)}</div>
|
|
8524
9062
|
</div>
|
|
8525
9063
|
`;
|
|
@@ -8537,9 +9075,9 @@
|
|
|
8537
9075
|
<div class="round-open-card ${isOpening ? "is-opening" : "is-reactive"}" data-message-id="${this.escape(m.id)}">
|
|
8538
9076
|
<span class="ro-chip">
|
|
8539
9077
|
<span class="ro-mark">${isOpening ? "◆" : "◇"}</span>
|
|
8540
|
-
<span class="ro-round"
|
|
9078
|
+
<span class="ro-round">${this.escape(this._t("ro_round", { n: roundNum }))}</span>
|
|
8541
9079
|
<span class="ro-sep">·</span>
|
|
8542
|
-
<span class="ro-mode">${isOpening ? "
|
|
9080
|
+
<span class="ro-mode">${this.escape(isOpening ? this._t("ro_mode_parallel") : this._t("ro_mode_reactive"))}</span>
|
|
8543
9081
|
</span>
|
|
8544
9082
|
</div>
|
|
8545
9083
|
`;
|
|
@@ -8558,27 +9096,25 @@
|
|
|
8558
9096
|
// as a historical marker only.
|
|
8559
9097
|
if (isChair && metaKind === "no-brief") {
|
|
8560
9098
|
const ts = this.timeFmt(m.createdAt);
|
|
8561
|
-
// System UI · always English (no-brief card chrome).
|
|
8562
|
-
const
|
|
9099
|
+
// System UI · always English (no-brief card chrome). const hasBrief = !!this.currentBrief;
|
|
9100
|
+
const chairName = (this.prefs?.name || "").trim() || this._t("nb_chair_fallback");
|
|
8563
9101
|
const cta = hasBrief
|
|
8564
9102
|
? ""
|
|
8565
9103
|
: `
|
|
8566
9104
|
<div class="nb-actions">
|
|
8567
9105
|
<button type="button" class="nb-cta" data-generate-brief>
|
|
8568
9106
|
<span class="nb-cta-mark">▸</span>
|
|
8569
|
-
<span class="nb-cta-text"
|
|
8570
|
-
</button>
|
|
9107
|
+
<span class="nb-cta-text">${this.escape(this._t("nb_cta"))}</span> </button>
|
|
8571
9108
|
</div>
|
|
8572
9109
|
`;
|
|
8573
9110
|
return `
|
|
8574
9111
|
<div class="no-brief-card" data-message-id="${this.escape(m.id)}">
|
|
8575
9112
|
<span class="nb-chip">
|
|
8576
9113
|
<span class="nb-mark">⊘</span>
|
|
8577
|
-
<span class="nb-eyebrow"
|
|
9114
|
+
<span class="nb-eyebrow">${this.escape(this._t("nb_eyebrow"))}</span>
|
|
8578
9115
|
</span>
|
|
8579
9116
|
<div class="nb-body">
|
|
8580
|
-
<strong>${this.escape(this.
|
|
8581
|
-
</div>
|
|
9117
|
+
<strong>${this.escape(chairName)}</strong> ${this.escape(this._t("nb_body"))} </div>
|
|
8582
9118
|
${cta}
|
|
8583
9119
|
<div class="nb-meta">${this.escape(ts)}</div>
|
|
8584
9120
|
</div>
|
|
@@ -8592,9 +9128,9 @@
|
|
|
8592
9128
|
: (author ? this.escape(author.id) : "agent");
|
|
8593
9129
|
const name = isUser ? (this.prefs?.name || "You") : (author?.name || "Agent");
|
|
8594
9130
|
const tag = isUser
|
|
8595
|
-
? "
|
|
9131
|
+
? this._t("msg_tag_you")
|
|
8596
9132
|
: isChair
|
|
8597
|
-
? "
|
|
9133
|
+
? this._t("msg_tag_chair")
|
|
8598
9134
|
: (author ? `// ${author.roleTag}` : "");
|
|
8599
9135
|
// Model badge · only shown for non-user messages. Falls back to
|
|
8600
9136
|
// the raw modelV string if the registry lookup misses (e.g. a
|
|
@@ -8648,7 +9184,7 @@
|
|
|
8648
9184
|
// is procedural; ctx count would be misleading).
|
|
8649
9185
|
const ctxCount = !isUser && !isChair ? this.contextCountAt(m.id) : 0;
|
|
8650
9186
|
const ctxBadge = ctxCount > 0
|
|
8651
|
-
? `<span class="msg-context" title="${
|
|
9187
|
+
? `<span class="msg-context" title="${this.escape(this._t("msg_ctx_title", { n: ctxCount, name }))}">${this.escape(this._t("msg_ctx_inline", { n: ctxCount }))}</span>`
|
|
8652
9188
|
: "";
|
|
8653
9189
|
|
|
8654
9190
|
// Skills badge · the orchestrator's Pass-1 router stamps which
|
|
@@ -8658,7 +9194,7 @@
|
|
|
8658
9194
|
const skillsUsed = (m.meta && Array.isArray(m.meta.skillsUsed)) ? m.meta.skillsUsed : [];
|
|
8659
9195
|
const skillsReason = (m.meta && typeof m.meta.skillsReason === "string") ? m.meta.skillsReason : "";
|
|
8660
9196
|
const skillsBadge = skillsUsed.length > 0
|
|
8661
|
-
? `<span class="msg-skills" title="${this.escape(skillsReason || ("
|
|
9197
|
+
? `<span class="msg-skills" title="${this.escape(skillsReason || this._t("msg_skills_used", { list: skillsUsed.join(", ") }))}">🛠 ${skillsUsed.map((s) => this.escape(s)).join(", ")}</span>`
|
|
8662
9198
|
: "";
|
|
8663
9199
|
|
|
8664
9200
|
// Web-search badge · meta is set by the orchestrator when the
|
|
@@ -8669,11 +9205,18 @@
|
|
|
8669
9205
|
const webSearchQuery = (m.meta && typeof m.meta.webSearchQuery === "string") ? m.meta.webSearchQuery : "";
|
|
8670
9206
|
const webSearchSources = (m.meta && Array.isArray(m.meta.webSearchSources)) ? m.meta.webSearchSources : [];
|
|
8671
9207
|
const webSearchBadge = webSearchUsed
|
|
8672
|
-
?
|
|
9208
|
+
? (() => {
|
|
9209
|
+
const n = webSearchSources.length;
|
|
9210
|
+
const title = this.escape(this._t("msg_ws_title", { query: webSearchQuery, n }));
|
|
9211
|
+
const label = n === 1
|
|
9212
|
+
? this.escape(this._t("msg_ws_btn_one"))
|
|
9213
|
+
: this.escape(this._t("msg_ws_btn", { n }));
|
|
9214
|
+
return `<button type="button" class="msg-web-search" data-msg-ws-toggle data-message-id="${this.escape(m.id)}" title="${title}">🔍 ${label}</button>`;
|
|
9215
|
+
})()
|
|
8673
9216
|
: "";
|
|
8674
9217
|
const webSearchSourcesPanel = webSearchUsed && webSearchSources.length > 0
|
|
8675
9218
|
? `<div class="msg-web-search-sources" data-msg-ws-sources data-message-id="${this.escape(m.id)}" hidden>
|
|
8676
|
-
<div class="msg-web-search-query"><span class="msg-web-search-query-label"
|
|
9219
|
+
<div class="msg-web-search-query"><span class="msg-web-search-query-label">${this.escape(this._t("msg_ws_query_label"))}</span><span class="msg-web-search-query-text">${this.escape(webSearchQuery)}</span></div>
|
|
8677
9220
|
<ol class="msg-web-search-list">
|
|
8678
9221
|
${webSearchSources.map((s, i) => `
|
|
8679
9222
|
<li>
|
|
@@ -8720,7 +9263,7 @@
|
|
|
8720
9263
|
? m.meta.chairPick.rationale.trim()
|
|
8721
9264
|
: "";
|
|
8722
9265
|
const chairPickKicker = (chairPick && !isUser && !isChair)
|
|
8723
|
-
? `<div class="msg-chair-pick" title="
|
|
9266
|
+
? `<div class="msg-chair-pick" title="${this.escape(this._t("room_chair_pick_title", { name: this.escape(name) }))}">${this.escape(this._t("msg_chair_pick", { body: chairPick }))}</div>`
|
|
8724
9267
|
: "";
|
|
8725
9268
|
|
|
8726
9269
|
// data-author-id is only attached for director messages (not user
|
|
@@ -8736,7 +9279,7 @@
|
|
|
8736
9279
|
${chairPickKicker}
|
|
8737
9280
|
<div class="msg-meta">
|
|
8738
9281
|
<span class="msg-name">${this.escape(name)}</span>
|
|
8739
|
-
${modelLabel ? `<span class="msg-model" title="
|
|
9282
|
+
${modelLabel ? `<span class="msg-model" title="${this.escape(this._t("msg_model_title", { label: modelLabel }))}">${this.escape(modelLabel)}</span>` : ""}
|
|
8740
9283
|
<span class="msg-tag">${tag}</span>
|
|
8741
9284
|
${skillsBadge}
|
|
8742
9285
|
${webSearchBadge}
|
|
@@ -8789,9 +9332,9 @@
|
|
|
8789
9332
|
// Old queue strip used .qw-label; new in-chat card uses .rp-label.
|
|
8790
9333
|
const label = btn.querySelector(".rp-label, .qw-label");
|
|
8791
9334
|
if (label) {
|
|
8792
|
-
if (this.currentRoom?.awaitingContinue) label.textContent = "
|
|
8793
|
-
else if (this.currentRoom?.awaitingClarify) label.textContent = "
|
|
8794
|
-
else label.textContent = "
|
|
9335
|
+
if (this.currentRoom?.awaitingContinue) label.textContent = this._t("round_end_vote_above");
|
|
9336
|
+
else if (this.currentRoom?.awaitingClarify) label.textContent = this._t("round_clarifying");
|
|
9337
|
+
else label.textContent = this._t("round_end_open_vote");
|
|
8795
9338
|
}
|
|
8796
9339
|
},
|
|
8797
9340
|
|
|
@@ -8801,7 +9344,7 @@
|
|
|
8801
9344
|
const slot = document.querySelector("[data-queue-collapsed]");
|
|
8802
9345
|
if (!slot) return;
|
|
8803
9346
|
if (!items || items.length === 0) {
|
|
8804
|
-
slot.innerHTML = `<span class="sum-marker">·</span><span class="sum-state"
|
|
9347
|
+
slot.innerHTML = `<span class="sum-marker">·</span><span class="sum-state">${this.escape(this._t("q_idle"))}</span>`;
|
|
8805
9348
|
return;
|
|
8806
9349
|
}
|
|
8807
9350
|
const head = items[0];
|
|
@@ -8810,14 +9353,14 @@
|
|
|
8810
9353
|
const speaking = head.status === "speaking";
|
|
8811
9354
|
const pending = head.status === "pending";
|
|
8812
9355
|
const stateLabel = speaking
|
|
8813
|
-
? "
|
|
9356
|
+
? this._t("q_speaking")
|
|
8814
9357
|
: pending
|
|
8815
9358
|
? (this.currentRoom?.awaitingContinue
|
|
8816
|
-
? "
|
|
8817
|
-
: "
|
|
8818
|
-
: "
|
|
9359
|
+
? this._t("q_pending_vote")
|
|
9360
|
+
: this._t("q_pending_chair"))
|
|
9361
|
+
: this._t("q_queued");
|
|
8819
9362
|
const next = items[1] ? this.agentsById[items[1].agentId] : null;
|
|
8820
|
-
const rest = items.length > 2 ?
|
|
9363
|
+
const rest = items.length > 2 ? this._t("q_more_queued", { n: items.length - 2 }) : "";
|
|
8821
9364
|
|
|
8822
9365
|
const parts = [];
|
|
8823
9366
|
parts.push(`<span class="sum-marker">${speaking ? "▶" : pending ? "◇" : "·"}</span>`);
|
|
@@ -8904,9 +9447,9 @@
|
|
|
8904
9447
|
<li class="queue-row user-queued" data-user-queued>
|
|
8905
9448
|
<span class="pos">↪</span>
|
|
8906
9449
|
<span class="who">${this.escape(userName)}</span>
|
|
8907
|
-
<span class="state"
|
|
9450
|
+
<span class="state">${this._t("q_user_queued", { preview: this.escape(preview) })}</span>
|
|
8908
9451
|
<span class="actions">
|
|
8909
|
-
<button type="button" class="user-queued-cancel" data-cancel-user-queued title="
|
|
9452
|
+
<button type="button" class="user-queued-cancel" data-cancel-user-queued title="${this.escape(this._t("q_cancel"))}">✕</button>
|
|
8910
9453
|
</span>
|
|
8911
9454
|
</li>
|
|
8912
9455
|
`;
|
|
@@ -8923,16 +9466,16 @@
|
|
|
8923
9466
|
const pos = speaking ? "▶" : (POS[i - (firstSpeaking ? 1 : 0)] || (i + 1));
|
|
8924
9467
|
let stateLabel;
|
|
8925
9468
|
if (speaking) {
|
|
8926
|
-
stateLabel =
|
|
9469
|
+
stateLabel = this._t("q_state_speaking", { n: ctxTotal });
|
|
8927
9470
|
} else if (pending) {
|
|
8928
9471
|
// Pending means "lined up, waiting on something off-queue".
|
|
8929
9472
|
// Phrasing matches the room's current pause kind so the
|
|
8930
9473
|
// user knows what they're waiting on.
|
|
8931
9474
|
stateLabel = this.currentRoom?.awaitingContinue
|
|
8932
|
-
? "
|
|
9475
|
+
? this._t("q_pending_your_vote")
|
|
8933
9476
|
: this.currentRoom?.awaitingClarify
|
|
8934
|
-
? "
|
|
8935
|
-
: "
|
|
9477
|
+
? this._t("q_pending_waits_chair")
|
|
9478
|
+
: this._t("q_pending_chair");
|
|
8936
9479
|
} else {
|
|
8937
9480
|
stateLabel = q.status;
|
|
8938
9481
|
}
|
|
@@ -8973,73 +9516,20 @@
|
|
|
8973
9516
|
.replace(/&[a-z]+;/gi, " "); // HTML entities
|
|
8974
9517
|
const cjk = stripped.match(/[一-鿿㐀-䶿豈--ゟ゠-ヿ]/g);
|
|
8975
9518
|
const cjkCount = cjk ? cjk.length : 0;
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
// debate) land in the middle; research / critique rooms shoulder
|
|
8991
|
-
// a denser shape (assumptions + scenarios + indicators + threats
|
|
8992
|
-
// to validity all naturally lengthen the body).
|
|
8993
|
-
const tone = (this.currentRoom?.mode || "constructive").toLowerCase();
|
|
8994
|
-
const bandKind = tone === "brainstorm"
|
|
8995
|
-
? "lean"
|
|
8996
|
-
: (tone === "research" || tone === "critique" ? "dense" : "standard");
|
|
8997
|
-
// Brainstorm `lean` was originally 600-1500 zh / 400-1000 en —
|
|
8998
|
-
// calibrated against "quick recap" briefs with 1-2 directors.
|
|
8999
|
-
// That undercounted real brainstorm rooms: 3-4 directors × 2-3
|
|
9000
|
-
// rounds × 2-3 ideas-per-turn easily produces 12-20 ideas, each
|
|
9001
|
-
// worth 80-150 words (concept + why-it-matters + what-it-opens).
|
|
9002
|
-
// 1500-2200 words / 2000-3000 字 is a healthy, idea-dense
|
|
9003
|
-
// brainstorm — bumping the band so that lands in `sweet`, not
|
|
9004
|
-
// `dense`. Standard / dense bands unchanged.
|
|
9005
|
-
const bands = isCjk
|
|
9006
|
-
? ({
|
|
9007
|
-
lean: { sweetLo: 1000, sweetHi: 2200, denseHi: 3800, longHi: 5500 },
|
|
9008
|
-
standard: { sweetLo: 1500, sweetHi: 2800, denseHi: 4500, longHi: 6500 },
|
|
9009
|
-
dense: { sweetLo: 2500, sweetHi: 4500, denseHi: 6500, longHi: 8000 },
|
|
9010
|
-
})[bandKind]
|
|
9011
|
-
: ({
|
|
9012
|
-
lean: { sweetLo: 800, sweetHi: 1600, denseHi: 2800, longHi: 4000 },
|
|
9013
|
-
standard: { sweetLo: 1000, sweetHi: 1800, denseHi: 3000, longHi: 4500 },
|
|
9014
|
-
dense: { sweetLo: 1800, sweetHi: 3000, denseHi: 4500, longHi: 5500 },
|
|
9015
|
-
})[bandKind];
|
|
9016
|
-
let tier;
|
|
9017
|
-
if (count < bands.sweetLo) tier = "thin";
|
|
9018
|
-
else if (count <= bands.sweetHi) tier = "sweet";
|
|
9019
|
-
else if (count <= bands.denseHi) tier = "dense";
|
|
9020
|
-
else if (count <= bands.longHi) tier = "long";
|
|
9021
|
-
else tier = "too-long";
|
|
9022
|
-
return { label, tier, count, isCjk, tone, bands };
|
|
9023
|
-
},
|
|
9024
|
-
|
|
9025
|
-
/** Tooltip explaining what the brief-card word-count chip's colour
|
|
9026
|
-
* means · tone-aware so the user understands why a 2,500-word
|
|
9027
|
-
* brainstorm recap reads "dense" while the same length in a
|
|
9028
|
-
* research note reads "sweet." */
|
|
9029
|
-
_briefWordCountTip(wc) {
|
|
9030
|
-
if (!wc) return "";
|
|
9031
|
-
// System UI · always English (word-count chip tooltip).
|
|
9032
|
-
const toneLabel = wc.tone;
|
|
9033
|
-
const range = `${wc.bands.sweetLo.toLocaleString("en-US")}-${wc.bands.sweetHi.toLocaleString("en-US")}`;
|
|
9034
|
-
const unit = "words";
|
|
9035
|
-
const copy = {
|
|
9036
|
-
"thin": `Lean · ${toneLabel} sweet zone is ${range} ${unit}`,
|
|
9037
|
-
"sweet": `Sweet zone for ${toneLabel} · most read-through-able length`,
|
|
9038
|
-
"dense": `Dense · past the ${toneLabel} sweet zone, still readable`,
|
|
9039
|
-
"long": `Long · approaching "filed instead of read"`,
|
|
9040
|
-
"too-long": `Too long · likely to be skimmed, not read`,
|
|
9041
|
-
};
|
|
9042
|
-
return copy[wc.tier] || "";
|
|
9519
|
+
// CJK-dominant docs: count chars (the "字数" the user reads off
|
|
9520
|
+
// a manuscript). The 30% threshold catches mixed zh-en docs
|
|
9521
|
+
// where the body is mostly Chinese with English brand names —
|
|
9522
|
+
// the natural unit there is still characters, not words.
|
|
9523
|
+
if (cjkCount >= stripped.length * 0.3 && cjkCount > 80) {
|
|
9524
|
+
const fmt = cjkCount.toLocaleString("en-US");
|
|
9525
|
+
return this._t("brief_wc_approx_chars", { n: fmt });
|
|
9526
|
+
}
|
|
9527
|
+
// English / Latin: whitespace-split, ignore empty tokens.
|
|
9528
|
+
const words = stripped.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
9529
|
+
const n = words.length;
|
|
9530
|
+
if (n === 0) return null;
|
|
9531
|
+
const fmt = n.toLocaleString("en-US");
|
|
9532
|
+
return n === 1 ? this._t("brief_wc_one_word") : this._t("brief_wc_words", { n: fmt });
|
|
9043
9533
|
},
|
|
9044
9534
|
|
|
9045
9535
|
/** Render the brief version tab strip · shared by both the error
|
|
@@ -9060,11 +9550,11 @@
|
|
|
9060
9550
|
const isInitial = i === 0;
|
|
9061
9551
|
const supp = bf.supplement && bf.supplement.trim()
|
|
9062
9552
|
? bf.supplement.trim()
|
|
9063
|
-
: (isInitial ? "
|
|
9553
|
+
: (isInitial ? this._t("brief_tab_initial") : "");
|
|
9064
9554
|
const tooltip = isInitial
|
|
9065
|
-
?
|
|
9066
|
-
:
|
|
9067
|
-
const closeTitle =
|
|
9555
|
+
? this._t("brief_tab_tooltip_initial")
|
|
9556
|
+
: `${this._t("brief_tab_supplement_prefix")}${supp || "—"}`;
|
|
9557
|
+
const closeTitle = this._t("brief_tab_delete_title");
|
|
9068
9558
|
// Errored / interrupted / timed-out tabs get a small
|
|
9069
9559
|
// visual marker so the user can spot which one needs
|
|
9070
9560
|
// attention without entering it. The full retry UI is
|
|
@@ -9078,7 +9568,7 @@
|
|
|
9078
9568
|
<span class="brief-version-num">${num}</span>
|
|
9079
9569
|
${stateMark}
|
|
9080
9570
|
${isInitial
|
|
9081
|
-
? `<span class="brief-version-label"
|
|
9571
|
+
? `<span class="brief-version-label">${this.escape(this._t("brief_tab_initial"))}</span>`
|
|
9082
9572
|
: `<span class="brief-version-label">${this.escape((supp || "").slice(0, 20))}${(supp || "").length > 20 ? "…" : ""}</span>`}
|
|
9083
9573
|
</button>
|
|
9084
9574
|
<button type="button" class="brief-version-close" data-brief-delete data-brief-id="${this.escape(bf.id)}" title="${this.escape(closeTitle)}" aria-label="${this.escape(closeTitle)}">×</button>
|
|
@@ -9151,14 +9641,13 @@
|
|
|
9151
9641
|
finally { this.currentBrief = prevCurrent; }
|
|
9152
9642
|
// Prepend the compact retry banner to the rendered card so
|
|
9153
9643
|
// the existing report stays fully visible below it.
|
|
9154
|
-
// System UI · always English (error banner chrome).
|
|
9155
9644
|
const detail = failed.timedOut
|
|
9156
|
-
? "
|
|
9645
|
+
? this._t("brief_regen_timeout")
|
|
9157
9646
|
: failed.interrupted
|
|
9158
|
-
? "
|
|
9159
|
-
: (failed.error || "
|
|
9160
|
-
const cta = "
|
|
9161
|
-
const dismiss = "
|
|
9647
|
+
? this._t("brief_regen_interrupted")
|
|
9648
|
+
: (failed.error || this._t("brief_regen_failed"));
|
|
9649
|
+
const cta = this._t("brief_retry");
|
|
9650
|
+
const dismiss = this._t("brief_dismiss");
|
|
9162
9651
|
card.insertAdjacentHTML("afterbegin", `
|
|
9163
9652
|
<div class="brief-retry-banner" data-brief-retry-banner data-failed-brief-id="${this.escape(failed.id)}">
|
|
9164
9653
|
<span class="brb-mark">⚠</span>
|
|
@@ -9172,40 +9661,34 @@
|
|
|
9172
9661
|
`);
|
|
9173
9662
|
return;
|
|
9174
9663
|
}
|
|
9175
|
-
// System UI · always English (full error-card copy).
|
|
9176
9664
|
const copy = b.timedOut
|
|
9177
9665
|
? {
|
|
9178
|
-
stamp: "
|
|
9179
|
-
kicker: "
|
|
9180
|
-
detail:
|
|
9666
|
+
stamp: this._t("brief_err_stamp_timeout"),
|
|
9667
|
+
kicker: this._t("brief_err_kicker_timeout"),
|
|
9668
|
+
detail: this._t("brief_err_detail_timeout"),
|
|
9181
9669
|
hint: "",
|
|
9182
|
-
cta: "
|
|
9670
|
+
cta: this._t("brief_retry"),
|
|
9183
9671
|
}
|
|
9184
9672
|
: b.interrupted
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9673
|
+
? {
|
|
9674
|
+
stamp: this._t("brief_err_stamp_interrupted"),
|
|
9675
|
+
kicker: this._t("brief_err_kicker_interrupted"),
|
|
9676
|
+
detail: this._t("brief_err_detail_interrupted"),
|
|
9677
|
+
hint: "",
|
|
9678
|
+
cta: this._t("brief_err_cta_regenerate"),
|
|
9679
|
+
}
|
|
9680
|
+
: {
|
|
9681
|
+
stamp: this._t("brief_err_stamp_failed"),
|
|
9682
|
+
kicker: this._t("brief_err_kicker_failed"),
|
|
9683
|
+
detail: this.escape(b.error || ""),
|
|
9684
|
+
hint: this._briefSecondaryHintHtml(b.error || ""),
|
|
9685
|
+
cta: this._t("brief_retry"),
|
|
9686
|
+
};
|
|
9199
9687
|
card.innerHTML = `
|
|
9200
9688
|
<div class="brief-card">
|
|
9201
9689
|
${tabsStripHtml}
|
|
9202
|
-
<div class="brief-banner">
|
|
9203
|
-
<span class="brief-banner-tag" style="color: var(--red);">// report</span>
|
|
9204
|
-
<span class="brief-banner-type" style="color: var(--red); border-color: var(--red);">${this.escape(this.briefModeLabel(b))}</span>
|
|
9205
|
-
<span class="brief-banner-stamp" style="color: var(--red);">${this.escape(copy.stamp)}</span>
|
|
9206
|
-
</div>
|
|
9207
9690
|
<div class="brief-body brief-body-error">
|
|
9208
|
-
<div class="brief-kicker" style="color: var(--red);">${this.escape(copy.kicker)}</div>
|
|
9691
|
+
<div class="brief-kicker" style="color: var(--red);">${this.escape(copy.kicker)} <span class="brief-kicker-sep" aria-hidden="true">·</span> ${this.escape(copy.stamp)} <span class="brief-kicker-sep" aria-hidden="true">·</span> <span class="brief-meta-type">${this.escape(this.briefModeLabel(b))}</span></div>
|
|
9209
9692
|
<div class="brief-meta-line" style="color: var(--text-soft); text-transform: none; letter-spacing: 0;">
|
|
9210
9693
|
${(b.interrupted || b.timedOut) ? this.escape(copy.detail) : copy.detail}
|
|
9211
9694
|
</div>
|
|
@@ -9230,8 +9713,10 @@
|
|
|
9230
9713
|
.join("");
|
|
9231
9714
|
|
|
9232
9715
|
const filedLabel = generating
|
|
9233
|
-
? "
|
|
9234
|
-
:
|
|
9716
|
+
? this._t("brief_filed_generating")
|
|
9717
|
+
: this._t("brief_filed_stamp", {
|
|
9718
|
+
when: b.createdAt ? this.timeFmt(b.createdAt) : this.timeFmt(Date.now()),
|
|
9719
|
+
});
|
|
9235
9720
|
|
|
9236
9721
|
// Open Report URL · routes to /magazine.html or /newspaper.html
|
|
9237
9722
|
// for the structured modes and /report.html for research-note
|
|
@@ -9250,30 +9735,39 @@
|
|
|
9250
9735
|
card.innerHTML = `
|
|
9251
9736
|
<header class="ending-block-head">
|
|
9252
9737
|
<span class="ending-block-line"></span>
|
|
9253
|
-
<span class="ending-block-label"
|
|
9738
|
+
<span class="ending-block-label">${this.escape(this._t("brief_output_head"))}</span>
|
|
9254
9739
|
<span class="ending-block-line"></span>
|
|
9255
9740
|
</header>
|
|
9256
9741
|
|
|
9257
9742
|
<div class="brief-card">
|
|
9258
9743
|
${tabsHtml}
|
|
9259
|
-
<div class="brief-banner">
|
|
9260
|
-
<span class="brief-banner-tag">// report</span>
|
|
9261
|
-
<span class="brief-banner-type">${this.escape(this.briefModeLabel(b))}</span>
|
|
9262
|
-
<span class="brief-banner-stamp">${filedLabel}</span>
|
|
9263
|
-
</div>
|
|
9264
9744
|
|
|
9265
9745
|
<div class="brief-body">
|
|
9266
9746
|
${generating
|
|
9267
9747
|
? `<div class="brief-info brief-info-generating">${this.renderBriefStages(b)}</div>`
|
|
9268
9748
|
: (() => {
|
|
9269
9749
|
const wc = this._briefWordCount(b);
|
|
9270
|
-
|
|
9750
|
+
// Tucked the mode chip into the meta-row alongside
|
|
9751
|
+
// authors / word count. The earlier `.brief-banner`
|
|
9752
|
+
// strip (kicker + chip + stamp) was retired — the
|
|
9753
|
+
// user found it ate too much vertical space for
|
|
9754
|
+
// info that's already implied by the surrounding
|
|
9755
|
+
// adjourned-room context.
|
|
9756
|
+
const filedAgo = b.createdAt ? this.relTime(b.createdAt) : "";
|
|
9757
|
+
const filedKicker = this._t("brief_filed_by", {
|
|
9758
|
+
name: this.currentChair?.name ? this.escape(this.currentChair.name) : this._t("brief_chair_fallback"),
|
|
9759
|
+
});
|
|
9760
|
+
const kickerLine = filedAgo
|
|
9761
|
+
? `${filedKicker} <span class="brief-kicker-sep" aria-hidden="true">·</span> ${this.escape(filedAgo)}`
|
|
9762
|
+
: filedKicker;
|
|
9271
9763
|
return `<div class="brief-info">
|
|
9272
|
-
<div class="brief-kicker"
|
|
9273
|
-
<h2 class="brief-title" data-brief-title>${this.escape(b.title ||
|
|
9764
|
+
<div class="brief-kicker">${kickerLine}</div>
|
|
9765
|
+
<h2 class="brief-title" data-brief-title>${this.escape(b.title || this._t("brief_untitled"))}</h2>
|
|
9274
9766
|
<div class="brief-meta-row">
|
|
9275
|
-
<span class="brief-meta-line">${this.currentMembers.length}
|
|
9276
|
-
${wc ? `<span class="brief-meta-sep" aria-hidden="true">·</span><span class="brief-meta-line brief-meta-words
|
|
9767
|
+
<span class="brief-meta-line">${this.escape(this._t("brief_meta_authors", { n: this.currentMembers.length }))}</span>
|
|
9768
|
+
${wc ? `<span class="brief-meta-sep" aria-hidden="true">·</span><span class="brief-meta-line brief-meta-words">${this.escape(wc)}</span>` : ""}
|
|
9769
|
+
<span class="brief-meta-sep" aria-hidden="true">·</span>
|
|
9770
|
+
<span class="brief-meta-type">${this.escape(this.briefModeLabel(b))}</span>
|
|
9277
9771
|
<div class="brief-signed">
|
|
9278
9772
|
<div class="brief-signed-avatars">${signed}</div>
|
|
9279
9773
|
</div>
|
|
@@ -9285,7 +9779,7 @@
|
|
|
9285
9779
|
${reportHref && !generating ? `
|
|
9286
9780
|
<a href="${reportHref}" class="brief-open" target="_blank" rel="noopener">
|
|
9287
9781
|
<span class="brief-open-icon">▸</span>
|
|
9288
|
-
<span class="brief-open-label"
|
|
9782
|
+
<span class="brief-open-label">${this.escape(this._t("brief_open_report"))}</span>
|
|
9289
9783
|
<span class="brief-open-arrow">→</span>
|
|
9290
9784
|
</a>
|
|
9291
9785
|
` : ""}
|
|
@@ -9295,19 +9789,18 @@
|
|
|
9295
9789
|
<div class="brief-supplement-row">
|
|
9296
9790
|
<button type="button" class="brief-supplement-btn" data-brief-supplement>
|
|
9297
9791
|
<span class="brief-supplement-mark">+</span>
|
|
9298
|
-
<span class="brief-supplement-label"
|
|
9792
|
+
<span class="brief-supplement-label">${this.escape(this._t("brief_supplement_btn"))}</span>
|
|
9299
9793
|
</button>
|
|
9300
|
-
<button type="button" class="brief-delete-btn" data-brief-delete data-brief-id="${this.escape(b.id)}" title="
|
|
9794
|
+
<button type="button" class="brief-delete-btn" data-brief-delete data-brief-id="${this.escape(b.id)}" title="${this.escape(this._t("brief_tab_delete_title"))}">
|
|
9301
9795
|
<span class="brief-delete-mark">⌫</span>
|
|
9302
|
-
<span class="brief-delete-label"
|
|
9303
|
-
</button>
|
|
9796
|
+
<span class="brief-delete-label">${this.escape(this._t("brief_delete_label"))}</span> </button>
|
|
9304
9797
|
</div>
|
|
9305
9798
|
` : ""}
|
|
9306
9799
|
</div>
|
|
9307
9800
|
|
|
9308
9801
|
<footer class="ending-block-foot">
|
|
9309
9802
|
<span class="ending-block-foot-line"></span>
|
|
9310
|
-
<span class="ending-block-foot-label"
|
|
9803
|
+
<span class="ending-block-foot-label">${this.escape(this._t("ending_session_foot"))}</span>
|
|
9311
9804
|
<span class="ending-block-foot-line"></span>
|
|
9312
9805
|
</footer>
|
|
9313
9806
|
`;
|
|
@@ -9425,6 +9918,67 @@
|
|
|
9425
9918
|
get zh() { return this.en; },
|
|
9426
9919
|
},
|
|
9427
9920
|
|
|
9921
|
+
renderBriefLlmTrace(b) {
|
|
9922
|
+
const logs = Array.isArray(b.llmLogs) ? b.llmLogs : [];
|
|
9923
|
+
const open = b.llmLogOpen === true;
|
|
9924
|
+
const running = logs.filter((l) => l.status === "running").length;
|
|
9925
|
+
const failed = logs.filter((l) => l.status === "failed").length;
|
|
9926
|
+
const label = open
|
|
9927
|
+
? (b.language === "zh" ? "收起模型流水" : "Hide model stream")
|
|
9928
|
+
: (b.language === "zh" ? "查看模型流水" : "View model stream");
|
|
9929
|
+
const countText = logs.length
|
|
9930
|
+
? `${logs.length} call${logs.length === 1 ? "" : "s"}${running ? ` · ${running} running` : ""}${failed ? ` · ${failed} failed` : ""}`
|
|
9931
|
+
: (b.language === "zh" ? "等待首个调用" : "waiting for first call");
|
|
9932
|
+
const button = `
|
|
9933
|
+
<button type="button" class="brief-llm-toggle" data-brief-llm-toggle data-brief-id="${this.escape(b.id || "")}" aria-expanded="${open ? "true" : "false"}">
|
|
9934
|
+
<span class="brief-llm-toggle-mark">${open ? "−" : "+"}</span>
|
|
9935
|
+
<span>${this.escape(label)}</span>
|
|
9936
|
+
<span class="brief-llm-toggle-meta">${this.escape(countText)}</span>
|
|
9937
|
+
</button>
|
|
9938
|
+
`;
|
|
9939
|
+
if (!open) return `<div class="brief-llm-trace">${button}</div>`;
|
|
9940
|
+
const fmt = (ms) => {
|
|
9941
|
+
if (!ms || ms < 0) return "";
|
|
9942
|
+
const s = Math.round(ms / 1000);
|
|
9943
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
9944
|
+
};
|
|
9945
|
+
const rows = logs.map((l) => {
|
|
9946
|
+
const elapsed = fmt((l.finishedAt || Date.now()) - (l.startedAt || Date.now()));
|
|
9947
|
+
const text = (l.text || "").trim();
|
|
9948
|
+
const preview = text
|
|
9949
|
+
? this.escape(text)
|
|
9950
|
+
: `<span class="brief-llm-empty">${this.escape(b.language === "zh" ? "等待模型输出…" : "waiting for output…")}</span>`;
|
|
9951
|
+
const meta = [
|
|
9952
|
+
l.modelV || "",
|
|
9953
|
+
elapsed,
|
|
9954
|
+
typeof l.totalTokens === "number" ? `${l.totalTokens} tok` : "",
|
|
9955
|
+
].filter(Boolean).join(" · ");
|
|
9956
|
+
return `
|
|
9957
|
+
<div class="brief-llm-row is-${this.escape(l.status || "running")}">
|
|
9958
|
+
<div class="brief-llm-row-head">
|
|
9959
|
+
<span class="brief-llm-row-title">${this.escape(l.label || l.stage || "LLM call")}</span>
|
|
9960
|
+
<span class="brief-llm-row-meta">${this.escape(meta)}</span>
|
|
9961
|
+
</div>
|
|
9962
|
+
${l.error ? `<div class="brief-llm-error">${this.escape(l.error)}</div>` : ""}
|
|
9963
|
+
<pre class="brief-llm-output">${preview}</pre>
|
|
9964
|
+
</div>
|
|
9965
|
+
`;
|
|
9966
|
+
}).join("");
|
|
9967
|
+
return `<div class="brief-llm-trace">${button}<div class="brief-llm-panel">${rows || `<div class="brief-llm-empty">${this.escape(countText)}</div>`}</div></div>`;
|
|
9968
|
+
},
|
|
9969
|
+
|
|
9970
|
+
_briefSubList(stageKey) {
|
|
9971
|
+
const sk = String(stageKey).replace(/-/g, "_");
|
|
9972
|
+
const list = [];
|
|
9973
|
+
for (let i = 0; i < 24; i++) {
|
|
9974
|
+
const k = `brief_sub_${sk}_${i}`;
|
|
9975
|
+
const s = this._t(k);
|
|
9976
|
+
if (s === k) break;
|
|
9977
|
+
list.push(s);
|
|
9978
|
+
}
|
|
9979
|
+
return list;
|
|
9980
|
+
},
|
|
9981
|
+
|
|
9428
9982
|
/** Set up a 1s tick that re-renders the brief stages while at least
|
|
9429
9983
|
* one stage is active OR the writing stage is still streaming.
|
|
9430
9984
|
* Idempotent — calling repeatedly is fine. */
|
|
@@ -9518,8 +10072,7 @@
|
|
|
9518
10072
|
// Hard ceiling · regardless of server state, flip the card to
|
|
9519
10073
|
// a timed-out error so the user always has a way out.
|
|
9520
10074
|
if (now - startedAt > this.BRIEF_HARD_TIMEOUT_MS) {
|
|
9521
|
-
|
|
9522
|
-
b.error = "Brief generation timed out (no completion after 8 minutes).";
|
|
10075
|
+
b.error = this._t("brief_hard_timeout_msg");
|
|
9523
10076
|
b.timedOut = true;
|
|
9524
10077
|
this.stopBriefStageTick();
|
|
9525
10078
|
this.stopBriefStallWatch();
|
|
@@ -9563,53 +10116,52 @@
|
|
|
9563
10116
|
"scaffold-actions": { status: "pending", detail: "", progress: null, startedAt: null },
|
|
9564
10117
|
write: { status: "pending", detail: "", progress: null, startedAt: null },
|
|
9565
10118
|
};
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
10119
|
+
// Mode-aware stage rail. The wire format emitted by emitStage()
|
|
10120
|
+
// in src/orchestrator/brief.ts depends on which pipeline ran:
|
|
10121
|
+
//
|
|
10122
|
+
// research-note (default) · extract → compose →
|
|
10123
|
+
// scaffold-{anchor,findings,cluster,actions} → write
|
|
10124
|
+
// (7 stages · the 4 scaffold sub-stages are driven by
|
|
10125
|
+
// JSON-key arrival in the streaming buffer)
|
|
10126
|
+
//
|
|
10127
|
+
// magazine / newspaper / ppt · extract → write
|
|
10128
|
+
// (2 stages · runBentoStage runs ONE chair-LLM call that
|
|
10129
|
+
// produces the BentoScaffold; composer + scaffold sub-
|
|
10130
|
+
// stages are skipped entirely. Each mode customises only
|
|
10131
|
+
// the write label so the user sees what's actually
|
|
10132
|
+
// being composed.)
|
|
10133
|
+
const isStructured = this.isStructuredBriefMode(b.mode);
|
|
10134
|
+
const STAGE_ORDER = isStructured
|
|
10135
|
+
? ["extract", "write"]
|
|
10136
|
+
: ["extract", "compose", "scaffold-anchor", "scaffold-findings", "scaffold-cluster", "scaffold-actions", "write"];
|
|
10137
|
+
// For structured modes the write key swaps to a per-mode label
|
|
10138
|
+
// (`brief_stage_magazine_write_label`, etc) so the user reads
|
|
10139
|
+
// "Composing the magazine" instead of the generic "Writing the
|
|
10140
|
+
// report" copy that suits research-note's chapter-by-chapter
|
|
10141
|
+
// pipeline.
|
|
10142
|
+
const STAGE_DEFS = STAGE_ORDER.map((key) => {
|
|
10143
|
+
if (isStructured && key === "write") {
|
|
10144
|
+
return {
|
|
10145
|
+
key,
|
|
10146
|
+
label: this._t(`brief_stage_${b.mode}_write_label`),
|
|
10147
|
+
pipShort: this._t(`brief_stage_${b.mode}_write_pip`),
|
|
10148
|
+
};
|
|
10149
|
+
}
|
|
10150
|
+
const sk = key.replace(/-/g, "_");
|
|
10151
|
+
return {
|
|
10152
|
+
key,
|
|
10153
|
+
label: this._t(`brief_stage_${sk}_label`),
|
|
10154
|
+
pipShort: this._t(`brief_stage_${sk}_pip`),
|
|
10155
|
+
};
|
|
10156
|
+
});
|
|
9569
10157
|
const wordCount = b.bodyMd
|
|
9570
10158
|
? (b.bodyMd.trim().match(/\S+/g) || []).length
|
|
9571
10159
|
: 0;
|
|
9572
10160
|
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
//
|
|
9577
|
-
// research-note (default) · extract → compose → scaffold-
|
|
9578
|
-
// {anchor,findings,cluster,actions} → write (7 stages · the
|
|
9579
|
-
// 4 scaffold sub-stages are driven by JSON-key arrival in
|
|
9580
|
-
// the streaming buffer, see SCAFFOLD_TRIGGERS in brief.ts)
|
|
9581
|
-
//
|
|
9582
|
-
// bento + magazine · extract → write (2 stages · runBentoStage
|
|
9583
|
-
// runs ONE chair-LLM call that produces the BentoScaffold;
|
|
9584
|
-
// composer and the scaffold sub-stages are skipped entirely.
|
|
9585
|
-
// Both modes share the same 2-stage rail · only the write
|
|
9586
|
-
// label and renderer differ)
|
|
9587
|
-
//
|
|
9588
|
-
// System UI · always English regardless of brief language. The
|
|
9589
|
-
// pipeline labels are the app's voice (chrome around the
|
|
9590
|
-
// generation), not the report content itself.
|
|
9591
|
-
const STRUCTURED_WRITE_LABELS = {
|
|
9592
|
-
magazine: "Composing the magazine",
|
|
9593
|
-
newspaper: "Composing the newspaper",
|
|
9594
|
-
ppt: "Composing the deck",
|
|
9595
|
-
};
|
|
9596
|
-
const STAGE_DEFS = this.isStructuredBriefMode(b.mode)
|
|
9597
|
-
? [
|
|
9598
|
-
{ key: "extract", label: "Reading what each director said", pipShort: "read" },
|
|
9599
|
-
{ key: "write", label: STRUCTURED_WRITE_LABELS[b.mode] || "Composing the report", pipShort: "compose" },
|
|
9600
|
-
]
|
|
9601
|
-
: [
|
|
9602
|
-
{ key: "extract", label: "Reading what each director said", pipShort: "read" },
|
|
9603
|
-
{ key: "compose", label: "Picking the report shape", pipShort: "pick" },
|
|
9604
|
-
{ key: "scaffold-anchor", label: "Setting the anchor", pipShort: "anchor" },
|
|
9605
|
-
{ key: "scaffold-findings", label: "Sketching findings", pipShort: "find" },
|
|
9606
|
-
{ key: "scaffold-cluster", label: "Mapping consensus + dissent", pipShort: "split" },
|
|
9607
|
-
{ key: "scaffold-actions", label: "Drafting actions + risks", pipShort: "act" },
|
|
9608
|
-
{ key: "write", label: "Writing the report", pipShort: "write" },
|
|
9609
|
-
];
|
|
9610
|
-
|
|
10161
|
+
const chairDisp = (b.chairName || this.currentChair?.name)
|
|
10162
|
+
? this.escape(b.chairName || this.currentChair.name)
|
|
10163
|
+
: this._t("brief_chair_fallback");
|
|
9611
10164
|
const meta = this.BRIEF_STAGE_META;
|
|
9612
|
-
const substages = (this.BRIEF_SUBSTAGES[lang] || this.BRIEF_SUBSTAGES.en);
|
|
9613
10165
|
const stageEta = (key) => {
|
|
9614
10166
|
const st = stages[key];
|
|
9615
10167
|
if (st?.etaSec && typeof st.etaSec.lo === "number") return [st.etaSec.lo, st.etaSec.hi];
|
|
@@ -9664,14 +10216,9 @@
|
|
|
9664
10216
|
// Falls through to the regular stage key for every other
|
|
9665
10217
|
// stage / mode.
|
|
9666
10218
|
let substageText = "";
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
}
|
|
9671
|
-
if (activeStatus === "active" && substages[subKey]?.length) {
|
|
9672
|
-
const list = substages[subKey];
|
|
9673
|
-
substageText = list[Math.floor(activeElapsed / 3) % list.length];
|
|
9674
|
-
}
|
|
10219
|
+
if (activeStatus === "active") {
|
|
10220
|
+
const list = this._briefSubList(activeDef.key);
|
|
10221
|
+
if (list.length) substageText = list[Math.floor(activeElapsed / 3) % list.length]; }
|
|
9675
10222
|
|
|
9676
10223
|
// Detail · cur/total directors during extract, word count during
|
|
9677
10224
|
// write, otherwise the server-supplied detail string.
|
|
@@ -9679,13 +10226,11 @@
|
|
|
9679
10226
|
if (activeDef.key === "extract" && activeStage.progress?.total) {
|
|
9680
10227
|
const cur = activeStage.progress.current;
|
|
9681
10228
|
const tot = activeStage.progress.total;
|
|
9682
|
-
detailParts.push(
|
|
9683
|
-
} else if (activeStage.detail) {
|
|
10229
|
+
detailParts.push(this._t(tot === 1 ? "brief_prog_directors_one" : "brief_prog_directors", { cur, tot })); } else if (activeStage.detail) {
|
|
9684
10230
|
detailParts.push(activeStage.detail);
|
|
9685
10231
|
}
|
|
9686
10232
|
if (activeDef.key === "write" && activeStatus === "active" && wordCount > 0) {
|
|
9687
|
-
detailParts.push(
|
|
9688
|
-
}
|
|
10233
|
+
detailParts.push(this._t(wordCount === 1 ? "brief_prog_words_one" : "brief_prog_words", { n: wordCount })); }
|
|
9689
10234
|
const detailLine = detailParts.join(" · ");
|
|
9690
10235
|
|
|
9691
10236
|
// Timing for active stage · ETA range while in-band, elapsed once over.
|
|
@@ -9693,11 +10238,10 @@
|
|
|
9693
10238
|
if (activeStatus === "active") {
|
|
9694
10239
|
if (activeElapsed <= activeEta[1]) {
|
|
9695
10240
|
timing = activeElapsed > 0
|
|
9696
|
-
?
|
|
9697
|
-
:
|
|
10241
|
+
? this._t("brief_timing_inband", { e: activeElapsed, lo: activeEta[0], hi: activeEta[1] })
|
|
10242
|
+
: this._t("brief_timing_eta", { lo: activeEta[0], hi: activeEta[1] });
|
|
9698
10243
|
} else {
|
|
9699
|
-
timing =
|
|
9700
|
-
}
|
|
10244
|
+
timing = this._t("brief_timing_over", { n: activeElapsed }); }
|
|
9701
10245
|
}
|
|
9702
10246
|
|
|
9703
10247
|
// Active-card progress line · in-band 0→95% linear, then a Zeno
|
|
@@ -9746,22 +10290,24 @@
|
|
|
9746
10290
|
|
|
9747
10291
|
// Total ETA formatting · "1m 12s · ~3-5m left" / "已 1m 12s · 还需约 3–5 分钟"
|
|
9748
10292
|
const fmtSec = (s) => {
|
|
9749
|
-
if (s < 60) return
|
|
10293
|
+
if (s < 60) return this._t("brief_fmt_sec", { n: s });
|
|
9750
10294
|
const m = Math.floor(s / 60);
|
|
9751
10295
|
const r = s % 60;
|
|
9752
|
-
return r === 0 ?
|
|
10296
|
+
return r === 0 ? this._t("brief_fmt_min", { n: m }) : this._t("brief_fmt_min_sec", { m, r });
|
|
9753
10297
|
};
|
|
9754
10298
|
const fmtRange = (lo, hi) => {
|
|
9755
|
-
if (hi < 60) return
|
|
10299
|
+
if (hi < 60) return this._t("brief_range_sec", { lo, hi });
|
|
9756
10300
|
const loM = Math.max(1, Math.round(lo / 60));
|
|
9757
10301
|
const hiM = Math.max(loM, Math.round(hi / 60));
|
|
9758
|
-
return
|
|
10302
|
+
return this._t("brief_range_min", { lo: loM, hi: hiM });
|
|
9759
10303
|
};
|
|
9760
10304
|
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
10305
|
+
const totalText = this._t("brief_total_line", {
|
|
10306
|
+
elapsed: fmtSec(totalElapsed),
|
|
10307
|
+
range: fmtRange(totalLo, totalHi),
|
|
10308
|
+
});
|
|
9764
10309
|
|
|
10310
|
+
const kickerCore = this._t("brief_stage_kicker", { chair: chairDisp, total: totalText });
|
|
9765
10311
|
const metaHtml = (detailLine || timing)
|
|
9766
10312
|
? `<span class="brief-active-meta">` +
|
|
9767
10313
|
(detailLine ? `<span class="meta-detail">${this.escape(detailLine)}</span>` : "") +
|
|
@@ -9792,8 +10338,17 @@
|
|
|
9792
10338
|
this._briefSeenHarvestKeys = this._briefSeenHarvestKeys || {};
|
|
9793
10339
|
const seenH = this._briefSeenHarvestKeys[b.id] = this._briefSeenHarvestKeys[b.id] || new Set();
|
|
9794
10340
|
const harvest = Array.isArray(b.extractHarvest) ? b.extractHarvest : [];
|
|
9795
|
-
|
|
9796
|
-
|
|
10341
|
+
const kindLabels = {
|
|
10342
|
+
claims: this._t("brief_harvest_claims"),
|
|
10343
|
+
evidence: this._t("brief_harvest_evidence"),
|
|
10344
|
+
tensions: this._t("brief_harvest_tensions"),
|
|
10345
|
+
assumptions: this._t("brief_harvest_assumptions"),
|
|
10346
|
+
risks: this._t("brief_harvest_risks"),
|
|
10347
|
+
opportunities: this._t("brief_harvest_opportunities"),
|
|
10348
|
+
actions: this._t("brief_harvest_actions"),
|
|
10349
|
+
quotes: this._t("brief_harvest_quotes"),
|
|
10350
|
+
openQuestions: this._t("brief_harvest_open_questions"),
|
|
10351
|
+
};
|
|
9797
10352
|
if (harvest.length) {
|
|
9798
10353
|
const chips = harvest.map((h) => {
|
|
9799
10354
|
const isFresh = !seenH.has(h.directorId);
|
|
@@ -9818,7 +10373,7 @@
|
|
|
9818
10373
|
// Live word count during write — large and visible since it's the
|
|
9819
10374
|
// most engaging signal during the long write stage.
|
|
9820
10375
|
if (writeActive && wordCount > 0) {
|
|
9821
|
-
const w =
|
|
10376
|
+
const w = this._t("brief_stat_writing", { n: wordCount });
|
|
9822
10377
|
stats.push(`<span class="brief-stat-fact brief-stat-live">${this.escape(w)}</span>`);
|
|
9823
10378
|
}
|
|
9824
10379
|
const statsHtml = stats.length
|
|
@@ -9838,6 +10393,7 @@
|
|
|
9838
10393
|
</div>
|
|
9839
10394
|
<div class="brief-pip-rail">${pipHtml}</div>
|
|
9840
10395
|
${statsHtml}
|
|
10396
|
+
${this.renderBriefLlmTrace(b)}
|
|
9841
10397
|
</div>
|
|
9842
10398
|
`;
|
|
9843
10399
|
},
|
|
@@ -9882,15 +10438,9 @@
|
|
|
9882
10438
|
|
|
9883
10439
|
/** Map internal tag id → short visible label. */
|
|
9884
10440
|
noteTagLabel(tag) {
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
insight: "claim",
|
|
9889
|
-
warn: "drop",
|
|
9890
|
-
crux: "crux",
|
|
9891
|
-
soln: "pursue",
|
|
9892
|
-
open: "ask",
|
|
9893
|
-
})[tag] || tag;
|
|
10441
|
+
const k = "note_tag_" + String(tag || "").replace(/[^a-z0-9_]/g, "");
|
|
10442
|
+
const tr = this._t(k);
|
|
10443
|
+
return tr === k ? String(tag || "") : tr;
|
|
9894
10444
|
},
|
|
9895
10445
|
|
|
9896
10446
|
/** True when the user is "stuck to bottom" — within a small margin
|
|
@@ -10021,6 +10571,8 @@
|
|
|
10021
10571
|
}
|
|
10022
10572
|
return;
|
|
10023
10573
|
}
|
|
10574
|
+
// Toggle voice/text delivery mode — REMOVED (mid-session switching
|
|
10575
|
+
// causes too many timing issues; delivery mode is set at room creation).
|
|
10024
10576
|
// Pause-choice modal buttons
|
|
10025
10577
|
const choice = e.target.closest("[data-pause-choice]");
|
|
10026
10578
|
if (choice) {
|
|
@@ -10050,6 +10602,23 @@
|
|
|
10050
10602
|
window.location.href = "/api/rooms/" + encodeURIComponent(app.currentRoomId) + "/export.md";
|
|
10051
10603
|
return;
|
|
10052
10604
|
}
|
|
10605
|
+
// Voice Replay · adjourned-bar action. Plays the transcript
|
|
10606
|
+
// back via TTS in chronological order, each director in their
|
|
10607
|
+
// own voice. Routed through the standalone voice-replay module
|
|
10608
|
+
// so the playback state machine + overlay live in one place.
|
|
10609
|
+
if (e.target.closest("[data-room-replay]")) {
|
|
10610
|
+
e.preventDefault();
|
|
10611
|
+
if (!app.currentRoomId) return;
|
|
10612
|
+
if (window.boardroomVoiceReplay && typeof window.boardroomVoiceReplay.open === "function") {
|
|
10613
|
+
window.boardroomVoiceReplay.open({
|
|
10614
|
+
roomId: app.currentRoomId,
|
|
10615
|
+
messages: Array.isArray(app.currentMessages) ? app.currentMessages.slice() : [],
|
|
10616
|
+
members: Array.isArray(app.currentMembers) ? app.currentMembers.slice() : [],
|
|
10617
|
+
chair: app.currentChair || null,
|
|
10618
|
+
});
|
|
10619
|
+
}
|
|
10620
|
+
return;
|
|
10621
|
+
}
|
|
10053
10622
|
// Convene Follow-up · adjourned-bar action. Opens the follow-up
|
|
10054
10623
|
// overlay with parent reference + form for the new question.
|
|
10055
10624
|
if (e.target.closest("[data-room-followup]")) {
|
|
@@ -10242,6 +10811,17 @@
|
|
|
10242
10811
|
app.retryBriefGeneration(targetId);
|
|
10243
10812
|
return;
|
|
10244
10813
|
}
|
|
10814
|
+
const llmToggle = e.target.closest("[data-brief-llm-toggle]");
|
|
10815
|
+
if (llmToggle) {
|
|
10816
|
+
e.preventDefault();
|
|
10817
|
+
const id = llmToggle.getAttribute("data-brief-id");
|
|
10818
|
+
const brief = id ? app._briefById(id) : app.currentBrief;
|
|
10819
|
+
if (brief) {
|
|
10820
|
+
brief.llmLogOpen = !brief.llmLogOpen;
|
|
10821
|
+
app.renderBrief();
|
|
10822
|
+
}
|
|
10823
|
+
return;
|
|
10824
|
+
}
|
|
10245
10825
|
// Salvage banner · dismiss button. Just removes the banner DOM —
|
|
10246
10826
|
// the failed brief stays in currentBriefs so the user can still
|
|
10247
10827
|
// retry from a brief tab if they change their mind.
|
|
@@ -10436,6 +11016,43 @@
|
|
|
10436
11016
|
if (typeof window.openNewAgent === "function") window.openNewAgent();
|
|
10437
11017
|
return;
|
|
10438
11018
|
}
|
|
11019
|
+
// ─── New-room composer · voice-mode toggle. Same three paths
|
|
11020
|
+
// as the websearch toggle just below: unconfigured → open
|
|
11021
|
+
// keys panel; on → off; off → on. Done with in-place class /
|
|
11022
|
+
// text mutation so the composer textarea isn't blown away by
|
|
11023
|
+
// a full repaint on every click.
|
|
11024
|
+
const voiceToggle = e.target.closest("[data-composer-voice-toggle]");
|
|
11025
|
+
if (voiceToggle) {
|
|
11026
|
+
e.preventDefault();
|
|
11027
|
+
const configured = app.hasAnyVoiceKey();
|
|
11028
|
+
if (!configured) {
|
|
11029
|
+
// Stale `data-configured` may say 1 if the user added then
|
|
11030
|
+
// removed a key without a re-render; live cache wins.
|
|
11031
|
+
if (typeof window.openUserSettings === "function") {
|
|
11032
|
+
window.openUserSettings({ section: "keys", focusProvider: "minimax" });
|
|
11033
|
+
}
|
|
11034
|
+
return;
|
|
11035
|
+
}
|
|
11036
|
+
// Live cache says configured · sync the toggle's stale attrs
|
|
11037
|
+
// so the next click flips rather than re-prompts.
|
|
11038
|
+
if (voiceToggle.getAttribute("data-configured") !== "1") {
|
|
11039
|
+
voiceToggle.setAttribute("data-configured", "1");
|
|
11040
|
+
voiceToggle.classList.remove("needs-key");
|
|
11041
|
+
}
|
|
11042
|
+
const wasOn = voiceToggle.getAttribute("data-on") === "1";
|
|
11043
|
+
const next = !wasOn;
|
|
11044
|
+
app.setComposerDeliveryMode(next ? "voice" : "text");
|
|
11045
|
+
voiceToggle.classList.toggle("on", next);
|
|
11046
|
+
voiceToggle.classList.toggle("off", !next);
|
|
11047
|
+
voiceToggle.setAttribute("data-on", next ? "1" : "0");
|
|
11048
|
+
voiceToggle.setAttribute("aria-pressed", next ? "true" : "false");
|
|
11049
|
+
const txt = voiceToggle.querySelector(".ap-skill-row-toggle-text");
|
|
11050
|
+
if (txt) txt.textContent = app._t("cmp_voice_label");
|
|
11051
|
+
voiceToggle.title = next
|
|
11052
|
+
? "Voice mode on · directors speak aloud during the room"
|
|
11053
|
+
: "Voice mode off · click to enable";
|
|
11054
|
+
return;
|
|
11055
|
+
}
|
|
10439
11056
|
// ─── Agent composer · websearch toggle. Three paths:
|
|
10440
11057
|
// · unconfigured → confirm + open Preferences → Brave row
|
|
10441
11058
|
// · on → save off + flip toggle in place
|
|
@@ -10454,10 +11071,15 @@
|
|
|
10454
11071
|
// has it. boardroomKeys() reads through the shared _keysMeta
|
|
10455
11072
|
// map, which user-settings.js patches in place after every
|
|
10456
11073
|
// setProviderKey, so we get fresh truth here.
|
|
10457
|
-
const configured = !!(app.
|
|
11074
|
+
const configured = !!(app.agentComposerWebSearchConfigured && app.agentComposerWebSearchConfigured());
|
|
10458
11075
|
if (!configured) {
|
|
10459
|
-
|
|
10460
|
-
|
|
11076
|
+
const fallback =
|
|
11077
|
+
"Web Search needs Brave Search or Tavily API credentials.\n\nOpen Preferences now?";
|
|
11078
|
+
const ok = confirm(
|
|
11079
|
+
(window.I18n && typeof window.I18n.t === "function")
|
|
11080
|
+
? window.I18n.t("ag_ws_need_key_confirm")
|
|
11081
|
+
: fallback,
|
|
11082
|
+
);
|
|
10461
11083
|
if (ok && typeof window.openUserSettings === "function") {
|
|
10462
11084
|
window.openUserSettings({ section: "keys", focusProvider: "brave" });
|
|
10463
11085
|
}
|
|
@@ -10500,6 +11122,14 @@
|
|
|
10500
11122
|
app.discardAgentSpec();
|
|
10501
11123
|
return;
|
|
10502
11124
|
}
|
|
11125
|
+
if (e.target.closest("[data-agent-spec-stop]")) {
|
|
11126
|
+
// Stop · same effect as Discard while generation is in flight ·
|
|
11127
|
+
// aborts the AbortController, which propagates to the fetch
|
|
11128
|
+
// (cancels the LLM call server-side) and resets composer state.
|
|
11129
|
+
e.preventDefault();
|
|
11130
|
+
app.discardAgentSpec();
|
|
11131
|
+
return;
|
|
11132
|
+
}
|
|
10503
11133
|
if (e.target.closest("[data-agent-spec-redo]")) {
|
|
10504
11134
|
e.preventDefault();
|
|
10505
11135
|
app.redoAgentSpec();
|
|
@@ -10585,7 +11215,25 @@
|
|
|
10585
11215
|
} else {
|
|
10586
11216
|
if (kind === "tone") app.setComposerTone(v);
|
|
10587
11217
|
else if (kind === "intensity") app.setComposerIntensity(v);
|
|
11218
|
+
else if (kind === "delivery") app.setComposerDeliveryMode(v);
|
|
10588
11219
|
else if (kind === "agent-model") app.setAgentComposerModel(v);
|
|
11220
|
+
else if (kind === "locale") {
|
|
11221
|
+
// Interface language · runs through the shared I18n
|
|
11222
|
+
// setter so document.documentElement.lang, the persisted
|
|
11223
|
+
// boardroom.uiLocale storage entry, applyDom, and the
|
|
11224
|
+
// boardroom:locale event all fire as a unit. The trigger's
|
|
11225
|
+
// value span is updated explicitly because applyDom won't
|
|
11226
|
+
// touch a `data-cmp-dd-value` (it has no data-i18n key).
|
|
11227
|
+
if (window.I18n && typeof window.I18n.setLocale === "function") {
|
|
11228
|
+
window.I18n.setLocale(v);
|
|
11229
|
+
}
|
|
11230
|
+
if (trigger) {
|
|
11231
|
+
const valSpan = trigger.querySelector("[data-cmp-dd-value]");
|
|
11232
|
+
if (valSpan) {
|
|
11233
|
+
valSpan.textContent = v === "zh" ? "中文" : "EN";
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
10589
11237
|
}
|
|
10590
11238
|
app.closeComposerDropdown();
|
|
10591
11239
|
return;
|
|
@@ -10836,6 +11484,18 @@
|
|
|
10836
11484
|
}
|
|
10837
11485
|
});
|
|
10838
11486
|
|
|
11487
|
+
// Global one-shot listener: unlock audio playback on first user
|
|
11488
|
+
// interaction so voice mode works regardless of which button was
|
|
11489
|
+
// clicked first.
|
|
11490
|
+
document.addEventListener("click", function _unlockAudio() {
|
|
11491
|
+
app.unlockAudioPlayback();
|
|
11492
|
+
document.removeEventListener("click", _unlockAudio);
|
|
11493
|
+
});
|
|
11494
|
+
document.addEventListener("keydown", function _unlockAudioKey() {
|
|
11495
|
+
app.unlockAudioPlayback();
|
|
11496
|
+
document.removeEventListener("keydown", _unlockAudioKey);
|
|
11497
|
+
});
|
|
11498
|
+
|
|
10839
11499
|
window.app = app;
|
|
10840
11500
|
|
|
10841
11501
|
if (document.readyState === "loading") {
|