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.
- package/dist/boot.js +327 -50
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +327 -50
- package/dist/cli.js.map +1 -1
- package/dist/server.js +201 -40
- 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/agent-overlay.css +14 -6
- package/public/agent-overlay.js +6 -6
- package/public/agent-profile.css +9 -12
- package/public/agent-profile.js +91 -38
- package/public/app.js +471 -528
- package/public/avatar-3d-snap.js +205 -0
- package/public/avatar-3d.js +836 -0
- package/public/avatar-customizer.html +274 -0
- package/public/avatar3d-editor.css +240 -0
- package/public/avatar3d-editor.js +484 -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 +25 -14
- package/public/home.html +78 -16
- package/public/i18n.js +8 -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/new-style6.glb +0 -0
- package/public/icons/office.glb +0 -0
- package/public/icons/stuff.glb +0 -0
- package/public/index.html +169 -141
- package/public/magazine.html +1 -1
- package/public/new-agent.js +46 -20
- package/public/newspaper.html +1 -1
- package/public/office-viewer.html +340 -0
- package/public/ppt.html +1 -1
- package/public/stuff-viewer.html +330 -0
- package/public/thread.css +16 -15
- package/public/user-settings.css +7 -31
- package/public/user-settings.js +75 -89
- 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 +19 -7
- package/public/voice-3d.js +1407 -432
- package/public/voice-replay.js +21 -0
- package/public/avatar-skill.js +0 -629
- package/public/avatars/chair-blink.svg +0 -1
- package/public/avatars/chair.svg +0 -1
- package/public/avatars/first-principles.svg +0 -1
- package/public/avatars/historian.svg +0 -1
- package/public/avatars/long-horizon.svg +0 -1
- package/public/avatars/phenomenologist.svg +0 -1
- package/public/avatars/socrates.svg +0 -1
- package/public/avatars/user-empathy.svg +0 -1
- package/public/avatars/value-investor.svg +0 -1
- package/public/icons/folded-sidebar.png +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>stuff.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>// stuff.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/stuff.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 { RoomEnvironment } = await import("/vendor/RoomEnvironment.js");
|
|
114
|
+
// Many Blender / Spline exports run the `meshopt` compressor on
|
|
115
|
+
// attributes (EXT_meshopt_compression). GLTFLoader refuses to
|
|
116
|
+
// load them until a decoder is attached. The decoder is a WASM-
|
|
117
|
+
// ish module shipped under three's examples; we vendored it
|
|
118
|
+
// into /vendor so cold-load works offline.
|
|
119
|
+
const { MeshoptDecoder } = await import("/vendor/meshopt_decoder.module.js");
|
|
120
|
+
|
|
121
|
+
// ── Renderer / scene / camera ───────────────────────────
|
|
122
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
123
|
+
renderer.setSize(innerWidth, innerHeight);
|
|
124
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
125
|
+
renderer.shadowMap.enabled = true;
|
|
126
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
127
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
128
|
+
renderer.toneMappingExposure = 1.0;
|
|
129
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
130
|
+
document.body.appendChild(renderer.domElement);
|
|
131
|
+
|
|
132
|
+
const scene = new THREE.Scene();
|
|
133
|
+
scene.background = new THREE.Color(0x16181c);
|
|
134
|
+
|
|
135
|
+
const camera = new THREE.PerspectiveCamera(38, innerWidth / innerHeight, 0.01, 100);
|
|
136
|
+
camera.position.set(2.0, 1.6, 2.8);
|
|
137
|
+
|
|
138
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
139
|
+
controls.enableDamping = true;
|
|
140
|
+
controls.dampingFactor = 0.08;
|
|
141
|
+
controls.target.set(0, 0.5, 0);
|
|
142
|
+
controls.update();
|
|
143
|
+
|
|
144
|
+
// IBL · same recipe as the avatar viewer; gives any GLB
|
|
145
|
+
// PBR materials something to reflect.
|
|
146
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
147
|
+
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
148
|
+
scene.environmentIntensity = 0.85;
|
|
149
|
+
|
|
150
|
+
// Direct lights · key + fill + rim. Soft enough that PBR
|
|
151
|
+
// doesn't blow out, strong enough to read shape on matte
|
|
152
|
+
// surfaces.
|
|
153
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x2a3140, 0.32));
|
|
154
|
+
const key = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
155
|
+
key.position.set(3, 5, 4);
|
|
156
|
+
key.castShadow = true;
|
|
157
|
+
key.shadow.mapSize.set(1024, 1024);
|
|
158
|
+
key.shadow.camera.near = 0.1;
|
|
159
|
+
key.shadow.camera.far = 20;
|
|
160
|
+
key.shadow.camera.top = 4;
|
|
161
|
+
key.shadow.camera.bottom = -4;
|
|
162
|
+
key.shadow.camera.left = -4;
|
|
163
|
+
key.shadow.camera.right = 4;
|
|
164
|
+
key.shadow.bias = -0.0005;
|
|
165
|
+
scene.add(key);
|
|
166
|
+
const rim = new THREE.DirectionalLight(0xbfd4ff, 0.35);
|
|
167
|
+
rim.position.set(-4, 3, -3);
|
|
168
|
+
scene.add(rim);
|
|
169
|
+
|
|
170
|
+
// Ground · circular disk so shadows can land; matches the
|
|
171
|
+
// dark BG so it reads as "stage" not "floor".
|
|
172
|
+
const ground = new THREE.Mesh(
|
|
173
|
+
new THREE.CircleGeometry(4.0, 64),
|
|
174
|
+
new THREE.MeshStandardMaterial({ color: 0x1d2026, roughness: 1, metalness: 0 }),
|
|
175
|
+
);
|
|
176
|
+
ground.rotation.x = -Math.PI / 2;
|
|
177
|
+
ground.position.y = -0.001;
|
|
178
|
+
ground.receiveShadow = true;
|
|
179
|
+
scene.add(ground);
|
|
180
|
+
|
|
181
|
+
// Grid for scale reference · soft, low-contrast so it doesn't
|
|
182
|
+
// compete with the model.
|
|
183
|
+
const grid = new THREE.GridHelper(8, 32, 0x2a3140, 0x222831);
|
|
184
|
+
grid.material.transparent = true;
|
|
185
|
+
grid.material.opacity = 0.5;
|
|
186
|
+
scene.add(grid);
|
|
187
|
+
|
|
188
|
+
// ── Load GLB ────────────────────────────────────────────
|
|
189
|
+
S("downloading + parsing " + GLB + " …");
|
|
190
|
+
// The decoder boots its WASM lazily on first use — `ready` is a
|
|
191
|
+
// Promise we have to await so subsequent .load() calls find an
|
|
192
|
+
// initialised decoder. Without this the loader fires the same
|
|
193
|
+
// "setMeshoptDecoder must be called before loading compressed
|
|
194
|
+
// files" again on the next compressed buffer view.
|
|
195
|
+
await MeshoptDecoder.ready;
|
|
196
|
+
const loader = new GLTFLoader();
|
|
197
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
198
|
+
const gltf = await new Promise((resolve, reject) => {
|
|
199
|
+
loader.load(
|
|
200
|
+
GLB,
|
|
201
|
+
(g) => resolve(g),
|
|
202
|
+
(xhr) => {
|
|
203
|
+
if (xhr.total) {
|
|
204
|
+
S(`downloading … ${((xhr.loaded / xhr.total) * 100).toFixed(0)} %`);
|
|
205
|
+
} else {
|
|
206
|
+
S(`downloading … ${(xhr.loaded / 1024).toFixed(0)} KB`);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
(err) => reject(err),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const model = gltf.scene;
|
|
214
|
+
// Enable shadows on every mesh in the imported tree.
|
|
215
|
+
let meshCount = 0;
|
|
216
|
+
let vertCount = 0;
|
|
217
|
+
const mats = new Set();
|
|
218
|
+
model.traverse((node) => {
|
|
219
|
+
if (node.isMesh) {
|
|
220
|
+
node.castShadow = true;
|
|
221
|
+
node.receiveShadow = true;
|
|
222
|
+
meshCount += 1;
|
|
223
|
+
if (node.geometry && node.geometry.attributes && node.geometry.attributes.position) {
|
|
224
|
+
vertCount += node.geometry.attributes.position.count;
|
|
225
|
+
}
|
|
226
|
+
if (node.material) {
|
|
227
|
+
if (Array.isArray(node.material)) node.material.forEach((m) => mats.add(m));
|
|
228
|
+
else mats.add(node.material);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
scene.add(model);
|
|
233
|
+
|
|
234
|
+
// Animations · play all clips at 1× if present.
|
|
235
|
+
let mixer = null;
|
|
236
|
+
if (gltf.animations && gltf.animations.length) {
|
|
237
|
+
mixer = new THREE.AnimationMixer(model);
|
|
238
|
+
for (const clip of gltf.animations) mixer.clipAction(clip).play();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Fit camera to model bounds ─────────────────────────
|
|
242
|
+
function reframe() {
|
|
243
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
244
|
+
const size = box.getSize(new THREE.Vector3());
|
|
245
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
246
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
247
|
+
const fov = camera.fov * Math.PI / 180;
|
|
248
|
+
const dist = (maxDim / 2) / Math.tan(fov / 2) * 1.7;
|
|
249
|
+
camera.position.set(
|
|
250
|
+
center.x + dist * 0.6,
|
|
251
|
+
center.y + dist * 0.55,
|
|
252
|
+
center.z + dist * 0.9,
|
|
253
|
+
);
|
|
254
|
+
camera.near = Math.max(0.005, maxDim / 200);
|
|
255
|
+
camera.far = Math.max(20, dist * 4);
|
|
256
|
+
camera.updateProjectionMatrix();
|
|
257
|
+
controls.target.copy(center);
|
|
258
|
+
controls.update();
|
|
259
|
+
document.getElementById("iBounds").textContent =
|
|
260
|
+
`${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}`;
|
|
261
|
+
}
|
|
262
|
+
reframe();
|
|
263
|
+
|
|
264
|
+
// ── Info panel ─────────────────────────────────────────
|
|
265
|
+
document.getElementById("iMesh").textContent = String(meshCount);
|
|
266
|
+
document.getElementById("iVerts").textContent = vertCount.toLocaleString();
|
|
267
|
+
document.getElementById("iMats").textContent = String(mats.size);
|
|
268
|
+
|
|
269
|
+
// ── UI wiring ───────────────────────────────────────────
|
|
270
|
+
const $ = (id) => document.getElementById(id);
|
|
271
|
+
const wire = (id, vid, fn, fmt) => {
|
|
272
|
+
const el = $(id), lab = $(vid);
|
|
273
|
+
const upd = () => { const v = parseFloat(el.value); fn(v); lab.textContent = fmt(v); };
|
|
274
|
+
el.addEventListener("input", upd); upd();
|
|
275
|
+
};
|
|
276
|
+
wire("exp", "vExp", (v) => { renderer.toneMappingExposure = v; }, (v) => v.toFixed(2));
|
|
277
|
+
wire("env", "vEnv", (v) => { scene.environmentIntensity = v; }, (v) => v.toFixed(2));
|
|
278
|
+
wire("key", "vKey", (v) => { key.intensity = v; }, (v) => v.toFixed(2));
|
|
279
|
+
const rotEl = $("rot");
|
|
280
|
+
rotEl.addEventListener("input", () => {
|
|
281
|
+
autoRotate = false;
|
|
282
|
+
autoBtn.textContent = "↻ auto-rotate: OFF";
|
|
283
|
+
const deg = parseFloat(rotEl.value);
|
|
284
|
+
model.rotation.y = deg * Math.PI / 180;
|
|
285
|
+
$("vRot").textContent = deg.toFixed(0) + "°";
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
let autoRotate = true;
|
|
289
|
+
const autoBtn = $("autoRotateToggle");
|
|
290
|
+
autoBtn.addEventListener("click", () => {
|
|
291
|
+
autoRotate = !autoRotate;
|
|
292
|
+
autoBtn.textContent = "↻ auto-rotate: " + (autoRotate ? "ON" : "OFF");
|
|
293
|
+
});
|
|
294
|
+
$("reframeBtn").addEventListener("click", reframe);
|
|
295
|
+
|
|
296
|
+
// ── Render loop ────────────────────────────────────────
|
|
297
|
+
const clock = new THREE.Clock();
|
|
298
|
+
let frames = 0, fpsAcc = 0;
|
|
299
|
+
renderer.setAnimationLoop(() => {
|
|
300
|
+
const dt = clock.getDelta();
|
|
301
|
+
if (autoRotate) {
|
|
302
|
+
model.rotation.y += dt * 0.6;
|
|
303
|
+
const deg = (model.rotation.y * 180 / Math.PI) % 360;
|
|
304
|
+
rotEl.value = ((deg + 360) % 360).toFixed(0);
|
|
305
|
+
$("vRot").textContent = "auto";
|
|
306
|
+
}
|
|
307
|
+
if (mixer) mixer.update(dt);
|
|
308
|
+
controls.update();
|
|
309
|
+
renderer.render(scene, camera);
|
|
310
|
+
frames += 1; fpsAcc += dt;
|
|
311
|
+
if (fpsAcc >= 0.5) {
|
|
312
|
+
$("iFps").textContent = (frames / fpsAcc).toFixed(0) + " fps";
|
|
313
|
+
frames = 0; fpsAcc = 0;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
addEventListener("resize", () => {
|
|
318
|
+
camera.aspect = innerWidth / innerHeight;
|
|
319
|
+
camera.updateProjectionMatrix();
|
|
320
|
+
renderer.setSize(innerWidth, innerHeight);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
S(`✓ loaded · ${meshCount} mesh · ${vertCount.toLocaleString()} verts · drag to orbit, scroll to zoom`);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
S("ERROR: " + (e && e.stack ? e.stack : (e && e.message) || e), true);
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
</script>
|
|
329
|
+
</body>
|
|
330
|
+
</html>
|
package/public/thread.css
CHANGED
|
@@ -204,8 +204,9 @@
|
|
|
204
204
|
the director's agent-profile overlay — wired through the
|
|
205
205
|
standard `[data-agent-profile]` capture-phase handler in
|
|
206
206
|
agent-profile.js. */
|
|
207
|
-
width:
|
|
208
|
-
height:
|
|
207
|
+
width: 36px;
|
|
208
|
+
height: 36px;
|
|
209
|
+
margin-top: -10px;
|
|
209
210
|
flex-shrink: 0;
|
|
210
211
|
image-rendering: pixelated;
|
|
211
212
|
image-rendering: crisp-edges;
|
|
@@ -399,11 +400,11 @@
|
|
|
399
400
|
order: 99;
|
|
400
401
|
}
|
|
401
402
|
.thread-float-messages article.msg .msg-bubble {
|
|
402
|
-
/*
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
font-size:
|
|
406
|
-
line-height: 1.
|
|
403
|
+
/* Thread bubbles read at the app body register (14px) — tighter than
|
|
404
|
+
the main chat's 16px since the float panel is a narrower column.
|
|
405
|
+
line-height eased to 1.6 to keep prose rhythm at the smaller size. */
|
|
406
|
+
font-size: 14px;
|
|
407
|
+
line-height: 1.6;
|
|
407
408
|
}
|
|
408
409
|
/* In threads we don't show round-end vote cards or chair pick
|
|
409
410
|
kickers — there are no rounds, no chair. Hide aggressively. */
|
|
@@ -922,14 +923,14 @@
|
|
|
922
923
|
actions. Hidden by default, slides in on article hover. */
|
|
923
924
|
.msg-toolbar {
|
|
924
925
|
position: absolute;
|
|
925
|
-
/*
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
top: -
|
|
932
|
-
right:
|
|
926
|
+
/* Sits at the RIGHT END of the meta row (the timestamp no longer
|
|
927
|
+
pins to the right — see `.msg-time` — so the far-right is free),
|
|
928
|
+
vertically aligned with that line rather than floating above the
|
|
929
|
+
message. The small negative top centres the ~26px chip on the
|
|
930
|
+
~16px meta line; right:0 puts it flush with the content column's
|
|
931
|
+
right edge. */
|
|
932
|
+
top: -4px;
|
|
933
|
+
right: 0;
|
|
933
934
|
z-index: 3;
|
|
934
935
|
display: inline-flex;
|
|
935
936
|
align-items: center;
|
package/public/user-settings.css
CHANGED
|
@@ -452,25 +452,16 @@
|
|
|
452
452
|
gap: 12px;
|
|
453
453
|
}
|
|
454
454
|
.us-avatar-frame {
|
|
455
|
-
width:
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
.us-avatar-frame svg {
|
|
460
|
+
/* 3D portrait PNG · the capture is already framed head-and-shoulders
|
|
461
|
+
(face-anchored, identical to the agent profile avatar), so just cover-fit. */
|
|
462
|
+
.us-avatar-frame img {
|
|
471
463
|
width: 100%; height: 100%;
|
|
472
|
-
|
|
473
|
-
image-rendering: -moz-crisp-edges;
|
|
464
|
+
object-fit: cover;
|
|
474
465
|
display: block;
|
|
475
466
|
}
|
|
476
467
|
|
|
@@ -494,21 +485,6 @@
|
|
|
494
485
|
}
|
|
495
486
|
.us-mini-btn:hover { border-color: var(--lime); color: var(--lime); }
|
|
496
487
|
.us-mini-btn-mark { font-size: 11px; line-height: 1; color: var(--lime); }
|
|
497
|
-
.us-mini-btn.primary {
|
|
498
|
-
background: var(--lime);
|
|
499
|
-
color: var(--bg);
|
|
500
|
-
border-color: var(--lime);
|
|
501
|
-
}
|
|
502
|
-
.us-mini-btn.primary:hover { background: transparent; color: var(--lime); }
|
|
503
|
-
.us-mini-btn.danger {
|
|
504
|
-
border-color: rgba(181, 112, 106, 0.5);
|
|
505
|
-
color: var(--red);
|
|
506
|
-
}
|
|
507
|
-
.us-mini-btn.danger:hover {
|
|
508
|
-
background: var(--red);
|
|
509
|
-
color: var(--bg);
|
|
510
|
-
border-color: var(--red);
|
|
511
|
-
}
|
|
512
488
|
|
|
513
489
|
/* Inputs (shared with API Key section) */
|
|
514
490
|
.us-input-wrap {
|