@echomem/mcp 1.4.44 → 1.4.45
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/README.md +28 -25
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +2232 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/context-analysis/claude-native-canonical.js +2 -2
- package/dist/context-analysis/vendored-canonical.js +2 -2
- package/dist/context-analysis/workspace-report.js +3 -3
- package/dist/forensics.js +1531 -0
- package/dist/hud/hooks.js +31 -43
- package/dist/index.js +79 -15
- package/dist/local-data-paths.js +38 -0
- package/dist/migrate.js +140 -70
- package/dist/report.js +721 -0
- package/dist/save-checkpoint-hook.js +1 -1
- package/dist/setup-page/client-core.js +372 -18
- package/dist/setup-page/client-extraction.js +204 -37
- package/dist/setup-page/client-lifecycle.js +101 -31
- package/dist/setup-page/client-report-audit.js +819 -0
- package/dist/setup-page/client-report-city.js +356 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +2 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +31 -1
- package/dist/setup-page/styles-foundation.js +89 -0
- package/dist/setup-page/styles-mvp.js +155 -10
- package/dist/setup-page/styles-website-alignment.js +204 -0
- package/dist/setup-page/styles.js +4 -0
- package/dist/setup-page.js +4 -4
- package/dist/setup-preview.js +212 -4
- package/dist/setup.js +702 -321
- package/dist/source-session.js +3 -9
- package/dist/v1-contract.js +8 -0
- package/package.json +7 -9
- package/dist/config-files.js +0 -63
- package/dist/local-jsonl.js +0 -87
- package/dist/onboarding-stats.js +0 -16
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Echo extraction plate</title>
|
|
6
|
+
<style>
|
|
7
|
+
*{margin:0;padding:0;box-sizing:border-box;}
|
|
8
|
+
html,body{width:100%;height:100%;background:transparent;overflow:hidden;}
|
|
9
|
+
#plate{width:100%;height:100%;display:block;}
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="plate"></div>
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
{ "imports": { "three": "./vendor/three.module.min.js" } }
|
|
16
|
+
</script>
|
|
17
|
+
<script type="module">
|
|
18
|
+
// The real Echo clay city (ported from echo-ai-city-only / EchoCityScene), used as the extraction
|
|
19
|
+
// plate: it renders the user's full repo-city from /report (token signpost = total tokens / input%).
|
|
20
|
+
// The city stays standing (extraction is additive). While /progress is running, a continuous gentle
|
|
21
|
+
// stream of memory motes flows from the city into the signpost; each completed conversation fires a
|
|
22
|
+
// brighter BURST aimed at that conversation's repo building (p.latestRepo). The signpost cross-fades to
|
|
23
|
+
// the real memory count (extracted). On any WebGL/data failure it stays transparent (SVG fallback shows).
|
|
24
|
+
import * as THREE from 'three';
|
|
25
|
+
import { RoundedBoxGeometry } from './vendor/RoundedBoxGeometry.js';
|
|
26
|
+
|
|
27
|
+
const params = new URLSearchParams(location.search);
|
|
28
|
+
const nonce = params.get('nonce') || '';
|
|
29
|
+
|
|
30
|
+
const TOOL = { codex: 0x10a37f, claude: 0xd97757 };
|
|
31
|
+
const TRAY_COL = 0xece5d3;
|
|
32
|
+
const TPU = 80e6, BASE_EDGE = 36, TOWER = 1.4, MIN_TOKENS = 4e6, TRAY_TOP = 25;
|
|
33
|
+
const LABEL_MIN_BASE = 24, MARGIN = 56, GAP = 16, MARGIN_FACTOR = 1.3;
|
|
34
|
+
const fmtTok = (n) => n >= 1e9 ? (n / 1e9).toFixed(2) + 'B' : n >= 1e6 ? Math.round(n / 1e6) + 'M' : Math.round(n / 1e3) + 'K';
|
|
35
|
+
|
|
36
|
+
const mount = document.getElementById('plate');
|
|
37
|
+
|
|
38
|
+
(async function () {
|
|
39
|
+
let report = null;
|
|
40
|
+
try { report = await loadReport(); } catch (_) { return; }
|
|
41
|
+
const repos = buildRepos(report);
|
|
42
|
+
if (!repos.length) return; // nothing to draw → leave transparent, SVG fallback shows
|
|
43
|
+
try { run(repos, (report && report.scale) || {}); } catch (e) { console.error('extraction-plate render failed', e); }
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
async function loadReport() {
|
|
47
|
+
if (!nonce) return null;
|
|
48
|
+
for (let i = 0; i < 80; i++) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch('/report?nonce=' + encodeURIComponent(nonce), { credentials: 'omit', cache: 'no-store' });
|
|
51
|
+
if (res.status === 200) return await res.json();
|
|
52
|
+
if (res.status !== 202) return null;
|
|
53
|
+
} catch (_) { return null; }
|
|
54
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildRepos(report) {
|
|
60
|
+
const source = Array.isArray(report && report.repos) ? report.repos : [];
|
|
61
|
+
const usable = source.filter((r) => Number(r && r.tokens) > 0).sort((a, b) => (Number(b.tokens) || 0) - (Number(a.tokens) || 0)).slice(0, 16);
|
|
62
|
+
return usable.map((repo, i) => {
|
|
63
|
+
let x, z;
|
|
64
|
+
if (i === 0) { x = -36; z = -58; }
|
|
65
|
+
else if (i === 1) { x = 128; z = -12; }
|
|
66
|
+
else { const a = i * 2.399963229728653, rad = 72 + Math.sqrt(i) * 42; x = Math.cos(a) * rad; z = Math.sin(a) * rad + 28; }
|
|
67
|
+
const tool = (repo.dominantProvider === 'codex' || repo.dominantProvider === 'claude') ? repo.dominantProvider : 'claude';
|
|
68
|
+
return { name: String(repo.name || ('Repo ' + (i + 1))), x, z, tool, tokens: Number(repo.tokens) || 0, pin: i < 2 };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function run(repos, scale) {
|
|
73
|
+
for (const o of repos) { const u = o.tokens / TPU; const b = Math.cbrt((u * BASE_EDGE * BASE_EDGE * BASE_EDGE) / TOWER); o.w = b; o.d = b; o.h = b * TOWER; }
|
|
74
|
+
const all = repos.filter((o) => o.tokens >= MIN_TOKENS);
|
|
75
|
+
separate(all);
|
|
76
|
+
let minX = 1e9, maxX = -1e9, minZ = 1e9, maxZ = -1e9;
|
|
77
|
+
for (const o of all) { minX = Math.min(minX, o.x - o.w / 2); maxX = Math.max(maxX, o.x + o.w / 2); minZ = Math.min(minZ, o.z - o.d / 2); maxZ = Math.max(maxZ, o.z + o.d / 2); }
|
|
78
|
+
const CX = (minX + maxX) / 2, CZ = (minZ + maxZ) / 2;
|
|
79
|
+
const traySize = Math.max(maxX - minX, maxZ - minZ) + 2 * MARGIN, trayW = traySize, trayD = traySize;
|
|
80
|
+
|
|
81
|
+
const canvas = document.createElement('canvas');
|
|
82
|
+
canvas.style.cssText = 'display:block;width:100%;height:100%;';
|
|
83
|
+
mount.appendChild(canvas);
|
|
84
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
85
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
86
|
+
renderer.setClearColor(0xffffff, 0);
|
|
87
|
+
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
88
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
89
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05;
|
|
90
|
+
const scene = new THREE.Scene();
|
|
91
|
+
|
|
92
|
+
scene.environment = (() => {
|
|
93
|
+
const c = document.createElement('canvas'); c.width = 512; c.height = 256;
|
|
94
|
+
const g = c.getContext('2d');
|
|
95
|
+
const gr = g.createLinearGradient(0, 0, 0, 256); gr.addColorStop(0, '#ffffff'); gr.addColorStop(0.5, '#eef1f6'); gr.addColorStop(1, '#d2d7e0');
|
|
96
|
+
g.fillStyle = gr; g.fillRect(0, 0, 512, 256);
|
|
97
|
+
const sp = g.createRadialGradient(150, 62, 8, 150, 62, 150); sp.addColorStop(0, 'rgba(255,255,255,0.95)'); sp.addColorStop(1, 'rgba(255,255,255,0)');
|
|
98
|
+
g.fillStyle = sp; g.fillRect(0, 0, 512, 256);
|
|
99
|
+
const t = new THREE.CanvasTexture(c); t.mapping = THREE.EquirectangularReflectionMapping; t.colorSpace = THREE.SRGBColorSpace;
|
|
100
|
+
const pm = new THREE.PMREMGenerator(renderer); pm.compileEquirectangularShader();
|
|
101
|
+
const e = pm.fromEquirectangular(t).texture; t.dispose(); pm.dispose(); return e;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 6000);
|
|
105
|
+
const TARGET = new THREE.Vector3(CX, 78, CZ);
|
|
106
|
+
camera.position.copy(TARGET).add(new THREE.Vector3(760, 660, 760)); camera.lookAt(TARGET);
|
|
107
|
+
|
|
108
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0xdadce2, 0.7));
|
|
109
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.2));
|
|
110
|
+
const key = new THREE.DirectionalLight(0xffffff, 2.3);
|
|
111
|
+
key.position.set(CX - 360, 560, CZ + 360); key.target.position.copy(TARGET);
|
|
112
|
+
key.castShadow = true; key.shadow.mapSize.set(2048, 2048); key.shadow.radius = 16; key.shadow.bias = -0.0004;
|
|
113
|
+
const sh = Math.max(trayW, trayD) * 0.8 + 140, scam = key.shadow.camera;
|
|
114
|
+
scam.left = -sh; scam.right = sh; scam.top = sh; scam.bottom = -sh; scam.near = 100; scam.far = 1800;
|
|
115
|
+
scene.add(key); scene.add(key.target);
|
|
116
|
+
const floor = new THREE.Mesh(new THREE.PlaneGeometry(6000, 6000), new THREE.ShadowMaterial({ opacity: 0.22 }));
|
|
117
|
+
floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
|
|
118
|
+
|
|
119
|
+
const tray = new THREE.Mesh(new RoundedBoxGeometry(trayW, TRAY_TOP, trayD, 8, 12), new THREE.MeshPhysicalMaterial({ color: TRAY_COL, roughness: 0.6, clearcoat: 0.3, clearcoatRoughness: 0.5, envMapIntensity: 0.6 }));
|
|
120
|
+
tray.position.set(CX, TRAY_TOP / 2, CZ); tray.castShadow = true; tray.receiveShadow = true; scene.add(tray);
|
|
121
|
+
(() => {
|
|
122
|
+
const scl = 3, W = Math.round(trayW * scl), H = Math.round(trayD * scl), cv = document.createElement('canvas'); cv.width = W; cv.height = H;
|
|
123
|
+
const g = cv.getContext('2d'); const m = 14 * scl, rad = 26 * scl, rw = W - 2 * m, rh = H - 2 * m;
|
|
124
|
+
g.save(); g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.clip();
|
|
125
|
+
const cell = 21 * scl; g.strokeStyle = 'rgba(74,80,96,0.42)'; g.lineWidth = 1.8; g.beginPath();
|
|
126
|
+
for (let x = m; x <= m + rw + 0.5; x += cell) { g.moveTo(x, m); g.lineTo(x, m + rh); }
|
|
127
|
+
for (let y = m; y <= m + rh + 0.5; y += cell) { g.moveTo(m, y); g.lineTo(m + rw, y); }
|
|
128
|
+
g.stroke(); g.restore();
|
|
129
|
+
g.strokeStyle = 'rgba(120,126,140,0.26)'; g.lineWidth = 0.7 * scl; g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.stroke();
|
|
130
|
+
const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
|
|
131
|
+
const pl = new THREE.Mesh(new THREE.PlaneGeometry(trayW, trayD), new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false, toneMapped: false }));
|
|
132
|
+
pl.rotation.x = -Math.PI / 2; pl.position.set(CX, TRAY_TOP + 0.6, CZ); scene.add(pl);
|
|
133
|
+
})();
|
|
134
|
+
|
|
135
|
+
const clay = (hex) => { const c = new THREE.Color(hex); return new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.42, transmission: 0.85, ior: 1.34, thickness: 70, attenuationColor: c.clone(), attenuationDistance: 95, clearcoat: 0.28, clearcoatRoughness: 0.45, envMapIntensity: 1.05, transparent: true }); };
|
|
136
|
+
const mkLabel = (o) => {
|
|
137
|
+
const PW = 320, PH = 280, cv = document.createElement('canvas'); cv.width = PW; cv.height = PH;
|
|
138
|
+
const g = cv.getContext('2d'); g.textAlign = 'center'; g.shadowColor = 'rgba(0,0,0,0.34)'; g.shadowBlur = 9; g.shadowOffsetY = 1;
|
|
139
|
+
g.fillStyle = '#ffffff'; g.font = '800 ' + Math.round(PW * 0.19) + 'px "IBM Plex Mono", monospace'; g.fillText(fmtTok(o.tokens), PW / 2, PH * 0.2);
|
|
140
|
+
g.fillStyle = 'rgba(255,255,255,0.95)'; g.font = '600 ' + Math.round(PW * 0.078) + 'px "IBM Plex Mono", monospace';
|
|
141
|
+
const fw = g.measureText(o.name).width, mw = PW * 0.94; g.save(); g.translate(PW / 2, PH * 0.86); if (fw > mw) g.scale(mw / fw, mw / fw); g.fillText(o.name, 0, 0); g.restore();
|
|
142
|
+
const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
|
|
143
|
+
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, toneMapped: false, depthWrite: false, opacity: 0 }));
|
|
144
|
+
const ww = Math.min(Math.max(o.w * 0.95, 42), 128), hw = (ww * PH) / PW; sp.scale.set(ww, hw, 1);
|
|
145
|
+
return { sp, hw };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const items = []; let maxTop = 0;
|
|
149
|
+
for (const o of all) {
|
|
150
|
+
const r = Math.min(Math.min(o.w, o.h, o.d) * 0.12, 12);
|
|
151
|
+
const mesh = new THREE.Mesh(new RoundedBoxGeometry(o.w, o.h, o.d, 8, r), clay(TOOL[o.tool]));
|
|
152
|
+
mesh.castShadow = true; mesh.receiveShadow = true;
|
|
153
|
+
mesh.position.set(o.x, TRAY_TOP + o.h / 2 - 3, o.z); // full height, fixed — extraction is additive, the city never shrinks
|
|
154
|
+
scene.add(mesh);
|
|
155
|
+
const L = (o.w >= LABEL_MIN_BASE) ? mkLabel(o) : null;
|
|
156
|
+
if (L) { L.sp.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36 * L.hw, o.z); L.sp.material.opacity = 1; scene.add(L.sp); }
|
|
157
|
+
items.push({ o, mesh, L, base: new THREE.Color(TOOL[o.tool]), pop: 0, lit: 0 }); maxTop = Math.max(maxTop, TRAY_TOP + o.h);
|
|
158
|
+
}
|
|
159
|
+
const byName = {}; for (const it of items) byName[it.o.name] = it; // building lookup for aiming a burst at a repo
|
|
160
|
+
|
|
161
|
+
// Signpost on a post: a "before" face (total tokens / input%) cross-fades into the "after" face
|
|
162
|
+
// (memories created) as the city distills.
|
|
163
|
+
const sgx = CX + trayW * 0.3, sgz = CZ + trayD * 0.3, SIGN_Y = TRAY_TOP + 70;
|
|
164
|
+
const post = new THREE.Mesh(new THREE.CylinderGeometry(2.2, 2.2, 52, 12), new THREE.MeshStandardMaterial({ color: 0xddd9cc, roughness: 0.7 }));
|
|
165
|
+
post.position.set(sgx, TRAY_TOP + 26, sgz); post.castShadow = true; scene.add(post);
|
|
166
|
+
const mkSign = () => {
|
|
167
|
+
const cv = document.createElement('canvas'); cv.width = 640; cv.height = 300;
|
|
168
|
+
const tex = new THREE.CanvasTexture(cv); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
|
|
169
|
+
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, toneMapped: false, depthTest: false }));
|
|
170
|
+
sp.position.set(sgx, SIGN_Y, sgz); sp.scale.set(150, 70, 1); scene.add(sp);
|
|
171
|
+
return { cv, tex, sp };
|
|
172
|
+
};
|
|
173
|
+
const cardBg = (g) => {
|
|
174
|
+
g.clearRect(0, 0, 640, 300); g.textAlign = 'center';
|
|
175
|
+
g.fillStyle = 'rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8, 8, 624, 284, 30); g.fill();
|
|
176
|
+
g.strokeStyle = 'rgba(120,126,140,0.30)'; g.lineWidth = 3; g.beginPath(); g.roundRect(8, 8, 624, 284, 30); g.stroke();
|
|
177
|
+
};
|
|
178
|
+
const tokSign = mkSign();
|
|
179
|
+
(() => {
|
|
180
|
+
const g = tokSign.cv.getContext('2d'); cardBg(g);
|
|
181
|
+
g.fillStyle = '#1a1a1a'; g.font = '800 116px "IBM Plex Mono", monospace'; g.fillText(fmtTok(scale.totalTokens || 0), 320, 142);
|
|
182
|
+
g.fillStyle = '#6b6b6b'; g.font = '600 34px "IBM Plex Mono", monospace'; g.fillText('TOTAL TOKENS', 320, 196);
|
|
183
|
+
g.fillStyle = '#c7372f'; g.font = '800 52px "IBM Plex Mono", monospace'; g.fillText((scale.inputPct || 0) + '% INPUT', 320, 268);
|
|
184
|
+
tokSign.tex.needsUpdate = true;
|
|
185
|
+
})();
|
|
186
|
+
const memSign = mkSign(); memSign.sp.material.opacity = 0;
|
|
187
|
+
const drawMem = (mem) => {
|
|
188
|
+
const g = memSign.cv.getContext('2d'); cardBg(g);
|
|
189
|
+
g.fillStyle = '#1a3a8f'; g.font = '800 150px "IBM Plex Mono", monospace'; g.fillText(String(Math.round(mem)), 320, 168);
|
|
190
|
+
g.fillStyle = '#6b6b6b'; g.font = '600 38px "IBM Plex Mono", monospace'; g.fillText('MEMORIES CREATED', 320, 232);
|
|
191
|
+
memSign.tex.needsUpdate = true;
|
|
192
|
+
};
|
|
193
|
+
drawMem(0);
|
|
194
|
+
|
|
195
|
+
// Memory motes: a continuous gentle stream rises off the city into the memory bank while a conversation
|
|
196
|
+
// is being processed (so it is never frozen between completions), plus a brighter BURST aimed at a
|
|
197
|
+
// repo's building each time a conversation completes. The signpost pulses as bursts land.
|
|
198
|
+
const SG = new THREE.Vector3(sgx, SIGN_Y - 4, sgz), ARC = 52;
|
|
199
|
+
const motes = [];
|
|
200
|
+
for (let i = 0; i < 72; i++) {
|
|
201
|
+
const m = new THREE.Mesh(new THREE.SphereGeometry(1, 10, 10), new THREE.MeshBasicMaterial({ color: 0x9fc4ff, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false }));
|
|
202
|
+
m.visible = false; scene.add(m);
|
|
203
|
+
motes.push({ m, life: 0, t: 0, dur: 1, sz: 3, p0: new THREE.Vector3(), tgt: new THREE.Vector3() });
|
|
204
|
+
}
|
|
205
|
+
let signPulse = 0;
|
|
206
|
+
const launch = (it, big) => {
|
|
207
|
+
if (!it) return;
|
|
208
|
+
const f = motes.find((p) => p.life <= 0); if (!f) return;
|
|
209
|
+
const o = it.o, h = o.h;
|
|
210
|
+
f.p0.set(o.x + (Math.random() * 18 - 9), TRAY_TOP + h - 3 + 5, o.z + (Math.random() * 18 - 9));
|
|
211
|
+
f.tgt.copy(SG).add(new THREE.Vector3(Math.random() * 46 - 23, Math.random() * 22 - 6, Math.random() * 46 - 23));
|
|
212
|
+
f.life = 1; f.t = 0;
|
|
213
|
+
f.dur = big ? (0.7 + Math.random() * 0.4) : (1.0 + Math.random() * 0.6);
|
|
214
|
+
f.sz = big ? (3.4 + Math.random() * 2.8) : (1.7 + Math.random() * 1.9);
|
|
215
|
+
f.m.material.color.setHex(big ? (Math.random() < 0.7 ? 0xbfd6ff : 0xffe6c2) : 0x9fc4ff);
|
|
216
|
+
f.m.visible = true;
|
|
217
|
+
};
|
|
218
|
+
// pick a building weighted by token size, so the ambient flow + any fallback bursts favour bigger repos
|
|
219
|
+
const pickWeighted = () => {
|
|
220
|
+
let total = 0; for (const it of items) total += Math.sqrt(it.o.tokens || 1);
|
|
221
|
+
let r = Math.random() * total;
|
|
222
|
+
for (const it of items) { r -= Math.sqrt(it.o.tokens || 1); if (r <= 0) return it; }
|
|
223
|
+
return items[items.length - 1];
|
|
224
|
+
};
|
|
225
|
+
const fireBurst = (it, n) => {
|
|
226
|
+
if (it) { it.pop = 1; it.lit = Math.min(1, it.lit + 0.5); } // the building reacts (pop + flash) and keeps a "processed" glow
|
|
227
|
+
for (let k = 0; k < n; k++) launch(it, true);
|
|
228
|
+
signPulse = Math.max(signPulse, 1.9);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const corners = [];
|
|
232
|
+
// frame to maxTop + headroom so the floating labels and the signpost (which fly above the cube tops)
|
|
233
|
+
// are never clipped at the top edge.
|
|
234
|
+
const frameTop = maxTop + 70;
|
|
235
|
+
for (const sx of [CX - trayW / 2, CX + trayW / 2]) for (const sy of [0, frameTop]) for (const sz of [CZ - trayD / 2, CZ + trayD / 2]) corners.push(new THREE.Vector3(sx, sy, sz));
|
|
236
|
+
const autoFrame = () => {
|
|
237
|
+
const w = mount.clientWidth || 1, h = mount.clientHeight || 1; renderer.setSize(w, h, false);
|
|
238
|
+
const aspect = w / h; camera.updateMatrixWorld();
|
|
239
|
+
let a = 1e9, b = -1e9, c = 1e9, d = -1e9;
|
|
240
|
+
for (const cc of corners) { const q = cc.clone().applyMatrix4(camera.matrixWorldInverse); a = Math.min(a, q.x); b = Math.max(b, q.x); c = Math.min(c, q.y); d = Math.max(d, q.y); }
|
|
241
|
+
const mx = (a + b) / 2, my = (c + d) / 2, hw = ((b - a) / 2) * MARGIN_FACTOR, hh = ((d - c) / 2) * MARGIN_FACTOR;
|
|
242
|
+
const Vh = Math.max(hh, hw / aspect), Vw = Vh * aspect;
|
|
243
|
+
camera.left = mx - Vw; camera.right = mx + Vw; camera.top = my + Vh; camera.bottom = my - Vh; camera.updateProjectionMatrix();
|
|
244
|
+
};
|
|
245
|
+
autoFrame(); new ResizeObserver(autoFrame).observe(mount);
|
|
246
|
+
|
|
247
|
+
let actAmt = 0, actTarget = 0, running = false, mem = 0, shownMem = 0, ambientAcc = 0, clock = 0, prevCompleted = 0;
|
|
248
|
+
let last = performance.now();
|
|
249
|
+
const loop = (now) => {
|
|
250
|
+
requestAnimationFrame(loop);
|
|
251
|
+
const dt = Math.min(0.05, (now - last) / 1000); last = now; clock += dt;
|
|
252
|
+
if (Math.abs(actTarget - actAmt) > 0.001) actAmt += (actTarget - actAmt) * 0.06;
|
|
253
|
+
// signpost cross-fade: total-tokens "before" → memories "after" once extraction is engaged
|
|
254
|
+
tokSign.sp.material.opacity = Math.max(0, 1 - 1.6 * actAmt);
|
|
255
|
+
memSign.sp.material.opacity = Math.max(0, (actAmt - 0.12) / 0.88);
|
|
256
|
+
// per-building: a faint breathing shimmer while running, a POP (vertical stretch + bright flash) on
|
|
257
|
+
// the building whose conversation just completed, and a lasting "processed" glow that accumulates.
|
|
258
|
+
for (const it of items) {
|
|
259
|
+
it.pop *= 0.88;
|
|
260
|
+
const k = 1 + 0.2 * it.pop;
|
|
261
|
+
it.mesh.scale.set(1, k, 1);
|
|
262
|
+
it.mesh.position.y = TRAY_TOP + (it.o.h * k) / 2 - 3;
|
|
263
|
+
if (it.L) it.L.sp.position.y = TRAY_TOP + it.o.h * k - 3 + 6 + 0.36 * it.L.hw;
|
|
264
|
+
const breathe = running ? 0.05 * (0.5 + 0.5 * Math.sin(clock * 1.6 + it.o.x * 0.02)) : 0;
|
|
265
|
+
it.mesh.material.emissiveIntensity = breathe + 0.8 * it.pop + 0.2 * it.lit;
|
|
266
|
+
}
|
|
267
|
+
if (Math.abs(mem - shownMem) > 0.4) { shownMem += (mem - shownMem) * 0.1; drawMem(shownMem); }
|
|
268
|
+
// continuous ambient flow while a conversation is being processed
|
|
269
|
+
if (running) { ambientAcc += dt; while (ambientAcc > 0.11) { ambientAcc -= 0.11; launch(pickWeighted(), false); } }
|
|
270
|
+
for (const p of motes) {
|
|
271
|
+
if (p.life <= 0) continue;
|
|
272
|
+
p.t += dt / p.dur; const tt = p.t;
|
|
273
|
+
if (tt >= 1) { p.life = 0; p.m.visible = false; signPulse = Math.max(signPulse, 0.5); continue; }
|
|
274
|
+
const e = tt * tt * (3 - 2 * tt);
|
|
275
|
+
p.m.position.lerpVectors(p.p0, p.tgt, e); p.m.position.y += Math.sin(Math.PI * tt) * ARC;
|
|
276
|
+
const op = tt < 0.14 ? tt / 0.14 : (tt > 0.8 ? (1 - tt) / 0.2 : 1); p.m.material.opacity = op * 0.95;
|
|
277
|
+
const s = p.sz * (0.55 + 0.6 * Math.sin(Math.PI * Math.min(1, tt * 1.05))); p.m.scale.set(s, s, s);
|
|
278
|
+
}
|
|
279
|
+
if (signPulse > 0.002) { const k = 1 + 0.06 * signPulse; memSign.sp.scale.set(150 * k, 70 * k, 1); signPulse *= 0.9; }
|
|
280
|
+
else memSign.sp.scale.set(150, 70, 1);
|
|
281
|
+
renderer.render(scene, camera);
|
|
282
|
+
};
|
|
283
|
+
requestAnimationFrame((t) => { last = t; loop(t); });
|
|
284
|
+
|
|
285
|
+
// Drive the animation from real extraction progress: one burst per completed conversation, aimed at that
|
|
286
|
+
// conversation's repo building (p.latestRepo); the count is the real memory total (extracted). The city
|
|
287
|
+
// stays standing. Idle/pre-extract → the full city + the total-tokens signpost.
|
|
288
|
+
(async function pollProgress() {
|
|
289
|
+
if (!nonce) return;
|
|
290
|
+
for (;;) {
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch('/progress?nonce=' + encodeURIComponent(nonce), { credentials: 'omit' });
|
|
293
|
+
if (res.ok) {
|
|
294
|
+
const p = await res.json(); const status = p.status || 'idle';
|
|
295
|
+
const completed = p.completed || 0;
|
|
296
|
+
actTarget = (status === 'starting' || status === 'preparing' || status === 'running' || status === 'completed' || status === 'failed') ? 1 : 0;
|
|
297
|
+
running = status === 'running';
|
|
298
|
+
mem = p.extracted || 0;
|
|
299
|
+
if (status === 'idle') prevCompleted = 0;
|
|
300
|
+
if (completed > prevCompleted) {
|
|
301
|
+
const delta = completed - prevCompleted; prevCompleted = completed;
|
|
302
|
+
const src = (p.latestRepo && byName[p.latestRepo]) ? byName[p.latestRepo] : null;
|
|
303
|
+
for (let k = 0; k < delta; k++) fireBurst(src || pickWeighted(), 18);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (_) { /* keep last */ }
|
|
307
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function separate(items, iters = 400) {
|
|
313
|
+
for (let k = 0; k < iters; k++) {
|
|
314
|
+
let moved = false;
|
|
315
|
+
for (let i = 0; i < items.length; i++) for (let j = i + 1; j < items.length; j++) {
|
|
316
|
+
const a = items[i], b = items[j];
|
|
317
|
+
const hx = (a.w + b.w) / 2 + GAP, hz = (a.d + b.d) / 2 + GAP, dx = b.x - a.x, dz = b.z - a.z;
|
|
318
|
+
const ox = hx - Math.abs(dx), oz = hz - Math.abs(dz);
|
|
319
|
+
if (ox > 0 && oz > 0) {
|
|
320
|
+
moved = true;
|
|
321
|
+
if (ox < oz) { const s = dx < 0 ? -1 : 1; if (a.pin && b.pin) {} else if (a.pin) b.x += ox * s; else if (b.pin) a.x -= ox * s; else { a.x -= ox * 0.5 * s; b.x += ox * 0.5 * s; } }
|
|
322
|
+
else { const s = dz < 0 ? -1 : 1; if (a.pin && b.pin) {} else if (a.pin) b.z += oz * s; else if (b.pin) a.z -= oz * s; else { a.z -= oz * 0.5 * s; b.z += oz * 0.5 * s; } }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!moved) break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
</script>
|
|
329
|
+
</body>
|
|
330
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|