@echomem/mcp 1.4.7 → 1.4.9
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 +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- package/templates/echomem-recall.md +2 -2
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
.v7-head{right:56px;top:48px;width:410px;text-align:right;}
|
|
39
39
|
.v7-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
40
40
|
.v7-title{font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.05;margin-top:8px;}
|
|
41
|
-
|
|
41
|
+
/* Persona is identity flavor (a badge), not the page headline — one display-size element per
|
|
42
|
+
screen, and that's "Here's what you built." */
|
|
43
|
+
.v7-personaName{font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:.92;margin-top:8px;text-transform:lowercase;}
|
|
42
44
|
.v7-rank{margin-top:12px;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
|
|
43
45
|
.v7-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
|
|
44
46
|
.v7-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
|
|
@@ -59,6 +61,9 @@
|
|
|
59
61
|
.v7-stat .v{font-size:23px;font-weight:800;letter-spacing:-.01em;line-height:1;}
|
|
60
62
|
.v7-stat .k{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9296a0;margin-top:4px;}
|
|
61
63
|
.v7-band{left:90px;right:90px;bottom:250px;text-align:center;}
|
|
64
|
+
/* Echo delivers the verdict: the mascot makes the band a character speaking, not a floating stat. */
|
|
65
|
+
.v7-echo{width:56px;height:56px;object-fit:contain;margin:10px auto 2px;display:block;
|
|
66
|
+
filter:drop-shadow(0 12px 20px rgba(26,58,143,.22));}
|
|
62
67
|
.v7-band .eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
|
|
63
68
|
.v7-band .bignum{font-size:64px;font-weight:800;letter-spacing:-.03em;line-height:.9;margin-top:10px;color:var(--red);
|
|
64
69
|
min-height:64px;transition:opacity .52s ease,transform .52s ease;}
|
|
@@ -160,6 +165,11 @@
|
|
|
160
165
|
box-shadow:0 16px 36px rgba(20,24,40,.18);cursor:pointer;}
|
|
161
166
|
.setup-connect:hover{background:#223f9b;}
|
|
162
167
|
body.setup-mode{background:#fff;}
|
|
168
|
+
/* figure-mode: embedded as an inline article figure — city only, chrome hidden, paper shows through */
|
|
169
|
+
body.figure-mode{background:transparent;}
|
|
170
|
+
body.figure-mode .share-card{background:transparent;box-shadow:none;}
|
|
171
|
+
body.figure-mode .fr-panel,body.figure-mode .corner-ribbon,body.figure-mode .city-toggle,
|
|
172
|
+
body.figure-mode .city-split,body.figure-mode .city-timeline,body.figure-mode .v7-band{display:none!important;}
|
|
163
173
|
body.setup-mode .share-card{width:100vw;height:100vh;border-radius:0;box-shadow:none;overflow:hidden;}
|
|
164
174
|
body.setup-mode .compute-cost,
|
|
165
175
|
body.setup-mode .tool-split,
|
|
@@ -227,7 +237,7 @@
|
|
|
227
237
|
body.setup-mode .counterfactual,
|
|
228
238
|
body.setup-mode .setup-connect{display:none;}
|
|
229
239
|
body.setup-mode .v7-persona{left:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(360px,32vw);}
|
|
230
|
-
body.setup-mode .v7-personaName{font-size:clamp(
|
|
240
|
+
body.setup-mode .v7-personaName{font-size:clamp(38px,3.4vw,54px);}
|
|
231
241
|
body.setup-mode .v7-head{right:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(520px,38vw);}
|
|
232
242
|
body.setup-mode .v7-head .v7-eyebrow{font-size:clamp(10px,.72vw,12px);}
|
|
233
243
|
body.setup-mode .v7-title{font-size:clamp(28px,2.1vw,40px);}
|
|
@@ -235,7 +245,10 @@
|
|
|
235
245
|
body.setup-mode .v7-stat .v{font-size:clamp(20px,1.45vw,28px);}
|
|
236
246
|
body.setup-mode .v7-stat .k{font-size:clamp(9px,.72vw,11px);}
|
|
237
247
|
body.setup-mode .v7-band{left:clamp(48px,5vw,86px);right:auto;bottom:clamp(82px,8vh,130px);
|
|
238
|
-
width:min(
|
|
248
|
+
width:min(640px,42vw);text-align:left;display:grid;grid-template-columns:72px minmax(0,1fr);
|
|
249
|
+
column-gap:16px;align-items:start;}
|
|
250
|
+
body.setup-mode .v7-echo{width:72px;height:72px;margin:2px 0 0;}
|
|
251
|
+
body.setup-mode .v7-bandCopy{min-width:0;}
|
|
239
252
|
body.setup-mode .v7-band .eyebrow{display:none;}
|
|
240
253
|
body.setup-mode .v7-band .bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
241
254
|
margin-top:0;min-height:0;line-height:.96;}
|
|
@@ -250,13 +263,146 @@
|
|
|
250
263
|
body.setup-mode .v7-ctaWrap .micro{max-width:430px;margin-left:auto;}
|
|
251
264
|
body.setup-mode .v7-foot{left:clamp(48px,5vw,86px);right:clamp(48px,5vw,86px);}
|
|
252
265
|
@media (max-height:820px){
|
|
253
|
-
body.setup-mode .v7-personaName{font-size:clamp(
|
|
266
|
+
body.setup-mode .v7-personaName{font-size:clamp(32px,4.4vh,46px);}
|
|
254
267
|
body.setup-mode .v7-title{font-size:clamp(30px,5vh,44px);}
|
|
255
268
|
body.setup-mode .v7-band{bottom:58px;width:min(500px,40vw);}
|
|
256
269
|
body.setup-mode .v7-ctaWrap{bottom:58px;width:min(390px,32vw);}
|
|
257
270
|
body.setup-mode .v7-foot{display:none;}
|
|
258
271
|
}
|
|
259
272
|
|
|
273
|
+
/* setup split report: city on the left, field report on the right */
|
|
274
|
+
/* Old full-screen setup overlay reserves 620px for the right-hand text panel.
|
|
275
|
+
figure-mode embeds (city as its own column) must NOT reserve anything: the
|
|
276
|
+
canvas fills the iframe, matching the full-width render buffer (VIZ_W). */
|
|
277
|
+
body.setup-mode:not(.figure-mode) #repo-visualizer{right:auto;width:calc(100vw - min(40vw,620px));}
|
|
278
|
+
body.with-echo .fr-metric .fr-v{color:#2a8f7d;}
|
|
279
|
+
body.with-echo .fr-pull{border-left-color:#2a8f7d;}
|
|
280
|
+
.city-toggle{display:none;transition:opacity .4s ease;}
|
|
281
|
+
body.setup-mode .city-toggle{display:inline-flex;position:absolute;z-index:9;
|
|
282
|
+
left:calc((100vw - min(40vw,620px)) / 2);top:clamp(92px,12.5vh,104px);transform:translateX(-50%);gap:2px;
|
|
283
|
+
background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:999px;padding:4px;
|
|
284
|
+
box-shadow:0 8px 22px rgba(20,24,40,.10);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);}
|
|
285
|
+
.ct-thumb{position:absolute;left:0;top:0;z-index:0;background:#1a3a8f;border-radius:999px;
|
|
286
|
+
transition:transform .32s cubic-bezier(.4,0,.2,1),width .32s cubic-bezier(.4,0,.2,1),height .32s ease;}
|
|
287
|
+
.city-toggle button{position:relative;z-index:1;appearance:none;border:0;background:transparent;cursor:pointer;
|
|
288
|
+
font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
|
|
289
|
+
color:#80838c;padding:7px 14px;border-radius:999px;transition:color .25s ease;}
|
|
290
|
+
.city-toggle button.on{color:#fff;}
|
|
291
|
+
.city-toggle button:not(.on):hover{color:#1b1c1f;}
|
|
292
|
+
.corner-ribbon{display:none;}
|
|
293
|
+
body.setup-mode .corner-ribbon{display:block;position:absolute;top:0;left:0;width:180px;height:180px;
|
|
294
|
+
overflow:hidden;z-index:12;pointer-events:none;}
|
|
295
|
+
.corner-ribbon>span{position:absolute;left:-70px;top:51px;width:280px;transform:rotate(-45deg);
|
|
296
|
+
background:#1a3a8f;color:#fff;text-align:center;padding:7px 0;line-height:1.2;
|
|
297
|
+
box-shadow:0 6px 16px rgba(20,24,40,.18);}
|
|
298
|
+
.corner-ribbon .l1{display:block;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;}
|
|
299
|
+
.corner-ribbon .l2{display:block;font-family:var(--mono);font-size:8.5px;font-weight:700;letter-spacing:.08em;
|
|
300
|
+
text-transform:uppercase;color:rgba(255,255,255,.86);margin-top:2px;}
|
|
301
|
+
.city-split{display:none;transition:opacity .4s ease;}
|
|
302
|
+
body.setup-mode .city-split{display:block;position:absolute;z-index:9;
|
|
303
|
+
left:clamp(28px,3vw,48px);bottom:clamp(72px,9vh,104px);width:min(260px,24vw);}
|
|
304
|
+
.city-split .cs-bar{display:flex;height:8px;border-radius:999px;overflow:hidden;background:#eceef2;
|
|
305
|
+
box-shadow:inset 0 0 0 1px rgba(20,24,40,.05);}
|
|
306
|
+
.city-split .cs-bar i{height:100%;display:block;transition:width .4s cubic-bezier(.4,0,.2,1);}
|
|
307
|
+
.city-split .cs-claude{background:#d97757;}
|
|
308
|
+
.city-split .cs-codex{background:#10a37f;}
|
|
309
|
+
.city-split .cs-legend{display:flex;justify-content:space-between;margin-top:9px;
|
|
310
|
+
font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#80838c;}
|
|
311
|
+
.city-split .cs-l{display:flex;align-items:center;gap:6px;}
|
|
312
|
+
.city-split .cs-l i{width:8px;height:8px;border-radius:50%;}
|
|
313
|
+
.city-split .cs-l.claude i{background:#d97757;}
|
|
314
|
+
.city-split .cs-l.codex i{background:#10a37f;}
|
|
315
|
+
.city-split .cs-l b{color:#1b1c1f;font-weight:800;margin-left:2px;}
|
|
316
|
+
.city-timeline{display:none;transition:opacity .4s ease;}
|
|
317
|
+
body.setup-mode .city-timeline{display:block;position:absolute;z-index:9;
|
|
318
|
+
left:clamp(28px,3vw,48px);bottom:clamp(20px,2.6vh,32px);
|
|
319
|
+
width:calc(100vw - min(40vw,620px) - 2 * clamp(28px,3vw,48px));touch-action:none;user-select:none;}
|
|
320
|
+
.city-timeline .tl-track{position:relative;height:18px;cursor:pointer;}
|
|
321
|
+
.city-timeline .tl-line{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
|
|
322
|
+
height:3px;border-radius:3px;background:#d8dbe1;}
|
|
323
|
+
.city-timeline .tl-progress{position:absolute;left:0;top:50%;transform:translateY(-50%);
|
|
324
|
+
width:100%;height:3px;border-radius:3px;background:#1a3a8f;pointer-events:none;}
|
|
325
|
+
.city-timeline .tl-dot{position:absolute;top:50%;transform:translate(-50%,-50%);width:9px;height:9px;border-radius:50%;}
|
|
326
|
+
.city-timeline .tl-dot.start{left:0;background:#9aa0aa;}
|
|
327
|
+
.city-timeline .tl-dot.end{left:100%;background:#1a3a8f;}
|
|
328
|
+
.city-timeline .tl-dot.end::after{content:'';position:absolute;inset:-4px;border-radius:50%;
|
|
329
|
+
border:2px solid rgba(26,58,143,.4);animation:tlPulse 2.2s ease-out infinite;}
|
|
330
|
+
@keyframes tlPulse{0%{transform:scale(.6);opacity:.9}100%{transform:scale(1.85);opacity:0}}
|
|
331
|
+
.city-timeline .tl-handle{position:absolute;left:100%;top:50%;transform:translate(-50%,-50%);
|
|
332
|
+
width:16px;height:16px;border-radius:50%;background:#1a3a8f;border:3px solid #fff;
|
|
333
|
+
box-shadow:0 3px 10px rgba(20,24,40,.28);z-index:3;cursor:grab;}
|
|
334
|
+
.city-timeline.dragging .tl-handle{cursor:grabbing;box-shadow:0 3px 14px rgba(26,58,143,.42);}
|
|
335
|
+
.city-timeline .tl-selected{position:absolute;left:100%;top:-17px;transform:translateX(-50%);
|
|
336
|
+
font-family:var(--mono);font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
|
|
337
|
+
color:#1a3a8f;white-space:nowrap;pointer-events:none;}
|
|
338
|
+
.city-timeline .tl-labels{display:flex;justify-content:space-between;align-items:baseline;margin-top:8px;
|
|
339
|
+
font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#80838c;}
|
|
340
|
+
.city-timeline .tl-labels .tl-span{color:#1b1c1f;font-weight:800;}
|
|
341
|
+
body.setup-mode .v7-persona,
|
|
342
|
+
body.setup-mode .v7-head,
|
|
343
|
+
body.setup-mode .v7-band,
|
|
344
|
+
body.setup-mode .v7-ctaWrap,
|
|
345
|
+
body.setup-mode .v7-foot{display:none;}
|
|
346
|
+
|
|
347
|
+
.fr-panel{display:none;transition:opacity .6s ease;}
|
|
348
|
+
body.setup-mode .fr-panel{display:flex;flex-direction:column;align-items:center;position:absolute;z-index:8;
|
|
349
|
+
right:0;top:0;width:min(40vw,620px);height:100vh;border-left:1px solid var(--line);background:#fff;
|
|
350
|
+
padding:clamp(44px,6vh,90px) clamp(36px,3.2vw,60px);}
|
|
351
|
+
.fr{width:min(54ch,100%);flex:1;display:flex;flex-direction:column;justify-content:space-between;
|
|
352
|
+
gap:clamp(22px,3.4vh,46px);}
|
|
353
|
+
.fr-dateline{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:11px;
|
|
354
|
+
font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
355
|
+
.fr-dateline .fr-rule{flex:1;height:1px;background:var(--line);}
|
|
356
|
+
.fr-intro{display:flex;flex-direction:column;position:relative;
|
|
357
|
+
padding-bottom:clamp(20px,3vh,38px);border-bottom:1px solid var(--line);}
|
|
358
|
+
.fr-avatar{width:clamp(92px,9.5vw,128px);height:auto;display:block;margin-bottom:10px;}
|
|
359
|
+
.fr-greet{font-size:clamp(34px,3.2vw,52px);font-weight:800;letter-spacing:-.035em;line-height:.95;
|
|
360
|
+
text-transform:lowercase;}
|
|
361
|
+
.fr-greet .fr-name{color:#1a3a8f;}
|
|
362
|
+
.fr-greet .fr-dot{color:var(--amber);}
|
|
363
|
+
.fr-rank{margin-top:13px;font-family:var(--mono);font-size:clamp(12px,1vw,14px);font-weight:600;
|
|
364
|
+
letter-spacing:.04em;color:#9296a0;}
|
|
365
|
+
.fr-rank b{font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#3a3c43;}
|
|
366
|
+
.fr-readBtn{position:absolute;z-index:9;top:clamp(2px,0.5vw,8px);right:0;
|
|
367
|
+
display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
|
|
368
|
+
cursor:pointer;font-family:var(--mono);font-size:14px;font-weight:800;line-height:1;
|
|
369
|
+
color:#5a5e66;background:#fafafb;border:1px solid var(--line);border-radius:50%;
|
|
370
|
+
transition:background .15s ease,border-color .15s ease,color .15s ease;}
|
|
371
|
+
.fr-readBtn:hover,.fr-readBtn:focus-visible{background:#fff;border-color:#d7dade;color:var(--ink);outline:none;}
|
|
372
|
+
.fr-readBtn[aria-expanded="true"]{background:#1a3a8f;border-color:#1a3a8f;color:#fff;}
|
|
373
|
+
.fr-personaHover{position:absolute;z-index:32;top:clamp(42px,4.6vw,56px);right:0;
|
|
374
|
+
width:min(340px,100%);max-height:min(72vh,540px);overflow:auto;
|
|
375
|
+
background:rgba(255,255,255,.98);border:1px solid var(--line);border-radius:16px;padding:15px 17px;
|
|
376
|
+
box-shadow:0 18px 46px rgba(20,24,40,.18);opacity:0;visibility:hidden;transform:translateY(-4px);
|
|
377
|
+
transition:opacity .14s ease,transform .14s ease;pointer-events:none;}
|
|
378
|
+
.fr-personaHover.open{opacity:1;visibility:visible;transform:none;pointer-events:auto;}
|
|
379
|
+
.fr-personaHover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
380
|
+
.fr-personaHover .ph-line:last-of-type{margin-bottom:0;}
|
|
381
|
+
.fr-personaHover .ph-line b{color:var(--ink);font-weight:800;}
|
|
382
|
+
.fr-personaHover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
383
|
+
padding-top:9px;border-top:1px solid var(--line);}
|
|
384
|
+
.fr-personaHover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
385
|
+
.fr-mid{display:flex;flex-direction:column;gap:clamp(20px,3vh,40px);}
|
|
386
|
+
.fr-lede{font-size:clamp(16px,1.1vw,18px);line-height:1.68;color:#3c3e45;}
|
|
387
|
+
.fr-lede mark{background:linear-gradient(transparent 58%, rgba(240,168,63,.42) 58%);
|
|
388
|
+
color:var(--ink);font-weight:800;padding:0 1px;}
|
|
389
|
+
.fr-pull{padding-left:20px;border-left:3px solid var(--red);}
|
|
390
|
+
.fr-metric{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
391
|
+
transition:opacity .5s ease,transform .5s ease;}
|
|
392
|
+
.fr-metric .fr-v{font-size:clamp(32px,2.8vw,44px);font-weight:800;color:var(--red);letter-spacing:-.03em;line-height:.9;}
|
|
393
|
+
.fr-metric .fr-l{font-size:clamp(16px,1.2vw,19px);font-weight:700;color:var(--ink);letter-spacing:-.01em;}
|
|
394
|
+
.fr-say{font-size:clamp(14px,.9vw,15px);color:#56585f;margin-top:9px;line-height:1.5;max-width:46ch;
|
|
395
|
+
min-height:4.6em;transition:opacity .5s ease,transform .5s ease;}
|
|
396
|
+
.fr-say b{color:var(--ink);font-weight:700;}
|
|
397
|
+
.fr-metric.is-swapping,.fr-say.is-swapping{opacity:0;transform:translateY(6px);}
|
|
398
|
+
.fr-signoff{display:flex;align-items:center;gap:18px;flex-wrap:wrap;}
|
|
399
|
+
.fr-cta{display:inline-flex;align-items:center;gap:10px;background:#1a3a8f;color:#fff;border:0;
|
|
400
|
+
border-radius:13px;font-family:var(--mono);font-size:clamp(13px,1vw,15px);font-weight:800;
|
|
401
|
+
letter-spacing:.08em;text-transform:uppercase;padding:16px 34px;
|
|
402
|
+
box-shadow:0 18px 40px rgba(26,58,143,.30);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
403
|
+
.fr-cta:hover{background:#223f9b;transform:translateY(-2px);}
|
|
404
|
+
.fr-micro{font-size:12.5px;color:#8b8d94;line-height:1.45;max-width:24ch;}
|
|
405
|
+
|
|
260
406
|
/* ===== in-canvas scan loader (setup mode) — loading lives inside the report, matches its type ===== */
|
|
261
407
|
.scan-loading{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
262
408
|
text-align:center;gap:14px;padding:24px;opacity:1;transition:opacity .5s ease;pointer-events:none;}
|
|
@@ -270,10 +416,14 @@
|
|
|
270
416
|
/* during scan: hide the city + the value-bearing chrome (so demo numbers never flash), then fade in together */
|
|
271
417
|
#repo-visualizer{transition:opacity .7s ease;}
|
|
272
418
|
body.scanning #repo-visualizer{opacity:0;}
|
|
273
|
-
.setup-header,.dirty-context,.counterfactual,.setup-connect,.v7-z{transition:opacity .6s ease;}
|
|
274
|
-
body.scanning .setup-header,body.scanning .dirty-context,body.scanning .counterfactual,body.scanning .setup-connect,body.scanning .v7-z{opacity:0;pointer-events:none;}
|
|
419
|
+
.setup-header,.dirty-context,.counterfactual,.setup-connect,.v7-z,.city-toggle,.city-split,.city-timeline,.fr-panel{transition:opacity .6s ease;}
|
|
420
|
+
body.scanning .setup-header,body.scanning .dirty-context,body.scanning .counterfactual,body.scanning .setup-connect,body.scanning .v7-z,body.scanning .city-toggle,body.scanning .city-split,body.scanning .city-timeline,body.scanning .fr-panel{opacity:0;pointer-events:none;}
|
|
275
421
|
body.setup-mode:not(.scanning) #repo-visualizer,
|
|
276
|
-
body.setup-mode:not(.scanning) .v7-z
|
|
422
|
+
body.setup-mode:not(.scanning) .v7-z,
|
|
423
|
+
body.setup-mode:not(.scanning) .city-toggle,
|
|
424
|
+
body.setup-mode:not(.scanning) .city-split,
|
|
425
|
+
body.setup-mode:not(.scanning) .city-timeline,
|
|
426
|
+
body.setup-mode:not(.scanning) .fr-panel{opacity:1;}
|
|
277
427
|
|
|
278
428
|
/* hover-to-preview repo detail (interaction overlay, not part of the static export) */
|
|
279
429
|
#repo-detail{position:fixed;display:none;z-index:50;width:286px;background:rgba(255,255,255,.96);
|
|
@@ -300,10 +450,38 @@
|
|
|
300
450
|
</style>
|
|
301
451
|
</head>
|
|
302
452
|
<body>
|
|
303
|
-
<script>try{const p=new URLSearchParams(location.search);if(p.get('setup')==='1'){document.body.classList.add('setup-mode');if(p.get('
|
|
453
|
+
<script>try{const p=new URLSearchParams(location.search);if(p.get('setup')==='1'){document.body.classList.add('setup-mode','scanning');}if(p.get('figure')==='1'){document.body.classList.add('figure-mode');window.addEventListener('message',function(e){if(e.origin!==location.origin)return;var d=e.data||{};if(d.type==='echomem:city-figure')document.body.classList.toggle('figure-mode',!d.expanded);});}}catch(e){}</script>
|
|
304
454
|
<div class="share-card">
|
|
305
455
|
<canvas id="repo-visualizer"></canvas>
|
|
306
456
|
|
|
457
|
+
<div class="city-toggle" id="city-toggle" role="group" aria-label="City metric">
|
|
458
|
+
<span class="ct-thumb" aria-hidden="true"></span>
|
|
459
|
+
<button type="button" data-metric="time" class="on">Time</button>
|
|
460
|
+
<button type="button" data-metric="tokens">Tokens</button>
|
|
461
|
+
<button type="button" data-metric="cost">API $</button>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div class="corner-ribbon" aria-hidden="true"><span><span class="l1">Local Scan</span><span class="l2">100% private & safe</span></span></div>
|
|
465
|
+
|
|
466
|
+
<div class="city-split" id="city-split">
|
|
467
|
+
<div class="cs-bar"><i class="cs-claude" style="width:{{claudePct}}%"></i><i class="cs-codex" style="width:{{codexPct}}%"></i></div>
|
|
468
|
+
<div class="cs-legend">
|
|
469
|
+
<span class="cs-l claude"><i></i>Claude <b>{{claudePct}}%</b></span>
|
|
470
|
+
<span class="cs-l codex"><i></i>Codex <b>{{codexPct}}%</b></span>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div class="city-timeline" id="city-timeline">
|
|
475
|
+
<div class="tl-track">
|
|
476
|
+
<span class="tl-line"></span>
|
|
477
|
+
<span class="tl-progress"></span>
|
|
478
|
+
<span class="tl-dot start"></span><span class="tl-dot end"></span>
|
|
479
|
+
<span class="tl-selected">Today</span>
|
|
480
|
+
<span class="tl-handle" role="slider" tabindex="0" aria-label="Coding history date" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"></span>
|
|
481
|
+
</div>
|
|
482
|
+
<div class="tl-labels"><span class="tl-start">Dec 2024</span><span class="tl-span">AI coding history</span><span class="tl-end">Today</span></div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
307
485
|
<div class="v7-z v7-persona" tabindex="0" aria-describedby="v7-persona-hover">
|
|
308
486
|
<div class="v7-eyebrow">AI Coding City</div>
|
|
309
487
|
<div class="v7-personaName">{{personaPrimary}}</div>
|
|
@@ -323,24 +501,50 @@
|
|
|
323
501
|
</div>
|
|
324
502
|
<div class="v7-z v7-band">
|
|
325
503
|
<div class="eyebrow">What we found</div>
|
|
326
|
-
<
|
|
327
|
-
<div class="
|
|
504
|
+
<img class="v7-echo" src="echo-face-cutout.png" alt="" />
|
|
505
|
+
<div class="v7-bandCopy">
|
|
506
|
+
<div class="bignum" id="v7-metric"><span id="v7-metric-value">{{dirtyPct}}%</span><span id="v7-metric-label">wasteful context</span><button class="v7-infoBtn" id="v7-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
|
|
507
|
+
<div class="say" id="v7-metric-say">The golden-standard model separates useful context from wasteful context, then attributes the waste to concrete problem types across your workspace.</div>
|
|
508
|
+
</div>
|
|
328
509
|
</div>
|
|
329
510
|
<div class="v7-z v7-ctaWrap">
|
|
330
511
|
<div class="pre">Ready when you are</div>
|
|
331
512
|
<button class="v7-cta" id="v7-connect" type="button">Connect Echo →</button>
|
|
332
|
-
<div class="micro">Clears the dirty context, so agents stop relearning your workspace.</div>
|
|
333
513
|
</div>
|
|
334
514
|
<div class="v7-z v7-foot"><span>Generated by Echo · No transcripts shown</span><span>Local scan · Share-safe city mode</span></div>
|
|
515
|
+
|
|
516
|
+
<div class="fr-panel">
|
|
517
|
+
<div class="fr">
|
|
518
|
+
<div class="fr-dateline"><span>Echo field report</span><span class="fr-rule"></span></div>
|
|
519
|
+
<div class="fr-intro">
|
|
520
|
+
<button class="fr-readBtn" id="fr-read-btn" type="button" aria-expanded="false" aria-controls="fr-persona-hover" aria-label="Your full coding-personality read">?</button>
|
|
521
|
+
<div class="fr-personaHover" id="fr-persona-hover" role="region" aria-label="Your full coding-personality read"></div>
|
|
522
|
+
<img class="fr-avatar" id="fr-avatar" src="./personality_stickers/bossy.png" alt="" />
|
|
523
|
+
<h1 class="fr-greet">hey, <span class="fr-name" id="fr-name">{{personaPrimary}}</span><span class="fr-dot">.</span></h1>
|
|
524
|
+
<div class="fr-rank" id="fr-rank">You are <b>{{personaRankRhythm}}</b></div>
|
|
525
|
+
</div>
|
|
526
|
+
<div class="fr-mid">
|
|
527
|
+
<p class="fr-lede">Across <mark id="fr-repos">13 repos</mark> you poured in <mark id="fr-hours">{{attentionHours}} hours</mark> and <mark id="fr-tokens">7.49B tokens</mark> — about <mark id="fr-cost">{{computeCost}}</mark> of API-equivalent compute. That whole skyline on the left? You built it.</p>
|
|
528
|
+
<div class="fr-pull">
|
|
529
|
+
<div class="fr-metric" id="fr-metric"><span class="fr-v" id="fr-metric-value">{{dirtyPct}}%</span><span class="fr-l" id="fr-metric-label">wasteful context</span></div>
|
|
530
|
+
<div class="fr-say" id="fr-metric-say">The golden-standard model separates useful context from wasteful context, then attributes the waste to concrete problem types.</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
<div class="fr-signoff">
|
|
534
|
+
<button class="fr-cta" id="fr-connect" type="button">Connect Echo →</button>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
335
539
|
<div class="v7-infoSheet" id="v7-info-sheet" aria-hidden="true">
|
|
336
540
|
<div class="v7-infoCard" role="dialog" aria-modal="true" aria-labelledby="v7-info-title">
|
|
337
541
|
<button class="v7-infoClose" id="v7-info-close" type="button" aria-label="Close">×</button>
|
|
338
542
|
<div class="v7-infoTitle" id="v7-info-title">How we read your city</div>
|
|
339
543
|
<div class="v7-infoSub">Every number comes from your local Codex + Claude Code logs. Nothing here is an estimate of you — it's a readout of the tools.</div>
|
|
340
|
-
<div class="v7-infoRow"><div class="sk">
|
|
341
|
-
<div class="v7-infoRow"><div class="sk">
|
|
544
|
+
<div class="v7-infoRow"><div class="sk">Wasteful context</div><div class="sv">The headline comes from the golden-standard workspace rollup: waste tokens divided by official input tokens.</div></div>
|
|
545
|
+
<div class="v7-infoRow"><div class="sk">Useful context</div><div class="sv">Useful context is the portion worth keeping: active task state, product facts, current files, and required overhead.</div></div>
|
|
342
546
|
<div class="v7-infoRow"><div class="sk">{{computeCost}}</div><div class="sv"><b>API-equivalent</b> — what this usage would cost at list API rates. Most of it is cache / input reads, so it's a measure of <b>volume, not a bill you paid</b>.</div></div>
|
|
343
|
-
<div class="v7-infoRow"><div class="sk">
|
|
547
|
+
<div class="v7-infoRow"><div class="sk">Evidence</div><div class="sv">Repeated reads are still evidence, but the top-line waste number now includes duplicate context, re-finding, dead work, and residue.</div></div>
|
|
344
548
|
<div class="v7-infoNote">Computed locally · no transcripts leave your machine.</div>
|
|
345
549
|
</div>
|
|
346
550
|
</div>
|
|
@@ -394,8 +598,8 @@
|
|
|
394
598
|
|
|
395
599
|
<!-- dirty context -->
|
|
396
600
|
<div class="card-zone dirty-context">
|
|
397
|
-
<div class="pct" id="setup-metric"><span id="setup-metric-value">{{dirtyPct}}%</span><span class="word" id="setup-metric-label">
|
|
398
|
-
<div class="desc" id="setup-metric-say">
|
|
601
|
+
<div class="pct" id="setup-metric"><span id="setup-metric-value">{{dirtyPct}}%</span><span class="word" id="setup-metric-label">wasteful context</span><button class="v7-infoBtn" id="setup-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
|
|
602
|
+
<div class="desc" id="setup-metric-say">The golden-standard model separates useful context from wasteful context across your workspace.</div>
|
|
399
603
|
</div>
|
|
400
604
|
|
|
401
605
|
<!-- counterfactual: one line — 18 days saved -->
|
|
@@ -424,6 +628,30 @@
|
|
|
424
628
|
|
|
425
629
|
<div id="repo-detail"></div>
|
|
426
630
|
|
|
631
|
+
<script>
|
|
632
|
+
// The poster shares this artifact, but setup must never expose poster labels while it is waiting or
|
|
633
|
+
// failed. Scrub personalized defaults after the DOM exists; the validated report repopulates them.
|
|
634
|
+
try{
|
|
635
|
+
if(new URLSearchParams(location.search).get('setup')==='1'){
|
|
636
|
+
const pendingText={
|
|
637
|
+
'v7-repo-count':'—','v7-attention':'—','v7-cost':'—','v7-total-tokens':'—',
|
|
638
|
+
'v7-metric-value':'—','fr-name':'—','fr-rank':'Waiting for local scan',
|
|
639
|
+
'fr-repos':'— repos','fr-hours':'— hours','fr-tokens':'— tokens','fr-cost':'—',
|
|
640
|
+
'fr-metric-value':'—','setup-metric-value':'—'
|
|
641
|
+
};
|
|
642
|
+
for(const [id,value] of Object.entries(pendingText)){ const el=document.getElementById(id); if(el) el.textContent=value; }
|
|
643
|
+
document.querySelectorAll('.v7-personaName,.setup-persona-name,.persona-card .name').forEach((el)=>{ el.textContent='—'; });
|
|
644
|
+
document.querySelectorAll('.v7-rank,.setup-rank,.persona-card .rankrhythm').forEach((el)=>{ el.textContent='Waiting for local scan'; });
|
|
645
|
+
document.querySelectorAll('.v7-combo,.setup-combo,.persona-card .combo,.setup-split,#tool-split').forEach((el)=>{ el.replaceChildren(); });
|
|
646
|
+
document.querySelectorAll('.setup-stats .amount,.compute-cost .amount').forEach((el)=>{ el.textContent='—'; });
|
|
647
|
+
document.querySelectorAll('.setup-stats .detail,.compute-cost .detail').forEach((el)=>{ el.textContent='Waiting for validated local data.'; });
|
|
648
|
+
document.querySelectorAll('#v7-metric-say,#fr-metric-say,#setup-metric-say').forEach((el)=>{ el.textContent='Waiting for a validated local report.'; });
|
|
649
|
+
const frAvatar=document.getElementById('fr-avatar'); if(frAvatar) frAvatar.removeAttribute('src');
|
|
650
|
+
document.querySelectorAll('.share-card > :not(#scan-loading)').forEach((el)=>{ el.setAttribute('aria-hidden','true'); el.setAttribute('data-setup-pending-hidden',''); });
|
|
651
|
+
}
|
|
652
|
+
}catch(_){}
|
|
653
|
+
</script>
|
|
654
|
+
|
|
427
655
|
<script type="importmap">
|
|
428
656
|
{ "imports": { "three": "./vendor/three.module.min.js" } }
|
|
429
657
|
</script>
|
|
@@ -434,11 +662,41 @@ import { OrbitControls } from './vendor/OrbitControls.js';
|
|
|
434
662
|
|
|
435
663
|
const pageParams=new URLSearchParams(location.search);
|
|
436
664
|
const setupMode=pageParams.get('setup')==='1';
|
|
665
|
+
const previewMode=pageParams.get('preview')==='1';
|
|
666
|
+
const figureModeEmbed=pageParams.get('figure')==='1';
|
|
437
667
|
if(setupMode) document.body.classList.add('setup-mode');
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
668
|
+
// figure-mode embeds the city as its own column beside the text: the canvas owns the whole
|
|
669
|
+
// iframe. The 620px text reservation only applies to the old full-screen setup overlay.
|
|
670
|
+
let VIZ_W=setupMode ? (figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620))) : 920;
|
|
671
|
+
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode, left city in setup mode
|
|
672
|
+
const TRAY_COL = 0xd9d5ca; // worn warm concrete; keeps the corrupted city distinct from the white report panel
|
|
441
673
|
const TOOL = { codex:0x10a37f, claude:0xd97757 }; // ONLY two cube colours — exact OpenAI / Claude chip-icon hexes
|
|
674
|
+
const CORRUPTION = 0.82;
|
|
675
|
+
const corruptionFloaters=[];
|
|
676
|
+
const repairGroups=[];
|
|
677
|
+
const repairPieces=[];
|
|
678
|
+
const corruptionFades=[];
|
|
679
|
+
const corruptionSurfaces=[];
|
|
680
|
+
const buildingMaterials=[];
|
|
681
|
+
|
|
682
|
+
function hash01(value, salt=0){
|
|
683
|
+
let h=2166136261 ^ salt;
|
|
684
|
+
for(let i=0;i<String(value).length;i++){
|
|
685
|
+
h^=String(value).charCodeAt(i);
|
|
686
|
+
h=Math.imul(h,16777619);
|
|
687
|
+
}
|
|
688
|
+
return (h>>>0)/4294967295;
|
|
689
|
+
}
|
|
690
|
+
function corruptionProfile(o){
|
|
691
|
+
const pressure=Math.min(1, Math.max(0.28, Math.log10(Math.max(10, o.tokens||10))/10));
|
|
692
|
+
return {
|
|
693
|
+
amount:CORRUPTION*(0.58+pressure*0.42),
|
|
694
|
+
leanX:(hash01(o.name,11)-0.5)*0.25,
|
|
695
|
+
leanZ:(hash01(o.name,19)-0.5)*0.25,
|
|
696
|
+
phase:hash01(o.name,29)*Math.PI*2,
|
|
697
|
+
side:hash01(o.name,37)<0.5?-1:1,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
442
700
|
|
|
443
701
|
// ── volume normalization ─────────────────────────────────────────────────────
|
|
444
702
|
// Each cube's VOLUME is proportional to that repo's token usage, measured in a fixed unit so
|
|
@@ -453,6 +711,8 @@ const LABEL_MIN_BASE = 24; // cubes with footprint ≥ this get a top-face
|
|
|
453
711
|
const fmtTok=(n)=> n>=1e9?`${(n/1e9).toFixed(2)}B` : n>=1e6?`${Math.round(n/1e6)}M` : `${Math.round(n/1e3)}K`;
|
|
454
712
|
const money=(n)=>'$'+Math.round(Number(n)||0).toLocaleString('en-US');
|
|
455
713
|
const oneDec=(n)=>Number(n||0).toFixed(1).replace(/\.0$/,'');
|
|
714
|
+
const REPEATED_READS_SAY="Your agents re-read unchanged files, re-run the same searches, and check git again before they can move. Echo turns those repeated proofs into recall.";
|
|
715
|
+
const GOLDEN_WASTE_SAY="The golden-standard model labels every analyzed input token as useful or wasteful, then attributes waste to duplicate context, re-finding, dead work, and residue across Codex and Claude Code.";
|
|
456
716
|
const monthDay=(iso)=>{
|
|
457
717
|
const d=new Date(iso);
|
|
458
718
|
if(Number.isNaN(d.getTime())) return {month:'JUN', day:'2', label:'Jun 2'};
|
|
@@ -470,9 +730,71 @@ function providerSplit(report){
|
|
|
470
730
|
const codexPct=Math.round(codex/total*100);
|
|
471
731
|
return {codexPct, claudePct:100-codexPct, dominant:codex>=claude?'codex':'claude'};
|
|
472
732
|
}
|
|
733
|
+
function clampScore(value){
|
|
734
|
+
return Math.max(0, Math.min(100, Math.round(Number(value)||0)));
|
|
735
|
+
}
|
|
736
|
+
function reportContextMetrics(report){
|
|
737
|
+
const metrics=report?.contextWindow?.metrics;
|
|
738
|
+
return metrics && typeof metrics==='object' ? metrics : null;
|
|
739
|
+
}
|
|
740
|
+
function contextHealthScore(report){
|
|
741
|
+
const metrics=reportContextMetrics(report);
|
|
742
|
+
const direct=Number(metrics?.healthScorePct);
|
|
743
|
+
if(Number.isFinite(direct)) return clampScore(direct);
|
|
744
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
745
|
+
return report && Number.isFinite(stale) ? clampScore(100-stale) : null;
|
|
746
|
+
}
|
|
747
|
+
function pctText(value){
|
|
748
|
+
const n=Number(value);
|
|
749
|
+
return Number.isFinite(n) ? `${Math.round(Math.max(0, Math.min(1, n))*100)}%` : null;
|
|
750
|
+
}
|
|
751
|
+
function contextPressureText(metrics){
|
|
752
|
+
const projected=pctText(metrics?.projectedNextFullnessPct);
|
|
753
|
+
if(projected) return `${projected} projected window pressure`;
|
|
754
|
+
const fullness=pctText(metrics?.contextFullnessPct);
|
|
755
|
+
if(fullness) return `${fullness} window full`;
|
|
756
|
+
const latest=Number(metrics?.latestInputTokens);
|
|
757
|
+
return Number.isFinite(latest) && latest>0 ? `${fmtTok(latest)} latest input` : 'current window pressure';
|
|
758
|
+
}
|
|
759
|
+
function contextNoiseText(report){
|
|
760
|
+
const metrics=reportContextMetrics(report);
|
|
761
|
+
const noise=pctText(metrics?.noisePct);
|
|
762
|
+
if(noise) return `${noise} resident noise`;
|
|
763
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
764
|
+
return Number.isFinite(stale) ? `${oneDec(stale)}% repeated-read signal` : 'resident noise';
|
|
765
|
+
}
|
|
766
|
+
function contextScoreSay(report){
|
|
767
|
+
const score=contextHealthScore(report);
|
|
768
|
+
const metrics=reportContextMetrics(report);
|
|
769
|
+
return `This is the same <b>${score}% context score</b> the HUD uses: ${contextNoiseText(report)} and ${contextPressureText(metrics)}, folded into one live setup readout.`;
|
|
770
|
+
}
|
|
771
|
+
function goldenReport(report){
|
|
772
|
+
const gs=report?.canonicalGoldenStandard || report?.goldenStandard;
|
|
773
|
+
return gs && typeof gs==='object' && !gs.error && gs.summary ? gs : null;
|
|
774
|
+
}
|
|
775
|
+
function goldenSummary(report){
|
|
776
|
+
return goldenReport(report)?.summary || null;
|
|
777
|
+
}
|
|
778
|
+
function goldenWastePct(report){
|
|
779
|
+
const pct=Number(goldenSummary(report)?.wastePct);
|
|
780
|
+
return Number.isFinite(pct) ? pct : Number(report?.cleanliness?.staleReadSharePct)||0;
|
|
781
|
+
}
|
|
782
|
+
function goldenUsefulPct(report){
|
|
783
|
+
const pct=Number(goldenSummary(report)?.usefulPct);
|
|
784
|
+
return Number.isFinite(pct) ? pct : Math.max(0, 100-goldenWastePct(report));
|
|
785
|
+
}
|
|
786
|
+
function goldenWasteSay(report){
|
|
787
|
+
const s=goldenSummary(report);
|
|
788
|
+
if(!s) return REPEATED_READS_SAY;
|
|
789
|
+
const p=report?.billedWasteProjection;
|
|
790
|
+
if(p && p.method==='canonical-window-share-over-provider-input'){
|
|
791
|
+
return `Across <b>${Number(s.sessionsAnalyzed||0).toLocaleString('en-US')} sessions</b>, the canonical windows found <b>${oneDec(s.wastePct)}% wasteful context</b>: a provider-billed projection of ${fmtTok(Number(p.projectedWasteTokens)||0)} waste tokens out of ${fmtTok(Number(p.billedInputTokens)||0)} input.`;
|
|
792
|
+
}
|
|
793
|
+
return `Across <b>${Number(s.sessionsAnalyzed||0).toLocaleString('en-US')} sessions</b>, the canonical windows found <b>${oneDec(s.wastePct)}% wasteful context</b>.`;
|
|
794
|
+
}
|
|
473
795
|
// ---- coding-persona engine (Phase 1: only signals the scan actually has — no commits / accept-all yet) ----
|
|
474
796
|
const PERSONA_TRAITS={
|
|
475
|
-
loopy:{label:'loopy', blurb:"opened the same file 14 times and still
|
|
797
|
+
loopy:{label:'loopy', blurb:"opened the same file 14 times and still had to prove it again. not a skill issue — the session forgot what it had already seen."},
|
|
476
798
|
lusty:{label:'lusty', blurb:"burned a car payment in tokens for demos that'll never ship. zero regrets. Anthropic's about to send a thank-you card. 💸"},
|
|
477
799
|
maxxy:{label:'maxxy', blurb:"Opus to rename a variable, Opus to fix a typo. the small models are right there — but you don't do small models, you do consequences."},
|
|
478
800
|
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features, breadth over depth. your brain is a browser with a memory leak. 🫠"},
|
|
@@ -504,7 +826,7 @@ function derivePersona(report){
|
|
|
504
826
|
const turns=Math.max(1, Number(uwh.realUserTurns)||0);
|
|
505
827
|
const autonomy=assistant/turns; // AI turns per human prompt → how hands-off you run
|
|
506
828
|
const repoCount=Number(scale.repoCount)||repos.length;
|
|
507
|
-
const stale=
|
|
829
|
+
const stale=goldenWastePct(report), spend=Number(cost.total)||0;
|
|
508
830
|
// Phase-2 git signal: commits landed vs building activity. Gated on commits>0 so a missing git pass can't false-fire ghosty.
|
|
509
831
|
const commits=Number(scale.totalCommits)||0, attention=Number(uwh.userAttentionHours)||0;
|
|
510
832
|
const activeDaysG=Number(uwh.activeAttentionDays)||0;
|
|
@@ -567,6 +889,8 @@ function renderV7PersonaHover(persona){
|
|
|
567
889
|
host.innerHTML = line(persona.primary) + (persona.sides||[]).map(line).join('') +
|
|
568
890
|
(persona.rank?`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>`:'') +
|
|
569
891
|
(persona.rhythm?`<div class="ph-meta"><b>${esc(persona.rhythm.label)}</b> · ${esc(persona.rhythm.blurb)}</div>`:'');
|
|
892
|
+
const frHost=document.getElementById('fr-persona-hover');
|
|
893
|
+
if(frHost) frHost.innerHTML=host.innerHTML;
|
|
570
894
|
}
|
|
571
895
|
function buildSetupRepos(report){
|
|
572
896
|
const split=providerSplit(report);
|
|
@@ -598,6 +922,7 @@ function buildSetupRepos(report){
|
|
|
598
922
|
sessions:Number(repo.sessions)||0,
|
|
599
923
|
codexPct, claudePct:100-codexPct,
|
|
600
924
|
pin:i<2,
|
|
925
|
+
daily:Array.isArray(repo.daily)?repo.daily:null,
|
|
601
926
|
};
|
|
602
927
|
});
|
|
603
928
|
}
|
|
@@ -607,13 +932,17 @@ function applySetupReport(report){
|
|
|
607
932
|
const persona=derivePersona(report);
|
|
608
933
|
const cost=money(report?.cost?.total);
|
|
609
934
|
const attention=oneDec(report?.userWorkingHours?.userAttentionHours);
|
|
610
|
-
const dirty=oneDec(report
|
|
935
|
+
const dirty=oneDec(goldenWastePct(report));
|
|
936
|
+
const metricValue=`${dirty}%`;
|
|
937
|
+
const metricLabel='wasteful context';
|
|
938
|
+
const metricSay=goldenWasteSay(report);
|
|
611
939
|
const cf=report?.counterfactual || {};
|
|
612
940
|
const reach=monthDay(cf.counterfactualDate);
|
|
613
941
|
const instead=monthDay(report?.scanEndDate).label;
|
|
614
|
-
const days=Math.max(
|
|
942
|
+
const days=Math.max(0, Math.round(Number(cf.daysGained)||0));
|
|
615
943
|
document.querySelectorAll('.setup-persona-name,.persona-card .name,.v7-personaName').forEach((el)=>{ el.textContent=persona.primary.label; });
|
|
616
944
|
document.querySelectorAll('.setup-rank,.persona-card .rankrhythm,.v7-rank').forEach((el)=>{ el.textContent=`${persona.rank.label} · ${persona.rhythm.label}`; });
|
|
945
|
+
const frRank=document.getElementById('fr-rank'); if(frRank) frRank.innerHTML=`You are <b>${persona.rank.label}</b> on <b>${persona.rhythm.label}</b>`;
|
|
617
946
|
document.querySelectorAll('.setup-combo,.persona-card .combo,.v7-combo').forEach((el)=>{ el.innerHTML=persona.sides.map((s)=>`<span class="ct">${s.label}</span>`).join(''); });
|
|
618
947
|
renderPersonaHover(persona);
|
|
619
948
|
renderV7PersonaHover(persona);
|
|
@@ -624,45 +953,343 @@ function applySetupReport(report){
|
|
|
624
953
|
const repoCount=Number(report?.scale?.repoCount) || (Array.isArray(report?.repos) ? report.repos.length : 0);
|
|
625
954
|
const v7RepoCount=document.getElementById('v7-repo-count'); if(v7RepoCount && repoCount) v7RepoCount.textContent=String(repoCount);
|
|
626
955
|
const v7Tokens=document.getElementById('v7-total-tokens'); if(v7Tokens) v7Tokens.textContent=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
627
|
-
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent
|
|
628
|
-
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent=
|
|
956
|
+
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent=metricValue;
|
|
957
|
+
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent=metricLabel;
|
|
629
958
|
const v7MetricSay=document.getElementById('v7-metric-say');
|
|
630
|
-
if(v7MetricSay) v7MetricSay.innerHTML=
|
|
959
|
+
if(v7MetricSay) v7MetricSay.innerHTML=metricSay;
|
|
960
|
+
const frName=document.getElementById('fr-name'); if(frName) frName.textContent=persona.primary.label;
|
|
961
|
+
const frAvatar=document.getElementById('fr-avatar');
|
|
962
|
+
if(frAvatar){
|
|
963
|
+
const key=String(persona.primary?.label||'').toLowerCase();
|
|
964
|
+
if(PERSONA_TRAITS[key]){ frAvatar.src=`./personality_stickers/${key}.png`; frAvatar.alt=`${key} avatar`; }
|
|
965
|
+
}
|
|
966
|
+
const frRepos=document.getElementById('fr-repos'); if(frRepos && repoCount) frRepos.textContent=`${repoCount} ${repoCount===1?'repo':'repos'}`;
|
|
967
|
+
const frHours=document.getElementById('fr-hours'); if(frHours) frHours.textContent=`${attention} hours`;
|
|
968
|
+
const frTokens=document.getElementById('fr-tokens'); if(frTokens) frTokens.textContent=`${fmtTok(Number(report?.scale?.totalTokens)||0)} tokens`;
|
|
969
|
+
const frCost=document.getElementById('fr-cost'); if(frCost) frCost.textContent=cost;
|
|
970
|
+
const frMetricValue=document.getElementById('fr-metric-value'); if(frMetricValue) frMetricValue.textContent=metricValue;
|
|
971
|
+
const frMetricLabel=document.getElementById('fr-metric-label'); if(frMetricLabel) frMetricLabel.textContent=metricLabel;
|
|
972
|
+
const frMetricSay=document.getElementById('fr-metric-say'); if(frMetricSay) frMetricSay.innerHTML=metricSay;
|
|
973
|
+
try{ window.parent?.postMessage({type:'echomem:city-fieldnote',
|
|
974
|
+
avatar:(frAvatar&&frAvatar.src)||'',
|
|
975
|
+
name:String(persona?.primary?.label||''),
|
|
976
|
+
rank:(document.getElementById('fr-rank')||{}).innerHTML||'',
|
|
977
|
+
lede:(document.querySelector('.fr-lede')||{}).innerHTML||'',
|
|
978
|
+
guide:{
|
|
979
|
+
current:{primary:String(persona?.primary?.label||''), rank:String(persona?.rank?.label||''), rhythm:String(persona?.rhythm?.label||'')},
|
|
980
|
+
traits:Object.values(PERSONA_TRAITS).map(({label,blurb})=>({label,blurb})),
|
|
981
|
+
ranks:Object.values(PERSONA_RANKS).map(({label,blurb})=>({label,blurb})),
|
|
982
|
+
rhythms:Object.values(PERSONA_RHYTHMS).map(({label,blurb})=>({label,blurb}))
|
|
983
|
+
}
|
|
984
|
+
}, location.origin); }catch(_){}
|
|
631
985
|
const splitHtml=`<span>${split.codexPct}% Codex</span><span>${split.claudePct}% Claude</span>`;
|
|
632
986
|
const setupSplit=document.querySelector('.setup-split'); if(setupSplit) setupSplit.innerHTML=splitHtml;
|
|
987
|
+
const csClaude=document.querySelector('#city-split .cs-claude'); if(csClaude) csClaude.style.width=`${split.claudePct}%`;
|
|
988
|
+
const csCodex=document.querySelector('#city-split .cs-codex'); if(csCodex) csCodex.style.width=`${split.codexPct}%`;
|
|
989
|
+
const csClB=document.querySelector('#city-split .cs-l.claude b'); if(csClB) csClB.textContent=`${split.claudePct}%`;
|
|
990
|
+
const csCdB=document.querySelector('#city-split .cs-l.codex b'); if(csCdB) csCdB.textContent=`${split.codexPct}%`;
|
|
991
|
+
const tlStartDate=new Date(report?.scanStartDate);
|
|
992
|
+
if(!Number.isNaN(tlStartDate.getTime())){
|
|
993
|
+
const tlStartEl=document.querySelector('#city-timeline .tl-start');
|
|
994
|
+
if(tlStartEl) tlStartEl.textContent=tlStartDate.toLocaleString('en-US',{month:'short', year:'numeric', timeZone:'UTC'});
|
|
995
|
+
}
|
|
996
|
+
const spanDays=daysBetween(report?.scanStartDate, report?.scanEndDate);
|
|
997
|
+
const spanLabel=spanDays>=60 ? `${Math.round(spanDays/30.44)} months with AI` : `${spanDays} day${spanDays===1?'':'s'} with AI`;
|
|
998
|
+
const tlSpanEl=document.querySelector('#city-timeline .tl-span'); if(tlSpanEl) tlSpanEl.textContent=spanLabel;
|
|
633
999
|
const dirtyPct=document.querySelector('.dirty-context .pct');
|
|
634
|
-
if(dirtyPct) dirtyPct.innerHTML=`<span id="setup-metric-value">${
|
|
1000
|
+
if(dirtyPct) dirtyPct.innerHTML=`<span id="setup-metric-value">${metricValue}</span><span class="word" id="setup-metric-label">${metricLabel}</span><button class="v7-infoBtn" id="setup-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button>`;
|
|
635
1001
|
const dirtyDesc=document.querySelector('.dirty-context .desc');
|
|
636
|
-
if(dirtyDesc) dirtyDesc.id='setup-metric-say';
|
|
1002
|
+
if(dirtyDesc){ dirtyDesc.id='setup-metric-say'; dirtyDesc.innerHTML=metricSay; }
|
|
637
1003
|
const monthEl=document.querySelector('.counterfactual text:first-of-type'); if(monthEl) monthEl.textContent=reach.month;
|
|
638
1004
|
const dayEl=document.querySelector('.counterfactual text:last-of-type'); if(dayEl) dayEl.textContent=reach.day;
|
|
639
1005
|
const cfHero=document.querySelector('.cf-hero'); if(cfHero) cfHero.innerHTML=`Echo saves you <b>${days} days</b>`;
|
|
640
1006
|
const cfSay=document.querySelector('.cf-say'); if(cfSay) cfSay.innerHTML=`You'd have reached today's workspace on <b>${reach.label}</b>, not ${instead}.`;
|
|
641
1007
|
signpostTokens=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
642
|
-
|
|
643
|
-
|
|
1008
|
+
signpostHours=`${attention}h`;
|
|
1009
|
+
signpostCost=cost;
|
|
1010
|
+
const dirtyShare=Math.max(0, Number(dirty)||0)/100;
|
|
1011
|
+
signpostWastedTime=`≈${Math.round((Number(attention)||0)*dirtyShare)}h WASTED`;
|
|
1012
|
+
signpostWastedPct=`${Math.round(Number(dirty))||0}% WASTED`;
|
|
1013
|
+
signpostWastedCost=`≈${money((Number(report?.cost?.total)||0)*dirtyShare)} WASTED`;
|
|
644
1014
|
}
|
|
645
1015
|
async function loadSetupReport(){
|
|
646
1016
|
const nonce=pageParams.get('nonce');
|
|
647
|
-
if(!setupMode
|
|
1017
|
+
if(!setupMode) return null;
|
|
648
1018
|
const countEl=document.getElementById('scan-count');
|
|
649
|
-
|
|
1019
|
+
const loadingEl=document.getElementById('scan-loading');
|
|
1020
|
+
const setLoadState=(kind, title, detail)=>{
|
|
1021
|
+
const titleEl=loadingEl?.querySelector('.title');
|
|
1022
|
+
const eyebrowEl=loadingEl?.querySelector('.eyebrow');
|
|
1023
|
+
const barEl=loadingEl?.querySelector('.bar');
|
|
1024
|
+
if(titleEl) titleEl.textContent=title;
|
|
1025
|
+
if(eyebrowEl && kind!=='running') eyebrowEl.textContent=kind==='empty'?'Local scan complete':'Local scan error';
|
|
1026
|
+
if(barEl) barEl.hidden=kind!=='running';
|
|
1027
|
+
if(countEl) countEl.textContent=String(detail||'').slice(0, 240);
|
|
1028
|
+
};
|
|
1029
|
+
const reportShape=(report)=>{
|
|
1030
|
+
if(!report || typeof report!=='object') return {ok:false, code:'REPORT_NOT_OBJECT'};
|
|
1031
|
+
if(report.schemaVersion!==1) return {ok:false, code:'REPORT_SCHEMA_UNSUPPORTED'};
|
|
1032
|
+
const origin=report.dataOrigin;
|
|
1033
|
+
if((previewMode && origin!=='design-preview') || (!previewMode && origin!=='local-workspace-scan')){
|
|
1034
|
+
return {ok:false, code:'REPORT_ORIGIN_REJECTED'};
|
|
1035
|
+
}
|
|
1036
|
+
const finiteNonNegative=(value)=>typeof value==='number' && Number.isFinite(value) && value>=0;
|
|
1037
|
+
const scale=report.scale;
|
|
1038
|
+
const cost=report.cost;
|
|
1039
|
+
const hours=report.userWorkingHours;
|
|
1040
|
+
const summary=goldenSummary(report);
|
|
1041
|
+
const projection=report.billedWasteProjection;
|
|
1042
|
+
const cf=report.counterfactual;
|
|
1043
|
+
if(!scale || typeof scale!=='object' || !finiteNonNegative(scale.totalTokens) || Number(scale.totalTokens)<=0 ||
|
|
1044
|
+
!finiteNonNegative(scale.totalInputTokens) || Number(scale.totalInputTokens)<=0 ||
|
|
1045
|
+
!finiteNonNegative(scale.sessionCount) || Number(scale.sessionCount)<=0 ||
|
|
1046
|
+
!finiteNonNegative(scale.repoCount) || Number(scale.repoCount)<=0){
|
|
1047
|
+
return {ok:false, code:'REPORT_SCALE_INVALID'};
|
|
1048
|
+
}
|
|
1049
|
+
if(!cost || typeof cost!=='object' || !finiteNonNegative(cost.total) || !cost.byModel || typeof cost.byModel!=='object'){
|
|
1050
|
+
return {ok:false, code:'REPORT_COST_INVALID'};
|
|
1051
|
+
}
|
|
1052
|
+
if(!hours || typeof hours!=='object' || !finiteNonNegative(hours.userAttentionHours) ||
|
|
1053
|
+
!finiteNonNegative(hours.averageDailyAttentionHours)){
|
|
1054
|
+
return {ok:false, code:'REPORT_ATTENTION_INVALID'};
|
|
1055
|
+
}
|
|
1056
|
+
if(!summary || !finiteNonNegative(summary.sessionsAnalyzed) || Number(summary.sessionsAnalyzed)<=0 ||
|
|
1057
|
+
!finiteNonNegative(summary.officialInputTokens) || Number(summary.officialInputTokens)<=0 ||
|
|
1058
|
+
!finiteNonNegative(summary.wasteTokens) || !finiteNonNegative(summary.usefulTokens) ||
|
|
1059
|
+
!finiteNonNegative(summary.wastePct) || Number(summary.wastePct)>100){
|
|
1060
|
+
return {ok:false, code:'REPORT_CANONICAL_INVALID'};
|
|
1061
|
+
}
|
|
1062
|
+
if(!projection || typeof projection!=='object' || projection.method!=='canonical-window-share-over-provider-input' ||
|
|
1063
|
+
!finiteNonNegative(projection.billedInputTokens) || projection.billedInputTokens!==scale.totalInputTokens ||
|
|
1064
|
+
!finiteNonNegative(projection.canonicalInputTokens) || projection.canonicalInputTokens!==summary.officialInputTokens ||
|
|
1065
|
+
!finiteNonNegative(projection.canonicalWasteTokens) || projection.canonicalWasteTokens!==summary.wasteTokens ||
|
|
1066
|
+
!finiteNonNegative(projection.projectedUsefulTokens) || !finiteNonNegative(projection.projectedWasteTokens) ||
|
|
1067
|
+
projection.projectedUsefulTokens+projection.projectedWasteTokens!==projection.billedInputTokens){
|
|
1068
|
+
return {ok:false, code:'REPORT_PROJECTION_INVALID'};
|
|
1069
|
+
}
|
|
1070
|
+
if(!Array.isArray(report.repos) || !report.repos.some((repo)=>repo && typeof repo.name==='string' && repo.name.trim() &&
|
|
1071
|
+
typeof repo.tokens==='number' && Number.isFinite(repo.tokens) && repo.tokens>0)){
|
|
1072
|
+
return {ok:false, code:'REPORT_REPOS_EMPTY'};
|
|
1073
|
+
}
|
|
1074
|
+
if(!Number.isFinite(contextHealthScore(report)) || !cf || typeof cf!=='object' ||
|
|
1075
|
+
!finiteNonNegative(cf.daysGained) || !finiteNonNegative(cf.targetScore) || cf.targetScore>100 ||
|
|
1076
|
+
typeof report.scanStartDate!=='string' || Number.isNaN(Date.parse(report.scanStartDate)) ||
|
|
1077
|
+
typeof report.scanEndDate!=='string' || Number.isNaN(Date.parse(report.scanEndDate)) ||
|
|
1078
|
+
typeof cf.counterfactualDate!=='string' || Number.isNaN(Date.parse(cf.counterfactualDate))){
|
|
1079
|
+
return {ok:false, code:'REPORT_DERIVED_FIELDS_INVALID'};
|
|
1080
|
+
}
|
|
1081
|
+
return {ok:true};
|
|
1082
|
+
};
|
|
1083
|
+
const classifyEnvelope=(envelope)=>{
|
|
1084
|
+
if(!envelope || typeof envelope!=='object' || envelope.schemaVersion!==1 ||
|
|
1085
|
+
typeof envelope.scanId!=='string' || !envelope.scanId.trim()){
|
|
1086
|
+
return {kind:'invalid', code:'REPORT_ENVELOPE_INVALID'};
|
|
1087
|
+
}
|
|
1088
|
+
if(envelope.kind==='empty') return {kind:'empty'};
|
|
1089
|
+
if(envelope.kind==='failed'){
|
|
1090
|
+
const error=envelope.error && typeof envelope.error==='object' ? envelope.error : {};
|
|
1091
|
+
return {kind:'failed', code:String(error.code||'REPORT_BUILD_FAILED'), message:String(error.message||'The local report could not be built.')};
|
|
1092
|
+
}
|
|
1093
|
+
if(envelope.kind!=='ready') return {kind:'invalid', code:'REPORT_ENVELOPE_KIND_INVALID'};
|
|
1094
|
+
const shape=reportShape(envelope.report);
|
|
1095
|
+
if(!shape.ok) return {kind:'invalid', code:shape.code};
|
|
1096
|
+
const origin=envelope.report.dataOrigin;
|
|
1097
|
+
if(origin==='local-workspace-scan' && envelope.mode!=='production') return {kind:'invalid', code:'REPORT_MODE_INVALID'};
|
|
1098
|
+
if(origin==='design-preview' && (!previewMode || envelope.mode!=='preview')) return {kind:'invalid', code:'REPORT_PREVIEW_REJECTED'};
|
|
1099
|
+
return {kind:'ready', report:envelope.report};
|
|
1100
|
+
};
|
|
1101
|
+
if(!nonce){
|
|
1102
|
+
setLoadState('failed', 'Your local report could not be shown', 'REPORT_NONCE_MISSING: Rerun Echo setup to open a verified local scan.');
|
|
1103
|
+
return new Promise(()=>{});
|
|
1104
|
+
}
|
|
1105
|
+
let cleanupParentListener=()=>{};
|
|
1106
|
+
let readyTimer=null;
|
|
1107
|
+
const parentEnvelope=new Promise((resolve)=>{
|
|
1108
|
+
const postReady=()=>{ try{ window.parent?.postMessage({type:'echomem:city-ready', nonce}, location.origin); }catch(_){} };
|
|
1109
|
+
const onMessage=(event)=>{
|
|
1110
|
+
if(event.origin!==location.origin) return;
|
|
1111
|
+
const data=event.data||{};
|
|
1112
|
+
if(data.type==='echomem:setup-report' && data.nonce===nonce && Object.prototype.hasOwnProperty.call(data,'envelope')){
|
|
1113
|
+
resolve({source:'parent', envelope:data.envelope});
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
cleanupParentListener=()=>{ window.removeEventListener('message', onMessage); if(readyTimer) window.clearInterval(readyTimer); };
|
|
1117
|
+
window.addEventListener('message', onMessage);
|
|
1118
|
+
postReady();
|
|
1119
|
+
readyTimer=window.setInterval(postReady, 500);
|
|
1120
|
+
});
|
|
1121
|
+
const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1122
|
+
const fetchOnce=async()=>{
|
|
1123
|
+
// A design-preview iframe may consume only its explicit parent envelope. It must
|
|
1124
|
+
// never race the production /report endpoint and accidentally display local data.
|
|
1125
|
+
if(previewMode) return null;
|
|
1126
|
+
const controller=typeof AbortController!=='undefined' ? new AbortController() : null;
|
|
1127
|
+
const timer=controller ? window.setTimeout(()=>controller.abort(), 3500) : null;
|
|
650
1128
|
try{
|
|
651
|
-
const res=await fetch(`/report?nonce=${encodeURIComponent(nonce)}`, {credentials:'omit', cache:'no-store'});
|
|
652
|
-
if(res.status===200)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1129
|
+
const res=await fetch(`/report?nonce=${encodeURIComponent(nonce)}`, {credentials:'omit', cache:'no-store', signal:controller?.signal});
|
|
1130
|
+
if(res.status===200){
|
|
1131
|
+
try{ return {source:'server', envelope:await res.json()}; }
|
|
1132
|
+
catch(_){ return {source:'failure', code:'REPORT_RESPONSE_INVALID', message:'The local scanner returned an unreadable report.'}; }
|
|
1133
|
+
}
|
|
1134
|
+
if(res.status!==202){
|
|
1135
|
+
let payload=null;
|
|
1136
|
+
try{ payload=await res.json(); }catch(_){}
|
|
1137
|
+
if(payload && payload.kind==='failed') return {source:'server', envelope:payload};
|
|
1138
|
+
const error=payload?.error && typeof payload.error==='object' ? payload.error : {};
|
|
1139
|
+
return {
|
|
1140
|
+
source:'failure',
|
|
1141
|
+
code:String(error.code||`REPORT_HTTP_${res.status}`),
|
|
1142
|
+
message:String(error.message||'The local scanner could not provide this report.'),
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
// 202 = still scanning. This is a file-processing counter; final session totals are
|
|
1146
|
+
// deduplicated separately once the report has finished parsing.
|
|
1147
|
+
try{
|
|
1148
|
+
const p=await res.json();
|
|
1149
|
+
if(p && p.kind==='failed') return {source:'server', envelope:p};
|
|
1150
|
+
const progress=p?.progress || p;
|
|
1151
|
+
if(countEl && progress && Number(progress.total)>0){
|
|
1152
|
+
const scanned=Number(progress.scanned||0);
|
|
1153
|
+
const total=Number(progress.total||0);
|
|
1154
|
+
const elapsed=Number(progress.stageElapsedMs||0)>0 ? ` · ${Math.round(Number(progress.stageElapsedMs)/1000)}s` : '';
|
|
1155
|
+
if(progress.stage && progress.stage!=='reading-transcripts'){
|
|
1156
|
+
countEl.textContent=progress.stage==='classifying-repeated-context'
|
|
1157
|
+
? `${progress.label || 'classifying repeated context'} ${scanned.toLocaleString()} / ${total.toLocaleString()}${elapsed}`
|
|
1158
|
+
: `${progress.label || 'building report'}${elapsed}`;
|
|
1159
|
+
}else{
|
|
1160
|
+
countEl.textContent=`read ${scanned.toLocaleString()} / ${total.toLocaleString()} transcript files`;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}catch(_){ return {source:'failure', code:'REPORT_PROGRESS_INVALID', message:'The local scanner returned an unreadable progress update.'}; }
|
|
1164
|
+
return null;
|
|
1165
|
+
}catch(error){
|
|
1166
|
+
if(error?.name!=='AbortError') setLoadState('running', 'Reading your local history', 'Local scanner connection interrupted · retrying');
|
|
1167
|
+
return null;
|
|
1168
|
+
}finally{
|
|
1169
|
+
if(timer) window.clearTimeout(timer);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
const stopClosed=(kind, code, message)=>{
|
|
1173
|
+
cleanupParentListener();
|
|
1174
|
+
if(kind==='empty'){
|
|
1175
|
+
setLoadState('empty', 'No local workspace data found', 'No demo data was substituted.');
|
|
1176
|
+
}else{
|
|
1177
|
+
setLoadState('failed', 'Your local report could not be shown', `${code}: ${message||'Rerun Echo setup to try again.'}`);
|
|
1178
|
+
}
|
|
1179
|
+
return new Promise(()=>{});
|
|
1180
|
+
};
|
|
1181
|
+
while(true){
|
|
1182
|
+
const result=await Promise.race([parentEnvelope, fetchOnce()]);
|
|
1183
|
+
if(result){
|
|
1184
|
+
if(result.source==='failure') return stopClosed('failed', result.code, result.message);
|
|
1185
|
+
const classified=classifyEnvelope(result.envelope);
|
|
1186
|
+
if(classified.kind==='ready'){
|
|
1187
|
+
cleanupParentListener();
|
|
1188
|
+
return result.envelope;
|
|
1189
|
+
}
|
|
1190
|
+
if(classified.kind==='empty') return stopClosed('empty');
|
|
1191
|
+
return stopClosed('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
|
|
1192
|
+
}
|
|
1193
|
+
const duringSleep=await Promise.race([parentEnvelope, sleep(500).then(()=>null)]);
|
|
1194
|
+
if(duringSleep){
|
|
1195
|
+
const classified=classifyEnvelope(duringSleep.envelope);
|
|
1196
|
+
if(classified.kind==='ready'){
|
|
1197
|
+
cleanupParentListener();
|
|
1198
|
+
return duringSleep.envelope;
|
|
1199
|
+
}
|
|
1200
|
+
if(classified.kind==='empty') return stopClosed('empty');
|
|
1201
|
+
return stopClosed('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
|
|
1202
|
+
}
|
|
658
1203
|
}
|
|
659
|
-
return null;
|
|
660
1204
|
}
|
|
661
|
-
let signpostTokens='7.49B';
|
|
662
|
-
let
|
|
663
|
-
|
|
1205
|
+
let signpostTokens=setupMode?'—':'7.49B';
|
|
1206
|
+
let signpostHours=setupMode?'—':'191.1h';
|
|
1207
|
+
let signpostCost=setupMode?'—':'$13,689';
|
|
1208
|
+
let signpostWastedTime=setupMode?'—':'≈162h WASTED';
|
|
1209
|
+
let signpostWastedPct=setupMode?'—':'85% WASTED';
|
|
1210
|
+
let signpostWastedCost=setupMode?'—':'≈$11,636 WASTED';
|
|
1211
|
+
const setupEnvelope=await loadSetupReport();
|
|
1212
|
+
const setupReport=setupMode ? setupEnvelope.report : null;
|
|
1213
|
+
const setupLiveRepos=setupMode ? buildSetupRepos(setupReport) : [];
|
|
664
1214
|
applySetupReport(setupReport);
|
|
665
1215
|
renderV7PersonaHover();
|
|
1216
|
+
let comparisonBefore=contextHealthScore(setupReport);
|
|
1217
|
+
if(!Number.isFinite(comparisonBefore)) comparisonBefore=setupMode?0:58;
|
|
1218
|
+
const reportTargetScore=Number(setupReport?.counterfactual?.targetScore);
|
|
1219
|
+
let comparisonAfter=Number.isFinite(reportTargetScore) ? clampScore(reportTargetScore) : (setupMode?comparisonBefore:clampScore(comparisonBefore+28));
|
|
1220
|
+
comparisonAfter=Math.max(comparisonBefore,comparisonAfter);
|
|
1221
|
+
let cityComparison='without';
|
|
1222
|
+
let repairTarget=0;
|
|
1223
|
+
let repairProgress=0;
|
|
1224
|
+
let dirty=true;
|
|
1225
|
+
let shadowFrame=0;
|
|
1226
|
+
|
|
1227
|
+
function setupWasteShare(){
|
|
1228
|
+
const pct=Number(goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || 0);
|
|
1229
|
+
if(Number.isFinite(pct) && pct>0) return Math.max(0, Math.min(0.95, pct/100));
|
|
1230
|
+
return Math.max(0, Math.min(0.95, (100-comparisonBefore)/100));
|
|
1231
|
+
}
|
|
1232
|
+
function setupAttentionHours(){
|
|
1233
|
+
const direct=Number(setupReport?.userWorkingHours?.userAttentionHours);
|
|
1234
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1235
|
+
return all.reduce((sum,o)=>sum+(Number(o.hours)||0),0);
|
|
1236
|
+
}
|
|
1237
|
+
function setupInputTokens(){
|
|
1238
|
+
const direct=Number(setupReport?.billedWasteProjection?.billedInputTokens);
|
|
1239
|
+
if(Number.isFinite(direct) && direct>0) return direct;
|
|
1240
|
+
return 0;
|
|
1241
|
+
}
|
|
1242
|
+
function setupUsefulTokens(){
|
|
1243
|
+
const direct=Number(setupReport?.billedWasteProjection?.projectedUsefulTokens);
|
|
1244
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1245
|
+
return 0;
|
|
1246
|
+
}
|
|
1247
|
+
function setupApiCost(){
|
|
1248
|
+
const direct=Number(setupReport?.cost?.total);
|
|
1249
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1250
|
+
return all.reduce((sum,o)=>sum+(Number(o.cost)||0),0);
|
|
1251
|
+
}
|
|
1252
|
+
function fmtInt(n){ return Math.round(Number(n)||0).toLocaleString('en-US'); }
|
|
1253
|
+
function cityMetricReadout(){
|
|
1254
|
+
const waste=setupWasteShare();
|
|
1255
|
+
const attention=setupAttentionHours();
|
|
1256
|
+
const reportedDaily=Number(setupReport?.userWorkingHours?.averageDailyAttentionHours);
|
|
1257
|
+
const daily=Number.isFinite(reportedDaily) && reportedDaily>0 ? reportedDaily : (attention>0?attention:0);
|
|
1258
|
+
const currentDays=daily>0?Math.ceil(attention/daily):0;
|
|
1259
|
+
const withHours=Math.max(0, attention*(1-waste));
|
|
1260
|
+
const withDays=daily>0?Math.ceil(withHours/daily):0;
|
|
1261
|
+
const inputTokens=setupInputTokens();
|
|
1262
|
+
const usefulTokens=Math.max(0, Math.min(inputTokens, setupUsefulTokens()));
|
|
1263
|
+
const apiCost=setupApiCost();
|
|
1264
|
+
const withCost=Math.max(0, apiCost*(1-waste));
|
|
1265
|
+
const savedDays=Math.max(0, currentDays-withDays);
|
|
1266
|
+
const savedTokens=Math.max(0, inputTokens-usefulTokens);
|
|
1267
|
+
const savedCost=Math.max(0, apiCost-withCost);
|
|
1268
|
+
const mode=cityComparison==='with'?'with':'without';
|
|
1269
|
+
const metric=cityMetric==='tokens'||cityMetric==='cost'?cityMetric:'time';
|
|
1270
|
+
const data={
|
|
1271
|
+
time:{
|
|
1272
|
+
without:{value:`${fmtInt(currentDays)} days`,label:'time without Echo',say:`${fmtInt(attention)}h of agent work at <b>${oneDec(daily)}h per active day</b>. This is the pace that built the current city.`},
|
|
1273
|
+
with:{value:`${fmtInt(withDays)} days`,label:'time with Echo',say:`Same project state, with repeat context cleared. That frees about <b>${fmtInt(savedDays)} working days</b> for everything else.`}
|
|
1274
|
+
},
|
|
1275
|
+
tokens:{
|
|
1276
|
+
without:{value:fmtTok(inputTokens),label:'tokens without Echo',say:`The current scan analyzed <b>${fmtTok(inputTokens)} input tokens</b>, including context the agent had to reload again.`},
|
|
1277
|
+
with:{value:fmtTok(usefulTokens),label:'tokens with Echo',say:`Echo carries the useful checkpoint forward, so about <b>${fmtTok(savedTokens)}</b> repeat tokens do not need to be reloaded.`}
|
|
1278
|
+
},
|
|
1279
|
+
cost:{
|
|
1280
|
+
without:{value:money(apiCost),label:'API cost without Echo',say:`API-equivalent list-price scale for the context volume in this scan. This is a volume estimate, not your subscription bill.`},
|
|
1281
|
+
with:{value:money(withCost),label:'API cost with Echo',say:`Modeled after removing repeat context from the scan, avoiding about <b>${money(savedCost)}</b> of reloaded context volume.`}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
return data[metric][mode];
|
|
1285
|
+
}
|
|
1286
|
+
function updateFieldReportMetric(){
|
|
1287
|
+
if(!setupMode) return;
|
|
1288
|
+
const readout=cityMetricReadout();
|
|
1289
|
+
const metricValue=document.getElementById('fr-metric-value'); if(metricValue) metricValue.textContent=readout.value;
|
|
1290
|
+
const metricLabel=document.getElementById('fr-metric-label'); if(metricLabel) metricLabel.textContent=readout.label;
|
|
1291
|
+
const metricSay=document.getElementById('fr-metric-say'); if(metricSay) metricSay.innerHTML=readout.say;
|
|
1292
|
+
}
|
|
666
1293
|
|
|
667
1294
|
const renderer = new THREE.WebGLRenderer({canvas:document.getElementById('repo-visualizer'), antialias:true, alpha:true});
|
|
668
1295
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
@@ -694,8 +1321,9 @@ function buildEnv(){
|
|
|
694
1321
|
}
|
|
695
1322
|
scene.environment = buildEnv();
|
|
696
1323
|
|
|
697
|
-
//
|
|
698
|
-
|
|
1324
|
+
// Frozen poster data is available only outside setup. Setup starts empty and can be
|
|
1325
|
+
// populated solely by the validated local report envelope above.
|
|
1326
|
+
const POSTER_REPOS=[
|
|
699
1327
|
{name:'WebPageReactVersion', x:-30, z:-60, tool:'claude', pin:true, tokens:3436946364, cost:8896, days:18.6, hours:92.8, sessions:74},
|
|
700
1328
|
{name:'EchoMem-Chrome', x:140, z:-14, tool:'claude', pin:true, tokens:2863590896, cost:6959, days:14.0, hours:70.1, sessions:38},
|
|
701
1329
|
{name:'echo-design-system', x:30, z:30, tool:'codex', tokens:382642012, cost:1106, days:4.2, hours:21.2, sessions:29},
|
|
@@ -710,12 +1338,12 @@ let REPOS=[
|
|
|
710
1338
|
{name:'dist', x:-210,z:118, tool:'claude', tokens:4550877, cost:8, days:0, hours:0, sessions:2},
|
|
711
1339
|
{name:'Daimler Engineering', x:-168,z:160, tool:'codex', tokens:4439902, cost:15, days:0, hours:0.2, sessions:2},
|
|
712
1340
|
];
|
|
1341
|
+
let REPOS=setupMode ? [] : POSTER_REPOS;
|
|
713
1342
|
// per-repo codex/claude split (frozen poster values from city-data.json) → drives the stacked two-colour buildings
|
|
714
1343
|
const REPO_SPLIT={'WebPageReactVersion':[31,69],'EchoMem-Chrome':[19,81],'echo-design-system':[44,56],'sleepy-merkle-3d0470':[0,100],'DG-Web':[0,100],'Kobe Life':[100,0],'public':[0,100],'EchoMemory_log':[63,37],'production-prompt':[0,100],'brands':[0,100],'components':[0,100],'dist':[0,100],'Daimler Engineering':[100,0]};
|
|
715
1344
|
for(const o of REPOS){ const s=REPO_SPLIT[o.name]; if(s){ o.codexPct=s[0]; o.claudePct=s[1]; } else { o.codexPct=o.tool==='codex'?100:0; o.claudePct=100-o.codexPct; } }
|
|
716
|
-
if(setupMode
|
|
717
|
-
|
|
718
|
-
if(liveRepos.length) REPOS=liveRepos;
|
|
1345
|
+
if(setupMode){
|
|
1346
|
+
REPOS=setupLiveRepos;
|
|
719
1347
|
window.__ECHOMEM_CITY_REPORT_REPO_COUNT=Array.isArray(setupReport?.repos) ? setupReport.repos.length : -1;
|
|
720
1348
|
window.__ECHOMEM_CITY_REPOS=REPOS.map(({name,tokens,cost,hours,sessions,tool})=>({name,tokens,cost,hours,sessions,tool}));
|
|
721
1349
|
}
|
|
@@ -818,17 +1446,21 @@ const fill = new THREE.DirectionalLight(0xffffff, 0.4); fill.position.set(CX+420
|
|
|
818
1446
|
const floor = new THREE.Mesh(new THREE.PlaneGeometry(6000,6000), new THREE.ShadowMaterial({opacity:0.22}));
|
|
819
1447
|
floor.rotation.x=-Math.PI/2; floor.position.y=0; floor.receiveShadow=true; scene.add(floor);
|
|
820
1448
|
|
|
821
|
-
//
|
|
1449
|
+
// Comparison material: corrupted buildings are dark and chalky; repaired buildings regain colour
|
|
1450
|
+
// without returning to the glossy toy-plastic finish.
|
|
822
1451
|
function clayGlass(hex){
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1452
|
+
const clean=new THREE.Color(hex).multiplyScalar(0.82);
|
|
1453
|
+
const damaged=clean.clone().multiplyScalar(0.58);
|
|
1454
|
+
const material=new THREE.MeshPhysicalMaterial({
|
|
1455
|
+
color:damaged,
|
|
1456
|
+
roughness:0.9, metalness:0.01,
|
|
1457
|
+
transmission:0.05, ior:1.25, thickness:45,
|
|
1458
|
+
attenuationColor:damaged.clone(), attenuationDistance:58,
|
|
1459
|
+
clearcoat:0.015, clearcoatRoughness:0.94,
|
|
1460
|
+
envMapIntensity:0.24, transparent:true,
|
|
831
1461
|
});
|
|
1462
|
+
buildingMaterials.push({material,damaged,clean});
|
|
1463
|
+
return material;
|
|
832
1464
|
}
|
|
833
1465
|
function selectiveRoundedBoxGeometry(width, height, depth, segments=8, radius=0.1, opts={}){
|
|
834
1466
|
const roundTop=opts.roundTop!==false, roundBottom=opts.roundBottom!==false;
|
|
@@ -870,6 +1502,7 @@ function selectiveRoundedBoxGeometry(width, height, depth, segments=8, radius=0.
|
|
|
870
1502
|
}
|
|
871
1503
|
function addCube(o){
|
|
872
1504
|
const {x,z,w,h,d,tool}=o;
|
|
1505
|
+
const damage=corruptionProfile(o);
|
|
873
1506
|
const r=Math.min(Math.min(w,h,d)*0.12, 12);
|
|
874
1507
|
// proportional split: Claude (orange) on the bottom, Codex (green) on top; segment heights ∝ each tool's token share
|
|
875
1508
|
let cl=Number(o.claudePct), cx=Number(o.codexPct);
|
|
@@ -877,46 +1510,156 @@ function addCube(o){
|
|
|
877
1510
|
const total=cl+cx, segs=[];
|
|
878
1511
|
if(cl>0) segs.push([h*cl/total, TOOL.claude]);
|
|
879
1512
|
if(cx>0) segs.push([h*cx/total, TOOL.codex]);
|
|
1513
|
+
const slices=[];
|
|
1514
|
+
for(const [hSeg,color] of segs){
|
|
1515
|
+
const count=hSeg>78?4:hSeg>44?3:hSeg>24?2:1;
|
|
1516
|
+
const gap=count>1?Math.min(3.2,hSeg*0.055):0;
|
|
1517
|
+
const sliceH=(hSeg-gap*(count-1))/count;
|
|
1518
|
+
for(let i=0;i<count;i++) slices.push({h:sliceH,color,gap:i===count-1?0:gap});
|
|
1519
|
+
}
|
|
880
1520
|
const group=new THREE.Group();
|
|
1521
|
+
group.position.set(x,0,z);
|
|
1522
|
+
group.rotation.x=damage.leanX*damage.amount;
|
|
1523
|
+
group.rotation.z=damage.leanZ*damage.amount;
|
|
1524
|
+
repairGroups.push({group,damageX:group.rotation.x,damageZ:group.rotation.z});
|
|
881
1525
|
let yBase=TRAY_TOP - 3;
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1526
|
+
let cleanY=TRAY_TOP - 3;
|
|
1527
|
+
const fractured=slices.length>1;
|
|
1528
|
+
for(let i=0;i<slices.length;i++){
|
|
1529
|
+
const {h:hSeg,color,gap}=slices[i];
|
|
885
1530
|
const mat=clayGlass(color);
|
|
886
|
-
const geom=
|
|
887
|
-
? selectiveRoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45), {roundBottom:i===0, roundTop:i===
|
|
1531
|
+
const geom=fractured
|
|
1532
|
+
? selectiveRoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45), {roundBottom:i===0, roundTop:i===slices.length-1})
|
|
888
1533
|
: new RoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45));
|
|
889
1534
|
const m=new THREE.Mesh(geom, mat);
|
|
890
|
-
|
|
1535
|
+
const stagger=i===0?0:(hash01(o.name,i+61)-0.5)*2;
|
|
1536
|
+
const splitShift=stagger*Math.min(w*0.24, 13)*damage.amount;
|
|
1537
|
+
m.position.set(splitShift, yBase + hSeg/2, -splitShift*(0.25+hash01(o.name,i+67)*0.45));
|
|
1538
|
+
if(i>0) m.rotation.y=(hash01(o.name,i+71)-0.5)*0.30*damage.amount;
|
|
1539
|
+
repairPieces.push({
|
|
1540
|
+
mesh:m,
|
|
1541
|
+
damagePos:m.position.clone(),
|
|
1542
|
+
cleanPos:new THREE.Vector3(0,cleanY+hSeg/2,0),
|
|
1543
|
+
damageRotY:m.rotation.y,
|
|
1544
|
+
});
|
|
891
1545
|
m.castShadow=true; m.receiveShadow=true; m.userData=o;
|
|
892
1546
|
group.add(m);
|
|
893
|
-
yBase+=hSeg;
|
|
1547
|
+
yBase+=hSeg+gap;
|
|
1548
|
+
cleanY+=hSeg;
|
|
1549
|
+
}
|
|
1550
|
+
const scarMat=new THREE.MeshStandardMaterial({color:0x343238,roughness:0.92,metalness:0.08,transparent:true});
|
|
1551
|
+
const scarCount=Math.max(1,Math.round(2+damage.amount*3));
|
|
1552
|
+
for(let i=0;i<scarCount;i++){
|
|
1553
|
+
const sw=Math.max(3,w*(0.10+hash01(o.name,101+i)*0.20));
|
|
1554
|
+
const sh=Math.max(2,Math.min(6,h*0.045));
|
|
1555
|
+
const sd=Math.max(2,d*0.06);
|
|
1556
|
+
const scar=new THREE.Mesh(new THREE.BoxGeometry(sw,sh,sd),scarMat);
|
|
1557
|
+
scar.position.set((hash01(o.name,131+i)-0.5)*w*0.72,TRAY_TOP+h*(0.18+hash01(o.name,151+i)*0.67),d*0.5+1.2);
|
|
1558
|
+
scar.rotation.z=(hash01(o.name,171+i)-0.5)*0.55;
|
|
1559
|
+
scar.castShadow=true; scar.userData=o; group.add(scar);
|
|
1560
|
+
corruptionFades.push({mesh:scar,material:scar.material,damagePos:scar.position.clone(),cleanPos:scar.position.clone(),baseScale:scar.scale.clone()});
|
|
1561
|
+
}
|
|
1562
|
+
const chunkCount=Math.max(4,Math.round(4+damage.amount*5));
|
|
1563
|
+
for(let i=0;i<chunkCount;i++){
|
|
1564
|
+
const size=Math.max(3,Math.min(w,d)*(0.07+hash01(o.name,201+i)*0.10));
|
|
1565
|
+
const chunk=new THREE.Mesh(
|
|
1566
|
+
i%3===0?new THREE.TetrahedronGeometry(size,0):new THREE.BoxGeometry(size,size*(0.5+hash01(o.name,221+i)),size*0.72),
|
|
1567
|
+
i%2?scarMat:clayGlass(tool==='codex'?TOOL.codex:TOOL.claude)
|
|
1568
|
+
);
|
|
1569
|
+
const angle=hash01(o.name,241+i)*Math.PI*2;
|
|
1570
|
+
const radius=Math.max(w,d)*(0.62+hash01(o.name,261+i)*1.05);
|
|
1571
|
+
const baseY=TRAY_TOP+2+hash01(o.name,281+i)*Math.min(h*0.72,58);
|
|
1572
|
+
chunk.position.set(Math.cos(angle)*radius,baseY,Math.sin(angle)*radius);
|
|
1573
|
+
chunk.rotation.set(hash01(o.name,301+i)*2.4,hash01(o.name,321+i)*2.4,hash01(o.name,341+i)*2.4);
|
|
1574
|
+
chunk.castShadow=true; chunk.receiveShadow=true; chunk.userData=o; group.add(chunk);
|
|
1575
|
+
chunk.material.transparent=true;
|
|
1576
|
+
corruptionFades.push({
|
|
1577
|
+
mesh:chunk,
|
|
1578
|
+
material:chunk.material,
|
|
1579
|
+
damagePos:chunk.position.clone(),
|
|
1580
|
+
cleanPos:new THREE.Vector3(0,Math.max(TRAY_TOP+4,Math.min(TRAY_TOP+h-5,baseY)),0),
|
|
1581
|
+
baseScale:chunk.scale.clone(),
|
|
1582
|
+
});
|
|
1583
|
+
if(i===0 && h>42) corruptionFloaters.push({mesh:chunk,baseY,phase:damage.phase,speed:0.65+hash01(o.name,361)*0.55});
|
|
894
1584
|
}
|
|
895
1585
|
group.userData=o; scene.add(group); return group;
|
|
896
1586
|
}
|
|
897
1587
|
// repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
|
|
898
|
-
//
|
|
899
|
-
|
|
1588
|
+
// setup mode uses the selected city metric; share-card mode keeps the original tokens + $ poster readout.
|
|
1589
|
+
let cityMetric=setupMode ? 'time' : 'tokens';
|
|
1590
|
+
let timelineProgress=1;
|
|
1591
|
+
const repoLabels=[];
|
|
1592
|
+
function fmtMetric(hours, tokens, cost){
|
|
1593
|
+
if(cityMetric==='tokens') return fmtTok(tokens);
|
|
1594
|
+
if(cityMetric==='cost') return '$'+Math.round(cost).toLocaleString('en-US');
|
|
1595
|
+
return `${(Number(hours)||0).toFixed(1)}h`;
|
|
1596
|
+
}
|
|
1597
|
+
function repoMetricText(o){
|
|
1598
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1599
|
+
return fmtMetric((Number(o.hours)||0)*fraction,(Number(o.tokens)||0)*fraction,(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE));
|
|
1600
|
+
}
|
|
1601
|
+
// GitHub octocat mark (24x24) — same glyph as the report's repo chips.
|
|
1602
|
+
const GH_MARK=new Path2D('M12 .7a11.5 11.5 0 0 0-3.64 22.41c.58.11.79-.25.79-.56v-2.23c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.17.08 1.78 1.2 1.78 1.2 1.04 1.78 2.72 1.27 3.38.97.1-.75.41-1.27.74-1.56-2.57-.29-5.27-1.29-5.27-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.47.11-3.05 0 0 .97-.31 3.16 1.18A10.97 10.97 0 0 1 12 6.12c.98 0 1.96.13 2.88.39 2.2-1.49 3.16-1.18 3.16-1.18.63 1.58.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.4-2.71 5.38-5.29 5.67.42.36.79 1.06.79 2.14v3.27c0 .31.21.68.8.56A11.5 11.5 0 0 0 12 .7Z');
|
|
1603
|
+
function drawRepoLabel(e){
|
|
1604
|
+
const {o, g, tex, PW, PH, numbers}=e;
|
|
1605
|
+
g.clearRect(0,0,PW,PH);
|
|
1606
|
+
g.textAlign='center';
|
|
1607
|
+
if(numbers){
|
|
1608
|
+
// dark ink + white halo: stays readable over the light grid AND the coloured roofs
|
|
1609
|
+
g.save();
|
|
1610
|
+
g.shadowColor='rgba(255,255,255,0.92)'; g.shadowBlur=12; g.shadowOffsetY=0;
|
|
1611
|
+
g.fillStyle='#171717'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
1612
|
+
g.fillText(setupMode ? repoMetricText(o) : fmtTok(o.tokens), PW/2, setupMode ? PH*0.66 : PH*0.18);
|
|
1613
|
+
if(!setupMode){
|
|
1614
|
+
g.fillStyle='rgba(23,23,23,0.85)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
1615
|
+
g.fillText('$'+Math.round(o.cost*COST_SCALE).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
1616
|
+
}
|
|
1617
|
+
g.restore();
|
|
1618
|
+
}
|
|
1619
|
+
// repo name rides a white chip with the octocat, matching .audit-repo-chip in the report
|
|
1620
|
+
const fs=Math.round(PW*0.075);
|
|
1621
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1622
|
+
const icon=Math.round(fs*1.25), gap=Math.round(fs*0.45), padX=Math.round(fs*0.7);
|
|
1623
|
+
const tw=g.measureText(o.name).width;
|
|
1624
|
+
const maxText=PW*0.94-icon-gap-padX*2;
|
|
1625
|
+
const drawTw=Math.min(tw,maxText);
|
|
1626
|
+
const chipW=drawTw+icon+gap+padX*2;
|
|
1627
|
+
const chipH=Math.round(icon+fs*0.85);
|
|
1628
|
+
const cx=PW/2, cyMid=PH*0.86-fs*0.35;
|
|
1629
|
+
const rx=cx-chipW/2, ry=cyMid-chipH/2;
|
|
1630
|
+
g.save();
|
|
1631
|
+
g.shadowColor='rgba(31,35,40,0.25)'; g.shadowBlur=8; g.shadowOffsetY=2;
|
|
1632
|
+
g.beginPath();
|
|
1633
|
+
if(g.roundRect) g.roundRect(rx,ry,chipW,chipH,chipH/2); else g.rect(rx,ry,chipW,chipH);
|
|
1634
|
+
g.fillStyle='rgba(255,255,255,0.96)'; g.fill();
|
|
1635
|
+
g.shadowColor='transparent';
|
|
1636
|
+
g.lineWidth=1.5; g.strokeStyle='rgba(24,23,23,0.32)'; g.stroke();
|
|
1637
|
+
g.translate(rx+padX, cyMid-icon/2); g.scale(icon/24, icon/24);
|
|
1638
|
+
g.fillStyle='#181717'; g.fill(GH_MARK);
|
|
1639
|
+
g.restore();
|
|
1640
|
+
g.save();
|
|
1641
|
+
g.fillStyle='#171717'; g.textAlign='left'; g.textBaseline='middle';
|
|
1642
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1643
|
+
g.translate(rx+padX+icon+gap, cyMid+fs*0.06);
|
|
1644
|
+
if(tw>maxText) g.scale(maxText/tw,1);
|
|
1645
|
+
g.fillText(o.name,0,0);
|
|
1646
|
+
g.restore();
|
|
1647
|
+
tex.needsUpdate=true;
|
|
1648
|
+
}
|
|
1649
|
+
function addTopLabel(o, numbers=true, lift=0){
|
|
900
1650
|
const PW=320, PH=280;
|
|
901
1651
|
const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
|
|
902
|
-
const g=cv.getContext('2d');
|
|
903
|
-
g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
|
|
904
|
-
// tokens — the headline, flies highest
|
|
905
|
-
g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
906
|
-
g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
|
|
907
|
-
// dollars — just under the tokens, also up high
|
|
908
|
-
g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
909
|
-
g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
910
|
-
// name — small tag sitting just above the bar top
|
|
911
|
-
g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
|
|
912
|
-
const fw=g.measureText(o.name).width, maxw=PW*0.94;
|
|
913
|
-
g.save(); g.translate(PW/2, PH*0.86); if(fw>maxw){ const s=maxw/fw; g.scale(s,s); } g.fillText(o.name, 0, 0); g.restore();
|
|
1652
|
+
const g=cv.getContext('2d');
|
|
914
1653
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
915
1654
|
const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
|
|
916
1655
|
const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
|
|
1656
|
+
const labelOffset=-3+6+0.36*hWorld+lift;
|
|
917
1657
|
sprite.scale.set(wWorld, hWorld, 1);
|
|
918
|
-
sprite.position.set(o.x, TRAY_TOP + o.h
|
|
1658
|
+
sprite.position.set(o.x, TRAY_TOP + o.h + labelOffset, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
|
|
919
1659
|
scene.add(sprite);
|
|
1660
|
+
const entry={o, g, tex, PW, PH, sprite, numbers, labelOffset};
|
|
1661
|
+
repoLabels.push(entry);
|
|
1662
|
+
drawRepoLabel(entry);
|
|
920
1663
|
}
|
|
921
1664
|
// fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
|
|
922
1665
|
const grain=(function(){
|
|
@@ -931,9 +1674,9 @@ const grain=(function(){
|
|
|
931
1674
|
})();
|
|
932
1675
|
const trayGeo=new RoundedBoxGeometry(trayW, TRAY_TOP, trayD, 8, 12);
|
|
933
1676
|
const tray=new THREE.Mesh(trayGeo, new THREE.MeshPhysicalMaterial({
|
|
934
|
-
color:TRAY_COL, roughness:0.
|
|
935
|
-
clearcoat:0.
|
|
936
|
-
bumpMap:grain, bumpScale:0.
|
|
1677
|
+
color:TRAY_COL, roughness:0.84, metalness:0.02,
|
|
1678
|
+
clearcoat:0.08, clearcoatRoughness:0.82, envMapIntensity:0.4,
|
|
1679
|
+
bumpMap:grain, bumpScale:0.9,
|
|
937
1680
|
}));
|
|
938
1681
|
tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=true; scene.add(tray);
|
|
939
1682
|
// grid + perimeter border on ONE decal: the grid is CLIPPED to the border's rounded rect, so
|
|
@@ -960,13 +1703,49 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
|
|
|
960
1703
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
961
1704
|
const plane=new THREE.Mesh(new THREE.PlaneGeometry(trayW, trayD), new THREE.MeshBasicMaterial({map:tex, transparent:true, depthWrite:false, toneMapped:false}));
|
|
962
1705
|
plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
|
|
1706
|
+
|
|
1707
|
+
// Keep corruption on its own decal so the same city can visibly heal without losing its clean grid.
|
|
1708
|
+
const damageCv=document.createElement('canvas'); damageCv.width=W; damageCv.height=H;
|
|
1709
|
+
const dg=damageCv.getContext('2d');
|
|
1710
|
+
dg.save(); dg.beginPath(); dg.roundRect(m,m,rw,rh,rad); dg.clip();
|
|
1711
|
+
let seed=0xdecafbad;
|
|
1712
|
+
const rnd=()=>{ seed=(Math.imul(seed,1664525)+1013904223)>>>0; return seed/4294967296; };
|
|
1713
|
+
for(let i=0;i<16;i++){
|
|
1714
|
+
const x=m+rnd()*rw, y=m+rnd()*rh, ww=(18+rnd()*54)*scale, hh=(8+rnd()*24)*scale;
|
|
1715
|
+
dg.save(); dg.translate(x,y); dg.rotate((rnd()-0.5)*0.7);
|
|
1716
|
+
dg.fillStyle=i%3===0?'rgba(170,59,51,0.18)':'rgba(43,43,49,0.16)';
|
|
1717
|
+
dg.fillRect(-ww/2,-hh/2,ww,hh); dg.restore();
|
|
1718
|
+
}
|
|
1719
|
+
dg.lineCap='square';
|
|
1720
|
+
for(let i=0;i<22;i++){
|
|
1721
|
+
let x=m+rnd()*rw, y=m+rnd()*rh;
|
|
1722
|
+
dg.strokeStyle=i%5===0?'rgba(176,58,51,0.62)':'rgba(45,44,49,0.55)';
|
|
1723
|
+
dg.lineWidth=(0.9+rnd()*1.5)*scale; dg.beginPath(); dg.moveTo(x,y);
|
|
1724
|
+
const steps=2+Math.floor(rnd()*4);
|
|
1725
|
+
for(let j=0;j<steps;j++){
|
|
1726
|
+
x+=((rnd()-0.5)*42)*scale; y+=((rnd()-0.5)*42)*scale; dg.lineTo(x,y);
|
|
1727
|
+
if(j===1 && rnd()>0.45){ dg.moveTo(x,y); dg.lineTo(x+(rnd()-0.5)*24*scale,y+(rnd()-0.5)*24*scale); dg.moveTo(x,y); }
|
|
1728
|
+
}
|
|
1729
|
+
dg.stroke();
|
|
1730
|
+
}
|
|
1731
|
+
dg.restore();
|
|
1732
|
+
const damageTex=new THREE.CanvasTexture(damageCv); damageTex.colorSpace=THREE.SRGBColorSpace; damageTex.anisotropy=8;
|
|
1733
|
+
const damageMat=new THREE.MeshBasicMaterial({map:damageTex,transparent:true,depthWrite:false,toneMapped:false});
|
|
1734
|
+
const damagePlane=new THREE.Mesh(new THREE.PlaneGeometry(trayW,trayD),damageMat);
|
|
1735
|
+
damagePlane.rotation.x=-Math.PI/2; damagePlane.position.set(CX,TRAY_TOP+0.72,CZ); scene.add(damagePlane);
|
|
1736
|
+
corruptionSurfaces.push(damageMat);
|
|
963
1737
|
})();
|
|
964
1738
|
|
|
965
|
-
|
|
1739
|
+
// De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
|
|
1740
|
+
// its name tag (hover still shows the full repo detail card). Kills the mid-air label collisions.
|
|
1741
|
+
const NUMBERED_LABELS=4;
|
|
1742
|
+
const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0,NUMBERED_LABELS);
|
|
1743
|
+
const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
|
|
1744
|
+
all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o, numberedLift.has(o.name), numberedLift.get(o.name)||0); });
|
|
966
1745
|
|
|
967
|
-
// bottom-right standing "signpost": tray summary
|
|
1746
|
+
// bottom-right standing "signpost": tray summary follows the selected city metric.
|
|
968
1747
|
let signpostCanvas=null, signpostTexture=null;
|
|
969
|
-
function drawSignpost(
|
|
1748
|
+
function drawSignpost(bigText, subLabel, bottomText, accent='#e8473f'){
|
|
970
1749
|
if(!signpostCanvas) return;
|
|
971
1750
|
const W=signpostCanvas.width, H=signpostCanvas.height;
|
|
972
1751
|
const g=signpostCanvas.getContext('2d');
|
|
@@ -974,18 +1753,44 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
974
1753
|
g.fillStyle='rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.fill();
|
|
975
1754
|
g.strokeStyle='rgba(120,126,140,0.30)'; g.lineWidth=3; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.stroke();
|
|
976
1755
|
g.textAlign='center';
|
|
977
|
-
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(
|
|
978
|
-
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(
|
|
1756
|
+
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(bigText, W/2, 150);
|
|
1757
|
+
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(subLabel, W/2, 204);
|
|
979
1758
|
g.fillStyle=accent; g.font='800 62px "IBM Plex Mono", monospace'; g.fillText(bottomText, W/2, 280);
|
|
980
1759
|
if(signpostTexture) signpostTexture.needsUpdate=true;
|
|
981
1760
|
}
|
|
1761
|
+
function signpostBig(){
|
|
1762
|
+
if(setupMode && cityComparison==='with') return cityMetricReadout().value;
|
|
1763
|
+
if(timelineProgress>=0.999) return cityMetric==='tokens'?signpostTokens : cityMetric==='cost'?signpostCost : signpostHours;
|
|
1764
|
+
const totals=all.reduce((sum,o)=>{
|
|
1765
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1766
|
+
sum.tokens+=(Number(o.tokens)||0)*fraction;
|
|
1767
|
+
sum.cost+=(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE);
|
|
1768
|
+
sum.hours+=(Number(o.hours)||0)*fraction;
|
|
1769
|
+
return sum;
|
|
1770
|
+
},{tokens:0,cost:0,hours:0});
|
|
1771
|
+
return cityMetric==='tokens'?fmtTok(totals.tokens):cityMetric==='cost'?money(totals.cost):`${totals.hours.toFixed(1)}h`;
|
|
1772
|
+
}
|
|
1773
|
+
function signpostSub(){
|
|
1774
|
+
if(setupMode && cityComparison==='with') return cityMetric==='tokens'?'MODELED TOKENS' : cityMetric==='cost'?'MODELED API COST' : 'MODELED TIME';
|
|
1775
|
+
return cityMetric==='tokens'?'TOTAL TOKENS' : cityMetric==='cost'?'TOTAL SPENT' : 'TOTAL TIME';
|
|
1776
|
+
}
|
|
1777
|
+
function signpostBottom(){
|
|
1778
|
+
if(cityComparison==='with') return `${comparisonAfter} CONTEXT SCORE`;
|
|
1779
|
+
if(timelineProgress<0.999) return 'HISTORY AT DATE';
|
|
1780
|
+
return cityMetric==='tokens'?signpostWastedPct : cityMetric==='cost'?signpostWastedCost : signpostWastedTime;
|
|
1781
|
+
}
|
|
1782
|
+
function redrawCityMetric(){
|
|
1783
|
+
repoLabels.forEach(drawRepoLabel);
|
|
1784
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom(),cityComparison==='with'?'#2a8f7d':'#e8473f');
|
|
1785
|
+
dirty=true;
|
|
1786
|
+
}
|
|
982
1787
|
(function(){
|
|
983
1788
|
const W=640, H=320;
|
|
984
1789
|
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
985
1790
|
signpostCanvas=cv;
|
|
986
1791
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
987
1792
|
signpostTexture=tex;
|
|
988
|
-
drawSignpost(
|
|
1793
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
989
1794
|
const SIGN_W=152, SIGN_H=76, SIGN_Y=TRAY_TOP+64;
|
|
990
1795
|
const sign=new THREE.Mesh(
|
|
991
1796
|
new THREE.PlaneGeometry(SIGN_W, SIGN_H),
|
|
@@ -1002,6 +1807,114 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
1002
1807
|
post.position.set(sx, TRAY_TOP+postH/2, sz); post.castShadow=true; scene.add(post);
|
|
1003
1808
|
})();
|
|
1004
1809
|
|
|
1810
|
+
(function(){
|
|
1811
|
+
const ct=document.getElementById('city-toggle');
|
|
1812
|
+
if(!ct) return;
|
|
1813
|
+
const thumb=ct.querySelector('.ct-thumb');
|
|
1814
|
+
const moveThumb=(btn)=>{ if(!thumb||!btn) return;
|
|
1815
|
+
thumb.style.width=btn.offsetWidth+'px'; thumb.style.height=btn.offsetHeight+'px';
|
|
1816
|
+
thumb.style.transform=`translate(${btn.offsetLeft}px, ${btn.offsetTop}px)`;
|
|
1817
|
+
};
|
|
1818
|
+
requestAnimationFrame(()=>{
|
|
1819
|
+
const on=ct.querySelector('button.on')||ct.querySelector('button[data-metric]');
|
|
1820
|
+
if(thumb) thumb.style.transition='none';
|
|
1821
|
+
moveThumb(on);
|
|
1822
|
+
requestAnimationFrame(()=>{ if(thumb) thumb.style.transition=''; });
|
|
1823
|
+
});
|
|
1824
|
+
ct.addEventListener('click', (e)=>{
|
|
1825
|
+
const b=e.target.closest('button[data-metric]'); if(!b) return;
|
|
1826
|
+
cityMetric=b.getAttribute('data-metric') || 'time';
|
|
1827
|
+
ct.querySelectorAll('button').forEach((x)=>x.classList.toggle('on', x===b));
|
|
1828
|
+
moveThumb(b);
|
|
1829
|
+
redrawCityMetric();
|
|
1830
|
+
updateFieldReportMetric();
|
|
1831
|
+
});
|
|
1832
|
+
})();
|
|
1833
|
+
|
|
1834
|
+
function setCityComparison(mode){
|
|
1835
|
+
cityComparison=mode==='with'?'with':'without';
|
|
1836
|
+
repairTarget=cityComparison==='with'?1:0;
|
|
1837
|
+
document.body.classList.toggle('with-echo',cityComparison==='with');
|
|
1838
|
+
updateFieldReportMetric();
|
|
1839
|
+
redrawCityMetric();
|
|
1840
|
+
dirty=true;
|
|
1841
|
+
}
|
|
1842
|
+
setCityComparison('without');
|
|
1843
|
+
// Parent page (setup report fullscreen modal) drives the dirty/clean comparison remotely.
|
|
1844
|
+
window.addEventListener('message',(e)=>{if(e.origin!==location.origin)return;const d=e.data||{};if(d.type==='echomem:city-comparison')setCityComparison(d.mode==='with'?'with':'without');if(d.type==='echomem:city-metric'){const m=({time:1,tokens:1,cost:1})[d.metric]?d.metric:'time';const b=document.querySelector('#city-toggle button[data-metric="'+m+'"]');if(b)b.click();}});
|
|
1845
|
+
|
|
1846
|
+
const timelineStartMs=Date.parse(setupReport?.scanStartDate||'2024-12-01T00:00:00Z');
|
|
1847
|
+
const timelineEndMs=Math.max(timelineStartMs+86400000,Date.parse(setupReport?.scanEndDate||new Date().toISOString()));
|
|
1848
|
+
function repoFractionAt(o,cutoffMs){
|
|
1849
|
+
if(timelineProgress>=0.999) return 1;
|
|
1850
|
+
if(!Array.isArray(o.daily)||!o.daily.length) return timelineProgress;
|
|
1851
|
+
let total=0,visible=0;
|
|
1852
|
+
for(const point of o.daily){
|
|
1853
|
+
if(!Array.isArray(point)||point.length<2) continue;
|
|
1854
|
+
const amount=Math.max(0,Number(point[1])||0);
|
|
1855
|
+
const day=Date.parse(String(point[0])+'T23:59:59Z');
|
|
1856
|
+
total+=amount;
|
|
1857
|
+
if(Number.isFinite(day)&&day<=cutoffMs) visible+=amount;
|
|
1858
|
+
}
|
|
1859
|
+
return total>0?Math.max(0,Math.min(1,visible/total)):timelineProgress;
|
|
1860
|
+
}
|
|
1861
|
+
function setTimelineProgress(value){
|
|
1862
|
+
timelineProgress=Math.max(0,Math.min(1,Number(value)||0));
|
|
1863
|
+
const cutoffMs=timelineStartMs+(timelineEndMs-timelineStartMs)*timelineProgress;
|
|
1864
|
+
const handle=document.querySelector('#city-timeline .tl-handle');
|
|
1865
|
+
const progress=document.querySelector('#city-timeline .tl-progress');
|
|
1866
|
+
const selected=document.querySelector('#city-timeline .tl-selected');
|
|
1867
|
+
const pct=timelineProgress*100;
|
|
1868
|
+
if(handle){ handle.style.left=`${pct}%`; handle.setAttribute('aria-valuenow',String(Math.round(pct))); }
|
|
1869
|
+
if(progress) progress.style.width=`${pct}%`;
|
|
1870
|
+
if(selected){
|
|
1871
|
+
selected.style.left=`${Math.max(7,Math.min(93,pct))}%`;
|
|
1872
|
+
selected.textContent=timelineProgress>=0.995?'Today':new Date(cutoffMs).toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',timeZone:'UTC'});
|
|
1873
|
+
}
|
|
1874
|
+
for(const o of all){
|
|
1875
|
+
const fraction=repoFractionAt(o,cutoffMs);
|
|
1876
|
+
o.timelineFraction=fraction;
|
|
1877
|
+
if(o.mesh){
|
|
1878
|
+
o.mesh.visible=fraction>0.002;
|
|
1879
|
+
o.mesh.scale.y=Math.max(0.001,fraction);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
for(const entry of repoLabels){
|
|
1883
|
+
const fraction=Number.isFinite(entry.o.timelineFraction)?entry.o.timelineFraction:1;
|
|
1884
|
+
entry.sprite.visible=fraction>0.08;
|
|
1885
|
+
entry.sprite.material.opacity=Math.max(0,Math.min(1,(fraction-0.08)/0.18));
|
|
1886
|
+
entry.sprite.position.y=TRAY_TOP+entry.o.h*fraction+entry.labelOffset;
|
|
1887
|
+
}
|
|
1888
|
+
redrawCityMetric();
|
|
1889
|
+
key.shadow.needsUpdate=true;
|
|
1890
|
+
dirty=true;
|
|
1891
|
+
}
|
|
1892
|
+
(function(){
|
|
1893
|
+
const timeline=document.getElementById('city-timeline');
|
|
1894
|
+
const track=timeline?.querySelector('.tl-track');
|
|
1895
|
+
const handle=timeline?.querySelector('.tl-handle');
|
|
1896
|
+
if(!timeline||!track||!handle) return;
|
|
1897
|
+
let dragging=false;
|
|
1898
|
+
const updateFromPointer=(event)=>{
|
|
1899
|
+
const rect=track.getBoundingClientRect();
|
|
1900
|
+
setTimelineProgress((event.clientX-rect.left)/Math.max(1,rect.width));
|
|
1901
|
+
};
|
|
1902
|
+
track.addEventListener('pointerdown',(event)=>{
|
|
1903
|
+
dragging=true; timeline.classList.add('dragging'); updateFromPointer(event); event.preventDefault();
|
|
1904
|
+
});
|
|
1905
|
+
window.addEventListener('pointermove',(event)=>{ if(dragging){ updateFromPointer(event); event.preventDefault(); } });
|
|
1906
|
+
window.addEventListener('pointerup',()=>{ dragging=false; timeline.classList.remove('dragging'); });
|
|
1907
|
+
handle.addEventListener('keydown',(event)=>{
|
|
1908
|
+
if(event.key==='ArrowLeft'||event.key==='ArrowDown') setTimelineProgress(timelineProgress-0.025);
|
|
1909
|
+
else if(event.key==='ArrowRight'||event.key==='ArrowUp') setTimelineProgress(timelineProgress+0.025);
|
|
1910
|
+
else if(event.key==='Home') setTimelineProgress(0);
|
|
1911
|
+
else if(event.key==='End') setTimelineProgress(1);
|
|
1912
|
+
else return;
|
|
1913
|
+
event.preventDefault();
|
|
1914
|
+
});
|
|
1915
|
+
setTimelineProgress(1);
|
|
1916
|
+
})();
|
|
1917
|
+
|
|
1005
1918
|
const MINZ=0.4, MAXZ=2.8; // lower floor → can zoom out to ~2/3 of the previous limit
|
|
1006
1919
|
const controls=new OrbitControls(camera, renderer.domElement);
|
|
1007
1920
|
controls.target.copy(TARGET);
|
|
@@ -1009,16 +1922,63 @@ controls.enableDamping=true; controls.dampingFactor=0.08; controls.enablePan=fal
|
|
|
1009
1922
|
controls.minPolarAngle=0.18; controls.maxPolarAngle=Math.PI*0.47; controls.rotateSpeed=0.85;
|
|
1010
1923
|
controls.update();
|
|
1011
1924
|
let targetZoom=camera.zoom;
|
|
1925
|
+
// Hover auto-focus: wheel + drag behave differently over the city, so cue it —
|
|
1926
|
+
// the camera eases into a slight zoom when the pointer enters, back out on leave.
|
|
1927
|
+
let hoverFocus=1, hoverFocusTarget=1;
|
|
1928
|
+
renderer.domElement.addEventListener('pointerenter',()=>{ hoverFocusTarget=1.05; });
|
|
1929
|
+
renderer.domElement.addEventListener('pointerleave',()=>{ hoverFocusTarget=1; });
|
|
1012
1930
|
renderer.domElement.addEventListener('wheel', (e)=>{
|
|
1013
1931
|
e.preventDefault();
|
|
1014
1932
|
const dy=e.deltaY * (e.deltaMode===1 ? 16 : 1);
|
|
1015
1933
|
targetZoom=Math.min(MAXZ, Math.max(MINZ, targetZoom * Math.pow(0.999, dy)));
|
|
1016
1934
|
}, {passive:false});
|
|
1017
1935
|
|
|
1018
|
-
let dirty=true;
|
|
1019
1936
|
function render(){ renderer.render(scene,camera); }
|
|
1020
1937
|
function loop(){ requestAnimationFrame(loop); let need=controls.update();
|
|
1021
|
-
|
|
1938
|
+
let shadowNeedsUpdate=false;
|
|
1939
|
+
if(Math.abs(hoverFocusTarget-hoverFocus)>0.0004){ hoverFocus+=(hoverFocusTarget-hoverFocus)*0.1; if(Math.abs(hoverFocusTarget-hoverFocus)<0.0005) hoverFocus=hoverFocusTarget; }
|
|
1940
|
+
const zoomGoal=Math.min(MAXZ, Math.max(MINZ, targetZoom*hoverFocus));
|
|
1941
|
+
if(Math.abs(zoomGoal-camera.zoom) > 0.0006){ camera.zoom += (zoomGoal-camera.zoom)*0.16; if(Math.abs(zoomGoal-camera.zoom)<0.001) camera.zoom=zoomGoal; camera.updateProjectionMatrix(); need=true; }
|
|
1942
|
+
if(Math.abs(repairTarget-repairProgress)>0.0005){
|
|
1943
|
+
repairProgress+=(repairTarget-repairProgress)*0.085;
|
|
1944
|
+
if(Math.abs(repairTarget-repairProgress)<0.001) repairProgress=repairTarget;
|
|
1945
|
+
need=true; shadowNeedsUpdate=true;
|
|
1946
|
+
}
|
|
1947
|
+
const repairEase=repairProgress*repairProgress*(3-2*repairProgress);
|
|
1948
|
+
for(const entry of repairGroups){
|
|
1949
|
+
entry.group.rotation.x=entry.damageX*(1-repairEase);
|
|
1950
|
+
entry.group.rotation.z=entry.damageZ*(1-repairEase);
|
|
1951
|
+
}
|
|
1952
|
+
for(const entry of repairPieces){
|
|
1953
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1954
|
+
entry.mesh.rotation.y=entry.damageRotY*(1-repairEase);
|
|
1955
|
+
}
|
|
1956
|
+
for(const entry of buildingMaterials){
|
|
1957
|
+
entry.material.color.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1958
|
+
entry.material.attenuationColor.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1959
|
+
entry.material.roughness=0.9-(0.2*repairEase);
|
|
1960
|
+
entry.material.transmission=0.05+(0.11*repairEase);
|
|
1961
|
+
entry.material.clearcoat=0.015+(0.035*repairEase);
|
|
1962
|
+
entry.material.clearcoatRoughness=0.94-(0.14*repairEase);
|
|
1963
|
+
entry.material.envMapIntensity=0.24+(0.21*repairEase);
|
|
1964
|
+
}
|
|
1965
|
+
for(const entry of corruptionFades){
|
|
1966
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1967
|
+
const scale=1-repairEase*0.92;
|
|
1968
|
+
entry.mesh.scale.copy(entry.baseScale).multiplyScalar(scale);
|
|
1969
|
+
entry.material.opacity=1-repairEase;
|
|
1970
|
+
entry.mesh.visible=repairEase<0.985;
|
|
1971
|
+
}
|
|
1972
|
+
for(const material of corruptionSurfaces) material.opacity=1-repairEase;
|
|
1973
|
+
const now=performance.now()*0.001;
|
|
1974
|
+
for(const floater of corruptionFloaters){
|
|
1975
|
+
if(repairEase<0.985){
|
|
1976
|
+
floater.mesh.position.y+=Math.sin(now*floater.speed+floater.phase)*3.2*(1-repairEase);
|
|
1977
|
+
floater.mesh.rotation.y+=0.0025*floater.speed*(1-repairEase);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if(corruptionFloaters.length && repairEase<0.985) need=true;
|
|
1981
|
+
if(shadowNeedsUpdate && (++shadowFrame%3===0 || repairProgress===repairTarget)) key.shadow.needsUpdate=true;
|
|
1022
1982
|
if(need||dirty){ render(); dirty=false; }
|
|
1023
1983
|
}
|
|
1024
1984
|
loop();
|
|
@@ -1027,7 +1987,7 @@ loop();
|
|
|
1027
1987
|
const cardEl=document.querySelector('.share-card');
|
|
1028
1988
|
function fit(){
|
|
1029
1989
|
if(setupMode){
|
|
1030
|
-
VIZ_W=window.innerWidth; VIZ_H=window.innerHeight;
|
|
1990
|
+
VIZ_W=figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620)); VIZ_H=window.innerHeight;
|
|
1031
1991
|
cardEl.style.transform='none';
|
|
1032
1992
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1033
1993
|
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
@@ -1045,51 +2005,79 @@ function fit(){
|
|
|
1045
2005
|
fit();
|
|
1046
2006
|
window.addEventListener('resize', fit);
|
|
1047
2007
|
|
|
1048
|
-
// Loading lives inside the report:
|
|
1049
|
-
//
|
|
2008
|
+
// Loading lives inside the report: reveal only after a provenance-checked report has
|
|
2009
|
+
// populated at least one real repo. Empty, failed, and invalid setup states stay closed.
|
|
1050
2010
|
function finishSetupReveal(){
|
|
2011
|
+
document.querySelectorAll('[data-setup-pending-hidden]').forEach((el)=>{ el.removeAttribute('aria-hidden'); el.removeAttribute('data-setup-pending-hidden'); });
|
|
1051
2012
|
document.body.classList.remove('scanning');
|
|
1052
2013
|
}
|
|
1053
|
-
if(setupMode){
|
|
2014
|
+
if(setupMode && setupReport && setupLiveRepos.length && all.length){
|
|
1054
2015
|
requestAnimationFrame(()=>requestAnimationFrame(finishSetupReveal));
|
|
1055
2016
|
window.setTimeout(finishSetupReveal, 1200);
|
|
1056
2017
|
}
|
|
1057
2018
|
|
|
1058
|
-
|
|
2019
|
+
(function(){
|
|
2020
|
+
const btn=document.getElementById('fr-read-btn');
|
|
2021
|
+
const panel=document.getElementById('fr-persona-hover');
|
|
2022
|
+
if(!btn || !panel) return;
|
|
2023
|
+
const setOpen=(open)=>{
|
|
2024
|
+
panel.classList.toggle('open', open);
|
|
2025
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
2026
|
+
};
|
|
2027
|
+
btn.addEventListener('click', (event)=>{
|
|
2028
|
+
event.stopPropagation();
|
|
2029
|
+
setOpen(!panel.classList.contains('open'));
|
|
2030
|
+
});
|
|
2031
|
+
document.addEventListener('click', (event)=>{
|
|
2032
|
+
if(event.target!==btn && !panel.contains(event.target)) setOpen(false);
|
|
2033
|
+
});
|
|
2034
|
+
window.addEventListener('keydown', (event)=>{ if(event.key==='Escape') setOpen(false); });
|
|
2035
|
+
})();
|
|
2036
|
+
|
|
2037
|
+
document.querySelectorAll('#setup-connect,#v7-connect,#fr-connect').forEach((connectButton)=>{
|
|
1059
2038
|
connectButton.addEventListener('click', (event)=>{
|
|
1060
2039
|
event.stopPropagation();
|
|
1061
2040
|
if(setupMode) window.parent?.postMessage({type:'echomem:connect-click'}, window.location.origin);
|
|
1062
2041
|
});
|
|
1063
2042
|
});
|
|
1064
2043
|
|
|
1065
|
-
// ---- rotating
|
|
2044
|
+
// ---- rotating context-health readout: HUD score, share, time, and API-equivalent waste ----
|
|
1066
2045
|
const parseScalar=(value)=>{
|
|
1067
2046
|
const match=String(value||'').match(/[0-9][0-9,]*(?:\.[0-9]+)?/);
|
|
1068
2047
|
return match ? Number(match[0].replace(/,/g,''))||0 : 0;
|
|
1069
2048
|
};
|
|
1070
2049
|
const formatWasteMoney=(value)=>`$${Math.round(value).toLocaleString('en-US')}`;
|
|
1071
|
-
function buildMetricSlides(dirtyPct, attentionHours, computeCost){
|
|
1072
|
-
const
|
|
2050
|
+
function buildMetricSlides(dirtyPct, attentionHours, computeCost, report){
|
|
2051
|
+
const wastePct=goldenWastePct(report);
|
|
2052
|
+
const usefulPct=goldenUsefulPct(report);
|
|
2053
|
+
const dirtyShare=Math.max(0, Number.isFinite(wastePct) ? wastePct : parseScalar(dirtyPct))/100;
|
|
1073
2054
|
const wastedHours=Math.max(0, parseScalar(attentionHours))*dirtyShare;
|
|
1074
2055
|
const wastedDays=wastedHours/8;
|
|
1075
2056
|
const wastedCost=Math.max(0, parseScalar(computeCost))*dirtyShare;
|
|
1076
|
-
|
|
2057
|
+
const slides=[];
|
|
2058
|
+
slides.push(
|
|
2059
|
+
{
|
|
2060
|
+
value:`${oneDec(wastePct)}%`,
|
|
2061
|
+
label:'wasteful context',
|
|
2062
|
+
say:goldenWasteSay(report)
|
|
2063
|
+
},
|
|
1077
2064
|
{
|
|
1078
|
-
value:`${
|
|
1079
|
-
label:'
|
|
1080
|
-
say:
|
|
2065
|
+
value:`${oneDec(usefulPct)}%`,
|
|
2066
|
+
label:'useful context',
|
|
2067
|
+
say:'Useful context is the portion of the analyzed window that still helps the agent: active task state, product facts, current files, and required overhead.'
|
|
1081
2068
|
},
|
|
1082
2069
|
{
|
|
1083
2070
|
value:`≈${Math.round(wastedHours)}h`,
|
|
1084
|
-
label:'
|
|
1085
|
-
say:`That is roughly <b>${wastedDays.toFixed(1)} workdays</b> of agent attention
|
|
2071
|
+
label:'waste-weighted time',
|
|
2072
|
+
say:`That is roughly <b>${wastedDays.toFixed(1)} workdays</b> of agent attention weighted by the golden-standard wasteful-context share.`
|
|
1086
2073
|
},
|
|
1087
2074
|
{
|
|
1088
2075
|
value:`≈${formatWasteMoney(wastedCost)}`,
|
|
1089
2076
|
label:'API-equiv waste',
|
|
1090
|
-
say:`At list API-equivalent rates,
|
|
2077
|
+
say:`At list API-equivalent rates, the wasteful-context share represents about <b>${formatWasteMoney(wastedCost)}</b> of the compute volume shown above. Not your bill — the scale of the waste.`
|
|
1091
2078
|
},
|
|
1092
|
-
|
|
2079
|
+
);
|
|
2080
|
+
return slides;
|
|
1093
2081
|
}
|
|
1094
2082
|
function bindMetricCarousel({metric,value,label,say,slides}){
|
|
1095
2083
|
if(!metric || !value || !label || !say || !slides?.length) return;
|
|
@@ -1106,7 +2094,7 @@ function bindMetricCarousel({metric,value,label,say,slides}){
|
|
|
1106
2094
|
say.classList.remove('is-swapping');
|
|
1107
2095
|
}, 520);
|
|
1108
2096
|
}
|
|
1109
|
-
if(slides.length>1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
|
|
2097
|
+
if(!setupMode && slides.length>1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
|
|
1110
2098
|
window.setInterval(()=>{
|
|
1111
2099
|
index=(index+1)%slides.length;
|
|
1112
2100
|
show(index);
|
|
@@ -1119,11 +2107,40 @@ bindMetricCarousel({
|
|
|
1119
2107
|
label:document.getElementById('v7-metric-label'),
|
|
1120
2108
|
say:document.getElementById('v7-metric-say'),
|
|
1121
2109
|
slides:buildMetricSlides(
|
|
1122
|
-
|
|
2110
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '{{dirtyPct}}',
|
|
1123
2111
|
document.getElementById('v7-attention')?.textContent || '{{attentionHours}}',
|
|
1124
|
-
document.getElementById('v7-cost')?.textContent || '{{computeCost}}'
|
|
2112
|
+
document.getElementById('v7-cost')?.textContent || '{{computeCost}}',
|
|
2113
|
+
setupReport
|
|
1125
2114
|
),
|
|
1126
2115
|
});
|
|
2116
|
+
bindMetricCarousel({
|
|
2117
|
+
metric:document.getElementById('fr-metric'),
|
|
2118
|
+
value:document.getElementById('fr-metric-value'),
|
|
2119
|
+
label:document.getElementById('fr-metric-label'),
|
|
2120
|
+
say:document.getElementById('fr-metric-say'),
|
|
2121
|
+
slides:buildMetricSlides(
|
|
2122
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '{{dirtyPct}}',
|
|
2123
|
+
document.getElementById('v7-attention')?.textContent || '{{attentionHours}}',
|
|
2124
|
+
document.getElementById('v7-cost')?.textContent || '{{computeCost}}',
|
|
2125
|
+
setupReport
|
|
2126
|
+
),
|
|
2127
|
+
});
|
|
2128
|
+
function reserveFrSayHeight(){
|
|
2129
|
+
if(!setupMode) return;
|
|
2130
|
+
const say=document.getElementById('fr-metric-say'); if(!say) return;
|
|
2131
|
+
const slides=buildMetricSlides(
|
|
2132
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '{{dirtyPct}}',
|
|
2133
|
+
document.getElementById('v7-attention')?.textContent || '{{attentionHours}}',
|
|
2134
|
+
document.getElementById('v7-cost')?.textContent || '{{computeCost}}',
|
|
2135
|
+
setupReport
|
|
2136
|
+
);
|
|
2137
|
+
const prev=say.innerHTML; say.style.minHeight='0px'; let max=0;
|
|
2138
|
+
for(const slide of slides){ say.innerHTML=slide.say; max=Math.max(max, say.offsetHeight); }
|
|
2139
|
+
say.innerHTML=prev;
|
|
2140
|
+
if(max>0) say.style.minHeight=max+'px';
|
|
2141
|
+
}
|
|
2142
|
+
reserveFrSayHeight();
|
|
2143
|
+
window.addEventListener('resize', reserveFrSayHeight);
|
|
1127
2144
|
|
|
1128
2145
|
// ---- repeated-reads info sheet: v7 "how we read your city" disclosure, moved behind the info button ----
|
|
1129
2146
|
const v7InfoBtns=Array.from(document.querySelectorAll('[aria-controls="v7-info-sheet"]'));
|