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.
- package/dist/boot.js +1415 -91
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +1415 -91
- package/dist/cli.js.map +1 -1
- package/dist/server.js +1271 -81
- 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 +1 -1
- package/public/__avatar3d_test.html +156 -0
- package/public/adjourn-overlay.css +2 -2
- package/public/agent-overlay.css +27 -15
- package/public/agent-overlay.js +3 -1
- package/public/agent-profile.css +331 -41
- package/public/agent-profile.js +499 -75
- package/public/app-updater.css +1 -1
- package/public/app.js +2090 -547
- package/public/avatar-3d-snap.js +205 -0
- package/public/avatar-3d.js +792 -0
- package/public/avatar-customizer.html +274 -0
- package/public/avatar3d-editor.css +240 -0
- package/public/avatar3d-editor.js +481 -0
- package/public/avatars/3d/chair.png +0 -0
- package/public/avatars/3d/first-principles.png +0 -0
- package/public/avatars/3d/historian.png +0 -0
- package/public/avatars/3d/long-horizon.png +0 -0
- package/public/avatars/3d/phenomenologist.png +0 -0
- package/public/avatars/3d/socrates.png +0 -0
- package/public/avatars/3d/user-empathy.png +0 -0
- package/public/avatars/3d/value-investor.png +0 -0
- package/public/core-avatars.js +86 -0
- package/public/home-3d-loader.js +15 -4
- package/public/home-3d-mock.js +18 -7
- package/public/home.html +80 -18
- package/public/i18n.js +279 -4
- package/public/icons/avatar_1779855104027.glb +0 -0
- package/public/icons/logo.png +0 -0
- package/public/icons/new-style.glb +0 -0
- package/public/icons/new-style2.glb +0 -0
- package/public/icons/new-style3.glb +0 -0
- package/public/icons/new-style4.glb +0 -0
- package/public/icons/new-style5.glb +0 -0
- package/public/icons/office.glb +0 -0
- package/public/icons/stuff.glb +0 -0
- package/public/index.html +203 -182
- package/public/mention-picker.js +1 -1
- package/public/new-agent.css +7 -7
- package/public/new-agent.js +46 -20
- package/public/office-viewer.html +340 -0
- package/public/onboarding.css +5 -5
- package/public/quote-cta.css +5 -4
- package/public/quote-cta.js +50 -5
- package/public/room-settings.css +24 -9
- package/public/stuff-viewer.html +330 -0
- package/public/thread.css +1211 -0
- package/public/user-settings.css +16 -19
- package/public/user-settings.js +86 -78
- package/public/vendor/BufferGeometryUtils.js +1434 -0
- package/public/vendor/DRACOLoader.js +739 -0
- package/public/vendor/GLTFLoader.js +4860 -0
- package/public/vendor/RoomEnvironment.js +185 -0
- package/public/vendor/SkeletonUtils.js +496 -0
- package/public/vendor/draco/draco_decoder.js +34 -0
- package/public/vendor/draco/draco_decoder.wasm +0 -0
- package/public/vendor/draco/draco_encoder.js +33 -0
- package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
- package/public/vendor/meshopt_decoder.module.js +196 -0
- package/public/voice-3d-banner.js +12 -0
- package/public/voice-3d.js +1407 -432
- package/public/voice-clone.css +875 -0
- package/public/voice-clone.js +1351 -0
- package/public/voice-replay.css +3 -3
- package/public/voice-replay.js +21 -0
- package/public/avatar-skill.js +0 -629
- package/public/icons/folded-sidebar.png +0 -0
package/public/mention-picker.js
CHANGED
package/public/new-agent.css
CHANGED
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
}
|
|
305
305
|
.na-avatar-regen .icon {
|
|
306
306
|
font-family: var(--mono);
|
|
307
|
-
font-size:
|
|
307
|
+
font-size: 14px;
|
|
308
308
|
font-weight: 400;
|
|
309
309
|
line-height: 1;
|
|
310
310
|
}
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
color: var(--lime);
|
|
363
363
|
}
|
|
364
364
|
.na-model-trigger .trigger-l .name {
|
|
365
|
-
font-size:
|
|
365
|
+
font-size: 14px;
|
|
366
366
|
font-weight: 700;
|
|
367
367
|
color: var(--text);
|
|
368
368
|
letter-spacing: -0.003em;
|
|
@@ -779,7 +779,7 @@
|
|
|
779
779
|
border: none;
|
|
780
780
|
background: transparent;
|
|
781
781
|
font-family: var(--font-human);
|
|
782
|
-
font-size:
|
|
782
|
+
font-size: 14px;
|
|
783
783
|
line-height: 1.5;
|
|
784
784
|
color: var(--text);
|
|
785
785
|
outline: none;
|
|
@@ -951,7 +951,7 @@
|
|
|
951
951
|
margin-top: 6px;
|
|
952
952
|
align-self: flex-start;
|
|
953
953
|
}
|
|
954
|
-
.new-agent-overlay .na-rule-add .plus { color: var(--lime); font-size:
|
|
954
|
+
.new-agent-overlay .na-rule-add .plus { color: var(--lime); font-size: 14px; line-height: 1; }
|
|
955
955
|
.new-agent-overlay .na-rule-add:hover { border-color: var(--lime); color: var(--lime); border-style: solid; }
|
|
956
956
|
.new-agent-overlay .na-rule-add[disabled],
|
|
957
957
|
.new-agent-overlay .na-rule-add.at-cap {
|
|
@@ -1119,7 +1119,7 @@
|
|
|
1119
1119
|
border: 0.5px solid var(--line-bright);
|
|
1120
1120
|
color: var(--text-faint);
|
|
1121
1121
|
font-family: var(--mono);
|
|
1122
|
-
font-size:
|
|
1122
|
+
font-size: 14px;
|
|
1123
1123
|
line-height: 1;
|
|
1124
1124
|
cursor: pointer;
|
|
1125
1125
|
display: flex;
|
|
@@ -1352,7 +1352,7 @@
|
|
|
1352
1352
|
.new-agent-overlay .na-textarea-wrap.tall textarea {
|
|
1353
1353
|
min-height: 220px;
|
|
1354
1354
|
font-family: var(--mono);
|
|
1355
|
-
font-size:
|
|
1355
|
+
font-size: 14px;
|
|
1356
1356
|
line-height: 1.5;
|
|
1357
1357
|
resize: vertical;
|
|
1358
1358
|
overflow-y: auto;
|
|
@@ -1382,7 +1382,7 @@
|
|
|
1382
1382
|
font-weight: 700;
|
|
1383
1383
|
letter-spacing: -0.005em;
|
|
1384
1384
|
font-family: var(--font-human);
|
|
1385
|
-
font-size:
|
|
1385
|
+
font-size: 14px;
|
|
1386
1386
|
}
|
|
1387
1387
|
.na-summary-handle { color: var(--lime); }
|
|
1388
1388
|
.na-foot-sep { color: var(--text-faint); }
|
package/public/new-agent.js
CHANGED
|
@@ -141,12 +141,15 @@
|
|
|
141
141
|
const rng = makeRng(seed);
|
|
142
142
|
return [0,0,0,0].map(() => Math.floor(rng() * 16).toString(16)).join("");
|
|
143
143
|
}
|
|
144
|
-
// Avatar generation delegates to the shared
|
|
145
|
-
// (
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
// Avatar generation delegates to the shared Avatar3DSnap helper
|
|
145
|
+
// (public/avatar-3d-snap.js). One source of truth for the 3D
|
|
146
|
+
// voxel head-and-shoulders portrait used everywhere a director
|
|
147
|
+
// face appears. The legacy AvatarSkill 8-bit SVG generator was
|
|
148
|
+
// retired in favour of this 3D pipeline.
|
|
149
|
+
async function generateAvatarDataUrl(seed) {
|
|
150
|
+
const snap = window.Avatar3DSnap;
|
|
151
|
+
if (!snap || typeof snap.generate !== "function") return "";
|
|
152
|
+
return snap.generate(seed);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
function modalHTML() {
|
|
@@ -612,16 +615,37 @@
|
|
|
612
615
|
if (!frame) return;
|
|
613
616
|
if (avatarState.placeholder) {
|
|
614
617
|
frame.classList.add("placeholder");
|
|
615
|
-
frame
|
|
618
|
+
// Empty frame · the wrapper's CSS placeholder (initial letter
|
|
619
|
+
// / texture) is the resting state until the user clicks
|
|
620
|
+
// regenerate. The legacy 8-bit SVG silhouette was retired.
|
|
621
|
+
frame.innerHTML = "";
|
|
616
622
|
if (seedEl) seedEl.textContent = "—";
|
|
617
623
|
if (rollEl) rollEl.textContent = "";
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
frame.classList.remove("placeholder");
|
|
627
|
+
const seedKey = avatarState.seed + "::" + avatarState.roll;
|
|
628
|
+
if (seedEl) seedEl.textContent = shortHash(avatarState.seed);
|
|
629
|
+
if (rollEl) rollEl.textContent = " · #" + avatarState.roll;
|
|
630
|
+
const snap = window.Avatar3DSnap;
|
|
631
|
+
const cached = snap && typeof snap.cacheGet === "function" ? snap.cacheGet(seedKey) : null;
|
|
632
|
+
if (cached) {
|
|
633
|
+
frame.innerHTML = `<img src="${cached}" alt="">`;
|
|
634
|
+
return;
|
|
624
635
|
}
|
|
636
|
+
frame.innerHTML = '<div class="na-avatar-loading" aria-hidden="true">…</div>';
|
|
637
|
+
if (!snap || typeof snap.generate !== "function") return;
|
|
638
|
+
const expected = seedKey;
|
|
639
|
+
snap.generate(seedKey).then((dataUrl) => {
|
|
640
|
+
if (!dataUrl) return;
|
|
641
|
+
// Bail if the user has already rolled again — only the latest
|
|
642
|
+
// seed's render should land in the frame.
|
|
643
|
+
const cur = avatarState.placeholder ? null : (avatarState.seed + "::" + avatarState.roll);
|
|
644
|
+
if (cur !== expected) return;
|
|
645
|
+
const f2 = modal.querySelector("[data-na-avatar]");
|
|
646
|
+
if (!f2) return;
|
|
647
|
+
f2.innerHTML = `<img src="${dataUrl}" alt="">`;
|
|
648
|
+
}).catch(() => { /* */ });
|
|
625
649
|
}
|
|
626
650
|
|
|
627
651
|
function positionDropdown() {
|
|
@@ -665,10 +689,10 @@
|
|
|
665
689
|
}
|
|
666
690
|
|
|
667
691
|
/** Regenerate the avatar by asking the LLM for a "vibe seed" derived
|
|
668
|
-
* from the director's name + bio, then painting the
|
|
669
|
-
* shared
|
|
670
|
-
* endpoint errors (no key, network, etc.) so the button
|
|
671
|
-
* produces a fresh face. */
|
|
692
|
+
* from the director's name + bio, then painting the 3D portrait
|
|
693
|
+
* via the shared Avatar3DSnap. Falls back to a local random seed
|
|
694
|
+
* if the endpoint errors (no key, network, etc.) so the button
|
|
695
|
+
* always produces a fresh face. */
|
|
672
696
|
async function regenerateAvatar() {
|
|
673
697
|
const name = modal.querySelector(".na-name-input").value.trim();
|
|
674
698
|
const desc = modal.querySelector(".na-desc-input").value.trim();
|
|
@@ -682,7 +706,7 @@
|
|
|
682
706
|
// Without a name, just produce a random seed locally — no point
|
|
683
707
|
// burning an LLM call on an empty form.
|
|
684
708
|
if (!name) {
|
|
685
|
-
avatarState.seed = (window.
|
|
709
|
+
avatarState.seed = (window.Avatar3DSnap?.randomSeed?.() || ("anon|" + Date.now()));
|
|
686
710
|
if (vibeEl) vibeEl.textContent = "";
|
|
687
711
|
paintAvatar();
|
|
688
712
|
return;
|
|
@@ -800,14 +824,16 @@
|
|
|
800
824
|
|
|
801
825
|
// Avatar → data URL. If the user never clicked "regenerate", we
|
|
802
826
|
// build one off the form values now so the agent has a real face.
|
|
827
|
+
// The render is async (3D portrait via Avatar3DSnap); falls back
|
|
828
|
+
// to an empty string when WebGL isn't available — the backend
|
|
829
|
+
// already tolerates a missing avatarPath.
|
|
803
830
|
let avatarSeed = avatarState.seed;
|
|
804
831
|
let avatarRoll = avatarState.roll || 1;
|
|
805
832
|
if (avatarState.placeholder) {
|
|
806
833
|
avatarSeed = (name + "|" + bio) || "anon";
|
|
807
834
|
avatarRoll = 1;
|
|
808
835
|
}
|
|
809
|
-
const
|
|
810
|
-
const avatarPath = "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
836
|
+
const avatarPath = (await generateAvatarDataUrl(avatarSeed + "::" + avatarRoll)) || "";
|
|
811
837
|
|
|
812
838
|
// Lock the button while the request is in flight.
|
|
813
839
|
create.disabled = true;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>office.glb · viewer</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body { margin: 0; height: 100%; background: #16181c; overflow: hidden; color: #cde; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
8
|
+
#status {
|
|
9
|
+
position: fixed; top: 8px; left: 10px;
|
|
10
|
+
color: #9fe; z-index: 9; white-space: pre-wrap;
|
|
11
|
+
max-width: 60vw;
|
|
12
|
+
padding: 6px 10px;
|
|
13
|
+
background: rgba(0, 0, 0, 0.45);
|
|
14
|
+
border: 0.5px solid #2c3038;
|
|
15
|
+
border-radius: 4px;
|
|
16
|
+
}
|
|
17
|
+
#status.is-err { color: #f88; border-color: #5a2a2a; }
|
|
18
|
+
#info {
|
|
19
|
+
position: fixed; bottom: 10px; left: 10px; z-index: 9;
|
|
20
|
+
background: rgba(0, 0, 0, 0.55);
|
|
21
|
+
border: 0.5px solid #2c3038;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
padding: 8px 12px;
|
|
24
|
+
min-width: 220px;
|
|
25
|
+
}
|
|
26
|
+
#info h1 { margin: 0 0 6px; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: #6ad29a; font-weight: 600; }
|
|
27
|
+
#info dl { margin: 0; display: grid; grid-template-columns: auto 1fr; column-gap: 12px; row-gap: 2px; font-size: 11px; }
|
|
28
|
+
#info dt { color: #7a8290; }
|
|
29
|
+
#info dd { margin: 0; color: #cde; }
|
|
30
|
+
#panel {
|
|
31
|
+
position: fixed; top: 8px; right: 10px; z-index: 9;
|
|
32
|
+
background: rgba(0, 0, 0, 0.55);
|
|
33
|
+
border: 0.5px solid #2c3038; border-radius: 4px;
|
|
34
|
+
padding: 10px 12px; width: 230px;
|
|
35
|
+
}
|
|
36
|
+
#panel label { display: block; margin: 6px 0 2px; color: #aab2c0; }
|
|
37
|
+
#panel input[type=range] { width: 100%; accent-color: #6ad29a; }
|
|
38
|
+
#panel b { color: #6ad29a; }
|
|
39
|
+
#panel button {
|
|
40
|
+
width: 100%; margin-top: 8px; padding: 6px;
|
|
41
|
+
background: #232a36; color: #cde;
|
|
42
|
+
border: 0.5px solid #3a414d; border-radius: 4px;
|
|
43
|
+
font: 11px ui-monospace, monospace; cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
#panel button:hover { border-color: #6ad29a; color: #6ad29a; }
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<div id="status">booting…</div>
|
|
50
|
+
<div id="panel">
|
|
51
|
+
<label>exposure · <b id="vExp">1.00</b></label>
|
|
52
|
+
<input type="range" id="exp" min="0.2" max="2.0" step="0.01" value="1.00">
|
|
53
|
+
<label>env intensity · <b id="vEnv">0.85</b></label>
|
|
54
|
+
<input type="range" id="env" min="0" max="2" step="0.01" value="0.85">
|
|
55
|
+
<label>key light · <b id="vKey">1.00</b></label>
|
|
56
|
+
<input type="range" id="key" min="0" max="3" step="0.01" value="1.00">
|
|
57
|
+
<label>rotate model · <b id="vRot">auto</b></label>
|
|
58
|
+
<input type="range" id="rot" min="0" max="360" step="1" value="0">
|
|
59
|
+
<button id="autoRotateToggle">↻ auto-rotate: ON</button>
|
|
60
|
+
<button id="reframeBtn">⤧ re-fit camera</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div id="info">
|
|
63
|
+
<h1>// office.glb</h1>
|
|
64
|
+
<dl>
|
|
65
|
+
<dt>size</dt><dd id="iSize">—</dd>
|
|
66
|
+
<dt>meshes</dt><dd id="iMesh">—</dd>
|
|
67
|
+
<dt>vertices</dt><dd id="iVerts">—</dd>
|
|
68
|
+
<dt>materials</dt><dd id="iMats">—</dd>
|
|
69
|
+
<dt>bounds</dt><dd id="iBounds">—</dd>
|
|
70
|
+
<dt>fps</dt><dd id="iFps">—</dd>
|
|
71
|
+
</dl>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<script>
|
|
75
|
+
window.__setStatus = function (msg, isErr) {
|
|
76
|
+
const el = document.getElementById("status");
|
|
77
|
+
if (!el) return;
|
|
78
|
+
el.textContent = msg;
|
|
79
|
+
el.classList.toggle("is-err", !!isErr);
|
|
80
|
+
};
|
|
81
|
+
if (location.protocol === "file:") {
|
|
82
|
+
window.__setStatus("⚠ Open via http://localhost:<port>/stuff-viewer.html · file:// blocks module + GLB loads.", true);
|
|
83
|
+
}
|
|
84
|
+
window.addEventListener("error", (e) => {
|
|
85
|
+
const what = e.message || (e.target && (e.target.src || e.target.href)) || "unknown";
|
|
86
|
+
window.__setStatus("JS / asset error: " + what, true);
|
|
87
|
+
}, true);
|
|
88
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
89
|
+
const r = e.reason;
|
|
90
|
+
window.__setStatus("Unhandled rejection: " + ((r && r.stack) || (r && r.message) || r), true);
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<script type="module">
|
|
95
|
+
const S = window.__setStatus;
|
|
96
|
+
(async () => {
|
|
97
|
+
try {
|
|
98
|
+
const GLB = "/icons/office.glb";
|
|
99
|
+
S("checking " + GLB + " …");
|
|
100
|
+
const head = await fetch(GLB, { method: "HEAD" }).catch((err) => ({ ok: false, status: "fetch failed: " + err.message }));
|
|
101
|
+
if (!head.ok) {
|
|
102
|
+
S("GLB unreachable: " + GLB + " → " + head.status, true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const bytes = parseInt(head.headers.get("content-length") || "0", 10);
|
|
106
|
+
document.getElementById("iSize").textContent = bytes ? (bytes / 1024).toFixed(1) + " KB" : "?";
|
|
107
|
+
|
|
108
|
+
S("loading three.js …");
|
|
109
|
+
const THREE = await import("/vendor/three.module.min.js");
|
|
110
|
+
S("loading OrbitControls + GLTFLoader + RoomEnvironment …");
|
|
111
|
+
const { OrbitControls } = await import("/vendor/OrbitControls.js");
|
|
112
|
+
const { GLTFLoader } = await import("/vendor/GLTFLoader.js");
|
|
113
|
+
const { DRACOLoader } = await import("/vendor/DRACOLoader.js");
|
|
114
|
+
const { RoomEnvironment } = await import("/vendor/RoomEnvironment.js");
|
|
115
|
+
// GLBs in the wild can use TWO compression schemes:
|
|
116
|
+
// · meshopt (EXT_meshopt_compression) — stuff.glb
|
|
117
|
+
// · Draco (KHR_draco_mesh_compression) — office.glb
|
|
118
|
+
// Wire BOTH decoders unconditionally so the viewer handles
|
|
119
|
+
// either input without further config. Both are vendored
|
|
120
|
+
// under /vendor/* so the viewer works offline.
|
|
121
|
+
const { MeshoptDecoder } = await import("/vendor/meshopt_decoder.module.js");
|
|
122
|
+
|
|
123
|
+
// ── Renderer / scene / camera ───────────────────────────
|
|
124
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
125
|
+
renderer.setSize(innerWidth, innerHeight);
|
|
126
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
127
|
+
renderer.shadowMap.enabled = true;
|
|
128
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
129
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
130
|
+
renderer.toneMappingExposure = 1.0;
|
|
131
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
132
|
+
document.body.appendChild(renderer.domElement);
|
|
133
|
+
|
|
134
|
+
const scene = new THREE.Scene();
|
|
135
|
+
scene.background = new THREE.Color(0x16181c);
|
|
136
|
+
|
|
137
|
+
const camera = new THREE.PerspectiveCamera(38, innerWidth / innerHeight, 0.01, 100);
|
|
138
|
+
camera.position.set(2.0, 1.6, 2.8);
|
|
139
|
+
|
|
140
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
141
|
+
controls.enableDamping = true;
|
|
142
|
+
controls.dampingFactor = 0.08;
|
|
143
|
+
controls.target.set(0, 0.5, 0);
|
|
144
|
+
controls.update();
|
|
145
|
+
|
|
146
|
+
// IBL · same recipe as the avatar viewer; gives any GLB
|
|
147
|
+
// PBR materials something to reflect.
|
|
148
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
149
|
+
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
150
|
+
scene.environmentIntensity = 0.85;
|
|
151
|
+
|
|
152
|
+
// Direct lights · key + fill + rim. Soft enough that PBR
|
|
153
|
+
// doesn't blow out, strong enough to read shape on matte
|
|
154
|
+
// surfaces.
|
|
155
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x2a3140, 0.32));
|
|
156
|
+
const key = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
157
|
+
key.position.set(3, 5, 4);
|
|
158
|
+
key.castShadow = true;
|
|
159
|
+
key.shadow.mapSize.set(1024, 1024);
|
|
160
|
+
key.shadow.camera.near = 0.1;
|
|
161
|
+
key.shadow.camera.far = 20;
|
|
162
|
+
key.shadow.camera.top = 4;
|
|
163
|
+
key.shadow.camera.bottom = -4;
|
|
164
|
+
key.shadow.camera.left = -4;
|
|
165
|
+
key.shadow.camera.right = 4;
|
|
166
|
+
key.shadow.bias = -0.0005;
|
|
167
|
+
scene.add(key);
|
|
168
|
+
const rim = new THREE.DirectionalLight(0xbfd4ff, 0.35);
|
|
169
|
+
rim.position.set(-4, 3, -3);
|
|
170
|
+
scene.add(rim);
|
|
171
|
+
|
|
172
|
+
// Ground · circular disk so shadows can land; matches the
|
|
173
|
+
// dark BG so it reads as "stage" not "floor".
|
|
174
|
+
const ground = new THREE.Mesh(
|
|
175
|
+
new THREE.CircleGeometry(4.0, 64),
|
|
176
|
+
new THREE.MeshStandardMaterial({ color: 0x1d2026, roughness: 1, metalness: 0 }),
|
|
177
|
+
);
|
|
178
|
+
ground.rotation.x = -Math.PI / 2;
|
|
179
|
+
ground.position.y = -0.001;
|
|
180
|
+
ground.receiveShadow = true;
|
|
181
|
+
scene.add(ground);
|
|
182
|
+
|
|
183
|
+
// Grid for scale reference · soft, low-contrast so it doesn't
|
|
184
|
+
// compete with the model.
|
|
185
|
+
const grid = new THREE.GridHelper(8, 32, 0x2a3140, 0x222831);
|
|
186
|
+
grid.material.transparent = true;
|
|
187
|
+
grid.material.opacity = 0.5;
|
|
188
|
+
scene.add(grid);
|
|
189
|
+
|
|
190
|
+
// ── Load GLB ────────────────────────────────────────────
|
|
191
|
+
S("downloading + parsing " + GLB + " …");
|
|
192
|
+
// The decoder boots its WASM lazily on first use — `ready` is a
|
|
193
|
+
// Promise we have to await so subsequent .load() calls find an
|
|
194
|
+
// initialised decoder. Without this the loader fires the same
|
|
195
|
+
// "setMeshoptDecoder must be called before loading compressed
|
|
196
|
+
// files" again on the next compressed buffer view.
|
|
197
|
+
await MeshoptDecoder.ready;
|
|
198
|
+
const loader = new GLTFLoader();
|
|
199
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
200
|
+
// Draco · decoder JS + wasm live under /vendor/draco/.
|
|
201
|
+
// `setDecoderPath` joins onto the directory; the loader
|
|
202
|
+
// fetches draco_decoder.js + draco_decoder.wasm lazily on
|
|
203
|
+
// the first compressed buffer it encounters.
|
|
204
|
+
const draco = new DRACOLoader();
|
|
205
|
+
draco.setDecoderPath("/vendor/draco/");
|
|
206
|
+
draco.setDecoderConfig({ type: "wasm" });
|
|
207
|
+
loader.setDRACOLoader(draco);
|
|
208
|
+
const gltf = await new Promise((resolve, reject) => {
|
|
209
|
+
loader.load(
|
|
210
|
+
GLB,
|
|
211
|
+
(g) => resolve(g),
|
|
212
|
+
(xhr) => {
|
|
213
|
+
if (xhr.total) {
|
|
214
|
+
S(`downloading … ${((xhr.loaded / xhr.total) * 100).toFixed(0)} %`);
|
|
215
|
+
} else {
|
|
216
|
+
S(`downloading … ${(xhr.loaded / 1024).toFixed(0)} KB`);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
(err) => reject(err),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const model = gltf.scene;
|
|
224
|
+
// Enable shadows on every mesh in the imported tree.
|
|
225
|
+
let meshCount = 0;
|
|
226
|
+
let vertCount = 0;
|
|
227
|
+
const mats = new Set();
|
|
228
|
+
model.traverse((node) => {
|
|
229
|
+
if (node.isMesh) {
|
|
230
|
+
node.castShadow = true;
|
|
231
|
+
node.receiveShadow = true;
|
|
232
|
+
meshCount += 1;
|
|
233
|
+
if (node.geometry && node.geometry.attributes && node.geometry.attributes.position) {
|
|
234
|
+
vertCount += node.geometry.attributes.position.count;
|
|
235
|
+
}
|
|
236
|
+
if (node.material) {
|
|
237
|
+
if (Array.isArray(node.material)) node.material.forEach((m) => mats.add(m));
|
|
238
|
+
else mats.add(node.material);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
scene.add(model);
|
|
243
|
+
|
|
244
|
+
// Animations · play all clips at 1× if present.
|
|
245
|
+
let mixer = null;
|
|
246
|
+
if (gltf.animations && gltf.animations.length) {
|
|
247
|
+
mixer = new THREE.AnimationMixer(model);
|
|
248
|
+
for (const clip of gltf.animations) mixer.clipAction(clip).play();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Fit camera to model bounds ─────────────────────────
|
|
252
|
+
function reframe() {
|
|
253
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
254
|
+
const size = box.getSize(new THREE.Vector3());
|
|
255
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
256
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
257
|
+
const fov = camera.fov * Math.PI / 180;
|
|
258
|
+
const dist = (maxDim / 2) / Math.tan(fov / 2) * 1.7;
|
|
259
|
+
camera.position.set(
|
|
260
|
+
center.x + dist * 0.6,
|
|
261
|
+
center.y + dist * 0.55,
|
|
262
|
+
center.z + dist * 0.9,
|
|
263
|
+
);
|
|
264
|
+
camera.near = Math.max(0.005, maxDim / 200);
|
|
265
|
+
camera.far = Math.max(20, dist * 4);
|
|
266
|
+
camera.updateProjectionMatrix();
|
|
267
|
+
controls.target.copy(center);
|
|
268
|
+
controls.update();
|
|
269
|
+
document.getElementById("iBounds").textContent =
|
|
270
|
+
`${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}`;
|
|
271
|
+
}
|
|
272
|
+
reframe();
|
|
273
|
+
|
|
274
|
+
// ── Info panel ─────────────────────────────────────────
|
|
275
|
+
document.getElementById("iMesh").textContent = String(meshCount);
|
|
276
|
+
document.getElementById("iVerts").textContent = vertCount.toLocaleString();
|
|
277
|
+
document.getElementById("iMats").textContent = String(mats.size);
|
|
278
|
+
|
|
279
|
+
// ── UI wiring ───────────────────────────────────────────
|
|
280
|
+
const $ = (id) => document.getElementById(id);
|
|
281
|
+
const wire = (id, vid, fn, fmt) => {
|
|
282
|
+
const el = $(id), lab = $(vid);
|
|
283
|
+
const upd = () => { const v = parseFloat(el.value); fn(v); lab.textContent = fmt(v); };
|
|
284
|
+
el.addEventListener("input", upd); upd();
|
|
285
|
+
};
|
|
286
|
+
wire("exp", "vExp", (v) => { renderer.toneMappingExposure = v; }, (v) => v.toFixed(2));
|
|
287
|
+
wire("env", "vEnv", (v) => { scene.environmentIntensity = v; }, (v) => v.toFixed(2));
|
|
288
|
+
wire("key", "vKey", (v) => { key.intensity = v; }, (v) => v.toFixed(2));
|
|
289
|
+
const rotEl = $("rot");
|
|
290
|
+
rotEl.addEventListener("input", () => {
|
|
291
|
+
autoRotate = false;
|
|
292
|
+
autoBtn.textContent = "↻ auto-rotate: OFF";
|
|
293
|
+
const deg = parseFloat(rotEl.value);
|
|
294
|
+
model.rotation.y = deg * Math.PI / 180;
|
|
295
|
+
$("vRot").textContent = deg.toFixed(0) + "°";
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let autoRotate = true;
|
|
299
|
+
const autoBtn = $("autoRotateToggle");
|
|
300
|
+
autoBtn.addEventListener("click", () => {
|
|
301
|
+
autoRotate = !autoRotate;
|
|
302
|
+
autoBtn.textContent = "↻ auto-rotate: " + (autoRotate ? "ON" : "OFF");
|
|
303
|
+
});
|
|
304
|
+
$("reframeBtn").addEventListener("click", reframe);
|
|
305
|
+
|
|
306
|
+
// ── Render loop ────────────────────────────────────────
|
|
307
|
+
const clock = new THREE.Clock();
|
|
308
|
+
let frames = 0, fpsAcc = 0;
|
|
309
|
+
renderer.setAnimationLoop(() => {
|
|
310
|
+
const dt = clock.getDelta();
|
|
311
|
+
if (autoRotate) {
|
|
312
|
+
model.rotation.y += dt * 0.6;
|
|
313
|
+
const deg = (model.rotation.y * 180 / Math.PI) % 360;
|
|
314
|
+
rotEl.value = ((deg + 360) % 360).toFixed(0);
|
|
315
|
+
$("vRot").textContent = "auto";
|
|
316
|
+
}
|
|
317
|
+
if (mixer) mixer.update(dt);
|
|
318
|
+
controls.update();
|
|
319
|
+
renderer.render(scene, camera);
|
|
320
|
+
frames += 1; fpsAcc += dt;
|
|
321
|
+
if (fpsAcc >= 0.5) {
|
|
322
|
+
$("iFps").textContent = (frames / fpsAcc).toFixed(0) + " fps";
|
|
323
|
+
frames = 0; fpsAcc = 0;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
addEventListener("resize", () => {
|
|
328
|
+
camera.aspect = innerWidth / innerHeight;
|
|
329
|
+
camera.updateProjectionMatrix();
|
|
330
|
+
renderer.setSize(innerWidth, innerHeight);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
S(`✓ loaded · ${meshCount} mesh · ${vertCount.toLocaleString()} verts · drag to orbit, scroll to zoom`);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
S("ERROR: " + (e && e.stack ? e.stack : (e && e.message) || e), true);
|
|
336
|
+
}
|
|
337
|
+
})();
|
|
338
|
+
</script>
|
|
339
|
+
</body>
|
|
340
|
+
</html>
|
package/public/onboarding.css
CHANGED
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
}
|
|
110
110
|
.onb-deck {
|
|
111
111
|
font-family: var(--font-human, var(--mono));
|
|
112
|
-
font-size:
|
|
112
|
+
font-size: 14px;
|
|
113
113
|
color: var(--text-soft);
|
|
114
114
|
line-height: 1.55;
|
|
115
115
|
margin-top: 8px;
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
content: ">";
|
|
180
180
|
color: var(--lime);
|
|
181
181
|
font-weight: 700;
|
|
182
|
-
font-size:
|
|
182
|
+
font-size: 14px;
|
|
183
183
|
font-family: var(--mono);
|
|
184
184
|
padding: 9px 0 0 11px;
|
|
185
185
|
align-self: flex-start;
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
}
|
|
333
333
|
.onb-voice-pitch {
|
|
334
334
|
font-family: var(--font-human, "Inter", system-ui, sans-serif);
|
|
335
|
-
font-size:
|
|
335
|
+
font-size: 14px;
|
|
336
336
|
line-height: 1.6;
|
|
337
337
|
color: var(--text-soft);
|
|
338
338
|
margin: 0;
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
}
|
|
394
394
|
.onb-cast-next-arrow {
|
|
395
395
|
font-family: var(--mono);
|
|
396
|
-
font-size:
|
|
396
|
+
font-size: 14px;
|
|
397
397
|
font-weight: 700;
|
|
398
398
|
color: var(--text-faint);
|
|
399
399
|
align-self: center;
|
|
@@ -592,7 +592,7 @@
|
|
|
592
592
|
color: var(--text-dim);
|
|
593
593
|
cursor: pointer;
|
|
594
594
|
font-family: var(--font-human, "Inter", system-ui, sans-serif);
|
|
595
|
-
font-size:
|
|
595
|
+
font-size: 14px;
|
|
596
596
|
font-weight: 500;
|
|
597
597
|
letter-spacing: -0.005em;
|
|
598
598
|
line-height: 1.2;
|
package/public/quote-cta.css
CHANGED
|
@@ -80,9 +80,10 @@
|
|
|
80
80
|
white-space: nowrap;
|
|
81
81
|
}
|
|
82
82
|
/* Adjourned-room state · Probe / Second hide (they post to a
|
|
83
|
-
closed room), but Save
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
closed room), but Save and Thread stay — bookmarking and opening a
|
|
84
|
+
private 1:1 follow-up thread are both primary review-mode uses
|
|
85
|
+
(a thread spawns its own live room, independent of the parent). */
|
|
86
|
+
.qcta.qcta-readonly .qcta-btn:not(.qcta-btn-save):not(.qcta-btn-thread) { display: none; }
|
|
86
87
|
.qcta.qcta-readonly .qcta-hint { display: inline-flex; align-items: center; }
|
|
87
88
|
|
|
88
89
|
/* ─── Save toast ─────────────────────────────────────────────
|
|
@@ -220,7 +221,7 @@
|
|
|
220
221
|
content: ">";
|
|
221
222
|
color: var(--lime);
|
|
222
223
|
font-weight: 700;
|
|
223
|
-
font-size:
|
|
224
|
+
font-size: 14px;
|
|
224
225
|
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
225
226
|
padding: 9px 0 0 11px;
|
|
226
227
|
align-self: flex-start;
|