privateboard 0.1.37 → 0.1.40

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.
Files changed (76) hide show
  1. package/dist/boot.js +1415 -91
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +1415 -91
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +1271 -81
  6. package/dist/server.js.map +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/version.js.map +1 -1
  10. package/package.json +1 -1
  11. package/public/__avatar3d_test.html +156 -0
  12. package/public/adjourn-overlay.css +2 -2
  13. package/public/agent-overlay.css +27 -15
  14. package/public/agent-overlay.js +3 -1
  15. package/public/agent-profile.css +331 -41
  16. package/public/agent-profile.js +499 -75
  17. package/public/app-updater.css +1 -1
  18. package/public/app.js +2090 -547
  19. package/public/avatar-3d-snap.js +205 -0
  20. package/public/avatar-3d.js +792 -0
  21. package/public/avatar-customizer.html +274 -0
  22. package/public/avatar3d-editor.css +240 -0
  23. package/public/avatar3d-editor.js +481 -0
  24. package/public/avatars/3d/chair.png +0 -0
  25. package/public/avatars/3d/first-principles.png +0 -0
  26. package/public/avatars/3d/historian.png +0 -0
  27. package/public/avatars/3d/long-horizon.png +0 -0
  28. package/public/avatars/3d/phenomenologist.png +0 -0
  29. package/public/avatars/3d/socrates.png +0 -0
  30. package/public/avatars/3d/user-empathy.png +0 -0
  31. package/public/avatars/3d/value-investor.png +0 -0
  32. package/public/core-avatars.js +86 -0
  33. package/public/home-3d-loader.js +15 -4
  34. package/public/home-3d-mock.js +18 -7
  35. package/public/home.html +80 -18
  36. package/public/i18n.js +279 -4
  37. package/public/icons/avatar_1779855104027.glb +0 -0
  38. package/public/icons/logo.png +0 -0
  39. package/public/icons/new-style.glb +0 -0
  40. package/public/icons/new-style2.glb +0 -0
  41. package/public/icons/new-style3.glb +0 -0
  42. package/public/icons/new-style4.glb +0 -0
  43. package/public/icons/new-style5.glb +0 -0
  44. package/public/icons/office.glb +0 -0
  45. package/public/icons/stuff.glb +0 -0
  46. package/public/index.html +203 -182
  47. package/public/mention-picker.js +1 -1
  48. package/public/new-agent.css +7 -7
  49. package/public/new-agent.js +46 -20
  50. package/public/office-viewer.html +340 -0
  51. package/public/onboarding.css +5 -5
  52. package/public/quote-cta.css +5 -4
  53. package/public/quote-cta.js +50 -5
  54. package/public/room-settings.css +24 -9
  55. package/public/stuff-viewer.html +330 -0
  56. package/public/thread.css +1211 -0
  57. package/public/user-settings.css +16 -19
  58. package/public/user-settings.js +86 -78
  59. package/public/vendor/BufferGeometryUtils.js +1434 -0
  60. package/public/vendor/DRACOLoader.js +739 -0
  61. package/public/vendor/GLTFLoader.js +4860 -0
  62. package/public/vendor/RoomEnvironment.js +185 -0
  63. package/public/vendor/SkeletonUtils.js +496 -0
  64. package/public/vendor/draco/draco_decoder.js +34 -0
  65. package/public/vendor/draco/draco_decoder.wasm +0 -0
  66. package/public/vendor/draco/draco_encoder.js +33 -0
  67. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  68. package/public/vendor/meshopt_decoder.module.js +196 -0
  69. package/public/voice-3d-banner.js +12 -0
  70. package/public/voice-3d.js +1407 -432
  71. package/public/voice-clone.css +875 -0
  72. package/public/voice-clone.js +1351 -0
  73. package/public/voice-replay.css +3 -3
  74. package/public/voice-replay.js +21 -0
  75. package/public/avatar-skill.js +0 -629
  76. package/public/icons/folded-sidebar.png +0 -0
@@ -452,27 +452,24 @@
452
452
  gap: 12px;
453
453
  }
