privateboard 0.1.38 → 0.1.41

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 (78) hide show
  1. package/dist/boot.js +327 -50
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +327 -50
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +201 -40
  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/agent-overlay.css +14 -6
  13. package/public/agent-overlay.js +6 -6
  14. package/public/agent-profile.css +9 -12
  15. package/public/agent-profile.js +91 -38
  16. package/public/app.js +471 -528
  17. package/public/avatar-3d-snap.js +205 -0
  18. package/public/avatar-3d.js +836 -0
  19. package/public/avatar-customizer.html +274 -0
  20. package/public/avatar3d-editor.css +240 -0
  21. package/public/avatar3d-editor.js +484 -0
  22. package/public/avatars/3d/chair.png +0 -0
  23. package/public/avatars/3d/first-principles.png +0 -0
  24. package/public/avatars/3d/historian.png +0 -0
  25. package/public/avatars/3d/long-horizon.png +0 -0
  26. package/public/avatars/3d/phenomenologist.png +0 -0
  27. package/public/avatars/3d/socrates.png +0 -0
  28. package/public/avatars/3d/user-empathy.png +0 -0
  29. package/public/avatars/3d/value-investor.png +0 -0
  30. package/public/core-avatars.js +86 -0
  31. package/public/home-3d-loader.js +15 -4
  32. package/public/home-3d-mock.js +25 -14
  33. package/public/home.html +78 -16
  34. package/public/i18n.js +8 -4
  35. package/public/icons/avatar_1779855104027.glb +0 -0
  36. package/public/icons/logo.png +0 -0
  37. package/public/icons/new-style.glb +0 -0
  38. package/public/icons/new-style2.glb +0 -0
  39. package/public/icons/new-style3.glb +0 -0
  40. package/public/icons/new-style4.glb +0 -0
  41. package/public/icons/new-style5.glb +0 -0
  42. package/public/icons/new-style6.glb +0 -0
  43. package/public/icons/office.glb +0 -0
  44. package/public/icons/stuff.glb +0 -0
  45. package/public/index.html +169 -141
  46. package/public/magazine.html +1 -1
  47. package/public/new-agent.js +46 -20
  48. package/public/newspaper.html +1 -1
  49. package/public/office-viewer.html +340 -0
  50. package/public/ppt.html +1 -1
  51. package/public/stuff-viewer.html +330 -0
  52. package/public/thread.css +16 -15
  53. package/public/user-settings.css +7 -31
  54. package/public/user-settings.js +75 -89
  55. package/public/vendor/BufferGeometryUtils.js +1434 -0
  56. package/public/vendor/DRACOLoader.js +739 -0
  57. package/public/vendor/GLTFLoader.js +4860 -0
  58. package/public/vendor/RoomEnvironment.js +185 -0
  59. package/public/vendor/SkeletonUtils.js +496 -0
  60. package/public/vendor/draco/draco_decoder.js +34 -0
  61. package/public/vendor/draco/draco_decoder.wasm +0 -0
  62. package/public/vendor/draco/draco_encoder.js +33 -0
  63. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  64. package/public/vendor/meshopt_decoder.module.js +196 -0
  65. package/public/voice-3d-banner.js +19 -7
  66. package/public/voice-3d.js +1407 -432
  67. package/public/voice-replay.js +21 -0
  68. package/public/avatar-skill.js +0 -629
  69. package/public/avatars/chair-blink.svg +0 -1
  70. package/public/avatars/chair.svg +0 -1
  71. package/public/avatars/first-principles.svg +0 -1
  72. package/public/avatars/historian.svg +0 -1
  73. package/public/avatars/long-horizon.svg +0 -1
  74. package/public/avatars/phenomenologist.svg +0 -1
  75. package/public/avatars/socrates.svg +0 -1
  76. package/public/avatars/user-empathy.svg +0 -1
  77. package/public/avatars/value-investor.svg +0 -1
  78. package/public/icons/folded-sidebar.png +0 -0
package/dist/version.d.ts CHANGED
@@ -12,6 +12,6 @@
12
12
  * number ends up surfaced in the user-facing footer or banner. Keep
13
13
  * this file as the canonical source — every callsite reads from here.
14
14
  */
