privateboard 0.1.36 → 0.1.37
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/boot.js +45 -4
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +45 -4
- package/dist/cli.js.map +1 -1
- package/dist/server.js +45 -4
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -2
- package/public/app.js +187 -15
- package/public/avatars/chair-blink.svg +1 -0
- package/public/home-3d-loader.js +6 -0
- package/public/home.html +1 -1
- package/public/i18n.js +12 -0
- package/public/icons/folded-sidebar.png +0 -0
- package/public/index.html +355 -86
- package/public/report.html +27 -7
- package/public/user-settings.js +37 -20
- package/public/voice-3d.js +167 -3
- package/public/icons/search.png +0 -0
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.37\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "PrivateBoard · your private board meeting, on call. Local-first, multi-agent thinking amplifier.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "electron-entry.cjs",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"electron:build-ts": "tsup && tsc -p electron/tsconfig.json",
|
|
31
31
|
"electron:build-icon": "node scripts/build-app-icon.mjs",
|
|
32
32
|
"electron:dev": "npm run electron:rebuild && npm run electron:build-ts && electron .",
|
|
33
|
-
"electron:dist": "electron-builder install-app-deps && npm run electron:build-ts && npm run electron:build-icon && electron-builder --mac --publish always",
|
|
33
|
+
"electron:dist": "electron-builder install-app-deps && npm run electron:build-ts && npm run electron:build-icon && electron-builder --mac --publish always && npm run release:mac:finalize",
|
|
34
|
+
"release:mac:finalize": "node scripts/finalize-mac-release.mjs",
|
|
34
35
|
"electron:dist:local": "electron-builder install-app-deps && npm run electron:build-ts && npm run electron:build-icon && electron-builder --mac --publish never",
|
|
35
36
|
"electron:dist:unsigned": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder install-app-deps && npm run electron:build-ts && npm run electron:build-icon && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac --publish never --config.mac.hardenedRuntime=false --config.mac.notarize=false"
|
|
36
37
|
},
|
package/public/app.js
CHANGED
|
@@ -148,6 +148,15 @@
|
|
|
148
148
|
* appendMessageDom, drained in _revealPrewarmedBubble, baked into
|
|
149
149
|
* messageHtml so every render preserves the hidden state. */
|
|
150
150
|
_prewarmedHidden: new Set(),
|
|
151
|
+
/** Server-authoritative "currently-visible speaker" messageId.
|
|
152
|
+
* Pushed via queue-update SSE (orchestrator/room.ts pumpQueue).
|
|
153
|
+
* Drives the .streaming class gate in `updateMessageBodyDom` so
|
|
154
|
+
* only the one director the server marks as the active turn
|
|
155
|
+
* shows the typing-dots / pulse animation, even in the brief
|
|
156
|
+
* same-frame window where A's finalize and B's appended SSE
|
|
157
|
+
* events both reach the DOM. Null between turns / before first
|
|
158
|
+
* turn. */
|
|
159
|
+
currentActiveMessageId: null,
|
|
151
160
|
/** Mini-player anchor · when set, the cross-room bar persists
|
|
152
161
|
* for this voice room throughout its whole live/paused life
|
|
153
162
|
* (between speakers, during votes / clarify, idle phases) —
|
|
@@ -1164,6 +1173,12 @@
|
|
|
1164
1173
|
: (data.members || []).map((m) => ({ ...m, removedAt: null }));
|
|
1165
1174
|
this.currentChair = data.chair || null;
|
|
1166
1175
|
this.currentQueue = data.queue || [];
|
|
1176
|
+
// Reset to null on room open · the queue-update SSE will push
|
|
1177
|
+
// the real value if a director is currently speaking. The
|
|
1178
|
+
// initial GET /api/rooms/:id snapshot doesn't carry this field
|
|
1179
|
+
// (it's a transient pump state, not persisted to messages), so
|
|
1180
|
+
// we wait for the first SSE tick.
|
|
1181
|
+
this.currentActiveMessageId = null;
|
|
1167
1182
|
this.currentRound = data.round || { spoken: 0, total: 0 };
|
|
1168
1183
|
this.currentKeyPoints = data.keyPoints || [];
|
|
1169
1184
|
// Reset the round-table HUD log when (re)opening a room. Past
|
|
@@ -1402,6 +1417,7 @@
|
|
|
1402
1417
|
// re-opened (since `openRoom` re-sets it from data.chair),
|
|
1403
1418
|
// but the empty + composer states looked broken.
|
|
1404
1419
|
this.currentQueue = [];
|
|
1420
|
+
this.currentActiveMessageId = null;
|
|
1405
1421
|
this.currentRound = { spoken: 0, total: 0 };
|
|
1406
1422
|
this.currentKeyPoints = [];
|
|
1407
1423
|
this.rtChairLog = [];
|
|
@@ -1978,6 +1994,15 @@
|
|
|
1978
1994
|
msg.body = data.body;
|
|
1979
1995
|
msg.meta = data.meta || {};
|
|
1980
1996
|
}
|
|
1997
|
+
// Server cleared meta.preWarmed (pump just consumed this
|
|
1998
|
+
// speaker · it is now the visible active turn). Drain from
|
|
1999
|
+
// the hidden set BEFORE renderChat so the article re-paints
|
|
2000
|
+
// without `data-prewarmed`. Otherwise the CSS `display:none`
|
|
2001
|
+
// would persist across this re-render until the next event.
|
|
2002
|
+
if (data.meta && data.meta.preWarmed === false
|
|
2003
|
+
&& this._prewarmedHidden && this._prewarmedHidden.has(data.messageId)) {
|
|
2004
|
+
this._revealPrewarmedBubble(data.messageId);
|
|
2005
|
+
}
|
|
1981
2006
|
// Re-render via a chat repaint · the tool-use renderer is
|
|
1982
2007
|
// selected by meta.kind so it rebuilds with the new status.
|
|
1983
2008
|
this.renderChat();
|
|
@@ -1987,6 +2012,32 @@
|
|
|
1987
2012
|
const data = JSON.parse(e.data);
|
|
1988
2013
|
this.currentQueue = data.queue || [];
|
|
1989
2014
|
if (data.round) this.currentRound = data.round;
|
|
2015
|
+
// activeMessageId · server tells us which director's bubble
|
|
2016
|
+
// is the currently-visible "speaking" turn. Two consumers:
|
|
2017
|
+
// (1) `updateMessageBodyDom` gates the streaming animation
|
|
2018
|
+
// on this id (suppresses the brief text-mode flash where
|
|
2019
|
+
// A's finalize and B's append cross paths in the same rAF),
|
|
2020
|
+
// (2) a hidden pre-warmed bubble whose id matches gets
|
|
2021
|
+
// revealed here (defense in depth alongside the message-
|
|
2022
|
+
// updated meta.preWarmed:false signal — whichever arrives
|
|
2023
|
+
// first wins).
|
|
2024
|
+
const prevActive = this.currentActiveMessageId || null;
|
|
2025
|
+
this.currentActiveMessageId = (data && typeof data.activeMessageId === "string")
|
|
2026
|
+
? data.activeMessageId
|
|
2027
|
+
: (data && data.activeMessageId === null ? null : prevActive);
|
|
2028
|
+
if (this.currentActiveMessageId
|
|
2029
|
+
&& this._prewarmedHidden
|
|
2030
|
+
&& this._prewarmedHidden.has(this.currentActiveMessageId)) {
|
|
2031
|
+
this._revealPrewarmedBubble(this.currentActiveMessageId);
|
|
2032
|
+
}
|
|
2033
|
+
// If the active id changed AND the streaming class is now on
|
|
2034
|
+
// the wrong bubble, refresh both. Only affects bubbles that
|
|
2035
|
+
// are still in streaming state; finalized bubbles never carry
|
|
2036
|
+
// .streaming so they're not re-touched.
|
|
2037
|
+
if (prevActive !== this.currentActiveMessageId) {
|
|
2038
|
+
this._refreshStreamingClassForId(prevActive);
|
|
2039
|
+
this._refreshStreamingClassForId(this.currentActiveMessageId);
|
|
2040
|
+
}
|
|
1990
2041
|
this.renderQueue();
|
|
1991
2042
|
});
|
|
1992
2043
|
|
|
@@ -3542,12 +3593,53 @@
|
|
|
3542
3593
|
`;
|
|
3543
3594
|
}).join("");
|
|
3544
3595
|
document.body.appendChild(pop);
|
|
3545
|
-
// Anchor
|
|
3546
|
-
// `position: fixed` so we set top/left
|
|
3596
|
+
// Anchor relative to the wrapper · `.ap-model-picker` is
|
|
3597
|
+
// `position: fixed` so we set top/left in viewport coords.
|
|
3598
|
+
// Flip-above logic · the house-style dropdown sits in the
|
|
3599
|
+
// SECOND row of the modal and the modal is centred in the
|
|
3600
|
+
// viewport, so the trigger often lands close to the lower
|
|
3601
|
+
// half of the screen. A naive "always below" placement gets
|
|
3602
|
+
// clipped at the viewport bottom (visually: half the list
|
|
3603
|
+
// hidden under the chrome). We measure the rendered popover
|
|
3604
|
+
// height and choose the side with more room — preferring
|
|
3605
|
+
// below by default, flipping above when below is tight and
|
|
3606
|
+
// above has more space. The base `.ap-model-picker` rule
|
|
3607
|
+
// caps height at 60vh + scrolls inside, so a long menu still
|
|
3608
|
+
// fits even after flip.
|
|
3547
3609
|
const r = wrap.getBoundingClientRect();
|
|
3548
|
-
pop.style.top = `${Math.round(r.bottom + 4)}px`;
|
|
3549
3610
|
pop.style.left = `${Math.round(r.left)}px`;
|
|
3550
3611
|
pop.style.width = `${Math.round(r.width)}px`;
|
|
3612
|
+
// First paint to measure; the visible top is intentionally
|
|
3613
|
+
// off-screen until the position decision lands (avoids a
|
|
3614
|
+
// one-frame flash at the wrong location).
|
|
3615
|
+
pop.style.top = "-9999px";
|
|
3616
|
+
pop.style.visibility = "hidden";
|
|
3617
|
+
const PAD = 8;
|
|
3618
|
+
const GAP = 4;
|
|
3619
|
+
const popH = pop.getBoundingClientRect().height;
|
|
3620
|
+
const spaceBelow = Math.max(0, window.innerHeight - r.bottom - PAD);
|
|
3621
|
+
const spaceAbove = Math.max(0, r.top - PAD);
|
|
3622
|
+
let topPx;
|
|
3623
|
+
if (popH + GAP <= spaceBelow) {
|
|
3624
|
+
// Fits below comfortably.
|
|
3625
|
+
topPx = Math.round(r.bottom + GAP);
|
|
3626
|
+
} else if (popH + GAP <= spaceAbove) {
|
|
3627
|
+
// Fits above comfortably · flip.
|
|
3628
|
+
topPx = Math.round(r.top - popH - GAP);
|
|
3629
|
+
} else if (spaceAbove > spaceBelow) {
|
|
3630
|
+
// Neither side fits the full list — pick the bigger side
|
|
3631
|
+
// (above) and cap max-height so the popover's own internal
|
|
3632
|
+
// scroll (`.ap-model-picker { overflow-y: auto }`) handles
|
|
3633
|
+
// the overflow rather than the viewport edge clipping us.
|
|
3634
|
+
topPx = PAD;
|
|
3635
|
+
pop.style.maxHeight = `${spaceAbove}px`;
|
|
3636
|
+
} else {
|
|
3637
|
+
// Stay below · same cap-and-scroll trick.
|
|
3638
|
+
topPx = Math.round(r.bottom + GAP);
|
|
3639
|
+
pop.style.maxHeight = `${spaceBelow}px`;
|
|
3640
|
+
}
|
|
3641
|
+
pop.style.top = `${topPx}px`;
|
|
3642
|
+
pop.style.visibility = "";
|
|
3551
3643
|
// Dismiss handlers · attached synchronously, but the trigger's
|
|
3552
3644
|
// own wrap is whitelisted so the click that OPENED the popover
|
|
3553
3645
|
// (which fires AFTER mousedown · same user gesture) doesn't
|
|
@@ -6964,6 +7056,25 @@
|
|
|
6964
7056
|
// when the agent profile takes focus, and otherwise the agent
|
|
6965
7057
|
// rows shouldn't be highlighted at all.
|
|
6966
7058
|
}
|
|
7059
|
+
// Collapsed mini-rail · light the "rooms" icon when an actual
|
|
7060
|
+
// room is the main view; clear it for composers (the new-room /
|
|
7061
|
+
// new-agent mini icon carries the highlight there instead).
|
|
7062
|
+
this.setMiniRailContext(roomId !== null ? "rooms" : null);
|
|
7063
|
+
},
|
|
7064
|
+
|
|
7065
|
+
/** Highlight the rooms / agents switcher in the collapsed mini
|
|
7066
|
+
* rail. Mutually exclusive · passing `null` clears both (used by
|
|
7067
|
+
* the reports / notes / composer destinations, which light their
|
|
7068
|
+
* own mini icon via the shared trigger attributes). Mirrors the
|
|
7069
|
+
* full sidebar's tab selection but scoped to the icon rail so the
|
|
7070
|
+
* collapsed sidebar still shows "where you are". */
|
|
7071
|
+
setMiniRailContext(ctx) {
|
|
7072
|
+
document.querySelectorAll('.mini-tab[data-mini-tab="rooms"]').forEach((el) => {
|
|
7073
|
+
el.classList.toggle("active", ctx === "rooms");
|
|
7074
|
+
});
|
|
7075
|
+
document.querySelectorAll('.mini-tab[data-mini-tab="agents"]').forEach((el) => {
|
|
7076
|
+
el.classList.toggle("active", ctx === "agents");
|
|
7077
|
+
});
|
|
6967
7078
|
},
|
|
6968
7079
|
|
|
6969
7080
|
/** Sidebar focus when an agent profile takes the main view. The
|
|
@@ -6993,6 +7104,9 @@
|
|
|
6993
7104
|
document.querySelectorAll(".agent-row").forEach((r) => {
|
|
6994
7105
|
r.classList.toggle("active", r.dataset.agentProfile === slug);
|
|
6995
7106
|
});
|
|
7107
|
+
// Collapsed mini-rail · an agent profile is the main view → light
|
|
7108
|
+
// the agents switcher icon.
|
|
7109
|
+
this.setMiniRailContext("agents");
|
|
6996
7110
|
// Reset composer mode so subsequent transitions are clean — the
|
|
6997
7111
|
// user is no longer "creating" anything.
|
|
6998
7112
|
this.composerMode = "room";
|
|
@@ -7255,17 +7369,23 @@
|
|
|
7255
7369
|
// matches what the user picked in settings; otherwise fall back
|
|
7256
7370
|
// to the initial-letter chip we shipped before AvatarSkill
|
|
7257
7371
|
// existed.
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7372
|
+
// Sync every avatar slot · the full sidebar foot AND the
|
|
7373
|
+
// collapsed mini-rail foot both carry [data-user-avatar], so
|
|
7374
|
+
// querySelectorAll keeps them identical regardless of which is
|
|
7375
|
+
// currently visible.
|
|
7376
|
+
const seed = this.prefs?.avatarSeed;
|
|
7377
|
+
const avHtml = (seed && window.AvatarSkill && typeof window.AvatarSkill.generate === "function")
|
|
7378
|
+
? window.AvatarSkill.generate(seed)
|
|
7379
|
+
: null;
|
|
7380
|
+
document.querySelectorAll("[data-user-avatar]").forEach((av) => {
|
|
7381
|
+
if (avHtml) {
|
|
7262
7382
|
av.classList.add("has-pixel-av");
|
|
7263
|
-
av.innerHTML =
|
|
7383
|
+
av.innerHTML = avHtml;
|
|
7264
7384
|
} else {
|
|
7265
7385
|
av.classList.remove("has-pixel-av");
|
|
7266
7386
|
av.textContent = initial;
|
|
7267
7387
|
}
|
|
7268
|
-
}
|
|
7388
|
+
});
|
|
7269
7389
|
const nm = document.querySelector("[data-user-name]");
|
|
7270
7390
|
if (nm) nm.textContent = name;
|
|
7271
7391
|
const mt = document.querySelector("[data-user-meta]");
|
|
@@ -8124,6 +8244,9 @@
|
|
|
8124
8244
|
document.querySelectorAll("[data-notes-trigger].active").forEach((el) => el.classList.remove("active"));
|
|
8125
8245
|
document.querySelectorAll("[data-search-trigger].active").forEach((el) => el.classList.remove("active"));
|
|
8126
8246
|
document.querySelectorAll("[data-reports-trigger]").forEach((el) => el.classList.add("active"));
|
|
8247
|
+
// Reports is its own destination · clear the mini-rail rooms /
|
|
8248
|
+
// agents switchers so only the reports mini icon reads active.
|
|
8249
|
+
this.setMiniRailContext(null);
|
|
8127
8250
|
|
|
8128
8251
|
// Persist the view via URL hash so refresh / back-button restore
|
|
8129
8252
|
// both the page content AND the sidebar highlight. replaceState
|
|
@@ -8610,6 +8733,9 @@
|
|
|
8610
8733
|
document.querySelectorAll("[data-reports-trigger].active").forEach((el) => el.classList.remove("active"));
|
|
8611
8734
|
document.querySelectorAll("[data-search-trigger].active").forEach((el) => el.classList.remove("active"));
|
|
8612
8735
|
document.querySelectorAll("[data-notes-trigger]").forEach((el) => el.classList.add("active"));
|
|
8736
|
+
// Notes is its own destination · clear the mini-rail rooms /
|
|
8737
|
+
// agents switchers so only the notes mini icon reads active.
|
|
8738
|
+
this.setMiniRailContext(null);
|
|
8613
8739
|
|
|
8614
8740
|
if (location.hash !== "#/notes") {
|
|
8615
8741
|
try { history.replaceState(null, "", "#/notes"); } catch { /* ignore */ }
|
|
@@ -13660,7 +13786,6 @@
|
|
|
13660
13786
|
// tokens are not routed through `_t()`.
|
|
13661
13787
|
const statusWord = r.status !== "live" ? String(r.status).toUpperCase() : "";
|
|
13662
13788
|
head.innerHTML = `
|
|
13663
|
-
<button type="button" class="room-head-expand" data-sidebar-expand title="${this.escape(this._t("sidebar_expand"))}" aria-label="${this.escape(this._t("sidebar_expand"))}" data-tip="${this.escape(this._t("sidebar_expand"))}"></button>
|
|
13664
13789
|
<div class="room-info">
|
|
13665
13790
|
<div class="room-kicker">
|
|
13666
13791
|
<span class="kicker-num">// ROOM #${r.number}</span>
|
|
@@ -15530,14 +15655,36 @@
|
|
|
15530
15655
|
const isChairCard =
|
|
15531
15656
|
article.classList.contains("chair-direct") ||
|
|
15532
15657
|
article.classList.contains("chair-intervention");
|
|
15533
|
-
|
|
15658
|
+
// Single-active-speaker gate · the streaming animation belongs
|
|
15659
|
+
// only to the director whose turn is currently visible per the
|
|
15660
|
+
// server's queue-update activeMessageId field. In text mode this
|
|
15661
|
+
// erases the brief same-frame flash where A's finalize and B's
|
|
15662
|
+
// appended SSE events both run their DOM mutations in the same
|
|
15663
|
+
// rAF — without the gate, both bubbles render `.streaming` and
|
|
15664
|
+
// the user reads "two directors speaking at once." Chair cards
|
|
15665
|
+
// and non-agent messages (user, system) keep their own
|
|
15666
|
+
// streaming semantics; the gate only applies to director
|
|
15667
|
+
// (agent.roleKind === "director") bubbles. We can't read
|
|
15668
|
+
// roleKind here without a getter, so use the same heuristic
|
|
15669
|
+
// updateMessageBodyDom already trusts: a regular .msg article
|
|
15670
|
+
// that is NOT a chair-card is a director or user; user
|
|
15671
|
+
// messages don't carry the streaming class anyway, so this
|
|
15672
|
+
// narrows safely.
|
|
15673
|
+
const isAgentArticle =
|
|
15674
|
+
article.classList.contains("msg") &&
|
|
15675
|
+
!article.classList.contains("user") &&
|
|
15676
|
+
!isChairCard;
|
|
15677
|
+
const activeId = this.currentActiveMessageId || null;
|
|
15678
|
+
const speakingGate = !isAgentArticle || !activeId || messageId === activeId;
|
|
15679
|
+
const wantStreaming = !!streaming && speakingGate;
|
|
15680
|
+
if (empty && wantStreaming) {
|
|
15534
15681
|
bubble.innerHTML = this.thinkingHtml();
|
|
15535
15682
|
article.classList.add("thinking");
|
|
15536
15683
|
article.classList.add(isChairCard ? "is-streaming" : "streaming");
|
|
15537
15684
|
} else {
|
|
15538
15685
|
bubble.innerHTML = this.renderBody(display);
|
|
15539
15686
|
article.classList.toggle("thinking", false);
|
|
15540
|
-
article.classList.toggle(isChairCard ? "is-streaming" : "streaming",
|
|
15687
|
+
article.classList.toggle(isChairCard ? "is-streaming" : "streaming", wantStreaming);
|
|
15541
15688
|
// Re-apply chairman's-notes highlights · the bubble's
|
|
15542
15689
|
// innerHTML rewrite above wipes any previously-injected
|
|
15543
15690
|
// .note-highlight spans. Skip mid-stream (offsets won't match
|
|
@@ -15548,6 +15695,21 @@
|
|
|
15548
15695
|
}
|
|
15549
15696
|
},
|
|
15550
15697
|
|
|
15698
|
+
/** Re-evaluate the streaming class on a single bubble using its
|
|
15699
|
+
* current `meta.streaming` and the current activeMessageId. Used
|
|
15700
|
+
* by the queue-update SSE handler to flip the speaking animation
|
|
15701
|
+
* between A (finalized) and B (newly active) without waiting
|
|
15702
|
+
* for a token tick. No-op when the message isn't streaming or
|
|
15703
|
+
* the article isn't in the DOM yet. */
|
|
15704
|
+
_refreshStreamingClassForId(messageId) {
|
|
15705
|
+
if (!messageId) return;
|
|
15706
|
+
const msg = this.currentMessages.find((m) => m.id === messageId);
|
|
15707
|
+
if (!msg) return;
|
|
15708
|
+
const streaming = !!(msg.meta && msg.meta.streaming);
|
|
15709
|
+
if (!streaming) return;
|
|
15710
|
+
this.updateMessageBodyDom(messageId, msg.body || "", true);
|
|
15711
|
+
},
|
|
15712
|
+
|
|
15551
15713
|
/** Count of prior non-empty messages this speaker would see as context. */
|
|
15552
15714
|
contextCountAt(messageId) {
|
|
15553
15715
|
const idx = this.currentMessages.findIndex((m) => m.id === messageId);
|
|
@@ -16212,12 +16374,22 @@
|
|
|
16212
16374
|
|
|
16213
16375
|
const empty = !displayBody;
|
|
16214
16376
|
const streaming = m.meta && m.meta.streaming === true;
|
|
16377
|
+
// Single-active-speaker gate · mirrors `updateMessageBodyDom`.
|
|
16378
|
+
// For director (non-chair, non-user, agent) messages, only the
|
|
16379
|
+
// bubble whose id matches the server's activeMessageId may
|
|
16380
|
+
// wear the .streaming + .thinking classes. This keeps the
|
|
16381
|
+
// first-paint and the live-update class state consistent so
|
|
16382
|
+
// the text-mode "two directors flashing" race vanishes.
|
|
16383
|
+
const isDirectorMsg = !isUser && !isChair;
|
|
16384
|
+
const activeId = this.currentActiveMessageId || null;
|
|
16385
|
+
const speakingGate = !isDirectorMsg || !activeId || m.id === activeId;
|
|
16386
|
+
const wantStreaming = !!streaming && speakingGate;
|
|
16215
16387
|
const stateCls = [];
|
|
16216
|
-
if (
|
|
16217
|
-
if (empty &&
|
|
16388
|
+
if (wantStreaming) stateCls.push("streaming");
|
|
16389
|
+
if (empty && wantStreaming) stateCls.push("thinking");
|
|
16218
16390
|
if (metaKind) stateCls.push(`kind-${metaKind}`);
|
|
16219
16391
|
|
|
16220
|
-
const bubbleHtml = (empty &&
|
|
16392
|
+
const bubbleHtml = (empty && wantStreaming)
|
|
16221
16393
|
? this.thinkingHtml()
|
|
16222
16394
|
: this.renderBody(displayBody);
|
|
16223
16395
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="120 128 272 272" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" preserveAspectRatio="xMidYMid meet"><rect x="192" y="160" width="16" height="16" fill="#ce8a5a"/><rect x="208" y="160" width="16" height="16" fill="#ce8a5a"/><rect x="224" y="160" width="16" height="16" fill="#a35a32"/><rect x="240" y="160" width="16" height="16" fill="#a35a32"/><rect x="256" y="160" width="16" height="16" fill="#a35a32"/><rect x="272" y="160" width="16" height="16" fill="#a35a32"/><rect x="288" y="160" width="16" height="16" fill="#a35a32"/><rect x="304" y="160" width="16" height="16" fill="#a35a32"/><rect x="176" y="176" width="16" height="16" fill="#ce8a5a"/><rect x="192" y="176" width="16" height="16" fill="#ce8a5a"/><rect x="208" y="176" width="16" height="16" fill="#a35a32"/><rect x="224" y="176" width="16" height="16" fill="#a35a32"/><rect x="240" y="176" width="16" height="16" fill="#a35a32"/><rect x="256" y="176" width="16" height="16" fill="#a35a32"/><rect x="272" y="176" width="16" height="16" fill="#a35a32"/><rect x="288" y="176" width="16" height="16" fill="#a35a32"/><rect x="304" y="176" width="16" height="16" fill="#a35a32"/><rect x="320" y="176" width="16" height="16" fill="#a35a32"/><rect x="160" y="192" width="16" height="16" fill="#ce8a5a"/><rect x="176" y="192" width="16" height="16" fill="#ce8a5a"/><rect x="192" y="192" width="16" height="16" fill="#a35a32"/><rect x="208" y="192" width="16" height="16" fill="#a35a32"/><rect x="224" y="192" width="16" height="16" fill="#a35a32"/><rect x="240" y="192" width="16" height="16" fill="#a35a32"/><rect x="256" y="192" width="16" height="16" fill="#a35a32"/><rect x="272" y="192" width="16" height="16" fill="#a35a32"/><rect x="288" y="192" width="16" height="16" fill="#a35a32"/><rect x="304" y="192" width="16" height="16" fill="#a35a32"/><rect x="320" y="192" width="16" height="16" fill="#a35a32"/><rect x="336" y="192" width="16" height="16" fill="#a35a32"/><rect x="160" y="208" width="16" height="16" fill="#a35a32"/><rect x="176" y="208" width="16" height="16" fill="#a35a32"/><rect x="192" y="208" width="16" height="16" fill="#a35a32"/><rect x="208" y="208" width="16" height="16" fill="#a35a32"/><rect x="224" y="208" width="16" height="16" fill="#a35a32"/><rect x="240" y="208" width="16" height="16" fill="#a35a32"/><rect x="256" y="208" width="16" height="16" fill="#a35a32"/><rect x="272" y="208" width="16" height="16" fill="#a35a32"/><rect x="288" y="208" width="16" height="16" fill="#a35a32"/><rect x="304" y="208" width="16" height="16" fill="#a35a32"/><rect x="320" y="208" width="16" height="16" fill="#a35a32"/><rect x="336" y="208" width="16" height="16" fill="#a35a32"/><rect x="160" y="224" width="16" height="16" fill="#a35a32"/><rect x="176" y="224" width="16" height="16" fill="#a35a32"/><rect x="192" y="224" width="16" height="16" fill="#a35a32"/><rect x="208" y="224" width="16" height="16" fill="#1a0805"/><rect x="224" y="224" width="16" height="16" fill="#a35a32"/><rect x="240" y="224" width="16" height="16" fill="#a35a32"/><rect x="256" y="224" width="16" height="16" fill="#a35a32"/><rect x="272" y="224" width="16" height="16" fill="#a35a32"/><rect x="288" y="224" width="16" height="16" fill="#1a0805"/><rect x="304" y="224" width="16" height="16" fill="#a35a32"/><rect x="320" y="224" width="16" height="16" fill="#a35a32"/><rect x="336" y="224" width="16" height="16" fill="#a35a32"/><rect x="160" y="240" width="16" height="16" fill="#a35a32"/><rect x="176" y="240" width="16" height="16" fill="#a35a32"/><rect x="192" y="240" width="16" height="16" fill="#a35a32"/><rect x="208" y="240" width="16" height="16" fill="#a35a32"/><rect x="224" y="240" width="16" height="16" fill="#a35a32"/><rect x="240" y="240" width="16" height="16" fill="#a35a32"/><rect x="256" y="240" width="16" height="16" fill="#a35a32"/><rect x="272" y="240" width="16" height="16" fill="#a35a32"/><rect x="288" y="240" width="16" height="16" fill="#a35a32"/><rect x="304" y="240" width="16" height="16" fill="#a35a32"/><rect x="320" y="240" width="16" height="16" fill="#a35a32"/><rect x="336" y="240" width="16" height="16" fill="#a35a32"/><rect x="160" y="256" width="16" height="16" fill="#a35a32"/><rect x="176" y="256" width="16" height="16" fill="#a35a32"/><rect x="192" y="256" width="16" height="16" fill="#a35a32"/><rect x="208" y="256" width="16" height="16" fill="#a35a32"/><rect x="224" y="256" width="16" height="16" fill="#a35a32"/><rect x="240" y="256" width="16" height="16" fill="#a35a32"/><rect x="256" y="256" width="16" height="16" fill="#a35a32"/><rect x="272" y="256" width="16" height="16" fill="#a35a32"/><rect x="288" y="256" width="16" height="16" fill="#a35a32"/><rect x="304" y="256" width="16" height="16" fill="#a35a32"/><rect x="320" y="256" width="16" height="16" fill="#a35a32"/><rect x="336" y="256" width="16" height="16" fill="#a35a32"/><rect x="160" y="272" width="16" height="16" fill="#5a2a10"/><rect x="176" y="272" width="16" height="16" fill="#5a2a10"/><rect x="192" y="272" width="16" height="16" fill="#5a2a10"/><rect x="208" y="272" width="16" height="16" fill="#5a2a10"/><rect x="224" y="272" width="16" height="16" fill="#5a2a10"/><rect x="240" y="272" width="16" height="16" fill="#cfb56a"/><rect x="256" y="272" width="16" height="16" fill="#cfb56a"/><rect x="272" y="272" width="16" height="16" fill="#5a2a10"/><rect x="288" y="272" width="16" height="16" fill="#5a2a10"/><rect x="304" y="272" width="16" height="16" fill="#5a2a10"/><rect x="320" y="272" width="16" height="16" fill="#5a2a10"/><rect x="336" y="272" width="16" height="16" fill="#5a2a10"/><rect x="160" y="288" width="16" height="16" fill="#a35a32"/><rect x="176" y="288" width="16" height="16" fill="#a35a32"/><rect x="192" y="288" width="16" height="16" fill="#a35a32"/><rect x="208" y="288" width="16" height="16" fill="#a35a32"/><rect x="224" y="288" width="16" height="16" fill="#a35a32"/><rect x="240" y="288" width="16" height="16" fill="#a35a32"/><rect x="256" y="288" width="16" height="16" fill="#a35a32"/><rect x="272" y="288" width="16" height="16" fill="#a35a32"/><rect x="288" y="288" width="16" height="16" fill="#a35a32"/><rect x="304" y="288" width="16" height="16" fill="#a35a32"/><rect x="320" y="288" width="16" height="16" fill="#6e3814"/><rect x="336" y="288" width="16" height="16" fill="#6e3814"/><rect x="160" y="304" width="16" height="16" fill="#a35a32"/><rect x="176" y="304" width="16" height="16" fill="#a35a32"/><rect x="192" y="304" width="16" height="16" fill="#a35a32"/><rect x="208" y="304" width="16" height="16" fill="#a35a32"/><rect x="224" y="304" width="16" height="16" fill="#a35a32"/><rect x="240" y="304" width="16" height="16" fill="#a35a32"/><rect x="256" y="304" width="16" height="16" fill="#a35a32"/><rect x="272" y="304" width="16" height="16" fill="#a35a32"/><rect x="288" y="304" width="16" height="16" fill="#a35a32"/><rect x="304" y="304" width="16" height="16" fill="#6e3814"/><rect x="320" y="304" width="16" height="16" fill="#6e3814"/><rect x="336" y="304" width="16" height="16" fill="#a35a32"/><rect x="176" y="320" width="16" height="16" fill="#a35a32"/><rect x="192" y="320" width="16" height="16" fill="#a35a32"/><rect x="208" y="320" width="16" height="16" fill="#a35a32"/><rect x="224" y="320" width="16" height="16" fill="#a35a32"/><rect x="240" y="320" width="16" height="16" fill="#a35a32"/><rect x="256" y="320" width="16" height="16" fill="#a35a32"/><rect x="272" y="320" width="16" height="16" fill="#a35a32"/><rect x="288" y="320" width="16" height="16" fill="#a35a32"/><rect x="304" y="320" width="16" height="16" fill="#a35a32"/><rect x="320" y="320" width="16" height="16" fill="#6e3814"/><rect x="192" y="336" width="16" height="16" fill="#a35a32"/><rect x="208" y="336" width="16" height="16" fill="#a35a32"/><rect x="288" y="336" width="16" height="16" fill="#a35a32"/><rect x="304" y="336" width="16" height="16" fill="#6e3814"/><rect x="192" y="352" width="16" height="16" fill="#3e1c08"/><rect x="208" y="352" width="16" height="16" fill="#3e1c08"/><rect x="288" y="352" width="16" height="16" fill="#3e1c08"/><rect x="304" y="352" width="16" height="16" fill="#3e1c08"/></svg>
|
package/public/home-3d-loader.js
CHANGED
|
@@ -90,6 +90,12 @@
|
|
|
90
90
|
elevationDeg: 24, // soft top-down lean (default 30 was steep, 18 too flat)
|
|
91
91
|
lookAtY: 0.75, // target slightly above the table top so seated heads centre
|
|
92
92
|
},
|
|
93
|
+
// Suppress the built-in loading veil · the marketing hero
|
|
94
|
+
// already shows a static poster (`home-3d-poster.*`) which
|
|
95
|
+
// covers the mount → first-frame gap. Layering the veil on
|
|
96
|
+
// top of the poster would just darken it before the canvas
|
|
97
|
+
// takes over.
|
|
98
|
+
loading: false,
|
|
93
99
|
})) return;
|
|
94
100
|
|
|
95
101
|
// Pull the cast + start the rotating-speaker driver. The mock
|
package/public/home.html
CHANGED
|
@@ -1622,7 +1622,7 @@
|
|
|
1622
1622
|
|
|
1623
1623
|
<div class="hero-actions">
|
|
1624
1624
|
<a href="#install" class="big-btn primary">[ ◆ Convene a Room ]</a>
|
|
1625
|
-
<a href="https://github.com/kaysaith1900/privateboard/releases/download/v0.1.
|
|
1625
|
+
<a href="https://github.com/kaysaith1900/privateboard/releases/download/v0.1.36/PrivateBoard-0.1.36-arm64.dmg"
|
|
1626
1626
|
class="big-btn secondary"
|
|
1627
1627
|
download
|
|
1628
1628
|
aria-label="Download PrivateBoard for macOS (Apple Silicon)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 3v12"/><polyline points="6 11 12 17 18 11"/><path d="M5 21h14"/></svg> Download for Mac</a>
|
package/public/i18n.js
CHANGED
|
@@ -1199,6 +1199,8 @@ When the room ___, raise an objection.`,
|
|
|
1199
1199
|
us_active_llm_save_btn: "Save & activate",
|
|
1200
1200
|
us_active_llm_save_failed: "Could not save the credential — check the key and try again.",
|
|
1201
1201
|
us_active_llm_delete_active_confirm: "Removing the active provider — the boardroom will fall back to another added provider, or stop working until you add a new one. Continue?",
|
|
1202
|
+
us_llm_delete_confirm: "Remove this LLM credential? This can't be undone.",
|
|
1203
|
+
us_provider_key_delete_confirm: "Remove this {label} API key? This can't be undone.",
|
|
1202
1204
|
us_active_llm_switch_confirm: "Switch active LLM provider to {label}? Every director will be reassigned to the new provider's models.",
|
|
1203
1205
|
voice_cred_section_title: "▸ Voice provider",
|
|
1204
1206
|
voice_cred_section_deck: "Powers spoken meetings · directors pull voices from the active provider's catalog only.",
|
|
@@ -1210,6 +1212,7 @@ When the room ___, raise an objection.`,
|
|
|
1210
1212
|
voice_cred_picker_title: "Choose a voice provider",
|
|
1211
1213
|
voice_cred_switch_confirm: "Switch active voice provider to {label}? Each director's voice will be reshuffled from the new provider's catalog.",
|
|
1212
1214
|
voice_cred_delete_confirm_active: "Remove the active voice credential? Director voices reshuffle from the next available credential.",
|
|
1215
|
+
voice_cred_delete_confirm: "Remove this voice credential? This can't be undone.",
|
|
1213
1216
|
voice_cred_save_failed: "Could not save the voice credential — check the key and try again.",
|
|
1214
1217
|
ap_voice_empty_no_active: "No active voice provider · open Settings to add one",
|
|
1215
1218
|
search_cred_section_title: "▸ Search provider",
|
|
@@ -1222,6 +1225,7 @@ When the room ___, raise an objection.`,
|
|
|
1222
1225
|
search_cred_picker_title: "Choose a search provider",
|
|
1223
1226
|
search_cred_switch_confirm: "Switch active search provider to {label}? Web Search routes through this credential from now on.",
|
|
1224
1227
|
search_cred_delete_confirm_active: "Remove the active search credential? Web Search rotates to the next available credential, or stops working until you add one.",
|
|
1228
|
+
search_cred_delete_confirm: "Remove this search credential? This can't be undone.",
|
|
1225
1229
|
search_cred_save_failed: "Could not save the search credential — check the key and try again.",
|
|
1226
1230
|
us_ws_backend_tag: "WEB SEARCH BACKEND",
|
|
1227
1231
|
us_ws_backend_deck:
|
|
@@ -2424,6 +2428,8 @@ When the room ___, raise an objection.`,
|
|
|
2424
2428
|
us_active_llm_save_btn: "保存并启用",
|
|
2425
2429
|
us_active_llm_save_failed: "保存失败 —— 请检查 key 后重试。",
|
|
2426
2430
|
us_active_llm_delete_active_confirm: "正在删除当前使用的服务商 —— 删除后将自动切换到下一个已添加的服务商;如果没有其他服务商,系统将无法正常使用。确定继续吗?",
|
|
2431
|
+
us_llm_delete_confirm: "确认删除这条 LLM 凭证?删除后无法恢复。",
|
|
2432
|
+
us_provider_key_delete_confirm: "确认删除 {label} 的 API key?删除后无法恢复。",
|
|
2427
2433
|
us_active_llm_switch_confirm: "切换当前服务商为 {label}?所有 director 会被重新分配到新服务商的模型。",
|
|
2428
2434
|
voice_cred_section_title: "▸ 语音模型",
|
|
2429
2435
|
voice_cred_section_deck: "支撑语音会议 · 所有 director 只从当前生效的语音模型里抽声音。",
|
|
@@ -2435,6 +2441,7 @@ When the room ___, raise an objection.`,
|
|
|
2435
2441
|
voice_cred_picker_title: "选择一个语音模型",
|
|
2436
2442
|
voice_cred_switch_confirm: "切换到 {label}?每位 director 的声音会从新模型的清单里重新分配。",
|
|
2437
2443
|
voice_cred_delete_confirm_active: "删除正在使用的语音凭证?Director 的声音会从下一个可用凭证里重新分配。",
|
|
2444
|
+
voice_cred_delete_confirm: "确认删除这条语音凭证?删除后无法恢复。",
|
|
2438
2445
|
voice_cred_save_failed: "保存语音凭证失败 — 请检查 key 后重试。",
|
|
2439
2446
|
ap_voice_empty_no_active: "还没启用语音模型 · 去 Settings 添加一个",
|
|
2440
2447
|
search_cred_section_title: "▸ 搜索服务",
|
|
@@ -2447,6 +2454,7 @@ When the room ___, raise an objection.`,
|
|
|
2447
2454
|
search_cred_picker_title: "选择一个搜索服务",
|
|
2448
2455
|
search_cred_switch_confirm: "切换到 {label}?之后 Web Search 都会走这把凭证。",
|
|
2449
2456
|
search_cred_delete_confirm_active: "删除正在使用的搜索凭证?Web Search 会切换到下一个可用凭证;如果都没了就停止工作,直到你重新添加。",
|
|
2457
|
+
search_cred_delete_confirm: "确认删除这条搜索凭证?删除后无法恢复。",
|
|
2450
2458
|
search_cred_save_failed: "保存搜索凭证失败 — 请检查 key 后重试。",
|
|
2451
2459
|
us_keys_group_skill: "技能服务",
|
|
2452
2460
|
us_keys_skill_deck: "为需要外部服务的系统技能授权。每位董事可在档案中单独开关。",
|
|
@@ -3391,6 +3399,8 @@ When the room ___, raise an objection.`,
|
|
|
3391
3399
|
us_active_llm_save_btn: "保存して有効化",
|
|
3392
3400
|
us_active_llm_save_failed: "保存に失敗しました —— キーを確認して再試行してください。",
|
|
3393
3401
|
us_active_llm_delete_active_confirm: "現在使用中のプロバイダーを削除します —— 削除後は別の追加済みプロバイダーに切り替わります。他になければシステムは動作しません。続行しますか?",
|
|
3402
|
+
us_llm_delete_confirm: "このLLMクレデンシャルを削除しますか?元には戻せません。",
|
|
3403
|
+
us_provider_key_delete_confirm: "{label} のAPIキーを削除しますか?元には戻せません。",
|
|
3394
3404
|
us_active_llm_switch_confirm: "現在のプロバイダーを {label} に切り替えますか?全 director が新しいプロバイダーのモデルに再割り当てされます。",
|
|
3395
3405
|
us_keys_skill_deck: "外部サービスを必要とするシステムスキルを有効化。各エージェントはプロフィール単位で参加可否を選べます。",
|
|
3396
3406
|
us_ws_backend_tag: "ウェブ検索バックエンド",
|
|
@@ -4324,6 +4334,8 @@ When the room ___, raise an objection.`,
|
|
|
4324
4334
|
us_active_llm_save_btn: "Guardar y activar",
|
|
4325
4335
|
us_active_llm_save_failed: "No se pudo guardar la credencial — revisa la clave y vuelve a intentarlo.",
|
|
4326
4336
|
us_active_llm_delete_active_confirm: "Eliminando el proveedor activo — la sala usará otro proveedor añadido, o dejará de funcionar hasta que añadas uno nuevo. ¿Continuar?",
|
|
4337
|
+
us_llm_delete_confirm: "¿Eliminar esta credencial LLM? No se puede deshacer.",
|
|
4338
|
+
us_provider_key_delete_confirm: "¿Eliminar la clave API de {label}? No se puede deshacer.",
|
|
4327
4339
|
us_active_llm_switch_confirm: "¿Cambiar el proveedor LLM activo a {label}? Todos los directores se reasignarán a los modelos del nuevo proveedor.",
|
|
4328
4340
|
us_keys_skill_deck: "habilita habilidades de sistema que necesitan un servicio externo. Cada agente puede optar in/out en su perfil.",
|
|
4329
4341
|
us_ws_backend_tag: "MOTOR DE BÚSQUEDA WEB",
|
|
Binary file
|