454
454
  .us-avatar-frame {
455
- width: 56px; height: 56px;
456
- border: 0.5px solid var(--line-strong);
457
- background: var(--bg);
458
- padding: 3px;
455
+ width: 64px; height: 64px;
456
+ padding: 0; /* avatar fills the container edge-to-edge */
457
+ overflow: hidden;
459
458
  position: relative;
460
459
  }
461
- .us-avatar-frame::before, .us-avatar-frame::after {
462
- content: "";
463
- position: absolute;
464
- width: 5px; height: 5px;
465
- border: 0.5px solid var(--lime);
466
- pointer-events: none;
467
- }
468
- .us-avatar-frame::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
469
- .us-avatar-frame::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
470
460
  .us-avatar-frame svg {
471
461
  width: 100%; height: 100%;
472
462
  image-rendering: pixelated;
473
463
  image-rendering: -moz-crisp-edges;
474
464
  display: block;
475
465
  }
466
+ /* 3D portrait PNG · the capture is already framed head-and-shoulders
467
+ (face-anchored, identical to the agent profile avatar), so just cover-fit. */
468
+ .us-avatar-frame img {
469
+ width: 100%; height: 100%;
470
+ object-fit: cover;
471
+ display: block;
472
+ }
476
473
 
477
474
  /* Buttons */
478
475
  .us-mini-btn {
@@ -537,7 +534,7 @@
537
534
  content: ">";
538
535
  color: var(--lime);
539
536
  font-weight: 700;
540
- font-size: 13px;
537
+ font-size: 14px;
541
538
  font-family: var(--mono);
542
539
  padding: 9px 0 0 11px;
543
540
  align-self: flex-start;
@@ -547,7 +544,7 @@
547
544
  border: none;
548
545
  background: transparent;
549
546
  font-family: var(--font-human);
550
- font-size: 13px;
547
+ font-size: 14px;
551
548
  line-height: 1.5;
552
549
  color: var(--text);
553
550
  outline: none;
@@ -857,7 +854,7 @@
857
854
  }
858
855
  .us-signout-text {
859
856
  font-family: var(--font-human);
860
- font-size: 13px;
857
+ font-size: 14px;
861
858
  color: var(--text-soft);
862
859
  line-height: 1.6;
863
860
  }
@@ -1021,7 +1018,7 @@
1021
1018
  font-weight: 600;
1022
1019
  }
1023
1020
  .us-chart-meta-value {
1024
- font-size: 13px;
1021
+ font-size: 14px;
1025
1022
  color: var(--text);
1026
1023
  font-weight: 700;
1027
1024
  font-variant-numeric: tabular-nums;
@@ -1293,7 +1290,7 @@
1293
1290
  font-family: var(--mono, "Inter", system-ui, sans-serif);
1294
1291
  }
1295
1292
  .us-model-tokens {
1296
- font-size: 13px;
1293
+ font-size: 14px;
1297
1294
  color: var(--text);
1298
1295
  font-weight: 700;
1299
1296
  font-variant-numeric: tabular-nums;
@@ -2101,7 +2098,7 @@
2101
2098
  .us-llm-pick-single { min-height: 64px; }
2102
2099
  .us-llm-pick-label {
2103
2100
  font-family: var(--mono);
2104
- font-size: 13px;
2101
+ font-size: 14px;
2105
2102
  font-weight: 700;
2106
2103
  color: var(--text);
2107
2104
  }
@@ -166,6 +166,8 @@
166
166
  name: typeof data.name === "string" ? data.name : "You",
167
167
  intro: typeof data.intro === "string" ? data.intro : "",
168
168
  avatarSeed: data.avatarSeed ?? null,
169
+ avatar3d: data.avatar3d ?? null,
170
+ avatarUrl: data.avatarUrl ?? null,
169
171
  webSearchProvider: data.webSearchProvider === "tavily" ? "tavily" : "brave",
170
172
  minimaxRegion: data.minimaxRegion === "intl" ? "intl" : "cn",
171
173
  };
@@ -187,11 +189,22 @@
187
189
  body: JSON.stringify({
188
190
  name: u.name,
189
191
  intro: u.intro,
190
- avatarSeed: u.avatarSeed
192
+ avatarSeed: u.avatarSeed,
193
+ ...("avatarUrl" in u ? { avatarUrl: u.avatarUrl } : {}),
194
+ ...("avatar3d" in u ? { avatar3d: u.avatar3d } : {}),
191
195
  })