15
- declare const VERSION = "0.1.38";
15
+ declare const VERSION = "0.1.41";
16
16
 
17
17
  export { VERSION };
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var VERSION = "0.1.38";
4
+ var VERSION = "0.1.41";
5
5
  export {
6
6
  VERSION
7
7
  };
@@ -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.38\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
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.41\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateboard",
3
- "version": "0.1.38",
3
+ "version": "0.1.41",
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",
@@ -0,0 +1,156 @@
1
+ <!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><title>avatar3d test</title>
3
+ <style>
4
+ html,body{margin:0;height:100%;background:#1a1a1a;overflow:hidden}
5
+ #status{position:fixed;top:8px;left:10px;color:#9fe;font:12px/1.5 monospace;z-index:9;white-space:pre-wrap;max-width:92vw}
6
+ #panel{position:fixed;top:8px;right:10px;z-index:9;background:rgba(0,0,0,.55);border:1px solid #444;border-radius:8px;padding:10px 12px;color:#cde;font:11px/1.6 monospace;width:230px}
7
+ #panel label{display:block;margin:6px 0 2px}
8
+ #panel input[type=range]{width:100%}
9
+ #panel b{color:#9fe}
10
+ #copy{margin-top:8px;width:100%;padding:5px;background:#2a3340;color:#cde;border:1px solid #556;border-radius:5px;cursor:pointer;font:11px monospace}
11
+ </style>
12
+ </head><body>
13
+ <div id="status">booting…</div>
14
+ <!-- Live brightness controls · drag to taste, then read the values off
15
+ the readout (or click "复制参数") and tell me — I'll bake them into
16
+ the room. -->
17
+ <div id="panel">
18
+ <label>曝光 exposure · <b id="vExp">0.70</b></label>
19
+ <input type="range" id="exp" min="0.2" max="1.4" step="0.01" value="0.70">
20
+ <label>环境强度 envIntensity · <b id="vEnv">0.35</b></label>
21
+ <input type="range" id="env" min="0" max="1.5" step="0.01" value="0.35">
22
+ <label>主光 key · <b id="vKey">0.70</b></label>
23
+ <input type="range" id="key" min="0" max="3" step="0.01" value="0.70">
24
+ <button id="copy">复制参数</button>
25
+ </div>
26
+
27
+ <!-- Classic script FIRST · catches module/import/protocol failures that a
28
+ top-level `import` would otherwise swallow into the console only. -->
29
+ <script>
30
+ window.__setStatus = function (msg, isErr) {
31
+ var el = document.getElementById("status");
32
+ if (!el) return;
33
+ el.textContent = msg;
34
+ el.style.color = isErr ? "#f88" : "#9fe";
35
+ };
36
+ if (location.protocol === "file:") {
37
+ window.__setStatus("⚠ 你是用 file:// 打开的 — 模块和 GLB 都加载不了。\n请在【运行中的 app】里访问 http://localhost:<端口>/__avatar3d_test.html", true);
38
+ }
39
+ window.addEventListener("error", function (e) {
40
+ // Resource (script/module) load failures arrive here as ErrorEvents.
41
+ var what = e.message || (e.target && (e.target.src || e.target.href)) || "unknown";
42
+ window.__setStatus("JS/资源错误: " + what, true);
43
+ }, true);
44
+ window.addEventListener("unhandledrejection", function (e) {
45
+ var r = e.reason; window.__setStatus("未处理的 Promise 拒绝: " + ((r && r.message) || r), true);
46
+ });
47
+ setTimeout(function () {
48
+ var el = document.getElementById("status");
49
+ if (el && /booting|importing|loading|building/i.test(el.textContent)) {
50
+ window.__setStatus(el.textContent + "\n(>6s 仍未完成 — 打开 DevTools Console 看具体报错)", true);
51
+ }
52
+ }, 6000);
53
+ </script>
54
+
55
+ <script type="module">
56
+ const S = window.__setStatus;
57
+ (async () => {
58
+ try {
59
+ // Step 0 · is the GLB even reachable on this server? (isolates
60
+ // "asset not served" from "module failed to load".)
61
+ S("检查 GLB 可达性…");
62
+ const GLB = "/icons/avatar_1779855104027.glb";
63
+ const head = await fetch(GLB, { method: "GET", headers: { Range: "bytes=0-0" } }).catch((e) => ({ ok: false, status: "fetch失败 " + e.message }));
64
+ if (!head.ok && head.status !== 206 && head.status !== 200) {
65
+ S("GLB 拿不到: " + GLB + " → " + head.status + "(文件没被服务?)", true);
66
+ return;
67
+ }
68
+
69
+ S("加载 three…");
70
+ const THREE = await import("/vendor/three.module.min.js");
71
+ S("加载 OrbitControls…");
72
+ const { OrbitControls } = await import("/vendor/OrbitControls.js");
73
+ const { RoomEnvironment } = await import("/vendor/RoomEnvironment.js");
74
+ S("加载 avatar-3d skill…");
75
+ const av = await import("/avatar-3d.js");
76
+
77
+ const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
78
+ renderer.setSize(innerWidth, innerHeight);
79
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
80
+ renderer.shadowMap.enabled = true;
81
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
82
+ // ACES tone mapping + sRGB output · the standard "make PBR look
83
+ // good" pair. Without tone mapping the env reflections clip / look
84
+ // flat; with it skin + hair get that soft filmic sheen.
85
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
86
+ renderer.toneMappingExposure = 0.7;
87
+ document.body.appendChild(renderer.domElement);
88
+
89
+ const scene = new THREE.Scene();
90
+ scene.background = new THREE.Color(0x222428);
91
+ const camera = new THREE.PerspectiveCamera(35, innerWidth / innerHeight, 0.1, 100);
92
+ camera.position.set(1.6, 1.7, 3.0);
93
+ const controls = new OrbitControls(camera, renderer.domElement);
94
+ controls.target.set(0, 0.9, 0);
95
+ controls.update();
96
+
97
+ // Image-based lighting · a PMREM-filtered procedural RoomEnvironment
98
+ // is what gives the materials real reflections / gloss (no HDRI
99
+ // file needed). This is the single biggest lever for "光泽 / 质感".
100
+ const pmrem = new THREE.PMREMGenerator(renderer);
101
+ scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
102
+ // RoomEnvironment is studio-bright · dim its contribution so it
103
+ // lights + reflects without washing the skin out. Keeps the gloss
104
+ // shape (reflections) while pulling overall exposure down.
105
+ scene.environmentIntensity = 0.35;
106
+
107
+ // The env map already provides ambient fill, so direct lights stay
108
+ // modest · a strong key + rim on top of IBL blew the faces out to
109
+ // pure white. One soft key for shape + a faint rim for separation.
110
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x33384a, 0.18));
111
+ const dir = new THREE.DirectionalLight(0xffffff, 0.7);
112
+ dir.position.set(3, 6, 4); dir.castShadow = true; scene.add(dir);
113
+ const rim = new THREE.DirectionalLight(0xbfd4ff, 0.3);
114
+ rim.position.set(-4, 3, -3); scene.add(rim);
115
+ const ground = new THREE.Mesh(
116
+ new THREE.CircleGeometry(2.4, 48),
117
+ new THREE.MeshStandardMaterial({ color: 0x33363c, roughness: 1 }),
118
+ );
119
+ ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground);
120
+
121
+ renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
122
+
123
+ // Live brightness sliders → renderer / scene / key-light.
124
+ const $ = (id) => document.getElementById(id);
125
+ const wire = (id, vid, fn, fmt) => {
126
+ const el = $(id); const lab = $(vid);
127
+ const upd = () => { const v = parseFloat(el.value); fn(v); lab.textContent = fmt(v); };
128
+ el.addEventListener("input", upd); upd();
129
+ };
130
+ wire("exp", "vExp", (v) => { renderer.toneMappingExposure = v; }, (v) => v.toFixed(2));
131
+ wire("env", "vEnv", (v) => { scene.environmentIntensity = v; }, (v) => v.toFixed(2));
132
+ wire("key", "vKey", (v) => { dir.intensity = v; }, (v) => v.toFixed(2));
133
+ $("copy").addEventListener("click", () => {
134
+ const t = `exposure=${$("exp").value} envIntensity=${$("env").value} key=${$("key").value}`;
135
+ navigator.clipboard && navigator.clipboard.writeText(t);
136
+ $("copy").textContent = "已复制: " + t;
137
+ });
138
+
139
+ S("下载 + 解析 GLB…");
140
+ await av.loadAvatar3D(GLB);
141
+
142
+ S("生成 avatar 实例…");
143
+ const seeds = ["yang-tianzhen", "socrates", "first-principles"];
144
+ seeds.forEach((seed, i) => {
145
+ const a = av.buildAvatar3D(seed, { height: 1.7 });
146
+ a.position.x = (i - 1) * 1.1;
147
+ a.rotation.y = 0;
148
+ scene.add(a);
149
+ });
150
+ S("✓ 加载完成 · " + seeds.length + " 个 avatar(拖动可旋转)");
151
+ } catch (e) {
152
+ S("ERROR: " + (e && e.stack ? e.stack : (e && e.message) || e), true);
153
+ }
154
+ })();
155
+ </script>
156
+ </body></html>
@@ -113,13 +113,10 @@ img[data-agent]:hover { filter: brightness(1.15); }
113
113
  border-bottom: 0.5px dashed var(--line-bright);
