privateboard 0.1.5 → 0.1.7
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 +12 -2
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
- package/public/agent-profile.js +14 -0
- package/public/app.js +465 -704
- package/public/home.html +3 -3
- package/public/index.html +193 -151
- package/public/report.html +356 -59
- package/public/typing-sfx.js +158 -0
- package/public/user-settings.css +90 -0
- package/public/user-settings.js +71 -0
package/public/app.js
CHANGED
|
@@ -292,20 +292,14 @@
|
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const lang = (this.composerLanguage && this.composerLanguage()) || "en";
|
|
296
295
|
const count = fresh.length;
|
|
297
296
|
const names = fresh.map((m) => m.name).join(", ");
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
: {
|
|
305
|
-
head: `Storage upgraded`,
|
|
306
|
-
body: `${count} new migration${count > 1 ? "s" : ""} applied · your existing rooms, agents, briefs, and settings were preserved.`,
|
|
307
|
-
tooltip: names,
|
|
308
|
-
};
|
|
297
|
+
// System UI · always English (storage-migration banner).
|
|
298
|
+
const copy = {
|
|
299
|
+
head: `Storage upgraded`,
|
|
300
|
+
body: `${count} new migration${count > 1 ? "s" : ""} applied · your existing rooms, agents, briefs, and settings were preserved.`,
|
|
301
|
+
tooltip: names,
|
|
302
|
+
};
|
|
309
303
|
textEl.innerHTML =
|
|
310
304
|
`<span class="sys-notice-strong">${this.escape(copy.head)}</span> · ${this.escape(copy.body)}`;
|
|
311
305
|
banner.title = copy.tooltip;
|
|
@@ -783,6 +777,10 @@
|
|
|
783
777
|
msg.body += data.delta;
|
|
784
778
|
this.updateMessageBodyDom(data.messageId, msg.body, true);
|
|
785
779
|
this.scrollChatToBottom();
|
|
780
|
+
// Typing-SFX presence cue · the module throttles internally,
|
|
781
|
+
// mutes when the tab is backgrounded, and respects the user's
|
|
782
|
+
// toggle in Preference → User. Safe to call on every chunk.
|
|
783
|
+
window.boardroomTypingSfx && window.boardroomTypingSfx.tick();
|
|
786
784
|
});
|
|
787
785
|
|
|
788
786
|
this.sse.addEventListener("message-final", (e) => {
|
|
@@ -1078,6 +1076,10 @@
|
|
|
1078
1076
|
}
|
|
1079
1077
|
}
|
|
1080
1078
|
}
|
|
1079
|
+
// Typing-SFX presence cue · same as message-token. Ticks
|
|
1080
|
+
// even on off-tab briefs so the user has aural awareness
|
|
1081
|
+
// that the brief writer is making progress.
|
|
1082
|
+
window.boardroomTypingSfx && window.boardroomTypingSfx.tick();
|
|
1081
1083
|
} else if (kind === "brief-final") {
|
|
1082
1084
|
this.markBriefEvent();
|
|
1083
1085
|
const target = this._briefById(payload.briefId);
|
|
@@ -1498,24 +1500,15 @@
|
|
|
1498
1500
|
* visual treatment stays consistent. */
|
|
1499
1501
|
openNoKeyModal() {
|
|
1500
1502
|
this.closeNoKeyModal();
|
|
1501
|
-
|
|
1502
|
-
const t =
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1511
|
-
: {
|
|
1512
|
-
title: "Configure a model API key",
|
|
1513
|
-
deck: "The chair and directors run on a large language model. Configure at least one provider key (Anthropic / OpenAI / Google / xAI / DeepSeek / OpenRouter) before AI features will work.",
|
|
1514
|
-
primary: "[ Open settings ▸ ]",
|
|
1515
|
-
dismiss: "[ Dismiss ]",
|
|
1516
|
-
classification: "● ai · no model key",
|
|
1517
|
-
tag: "▸ Configure API key",
|
|
1518
|
-
};
|
|
1503
|
+
// System UI · always English. No-key modal is app chrome.
|
|
1504
|
+
const t = {
|
|
1505
|
+
title: "Configure a model API key",
|
|
1506
|
+
deck: "The chair and directors run on a large language model. Configure at least one provider key (Anthropic / OpenAI / Google / xAI / DeepSeek / OpenRouter) before AI features will work.",
|
|
1507
|
+
primary: "[ Open settings ▸ ]",
|
|
1508
|
+
dismiss: "[ Dismiss ]",
|
|
1509
|
+
classification: "● ai · no model key",
|
|
1510
|
+
tag: "▸ Configure API key",
|
|
1511
|
+
};
|
|
1519
1512
|
const html = `
|
|
1520
1513
|
<div id="no-key-overlay" class="pc-overlay" data-no-key-overlay>
|
|
1521
1514
|
<div class="pc-modal">
|
|
@@ -1531,7 +1524,7 @@
|
|
|
1531
1524
|
<div class="pc-body">
|
|
1532
1525
|
<button type="button" class="pc-choice primary" data-no-key-open-settings>
|
|
1533
1526
|
<div class="pc-choice-mark">${this.escape(t.primary)}</div>
|
|
1534
|
-
<div class="pc-choice-deck"
|
|
1527
|
+
<div class="pc-choice-deck">Jump to Preferences → API Key. Paste any one provider's key to unlock the room.</div>
|
|
1535
1528
|
</button>
|
|
1536
1529
|
<button type="button" class="pc-choice ghost" data-no-key-dismiss>
|
|
1537
1530
|
<div class="pc-choice-mark">${this.escape(t.dismiss)}</div>
|
|
@@ -1783,30 +1776,18 @@
|
|
|
1783
1776
|
openSupplementOverlay() {
|
|
1784
1777
|
if (!this.currentRoomId || !this.currentBrief) return;
|
|
1785
1778
|
this.closeSupplementOverlay();
|
|
1786
|
-
|
|
1787
|
-
const t =
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
}
|
|
1799
|
-
: {
|
|
1800
|
-
classify: "report · regenerate with supplement",
|
|
1801
|
-
classifyRight: "// regenerate",
|
|
1802
|
-
title: "Add a perspective",
|
|
1803
|
-
metaPrefix: "// Current report",
|
|
1804
|
-
placeholder: "Describe an angle you want the chair to additionally consider. For example:\n· Bring in a commercial-viability lens\n· Center the experience of women users\n· Stretch the time window to 5 years",
|
|
1805
|
-
hint: "The new perspective will be woven through the existing Findings, Recommendations, and New Questions sections — not added as a separate section.",
|
|
1806
|
-
cancel: "[ Cancel ]",
|
|
1807
|
-
confirm: "[ Regenerate ]",
|
|
1808
|
-
confirmBusy: "[ Regenerating… ]",
|
|
1809
|
-
};
|
|
1779
|
+
// System UI · always English. Supplement-overlay chrome.
|
|
1780
|
+
const t = {
|
|
1781
|
+
classify: "report · regenerate with supplement",
|
|
1782
|
+
classifyRight: "// regenerate",
|
|
1783
|
+
title: "Add a perspective",
|
|
1784
|
+
metaPrefix: "// Current report",
|
|
1785
|
+
placeholder: "Describe an angle you want the chair to additionally consider. For example:\n· Bring in a commercial-viability lens\n· Center the experience of women users\n· Stretch the time window to 5 years",
|
|
1786
|
+
hint: "The new perspective will be woven through the existing Findings, Recommendations, and New Questions sections — not added as a separate section.",
|
|
1787
|
+
cancel: "[ Cancel ]",
|
|
1788
|
+
confirm: "[ Regenerate ]",
|
|
1789
|
+
confirmBusy: "[ Regenerating… ]",
|
|
1790
|
+
};
|
|
1810
1791
|
const html = `
|
|
1811
1792
|
<div class="supplement-overlay" id="supplement-overlay" role="dialog" aria-modal="true">
|
|
1812
1793
|
<div class="supplement-backdrop" data-supplement-close></div>
|
|
@@ -1875,31 +1856,19 @@
|
|
|
1875
1856
|
if (!this.currentRoomId || !this.currentRoom) return;
|
|
1876
1857
|
if (this.currentRoom.status !== "paused") return;
|
|
1877
1858
|
this.closePausedSupplementOverlay();
|
|
1878
|
-
|
|
1879
|
-
const t =
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
: {
|
|
1892
|
-
classify: "room · paused supplement",
|
|
1893
|
-
classifyRight: "// queued first",
|
|
1894
|
-
title: "Add a supplemental input",
|
|
1895
|
-
metaPrefix: "// Current room",
|
|
1896
|
-
placeholder: "Drop in an extra thought, a follow-up question, or an angle you'd like the board to take into account.\n\nIt lands in the chat as your message right now; when you hit [ Resume ], the next director picks up with this in front of them.",
|
|
1897
|
-
hint: "Posted while paused, the supplement lands as your message immediately; the saved speaker queue is untouched. After resume, the next director responds with the supplement first.",
|
|
1898
|
-
cancel: "[ Cancel ]",
|
|
1899
|
-
confirm: "[ Add to chat ]",
|
|
1900
|
-
confirmBusy: "[ Posting… ]",
|
|
1901
|
-
};
|
|
1902
|
-
const subject = (this.currentRoom.subject || "").trim() || (lang === "zh" ? "(无主题)" : "(no subject)");
|
|
1859
|
+
// System UI · always English. Paused-supplement overlay chrome.
|
|
1860
|
+
const t = {
|
|
1861
|
+
classify: "room · paused supplement",
|
|
1862
|
+
classifyRight: "// queued first",
|
|
1863
|
+
title: "Add a supplemental input",
|
|
1864
|
+
metaPrefix: "// Current room",
|
|
1865
|
+
placeholder: "Drop in an extra thought, a follow-up question, or an angle you'd like the board to take into account.\n\nIt lands in the chat as your message right now; when you hit [ Resume ], the next director picks up with this in front of them.",
|
|
1866
|
+
hint: "Posted while paused, the supplement lands as your message immediately; the saved speaker queue is untouched. After resume, the next director responds with the supplement first.",
|
|
1867
|
+
cancel: "[ Cancel ]",
|
|
1868
|
+
confirm: "[ Add to chat ]",
|
|
1869
|
+
confirmBusy: "[ Posting… ]",
|
|
1870
|
+
};
|
|
1871
|
+
const subject = (this.currentRoom.subject || "").trim() || "(no subject)";
|
|
1903
1872
|
const html = `
|
|
1904
1873
|
<div class="supplement-overlay" id="paused-supplement-overlay" role="dialog" aria-modal="true">
|
|
1905
1874
|
<div class="supplement-backdrop" data-paused-supplement-close></div>
|
|
@@ -2019,54 +1988,30 @@
|
|
|
2019
1988
|
if (this.currentRoom.status !== "adjourned") return;
|
|
2020
1989
|
this.closeFollowUpOverlay();
|
|
2021
1990
|
|
|
2022
|
-
|
|
2023
|
-
const t =
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
}
|
|
2047
|
-
: {
|
|
2048
|
-
classify: "follow-up · continuation room",
|
|
2049
|
-
classifyRight: "// continuing",
|
|
2050
|
-
title: "Convene a follow-up",
|
|
2051
|
-
metaPrefix: "// following up",
|
|
2052
|
-
placeholder: "What's the next question to chase, given what the prior session settled?",
|
|
2053
|
-
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.",
|
|
2054
|
-
castLabel: "Directors",
|
|
2055
|
-
castHint: "2–4 recommended",
|
|
2056
|
-
castSame: "Same cast as last session",
|
|
2057
|
-
pickerLabel: "Pick directors",
|
|
2058
|
-
autoLabel: "directors",
|
|
2059
|
-
autoVal: "auto-pick",
|
|
2060
|
-
countersDirectors: (n) => `${n} director${n === 1 ? "" : "s"}`,
|
|
2061
|
-
toneLabel: "Tone",
|
|
2062
|
-
intensityLabel: "Intensity",
|
|
2063
|
-
cancel: "[ Cancel ]",
|
|
2064
|
-
confirm: "[ Convene → ]",
|
|
2065
|
-
confirmBusy: "[ Convening… ]",
|
|
2066
|
-
adjournedAtPrefix: "adjourned",
|
|
2067
|
-
briefsCount: (n) => `${n} ${n === 1 ? "brief" : "briefs"} filed`,
|
|
2068
|
-
noBrief: "no brief filed",
|
|
2069
|
-
};
|
|
1991
|
+
// System UI · always English. Follow-up overlay chrome.
|
|
1992
|
+
const t = {
|
|
1993
|
+
classify: "follow-up · continuation room",
|
|
1994
|
+
classifyRight: "// continuing",
|
|
1995
|
+
title: "Convene a follow-up",
|
|
1996
|
+
metaPrefix: "// following up",
|
|
1997
|
+
placeholder: "What's the next question to chase, given what the prior session settled?",
|
|
1998
|
+
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.",
|
|
1999
|
+
castLabel: "Directors",
|
|
2000
|
+
castHint: "2–4 recommended",
|
|
2001
|
+
castSame: "Same cast as last session",
|
|
2002
|
+
pickerLabel: "Pick directors",
|
|
2003
|
+
autoLabel: "directors",
|
|
2004
|
+
autoVal: "auto-pick",
|
|
2005
|
+
countersDirectors: (n) => `${n} director${n === 1 ? "" : "s"}`,
|
|
2006
|
+
toneLabel: "Tone",
|
|
2007
|
+
intensityLabel: "Intensity",
|
|
2008
|
+
cancel: "[ Cancel ]",
|
|
2009
|
+
confirm: "[ Convene → ]",
|
|
2010
|
+
confirmBusy: "[ Convening… ]",
|
|
2011
|
+
adjournedAtPrefix: "adjourned",
|
|
2012
|
+
briefsCount: (n) => `${n} ${n === 1 ? "brief" : "briefs"} filed`,
|
|
2013
|
+
noBrief: "no brief filed",
|
|
2014
|
+
};
|
|
2070
2015
|
|
|
2071
2016
|
const room = this.currentRoom;
|
|
2072
2017
|
const briefCount = Array.isArray(this.currentBriefs) ? this.currentBriefs.length : 0;
|
|
@@ -2105,7 +2050,6 @@
|
|
|
2105
2050
|
directorIds: [],
|
|
2106
2051
|
autoPick: true,
|
|
2107
2052
|
parentDirectorIds: parentDirectorIds.slice(),
|
|
2108
|
-
lang,
|
|
2109
2053
|
};
|
|
2110
2054
|
|
|
2111
2055
|
const html = `
|
|
@@ -2269,7 +2213,6 @@
|
|
|
2269
2213
|
const btn = document.querySelector("[data-followup-cast-btn]");
|
|
2270
2214
|
if (!btn || !this._followupCastState) return;
|
|
2271
2215
|
const state = this._followupCastState;
|
|
2272
|
-
const lang = state.lang || "en";
|
|
2273
2216
|
|
|
2274
2217
|
btn.classList.remove("cmp-cast-btn-auto");
|
|
2275
2218
|
btn.removeAttribute("data-cast-mode");
|
|
@@ -2286,9 +2229,8 @@
|
|
|
2286
2229
|
const avatars = visible.map((a) =>
|
|
2287
2230
|
`<img class="cmp-cast-av" src="${this.escape(a.avatarPath || "")}" alt="${this.escape(a.name || "")}" title="${this.escape(a.name || "")}">`,
|
|
2288
2231
|
).join("");
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
: `${parents.length} · same as last`;
|
|
2232
|
+
// System UI · always English (cast button chrome).
|
|
2233
|
+
const count = `${parents.length} · same as last`;
|
|
2292
2234
|
btn.innerHTML =
|
|
2293
2235
|
`<span class="cmp-cast-stack">${avatars}${overflow > 0 ? `<span class="cmp-cast-more">+${overflow}</span>` : ""}</span>` +
|
|
2294
2236
|
`<span class="cmp-cast-count">${this.escape(count)}</span>`;
|
|
@@ -2304,8 +2246,9 @@
|
|
|
2304
2246
|
// Auto-pick · default
|
|
2305
2247
|
btn.classList.add("cmp-cast-btn-auto");
|
|
2306
2248
|
btn.setAttribute("data-cast-mode", "auto");
|
|
2307
|
-
|
|
2308
|
-
const
|
|
2249
|
+
// System UI · always English (cast button chrome).
|
|
2250
|
+
const autoKey = "directors";
|
|
2251
|
+
const autoVal = "auto-pick";
|
|
2309
2252
|
btn.innerHTML =
|
|
2310
2253
|
`<span class="cmp-cast-stack cmp-cast-stack-auto"><span class="cmp-cast-auto-mark">✦</span></span>` +
|
|
2311
2254
|
`<span class="cmp-cast-count cmp-cast-auto-label">` +
|
|
@@ -2320,9 +2263,8 @@
|
|
|
2320
2263
|
const avatars = visible.map((a) =>
|
|
2321
2264
|
`<img class="cmp-cast-av" src="${this.escape(a.avatarPath || "")}" alt="${this.escape(a.name || "")}" title="${this.escape(a.name || "")}">`,
|
|
2322
2265
|
).join("");
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
: `${picked.length} director${picked.length === 1 ? "" : "s"}`;
|
|
2266
|
+
// System UI · always English (cast button chrome count).
|
|
2267
|
+
const countText = `${picked.length} director${picked.length === 1 ? "" : "s"}`;
|
|
2326
2268
|
btn.setAttribute("data-cast-mode", "manual");
|
|
2327
2269
|
btn.innerHTML =
|
|
2328
2270
|
`<span class="cmp-cast-stack">${avatars}${overflow > 0 ? `<span class="cmp-cast-more">+${overflow}</span>` : ""}<span class="cmp-cast-add" aria-hidden="true">+</span></span>` +
|
|
@@ -2338,14 +2280,12 @@
|
|
|
2338
2280
|
if (!anchorBtn || !this._followupCastState) return;
|
|
2339
2281
|
if (this._followupCastState.sameAsLast) return; // disabled when "same cast" is on
|
|
2340
2282
|
const state = this._followupCastState;
|
|
2341
|
-
const lang = state.lang || "en";
|
|
2342
2283
|
const dirs = (this.agents || [])
|
|
2343
2284
|
.filter((a) => a.roleKind !== "moderator")
|
|
2344
2285
|
.slice()
|
|
2345
2286
|
.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
: { title: "Pick directors", hint: "2-4 recommended", info: "View profile" };
|
|
2287
|
+
// System UI · always English (director picker chrome).
|
|
2288
|
+
const t = { title: "Pick directors", hint: "2-4 recommended", info: "View profile" };
|
|
2349
2289
|
const rows = dirs.map((a) => {
|
|
2350
2290
|
const checked = state.directorIds.includes(a.id);
|
|
2351
2291
|
return `
|
|
@@ -2679,23 +2619,14 @@
|
|
|
2679
2619
|
async deleteBriefAt(briefId) {
|
|
2680
2620
|
if (!briefId) return;
|
|
2681
2621
|
const target = (this.currentBriefs || []).find((b) => b.id === briefId);
|
|
2682
|
-
|
|
2683
|
-
//
|
|
2684
|
-
// copy is sharper · the user is also cancelling an in-flight
|
|
2685
|
-
// pipeline (LLM calls actively burning tokens), not just removing
|
|
2686
|
-
// a finished row. Server aborts the upstream fetches when we DELETE.
|
|
2622
|
+
// System UI · always English. Confirm + alert dialogs are app
|
|
2623
|
+
// chrome and stay fixed-string regardless of brief language.
|
|
2687
2624
|
const isStillGenerating = !!(target && (!target.bodyMd || !target.bodyMd.trim()));
|
|
2688
|
-
const confirmText =
|
|
2689
|
-
?
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
: "删除这份报告?此操作不可恢复。"))
|
|
2694
|
-
: (isStillGenerating
|
|
2695
|
-
? "This report is still generating. Deleting will stop the generation and remove the report. This can't be undone."
|
|
2696
|
-
: (target?.supplement
|
|
2697
|
-
? `Delete the "${target.supplement.trim().slice(0, 20)}${target.supplement.length > 20 ? "…" : ""}" version? This can't be undone.`
|
|
2698
|
-
: "Delete this report? This can't be undone."));
|
|
2625
|
+
const confirmText = isStillGenerating
|
|
2626
|
+
? "This report is still generating. Deleting will stop the generation and remove the report. This can't be undone."
|
|
2627
|
+
: (target?.supplement
|
|
2628
|
+
? `Delete the "${target.supplement.trim().slice(0, 20)}${target.supplement.length > 20 ? "…" : ""}" version? This can't be undone.`
|
|
2629
|
+
: "Delete this report? This can't be undone.");
|
|
2699
2630
|
if (!confirm(confirmText)) return;
|
|
2700
2631
|
try {
|
|
2701
2632
|
const r = await fetch("/api/briefs/" + encodeURIComponent(briefId), { method: "DELETE" });
|
|
@@ -2704,7 +2635,7 @@
|
|
|
2704
2635
|
throw new Error(e.error || ("HTTP " + r.status));
|
|
2705
2636
|
}
|
|
2706
2637
|
} catch (e) {
|
|
2707
|
-
alert(
|
|
2638
|
+
alert("Delete failed: " + (e && e.message ? e.message : e));
|
|
2708
2639
|
return;
|
|
2709
2640
|
}
|
|
2710
2641
|
// Patch local state · remove the deleted brief, refresh active.
|
|
@@ -3047,7 +2978,7 @@
|
|
|
3047
2978
|
/** Spin up the countdown if conditions are met; otherwise cancel. */
|
|
3048
2979
|
maybeStartContinueCountdown() {
|
|
3049
2980
|
if (!this.canAutoContinue()) {
|
|
3050
|
-
this.cancelContinueCountdown(
|
|
2981
|
+
this.cancelContinueCountdown();
|
|
3051
2982
|
this.refreshContinueButton();
|
|
3052
2983
|
return;
|
|
3053
2984
|
}
|
|
@@ -3844,14 +3775,11 @@
|
|
|
3844
3775
|
renderSidebarCounts() {
|
|
3845
3776
|
const roomsCount = this.rooms.length;
|
|
3846
3777
|
const agentsCount = this.agents.length;
|
|
3847
|
-
const liveCount = this.rooms.filter((r) => r.status === "live").length;
|
|
3848
3778
|
|
|
3849
3779
|
const r = document.querySelector('[data-sidebar-tab-count="rooms"]');
|
|
3850
3780
|
if (r) r.textContent = String(roomsCount);
|
|
3851
3781
|
const a = document.querySelector('[data-sidebar-tab-count="agents"]');
|
|
3852
3782
|
if (a) a.textContent = String(agentsCount);
|
|
3853
|
-
const sum = document.querySelector("[data-sidebar-summary]");
|
|
3854
|
-
if (sum) sum.textContent = `${liveCount} LIVE / ${agentsCount} AGENTS`;
|
|
3855
3783
|
},
|
|
3856
3784
|
|
|
3857
3785
|
// ── Rendering · main view ─────────────────────────────────
|
|
@@ -3886,7 +3814,8 @@
|
|
|
3886
3814
|
if (existingBanner) existingBanner.remove();
|
|
3887
3815
|
const parent = this.currentParentRef;
|
|
3888
3816
|
if (chat && parent && parent.id) {
|
|
3889
|
-
|
|
3817
|
+
// System UI · always English (banner chrome on the follow-up parent link).
|
|
3818
|
+
const labelText = "// following up";
|
|
3890
3819
|
const subject = (parent.subject || "(no subject)").trim();
|
|
3891
3820
|
const banner = document.createElement("a");
|
|
3892
3821
|
banner.href = "#";
|
|
@@ -3906,9 +3835,8 @@
|
|
|
3906
3835
|
if (existingChildren) existingChildren.remove();
|
|
3907
3836
|
const kids = Array.isArray(this.currentFollowUps) ? this.currentFollowUps : [];
|
|
3908
3837
|
if (briefCard && kids.length > 0) {
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
: `Follow-up rooms · ${kids.length}`;
|
|
3838
|
+
// System UI · always English (sidebar / brief-card chrome head).
|
|
3839
|
+
const headLabel = `Follow-up rooms · ${kids.length}`;
|
|
3912
3840
|
const block = document.createElement("div");
|
|
3913
3841
|
block.className = "followup-children";
|
|
3914
3842
|
block.innerHTML = [
|
|
@@ -4031,36 +3959,23 @@
|
|
|
4031
3959
|
const briefCard = document.querySelector("[data-brief-card]");
|
|
4032
3960
|
if (!briefCard) return;
|
|
4033
3961
|
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
head: "// session analytics",
|
|
4052
|
-
stamp: "closed",
|
|
4053
|
-
tokens: "tokens",
|
|
4054
|
-
messages: "msgs",
|
|
4055
|
-
rounds: "rounds",
|
|
4056
|
-
minutes: "min",
|
|
4057
|
-
modelHead: "Model usage",
|
|
4058
|
-
valueHead: "What you valued",
|
|
4059
|
-
valueEmpty: "No ▲ key-point votes, probes, or seconds in this session.",
|
|
4060
|
-
voted: "▲ voted",
|
|
4061
|
-
seconded: "★ seconded",
|
|
4062
|
-
probed: "✎ probed",
|
|
4063
|
-
};
|
|
3962
|
+
// System UI · always English. Session-analytics tile chrome
|
|
3963
|
+
// (banner, metric labels, section heads, chip text) is part of
|
|
3964
|
+
// the app shell and doesn't follow the brief language.
|
|
3965
|
+
const t = {
|
|
3966
|
+
head: "// session analytics",
|
|
3967
|
+
stamp: "closed",
|
|
3968
|
+
tokens: "tokens",
|
|
3969
|
+
messages: "msgs",
|
|
3970
|
+
rounds: "rounds",
|
|
3971
|
+
minutes: "min",
|
|
3972
|
+
modelHead: "Model usage",
|
|
3973
|
+
valueHead: "What you valued",
|
|
3974
|
+
valueEmpty: "No ▲ key-point votes, probes, or seconds in this session.",
|
|
3975
|
+
voted: "▲ voted",
|
|
3976
|
+
seconded: "★ seconded",
|
|
3977
|
+
probed: "✎ probed",
|
|
3978
|
+
};
|
|
4064
3979
|
|
|
4065
3980
|
const fmtTokens = (n) => {
|
|
4066
3981
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
@@ -4164,8 +4079,10 @@
|
|
|
4164
4079
|
// analytics tile; capping keeps the section as a tight strip
|
|
4165
4080
|
// and lets the user opt in.
|
|
4166
4081
|
const VALUE_PREVIEW_CAP = 2;
|
|
4167
|
-
|
|
4168
|
-
|
|
4082
|
+
// System UI · always English (the surrounding tile content can
|
|
4083
|
+
// still localize via `t`, but toggle chrome is fixed-string).
|
|
4084
|
+
const moreLabel = (n) => `[ + show ${n} more ]`;
|
|
4085
|
+
const lessLabel = "[ collapse ]";
|
|
4169
4086
|
const upvotedHtml = stats.upvotedPoints.length > 0
|
|
4170
4087
|
? (() => {
|
|
4171
4088
|
const items = stats.upvotedPoints.map((p, i) => {
|
|
@@ -4281,12 +4198,13 @@
|
|
|
4281
4198
|
? this.escape(nextSpeaker.handle.replace(/^\//, ""))
|
|
4282
4199
|
: "";
|
|
4283
4200
|
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
const
|
|
4287
|
-
const
|
|
4288
|
-
const
|
|
4289
|
-
const
|
|
4201
|
+
// System UI · always English. App chrome (paused bar, action
|
|
4202
|
+
// buttons, status labels) doesn't follow the brief language.
|
|
4203
|
+
const addInputLabel = "[ + Add input ]";
|
|
4204
|
+
const adjournLabel = "[ ▸ Adjourn & File Brief ]";
|
|
4205
|
+
const resumeLabel = "[ ▶ Resume Discussion ]";
|
|
4206
|
+
const pausedLabel = "paused";
|
|
4207
|
+
const nextLabel = "next";
|
|
4290
4208
|
const nextChunk = nextHandle
|
|
4291
4209
|
? ` · ${nextLabel} → <span class="lime">${nextHandle}</span>`
|
|
4292
4210
|
: "";
|
|
@@ -4445,6 +4363,12 @@
|
|
|
4445
4363
|
try { this._reportsLoadObserver.disconnect(); } catch { /* noop */ }
|
|
4446
4364
|
this._reportsLoadObserver = null;
|
|
4447
4365
|
}
|
|
4366
|
+
// The floating sidebar-expand button is gated on `html.no-room`.
|
|
4367
|
+
// Without setting it here, a user who collapses the sidebar
|
|
4368
|
+
// while on All Reports loses access to the expand control —
|
|
4369
|
+
// they have to navigate back to a room view to recover. Same
|
|
4370
|
+
// reasoning for openAllNotes / openAgentProfile.
|
|
4371
|
+
document.documentElement.classList.add("no-room");
|
|
4448
4372
|
// If we're inside a room or on the agent profile, leave them.
|
|
4449
4373
|
if (this.currentRoomId) {
|
|
4450
4374
|
this.disconnectSSE?.();
|
|
@@ -4650,32 +4574,14 @@
|
|
|
4650
4574
|
`;
|
|
4651
4575
|
};
|
|
4652
4576
|
|
|
4653
|
-
//
|
|
4654
|
-
//
|
|
4655
|
-
//
|
|
4656
|
-
//
|
|
4657
|
-
//
|
|
4658
|
-
const groups = [];
|
|
4659
|
-
const yesterdayStart = todayStart - 86400_000;
|
|
4660
|
-
let currentGroup = null;
|
|
4661
|
-
const groupLabelFor = (ts) => {
|
|
4662
|
-
if (ts >= todayStart) return "Today";
|
|
4663
|
-
if (ts >= yesterdayStart) return "Yesterday";
|
|
4664
|
-
if (ts >= weekStart) return "This week";
|
|
4665
|
-
return "Earlier";
|
|
4666
|
-
};
|
|
4667
|
-
for (const b of visibleFiltered) {
|
|
4668
|
-
const label = groupLabelFor(b.createdAt);
|
|
4669
|
-
if (!currentGroup || currentGroup.label !== label) {
|
|
4670
|
-
currentGroup = { label, items: [] };
|
|
4671
|
-
groups.push(currentGroup);
|
|
4672
|
-
}
|
|
4673
|
-
currentGroup.items.push(b);
|
|
4674
|
-
}
|
|
4675
|
-
|
|
4577
|
+
// Flat list under the filter chips · earlier iterations grouped
|
|
4578
|
+
// by Today / Yesterday / This week / Earlier, but the user
|
|
4579
|
+
// wanted a single divider followed by the items. The filter
|
|
4580
|
+
// chips above already disclose the recency dimension; redundant
|
|
4581
|
+
// group headers were just visual chrome.
|
|
4676
4582
|
const filterLabels = { all: "the archive", today: "Today", week: "This week", earlier: "Earlier" };
|
|
4677
4583
|
const filterCopyTitle = filterLabels[activeFilter] || "this window";
|
|
4678
|
-
const groupsHtml =
|
|
4584
|
+
const groupsHtml = visibleFiltered.length === 0
|
|
4679
4585
|
? `
|
|
4680
4586
|
<div class="reports-list-empty">
|
|
4681
4587
|
<!-- Notice text · explains the empty window. -->
|
|
@@ -4700,14 +4606,11 @@
|
|
|
4700
4606
|
` : ""}
|
|
4701
4607
|
</div>
|
|
4702
4608
|
`
|
|
4703
|
-
:
|
|
4704
|
-
<
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
</ul>
|
|
4709
|
-
</div>
|
|
4710
|
-
`).join("");
|
|
4609
|
+
: `
|
|
4610
|
+
<ul class="reports-list">
|
|
4611
|
+
${visibleFiltered.map((b) => this.renderReportItemHtml(b)).join("")}
|
|
4612
|
+
</ul>
|
|
4613
|
+
`;
|
|
4711
4614
|
|
|
4712
4615
|
// Bottom sentinel · IntersectionObserver target. Renders only
|
|
4713
4616
|
// when there's more to load. The "+ N more" hint doubles as a
|
|
@@ -4862,6 +4765,9 @@
|
|
|
4862
4765
|
* Pulls the live list and renders three time-bucket sections
|
|
4863
4766
|
* (Today / This Week / Earlier). */
|
|
4864
4767
|
async openAllNotes() {
|
|
4768
|
+
// Set the no-room flag for the same reason openAllReports does
|
|
4769
|
+
// — the floating sidebar-expand button is gated on this class.
|
|
4770
|
+
document.documentElement.classList.add("no-room");
|
|
4865
4771
|
// Same view-leaving routine as openAllReports.
|
|
4866
4772
|
if (this.currentRoomId) {
|
|
4867
4773
|
this.disconnectSSE?.();
|
|
@@ -4990,9 +4896,30 @@
|
|
|
4990
4896
|
<div class="notes-empty-mark">○</div>
|
|
4991
4897
|
<div class="notes-empty-title">no saved notes yet</div>
|
|
4992
4898
|
<div class="notes-empty-deck">
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4899
|
+
Capture a director's line as a note:
|
|
4900
|
+
</div>
|
|
4901
|
+
<ol class="notes-empty-steps">
|
|
4902
|
+
<li>
|
|
4903
|
+
<span class="notes-empty-step-num">1</span>
|
|
4904
|
+
<span class="notes-empty-step-text">
|
|
4905
|
+
<strong>Open a room.</strong> Any director's reply can be saved.
|
|
4906
|
+
</span>
|
|
4907
|
+
</li>
|
|
4908
|
+
<li>
|
|
4909
|
+
<span class="notes-empty-step-num">2</span>
|
|
4910
|
+
<span class="notes-empty-step-text">
|
|
4911
|
+
<strong>Select a passage.</strong> Drag-highlight the sentence or paragraph you want to keep.
|
|
4912
|
+
</span>
|
|
4913
|
+
</li>
|
|
4914
|
+
<li>
|
|
4915
|
+
<span class="notes-empty-step-num">3</span>
|
|
4916
|
+
<span class="notes-empty-step-text">
|
|
4917
|
+
<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.
|
|
4918
|
+
</span>
|
|
4919
|
+
</li>
|
|
4920
|
+
</ol>
|
|
4921
|
+
<div class="notes-empty-foot">
|
|
4922
|
+
Each saved excerpt links back to the original room + speaker, so you can jump back any time.
|
|
4996
4923
|
</div>
|
|
4997
4924
|
</div>
|
|
4998
4925
|
`;
|
|
@@ -5007,27 +4934,12 @@
|
|
|
5007
4934
|
return true;
|
|
5008
4935
|
});
|
|
5009
4936
|
|
|
5010
|
-
//
|
|
5011
|
-
//
|
|
5012
|
-
//
|
|
5013
|
-
|
|
5014
|
-
if (ts >= todayStart) return "Today";
|
|
5015
|
-
if (ts >= weekStart) return "This week";
|
|
5016
|
-
return "Earlier";
|
|
5017
|
-
};
|
|
5018
|
-
const groups = [];
|
|
5019
|
-
let currentGroup = null;
|
|
5020
|
-
for (const n of filtered) {
|
|
5021
|
-
const label = groupLabelFor(n.createdAt || 0);
|
|
5022
|
-
if (!currentGroup || currentGroup.label !== label) {
|
|
5023
|
-
currentGroup = { label, items: [] };
|
|
5024
|
-
groups.push(currentGroup);
|
|
5025
|
-
}
|
|
5026
|
-
currentGroup.items.push(n);
|
|
5027
|
-
}
|
|
5028
|
-
|
|
4937
|
+
// Flat list under the filter chips. Earlier iterations grouped
|
|
4938
|
+
// by Today / This week / Earlier headers; the user wanted a
|
|
4939
|
+
// single divider followed by the items, since the chips above
|
|
4940
|
+
// already disclose the recency dimension.
|
|
5029
4941
|
const filterLabels = { all: "the archive", today: "Today", week: "This week", earlier: "Earlier" };
|
|
5030
|
-
const groupsHtml =
|
|
4942
|
+
const groupsHtml = filtered.length === 0
|
|
5031
4943
|
? `
|
|
5032
4944
|
<div class="notes-list-empty">
|
|
5033
4945
|
<div class="notes-empty-mark">○</div>
|
|
@@ -5041,17 +4953,11 @@
|
|
|
5041
4953
|
` : ""}
|
|
5042
4954
|
</div>
|
|
5043
4955
|
`
|
|
5044
|
-
:
|
|
5045
|
-
<
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
</div>
|
|
5050
|
-
<ul class="notes-list">
|
|
5051
|
-
${g.items.map((n) => this.renderNoteItemHtml(n)).join("")}
|
|
5052
|
-
</ul>
|
|
5053
|
-
</section>
|
|
5054
|
-
`).join("");
|
|
4956
|
+
: `
|
|
4957
|
+
<ul class="notes-list">
|
|
4958
|
+
${filtered.map((n) => this.renderNoteItemHtml(n)).join("")}
|
|
4959
|
+
</ul>
|
|
4960
|
+
`;
|
|
5055
4961
|
|
|
5056
4962
|
const totalLabel = `${total} ${total === 1 ? "note" : "notes"}`;
|
|
5057
4963
|
const roomLabel = distinctRooms > 0
|
|
@@ -5091,9 +4997,12 @@
|
|
|
5091
4997
|
const author = n.authorName || "Director";
|
|
5092
4998
|
// The jump link uses the room's hash route + the note id as a
|
|
5093
4999
|
// fragment-style query so openRoom can scroll to + flash the
|
|
5094
|
-
// matching span
|
|
5095
|
-
//
|
|
5000
|
+
// matching span. The delete button sits as a SIBLING of the
|
|
5001
|
+
// anchor (inside the .notes-item) so its click never bubbles
|
|
5002
|
+
// through the navigation link — same pattern as session-row
|
|
5003
|
+
// delete in the rooms sidebar.
|
|
5096
5004
|
const href = `#/r/${this.escape(n.roomId)}?note=${this.escape(n.id)}`;
|
|
5005
|
+
const deleteTitle = "Delete this note";
|
|
5097
5006
|
return `
|
|
5098
5007
|
<li class="notes-item" data-note-id="${this.escape(n.id)}">
|
|
5099
5008
|
<a class="notes-item-link" href="${href}" data-note-jump="${this.escape(n.id)}" data-note-room="${this.escape(n.roomId)}">
|
|
@@ -5110,10 +5019,39 @@
|
|
|
5110
5019
|
n.contextAfter ? `<span class="note-context note-context-after">${this.escape(n.contextAfter)}</span>` : ""
|
|
5111
5020
|
}</p>
|
|
5112
5021
|
</a>
|
|
5022
|
+
<button type="button" class="notes-item-delete" data-note-delete title="${this.escape(deleteTitle)}" aria-label="${this.escape(deleteTitle)}">✕</button>
|
|
5113
5023
|
</li>
|
|
5114
5024
|
`;
|
|
5115
5025
|
},
|
|
5116
5026
|
|
|
5027
|
+
/** DELETE /api/notes/:id · drops a saved excerpt from the index.
|
|
5028
|
+
* Confirmation is light because the action is reversible only by
|
|
5029
|
+
* re-saving the same passage; we keep the prompt as a single
|
|
5030
|
+
* click-confirm rather than a full overlay. */
|
|
5031
|
+
async deleteNoteAt(id) {
|
|
5032
|
+
if (!id) return;
|
|
5033
|
+
if (!confirm("Delete this note? You can save it again from the room.")) return;
|
|
5034
|
+
try {
|
|
5035
|
+
const r = await fetch("/api/notes/" + encodeURIComponent(id), { method: "DELETE" });
|
|
5036
|
+
if (!r.ok) {
|
|
5037
|
+
const j = await r.json().catch(() => ({}));
|
|
5038
|
+
alert("Delete failed: " + (j.error || r.statusText));
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
} catch (e) {
|
|
5042
|
+
alert("Delete failed: " + (e && e.message ? e.message : e));
|
|
5043
|
+
return;
|
|
5044
|
+
}
|
|
5045
|
+
// Patch local cache so the immediate re-render reflects the
|
|
5046
|
+
// delete without a refetch round-trip. The sidebar count badge
|
|
5047
|
+
// re-fetches via the /count endpoint.
|
|
5048
|
+
if (Array.isArray(this._notesCache)) {
|
|
5049
|
+
this._notesCache = this._notesCache.filter((n) => n && n.id !== id);
|
|
5050
|
+
this.renderNotesPage(this._notesCache);
|
|
5051
|
+
}
|
|
5052
|
+
this.refreshNotesCount();
|
|
5053
|
+
},
|
|
5054
|
+
|
|
5117
5055
|
/** Refresh the sidebar count badge · called on boot, after
|
|
5118
5056
|
* every successful save (note:created event), and after a
|
|
5119
5057
|
* delete. Hits /api/notes/count which is cheap (one COUNT query),
|
|
@@ -5535,31 +5473,23 @@
|
|
|
5535
5473
|
const userName = (this.prefs?.name || "you").trim() || "you";
|
|
5536
5474
|
const lang = this.composerLanguage();
|
|
5537
5475
|
const greeting = this.composerGreeting(lang, userName);
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
:
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
convene: "Convene",
|
|
5556
|
-
tuneLabel: "tune",
|
|
5557
|
-
starterLabel: "starter",
|
|
5558
|
-
starterCaption: "or try a starter",
|
|
5559
|
-
pickerLabel: "Pick directors",
|
|
5560
|
-
directorsLabel: (n) => `${n} director${n === 1 ? "" : "s"}`,
|
|
5561
|
-
directorsAdd: "add",
|
|
5562
|
-
};
|
|
5476
|
+
// System UI · always English. Composer chrome (prompt /
|
|
5477
|
+
// placeholder / button labels) doesn't follow the brief
|
|
5478
|
+
// language. The brief content + chair/director output still
|
|
5479
|
+
// honours the user's input language; only the app shell is
|
|
5480
|
+
// fixed-string.
|
|
5481
|
+
const t = {
|
|
5482
|
+
greet: greeting,
|
|
5483
|
+
prompt: "What's on your mind today?",
|
|
5484
|
+
placeholder: "an idea you're not sure about · a decision you keep avoiding · a thesis you want stress-tested",
|
|
5485
|
+
convene: "Convene",
|
|
5486
|
+
tuneLabel: "tune",
|
|
5487
|
+
starterLabel: "starter",
|
|
5488
|
+
starterCaption: "or try a starter",
|
|
5489
|
+
pickerLabel: "Pick directors",
|
|
5490
|
+
directorsLabel: (n) => `${n} director${n === 1 ? "" : "s"}`,
|
|
5491
|
+
directorsAdd: "add",
|
|
5492
|
+
};
|
|
5563
5493
|
|
|
5564
5494
|
// Cast slot · two visual modes:
|
|
5565
5495
|
// 1. Auto-pick (default · empty manual selection): chip
|
|
@@ -5578,16 +5508,15 @@
|
|
|
5578
5508
|
// Label uses "directors" (not "cast") so the chip's purpose
|
|
5579
5509
|
// is unambiguous: it's the slot that picks the boardroom's
|
|
5580
5510
|
// director agents. Tooltip carries the long-form explanation.
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
: "Chair picks 3 directors based on your subject when you Convene · click to pick manually";
|
|
5511
|
+
// System UI · always English (auto-pick chip tooltip).
|
|
5512
|
+
const autoTip = "Chair picks 3 directors based on your subject when you Convene · click to pick manually";
|
|
5584
5513
|
castInner = `
|
|
5585
5514
|
<span class="cmp-cast-stack cmp-cast-stack-auto" data-cast-auto title="${this.escape(autoTip)}">
|
|
5586
5515
|
<span class="cmp-cast-auto-mark">✦</span>
|
|
5587
5516
|
</span>
|
|
5588
5517
|
<span class="cmp-cast-count cmp-cast-auto-label" title="${this.escape(autoTip)}">
|
|
5589
|
-
<span class="cmp-cast-auto-key"
|
|
5590
|
-
<span class="cmp-cast-auto-val"
|
|
5518
|
+
<span class="cmp-cast-auto-key">directors</span>
|
|
5519
|
+
<span class="cmp-cast-auto-val">auto-pick</span>
|
|
5591
5520
|
</span>
|
|
5592
5521
|
`;
|
|
5593
5522
|
} else {
|
|
@@ -5598,7 +5527,7 @@
|
|
|
5598
5527
|
`).join("");
|
|
5599
5528
|
const dirCount = dirObjs.length
|
|
5600
5529
|
? `<span class="cmp-cast-count">${this.escape(t.directorsLabel(dirObjs.length))}</span>`
|
|
5601
|
-
: `<span class="cmp-cast-count cmp-cast-empty"
|
|
5530
|
+
: `<span class="cmp-cast-count cmp-cast-empty">no directors</span>`;
|
|
5602
5531
|
castInner = `
|
|
5603
5532
|
<span class="cmp-cast-stack">
|
|
5604
5533
|
${dirAvatars}
|
|
@@ -5612,8 +5541,9 @@
|
|
|
5612
5541
|
// Tune dropdowns · two trigger buttons that open option popovers
|
|
5613
5542
|
// on click. Discoverable (label + value + chevron pattern is
|
|
5614
5543
|
// unmistakeably a select) without taking up visual real estate.
|
|
5615
|
-
|
|
5616
|
-
const
|
|
5544
|
+
// System UI · always English (tune dropdown labels).
|
|
5545
|
+
const toneLbl = "tone";
|
|
5546
|
+
const intensityLbl = "intensity";
|
|
5617
5547
|
|
|
5618
5548
|
// Starter grid · 2-col responsive cards.
|
|
5619
5549
|
const starters = Array.isArray(window.BOARDROOM_STARTERS) ? window.BOARDROOM_STARTERS : [];
|
|
@@ -5679,12 +5609,10 @@
|
|
|
5679
5609
|
},
|
|
5680
5610
|
|
|
5681
5611
|
/** Time-of-day greeting like "// good evening, Kay" / "// 晚上好,Kay". */
|
|
5682
|
-
composerGreeting(
|
|
5612
|
+
composerGreeting(_lang, name) {
|
|
5613
|
+
// System UI · always English. Greeting is part of the composer
|
|
5614
|
+
// chrome; the brief language doesn't change the app's voice.
|
|
5683
5615
|
const h = new Date().getHours();
|
|
5684
|
-
if (lang === "zh") {
|
|
5685
|
-
const part = h < 5 ? "凌晨好" : h < 12 ? "早上好" : h < 14 ? "中午好" : h < 18 ? "下午好" : h < 23 ? "晚上好" : "夜深了";
|
|
5686
|
-
return `// ${part},${name}`;
|
|
5687
|
-
}
|
|
5688
5616
|
const part = h < 5 ? "Up late" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
|
|
5689
5617
|
return `// ${part}, ${name}`;
|
|
5690
5618
|
},
|
|
@@ -5851,7 +5779,7 @@
|
|
|
5851
5779
|
* user can edit before submitting. */
|
|
5852
5780
|
applyAgentStarter(idx) {
|
|
5853
5781
|
const lang = this.composerLanguage();
|
|
5854
|
-
const list =
|
|
5782
|
+
const list = this.AGENT_STARTERS_EN;
|
|
5855
5783
|
const item = list[idx];
|
|
5856
5784
|
if (!item) return;
|
|
5857
5785
|
const ta = document.querySelector("[data-agent-composer-desc]");
|
|
@@ -5878,39 +5806,27 @@
|
|
|
5878
5806
|
{ key: "voice", label: "Picking the model voice", startSec: 28, sub: ["matching depth to role"] },
|
|
5879
5807
|
{ key: "polish", label: "Polishing", startSec: 31, sub: ["clamping lengths", "final tightening"] },
|
|
5880
5808
|
],
|
|
5881
|
-
AGENT_GEN_STAGES_ZH_BASE: [
|
|
5882
|
-
{ key: "lineage", label: "勾画智识画像", startSec: 0, sub: ["梳理思想脉络", "标记反对的传统", "挑出具体引用"] },
|
|
5883
|
-
{ key: "imagine", label: "构思角色", startSec: 8, sub: ["勾画语气", "拟定立场", "确定视角"] },
|
|
5884
|
-
{ key: "name", label: "起名 + handle", startSec: 11, sub: ["试几个短名", "排查 handle 重名"] },
|
|
5885
|
-
{ key: "bio", label: "起草 bio", startSec: 14, sub: ["一两句话", "点明方法"] },
|
|
5886
|
-
{ key: "quote", label: "写一句开场问", startSec: 17, sub: ["这位董事每次会议会先问什么"] },
|
|
5887
|
-
{ key: "instruction", label: "撰写 instruction", startSec: 20, sub: ["脉络 + 概念", "method + 引用集", "语气 + 边界", "失效模式"] },
|
|
5888
|
-
{ key: "voice", label: "挑选模型嗓音", startSec: 28, sub: ["按角色深度匹配 model"] },
|
|
5889
|
-
{ key: "polish", label: "收尾打磨", startSec: 31, sub: ["长度修剪", "最后一遍"] },
|
|
5890
|
-
],
|
|
5891
5809
|
/** Web-search prefix · prepended to the base list when the user
|
|
5892
5810
|
* opted into web search this run. Adds ~5s to the perceived
|
|
5893
|
-
* pipeline; downstream startSecs are shifted in agentGenStagesFor.
|
|
5811
|
+
* pipeline; downstream startSecs are shifted in agentGenStagesFor.
|
|
5812
|
+
* System UI · English-only. */
|
|
5894
5813
|
AGENT_GEN_STAGES_WS_EN: { key: "search", label: "Searching the web for context", startSec: 0, sub: ["refining the query", "scanning Brave results", "distilling 5–6 named sources"] },
|
|
5895
|
-
AGENT_GEN_STAGES_WS_ZH: { key: "search", label: "联网检索领域上下文", startSec: 0, sub: ["精炼查询", "扫描 Brave 结果", "提炼 5–6 条具名来源"] },
|
|
5896
5814
|
|
|
5897
5815
|
/** Build the active stage list for THIS generation. When web search
|
|
5898
|
-
* is on, prepend the search stage and shift everything else later.
|
|
5899
|
-
|
|
5900
|
-
|
|
5816
|
+
* is on, prepend the search stage and shift everything else later.
|
|
5817
|
+
* System UI · English-only regardless of `lang`. */
|
|
5818
|
+
agentGenStagesFor(_lang) {
|
|
5819
|
+
const base = this.AGENT_GEN_STAGES_EN_BASE;
|
|
5901
5820
|
const useWs = !!this._agentGenUsingWebSearch;
|
|
5902
5821
|
if (!useWs) return base;
|
|
5903
|
-
const wsEntry = lang === "zh" ? this.AGENT_GEN_STAGES_WS_ZH : this.AGENT_GEN_STAGES_WS_EN;
|
|
5904
5822
|
const SHIFT = 5;
|
|
5905
5823
|
const shifted = base.map((s) => ({ ...s, startSec: s.startSec + SHIFT }));
|
|
5906
|
-
return [
|
|
5824
|
+
return [this.AGENT_GEN_STAGES_WS_EN, ...shifted];
|
|
5907
5825
|
},
|
|
5908
5826
|
|
|
5909
|
-
/** Back-compat shims · existing references read these names directly.
|
|
5910
|
-
* Resolved at call-time so the web-search variant is picked when
|
|
5911
|
-
* the flag is set. */
|
|
5827
|
+
/** Back-compat shims · existing references read these names directly. */
|
|
5912
5828
|
get AGENT_GEN_STAGES_EN() { return this.agentGenStagesFor("en"); },
|
|
5913
|
-
get AGENT_GEN_STAGES_ZH() { return this.agentGenStagesFor("
|
|
5829
|
+
get AGENT_GEN_STAGES_ZH() { return this.agentGenStagesFor("en"); },
|
|
5914
5830
|
|
|
5915
5831
|
/** Start the stage tick. Called when /generate-spec request fires.
|
|
5916
5832
|
* Idempotent — calling twice is safe. */
|
|
@@ -5925,8 +5841,7 @@
|
|
|
5925
5841
|
return;
|
|
5926
5842
|
}
|
|
5927
5843
|
const elapsed = (Date.now() - this.agentGenStartedAt) / 1000;
|
|
5928
|
-
const
|
|
5929
|
-
const stages = lang === "zh" ? this.AGENT_GEN_STAGES_ZH : this.AGENT_GEN_STAGES_EN;
|
|
5844
|
+
const stages = this.AGENT_GEN_STAGES_EN;
|
|
5930
5845
|
// Find current stage index by elapsed time.
|
|
5931
5846
|
let idx = 0;
|
|
5932
5847
|
for (let i = 0; i < stages.length; i++) {
|
|
@@ -5957,12 +5872,13 @@
|
|
|
5957
5872
|
},
|
|
5958
5873
|
|
|
5959
5874
|
renderAgentGenStagesInner() {
|
|
5960
|
-
|
|
5961
|
-
|
|
5875
|
+
// System UI · always English. Agent-generation stage panel is
|
|
5876
|
+
// app chrome around the LLM call.
|
|
5877
|
+
const stages = this.AGENT_GEN_STAGES_EN;
|
|
5962
5878
|
const active = this.agentGenStageIndex;
|
|
5963
5879
|
const elapsed = Math.max(0, (Date.now() - this.agentGenStartedAt) / 1000);
|
|
5964
|
-
const elapsedLabel =
|
|
5965
|
-
const headerLabel =
|
|
5880
|
+
const elapsedLabel = `${Math.round(elapsed)} s elapsed`;
|
|
5881
|
+
const headerLabel = "Summoning director";
|
|
5966
5882
|
const sigilSvg = this.renderAgentGenSigilSvg(stages, active, elapsed);
|
|
5967
5883
|
// The active stage's headline pulse — lifted out from the list so
|
|
5968
5884
|
// it reads as the focal "what's happening RIGHT NOW" line.
|
|
@@ -5980,7 +5896,7 @@
|
|
|
5980
5896
|
<div class="ag-gen-stage-area">
|
|
5981
5897
|
<div class="ag-gen-sigil" aria-hidden="true">${sigilSvg}</div>
|
|
5982
5898
|
<div class="ag-gen-active-block">
|
|
5983
|
-
<div class="ag-gen-active-kicker">${this.escape(
|
|
5899
|
+
<div class="ag-gen-active-kicker">${this.escape(`step ${active + 1} of ${stages.length}`)}</div>
|
|
5984
5900
|
<div class="ag-gen-active-label">${this.escape(activeStage ? activeStage.label : "")}</div>
|
|
5985
5901
|
${activeSubText ? `<div class="ag-gen-active-sub">${this.escape(activeSubText)}</div>` : ""}
|
|
5986
5902
|
</div>
|
|
@@ -6096,42 +6012,28 @@
|
|
|
6096
6012
|
{ tag: "critique-reviewer", text: "A senior critic who audits any deliverable systematically — labels each flaw blocker / major / minor, points at the load-bearing piece, names the mechanism. Won't praise without finding at least one major issue." },
|
|
6097
6013
|
{ tag: "phenomenologist", text: "An observer who notices what the room ISN'T saying. Tracks tone, what got skipped, who agreed too fast." },
|
|
6098
6014
|
],
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
{ tag: "critique-reviewer", text: "一位资深评审,对任何交付物做系统性审稿——每个瑕疵打 blocker / major / minor 严重度,指向具体段落、说出失败机制。不挑出至少一条 major 不会放过。" },
|
|
6105
|
-
{ tag: "phenomenologist", text: "一位观察者,捕捉房间里没说出来的东西:语气、被跳过的话题、太快达成的一致。" },
|
|
6106
|
-
],
|
|
6015
|
+
/** Legacy ZH list · kept as an alias of EN so any external caller
|
|
6016
|
+
* reading the property still resolves. System UI is English-only
|
|
6017
|
+
* per the global rule (the brief language doesn't change app
|
|
6018
|
+
* chrome). New code should reference AGENT_STARTERS_EN directly. */
|
|
6019
|
+
get AGENT_STARTERS_ZH() { return this.AGENT_STARTERS_EN; },
|
|
6107
6020
|
|
|
6108
6021
|
renderAgentComposerHtml() {
|
|
6109
6022
|
const userName = (this.prefs?.name || "you").trim() || "you";
|
|
6110
6023
|
const lang = this.composerLanguage();
|
|
6111
6024
|
const greeting = this.composerGreeting(lang, userName);
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
: {
|
|
6125
|
-
greet: greeting,
|
|
6126
|
-
prompt: "What kind of director do you want?",
|
|
6127
|
-
placeholder: "A few sentences on their role, method, stance. e.g. A seasoned product hand who reasons from the user's moment of friction. Will reject any argument that doesn't name what the user is doing right then.",
|
|
6128
|
-
cta: "Generate",
|
|
6129
|
-
ctaHint: "AI drafts the full spec — you'll edit before saving",
|
|
6130
|
-
manual: "Configure manually",
|
|
6131
|
-
generating: "Generating…",
|
|
6132
|
-
modelLabel: "model",
|
|
6133
|
-
starterCaption: "or start from an archetype",
|
|
6134
|
-
};
|
|
6025
|
+
// System UI · always English (new-agent composer chrome).
|
|
6026
|
+
const t = {
|
|
6027
|
+
greet: greeting,
|
|
6028
|
+
prompt: "What kind of director do you want?",
|
|
6029
|
+
placeholder: "A few sentences on their role, method, stance. e.g. A seasoned product hand who reasons from the user's moment of friction. Will reject any argument that doesn't name what the user is doing right then.",
|
|
6030
|
+
cta: "Generate",
|
|
6031
|
+
ctaHint: "AI drafts the full spec — you'll edit before saving",
|
|
6032
|
+
manual: "Configure manually",
|
|
6033
|
+
generating: "Generating…",
|
|
6034
|
+
modelLabel: "model",
|
|
6035
|
+
starterCaption: "or start from an archetype",
|
|
6036
|
+
};
|
|
6135
6037
|
// If we already have a spec preview, render that instead of the input.
|
|
6136
6038
|
if (this.agentSpec) {
|
|
6137
6039
|
return this.renderAgentSpecPreviewHtml(this.agentSpec, lang);
|
|
@@ -6145,7 +6047,7 @@
|
|
|
6145
6047
|
const generating = this.agentSpecGenerating;
|
|
6146
6048
|
const currentModel = this.loadAgentComposerModel();
|
|
6147
6049
|
const modelDisplay = MODEL_LABELS[currentModel] || currentModel;
|
|
6148
|
-
const starters =
|
|
6050
|
+
const starters = this.AGENT_STARTERS_EN;
|
|
6149
6051
|
const starterCards = starters.map((q, idx) => `
|
|
6150
6052
|
<button type="button" class="cmp-starter" data-agent-starter="${idx}">
|
|
6151
6053
|
<div class="cmp-starter-tag">${this.escape(q.tag)}</div>
|
|
@@ -6181,23 +6083,14 @@
|
|
|
6181
6083
|
// skill row at agent-profile.js:1411.
|
|
6182
6084
|
const configured = this.agentComposerBraveConfigured();
|
|
6183
6085
|
const on = configured && this.loadAgentComposerWebSearch();
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
: on
|
|
6187
|
-
? (lang === "zh" ? "已开启" : "enabled")
|
|
6188
|
-
: (lang === "zh" ? "已关闭" : "disabled");
|
|
6086
|
+
// System UI · always English (web-search toggle chrome).
|
|
6087
|
+
const stateLabel = !configured ? "needs key" : on ? "enabled" : "disabled";
|
|
6189
6088
|
const titleText = !configured
|
|
6190
|
-
?
|
|
6191
|
-
? "联网搜索需要 Brave Search API key · 点击配置"
|
|
6192
|
-
: "Web search needs a Brave Search API key · click to configure")
|
|
6089
|
+
? "Web search needs a Brave Search API key · click to configure"
|
|
6193
6090
|
: on
|
|
6194
|
-
?
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
: (lang === "zh"
|
|
6198
|
-
? "生成时不联网 · 点击开启"
|
|
6199
|
-
: "Generation runs offline · click to enable web search");
|
|
6200
|
-
const wsLabel = lang === "zh" ? "联网搜索" : "web search";
|
|
6091
|
+
? "Search the web for real domain references during generation · click to disable"
|
|
6092
|
+
: "Generation runs offline · click to enable web search";
|
|
6093
|
+
const wsLabel = "web search";
|
|
6201
6094
|
const cls = [
|
|
6202
6095
|
"ap-skill-row-toggle",
|
|
6203
6096
|
"cmp-ws-toggle",
|
|
@@ -6243,38 +6136,23 @@
|
|
|
6243
6136
|
},
|
|
6244
6137
|
|
|
6245
6138
|
/** Preview card · all generated fields editable inline. */
|
|
6246
|
-
renderAgentSpecPreviewHtml(spec,
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
: {
|
|
6264
|
-
kicker: "// generated director · edit and save",
|
|
6265
|
-
avatar: "Avatar",
|
|
6266
|
-
reroll: "Reroll",
|
|
6267
|
-
name: "Name",
|
|
6268
|
-
handle: "Handle",
|
|
6269
|
-
role: "Role tag",
|
|
6270
|
-
bio: "Bio",
|
|
6271
|
-
quote: "Cover quote",
|
|
6272
|
-
instruction: "Instruction",
|
|
6273
|
-
model: "Model",
|
|
6274
|
-
save: "Save director",
|
|
6275
|
-
discard: "Discard",
|
|
6276
|
-
redo: "Regenerate",
|
|
6277
|
-
};
|
|
6139
|
+
renderAgentSpecPreviewHtml(spec, _lang) {
|
|
6140
|
+
// System UI · always English (agent-spec preview card chrome).
|
|
6141
|
+
const t = {
|
|
6142
|
+
kicker: "// generated director · edit and save",
|
|
6143
|
+
avatar: "Avatar",
|
|
6144
|
+
reroll: "Reroll",
|
|
6145
|
+
name: "Name",
|
|
6146
|
+
handle: "Handle",
|
|
6147
|
+
role: "Role tag",
|
|
6148
|
+
bio: "Bio",
|
|
6149
|
+
quote: "Cover quote",
|
|
6150
|
+
instruction: "Instruction",
|
|
6151
|
+
model: "Model",
|
|
6152
|
+
save: "Save director",
|
|
6153
|
+
discard: "Discard",
|
|
6154
|
+
redo: "Regenerate",
|
|
6155
|
+
};
|
|
6278
6156
|
const seed = this.agentSpecAvatarSeed;
|
|
6279
6157
|
const avatarSvg = (window.AvatarSkill && seed)
|
|
6280
6158
|
? window.AvatarSkill.generate(seed, { size: 96 })
|
|
@@ -6361,25 +6239,16 @@
|
|
|
6361
6239
|
* composer back to its input state). The description text is
|
|
6362
6240
|
* surfaced read-only so the user can copy it before discard. */
|
|
6363
6241
|
renderAgentSpecErrorHtml(err, lang) {
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
: {
|
|
6375
|
-
kicker: err.kind === "timeout" ? "// generation timed out" : "// generation failed",
|
|
6376
|
-
title: err.kind === "timeout" ? "Generation didn't complete after 5 minutes" : "Generation failed",
|
|
6377
|
-
hintTimeout: "The model may be slow, the network flaky, or the backend pipeline stalled. Click retry to start a fresh run.",
|
|
6378
|
-
hintFailed: "Check your API key configuration and model reachability. Retry often clears transient failures.",
|
|
6379
|
-
descLabel: "Your description (re-used on retry)",
|
|
6380
|
-
retry: "Retry",
|
|
6381
|
-
discard: "Discard",
|
|
6382
|
-
};
|
|
6242
|
+
// System UI · always English (agent-spec error card chrome).
|
|
6243
|
+
const t = {
|
|
6244
|
+
kicker: err.kind === "timeout" ? "// generation timed out" : "// generation failed",
|
|
6245
|
+
title: err.kind === "timeout" ? "Generation didn't complete after 5 minutes" : "Generation failed",
|
|
6246
|
+
hintTimeout: "The model may be slow, the network flaky, or the backend pipeline stalled. Click retry to start a fresh run.",
|
|
6247
|
+
hintFailed: "Check your API key configuration and model reachability. Retry often clears transient failures.",
|
|
6248
|
+
descLabel: "Your description (re-used on retry)",
|
|
6249
|
+
retry: "Retry",
|
|
6250
|
+
discard: "Discard",
|
|
6251
|
+
};
|
|
6383
6252
|
const desc = this._agentComposerLastDesc || "";
|
|
6384
6253
|
const hint = err.kind === "timeout" ? t.hintTimeout : t.hintFailed;
|
|
6385
6254
|
const detail = err.message ? `<div class="ag-gen-error-detail">${this.escape(err.message)}</div>` : "";
|
|
@@ -6387,7 +6256,7 @@
|
|
|
6387
6256
|
<section class="cmp ag-cmp">
|
|
6388
6257
|
<header class="cmp-hero">
|
|
6389
6258
|
<div class="cmp-greet">${this.escape(this.composerGreeting(lang, (this.prefs?.name || "you").trim() || "you"))}</div>
|
|
6390
|
-
<h1 class="cmp-prompt">${this.escape(
|
|
6259
|
+
<h1 class="cmp-prompt">${this.escape("What kind of director do you want?")}</h1>
|
|
6391
6260
|
</header>
|
|
6392
6261
|
<div class="ag-gen-error-card">
|
|
6393
6262
|
<div class="ag-gen-error-kicker">${this.escape(t.kicker)}</div>
|
|
@@ -6552,13 +6421,11 @@
|
|
|
6552
6421
|
this.agentSpecError = null;
|
|
6553
6422
|
} catch (e) {
|
|
6554
6423
|
const isAbort = (e && (e.name === "AbortError" || /aborted/i.test(String(e.message))));
|
|
6555
|
-
const lang = this.composerLanguage();
|
|
6556
6424
|
if (timedOut || isAbort) {
|
|
6425
|
+
// System UI · always English (error message text).
|
|
6557
6426
|
this.agentSpecError = {
|
|
6558
6427
|
kind: "timeout",
|
|
6559
|
-
message:
|
|
6560
|
-
? "生成超过 5 分钟仍未完成 · 模型回应慢、网络波动,或后端流水线卡住了。"
|
|
6561
|
-
: "Generation took longer than 5 minutes · the model may be slow, the network flaky, or the backend stuck.",
|
|
6428
|
+
message: "Generation took longer than 5 minutes · the model may be slow, the network flaky, or the backend stuck.",
|
|
6562
6429
|
};
|
|
6563
6430
|
} else {
|
|
6564
6431
|
this.agentSpecError = {
|
|
@@ -6722,10 +6589,9 @@
|
|
|
6722
6589
|
// Sort newest-first · the most recently filed brief is the one a
|
|
6723
6590
|
// returning user most likely wants to re-open.
|
|
6724
6591
|
briefs.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
: { title: "Open a report", supplementPrefix: "Supplement: ", initial: "Initial", filed: "filed" };
|
|
6592
|
+
// System UI · always English. Brief picker chrome (popover
|
|
6593
|
+
// title, row labels) doesn't follow the brief language.
|
|
6594
|
+
const t = { title: "Open a report", supplementPrefix: "Supplement: ", initial: "Initial", filed: "filed" };
|
|
6729
6595
|
const initialIdx = briefs.length - 1; // oldest is "Initial"
|
|
6730
6596
|
const rows = briefs.map((b, i) => {
|
|
6731
6597
|
// The numbering is stable across renders: oldest = 01, newest
|
|
@@ -6740,7 +6606,7 @@
|
|
|
6740
6606
|
: "";
|
|
6741
6607
|
const subtitle = isInitial ? t.initial : (supplementSnippet ? `${t.supplementPrefix}${supplementSnippet}` : "");
|
|
6742
6608
|
const filedLabel = b.createdAt
|
|
6743
|
-
? new Date(b.createdAt).toLocaleString(
|
|
6609
|
+
? new Date(b.createdAt).toLocaleString(undefined, {
|
|
6744
6610
|
year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
6745
6611
|
})
|
|
6746
6612
|
: "";
|
|
@@ -6834,10 +6700,8 @@
|
|
|
6834
6700
|
const dirs = (this.agents || [])
|
|
6835
6701
|
.filter((a) => a.roleKind !== "moderator")
|
|
6836
6702
|
.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
6837
|
-
|
|
6838
|
-
const t =
|
|
6839
|
-
? { title: "选择董事", hint: "建议 2-4 位", done: "完成", info: "查看资料" }
|
|
6840
|
-
: { title: "Pick directors", hint: "2-4 recommended", done: "Done", info: "View profile" };
|
|
6703
|
+
// System UI · always English (composer director picker chrome).
|
|
6704
|
+
const t = { title: "Pick directors", hint: "2-4 recommended", done: "Done", info: "View profile" };
|
|
6841
6705
|
const rows = dirs.map((a) => {
|
|
6842
6706
|
const checked = state.directorIds.includes(a.id);
|
|
6843
6707
|
const modelLabel = MODEL_LABELS[a.modelV] || a.modelV || "";
|
|
@@ -6974,17 +6838,16 @@
|
|
|
6974
6838
|
const isAutoPick = state.autoPickDirectors === true && dirObjs.length === 0;
|
|
6975
6839
|
btn.classList.toggle("cmp-cast-btn-auto", isAutoPick);
|
|
6976
6840
|
if (isAutoPick) {
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
: "Chair picks 3 directors based on your subject when you Convene · click to pick manually";
|
|
6841
|
+
// System UI · always English (auto-pick chip tooltip).
|
|
6842
|
+
const autoTip = "Chair picks 3 directors based on your subject when you Convene · click to pick manually";
|
|
6980
6843
|
btn.title = autoTip;
|
|
6981
6844
|
btn.innerHTML = `
|
|
6982
6845
|
<span class="cmp-cast-stack cmp-cast-stack-auto" data-cast-auto>
|
|
6983
6846
|
<span class="cmp-cast-auto-mark">✦</span>
|
|
6984
6847
|
</span>
|
|
6985
6848
|
<span class="cmp-cast-count cmp-cast-auto-label">
|
|
6986
|
-
<span class="cmp-cast-auto-key"
|
|
6987
|
-
<span class="cmp-cast-auto-val"
|
|
6849
|
+
<span class="cmp-cast-auto-key">directors</span>
|
|
6850
|
+
<span class="cmp-cast-auto-val">auto-pick</span>
|
|
6988
6851
|
</span>
|
|
6989
6852
|
`;
|
|
6990
6853
|
return;
|
|
@@ -6992,9 +6855,10 @@
|
|
|
6992
6855
|
const visible = dirObjs.slice(0, 4);
|
|
6993
6856
|
const overflow = Math.max(0, dirObjs.length - 4);
|
|
6994
6857
|
const avs = visible.map((a) => `<img class="cmp-cast-av" src="${this.escape(a.avatarPath)}" alt="${this.escape(a.name)}" title="${this.escape(a.name)}">`).join("");
|
|
6858
|
+
// System UI · always English (cast button count chrome).
|
|
6995
6859
|
const countText = dirObjs.length
|
|
6996
|
-
?
|
|
6997
|
-
:
|
|
6860
|
+
? `${dirObjs.length} director${dirObjs.length === 1 ? "" : "s"}`
|
|
6861
|
+
: "no directors";
|
|
6998
6862
|
const countCls = dirObjs.length ? "cmp-cast-count" : "cmp-cast-count cmp-cast-empty";
|
|
6999
6863
|
btn.innerHTML = `
|
|
7000
6864
|
<span class="cmp-cast-stack">
|
|
@@ -7042,21 +6906,14 @@
|
|
|
7042
6906
|
let opts;
|
|
7043
6907
|
let current;
|
|
7044
6908
|
if (kind === "tone") {
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
: [
|
|
7054
|
-
{ v: "brainstorm", label: "Brainstorm", hint: "yes-and" },
|
|
7055
|
-
{ v: "constructive", label: "Constructive", hint: "push & sharpen" },
|
|
7056
|
-
{ v: "research", label: "Research", hint: "mine the material" },
|
|
7057
|
-
{ v: "debate", label: "Debate", hint: "find the holes" },
|
|
7058
|
-
{ v: "critique", label: "Critique", hint: "audit the deliverable" },
|
|
7059
|
-
];
|
|
6909
|
+
// System UI · always English (tune dropdown options).
|
|
6910
|
+
opts = [
|
|
6911
|
+
{ v: "brainstorm", label: "Brainstorm", hint: "yes-and" },
|
|
6912
|
+
{ v: "constructive", label: "Constructive", hint: "push & sharpen" },
|
|
6913
|
+
{ v: "research", label: "Research", hint: "mine the material" },
|
|
6914
|
+
{ v: "debate", label: "Debate", hint: "find the holes" },
|
|
6915
|
+
{ v: "critique", label: "Critique", hint: "audit the deliverable" },
|
|
6916
|
+
];
|
|
7060
6917
|
if (followUpScope) {
|
|
7061
6918
|
const valSpan = triggerBtn.querySelector("[data-cmp-dd-value]");
|
|
7062
6919
|
current = valSpan ? (valSpan.textContent || "").trim().toLowerCase() : "";
|
|
@@ -7068,17 +6925,12 @@
|
|
|
7068
6925
|
// the third value (terse) is a cadence dial, not a harshness
|
|
7069
6926
|
// dial. The earlier "no prisoners" / "直击痛点" copy pulled
|
|
7070
6927
|
// users into thinking it controlled tone.
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
: [
|
|
7078
|
-
{ v: "calm", label: "Calm", hint: "let them think" },
|
|
7079
|
-
{ v: "sharp", label: "Sharp", hint: "no hedging" },
|
|
7080
|
-
{ v: "terse", label: "Terse", hint: "telegraphic" },
|
|
7081
|
-
];
|
|
6928
|
+
// System UI · always English (intensity dropdown options).
|
|
6929
|
+
opts = [
|
|
6930
|
+
{ v: "calm", label: "Calm", hint: "let them think" },
|
|
6931
|
+
{ v: "sharp", label: "Sharp", hint: "no hedging" },
|
|
6932
|
+
{ v: "terse", label: "Terse", hint: "telegraphic" },
|
|
6933
|
+
];
|
|
7082
6934
|
if (followUpScope) {
|
|
7083
6935
|
const valSpan = triggerBtn.querySelector("[data-cmp-dd-value]");
|
|
7084
6936
|
current = valSpan ? (valSpan.textContent || "").trim().toLowerCase() : "";
|
|
@@ -7221,7 +7073,7 @@
|
|
|
7221
7073
|
const useAutoPick = state.autoPickDirectors === true && state.directorIds.length === 0;
|
|
7222
7074
|
if (!useAutoPick && !state.directorIds.length) {
|
|
7223
7075
|
const lang = this.composerLanguage();
|
|
7224
|
-
alert(
|
|
7076
|
+
alert("Pick at least one director before convening");
|
|
7225
7077
|
return;
|
|
7226
7078
|
}
|
|
7227
7079
|
const btn = document.querySelector("[data-composer-go]");
|
|
@@ -7432,10 +7284,10 @@
|
|
|
7432
7284
|
if (!multi) {
|
|
7433
7285
|
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report>[ View Report ]</a>`;
|
|
7434
7286
|
}
|
|
7435
|
-
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report data-view-report-trigger title="${this.escape(
|
|
7287
|
+
return `<a href="${directHref}" target="_blank" rel="noopener" class="view-report-btn" data-view-report data-view-report-trigger title="${this.escape(`${briefs.length} reports · click to choose`)}">[ View Report <span class="vr-count">· ${briefs.length}</span> ▾ ]</a>`;
|
|
7436
7288
|
})()
|
|
7437
7289
|
: (r.status === "adjourned"
|
|
7438
|
-
? `<a href="#" class="view-report-btn generate-report" data-generate-brief title="${this.escape(
|
|
7290
|
+
? `<a href="#" class="view-report-btn generate-report" data-generate-brief title="${this.escape("File a brief from this session")}"><span class="vr-mark">▸</span> Generate Report</a>`
|
|
7439
7291
|
: "")}
|
|
7440
7292
|
</div>
|
|
7441
7293
|
`;
|
|
@@ -7516,23 +7368,17 @@
|
|
|
7516
7368
|
conveningCardHtml() {
|
|
7517
7369
|
const s = this.conveneState;
|
|
7518
7370
|
if (!s) return "";
|
|
7519
|
-
|
|
7520
|
-
|
|
7371
|
+
// System UI · always English. Convening overlay (analyzing /
|
|
7372
|
+
// seating / preparing labels + decks) is app chrome.
|
|
7521
7373
|
// Stages · auto-picked rooms run all three; manually-cast rooms
|
|
7522
7374
|
// skip "analyzing" + "seating" because the cast is pre-set.
|
|
7523
7375
|
const stageOrder = s.autoPicked
|
|
7524
7376
|
? ["analyzing", "seating", "preparing"]
|
|
7525
7377
|
: ["preparing"];
|
|
7526
7378
|
const STAGE_LABELS = {
|
|
7527
|
-
analyzing:
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
seating: lang === "zh"
|
|
7531
|
-
? { title: "邀请董事", deck: "依据议题匹配的董事正在入席" }
|
|
7532
|
-
: { title: "Seating directors", deck: "Picking the right perspectives for this question" },
|
|
7533
|
-
preparing: lang === "zh"
|
|
7534
|
-
? { title: "主席组织开场陈词", deck: "主席正在准备介绍发言" }
|
|
7535
|
-
: { title: "Chair preparing remarks", deck: "Drafting the convening speech" },
|
|
7379
|
+
analyzing: { title: "Analyzing topic", deck: "Routing your topic to the right perspectives" },
|
|
7380
|
+
seating: { title: "Seating directors", deck: "Picking the right perspectives for this question" },
|
|
7381
|
+
preparing: { title: "Chair preparing remarks", deck: "Drafting the convening speech" },
|
|
7536
7382
|
};
|
|
7537
7383
|
|
|
7538
7384
|
const currentIdx = stageOrder.indexOf(s.stage);
|
|
@@ -7564,7 +7410,7 @@
|
|
|
7564
7410
|
// already in currentMembers; rendering it here would be redundant).
|
|
7565
7411
|
const seatedRow = (s.autoPicked && s.seated.length > 0) ? `
|
|
7566
7412
|
<div class="conv-seated">
|
|
7567
|
-
<div class="conv-seated-label"
|
|
7413
|
+
<div class="conv-seated-label">seated · ${s.seated.length}</div>
|
|
7568
7414
|
<div class="conv-seated-list">
|
|
7569
7415
|
${s.seated.map((a) => `
|
|
7570
7416
|
<div class="conv-seated-item" data-agent-id="${this.escape(a.id)}">
|
|
@@ -7581,7 +7427,7 @@
|
|
|
7581
7427
|
|
|
7582
7428
|
return `
|
|
7583
7429
|
<article class="convening-card" data-convene-card>
|
|
7584
|
-
<div class="conv-eyebrow"
|
|
7430
|
+
<div class="conv-eyebrow">▸ CONVENING</div>
|
|
7585
7431
|
${s.subject ? `<blockquote class="conv-subject">${this.escape(s.subject)}</blockquote>` : ""}
|
|
7586
7432
|
<ul class="conv-stages">${stagesHtml}</ul>
|
|
7587
7433
|
${seatedRow}
|
|
@@ -7618,11 +7464,9 @@
|
|
|
7618
7464
|
const chat = document.querySelector("[data-chat-messages]");
|
|
7619
7465
|
if (!chat) return;
|
|
7620
7466
|
const existing = chat.querySelector("[data-chair-pending]");
|
|
7621
|
-
|
|
7622
|
-
const chairName = (this.currentChair?.name) ||
|
|
7623
|
-
const labelMap =
|
|
7624
|
-
? { clarify: "正在整理你的问题", "chair-direct": "正在准备回应", "round-end": "正在收束这一轮", convening: "正在召集会议", "next-speaker": "正在判断接下来的发言", chair: "正在准备" }
|
|
7625
|
-
: { clarify: "is reading your question", "chair-direct": "is preparing a response", "round-end": "is wrapping up the round", convening: "is convening the room", "next-speaker": "is reading the room", chair: "is preparing" };
|
|
7467
|
+
// System UI · always English (chair-pending placeholder chrome).
|
|
7468
|
+
const chairName = (this.currentChair?.name) || "Chair";
|
|
7469
|
+
const labelMap = { clarify: "is reading your question", "chair-direct": "is preparing a response", "round-end": "is wrapping up the round", convening: "is convening the room", "next-speaker": "is reading the room", chair: "is preparing" };
|
|
7626
7470
|
const phraseRaw = labelMap[phase] || labelMap.chair;
|
|
7627
7471
|
const phrase = `${chairName} ${phraseRaw}…`;
|
|
7628
7472
|
if (existing) {
|
|
@@ -7635,7 +7479,7 @@
|
|
|
7635
7479
|
node.setAttribute("data-chair-pending", "");
|
|
7636
7480
|
node.innerHTML = `
|
|
7637
7481
|
<div class="cp-rule" aria-hidden="true"></div>
|
|
7638
|
-
<div class="cp-kicker">▸
|
|
7482
|
+
<div class="cp-kicker">▸ chair</div>
|
|
7639
7483
|
<div class="cp-body">
|
|
7640
7484
|
<span class="cp-text">${this.escape(phrase)}</span>
|
|
7641
7485
|
<span class="thinking-dots"><span></span><span></span><span></span></span>
|
|
@@ -7817,7 +7661,7 @@
|
|
|
7817
7661
|
// 中文 copy. Reads from the chair's prompt body which lands in
|
|
7818
7662
|
// the message body when a recommendation is present.
|
|
7819
7663
|
const lang = (promptMsg && promptMsg.body && /[一-鿿]/.test(promptMsg.body)) ? "zh" : "en";
|
|
7820
|
-
const recLabel =
|
|
7664
|
+
const recLabel = "chair recommends";
|
|
7821
7665
|
const recIndicator = recKind
|
|
7822
7666
|
? `
|
|
7823
7667
|
<div class="rp-rec-line rp-rec-line-${this.escape(recKind)}">
|
|
@@ -8045,14 +7889,14 @@
|
|
|
8045
7889
|
const truncated = parentSubject.length > 70
|
|
8046
7890
|
? parentSubject.slice(0, 70) + "…"
|
|
8047
7891
|
: parentSubject;
|
|
8048
|
-
|
|
8049
|
-
const label =
|
|
8050
|
-
const roomTag =
|
|
7892
|
+
// System UI · always English (follow-up origin link chrome).
|
|
7893
|
+
const label = "Following up on";
|
|
7894
|
+
const roomTag = "Room #";
|
|
8051
7895
|
const subjectChunk = truncated
|
|
8052
7896
|
? `<span class="convene-origin-sep">·</span><span class="convene-origin-subject">${this.escape(truncated)}</span>`
|
|
8053
7897
|
: "";
|
|
8054
7898
|
originHtml = `
|
|
8055
|
-
<a class="convene-origin" href="#/r/${this.escape(parentId)}" data-parent-room-id="${this.escape(parentId)}" title="${this.escape(
|
|
7899
|
+
<a class="convene-origin" href="#/r/${this.escape(parentId)}" data-parent-room-id="${this.escape(parentId)}" title="${this.escape("Open the prior session · " + (parentSubject || parentId))}">
|
|
8056
7900
|
<span class="convene-origin-arrow">↩</span>
|
|
8057
7901
|
<span class="convene-origin-label">${this.escape(label)}</span>
|
|
8058
7902
|
<span class="convene-origin-room">${this.escape(roomTag)}${this.escape(String(parentNum))}</span>
|
|
@@ -8065,9 +7909,11 @@
|
|
|
8065
7909
|
parentId ? "convene-opener-followup" : "",
|
|
8066
7910
|
isLongOpener ? "convene-opener-clamped" : "",
|
|
8067
7911
|
].filter(Boolean).join(" ");
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
7912
|
+
// System UI · always English. Earlier this followed the brief
|
|
7913
|
+
// language; now fixed-string per the rule that app chrome stays
|
|
7914
|
+
// English regardless of the user's query language.
|
|
7915
|
+
const moreLabel = "Show more ↓";
|
|
7916
|
+
const lessLabel = "Show less ↑";
|
|
8071
7917
|
const toggleHtml = isLongOpener
|
|
8072
7918
|
? `<button type="button" class="convene-toggle" data-convene-toggle data-more="${this.escape(moreLabel)}" data-less="${this.escape(lessLabel)}">${moreLabel}</button>`
|
|
8073
7919
|
: "";
|
|
@@ -8293,7 +8139,7 @@
|
|
|
8293
8139
|
// as a historical marker only.
|
|
8294
8140
|
if (isChair && metaKind === "no-brief") {
|
|
8295
8141
|
const ts = this.timeFmt(m.createdAt);
|
|
8296
|
-
|
|
8142
|
+
// System UI · always English (no-brief card chrome).
|
|
8297
8143
|
const hasBrief = !!this.currentBrief;
|
|
8298
8144
|
const cta = hasBrief
|
|
8299
8145
|
? ""
|
|
@@ -8301,7 +8147,7 @@
|
|
|
8301
8147
|
<div class="nb-actions">
|
|
8302
8148
|
<button type="button" class="nb-cta" data-generate-brief>
|
|
8303
8149
|
<span class="nb-cta-mark">▸</span>
|
|
8304
|
-
<span class="nb-cta-text"
|
|
8150
|
+
<span class="nb-cta-text">Generate report now</span>
|
|
8305
8151
|
</button>
|
|
8306
8152
|
</div>
|
|
8307
8153
|
`;
|
|
@@ -8312,7 +8158,7 @@
|
|
|
8312
8158
|
<span class="nb-eyebrow">adjourned · no brief filed</span>
|
|
8313
8159
|
</span>
|
|
8314
8160
|
<div class="nb-body">
|
|
8315
|
-
<strong>${this.escape(this.prefs?.name || "The chair")}</strong>
|
|
8161
|
+
<strong>${this.escape(this.prefs?.name || "The chair")}</strong> declared no report is needed for this session.
|
|
8316
8162
|
</div>
|
|
8317
8163
|
${cta}
|
|
8318
8164
|
<div class="nb-meta">${this.escape(ts)}</div>
|
|
@@ -8763,31 +8609,17 @@
|
|
|
8763
8609
|
* research note reads "sweet." */
|
|
8764
8610
|
_briefWordCountTip(wc) {
|
|
8765
8611
|
if (!wc) return "";
|
|
8766
|
-
|
|
8767
|
-
const toneLabel =
|
|
8768
|
-
brainstorm: isZh ? "脑暴" : "brainstorm",
|
|
8769
|
-
constructive: isZh ? "构建" : "constructive",
|
|
8770
|
-
debate: isZh ? "辩论" : "debate",
|
|
8771
|
-
research: isZh ? "研究" : "research",
|
|
8772
|
-
critique: isZh ? "评审" : "critique",
|
|
8773
|
-
})[wc.tone] || wc.tone;
|
|
8612
|
+
// System UI · always English (word-count chip tooltip).
|
|
8613
|
+
const toneLabel = wc.tone;
|
|
8774
8614
|
const range = `${wc.bands.sweetLo.toLocaleString("en-US")}-${wc.bands.sweetHi.toLocaleString("en-US")}`;
|
|
8775
|
-
const unit =
|
|
8776
|
-
const copy =
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
}
|
|
8784
|
-
: {
|
|
8785
|
-
"thin": `Lean · ${toneLabel} sweet zone is ${range} ${unit}`,
|
|
8786
|
-
"sweet": `Sweet zone for ${toneLabel} · most read-through-able length`,
|
|
8787
|
-
"dense": `Dense · past the ${toneLabel} sweet zone, still readable`,
|
|
8788
|
-
"long": `Long · approaching "filed instead of read"`,
|
|
8789
|
-
"too-long": `Too long · likely to be skimmed, not read`,
|
|
8790
|
-
};
|
|
8615
|
+
const unit = "words";
|
|
8616
|
+
const copy = {
|
|
8617
|
+
"thin": `Lean · ${toneLabel} sweet zone is ${range} ${unit}`,
|
|
8618
|
+
"sweet": `Sweet zone for ${toneLabel} · most read-through-able length`,
|
|
8619
|
+
"dense": `Dense · past the ${toneLabel} sweet zone, still readable`,
|
|
8620
|
+
"long": `Long · approaching "filed instead of read"`,
|
|
8621
|
+
"too-long": `Too long · likely to be skimmed, not read`,
|
|
8622
|
+
};
|
|
8791
8623
|
return copy[wc.tier] || "";
|
|
8792
8624
|
},
|
|
8793
8625
|
|
|
@@ -8800,9 +8632,7 @@
|
|
|
8800
8632
|
const briefs = Array.isArray(this.currentBriefs) ? this.currentBriefs : [];
|
|
8801
8633
|
if (briefs.length < 2) return "";
|
|
8802
8634
|
const sortedBriefs = briefs.slice().sort((x, y) => (x.createdAt || 0) - (y.createdAt || 0));
|
|
8803
|
-
|
|
8804
|
-
|| (this.currentRoom?.subject && /[一-鿿]/.test(this.currentRoom.subject))
|
|
8805
|
-
? "zh" : "en";
|
|
8635
|
+
// System UI · always English (brief-version tab strip chrome).
|
|
8806
8636
|
return `
|
|
8807
8637
|
<div class="brief-versions">
|
|
8808
8638
|
${sortedBriefs.map((bf, i) => {
|
|
@@ -8811,11 +8641,11 @@
|
|
|
8811
8641
|
const isInitial = i === 0;
|
|
8812
8642
|
const supp = bf.supplement && bf.supplement.trim()
|
|
8813
8643
|
? bf.supplement.trim()
|
|
8814
|
-
: (isInitial ?
|
|
8644
|
+
: (isInitial ? "Initial" : "");
|
|
8815
8645
|
const tooltip = isInitial
|
|
8816
|
-
?
|
|
8817
|
-
:
|
|
8818
|
-
const closeTitle =
|
|
8646
|
+
? `Initial brief · generated from the session`
|
|
8647
|
+
: `Supplement: ${supp || "—"}`;
|
|
8648
|
+
const closeTitle = "Delete this report";
|
|
8819
8649
|
// Errored / interrupted / timed-out tabs get a small
|
|
8820
8650
|
// visual marker so the user can spot which one needs
|
|
8821
8651
|
// attention without entering it. The full retry UI is
|
|
@@ -8829,7 +8659,7 @@
|
|
|
8829
8659
|
<span class="brief-version-num">${num}</span>
|
|
8830
8660
|
${stateMark}
|
|
8831
8661
|
${isInitial
|
|
8832
|
-
? `<span class="brief-version-label"
|
|
8662
|
+
? `<span class="brief-version-label">Initial</span>`
|
|
8833
8663
|
: `<span class="brief-version-label">${this.escape((supp || "").slice(0, 20))}${(supp || "").length > 20 ? "…" : ""}</span>`}
|
|
8834
8664
|
</button>
|
|
8835
8665
|
<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>
|
|
@@ -8902,14 +8732,14 @@
|
|
|
8902
8732
|
finally { this.currentBrief = prevCurrent; }
|
|
8903
8733
|
// Prepend the compact retry banner to the rendered card so
|
|
8904
8734
|
// the existing report stays fully visible below it.
|
|
8905
|
-
|
|
8735
|
+
// System UI · always English (error banner chrome).
|
|
8906
8736
|
const detail = failed.timedOut
|
|
8907
|
-
?
|
|
8737
|
+
? "regeneration timed out"
|
|
8908
8738
|
: failed.interrupted
|
|
8909
|
-
?
|
|
8910
|
-
: (failed.error ||
|
|
8911
|
-
const cta =
|
|
8912
|
-
const dismiss =
|
|
8739
|
+
? "regeneration interrupted"
|
|
8740
|
+
: (failed.error || "regeneration failed");
|
|
8741
|
+
const cta = "Retry";
|
|
8742
|
+
const dismiss = "Dismiss";
|
|
8913
8743
|
card.insertAdjacentHTML("afterbegin", `
|
|
8914
8744
|
<div class="brief-retry-banner" data-brief-retry-banner data-failed-brief-id="${this.escape(failed.id)}">
|
|
8915
8745
|
<span class="brb-mark">⚠</span>
|
|
@@ -8923,54 +8753,30 @@
|
|
|
8923
8753
|
`);
|
|
8924
8754
|
return;
|
|
8925
8755
|
}
|
|
8926
|
-
|
|
8756
|
+
// System UI · always English (full error-card copy).
|
|
8927
8757
|
const copy = b.timedOut
|
|
8928
|
-
?
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
}
|
|
8936
|
-
: {
|
|
8937
|
-
stamp: "timed out",
|
|
8938
|
-
kicker: "// generation timed out",
|
|
8939
|
-
detail: "No completion signal after 8 minutes — the model may be slow, the connection dropped, or the pipeline stalled. Click below to start a fresh run.",
|
|
8940
|
-
hint: "",
|
|
8941
|
-
cta: "Retry",
|
|
8942
|
-
})
|
|
8758
|
+
? {
|
|
8759
|
+
stamp: "timed out",
|
|
8760
|
+
kicker: "// generation timed out",
|
|
8761
|
+
detail: "No completion signal after 8 minutes — the model may be slow, the connection dropped, or the pipeline stalled. Click below to start a fresh run.",
|
|
8762
|
+
hint: "",
|
|
8763
|
+
cta: "Retry",
|
|
8764
|
+
}
|
|
8943
8765
|
: b.interrupted
|
|
8944
|
-
?
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
})
|
|
8959
|
-
: (lang === "zh"
|
|
8960
|
-
? {
|
|
8961
|
-
stamp: "failed",
|
|
8962
|
-
kicker: "// 报告生成失败",
|
|
8963
|
-
detail: this.escape(b.error || ""),
|
|
8964
|
-
hint: "Brief writer 需要一个 LLM key(OpenRouter,或 Anthropic / OpenAI / Google / xAI 直连)。在 <strong>Preference → API Key</strong> 中添加后再试。",
|
|
8965
|
-
cta: "重试",
|
|
8966
|
-
}
|
|
8967
|
-
: {
|
|
8968
|
-
stamp: "failed",
|
|
8969
|
-
kicker: "// brief generation failed",
|
|
8970
|
-
detail: this.escape(b.error || ""),
|
|
8971
|
-
hint: "The brief writer needs an LLM key (OpenRouter, or a direct Anthropic / OpenAI / Google / xAI key). Add one in <strong>Preference → API Key</strong> and try again.",
|
|
8972
|
-
cta: "Retry",
|
|
8973
|
-
});
|
|
8766
|
+
? {
|
|
8767
|
+
stamp: "interrupted",
|
|
8768
|
+
kicker: "// generation interrupted",
|
|
8769
|
+
detail: "The previous generation was cut short — likely by a browser refresh or a server restart. Click below to start a fresh report.",
|
|
8770
|
+
hint: "",
|
|
8771
|
+
cta: "Regenerate report",
|
|
8772
|
+
}
|
|
8773
|
+
: {
|
|
8774
|
+
stamp: "failed",
|
|
8775
|
+
kicker: "// brief generation failed",
|
|
8776
|
+
detail: this.escape(b.error || ""),
|
|
8777
|
+
hint: "The brief writer needs an LLM key (OpenRouter, or a direct Anthropic / OpenAI / Google / xAI key). Add one in <strong>Preference → API Key</strong> and try again.",
|
|
8778
|
+
cta: "Retry",
|
|
8779
|
+
};
|
|
8974
8780
|
card.innerHTML = `
|
|
8975
8781
|
<div class="brief-card">
|
|
8976
8782
|
${tabsStripHtml}
|
|
@@ -9067,11 +8873,11 @@
|
|
|
9067
8873
|
<div class="brief-supplement-row">
|
|
9068
8874
|
<button type="button" class="brief-supplement-btn" data-brief-supplement>
|
|
9069
8875
|
<span class="brief-supplement-mark">+</span>
|
|
9070
|
-
<span class="brief-supplement-label"
|
|
8876
|
+
<span class="brief-supplement-label">Add a perspective · regenerate</span>
|
|
9071
8877
|
</button>
|
|
9072
|
-
<button type="button" class="brief-delete-btn" data-brief-delete data-brief-id="${this.escape(b.id)}" title="
|
|
8878
|
+
<button type="button" class="brief-delete-btn" data-brief-delete data-brief-id="${this.escape(b.id)}" title="Delete this report">
|
|
9073
8879
|
<span class="brief-delete-mark">⌫</span>
|
|
9074
|
-
<span class="brief-delete-label"
|
|
8880
|
+
<span class="brief-delete-label">Delete report</span>
|
|
9075
8881
|
</button>
|
|
9076
8882
|
</div>
|
|
9077
8883
|
` : ""}
|
|
@@ -9155,49 +8961,10 @@
|
|
|
9155
8961
|
"Polishing the final pass",
|
|
9156
8962
|
],
|
|
9157
8963
|
},
|
|
9158
|
-
zh
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
"压缩到每位董事 2-4 条关键信号",
|
|
9163
|
-
],
|
|
9164
|
-
compose: [
|
|
9165
|
-
"选定本份报告的 spine 风格",
|
|
9166
|
-
"决定要纳入哪些组件块",
|
|
9167
|
-
"估算密度与节奏",
|
|
9168
|
-
],
|
|
9169
|
-
"scaffold-anchor": [
|
|
9170
|
-
"读出 takeaway",
|
|
9171
|
-
"校准 confidence",
|
|
9172
|
-
"落定 working hypothesis",
|
|
9173
|
-
],
|
|
9174
|
-
"scaffold-findings": [
|
|
9175
|
-
"提炼 3 条 headline findings",
|
|
9176
|
-
"复核每条是否撑住 anchor",
|
|
9177
|
-
"如有勉强,3 → 2 收敛",
|
|
9178
|
-
],
|
|
9179
|
-
"scaffold-cluster": [
|
|
9180
|
-
"定位董事们达成共识的地方",
|
|
9181
|
-
"标记核心张力",
|
|
9182
|
-
"辨认未消化的立场",
|
|
9183
|
-
],
|
|
9184
|
-
"scaffold-actions": [
|
|
9185
|
-
"起草 recommendations",
|
|
9186
|
-
"推演 pre-mortem",
|
|
9187
|
-
"梳理会议长出的新问题",
|
|
9188
|
-
],
|
|
9189
|
-
write: [
|
|
9190
|
-
"撰写 Bottom Line",
|
|
9191
|
-
"撰写 Frame Shift 段落",
|
|
9192
|
-
"撰写 3 条 Headline Findings",
|
|
9193
|
-
"起草 Convergence 与 Divergence 段落",
|
|
9194
|
-
"撰写 Recommendations",
|
|
9195
|
-
"撰写 Pre-mortem",
|
|
9196
|
-
"梳理 New Questions",
|
|
9197
|
-
"起草 Strategic Planning Assumption",
|
|
9198
|
-
"通读润色,准备交稿",
|
|
9199
|
-
],
|
|
9200
|
-
},
|
|
8964
|
+
// System UI · always English. The `zh` key is kept as an alias
|
|
8965
|
+
// of `en` so any caller indexing BRIEF_SUBSTAGES["zh"] still
|
|
8966
|
+
// resolves; brief language no longer changes the substage copy.
|
|
8967
|
+
get zh() { return this.en; },
|
|
9201
8968
|
},
|
|
9202
8969
|
|
|
9203
8970
|
/** Set up a 1s tick that re-renders the brief stages while at least
|
|
@@ -9293,9 +9060,8 @@
|
|
|
9293
9060
|
// Hard ceiling · regardless of server state, flip the card to
|
|
9294
9061
|
// a timed-out error so the user always has a way out.
|
|
9295
9062
|
if (now - startedAt > this.BRIEF_HARD_TIMEOUT_MS) {
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
: "Brief generation timed out (no completion after 8 minutes).";
|
|
9063
|
+
// System UI · always English (timeout error message).
|
|
9064
|
+
b.error = "Brief generation timed out (no completion after 8 minutes).";
|
|
9299
9065
|
b.timedOut = true;
|
|
9300
9066
|
this.stopBriefStageTick();
|
|
9301
9067
|
this.stopBriefStallWatch();
|
|
@@ -9340,7 +9106,7 @@
|
|
|
9340
9106
|
write: { status: "pending", detail: "", progress: null, startedAt: null },
|
|
9341
9107
|
};
|
|
9342
9108
|
const lang = b.language === "zh" ? "zh" : "en";
|
|
9343
|
-
const chairName = b.chairName || this.currentChair?.name ||
|
|
9109
|
+
const chairName = b.chairName || this.currentChair?.name || "Chair";
|
|
9344
9110
|
|
|
9345
9111
|
const wordCount = b.bodyMd
|
|
9346
9112
|
? (b.bodyMd.trim().match(/\S+/g) || []).length
|
|
@@ -9355,25 +9121,18 @@
|
|
|
9355
9121
|
// Stage 2 streaming buffer (see runStage2 / SCAFFOLD_TRIGGERS in
|
|
9356
9122
|
// brief.ts), so each pip transition reflects a real moment in the
|
|
9357
9123
|
// model's output — not a synthetic timer.
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
:
|
|
9369
|
-
|
|
9370
|
-
{ key: "compose", label: "Picking the report shape", pipShort: "pick" },
|
|
9371
|
-
{ key: "scaffold-anchor", label: "Setting the anchor", pipShort: "anchor" },
|
|
9372
|
-
{ key: "scaffold-findings", label: "Sketching findings", pipShort: "find" },
|
|
9373
|
-
{ key: "scaffold-cluster", label: "Mapping consensus + dissent", pipShort: "split" },
|
|
9374
|
-
{ key: "scaffold-actions", label: "Drafting actions + risks", pipShort: "act" },
|
|
9375
|
-
{ key: "write", label: "Writing the report", pipShort: "write" },
|
|
9376
|
-
];
|
|
9124
|
+
// System UI · always English regardless of brief language.
|
|
9125
|
+
// The pipeline labels are the app's voice (chrome around the
|
|
9126
|
+
// generation), not the report content itself.
|
|
9127
|
+
const STAGE_DEFS = [
|
|
9128
|
+
{ key: "extract", label: "Reading what each director said", pipShort: "read" },
|
|
9129
|
+
{ key: "compose", label: "Picking the report shape", pipShort: "pick" },
|
|
9130
|
+
{ key: "scaffold-anchor", label: "Setting the anchor", pipShort: "anchor" },
|
|
9131
|
+
{ key: "scaffold-findings", label: "Sketching findings", pipShort: "find" },
|
|
9132
|
+
{ key: "scaffold-cluster", label: "Mapping consensus + dissent", pipShort: "split" },
|
|
9133
|
+
{ key: "scaffold-actions", label: "Drafting actions + risks", pipShort: "act" },
|
|
9134
|
+
{ key: "write", label: "Writing the report", pipShort: "write" },
|
|
9135
|
+
];
|
|
9377
9136
|
|
|
9378
9137
|
const meta = this.BRIEF_STAGE_META;
|
|
9379
9138
|
const substages = (this.BRIEF_SUBSTAGES[lang] || this.BRIEF_SUBSTAGES.en);
|
|
@@ -9437,12 +9196,12 @@
|
|
|
9437
9196
|
if (activeDef.key === "extract" && activeStage.progress?.total) {
|
|
9438
9197
|
const cur = activeStage.progress.current;
|
|
9439
9198
|
const tot = activeStage.progress.total;
|
|
9440
|
-
detailParts.push(
|
|
9199
|
+
detailParts.push(`${cur}/${tot} director${tot === 1 ? "" : "s"}`);
|
|
9441
9200
|
} else if (activeStage.detail) {
|
|
9442
9201
|
detailParts.push(activeStage.detail);
|
|
9443
9202
|
}
|
|
9444
9203
|
if (activeDef.key === "write" && activeStatus === "active" && wordCount > 0) {
|
|
9445
|
-
detailParts.push(
|
|
9204
|
+
detailParts.push(`${wordCount} word${wordCount === 1 ? "" : "s"}`);
|
|
9446
9205
|
}
|
|
9447
9206
|
const detailLine = detailParts.join(" · ");
|
|
9448
9207
|
|
|
@@ -9454,7 +9213,7 @@
|
|
|
9454
9213
|
? `${activeElapsed}s · ~${activeEta[0]}–${activeEta[1]}s`
|
|
9455
9214
|
: `~${activeEta[0]}–${activeEta[1]}s`;
|
|
9456
9215
|
} else {
|
|
9457
|
-
timing =
|
|
9216
|
+
timing = `${activeElapsed}s elapsed`;
|
|
9458
9217
|
}
|
|
9459
9218
|
}
|
|
9460
9219
|
|
|
@@ -9510,19 +9269,15 @@
|
|
|
9510
9269
|
return r === 0 ? `${m}m` : `${m}m ${r}s`;
|
|
9511
9270
|
};
|
|
9512
9271
|
const fmtRange = (lo, hi) => {
|
|
9513
|
-
if (hi < 60) return
|
|
9272
|
+
if (hi < 60) return `~${lo}–${hi}s`;
|
|
9514
9273
|
const loM = Math.max(1, Math.round(lo / 60));
|
|
9515
9274
|
const hiM = Math.max(loM, Math.round(hi / 60));
|
|
9516
|
-
return
|
|
9275
|
+
return `~${loM}-${hiM}m`;
|
|
9517
9276
|
};
|
|
9518
9277
|
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
const kickerCore = lang === "zh"
|
|
9524
|
-
? `// ${chairName} 正在整理纪要 · ${totalText}`
|
|
9525
|
-
: `// ${chairName} is preparing the minutes · ${totalText}`;
|
|
9278
|
+
// System UI · always English (brief-progress kicker copy).
|
|
9279
|
+
const totalText = `${fmtSec(totalElapsed)} elapsed · ${fmtRange(totalLo, totalHi)} left`;
|
|
9280
|
+
const kickerCore = `// ${chairName} is preparing the minutes · ${totalText}`;
|
|
9526
9281
|
|
|
9527
9282
|
const metaHtml = (detailLine || timing)
|
|
9528
9283
|
? `<span class="brief-active-meta">` +
|
|
@@ -9554,9 +9309,8 @@
|
|
|
9554
9309
|
this._briefSeenHarvestKeys = this._briefSeenHarvestKeys || {};
|
|
9555
9310
|
const seenH = this._briefSeenHarvestKeys[b.id] = this._briefSeenHarvestKeys[b.id] || new Set();
|
|
9556
9311
|
const harvest = Array.isArray(b.extractHarvest) ? b.extractHarvest : [];
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
: { claims: "claims", evidence: "evidence", tensions: "tensions", assumptions: "assumptions", risks: "risks", opportunities: "opportunities", actions: "actions", quotes: "quotes", openQuestions: "open-q" };
|
|
9312
|
+
// System UI · always English (signal-kind chip labels).
|
|
9313
|
+
const kindLabels = { claims: "claims", evidence: "evidence", tensions: "tensions", assumptions: "assumptions", risks: "risks", opportunities: "opportunities", actions: "actions", quotes: "quotes", openQuestions: "open-q" };
|
|
9560
9314
|
if (harvest.length) {
|
|
9561
9315
|
const chips = harvest.map((h) => {
|
|
9562
9316
|
const isFresh = !seenH.has(h.directorId);
|
|
@@ -9581,7 +9335,7 @@
|
|
|
9581
9335
|
// Live word count during write — large and visible since it's the
|
|
9582
9336
|
// most engaging signal during the long write stage.
|
|
9583
9337
|
if (writeActive && wordCount > 0) {
|
|
9584
|
-
const w =
|
|
9338
|
+
const w = `${wordCount} word${wordCount === 1 ? "" : "s"} · still writing`;
|
|
9585
9339
|
stats.push(`<span class="brief-stat-fact brief-stat-live">${this.escape(w)}</span>`);
|
|
9586
9340
|
}
|
|
9587
9341
|
const statsHtml = stats.length
|
|
@@ -9902,8 +9656,8 @@
|
|
|
9902
9656
|
genBriefBtn.setAttribute("data-pending", "1");
|
|
9903
9657
|
if ("disabled" in genBriefBtn) genBriefBtn.disabled = true;
|
|
9904
9658
|
|
|
9905
|
-
|
|
9906
|
-
const generatingText =
|
|
9659
|
+
// System UI · always English (generating-button chrome).
|
|
9660
|
+
const generatingText = "generating…";
|
|
9907
9661
|
const originalHtml = genBriefBtn.innerHTML;
|
|
9908
9662
|
|
|
9909
9663
|
// Swap to a "generating…" state. The two button shapes both
|
|
@@ -9926,7 +9680,7 @@
|
|
|
9926
9680
|
genBriefBtn.removeAttribute("data-pending");
|
|
9927
9681
|
if ("disabled" in genBriefBtn) genBriefBtn.disabled = false;
|
|
9928
9682
|
genBriefBtn.innerHTML = originalHtml;
|
|
9929
|
-
alert(
|
|
9683
|
+
alert("Brief generation failed: " + (err && err.message ? err.message : err));
|
|
9930
9684
|
});
|
|
9931
9685
|
return;
|
|
9932
9686
|
}
|
|
@@ -10185,11 +9939,9 @@
|
|
|
10185
9939
|
// map, which user-settings.js patches in place after every
|
|
10186
9940
|
// setProviderKey, so we get fresh truth here.
|
|
10187
9941
|
const configured = !!(app.agentComposerBraveConfigured && app.agentComposerBraveConfigured());
|
|
10188
|
-
const isZh = (app.composerLanguage && app.composerLanguage()) === "zh";
|
|
10189
9942
|
if (!configured) {
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
: "Web Search needs a Brave Search API key.\n\nBrave Search · ≈ $5 per 1000 queries · privacy-respecting\n\nOpen Preferences to paste your key now?");
|
|
9943
|
+
// System UI · always English (Brave key prompt confirm dialog).
|
|
9944
|
+
const ok = confirm("Web Search needs a Brave Search API key.\n\nBrave Search · ≈ $5 per 1000 queries · privacy-respecting\n\nOpen Preferences to paste your key now?");
|
|
10193
9945
|
if (ok && typeof window.openUserSettings === "function") {
|
|
10194
9946
|
window.openUserSettings({ section: "keys", focusProvider: "brave" });
|
|
10195
9947
|
}
|
|
@@ -10211,19 +9963,14 @@
|
|
|
10211
9963
|
wsToggle.setAttribute("aria-pressed", next ? "true" : "false");
|
|
10212
9964
|
const txt = wsToggle.querySelector(".ap-skill-row-toggle-text");
|
|
10213
9965
|
if (txt) {
|
|
10214
|
-
|
|
10215
|
-
const
|
|
10216
|
-
|
|
10217
|
-
: (isZh ? "已关闭" : "disabled");
|
|
9966
|
+
// System UI · always English (web-search toggle chrome).
|
|
9967
|
+
const wsLabel = "web search";
|
|
9968
|
+
const stateLabel = next ? "enabled" : "disabled";
|
|
10218
9969
|
txt.textContent = `${wsLabel} · ${stateLabel}`;
|
|
10219
9970
|
}
|
|
10220
9971
|
wsToggle.title = next
|
|
10221
|
-
?
|
|
10222
|
-
|
|
10223
|
-
: "Search the web for real domain references during generation · click to disable")
|
|
10224
|
-
: (isZh
|
|
10225
|
-
? "生成时不联网 · 点击开启"
|
|
10226
|
-
: "Generation runs offline · click to enable web search");
|
|
9972
|
+
? "Search the web for real domain references during generation · click to disable"
|
|
9973
|
+
: "Generation runs offline · click to enable web search";
|
|
10227
9974
|
return;
|
|
10228
9975
|
}
|
|
10229
9976
|
// ─── Agent spec preview · field actions
|
|
@@ -10359,6 +10106,20 @@
|
|
|
10359
10106
|
if (id) app.deleteRoom(id);
|
|
10360
10107
|
return;
|
|
10361
10108
|
}
|
|
10109
|
+
// Delete a saved note from the All Notes list. The button is a
|
|
10110
|
+
// sibling of the note's anchor (inside .notes-item) so its click
|
|
10111
|
+
// never bubbles through the navigation link — but we still
|
|
10112
|
+
// preventDefault + stopPropagation defensively to keep the row's
|
|
10113
|
+
// hover/focus state quiet.
|
|
10114
|
+
const noteDel = e.target.closest("[data-note-delete]");
|
|
10115
|
+
if (noteDel) {
|
|
10116
|
+
e.preventDefault();
|
|
10117
|
+
e.stopPropagation();
|
|
10118
|
+
const item = noteDel.closest("[data-note-id]");
|
|
10119
|
+
const id = item?.dataset.noteId;
|
|
10120
|
+
if (id) app.deleteNoteAt(id);
|
|
10121
|
+
return;
|
|
10122
|
+
}
|
|
10362
10123
|
// Delete a custom agent · this used to live as an inline X button
|
|
10363
10124
|
// on the sidebar row. It's been moved into the agent profile's
|
|
10364
10125
|
// ⋯ overflow menu (see agent-profile.js → "delete" menu action),
|