192
196
  }).catch(() => { /* offline → cache stays, retry on next edit */ });
193
197
  }
194
198
 
199
+ // The 3D-avatar editor (avatar3d-editor.js) saves the user's portrait via
200
+ // PUT /api/prefs and fires this event. Sync our cache + repaint the frame so
201
+ // the settings avatar updates without reopening the pane.
202
+ window.addEventListener("pb:user-avatar-updated", (e) => {
203
+ const url = e && e.detail && e.detail.avatarUrl;
204
+ _prefsCache = { ...(_prefsCache || {}), avatarUrl: url || null };
205
+ try { paintUserAvatar(); } catch (_) {}
206
+ });
207
+
195
208
  // Provider keys · canonical state lives in keys-store.js (loaded as a
196
209
  // module script before this file). All reads/writes go through that store;
197
210
  // _keysMeta is a live accessor so the rest of this file needs no changes.
@@ -325,6 +338,10 @@
325
338
  <div class="us-row-label">${tr("us_avatar")}</div>
326
339
  <div class="us-row-field us-avatar-row">
327
340
  <div class="us-avatar-frame" data-us-avatar></div>
341
+ <button type="button" class="us-mini-btn" data-us-avatar3d>
342
+ <span class="us-mini-btn-mark">◈</span>
343
+ <span>${tr("us_avatar3d")}</span>
344
+ </button>
328
345
  <button type="button" class="us-mini-btn" data-us-regen-avatar>
329
346
  <span class="us-mini-btn-mark">◆</span>
330
347
  <span>${tr("us_regen_avatar")}</span>
@@ -367,30 +384,13 @@
367
384
  }).join("");
368
385
  }
369
386
 
370
- /* ── Room style toggle (3D voxel boardroom vs 2D flat round-table)
371
- Persists to `localStorage["boardroom.stage3d"]` ("on" | "off")
372
- same key voice-3d.js and renderRoundTable already gate on.
373
- Default "on" matches the existing implicit default. */
374
- const STAGE3D_KEY = "boardroom.stage3d";
375
- function getStage3d() {
376
- try { return localStorage.getItem(STAGE3D_KEY) !== "off"; }
377
- catch (_) { return true; }
378
- }
379
- function setStage3d(on) {
380
- try { localStorage.setItem(STAGE3D_KEY, on ? "on" : "off"); } catch (_) {}
381
- }
382
- function stageStyleSegmentsHTML() {
383
- const cur = getStage3d() ? "3d" : "2d";
384
- const items = [
385
- { key: "3d", labelKey: "us_stage_3d" },
386
- { key: "2d", labelKey: "us_stage_2d" },
387
- ];
388
- return items.map(({ key, labelKey }) => {
389
- const label = tr(labelKey);
390
- const cls = "us-seg-btn" + (key === cur ? " active" : "");
391
- return `<button type="button" class="${cls}" data-stage="${key}" role="radio" aria-checked="${key === cur ? "true" : "false"}">${escape(label)}</button>`;
392
- }).join("");
393
- }
387
+ /* The 2D / 3D stage toggle was retired (2026-05) · the voice room
388
+ is 3D-only now. `STAGE3D_KEY` / `getStage3d` / `setStage3d` /
389
+ `stageStyleSegmentsHTML` used to live here and have been removed
390
+ along with their row in `otherSettingsSectionHTML` and the click
391
+ handler below. The localStorage key the toggle persisted to
392
+ (`boardroom.stage3d`) is also no longer consulted anywhere; old
393
+ values are inert. */
394
394
 