114
114
  }
115
115
  .agent-card-avatar {
116
- width: 68px;
117
- height: 68px;
118
- background: var(--panel-2);
119
- border: 0.5px solid var(--line-bright);
120
- padding: 5px;
116
+ width: 72px;
117
+ height: 72px;
118
+ margin: -10px 0 10px;
121
119
  display: block;
122
- image-rendering: pixelated;
123
120
  }
124
121
  .agent-card-id .name {
125
122
  font-size: 18px;
@@ -528,6 +525,17 @@ img[data-agent]:hover { filter: brightness(1.15); }
528
525
  border-color: var(--lime-dim);
529
526
  }
530
527
 
528
+ /* Hide the secondary voice-block sections inside the agent overlay.
529
+ The same renderVoiceBlock() is shared with the full agent-profile
530
+ page (where these stay visible); the overlay is meant to be a
531
+ quick read-only summary, not a place to type a preview line, run
532
+ a multi-minute clone job, or tune speed / pitch / timbre sliders.
533
+ Scope by parent so the profile page is untouched. */
534
+ .agent-overlay .ap-voice-section:has(> .ap-voice-forge),
535
+ .agent-overlay .ap-voice-section:has(.ap-voice-preview-text),
536
+ .agent-overlay .ap-voice-section:has(.ap-voice-emotion-row),
537
+ .agent-overlay .ap-voice-section:has(.ap-voice-advanced) { display: none; }
538
+
531
539
  @media (max-width: 600px) {
532
540
  .agent-overlay { padding: 12px; }
533
541
  .agent-card-head { grid-template-columns: auto 1fr; gap: 12px; padding: 14px 14px 12px; }
@@ -39,7 +39,7 @@
39
39
  name: "Socrates",
40
40
  role: "The Skeptic",
41
41
  handle: "@socrates",
42
- avatar: "avatars/socrates.svg",
42
+ avatar: "avatars/3d/socrates.png",
43
43
  lens: "Won't let any sentence pass without unpacking its assumptions three layers deep. Treats every word as a contract that must be defined before reasoning can begin.",
44
44
  traits: ["probing", "definitional", "patient", "rarely concedes"],
45
45
  memory: [
@@ -63,7 +63,7 @@
63
63
  name: "First Principles",
64
64
  role: "Causal Reasoning",
65
65
  handle: "@first_p",
66
- avatar: "avatars/first-principles.svg",
66
+ avatar: "avatars/3d/first-principles.png",
67
67
  lens: "Strips problems to their primitives. Refuses to reason in the middle layer where most thinking dies. Will rebuild the argument from physics if necessary.",
68
68
  traits: ["reductive", "literal", "cold", "physics-first"],
69
69
  memory: [
@@ -87,7 +87,7 @@
87
87
  name: "Value Investor",
88
88
  role: "Pattern Recognition",
89
89
  handle: "@value_inv",
90
- avatar: "avatars/value-investor.svg",
90
+ avatar: "avatars/3d/value-investor.png",
91
91
  lens: "Reads every judgment through a ten-year lens. Pattern recognition trained on twenty years of market history. Sees what's already been tried — and how it ended.",
92
92
  traits: ["historical", "skeptical of hype", "long-horizon", "selectively quiet"],
93
93
  memory: [
@@ -111,7 +111,7 @@
111
111
  name: "User-Empathy",
112
112
  role: "Empathy Lens",
113
113
  handle: "@user_e",
114
- avatar: "avatars/user-empathy.svg",
114
+ avatar: "avatars/3d/user-empathy.png",
115
115
  lens: "Asks why anyone would actually use this — never lets a feature pass without a real-person scenario. Holds the room accountable to people who aren't in it.",
116
116
  traits: ["narrative", "scenario-driven", "warm", "uncompromising"],
117
117
  memory: [
@@ -135,7 +135,7 @@
135
135
  name: "Long Horizon",
136
136
  role: "Historical Lens",
137
137
  handle: "@long_h",
138
- avatar: "avatars/long-horizon.svg",
138
+ avatar: "avatars/3d/long-horizon.png",
139
139
  lens: "Reads everything on a hundred-year scale. Knows which patterns repeat and which never do. Treats the present as a single frame in a much longer film.",
140
140
  traits: ["macro", "civilizational", "calm", "rare interjector"],
141
141
  memory: [
@@ -159,7 +159,7 @@
159
159
  name: "Phenomenologist",
160
160
  role: "Experience-First · Intern",
161
161
  handle: "@phen",
162
- avatar: "avatars/phenomenologist.svg",
162
+ avatar: "avatars/3d/phenomenologist.png",
163
163
  lens: "Begins from experience itself, without imposing structure. Currently on probation — has to earn a permanent seat, or step back to observer.",
164
164
  traits: ["unstructured", "first-person", "uneven", "promising"],
165
165
  memory: [
@@ -1635,17 +1635,13 @@
1635
1635
  }
1636
1636
  .ap-avatar {
1637
1637
  position: relative;
1638
- width: 76px;
1639
- height: 76px;
1640
- margin-top: -46px;
1638
+ width: 96px;
1639
+ height: 96px;
1640
+ /* Straddles the 84px cover · margin-top scaled with the size bump
1641
+ (was 76px/-46px) so the avatar keeps the same ~60% overlap into
1642
+ the cover and ~40% dropping into the body row. */
1643
+ margin-top: -58px;
1641
1644
  margin-bottom: 0;
1642
- border-radius: 50%;
1643
- background: var(--panel-2);
1644
- /* Ring color follows the page bg so the avatar reads as separated
1645
- from the cover gradient on every theme · in light themes the ring
1646
- is white against the gray cover; in dark themes the ring is the
1647
- near-black bg against the panel-toned cover. */
1648
- border: 4px solid var(--bg);
1649
1645
  overflow: hidden;
1650
1646
  display: flex;
1651
1647
  align-items: center;
@@ -1657,8 +1653,9 @@
1657
1653
  width: 100%;
1658
1654
  height: 100%;
1659
1655
  object-fit: cover;
1660
- image-rendering: pixelated;
1661
- image-rendering: crisp-edges;
1656
+ /* The 3D-avatar portrait is a smooth downscaled PNG · keep it smooth
1657
+ (pixelated would render it blocky at this size). */
1658
+ image-rendering: auto;
1662
1659
  }
1663
1660
  /* Text column · sits to the right of the avatar in the same row,
1664
1661
  so name + meta read as one tight identifier block. */
@@ -43,7 +43,7 @@
43
43
  /* ════════════════════════════════════ SOCRATES ════════════════════════════════════ */
44
44
  "socrates": {
45
45
  name: "Socrates", role: "The Skeptic", handle: "@socrates",
46
- avatar: "avatars/socrates.svg", status: "active", tenure: "core · 4 yr",
46
+ avatar: "avatars/3d/socrates.png", status: "active", tenure: "core · 4 yr",
47
47
  coverQuote: "won't let any sentence pass",
48
48
  meta: { creator: "@Kay", joined: "2024-04-01" },
49
49
  bio: [
@@ -114,7 +114,7 @@
114
114
  /* ════════════════════════════════════ FIRST PRINCIPLES ════════════════════════════ */
115
115
  "first-principles": {
116
116
  name: "First Principles", role: "Causal Reasoning", handle: "@first_p",
117
- avatar: "avatars/first-principles.svg", status: "active", tenure: "core · 4 yr",
117
+ avatar: "avatars/3d/first-principles.png", status: "active", tenure: "core · 4 yr",
118
118
  coverQuote: "what's the smallest unit?",
119
119
  meta: { creator: "@Kay", joined: "2024-04-01" },
120
120
  bio: [
@@ -177,7 +177,7 @@
177
177
  /* ════════════════════════════════════ VALUE INVESTOR ════════════════════════════ */
178
178
  "value-investor": {
179
179
  name: "Value Investor", role: "Pattern Recognition", handle: "@value_inv",
180
- avatar: "avatars/value-investor.svg", status: "active", tenure: "core · 3 yr",
180
+ avatar: "avatars/3d/value-investor.png", status: "active", tenure: "core · 3 yr",
181
181
  coverQuote: "who's tried this before?",
182
182
  meta: { creator: "@Kay", joined: "2024-08-14" },
183
183
  bio: [
@@ -242,7 +242,7 @@
242
242
  /* ════════════════════════════════════ USER-EMPATHY ════════════════════════════════ */
243
243
  "user-empathy": {
244
244
  name: "User-Empathy", role: "Empathy Lens", handle: "@user_e",
245
- avatar: "avatars/user-empathy.svg", status: "active", tenure: "core · 2 yr",
245
+ avatar: "avatars/3d/user-empathy.png", status: "active", tenure: "core · 2 yr",
246
246
  coverQuote: "name one user who'd reach for it",
247
247
  meta: { creator: "@Kay", joined: "2024-11-02" },
248
248
  bio: [
@@ -306,7 +306,7 @@
306
306
  /* ════════════════════════════════════ LONG HORIZON ════════════════════════════════ */
307
307
  "long-horizon": {
308
308
  name: "Long Horizon", role: "Historical Lens", handle: "@long_h",
309
- avatar: "avatars/long-horizon.svg", status: "active", tenure: "core · 2 yr",
309
+ avatar: "avatars/3d/long-horizon.png", status: "active", tenure: "core · 2 yr",
310
310
  coverQuote: "this is the cycle's mid-point",
311
311
  meta: { creator: "@Kay", joined: "2025-01-22" },
312
312
  bio: [
@@ -368,7 +368,7 @@
368
368
  /* ════════════════════════════════════ PHENOMENOLOGIST ═══════════════════════════ */
369
369
  "phenomenologist": {
370
370
  name: "Phenomenologist", role: "Experience-First", handle: "@phen",
371
- avatar: "avatars/phenomenologist.svg", status: "intern", tenure: "intern · trial",
371
+ avatar: "avatars/3d/phenomenologist.png", status: "intern", tenure: "intern · trial",
372
372
  coverQuote: "what is it like, actually?",
373
373
  meta: { creator: "@Kay", joined: "2026-03-08" },
374
374
  bio: [
@@ -760,39 +760,81 @@
760
760
  return tag.toUpperCase().slice(0, 8);
761
761
  }
762
762
 
763
- /* Per-agent rules (visual only · localStorage). */
763
+ /* Per-agent rules · PERSISTED SERVER-SIDE (agent.userRules) so the
764
+ orchestrator can inject them into the director's turn prompt. (They
765
+ used to be localStorage-only / "visual" — which is why a rule like
766
+ "不要谈及范冰冰" had zero effect: it never reached the model.)
767
+ A per-slug working copy backs the inputs for snappy editing; changes
768
+ debounce-flush to PATCH /api/agents/:id. */
764
769
  const RULES_MAX = 5;
765
- function rulesKey(slug) { return "boardroom.agent.rules." + slug; }
766
- function rulesForAgent(slug) {
767
- try {
768
- const raw = localStorage.getItem(rulesKey(slug));
769
- if (raw) {
770
- const arr = JSON.parse(raw);
771
- if (Array.isArray(arr)) return arr;
770
+ const _rules = Object.create(null); // slug -> string[] working copy
771
+ const _rulesTimer = Object.create(null); // slug -> debounce timer
772
+ function _legacyRulesKey(slug) { return "boardroom.agent.rules." + slug; }
773
+ function _liveAgentFor(slug) {
774
+ return (window.app && window.app.agentsById) ? window.app.agentsById[slug] : null;
775
+ }
776
+ // Seed the working copy once per slug: prefer the server value
777
+ // (agent.userRules); else migrate any legacy localStorage rules up to
778
+ // the server so a user who set rules in the old "visual-only" era
779
+ // doesn't lose them (and they start actually working).
780
+ function seedRules(slug) {
781
+ if (_rules[slug]) return _rules[slug];
782
+ const live = _liveAgentFor(slug);
783
+ let arr = (live && Array.isArray(live.userRules)) ? live.userRules.slice() : [];
784
+ if (arr.length === 0) {
785
+ try {
786
+ const raw = localStorage.getItem(_legacyRulesKey(slug));
787
+ if (raw) {
788
+ const a = JSON.parse(raw);
789
+ if (Array.isArray(a)) {
790
+ const legacy = a.map((x) => String(x).trim()).filter((x) => x.length > 0);
791
+ if (legacy.length > 0) { arr = legacy; _rules[slug] = arr; persistRules(slug); }
792
+ }
793
+ }
794
+ } catch (e) { /* */ }
795
+ }
796
+ _rules[slug] = arr;
797
+ return _rules[slug];
798
+ }
799
+ function rulesForAgent(slug) { return seedRules(slug); }
800
+ function _cleanRules(slug) {
801
+ return (_rules[slug] || []).map((x) => String(x).trim()).filter((x) => x.length > 0).slice(0, RULES_MAX);
802
+ }
803
+ function persistRules(slug) {
804
+ const arr = _cleanRules(slug);
805
+ const live = _liveAgentFor(slug);
806
+ if (live) live.userRules = arr.slice(); // optimistic
807
+ fetch("/api/agents/" + encodeURIComponent(slug), {
808
+ method: "PATCH",
809
+ headers: { "content-type": "application/json" },
810
+ body: JSON.stringify({ userRules: arr }),
811
+ }).then((r) => (r.ok ? r.json() : null)).then((updated) => {
812
+ if (updated && Array.isArray(updated.userRules) && live) {
813
+ live.userRules = updated.userRules.slice();
772
814
  }
773
- } catch (e) { /* */ }
774
- return [];
815
+ try { localStorage.removeItem(_legacyRulesKey(slug)); } catch (e) { /* */ }
816
+ }).catch(() => { /* offline · working copy keeps the edit */ });
775
817
  }
776
- function setRulesFor(slug, arr) {
777
- try { localStorage.setItem(rulesKey(slug), JSON.stringify(arr)); } catch (e) { /* */ }
818
+ function persistRulesSoon(slug) {
819
+ if (_rulesTimer[slug]) clearTimeout(_rulesTimer[slug]);
820
+ _rulesTimer[slug] = setTimeout(() => { _rulesTimer[slug] = null; persistRules(slug); }, 600);
778
821
  }
779
822
  function addRuleFor(slug) {
780
- const rules = rulesForAgent(slug);
823
+ const rules = seedRules(slug);
781
824
  if (rules.length >= RULES_MAX) return;
782
- rules.push("");
783
- setRulesFor(slug, rules);
825
+ rules.push(""); // empty rows aren't persisted until typed into
784
826
  }
785
827
  function setRuleAt(slug, idx, body) {
786
- const rules = rulesForAgent(slug);
828
+ const rules = seedRules(slug);
787
829
  if (idx < 0 || idx >= rules.length) return;
788
830
  rules[idx] = body;
789
- setRulesFor(slug, rules);
831
+ persistRulesSoon(slug);
790
832
  }
791
833
  function removeRuleFor(slug, idx) {
792
- const rules = rulesForAgent(slug);
834
+ const rules = seedRules(slug);
793
835
  if (idx < 0 || idx >= rules.length) return;
794
836
  rules.splice(idx, 1);
795
- setRulesFor(slug, rules);
837
+ persistRules(slug); // immediate on remove
796
838
  }
797
839
  function repaintProfileRules(slug) {
798
840
  const card = document.querySelector(`.ap-card[data-ap-card-slug="${slug}"]`);
@@ -1123,8 +1165,9 @@
1123
1165
  /** Render the RULES block · editable list of numbered constraints.
1124
1166
  * Mirrors the new-agent overlay UX: each row is a numbered input
1125
1167
  * with a trailing remove button; an "add rule" button below the
1126
- * list (hidden when the cap of 5 is reached). All mutations
1127
- * persist immediately via setRulesFor. */
1168
+ * list (hidden when the cap of 5 is reached). Mutations persist
1169
+ * server-side via PATCH /api/agents/:id (see setRuleAt / removeRuleFor)
1170
+ * so the orchestrator injects them into the director's prompt. */
1128
1171
  function renderRulesBlock(slug) {
1129
1172
  return `<div class="ap-rules-block" data-ap-rules-block data-slug="${escape(slug)}">${renderRulesInner(slug)}</div>`;
1130
1173
  }
@@ -2402,6 +2445,11 @@
2402
2445
  <span class="ap-id-menu-mark">◆</span>
2403
2446
  <span>Regenerate 8-bit avatar</span>
2404
2447
  </button>`);
2448
+ parts.push(`
2449
+ <button type="button" class="ap-id-menu-item" data-ap-menu-action="edit-avatar3d">
2450
+ <span class="ap-id-menu-mark">◈</span>
2451
+ <span>Customize 3D avatar</span>
2452
+ </button>`);
2405
2453
  }
2406
2454
  // Persona MD download · only present for Full-mode agents (those
2407
2455
  // built via the deep persona-builder pipeline). Their `personaSpec`
@@ -2440,17 +2488,19 @@
2440
2488
  if (el) el.remove();
2441
2489
  }
2442
2490
 
2443
- /** Generate a fresh 8-bit SVG and persist it as the agent's
2491
+ /** Render a fresh 3D voxel portrait and persist it as the agent's
2444
2492
  * avatar. Updates the live store so subsequent renders use the
2445
- * new image, then repaints the profile in place. Seeded directors
2446
- * fall back to a localStorage override (the server only stores
2447
- * user-created agents). */
2493
+ * new image, then repaints the profile in place. Uses the shared
2494
+ * Avatar3DSnap helper (same pipeline the agent-profile capture
2495
+ * and home / new-agent flows go through) — no more 8-bit SVG.
2496
+ * Seeded directors fall back to a localStorage override (the
2497
+ * server only stores user-created agents). */
2448
2498
  async function regenerateProfileAvatar(slug) {
2449
- const skill = window.AvatarSkill;
2450
- if (!skill) return;
2451
- const seed = skill.randomSeed();
2452
- const svg = skill.generate(seed);
2453
- const dataUrl = "data:image/svg+xml;utf8," + encodeURIComponent(svg);
2499
+ const snap = window.Avatar3DSnap;
2500
+ if (!snap || typeof snap.generate !== "function") return;
2501
+ const seed = snap.randomSeed();
2502
+ const dataUrl = await snap.generate(seed);
2503
+ if (!dataUrl) return;
2454
2504
  const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
2455
2505
  if (live) {
2456
2506
  try {
@@ -4358,6 +4408,9 @@
4358
4408
  e.preventDefault();
4359
4409
  closeProfileIdMenu();
4360
4410
  if (action === "regen-avatar" && slug) regenerateProfileAvatar(slug);
4411
+ if (action === "edit-avatar3d" && slug && typeof window.openAvatar3DEditor === "function") {
4412
+ window.openAvatar3DEditor(slug);
4413
+ }
4361
4414
  if (action === "delete" && slug && window.app && typeof window.app.deleteAgent === "function") {
4362
4415
  // deleteAgent handles confirm + DELETE call + closes the
4363
4416
  // profile + refreshes the sidebar. No-op for seed/chair
@@ -5050,8 +5103,8 @@
5050
5103
  }
5051
5104
  });
5052
5105
 
5053
- // Rules · persist edits as the user types (debounce-free; the
5054
- // payload is small and writes go to localStorage).
5106
+ // Rules · persist edits as the user types · setRuleAt debounce-
5107
+ // flushes to PATCH /api/agents/:id so the orchestrator picks them up.
5055
5108
  document.addEventListener("input", (e) => {
5056
5109
  const ri = e.target.closest("[data-ap-rule-input]");
5057
5110
  if (!ri) return;