395
395
  function otherSettingsSectionHTML() {
396
396
  return `
@@ -410,16 +410,6 @@
410
410
  </div>
411
411
  </div>
412
412
 
413
- <div class="us-row">
414
- <div class="us-row-label">${tr("us_stage_label")}</div>
415
- <div class="us-row-field">
416
- <div class="us-seg" role="radiogroup" aria-label="${escape(tr("us_stage_label"))}" data-us-stage>
417
- ${stageStyleSegmentsHTML()}
418
- </div>
419
- <p class="us-locale-deck">${escape(tr("us_stage_deck"))}</p>
420
- </div>
421
- </div>
422
-
423
413
  <div class="us-row">
424
414
  <div class="us-row-label">${tr("us_locale_label")}</div>
425
415
  <div class="us-row-field">
@@ -493,25 +483,8 @@
493
483
  // the app to re-render the current round-table so the swap is
494
484
  // visible immediately for anyone currently sitting in a voice
495
485
  // room (instead of "have to leave + re-enter to see it").
496
- const stGroup = paneEl.querySelector("[data-us-stage]");
497
- if (stGroup) {
498
- stGroup.addEventListener("click", (e) => {
499
- const btn = e.target.closest(".us-seg-btn[data-stage]");
500
- if (!btn) return;
501
- const next = btn.dataset.stage; // "3d" | "2d"
502
- setStage3d(next === "3d");
503
- stGroup.querySelectorAll(".us-seg-btn").forEach((el) => {
504
- const on = el.dataset.stage === next;
505
- el.classList.toggle("active", on);
506
- el.setAttribute("aria-checked", on ? "true" : "false");
507
- });
508
- try {
509
- if (window.app && typeof window.app.renderRoundTable === "function") {
510
- window.app.renderRoundTable();
511
- }
512
- } catch (_) { /* room may not be a voice room · ignore */ }
513
- });
514
- }
486
+ // [removed 2026-05] The 2D/3D stage toggle wire-up used to live
487
+ // here · the voice room is 3D-only now, no toggle to bind.
515
488
  // Typing-sound toggle · the persistence + audio context lives in
516
489
  // window.boardroomTypingSfx (typing-sfx.js); this row only mirrors
517
490
  // the current state and proxies clicks. Reading inside wire-up
@@ -1840,11 +1813,14 @@
1840
1813
 
1841
1814
  /* Avatar generation · same flow as the agent profile's regenerate
1842
1815
  button (see agent-profile.js / regenerateProfileAvatar): each
1843
- click pulls a fresh seed from AvatarSkill.randomSeed(), saves it
1844
- to the user prefs (`avatarSeed`), and re-paints. The SVG is
1845
- rendered from that seed via AvatarSkill.generate(). */
1846
- function generateAvatar(seed) {
1847
- return window.AvatarSkill.generate(seed);
1816
+ click pulls a fresh seed from Avatar3DSnap.randomSeed(), saves
1817
+ it to the user prefs (`avatarSeed`), and re-paints. Renders a
1818
+ 3D voxel head-and-shoulders portrait (the legacy AvatarSkill
1819
+ 8-bit SVG generator was retired). */
1820
+ // Synchronous "still rendering" placeholder used while the async
1821
+ // 3D snap is in flight · pure CSS via the .us-avatar-frame parent.
1822
+ function loadingHtml() {
1823
+ return '<div class="us-avatar-loading" aria-hidden="true">…</div>';
1848
1824
  }
1849
1825
 
1850
1826
  /* ── Modal shell ──────────────────────────────────────────── */
@@ -1918,21 +1894,44 @@
1918
1894
  const frame = paneEl.querySelector("[data-us-avatar]");
1919
1895
  if (!frame) return;
1920
1896
  const u = getUser();
1921
- // Mirror the agent profile flow · the avatar is whatever seed is
1922
- // saved on the user prefs. If none has ever been generated, mint
1923
- // one now so the avatar is stable across reloads.
1897
+ // 3D portrait takes precedence · if the user customised a 3D
1898
+ // avatar in the editor (avatarUrl is the rendered PNG), show
1899
+ // that. Otherwise render an async 3D snap from the user's seed.
1900
+ if (u.avatarUrl) {
1901
+ frame.innerHTML = `<img src="${u.avatarUrl}" alt="">`;
1902
+ return;
1903
+ }
1904
+ const snap = window.Avatar3DSnap;
1924
1905
  let seed = u.avatarSeed;
1925
- if (!seed && window.AvatarSkill) {
1926
- seed = window.AvatarSkill.randomSeed();
1906
+ if (!seed && snap) {
1907
+ seed = snap.randomSeed();
1927
1908
  saveUser({ avatarSeed: seed });
1928
- // Cascade the freshly-minted seed to app.prefs so the sidebar
1929
- // foot picks it up on the same paint.
1930
1909
  if (window.app) {
1931
1910
  window.app.prefs = { ...(window.app.prefs || {}), avatarSeed: seed };
1932
1911
  if (typeof window.app.renderUserBlock === "function") window.app.renderUserBlock();
1933
1912
  }
1934
1913
  }
1935
- frame.innerHTML = generateAvatar(seed || "default");
1914
+ if (!seed || !snap) {
1915
+ // No snap helper / no seed · clear the frame so the underlying
1916
+ // CSS placeholder shows (the .us-avatar-frame already has its
1917
+ // own initial styling).
1918
+ frame.innerHTML = "";
1919
+ return;
1920
+ }
1921
+ const cached = typeof snap.cacheGet === "function" ? snap.cacheGet(seed) : null;
1922
+ if (cached) {
1923
+ frame.innerHTML = `<img src="${cached}" alt="">`;
1924
+ return;
1925
+ }
1926
+ frame.innerHTML = loadingHtml();
1927
+ snap.generate(seed).then((url) => {
1928
+ if (!url) return;
1929
+ // Only paint if the frame is still in the DOM and showing the
1930
+ // same loading state (user may have already swapped avatars).
1931
+ const f2 = paneEl.querySelector("[data-us-avatar]");
1932
+ if (!f2) return;
1933
+ f2.innerHTML = `<img src="${url}" alt="">`;
1934
+ }).catch(() => { /* */ });
1936
1935
  }
1937
1936
 
1938
1937
  function wireUserSection() {
@@ -1960,25 +1959,34 @@
1960
1959
  introCount.textContent = introInput.value.length;
1961
1960
 
1962
1961
  // Regenerate avatar · same pattern as agent-profile's
1963
- // regenerateProfileAvatar: pull a fresh randomSeed, persist it to
1964
- // the user prefs, repaint. No counter, no name/intro composition —
1965
- // the seed is the only thing that determines the avatar.
1962
+ // regenerateProfileAvatar: pull a fresh seed, persist to the
1963
+ // user prefs, and repaint. Clears any captured 3D customizer
1964
+ // PNG (avatarUrl / avatar3d) so the next paint takes the new
1965
+ // seed-derived 3D snap rather than the previous capture.
1966
1966
  paneEl.querySelector("[data-us-regen-avatar]").addEventListener("click", (e) => {
1967
1967
  e.preventDefault();
1968
- if (!window.AvatarSkill) return;
1969
- const seed = window.AvatarSkill.randomSeed();
1970
- saveUser({ avatarSeed: seed });
1968
+ const snap = window.Avatar3DSnap;
1969
+ if (!snap || typeof snap.randomSeed !== "function") return;
1970
+ const seed = snap.randomSeed();
1971
+ saveUser({ avatarSeed: seed, avatarUrl: null, avatar3d: null });
1971
1972
  paintUserAvatar();
1972
- // Push the new seed into app.prefs so the sidebar foot's user
1973
- // avatar repaints with the same SVG. Without this, the settings
1974
- // overlay shows the new face but the sidebar keeps the old one
1975
- // until the next reload.
1976
1973
  if (window.app) {
1977
- window.app.prefs = { ...(window.app.prefs || {}), avatarSeed: seed };
1974
+ window.app.prefs = { ...(window.app.prefs || {}), avatarSeed: seed, avatarUrl: null, avatar3d: null };
1978
1975
  if (typeof window.app.renderUserBlock === "function") window.app.renderUserBlock();
1979
1976
  }
1980
1977
  });
1981
1978
 
1979
+ // Customize 3D avatar · opens the shared editor in "user" mode. The
1980
+ // editor saves the rendered PNG + config to prefs (PUT /api/prefs) and
1981
+ // fires "pb:user-avatar-updated"; we repaint the frame on that event.
1982
+ const a3dBtn = paneEl.querySelector("[data-us-avatar3d]");
1983
+ if (a3dBtn) {
1984
+ a3dBtn.addEventListener("click", (e) => {
1985
+ e.preventDefault();
1986
+ if (typeof window.openAvatar3DEditor === "function") window.openAvatar3DEditor({ kind: "user" });
1987
+ });
1988
+ }
1989
+
1982
1990
  paintUserAvatar();
1983
1991
  }
1984
1992