@echomem/mcp 1.4.8 → 1.4.10
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 +23 -3
- 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/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +1067 -107
- 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-canonical-adapter.js +315 -0
- package/dist/context-analysis/claude-native-canonical.js +1216 -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 +43 -0
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -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 +77 -202
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +17 -102
- package/dist/hud/monitor.js +136 -28
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +622 -353
- package/dist/index.js +7 -3
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +37 -29
- package/dist/report.js +101 -40
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +15 -2538
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +456 -44
- package/package.json +8 -7
- package/templates/echomem-recall.md +2 -2
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -1272
- package/dist/city/generate-echo-city-only.mjs +0 -112
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
Runtime City source. The setup parent loads this file into an iframe and sends each local scan
|
|
4
|
+
report with postMessage. Do not generate user-specific HTML files: this renderer updates in place.
|
|
5
|
+
The retired static share-card template pipeline lives in artifacts/archive/.
|
|
6
|
+
-->
|
|
2
7
|
<html lang="en">
|
|
3
8
|
<head>
|
|
4
9
|
<meta charset="UTF-8" />
|
|
@@ -165,6 +170,11 @@
|
|
|
165
170
|
box-shadow:0 16px 36px rgba(20,24,40,.18);cursor:pointer;}
|
|
166
171
|
.setup-connect:hover{background:#223f9b;}
|
|
167
172
|
body.setup-mode{background:#fff;}
|
|
173
|
+
/* figure-mode: embedded as an inline article figure — city only, chrome hidden, paper shows through */
|
|
174
|
+
body.figure-mode{background:transparent;}
|
|
175
|
+
body.figure-mode .share-card{background:transparent;box-shadow:none;}
|
|
176
|
+
body.figure-mode .fr-panel,body.figure-mode .corner-ribbon,body.figure-mode .city-toggle,
|
|
177
|
+
body.figure-mode .city-split,body.figure-mode .city-timeline,body.figure-mode .v7-band{display:none!important;}
|
|
168
178
|
body.setup-mode .share-card{width:100vw;height:100vh;border-radius:0;box-shadow:none;overflow:hidden;}
|
|
169
179
|
body.setup-mode .compute-cost,
|
|
170
180
|
body.setup-mode .tool-split,
|
|
@@ -265,6 +275,139 @@
|
|
|
265
275
|
body.setup-mode .v7-foot{display:none;}
|
|
266
276
|
}
|
|
267
277
|
|
|
278
|
+
/* setup split report: city on the left, field report on the right */
|
|
279
|
+
/* Old full-screen setup overlay reserves 620px for the right-hand text panel.
|
|
280
|
+
figure-mode embeds (city as its own column) must NOT reserve anything: the
|
|
281
|
+
canvas fills the iframe, matching the full-width render buffer (VIZ_W). */
|
|
282
|
+
body.setup-mode:not(.figure-mode) #repo-visualizer{right:auto;width:calc(100vw - min(40vw,620px));}
|
|
283
|
+
body.with-echo .fr-metric .fr-v{color:#2a8f7d;}
|
|
284
|
+
body.with-echo .fr-pull{border-left-color:#2a8f7d;}
|
|
285
|
+
.city-toggle{display:none;transition:opacity .4s ease;}
|
|
286
|
+
body.setup-mode .city-toggle{display:inline-flex;position:absolute;z-index:9;
|
|
287
|
+
left:calc((100vw - min(40vw,620px)) / 2);top:clamp(92px,12.5vh,104px);transform:translateX(-50%);gap:2px;
|
|
288
|
+
background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:999px;padding:4px;
|
|
289
|
+
box-shadow:0 8px 22px rgba(20,24,40,.10);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);}
|
|
290
|
+
.ct-thumb{position:absolute;left:0;top:0;z-index:0;background:#1a3a8f;border-radius:999px;
|
|
291
|
+
transition:transform .32s cubic-bezier(.4,0,.2,1),width .32s cubic-bezier(.4,0,.2,1),height .32s ease;}
|
|
292
|
+
.city-toggle button{position:relative;z-index:1;appearance:none;border:0;background:transparent;cursor:pointer;
|
|
293
|
+
font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
|
|
294
|
+
color:#80838c;padding:7px 14px;border-radius:999px;transition:color .25s ease;}
|
|
295
|
+
.city-toggle button.on{color:#fff;}
|
|
296
|
+
.city-toggle button:not(.on):hover{color:#1b1c1f;}
|
|
297
|
+
.corner-ribbon{display:none;}
|
|
298
|
+
body.setup-mode .corner-ribbon{display:block;position:absolute;top:0;left:0;width:180px;height:180px;
|
|
299
|
+
overflow:hidden;z-index:12;pointer-events:none;}
|
|
300
|
+
.corner-ribbon>span{position:absolute;left:-70px;top:51px;width:280px;transform:rotate(-45deg);
|
|
301
|
+
background:#1a3a8f;color:#fff;text-align:center;padding:7px 0;line-height:1.2;
|
|
302
|
+
box-shadow:0 6px 16px rgba(20,24,40,.18);}
|
|
303
|
+
.corner-ribbon .l1{display:block;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;}
|
|
304
|
+
.corner-ribbon .l2{display:block;font-family:var(--mono);font-size:8.5px;font-weight:700;letter-spacing:.08em;
|
|
305
|
+
text-transform:uppercase;color:rgba(255,255,255,.86);margin-top:2px;}
|
|
306
|
+
.city-split{display:none;transition:opacity .4s ease;}
|
|
307
|
+
body.setup-mode .city-split{display:block;position:absolute;z-index:9;
|
|
308
|
+
left:clamp(28px,3vw,48px);bottom:clamp(72px,9vh,104px);width:min(260px,24vw);}
|
|
309
|
+
.city-split .cs-bar{display:flex;height:8px;border-radius:999px;overflow:hidden;background:#eceef2;
|
|
310
|
+
box-shadow:inset 0 0 0 1px rgba(20,24,40,.05);}
|
|
311
|
+
.city-split .cs-bar i{height:100%;display:block;transition:width .4s cubic-bezier(.4,0,.2,1);}
|
|
312
|
+
.city-split .cs-claude{background:#d97757;}
|
|
313
|
+
.city-split .cs-codex{background:#10a37f;}
|
|
314
|
+
.city-split .cs-legend{display:flex;justify-content:space-between;margin-top:9px;
|
|
315
|
+
font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#80838c;}
|
|
316
|
+
.city-split .cs-l{display:flex;align-items:center;gap:6px;}
|
|
317
|
+
.city-split .cs-l i{width:8px;height:8px;border-radius:50%;}
|
|
318
|
+
.city-split .cs-l.claude i{background:#d97757;}
|
|
319
|
+
.city-split .cs-l.codex i{background:#10a37f;}
|
|
320
|
+
.city-split .cs-l b{color:#1b1c1f;font-weight:800;margin-left:2px;}
|
|
321
|
+
.city-timeline{display:none;transition:opacity .4s ease;}
|
|
322
|
+
body.setup-mode .city-timeline{display:block;position:absolute;z-index:9;
|
|
323
|
+
left:clamp(28px,3vw,48px);bottom:clamp(20px,2.6vh,32px);
|
|
324
|
+
width:calc(100vw - min(40vw,620px) - 2 * clamp(28px,3vw,48px));touch-action:none;user-select:none;}
|
|
325
|
+
.city-timeline .tl-track{position:relative;height:18px;cursor:pointer;}
|
|
326
|
+
.city-timeline .tl-line{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
|
|
327
|
+
height:3px;border-radius:3px;background:#d8dbe1;}
|
|
328
|
+
.city-timeline .tl-progress{position:absolute;left:0;top:50%;transform:translateY(-50%);
|
|
329
|
+
width:100%;height:3px;border-radius:3px;background:#1a3a8f;pointer-events:none;}
|
|
330
|
+
.city-timeline .tl-dot{position:absolute;top:50%;transform:translate(-50%,-50%);width:9px;height:9px;border-radius:50%;}
|
|
331
|
+
.city-timeline .tl-dot.start{left:0;background:#9aa0aa;}
|
|
332
|
+
.city-timeline .tl-dot.end{left:100%;background:#1a3a8f;}
|
|
333
|
+
.city-timeline .tl-dot.end::after{content:'';position:absolute;inset:-4px;border-radius:50%;
|
|
334
|
+
border:2px solid rgba(26,58,143,.4);animation:tlPulse 2.2s ease-out infinite;}
|
|
335
|
+
@keyframes tlPulse{0%{transform:scale(.6);opacity:.9}100%{transform:scale(1.85);opacity:0}}
|
|
336
|
+
.city-timeline .tl-handle{position:absolute;left:100%;top:50%;transform:translate(-50%,-50%);
|
|
337
|
+
width:16px;height:16px;border-radius:50%;background:#1a3a8f;border:3px solid #fff;
|
|
338
|
+
box-shadow:0 3px 10px rgba(20,24,40,.28);z-index:3;cursor:grab;}
|
|
339
|
+
.city-timeline.dragging .tl-handle{cursor:grabbing;box-shadow:0 3px 14px rgba(26,58,143,.42);}
|
|
340
|
+
.city-timeline .tl-selected{position:absolute;left:100%;top:-17px;transform:translateX(-50%);
|
|
341
|
+
font-family:var(--mono);font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
|
|
342
|
+
color:#1a3a8f;white-space:nowrap;pointer-events:none;}
|
|
343
|
+
.city-timeline .tl-labels{display:flex;justify-content:space-between;align-items:baseline;margin-top:8px;
|
|
344
|
+
font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#80838c;}
|
|
345
|
+
.city-timeline .tl-labels .tl-span{color:#1b1c1f;font-weight:800;}
|
|
346
|
+
body.setup-mode .v7-persona,
|
|
347
|
+
body.setup-mode .v7-head,
|
|
348
|
+
body.setup-mode .v7-band,
|
|
349
|
+
body.setup-mode .v7-ctaWrap,
|
|
350
|
+
body.setup-mode .v7-foot{display:none;}
|
|
351
|
+
|
|
352
|
+
.fr-panel{display:none;transition:opacity .6s ease;}
|
|
353
|
+
body.setup-mode .fr-panel{display:flex;flex-direction:column;align-items:center;position:absolute;z-index:8;
|
|
354
|
+
right:0;top:0;width:min(40vw,620px);height:100vh;border-left:1px solid var(--line);background:#fff;
|
|
355
|
+
padding:clamp(44px,6vh,90px) clamp(36px,3.2vw,60px);}
|
|
356
|
+
.fr{width:min(54ch,100%);flex:1;display:flex;flex-direction:column;justify-content:space-between;
|
|
357
|
+
gap:clamp(22px,3.4vh,46px);}
|
|
358
|
+
.fr-dateline{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:11px;
|
|
359
|
+
font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
360
|
+
.fr-dateline .fr-rule{flex:1;height:1px;background:var(--line);}
|
|
361
|
+
.fr-intro{display:flex;flex-direction:column;position:relative;
|
|
362
|
+
padding-bottom:clamp(20px,3vh,38px);border-bottom:1px solid var(--line);}
|
|
363
|
+
.fr-avatar{width:clamp(92px,9.5vw,128px);height:auto;display:block;margin-bottom:10px;}
|
|
364
|
+
.fr-greet{font-size:clamp(34px,3.2vw,52px);font-weight:800;letter-spacing:-.035em;line-height:.95;
|
|
365
|
+
text-transform:lowercase;}
|
|
366
|
+
.fr-greet .fr-name{color:#1a3a8f;}
|
|
367
|
+
.fr-greet .fr-dot{color:var(--amber);}
|
|
368
|
+
.fr-rank{margin-top:13px;font-family:var(--mono);font-size:clamp(12px,1vw,14px);font-weight:600;
|
|
369
|
+
letter-spacing:.04em;color:#9296a0;}
|
|
370
|
+
.fr-rank b{font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#3a3c43;}
|
|
371
|
+
.fr-readBtn{position:absolute;z-index:9;top:clamp(2px,0.5vw,8px);right:0;
|
|
372
|
+
display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
|
|
373
|
+
cursor:pointer;font-family:var(--mono);font-size:14px;font-weight:800;line-height:1;
|
|
374
|
+
color:#5a5e66;background:#fafafb;border:1px solid var(--line);border-radius:50%;
|
|
375
|
+
transition:background .15s ease,border-color .15s ease,color .15s ease;}
|
|
376
|
+
.fr-readBtn:hover,.fr-readBtn:focus-visible{background:#fff;border-color:#d7dade;color:var(--ink);outline:none;}
|
|
377
|
+
.fr-readBtn[aria-expanded="true"]{background:#1a3a8f;border-color:#1a3a8f;color:#fff;}
|
|
378
|
+
.fr-personaHover{position:absolute;z-index:32;top:clamp(42px,4.6vw,56px);right:0;
|
|
379
|
+
width:min(340px,100%);max-height:min(72vh,540px);overflow:auto;
|
|
380
|
+
background:rgba(255,255,255,.98);border:1px solid var(--line);border-radius:16px;padding:15px 17px;
|
|
381
|
+
box-shadow:0 18px 46px rgba(20,24,40,.18);opacity:0;visibility:hidden;transform:translateY(-4px);
|
|
382
|
+
transition:opacity .14s ease,transform .14s ease;pointer-events:none;}
|
|
383
|
+
.fr-personaHover.open{opacity:1;visibility:visible;transform:none;pointer-events:auto;}
|
|
384
|
+
.fr-personaHover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
385
|
+
.fr-personaHover .ph-line:last-of-type{margin-bottom:0;}
|
|
386
|
+
.fr-personaHover .ph-line b{color:var(--ink);font-weight:800;}
|
|
387
|
+
.fr-personaHover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
388
|
+
padding-top:9px;border-top:1px solid var(--line);}
|
|
389
|
+
.fr-personaHover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
390
|
+
.fr-mid{display:flex;flex-direction:column;gap:clamp(20px,3vh,40px);}
|
|
391
|
+
.fr-lede{font-size:clamp(16px,1.1vw,18px);line-height:1.68;color:#3c3e45;}
|
|
392
|
+
.fr-lede mark{background:linear-gradient(transparent 58%, rgba(240,168,63,.42) 58%);
|
|
393
|
+
color:var(--ink);font-weight:800;padding:0 1px;}
|
|
394
|
+
.fr-pull{padding-left:20px;border-left:3px solid var(--red);}
|
|
395
|
+
.fr-metric{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
396
|
+
transition:opacity .5s ease,transform .5s ease;}
|
|
397
|
+
.fr-metric .fr-v{font-size:clamp(32px,2.8vw,44px);font-weight:800;color:var(--red);letter-spacing:-.03em;line-height:.9;}
|
|
398
|
+
.fr-metric .fr-l{font-size:clamp(16px,1.2vw,19px);font-weight:700;color:var(--ink);letter-spacing:-.01em;}
|
|
399
|
+
.fr-say{font-size:clamp(14px,.9vw,15px);color:#56585f;margin-top:9px;line-height:1.5;max-width:46ch;
|
|
400
|
+
min-height:4.6em;transition:opacity .5s ease,transform .5s ease;}
|
|
401
|
+
.fr-say b{color:var(--ink);font-weight:700;}
|
|
402
|
+
.fr-metric.is-swapping,.fr-say.is-swapping{opacity:0;transform:translateY(6px);}
|
|
403
|
+
.fr-signoff{display:flex;align-items:center;gap:18px;flex-wrap:wrap;}
|
|
404
|
+
.fr-cta{display:inline-flex;align-items:center;gap:10px;background:#1a3a8f;color:#fff;border:0;
|
|
405
|
+
border-radius:13px;font-family:var(--mono);font-size:clamp(13px,1vw,15px);font-weight:800;
|
|
406
|
+
letter-spacing:.08em;text-transform:uppercase;padding:16px 34px;
|
|
407
|
+
box-shadow:0 18px 40px rgba(26,58,143,.30);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
408
|
+
.fr-cta:hover{background:#223f9b;transform:translateY(-2px);}
|
|
409
|
+
.fr-micro{font-size:12.5px;color:#8b8d94;line-height:1.45;max-width:24ch;}
|
|
410
|
+
|
|
268
411
|
/* ===== in-canvas scan loader (setup mode) — loading lives inside the report, matches its type ===== */
|
|
269
412
|
.scan-loading{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
270
413
|
text-align:center;gap:14px;padding:24px;opacity:1;transition:opacity .5s ease;pointer-events:none;}
|
|
@@ -278,10 +421,14 @@
|
|
|
278
421
|
/* during scan: hide the city + the value-bearing chrome (so demo numbers never flash), then fade in together */
|
|
279
422
|
#repo-visualizer{transition:opacity .7s ease;}
|
|
280
423
|
body.scanning #repo-visualizer{opacity:0;}
|
|
281
|
-
.setup-header,.dirty-context,.counterfactual,.setup-connect,.v7-z{transition:opacity .6s ease;}
|
|
282
|
-
body.scanning .setup-header,body.scanning .dirty-context,body.scanning .counterfactual,body.scanning .setup-connect,body.scanning .v7-z{opacity:0;pointer-events:none;}
|
|
424
|
+
.setup-header,.dirty-context,.counterfactual,.setup-connect,.v7-z,.city-toggle,.city-split,.city-timeline,.fr-panel{transition:opacity .6s ease;}
|
|
425
|
+
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;}
|
|
283
426
|
body.setup-mode:not(.scanning) #repo-visualizer,
|
|
284
|
-
body.setup-mode:not(.scanning) .v7-z
|
|
427
|
+
body.setup-mode:not(.scanning) .v7-z,
|
|
428
|
+
body.setup-mode:not(.scanning) .city-toggle,
|
|
429
|
+
body.setup-mode:not(.scanning) .city-split,
|
|
430
|
+
body.setup-mode:not(.scanning) .city-timeline,
|
|
431
|
+
body.setup-mode:not(.scanning) .fr-panel{opacity:1;}
|
|
285
432
|
|
|
286
433
|
/* hover-to-preview repo detail (interaction overlay, not part of the static export) */
|
|
287
434
|
#repo-detail{position:fixed;display:none;z-index:50;width:286px;background:rgba(255,255,255,.96);
|
|
@@ -308,10 +455,38 @@
|
|
|
308
455
|
</style>
|
|
309
456
|
</head>
|
|
310
457
|
<body>
|
|
311
|
-
<script>try{const p=new URLSearchParams(location.search);if(p.get('setup')==='1'){document.body.classList.add('setup-mode');if(p.get('
|
|
458
|
+
<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>
|
|
312
459
|
<div class="share-card">
|
|
313
460
|
<canvas id="repo-visualizer"></canvas>
|
|
314
461
|
|
|
462
|
+
<div class="city-toggle" id="city-toggle" role="group" aria-label="City metric">
|
|
463
|
+
<span class="ct-thumb" aria-hidden="true"></span>
|
|
464
|
+
<button type="button" data-metric="time" class="on">Time</button>
|
|
465
|
+
<button type="button" data-metric="tokens">Tokens</button>
|
|
466
|
+
<button type="button" data-metric="cost">API $</button>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div class="corner-ribbon" aria-hidden="true"><span><span class="l1">Local Scan</span><span class="l2">100% private & safe</span></span></div>
|
|
470
|
+
|
|
471
|
+
<div class="city-split" id="city-split">
|
|
472
|
+
<div class="cs-bar"><i class="cs-claude" style="width:74%"></i><i class="cs-codex" style="width:26%"></i></div>
|
|
473
|
+
<div class="cs-legend">
|
|
474
|
+
<span class="cs-l claude"><i></i>Claude <b>74%</b></span>
|
|
475
|
+
<span class="cs-l codex"><i></i>Codex <b>26%</b></span>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="city-timeline" id="city-timeline">
|
|
480
|
+
<div class="tl-track">
|
|
481
|
+
<span class="tl-line"></span>
|
|
482
|
+
<span class="tl-progress"></span>
|
|
483
|
+
<span class="tl-dot start"></span><span class="tl-dot end"></span>
|
|
484
|
+
<span class="tl-selected">Today</span>
|
|
485
|
+
<span class="tl-handle" role="slider" tabindex="0" aria-label="Coding history date" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"></span>
|
|
486
|
+
</div>
|
|
487
|
+
<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>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
315
490
|
<div class="v7-z v7-persona" tabindex="0" aria-describedby="v7-persona-hover">
|
|
316
491
|
<div class="v7-eyebrow">AI Coding City</div>
|
|
317
492
|
<div class="v7-personaName">bossy</div>
|
|
@@ -333,25 +508,48 @@
|
|
|
333
508
|
<div class="eyebrow">What we found</div>
|
|
334
509
|
<img class="v7-echo" src="echo-face-cutout.png" alt="" />
|
|
335
510
|
<div class="v7-bandCopy">
|
|
336
|
-
<div class="bignum" id="v7-metric"><span id="v7-metric-value">85.0%</span><span id="v7-metric-label">
|
|
337
|
-
<div class="say" id="v7-metric-say">
|
|
511
|
+
<div class="bignum" id="v7-metric"><span id="v7-metric-value">85.0%</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>
|
|
512
|
+
<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>
|
|
338
513
|
</div>
|
|
339
514
|
</div>
|
|
340
515
|
<div class="v7-z v7-ctaWrap">
|
|
341
516
|
<div class="pre">Ready when you are</div>
|
|
342
517
|
<button class="v7-cta" id="v7-connect" type="button">Connect Echo →</button>
|
|
343
|
-
<div class="micro">Clears the dirty context, so agents stop relearning your workspace.</div>
|
|
344
518
|
</div>
|
|
345
519
|
<div class="v7-z v7-foot"><span>Generated by Echo · No transcripts shown</span><span>Local scan · Share-safe city mode</span></div>
|
|
520
|
+
|
|
521
|
+
<div class="fr-panel">
|
|
522
|
+
<div class="fr">
|
|
523
|
+
<div class="fr-dateline"><span>Echo field report</span><span class="fr-rule"></span></div>
|
|
524
|
+
<div class="fr-intro">
|
|
525
|
+
<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>
|
|
526
|
+
<div class="fr-personaHover" id="fr-persona-hover" role="region" aria-label="Your full coding-personality read"></div>
|
|
527
|
+
<img class="fr-avatar" id="fr-avatar" src="./personality_stickers/bossy.png" alt="" />
|
|
528
|
+
<h1 class="fr-greet">hey, <span class="fr-name" id="fr-name">bossy</span><span class="fr-dot">.</span></h1>
|
|
529
|
+
<div class="fr-rank" id="fr-rank">You are <b>the architect · marathoner</b></div>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="fr-mid">
|
|
532
|
+
<p class="fr-lede">Across <mark id="fr-repos">13 repos</mark> you poured in <mark id="fr-hours">191.1 hours</mark> and <mark id="fr-tokens">7.49B tokens</mark> — about <mark id="fr-cost">$13,689</mark> of API-equivalent compute. That whole skyline on the left? You built it.</p>
|
|
533
|
+
<div class="fr-pull">
|
|
534
|
+
<div class="fr-metric" id="fr-metric"><span class="fr-v" id="fr-metric-value">85.0%</span><span class="fr-l" id="fr-metric-label">wasteful context</span></div>
|
|
535
|
+
<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>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="fr-signoff">
|
|
539
|
+
<button class="fr-cta" id="fr-connect" type="button">Connect Echo →</button>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
346
544
|
<div class="v7-infoSheet" id="v7-info-sheet" aria-hidden="true">
|
|
347
545
|
<div class="v7-infoCard" role="dialog" aria-modal="true" aria-labelledby="v7-info-title">
|
|
348
546
|
<button class="v7-infoClose" id="v7-info-close" type="button" aria-label="Close">×</button>
|
|
349
547
|
<div class="v7-infoTitle" id="v7-info-title">How we read your city</div>
|
|
350
548
|
<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>
|
|
351
|
-
<div class="v7-infoRow"><div class="sk">
|
|
352
|
-
<div class="v7-infoRow"><div class="sk">
|
|
549
|
+
<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>
|
|
550
|
+
<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>
|
|
353
551
|
<div class="v7-infoRow"><div class="sk">$13,689</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>
|
|
354
|
-
<div class="v7-infoRow"><div class="sk">
|
|
552
|
+
<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>
|
|
355
553
|
<div class="v7-infoNote">Computed locally · no transcripts leave your machine.</div>
|
|
356
554
|
</div>
|
|
357
555
|
</div>
|
|
@@ -405,8 +603,8 @@
|
|
|
405
603
|
|
|
406
604
|
<!-- dirty context -->
|
|
407
605
|
<div class="card-zone dirty-context">
|
|
408
|
-
<div class="pct" id="setup-metric"><span id="setup-metric-value">85.0%</span><span class="word" id="setup-metric-label">
|
|
409
|
-
<div class="desc" id="setup-metric-say">
|
|
606
|
+
<div class="pct" id="setup-metric"><span id="setup-metric-value">85.0%</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>
|
|
607
|
+
<div class="desc" id="setup-metric-say">The golden-standard model separates useful context from wasteful context across your workspace.</div>
|
|
410
608
|
</div>
|
|
411
609
|
|
|
412
610
|
<!-- counterfactual: one line — 18 days saved -->
|
|
@@ -435,6 +633,30 @@
|
|
|
435
633
|
|
|
436
634
|
<div id="repo-detail"></div>
|
|
437
635
|
|
|
636
|
+
<script>
|
|
637
|
+
// The poster shares this artifact, but setup must never expose poster labels while it is waiting or
|
|
638
|
+
// failed. Scrub personalized defaults after the DOM exists; the validated report repopulates them.
|
|
639
|
+
try{
|
|
640
|
+
if(new URLSearchParams(location.search).get('setup')==='1'){
|
|
641
|
+
const pendingText={
|
|
642
|
+
'v7-repo-count':'—','v7-attention':'—','v7-cost':'—','v7-total-tokens':'—',
|
|
643
|
+
'v7-metric-value':'—','fr-name':'—','fr-rank':'Waiting for local scan',
|
|
644
|
+
'fr-repos':'— repos','fr-hours':'— hours','fr-tokens':'— tokens','fr-cost':'—',
|
|
645
|
+
'fr-metric-value':'—','setup-metric-value':'—'
|
|
646
|
+
};
|
|
647
|
+
for(const [id,value] of Object.entries(pendingText)){ const el=document.getElementById(id); if(el) el.textContent=value; }
|
|
648
|
+
document.querySelectorAll('.v7-personaName,.setup-persona-name,.persona-card .name').forEach((el)=>{ el.textContent='—'; });
|
|
649
|
+
document.querySelectorAll('.v7-rank,.setup-rank,.persona-card .rankrhythm').forEach((el)=>{ el.textContent='Waiting for local scan'; });
|
|
650
|
+
document.querySelectorAll('.v7-combo,.setup-combo,.persona-card .combo,.setup-split,#tool-split').forEach((el)=>{ el.replaceChildren(); });
|
|
651
|
+
document.querySelectorAll('.setup-stats .amount,.compute-cost .amount').forEach((el)=>{ el.textContent='—'; });
|
|
652
|
+
document.querySelectorAll('.setup-stats .detail,.compute-cost .detail').forEach((el)=>{ el.textContent='Waiting for validated local data.'; });
|
|
653
|
+
document.querySelectorAll('#v7-metric-say,#fr-metric-say,#setup-metric-say').forEach((el)=>{ el.textContent='Waiting for a validated local report.'; });
|
|
654
|
+
const frAvatar=document.getElementById('fr-avatar'); if(frAvatar) frAvatar.removeAttribute('src');
|
|
655
|
+
document.querySelectorAll('.share-card > :not(#scan-loading)').forEach((el)=>{ el.setAttribute('aria-hidden','true'); el.setAttribute('data-setup-pending-hidden',''); });
|
|
656
|
+
}
|
|
657
|
+
}catch(_){}
|
|
658
|
+
</script>
|
|
659
|
+
|
|
438
660
|
<script type="importmap">
|
|
439
661
|
{ "imports": { "three": "./vendor/three.module.min.js" } }
|
|
440
662
|
</script>
|
|
@@ -445,11 +667,41 @@ import { OrbitControls } from './vendor/OrbitControls.js';
|
|
|
445
667
|
|
|
446
668
|
const pageParams=new URLSearchParams(location.search);
|
|
447
669
|
const setupMode=pageParams.get('setup')==='1';
|
|
670
|
+
const previewMode=pageParams.get('preview')==='1';
|
|
671
|
+
const figureModeEmbed=pageParams.get('figure')==='1';
|
|
448
672
|
if(setupMode) document.body.classList.add('setup-mode');
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
673
|
+
// figure-mode embeds the city as its own column beside the text: the canvas owns the whole
|
|
674
|
+
// iframe. The 620px text reservation only applies to the old full-screen setup overlay.
|
|
675
|
+
let VIZ_W=setupMode ? (figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620))) : 920;
|
|
676
|
+
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode, left city in setup mode
|
|
677
|
+
const TRAY_COL = 0xd9d5ca; // worn warm concrete; keeps the corrupted city distinct from the white report panel
|
|
452
678
|
const TOOL = { codex:0x10a37f, claude:0xd97757 }; // ONLY two cube colours — exact OpenAI / Claude chip-icon hexes
|
|
679
|
+
const CORRUPTION = 0.82;
|
|
680
|
+
const corruptionFloaters=[];
|
|
681
|
+
const repairGroups=[];
|
|
682
|
+
const repairPieces=[];
|
|
683
|
+
const corruptionFades=[];
|
|
684
|
+
const corruptionSurfaces=[];
|
|
685
|
+
const buildingMaterials=[];
|
|
686
|
+
|
|
687
|
+
function hash01(value, salt=0){
|
|
688
|
+
let h=2166136261 ^ salt;
|
|
689
|
+
for(let i=0;i<String(value).length;i++){
|
|
690
|
+
h^=String(value).charCodeAt(i);
|
|
691
|
+
h=Math.imul(h,16777619);
|
|
692
|
+
}
|
|
693
|
+
return (h>>>0)/4294967295;
|
|
694
|
+
}
|
|
695
|
+
function corruptionProfile(o){
|
|
696
|
+
const pressure=Math.min(1, Math.max(0.28, Math.log10(Math.max(10, o.tokens||10))/10));
|
|
697
|
+
return {
|
|
698
|
+
amount:CORRUPTION*(0.58+pressure*0.42),
|
|
699
|
+
leanX:(hash01(o.name,11)-0.5)*0.25,
|
|
700
|
+
leanZ:(hash01(o.name,19)-0.5)*0.25,
|
|
701
|
+
phase:hash01(o.name,29)*Math.PI*2,
|
|
702
|
+
side:hash01(o.name,37)<0.5?-1:1,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
453
705
|
|
|
454
706
|
// ── volume normalization ─────────────────────────────────────────────────────
|
|
455
707
|
// Each cube's VOLUME is proportional to that repo's token usage, measured in a fixed unit so
|
|
@@ -464,6 +716,8 @@ const LABEL_MIN_BASE = 24; // cubes with footprint ≥ this get a top-face
|
|
|
464
716
|
const fmtTok=(n)=> n>=1e9?`${(n/1e9).toFixed(2)}B` : n>=1e6?`${Math.round(n/1e6)}M` : `${Math.round(n/1e3)}K`;
|
|
465
717
|
const money=(n)=>'$'+Math.round(Number(n)||0).toLocaleString('en-US');
|
|
466
718
|
const oneDec=(n)=>Number(n||0).toFixed(1).replace(/\.0$/,'');
|
|
719
|
+
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.";
|
|
720
|
+
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.";
|
|
467
721
|
const monthDay=(iso)=>{
|
|
468
722
|
const d=new Date(iso);
|
|
469
723
|
if(Number.isNaN(d.getTime())) return {month:'JUN', day:'2', label:'Jun 2'};
|
|
@@ -481,9 +735,71 @@ function providerSplit(report){
|
|
|
481
735
|
const codexPct=Math.round(codex/total*100);
|
|
482
736
|
return {codexPct, claudePct:100-codexPct, dominant:codex>=claude?'codex':'claude'};
|
|
483
737
|
}
|
|
738
|
+
function clampScore(value){
|
|
739
|
+
return Math.max(0, Math.min(100, Math.round(Number(value)||0)));
|
|
740
|
+
}
|
|
741
|
+
function reportContextMetrics(report){
|
|
742
|
+
const metrics=report?.contextWindow?.metrics;
|
|
743
|
+
return metrics && typeof metrics==='object' ? metrics : null;
|
|
744
|
+
}
|
|
745
|
+
function contextHealthScore(report){
|
|
746
|
+
const metrics=reportContextMetrics(report);
|
|
747
|
+
const direct=Number(metrics?.healthScorePct);
|
|
748
|
+
if(Number.isFinite(direct)) return clampScore(direct);
|
|
749
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
750
|
+
return report && Number.isFinite(stale) ? clampScore(100-stale) : null;
|
|
751
|
+
}
|
|
752
|
+
function pctText(value){
|
|
753
|
+
const n=Number(value);
|
|
754
|
+
return Number.isFinite(n) ? `${Math.round(Math.max(0, Math.min(1, n))*100)}%` : null;
|
|
755
|
+
}
|
|
756
|
+
function contextPressureText(metrics){
|
|
757
|
+
const projected=pctText(metrics?.projectedNextFullnessPct);
|
|
758
|
+
if(projected) return `${projected} projected window pressure`;
|
|
759
|
+
const fullness=pctText(metrics?.contextFullnessPct);
|
|
760
|
+
if(fullness) return `${fullness} window full`;
|
|
761
|
+
const latest=Number(metrics?.latestInputTokens);
|
|
762
|
+
return Number.isFinite(latest) && latest>0 ? `${fmtTok(latest)} latest input` : 'current window pressure';
|
|
763
|
+
}
|
|
764
|
+
function contextNoiseText(report){
|
|
765
|
+
const metrics=reportContextMetrics(report);
|
|
766
|
+
const noise=pctText(metrics?.noisePct);
|
|
767
|
+
if(noise) return `${noise} resident noise`;
|
|
768
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
769
|
+
return Number.isFinite(stale) ? `${oneDec(stale)}% repeated-read signal` : 'resident noise';
|
|
770
|
+
}
|
|
771
|
+
function contextScoreSay(report){
|
|
772
|
+
const score=contextHealthScore(report);
|
|
773
|
+
const metrics=reportContextMetrics(report);
|
|
774
|
+
return `This is the same <b>${score}% context score</b> the HUD uses: ${contextNoiseText(report)} and ${contextPressureText(metrics)}, folded into one live setup readout.`;
|
|
775
|
+
}
|
|
776
|
+
function goldenReport(report){
|
|
777
|
+
const gs=report?.canonicalGoldenStandard || report?.goldenStandard;
|
|
778
|
+
return gs && typeof gs==='object' && !gs.error && gs.summary ? gs : null;
|
|
779
|
+
}
|
|
780
|
+
function goldenSummary(report){
|
|
781
|
+
return goldenReport(report)?.summary || null;
|
|
782
|
+
}
|
|
783
|
+
function goldenWastePct(report){
|
|
784
|
+
const pct=Number(goldenSummary(report)?.wastePct);
|
|
785
|
+
return Number.isFinite(pct) ? pct : Number(report?.cleanliness?.staleReadSharePct)||0;
|
|
786
|
+
}
|
|
787
|
+
function goldenUsefulPct(report){
|
|
788
|
+
const pct=Number(goldenSummary(report)?.usefulPct);
|
|
789
|
+
return Number.isFinite(pct) ? pct : Math.max(0, 100-goldenWastePct(report));
|
|
790
|
+
}
|
|
791
|
+
function goldenWasteSay(report){
|
|
792
|
+
const s=goldenSummary(report);
|
|
793
|
+
if(!s) return REPEATED_READS_SAY;
|
|
794
|
+
const p=report?.billedWasteProjection;
|
|
795
|
+
if(p && p.method==='canonical-window-share-over-provider-input'){
|
|
796
|
+
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.`;
|
|
797
|
+
}
|
|
798
|
+
return `Across <b>${Number(s.sessionsAnalyzed||0).toLocaleString('en-US')} sessions</b>, the canonical windows found <b>${oneDec(s.wastePct)}% wasteful context</b>.`;
|
|
799
|
+
}
|
|
484
800
|
// ---- coding-persona engine (Phase 1: only signals the scan actually has — no commits / accept-all yet) ----
|
|
485
801
|
const PERSONA_TRAITS={
|
|
486
|
-
loopy:{label:'loopy', blurb:"opened the same file 14 times and still
|
|
802
|
+
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."},
|
|
487
803
|
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. 💸"},
|
|
488
804
|
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."},
|
|
489
805
|
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features, breadth over depth. your brain is a browser with a memory leak. 🫠"},
|
|
@@ -515,7 +831,7 @@ function derivePersona(report){
|
|
|
515
831
|
const turns=Math.max(1, Number(uwh.realUserTurns)||0);
|
|
516
832
|
const autonomy=assistant/turns; // AI turns per human prompt → how hands-off you run
|
|
517
833
|
const repoCount=Number(scale.repoCount)||repos.length;
|
|
518
|
-
const stale=
|
|
834
|
+
const stale=goldenWastePct(report), spend=Number(cost.total)||0;
|
|
519
835
|
// Phase-2 git signal: commits landed vs building activity. Gated on commits>0 so a missing git pass can't false-fire ghosty.
|
|
520
836
|
const commits=Number(scale.totalCommits)||0, attention=Number(uwh.userAttentionHours)||0;
|
|
521
837
|
const activeDaysG=Number(uwh.activeAttentionDays)||0;
|
|
@@ -578,6 +894,8 @@ function renderV7PersonaHover(persona){
|
|
|
578
894
|
host.innerHTML = line(persona.primary) + (persona.sides||[]).map(line).join('') +
|
|
579
895
|
(persona.rank?`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>`:'') +
|
|
580
896
|
(persona.rhythm?`<div class="ph-meta"><b>${esc(persona.rhythm.label)}</b> · ${esc(persona.rhythm.blurb)}</div>`:'');
|
|
897
|
+
const frHost=document.getElementById('fr-persona-hover');
|
|
898
|
+
if(frHost) frHost.innerHTML=host.innerHTML;
|
|
581
899
|
}
|
|
582
900
|
function buildSetupRepos(report){
|
|
583
901
|
const split=providerSplit(report);
|
|
@@ -609,6 +927,7 @@ function buildSetupRepos(report){
|
|
|
609
927
|
sessions:Number(repo.sessions)||0,
|
|
610
928
|
codexPct, claudePct:100-codexPct,
|
|
611
929
|
pin:i<2,
|
|
930
|
+
daily:Array.isArray(repo.daily)?repo.daily:null,
|
|
612
931
|
};
|
|
613
932
|
});
|
|
614
933
|
}
|
|
@@ -618,13 +937,17 @@ function applySetupReport(report){
|
|
|
618
937
|
const persona=derivePersona(report);
|
|
619
938
|
const cost=money(report?.cost?.total);
|
|
620
939
|
const attention=oneDec(report?.userWorkingHours?.userAttentionHours);
|
|
621
|
-
const dirty=oneDec(report
|
|
940
|
+
const dirty=oneDec(goldenWastePct(report));
|
|
941
|
+
const metricValue=`${dirty}%`;
|
|
942
|
+
const metricLabel='wasteful context';
|
|
943
|
+
const metricSay=goldenWasteSay(report);
|
|
622
944
|
const cf=report?.counterfactual || {};
|
|
623
945
|
const reach=monthDay(cf.counterfactualDate);
|
|
624
946
|
const instead=monthDay(report?.scanEndDate).label;
|
|
625
|
-
const days=Math.max(
|
|
947
|
+
const days=Math.max(0, Math.round(Number(cf.daysGained)||0));
|
|
626
948
|
document.querySelectorAll('.setup-persona-name,.persona-card .name,.v7-personaName').forEach((el)=>{ el.textContent=persona.primary.label; });
|
|
627
949
|
document.querySelectorAll('.setup-rank,.persona-card .rankrhythm,.v7-rank').forEach((el)=>{ el.textContent=`${persona.rank.label} · ${persona.rhythm.label}`; });
|
|
950
|
+
const frRank=document.getElementById('fr-rank'); if(frRank) frRank.innerHTML=`You are <b>${persona.rank.label}</b> on <b>${persona.rhythm.label}</b>`;
|
|
628
951
|
document.querySelectorAll('.setup-combo,.persona-card .combo,.v7-combo').forEach((el)=>{ el.innerHTML=persona.sides.map((s)=>`<span class="ct">${s.label}</span>`).join(''); });
|
|
629
952
|
renderPersonaHover(persona);
|
|
630
953
|
renderV7PersonaHover(persona);
|
|
@@ -635,45 +958,299 @@ function applySetupReport(report){
|
|
|
635
958
|
const repoCount=Number(report?.scale?.repoCount) || (Array.isArray(report?.repos) ? report.repos.length : 0);
|
|
636
959
|
const v7RepoCount=document.getElementById('v7-repo-count'); if(v7RepoCount && repoCount) v7RepoCount.textContent=String(repoCount);
|
|
637
960
|
const v7Tokens=document.getElementById('v7-total-tokens'); if(v7Tokens) v7Tokens.textContent=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
638
|
-
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent
|
|
639
|
-
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent=
|
|
961
|
+
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent=metricValue;
|
|
962
|
+
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent=metricLabel;
|
|
640
963
|
const v7MetricSay=document.getElementById('v7-metric-say');
|
|
641
|
-
if(v7MetricSay) v7MetricSay.innerHTML=
|
|
964
|
+
if(v7MetricSay) v7MetricSay.innerHTML=metricSay;
|
|
965
|
+
const frName=document.getElementById('fr-name'); if(frName) frName.textContent=persona.primary.label;
|
|
966
|
+
const frAvatar=document.getElementById('fr-avatar');
|
|
967
|
+
if(frAvatar){
|
|
968
|
+
const key=String(persona.primary?.label||'').toLowerCase();
|
|
969
|
+
if(PERSONA_TRAITS[key]){ frAvatar.src=`./personality_stickers/${key}.png`; frAvatar.alt=`${key} avatar`; }
|
|
970
|
+
}
|
|
971
|
+
const frRepos=document.getElementById('fr-repos'); if(frRepos && repoCount) frRepos.textContent=`${repoCount} ${repoCount===1?'repo':'repos'}`;
|
|
972
|
+
const frHours=document.getElementById('fr-hours'); if(frHours) frHours.textContent=`${attention} hours`;
|
|
973
|
+
const frTokens=document.getElementById('fr-tokens'); if(frTokens) frTokens.textContent=`${fmtTok(Number(report?.scale?.totalTokens)||0)} tokens`;
|
|
974
|
+
const frCost=document.getElementById('fr-cost'); if(frCost) frCost.textContent=cost;
|
|
975
|
+
const frMetricValue=document.getElementById('fr-metric-value'); if(frMetricValue) frMetricValue.textContent=metricValue;
|
|
976
|
+
const frMetricLabel=document.getElementById('fr-metric-label'); if(frMetricLabel) frMetricLabel.textContent=metricLabel;
|
|
977
|
+
const frMetricSay=document.getElementById('fr-metric-say'); if(frMetricSay) frMetricSay.innerHTML=metricSay;
|
|
978
|
+
try{ window.parent?.postMessage({type:'echomem:city-fieldnote',
|
|
979
|
+
avatar:(frAvatar&&frAvatar.src)||'',
|
|
980
|
+
name:String(persona?.primary?.label||''),
|
|
981
|
+
rank:(document.getElementById('fr-rank')||{}).innerHTML||'',
|
|
982
|
+
lede:(document.querySelector('.fr-lede')||{}).innerHTML||'',
|
|
983
|
+
guide:{
|
|
984
|
+
current:{primary:String(persona?.primary?.label||''), rank:String(persona?.rank?.label||''), rhythm:String(persona?.rhythm?.label||'')},
|
|
985
|
+
traits:Object.values(PERSONA_TRAITS).map(({label,blurb})=>({label,blurb})),
|
|
986
|
+
ranks:Object.values(PERSONA_RANKS).map(({label,blurb})=>({label,blurb})),
|
|
987
|
+
rhythms:Object.values(PERSONA_RHYTHMS).map(({label,blurb})=>({label,blurb}))
|
|
988
|
+
}
|
|
989
|
+
}, location.origin); }catch(_){}
|
|
642
990
|
const splitHtml=`<span>${split.codexPct}% Codex</span><span>${split.claudePct}% Claude</span>`;
|
|
643
991
|
const setupSplit=document.querySelector('.setup-split'); if(setupSplit) setupSplit.innerHTML=splitHtml;
|
|
992
|
+
const csClaude=document.querySelector('#city-split .cs-claude'); if(csClaude) csClaude.style.width=`${split.claudePct}%`;
|
|
993
|
+
const csCodex=document.querySelector('#city-split .cs-codex'); if(csCodex) csCodex.style.width=`${split.codexPct}%`;
|
|
994
|
+
const csClB=document.querySelector('#city-split .cs-l.claude b'); if(csClB) csClB.textContent=`${split.claudePct}%`;
|
|
995
|
+
const csCdB=document.querySelector('#city-split .cs-l.codex b'); if(csCdB) csCdB.textContent=`${split.codexPct}%`;
|
|
996
|
+
const tlStartDate=new Date(report?.scanStartDate);
|
|
997
|
+
if(!Number.isNaN(tlStartDate.getTime())){
|
|
998
|
+
const tlStartEl=document.querySelector('#city-timeline .tl-start');
|
|
999
|
+
if(tlStartEl) tlStartEl.textContent=tlStartDate.toLocaleString('en-US',{month:'short', year:'numeric', timeZone:'UTC'});
|
|
1000
|
+
}
|
|
1001
|
+
const spanDays=daysBetween(report?.scanStartDate, report?.scanEndDate);
|
|
1002
|
+
const spanLabel=spanDays>=60 ? `${Math.round(spanDays/30.44)} months with AI` : `${spanDays} day${spanDays===1?'':'s'} with AI`;
|
|
1003
|
+
const tlSpanEl=document.querySelector('#city-timeline .tl-span'); if(tlSpanEl) tlSpanEl.textContent=spanLabel;
|
|
644
1004
|
const dirtyPct=document.querySelector('.dirty-context .pct');
|
|
645
|
-
if(dirtyPct) dirtyPct.innerHTML=`<span id="setup-metric-value">${
|
|
1005
|
+
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>`;
|
|
646
1006
|
const dirtyDesc=document.querySelector('.dirty-context .desc');
|
|
647
|
-
if(dirtyDesc) dirtyDesc.id='setup-metric-say';
|
|
1007
|
+
if(dirtyDesc){ dirtyDesc.id='setup-metric-say'; dirtyDesc.innerHTML=metricSay; }
|
|
648
1008
|
const monthEl=document.querySelector('.counterfactual text:first-of-type'); if(monthEl) monthEl.textContent=reach.month;
|
|
649
1009
|
const dayEl=document.querySelector('.counterfactual text:last-of-type'); if(dayEl) dayEl.textContent=reach.day;
|
|
650
1010
|
const cfHero=document.querySelector('.cf-hero'); if(cfHero) cfHero.innerHTML=`Echo saves you <b>${days} days</b>`;
|
|
651
1011
|
const cfSay=document.querySelector('.cf-say'); if(cfSay) cfSay.innerHTML=`You'd have reached today's workspace on <b>${reach.label}</b>, not ${instead}.`;
|
|
652
1012
|
signpostTokens=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
653
|
-
|
|
654
|
-
|
|
1013
|
+
signpostHours=`${attention}h`;
|
|
1014
|
+
signpostCost=cost;
|
|
1015
|
+
const dirtyShare=Math.max(0, Number(dirty)||0)/100;
|
|
1016
|
+
signpostWastedTime=`≈${Math.round((Number(attention)||0)*dirtyShare)}h WASTED`;
|
|
1017
|
+
signpostWastedPct=`${Math.round(Number(dirty))||0}% WASTED`;
|
|
1018
|
+
signpostWastedCost=`≈${money((Number(report?.cost?.total)||0)*dirtyShare)} WASTED`;
|
|
655
1019
|
}
|
|
656
1020
|
async function loadSetupReport(){
|
|
657
1021
|
const nonce=pageParams.get('nonce');
|
|
658
|
-
if(!setupMode
|
|
1022
|
+
if(!setupMode) return null;
|
|
659
1023
|
const countEl=document.getElementById('scan-count');
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1024
|
+
const loadingEl=document.getElementById('scan-loading');
|
|
1025
|
+
const setLoadState=(kind, title, detail)=>{
|
|
1026
|
+
const titleEl=loadingEl?.querySelector('.title');
|
|
1027
|
+
const eyebrowEl=loadingEl?.querySelector('.eyebrow');
|
|
1028
|
+
const barEl=loadingEl?.querySelector('.bar');
|
|
1029
|
+
if(titleEl) titleEl.textContent=title;
|
|
1030
|
+
if(eyebrowEl && kind!=='running') eyebrowEl.textContent=kind==='empty'?'Local scan complete':'Local scan error';
|
|
1031
|
+
if(barEl) barEl.hidden=kind!=='running';
|
|
1032
|
+
if(countEl) countEl.textContent=String(detail||'').slice(0, 240);
|
|
1033
|
+
};
|
|
1034
|
+
const reportShape=(report)=>{
|
|
1035
|
+
if(!report || typeof report!=='object') return {ok:false, code:'REPORT_NOT_OBJECT'};
|
|
1036
|
+
if(report.schemaVersion!==1) return {ok:false, code:'REPORT_SCHEMA_UNSUPPORTED'};
|
|
1037
|
+
const origin=report.dataOrigin;
|
|
1038
|
+
if((previewMode && origin!=='design-preview') || (!previewMode && origin!=='local-workspace-scan')){
|
|
1039
|
+
return {ok:false, code:'REPORT_ORIGIN_REJECTED'};
|
|
1040
|
+
}
|
|
1041
|
+
const finiteNonNegative=(value)=>typeof value==='number' && Number.isFinite(value) && value>=0;
|
|
1042
|
+
const scale=report.scale;
|
|
1043
|
+
const cost=report.cost;
|
|
1044
|
+
const hours=report.userWorkingHours;
|
|
1045
|
+
const summary=goldenSummary(report);
|
|
1046
|
+
const projection=report.billedWasteProjection;
|
|
1047
|
+
const cf=report.counterfactual;
|
|
1048
|
+
if(!scale || typeof scale!=='object' || !finiteNonNegative(scale.totalTokens) || Number(scale.totalTokens)<=0 ||
|
|
1049
|
+
!finiteNonNegative(scale.totalInputTokens) || Number(scale.totalInputTokens)<=0 ||
|
|
1050
|
+
!finiteNonNegative(scale.sessionCount) || Number(scale.sessionCount)<=0 ||
|
|
1051
|
+
!finiteNonNegative(scale.repoCount) || Number(scale.repoCount)<=0){
|
|
1052
|
+
return {ok:false, code:'REPORT_SCALE_INVALID'};
|
|
1053
|
+
}
|
|
1054
|
+
if(!cost || typeof cost!=='object' || !finiteNonNegative(cost.total) || !cost.byModel || typeof cost.byModel!=='object'){
|
|
1055
|
+
return {ok:false, code:'REPORT_COST_INVALID'};
|
|
1056
|
+
}
|
|
1057
|
+
if(!hours || typeof hours!=='object' || !finiteNonNegative(hours.userAttentionHours) ||
|
|
1058
|
+
!finiteNonNegative(hours.averageDailyAttentionHours)){
|
|
1059
|
+
return {ok:false, code:'REPORT_ATTENTION_INVALID'};
|
|
1060
|
+
}
|
|
1061
|
+
if(!summary || !finiteNonNegative(summary.sessionsAnalyzed) || Number(summary.sessionsAnalyzed)<=0 ||
|
|
1062
|
+
!finiteNonNegative(summary.officialInputTokens) || Number(summary.officialInputTokens)<=0 ||
|
|
1063
|
+
!finiteNonNegative(summary.wasteTokens) || !finiteNonNegative(summary.usefulTokens) ||
|
|
1064
|
+
!finiteNonNegative(summary.wastePct) || Number(summary.wastePct)>100){
|
|
1065
|
+
return {ok:false, code:'REPORT_CANONICAL_INVALID'};
|
|
1066
|
+
}
|
|
1067
|
+
if(!projection || typeof projection!=='object' || projection.method!=='canonical-window-share-over-provider-input' ||
|
|
1068
|
+
!finiteNonNegative(projection.billedInputTokens) || projection.billedInputTokens!==scale.totalInputTokens ||
|
|
1069
|
+
!finiteNonNegative(projection.canonicalInputTokens) || projection.canonicalInputTokens!==summary.officialInputTokens ||
|
|
1070
|
+
!finiteNonNegative(projection.canonicalWasteTokens) || projection.canonicalWasteTokens!==summary.wasteTokens ||
|
|
1071
|
+
!finiteNonNegative(projection.projectedUsefulTokens) || !finiteNonNegative(projection.projectedWasteTokens) ||
|
|
1072
|
+
projection.projectedUsefulTokens+projection.projectedWasteTokens!==projection.billedInputTokens){
|
|
1073
|
+
return {ok:false, code:'REPORT_PROJECTION_INVALID'};
|
|
1074
|
+
}
|
|
1075
|
+
if(!Array.isArray(report.repos) || !report.repos.some((repo)=>repo && typeof repo.name==='string' && repo.name.trim() &&
|
|
1076
|
+
typeof repo.tokens==='number' && Number.isFinite(repo.tokens) && repo.tokens>0)){
|
|
1077
|
+
return {ok:false, code:'REPORT_REPOS_EMPTY'};
|
|
1078
|
+
}
|
|
1079
|
+
if(!Number.isFinite(contextHealthScore(report)) || !cf || typeof cf!=='object' ||
|
|
1080
|
+
!finiteNonNegative(cf.daysGained) || !finiteNonNegative(cf.targetScore) || cf.targetScore>100 ||
|
|
1081
|
+
typeof report.scanStartDate!=='string' || Number.isNaN(Date.parse(report.scanStartDate)) ||
|
|
1082
|
+
typeof report.scanEndDate!=='string' || Number.isNaN(Date.parse(report.scanEndDate)) ||
|
|
1083
|
+
typeof cf.counterfactualDate!=='string' || Number.isNaN(Date.parse(cf.counterfactualDate))){
|
|
1084
|
+
return {ok:false, code:'REPORT_DERIVED_FIELDS_INVALID'};
|
|
1085
|
+
}
|
|
1086
|
+
return {ok:true};
|
|
1087
|
+
};
|
|
1088
|
+
const classifyEnvelope=(envelope)=>{
|
|
1089
|
+
if(!envelope || typeof envelope!=='object' || envelope.schemaVersion!==1 ||
|
|
1090
|
+
typeof envelope.scanId!=='string' || !envelope.scanId.trim()){
|
|
1091
|
+
return {kind:'invalid', code:'REPORT_ENVELOPE_INVALID'};
|
|
1092
|
+
}
|
|
1093
|
+
if(envelope.kind==='scanning') return {kind:'scanning', progress:envelope.progress||{}};
|
|
1094
|
+
if(envelope.kind==='empty') return {kind:'empty'};
|
|
1095
|
+
if(envelope.kind==='failed'){
|
|
1096
|
+
const error=envelope.error && typeof envelope.error==='object' ? envelope.error : {};
|
|
1097
|
+
return {kind:'failed', code:String(error.code||'REPORT_BUILD_FAILED'), message:String(error.message||'The local report could not be built.')};
|
|
1098
|
+
}
|
|
1099
|
+
if(envelope.kind!=='ready') return {kind:'invalid', code:'REPORT_ENVELOPE_KIND_INVALID'};
|
|
1100
|
+
const shape=reportShape(envelope.report);
|
|
1101
|
+
if(!shape.ok) return {kind:'invalid', code:shape.code};
|
|
1102
|
+
const origin=envelope.report.dataOrigin;
|
|
1103
|
+
if(origin==='local-workspace-scan' && envelope.mode!=='production') return {kind:'invalid', code:'REPORT_MODE_INVALID'};
|
|
1104
|
+
if(origin==='design-preview' && (!previewMode || envelope.mode!=='preview')) return {kind:'invalid', code:'REPORT_PREVIEW_REJECTED'};
|
|
1105
|
+
return {kind:'ready', report:envelope.report};
|
|
1106
|
+
};
|
|
1107
|
+
if(!nonce){
|
|
1108
|
+
setLoadState('failed', 'Your local report could not be shown', 'REPORT_NONCE_MISSING: Rerun Echo setup to open a verified local scan.');
|
|
1109
|
+
return new Promise(()=>{});
|
|
669
1110
|
}
|
|
670
|
-
|
|
1111
|
+
const renderScanProgress=(progress)=>{
|
|
1112
|
+
const scanned=Number(progress?.scanned||0);
|
|
1113
|
+
const total=Number(progress?.total||0);
|
|
1114
|
+
const elapsed=Number(progress?.stageElapsedMs||0)>0 ? ' · '+Math.round(Number(progress.stageElapsedMs)/1000)+'s' : '';
|
|
1115
|
+
if(total>0){
|
|
1116
|
+
const detail=progress?.stage==='classifying-repeated-context'
|
|
1117
|
+
? (progress.label || 'classifying repeated context')+' '+scanned.toLocaleString()+' / '+total.toLocaleString()+elapsed
|
|
1118
|
+
: (progress?.stage && progress.stage!=='reading-transcripts'
|
|
1119
|
+
? (progress.label || 'building report')+elapsed
|
|
1120
|
+
: 'read '+scanned.toLocaleString()+' / '+total.toLocaleString()+' transcript files');
|
|
1121
|
+
setLoadState('running', 'Reading your local history', detail);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
// The parent owns /report polling and validation. The city only renders the
|
|
1125
|
+
// parent-provided state, keeping loading, failure, and ready state in one place.
|
|
1126
|
+
return await new Promise((resolve)=>{
|
|
1127
|
+
let closed=false;
|
|
1128
|
+
const close=()=>{
|
|
1129
|
+
if(closed) return;
|
|
1130
|
+
closed=true;
|
|
1131
|
+
window.removeEventListener('message', onMessage);
|
|
1132
|
+
};
|
|
1133
|
+
const stop=(kind, code, message)=>{
|
|
1134
|
+
close();
|
|
1135
|
+
if(kind==='empty'){
|
|
1136
|
+
setLoadState('empty', 'No local workspace data found', 'No demo data was substituted.');
|
|
1137
|
+
}else{
|
|
1138
|
+
setLoadState('failed', 'Your local report could not be shown', String(code||'REPORT_BUILD_FAILED')+': '+(message||'Rerun Echo setup to try again.'));
|
|
1139
|
+
}
|
|
1140
|
+
// Keep the city on its terminal loading/error surface instead of drawing a city without a report.
|
|
1141
|
+
};
|
|
1142
|
+
const onMessage=(event)=>{
|
|
1143
|
+
if(event.origin!==location.origin) return;
|
|
1144
|
+
const data=event.data||{};
|
|
1145
|
+
if(data.type!=='echomem:setup-report' || data.nonce!==nonce || !Object.prototype.hasOwnProperty.call(data,'envelope')) return;
|
|
1146
|
+
const classified=classifyEnvelope(data.envelope);
|
|
1147
|
+
if(classified.kind==='scanning'){
|
|
1148
|
+
renderScanProgress(classified.progress);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if(classified.kind==='ready'){
|
|
1152
|
+
close();
|
|
1153
|
+
resolve(data.envelope);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if(classified.kind==='empty'){
|
|
1157
|
+
stop('empty');
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
stop('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
|
|
1161
|
+
};
|
|
1162
|
+
window.addEventListener('message', onMessage);
|
|
1163
|
+
try{ window.parent?.postMessage({type:'echomem:city-ready', nonce}, location.origin); }catch(_){}
|
|
1164
|
+
});
|
|
671
1165
|
}
|
|
672
|
-
let signpostTokens='7.49B';
|
|
673
|
-
let
|
|
674
|
-
|
|
1166
|
+
let signpostTokens=setupMode?'—':'7.49B';
|
|
1167
|
+
let signpostHours=setupMode?'—':'191.1h';
|
|
1168
|
+
let signpostCost=setupMode?'—':'$13,689';
|
|
1169
|
+
let signpostWastedTime=setupMode?'—':'≈162h WASTED';
|
|
1170
|
+
let signpostWastedPct=setupMode?'—':'85% WASTED';
|
|
1171
|
+
let signpostWastedCost=setupMode?'—':'≈$11,636 WASTED';
|
|
1172
|
+
const setupEnvelope=await loadSetupReport();
|
|
1173
|
+
const setupReport=setupMode ? setupEnvelope.report : null;
|
|
1174
|
+
const setupLiveRepos=setupMode ? buildSetupRepos(setupReport) : [];
|
|
675
1175
|
applySetupReport(setupReport);
|
|
676
1176
|
renderV7PersonaHover();
|
|
1177
|
+
let comparisonBefore=contextHealthScore(setupReport);
|
|
1178
|
+
if(!Number.isFinite(comparisonBefore)) comparisonBefore=setupMode?0:58;
|
|
1179
|
+
const reportTargetScore=Number(setupReport?.counterfactual?.targetScore);
|
|
1180
|
+
let comparisonAfter=Number.isFinite(reportTargetScore) ? clampScore(reportTargetScore) : (setupMode?comparisonBefore:clampScore(comparisonBefore+28));
|
|
1181
|
+
comparisonAfter=Math.max(comparisonBefore,comparisonAfter);
|
|
1182
|
+
let cityComparison='without';
|
|
1183
|
+
let repairTarget=0;
|
|
1184
|
+
let repairProgress=0;
|
|
1185
|
+
let dirty=true;
|
|
1186
|
+
let shadowFrame=0;
|
|
1187
|
+
|
|
1188
|
+
function setupWasteShare(){
|
|
1189
|
+
const pct=Number(goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || 0);
|
|
1190
|
+
if(Number.isFinite(pct) && pct>0) return Math.max(0, Math.min(0.95, pct/100));
|
|
1191
|
+
return Math.max(0, Math.min(0.95, (100-comparisonBefore)/100));
|
|
1192
|
+
}
|
|
1193
|
+
function setupAttentionHours(){
|
|
1194
|
+
const direct=Number(setupReport?.userWorkingHours?.userAttentionHours);
|
|
1195
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1196
|
+
return all.reduce((sum,o)=>sum+(Number(o.hours)||0),0);
|
|
1197
|
+
}
|
|
1198
|
+
function setupInputTokens(){
|
|
1199
|
+
const direct=Number(setupReport?.billedWasteProjection?.billedInputTokens);
|
|
1200
|
+
if(Number.isFinite(direct) && direct>0) return direct;
|
|
1201
|
+
return 0;
|
|
1202
|
+
}
|
|
1203
|
+
function setupUsefulTokens(){
|
|
1204
|
+
const direct=Number(setupReport?.billedWasteProjection?.projectedUsefulTokens);
|
|
1205
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1206
|
+
return 0;
|
|
1207
|
+
}
|
|
1208
|
+
function setupApiCost(){
|
|
1209
|
+
const direct=Number(setupReport?.cost?.total);
|
|
1210
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1211
|
+
return all.reduce((sum,o)=>sum+(Number(o.cost)||0),0);
|
|
1212
|
+
}
|
|
1213
|
+
function fmtInt(n){ return Math.round(Number(n)||0).toLocaleString('en-US'); }
|
|
1214
|
+
function cityMetricReadout(){
|
|
1215
|
+
const waste=setupWasteShare();
|
|
1216
|
+
const attention=setupAttentionHours();
|
|
1217
|
+
const reportedDaily=Number(setupReport?.userWorkingHours?.averageDailyAttentionHours);
|
|
1218
|
+
const daily=Number.isFinite(reportedDaily) && reportedDaily>0 ? reportedDaily : (attention>0?attention:0);
|
|
1219
|
+
const currentDays=daily>0?Math.ceil(attention/daily):0;
|
|
1220
|
+
const withHours=Math.max(0, attention*(1-waste));
|
|
1221
|
+
const withDays=daily>0?Math.ceil(withHours/daily):0;
|
|
1222
|
+
const inputTokens=setupInputTokens();
|
|
1223
|
+
const usefulTokens=Math.max(0, Math.min(inputTokens, setupUsefulTokens()));
|
|
1224
|
+
const apiCost=setupApiCost();
|
|
1225
|
+
const withCost=Math.max(0, apiCost*(1-waste));
|
|
1226
|
+
const savedDays=Math.max(0, currentDays-withDays);
|
|
1227
|
+
const savedTokens=Math.max(0, inputTokens-usefulTokens);
|
|
1228
|
+
const savedCost=Math.max(0, apiCost-withCost);
|
|
1229
|
+
const mode=cityComparison==='with'?'with':'without';
|
|
1230
|
+
const metric=cityMetric==='tokens'||cityMetric==='cost'?cityMetric:'time';
|
|
1231
|
+
const data={
|
|
1232
|
+
time:{
|
|
1233
|
+
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.`},
|
|
1234
|
+
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.`}
|
|
1235
|
+
},
|
|
1236
|
+
tokens:{
|
|
1237
|
+
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.`},
|
|
1238
|
+
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.`}
|
|
1239
|
+
},
|
|
1240
|
+
cost:{
|
|
1241
|
+
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.`},
|
|
1242
|
+
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.`}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
return data[metric][mode];
|
|
1246
|
+
}
|
|
1247
|
+
function updateFieldReportMetric(){
|
|
1248
|
+
if(!setupMode) return;
|
|
1249
|
+
const readout=cityMetricReadout();
|
|
1250
|
+
const metricValue=document.getElementById('fr-metric-value'); if(metricValue) metricValue.textContent=readout.value;
|
|
1251
|
+
const metricLabel=document.getElementById('fr-metric-label'); if(metricLabel) metricLabel.textContent=readout.label;
|
|
1252
|
+
const metricSay=document.getElementById('fr-metric-say'); if(metricSay) metricSay.innerHTML=readout.say;
|
|
1253
|
+
}
|
|
677
1254
|
|
|
678
1255
|
const renderer = new THREE.WebGLRenderer({canvas:document.getElementById('repo-visualizer'), antialias:true, alpha:true});
|
|
679
1256
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
@@ -705,8 +1282,9 @@ function buildEnv(){
|
|
|
705
1282
|
}
|
|
706
1283
|
scene.environment = buildEnv();
|
|
707
1284
|
|
|
708
|
-
//
|
|
709
|
-
|
|
1285
|
+
// Frozen poster data is available only outside setup. Setup starts empty and can be
|
|
1286
|
+
// populated solely by the validated local report envelope above.
|
|
1287
|
+
const POSTER_REPOS=[
|
|
710
1288
|
{name:'WebPageReactVersion', x:-30, z:-60, tool:'claude', pin:true, tokens:3436946364, cost:8896, days:18.6, hours:92.8, sessions:74},
|
|
711
1289
|
{name:'EchoMem-Chrome', x:140, z:-14, tool:'claude', pin:true, tokens:2863590896, cost:6959, days:14.0, hours:70.1, sessions:38},
|
|
712
1290
|
{name:'echo-design-system', x:30, z:30, tool:'codex', tokens:382642012, cost:1106, days:4.2, hours:21.2, sessions:29},
|
|
@@ -721,12 +1299,12 @@ let REPOS=[
|
|
|
721
1299
|
{name:'dist', x:-210,z:118, tool:'claude', tokens:4550877, cost:8, days:0, hours:0, sessions:2},
|
|
722
1300
|
{name:'Daimler Engineering', x:-168,z:160, tool:'codex', tokens:4439902, cost:15, days:0, hours:0.2, sessions:2},
|
|
723
1301
|
];
|
|
724
|
-
|
|
1302
|
+
let REPOS=setupMode ? [] : POSTER_REPOS;
|
|
1303
|
+
// Per-repo Codex/Claude split for the retired standalone poster fallback.
|
|
725
1304
|
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]};
|
|
726
1305
|
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; } }
|
|
727
|
-
if(setupMode
|
|
728
|
-
|
|
729
|
-
if(liveRepos.length) REPOS=liveRepos;
|
|
1306
|
+
if(setupMode){
|
|
1307
|
+
REPOS=setupLiveRepos;
|
|
730
1308
|
window.__ECHOMEM_CITY_REPORT_REPO_COUNT=Array.isArray(setupReport?.repos) ? setupReport.repos.length : -1;
|
|
731
1309
|
window.__ECHOMEM_CITY_REPOS=REPOS.map(({name,tokens,cost,hours,sessions,tool})=>({name,tokens,cost,hours,sessions,tool}));
|
|
732
1310
|
}
|
|
@@ -829,17 +1407,21 @@ const fill = new THREE.DirectionalLight(0xffffff, 0.4); fill.position.set(CX+420
|
|
|
829
1407
|
const floor = new THREE.Mesh(new THREE.PlaneGeometry(6000,6000), new THREE.ShadowMaterial({opacity:0.22}));
|
|
830
1408
|
floor.rotation.x=-Math.PI/2; floor.position.y=0; floor.receiveShadow=true; scene.add(floor);
|
|
831
1409
|
|
|
832
|
-
//
|
|
1410
|
+
// Comparison material: corrupted buildings are dark and chalky; repaired buildings regain colour
|
|
1411
|
+
// without returning to the glossy toy-plastic finish.
|
|
833
1412
|
function clayGlass(hex){
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1413
|
+
const clean=new THREE.Color(hex).multiplyScalar(0.82);
|
|
1414
|
+
const damaged=clean.clone().multiplyScalar(0.58);
|
|
1415
|
+
const material=new THREE.MeshPhysicalMaterial({
|
|
1416
|
+
color:damaged,
|
|
1417
|
+
roughness:0.9, metalness:0.01,
|
|
1418
|
+
transmission:0.05, ior:1.25, thickness:45,
|
|
1419
|
+
attenuationColor:damaged.clone(), attenuationDistance:58,
|
|
1420
|
+
clearcoat:0.015, clearcoatRoughness:0.94,
|
|
1421
|
+
envMapIntensity:0.24, transparent:true,
|
|
842
1422
|
});
|
|
1423
|
+
buildingMaterials.push({material,damaged,clean});
|
|
1424
|
+
return material;
|
|
843
1425
|
}
|
|
844
1426
|
function selectiveRoundedBoxGeometry(width, height, depth, segments=8, radius=0.1, opts={}){
|
|
845
1427
|
const roundTop=opts.roundTop!==false, roundBottom=opts.roundBottom!==false;
|
|
@@ -881,6 +1463,7 @@ function selectiveRoundedBoxGeometry(width, height, depth, segments=8, radius=0.
|
|
|
881
1463
|
}
|
|
882
1464
|
function addCube(o){
|
|
883
1465
|
const {x,z,w,h,d,tool}=o;
|
|
1466
|
+
const damage=corruptionProfile(o);
|
|
884
1467
|
const r=Math.min(Math.min(w,h,d)*0.12, 12);
|
|
885
1468
|
// proportional split: Claude (orange) on the bottom, Codex (green) on top; segment heights ∝ each tool's token share
|
|
886
1469
|
let cl=Number(o.claudePct), cx=Number(o.codexPct);
|
|
@@ -888,48 +1471,156 @@ function addCube(o){
|
|
|
888
1471
|
const total=cl+cx, segs=[];
|
|
889
1472
|
if(cl>0) segs.push([h*cl/total, TOOL.claude]);
|
|
890
1473
|
if(cx>0) segs.push([h*cx/total, TOOL.codex]);
|
|
1474
|
+
const slices=[];
|
|
1475
|
+
for(const [hSeg,color] of segs){
|
|
1476
|
+
const count=hSeg>78?4:hSeg>44?3:hSeg>24?2:1;
|
|
1477
|
+
const gap=count>1?Math.min(3.2,hSeg*0.055):0;
|
|
1478
|
+
const sliceH=(hSeg-gap*(count-1))/count;
|
|
1479
|
+
for(let i=0;i<count;i++) slices.push({h:sliceH,color,gap:i===count-1?0:gap});
|
|
1480
|
+
}
|
|
891
1481
|
const group=new THREE.Group();
|
|
1482
|
+
group.position.set(x,0,z);
|
|
1483
|
+
group.rotation.x=damage.leanX*damage.amount;
|
|
1484
|
+
group.rotation.z=damage.leanZ*damage.amount;
|
|
1485
|
+
repairGroups.push({group,damageX:group.rotation.x,damageZ:group.rotation.z});
|
|
892
1486
|
let yBase=TRAY_TOP - 3;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1487
|
+
let cleanY=TRAY_TOP - 3;
|
|
1488
|
+
const fractured=slices.length>1;
|
|
1489
|
+
for(let i=0;i<slices.length;i++){
|
|
1490
|
+
const {h:hSeg,color,gap}=slices[i];
|
|
896
1491
|
const mat=clayGlass(color);
|
|
897
|
-
const geom=
|
|
898
|
-
? selectiveRoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45), {roundBottom:i===0, roundTop:i===
|
|
1492
|
+
const geom=fractured
|
|
1493
|
+
? selectiveRoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45), {roundBottom:i===0, roundTop:i===slices.length-1})
|
|
899
1494
|
: new RoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45));
|
|
900
1495
|
const m=new THREE.Mesh(geom, mat);
|
|
901
|
-
|
|
1496
|
+
const stagger=i===0?0:(hash01(o.name,i+61)-0.5)*2;
|
|
1497
|
+
const splitShift=stagger*Math.min(w*0.24, 13)*damage.amount;
|
|
1498
|
+
m.position.set(splitShift, yBase + hSeg/2, -splitShift*(0.25+hash01(o.name,i+67)*0.45));
|
|
1499
|
+
if(i>0) m.rotation.y=(hash01(o.name,i+71)-0.5)*0.30*damage.amount;
|
|
1500
|
+
repairPieces.push({
|
|
1501
|
+
mesh:m,
|
|
1502
|
+
damagePos:m.position.clone(),
|
|
1503
|
+
cleanPos:new THREE.Vector3(0,cleanY+hSeg/2,0),
|
|
1504
|
+
damageRotY:m.rotation.y,
|
|
1505
|
+
});
|
|
902
1506
|
m.castShadow=true; m.receiveShadow=true; m.userData=o;
|
|
903
1507
|
group.add(m);
|
|
904
|
-
yBase+=hSeg;
|
|
1508
|
+
yBase+=hSeg+gap;
|
|
1509
|
+
cleanY+=hSeg;
|
|
1510
|
+
}
|
|
1511
|
+
const scarMat=new THREE.MeshStandardMaterial({color:0x343238,roughness:0.92,metalness:0.08,transparent:true});
|
|
1512
|
+
const scarCount=Math.max(1,Math.round(2+damage.amount*3));
|
|
1513
|
+
for(let i=0;i<scarCount;i++){
|
|
1514
|
+
const sw=Math.max(3,w*(0.10+hash01(o.name,101+i)*0.20));
|
|
1515
|
+
const sh=Math.max(2,Math.min(6,h*0.045));
|
|
1516
|
+
const sd=Math.max(2,d*0.06);
|
|
1517
|
+
const scar=new THREE.Mesh(new THREE.BoxGeometry(sw,sh,sd),scarMat);
|
|
1518
|
+
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);
|
|
1519
|
+
scar.rotation.z=(hash01(o.name,171+i)-0.5)*0.55;
|
|
1520
|
+
scar.castShadow=true; scar.userData=o; group.add(scar);
|
|
1521
|
+
corruptionFades.push({mesh:scar,material:scar.material,damagePos:scar.position.clone(),cleanPos:scar.position.clone(),baseScale:scar.scale.clone()});
|
|
1522
|
+
}
|
|
1523
|
+
const chunkCount=Math.max(4,Math.round(4+damage.amount*5));
|
|
1524
|
+
for(let i=0;i<chunkCount;i++){
|
|
1525
|
+
const size=Math.max(3,Math.min(w,d)*(0.07+hash01(o.name,201+i)*0.10));
|
|
1526
|
+
const chunk=new THREE.Mesh(
|
|
1527
|
+
i%3===0?new THREE.TetrahedronGeometry(size,0):new THREE.BoxGeometry(size,size*(0.5+hash01(o.name,221+i)),size*0.72),
|
|
1528
|
+
i%2?scarMat:clayGlass(tool==='codex'?TOOL.codex:TOOL.claude)
|
|
1529
|
+
);
|
|
1530
|
+
const angle=hash01(o.name,241+i)*Math.PI*2;
|
|
1531
|
+
const radius=Math.max(w,d)*(0.62+hash01(o.name,261+i)*1.05);
|
|
1532
|
+
const baseY=TRAY_TOP+2+hash01(o.name,281+i)*Math.min(h*0.72,58);
|
|
1533
|
+
chunk.position.set(Math.cos(angle)*radius,baseY,Math.sin(angle)*radius);
|
|
1534
|
+
chunk.rotation.set(hash01(o.name,301+i)*2.4,hash01(o.name,321+i)*2.4,hash01(o.name,341+i)*2.4);
|
|
1535
|
+
chunk.castShadow=true; chunk.receiveShadow=true; chunk.userData=o; group.add(chunk);
|
|
1536
|
+
chunk.material.transparent=true;
|
|
1537
|
+
corruptionFades.push({
|
|
1538
|
+
mesh:chunk,
|
|
1539
|
+
material:chunk.material,
|
|
1540
|
+
damagePos:chunk.position.clone(),
|
|
1541
|
+
cleanPos:new THREE.Vector3(0,Math.max(TRAY_TOP+4,Math.min(TRAY_TOP+h-5,baseY)),0),
|
|
1542
|
+
baseScale:chunk.scale.clone(),
|
|
1543
|
+
});
|
|
1544
|
+
if(i===0 && h>42) corruptionFloaters.push({mesh:chunk,baseY,phase:damage.phase,speed:0.65+hash01(o.name,361)*0.55});
|
|
905
1545
|
}
|
|
906
1546
|
group.userData=o; scene.add(group); return group;
|
|
907
1547
|
}
|
|
908
1548
|
// repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
|
|
909
|
-
//
|
|
1549
|
+
// setup mode uses the selected city metric; share-card mode keeps the original tokens + $ poster readout.
|
|
1550
|
+
let cityMetric=setupMode ? 'time' : 'tokens';
|
|
1551
|
+
let timelineProgress=1;
|
|
1552
|
+
const repoLabels=[];
|
|
1553
|
+
function fmtMetric(hours, tokens, cost){
|
|
1554
|
+
if(cityMetric==='tokens') return fmtTok(tokens);
|
|
1555
|
+
if(cityMetric==='cost') return '$'+Math.round(cost).toLocaleString('en-US');
|
|
1556
|
+
return `${(Number(hours)||0).toFixed(1)}h`;
|
|
1557
|
+
}
|
|
1558
|
+
function repoMetricText(o){
|
|
1559
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1560
|
+
return fmtMetric((Number(o.hours)||0)*fraction,(Number(o.tokens)||0)*fraction,(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE));
|
|
1561
|
+
}
|
|
1562
|
+
// GitHub octocat mark (24x24) — same glyph as the report's repo chips.
|
|
1563
|
+
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');
|
|
1564
|
+
function drawRepoLabel(e){
|
|
1565
|
+
const {o, g, tex, PW, PH, numbers}=e;
|
|
1566
|
+
g.clearRect(0,0,PW,PH);
|
|
1567
|
+
g.textAlign='center';
|
|
1568
|
+
if(numbers){
|
|
1569
|
+
// dark ink + white halo: stays readable over the light grid AND the coloured roofs
|
|
1570
|
+
g.save();
|
|
1571
|
+
g.shadowColor='rgba(255,255,255,0.92)'; g.shadowBlur=12; g.shadowOffsetY=0;
|
|
1572
|
+
g.fillStyle='#171717'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
1573
|
+
g.fillText(setupMode ? repoMetricText(o) : fmtTok(o.tokens), PW/2, setupMode ? PH*0.66 : PH*0.18);
|
|
1574
|
+
if(!setupMode){
|
|
1575
|
+
g.fillStyle='rgba(23,23,23,0.85)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
1576
|
+
g.fillText('$'+Math.round(o.cost*COST_SCALE).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
1577
|
+
}
|
|
1578
|
+
g.restore();
|
|
1579
|
+
}
|
|
1580
|
+
// repo name rides a white chip with the octocat, matching .audit-repo-chip in the report
|
|
1581
|
+
const fs=Math.round(PW*0.075);
|
|
1582
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1583
|
+
const icon=Math.round(fs*1.25), gap=Math.round(fs*0.45), padX=Math.round(fs*0.7);
|
|
1584
|
+
const tw=g.measureText(o.name).width;
|
|
1585
|
+
const maxText=PW*0.94-icon-gap-padX*2;
|
|
1586
|
+
const drawTw=Math.min(tw,maxText);
|
|
1587
|
+
const chipW=drawTw+icon+gap+padX*2;
|
|
1588
|
+
const chipH=Math.round(icon+fs*0.85);
|
|
1589
|
+
const cx=PW/2, cyMid=PH*0.86-fs*0.35;
|
|
1590
|
+
const rx=cx-chipW/2, ry=cyMid-chipH/2;
|
|
1591
|
+
g.save();
|
|
1592
|
+
g.shadowColor='rgba(31,35,40,0.25)'; g.shadowBlur=8; g.shadowOffsetY=2;
|
|
1593
|
+
g.beginPath();
|
|
1594
|
+
if(g.roundRect) g.roundRect(rx,ry,chipW,chipH,chipH/2); else g.rect(rx,ry,chipW,chipH);
|
|
1595
|
+
g.fillStyle='rgba(255,255,255,0.96)'; g.fill();
|
|
1596
|
+
g.shadowColor='transparent';
|
|
1597
|
+
g.lineWidth=1.5; g.strokeStyle='rgba(24,23,23,0.32)'; g.stroke();
|
|
1598
|
+
g.translate(rx+padX, cyMid-icon/2); g.scale(icon/24, icon/24);
|
|
1599
|
+
g.fillStyle='#181717'; g.fill(GH_MARK);
|
|
1600
|
+
g.restore();
|
|
1601
|
+
g.save();
|
|
1602
|
+
g.fillStyle='#171717'; g.textAlign='left'; g.textBaseline='middle';
|
|
1603
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1604
|
+
g.translate(rx+padX+icon+gap, cyMid+fs*0.06);
|
|
1605
|
+
if(tw>maxText) g.scale(maxText/tw,1);
|
|
1606
|
+
g.fillText(o.name,0,0);
|
|
1607
|
+
g.restore();
|
|
1608
|
+
tex.needsUpdate=true;
|
|
1609
|
+
}
|
|
910
1610
|
function addTopLabel(o, numbers=true, lift=0){
|
|
911
1611
|
const PW=320, PH=280;
|
|
912
1612
|
const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
|
|
913
|
-
const g=cv.getContext('2d');
|
|
914
|
-
g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
|
|
915
|
-
if(numbers){
|
|
916
|
-
// tokens — the headline, flies highest
|
|
917
|
-
g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
918
|
-
g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
|
|
919
|
-
// dollars — just under the tokens, also up high
|
|
920
|
-
g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
921
|
-
g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
922
|
-
}
|
|
923
|
-
// name — small tag sitting just above the bar top
|
|
924
|
-
g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
|
|
925
|
-
const fw=g.measureText(o.name).width, maxw=PW*0.94;
|
|
926
|
-
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();
|
|
1613
|
+
const g=cv.getContext('2d');
|
|
927
1614
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
928
1615
|
const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
|
|
929
1616
|
const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
|
|
1617
|
+
const labelOffset=-3+6+0.36*hWorld+lift;
|
|
930
1618
|
sprite.scale.set(wWorld, hWorld, 1);
|
|
931
|
-
sprite.position.set(o.x, TRAY_TOP + o.h
|
|
1619
|
+
sprite.position.set(o.x, TRAY_TOP + o.h + labelOffset, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
|
|
932
1620
|
scene.add(sprite);
|
|
1621
|
+
const entry={o, g, tex, PW, PH, sprite, numbers, labelOffset};
|
|
1622
|
+
repoLabels.push(entry);
|
|
1623
|
+
drawRepoLabel(entry);
|
|
933
1624
|
}
|
|
934
1625
|
// fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
|
|
935
1626
|
const grain=(function(){
|
|
@@ -944,9 +1635,9 @@ const grain=(function(){
|
|
|
944
1635
|
})();
|
|
945
1636
|
const trayGeo=new RoundedBoxGeometry(trayW, TRAY_TOP, trayD, 8, 12);
|
|
946
1637
|
const tray=new THREE.Mesh(trayGeo, new THREE.MeshPhysicalMaterial({
|
|
947
|
-
color:TRAY_COL, roughness:0.
|
|
948
|
-
clearcoat:0.
|
|
949
|
-
bumpMap:grain, bumpScale:0.
|
|
1638
|
+
color:TRAY_COL, roughness:0.84, metalness:0.02,
|
|
1639
|
+
clearcoat:0.08, clearcoatRoughness:0.82, envMapIntensity:0.4,
|
|
1640
|
+
bumpMap:grain, bumpScale:0.9,
|
|
950
1641
|
}));
|
|
951
1642
|
tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=true; scene.add(tray);
|
|
952
1643
|
// grid + perimeter border on ONE decal: the grid is CLIPPED to the border's rounded rect, so
|
|
@@ -973,6 +1664,37 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
|
|
|
973
1664
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
974
1665
|
const plane=new THREE.Mesh(new THREE.PlaneGeometry(trayW, trayD), new THREE.MeshBasicMaterial({map:tex, transparent:true, depthWrite:false, toneMapped:false}));
|
|
975
1666
|
plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
|
|
1667
|
+
|
|
1668
|
+
// Keep corruption on its own decal so the same city can visibly heal without losing its clean grid.
|
|
1669
|
+
const damageCv=document.createElement('canvas'); damageCv.width=W; damageCv.height=H;
|
|
1670
|
+
const dg=damageCv.getContext('2d');
|
|
1671
|
+
dg.save(); dg.beginPath(); dg.roundRect(m,m,rw,rh,rad); dg.clip();
|
|
1672
|
+
let seed=0xdecafbad;
|
|
1673
|
+
const rnd=()=>{ seed=(Math.imul(seed,1664525)+1013904223)>>>0; return seed/4294967296; };
|
|
1674
|
+
for(let i=0;i<16;i++){
|
|
1675
|
+
const x=m+rnd()*rw, y=m+rnd()*rh, ww=(18+rnd()*54)*scale, hh=(8+rnd()*24)*scale;
|
|
1676
|
+
dg.save(); dg.translate(x,y); dg.rotate((rnd()-0.5)*0.7);
|
|
1677
|
+
dg.fillStyle=i%3===0?'rgba(170,59,51,0.18)':'rgba(43,43,49,0.16)';
|
|
1678
|
+
dg.fillRect(-ww/2,-hh/2,ww,hh); dg.restore();
|
|
1679
|
+
}
|
|
1680
|
+
dg.lineCap='square';
|
|
1681
|
+
for(let i=0;i<22;i++){
|
|
1682
|
+
let x=m+rnd()*rw, y=m+rnd()*rh;
|
|
1683
|
+
dg.strokeStyle=i%5===0?'rgba(176,58,51,0.62)':'rgba(45,44,49,0.55)';
|
|
1684
|
+
dg.lineWidth=(0.9+rnd()*1.5)*scale; dg.beginPath(); dg.moveTo(x,y);
|
|
1685
|
+
const steps=2+Math.floor(rnd()*4);
|
|
1686
|
+
for(let j=0;j<steps;j++){
|
|
1687
|
+
x+=((rnd()-0.5)*42)*scale; y+=((rnd()-0.5)*42)*scale; dg.lineTo(x,y);
|
|
1688
|
+
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); }
|
|
1689
|
+
}
|
|
1690
|
+
dg.stroke();
|
|
1691
|
+
}
|
|
1692
|
+
dg.restore();
|
|
1693
|
+
const damageTex=new THREE.CanvasTexture(damageCv); damageTex.colorSpace=THREE.SRGBColorSpace; damageTex.anisotropy=8;
|
|
1694
|
+
const damageMat=new THREE.MeshBasicMaterial({map:damageTex,transparent:true,depthWrite:false,toneMapped:false});
|
|
1695
|
+
const damagePlane=new THREE.Mesh(new THREE.PlaneGeometry(trayW,trayD),damageMat);
|
|
1696
|
+
damagePlane.rotation.x=-Math.PI/2; damagePlane.position.set(CX,TRAY_TOP+0.72,CZ); scene.add(damagePlane);
|
|
1697
|
+
corruptionSurfaces.push(damageMat);
|
|
976
1698
|
})();
|
|
977
1699
|
|
|
978
1700
|
// De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
|
|
@@ -982,9 +1704,9 @@ const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0
|
|
|
982
1704
|
const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
|
|
983
1705
|
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); });
|
|
984
1706
|
|
|
985
|
-
// bottom-right standing "signpost": tray summary
|
|
1707
|
+
// bottom-right standing "signpost": tray summary follows the selected city metric.
|
|
986
1708
|
let signpostCanvas=null, signpostTexture=null;
|
|
987
|
-
function drawSignpost(
|
|
1709
|
+
function drawSignpost(bigText, subLabel, bottomText, accent='#e8473f'){
|
|
988
1710
|
if(!signpostCanvas) return;
|
|
989
1711
|
const W=signpostCanvas.width, H=signpostCanvas.height;
|
|
990
1712
|
const g=signpostCanvas.getContext('2d');
|
|
@@ -992,18 +1714,44 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
992
1714
|
g.fillStyle='rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.fill();
|
|
993
1715
|
g.strokeStyle='rgba(120,126,140,0.30)'; g.lineWidth=3; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.stroke();
|
|
994
1716
|
g.textAlign='center';
|
|
995
|
-
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(
|
|
996
|
-
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(
|
|
1717
|
+
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(bigText, W/2, 150);
|
|
1718
|
+
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(subLabel, W/2, 204);
|
|
997
1719
|
g.fillStyle=accent; g.font='800 62px "IBM Plex Mono", monospace'; g.fillText(bottomText, W/2, 280);
|
|
998
1720
|
if(signpostTexture) signpostTexture.needsUpdate=true;
|
|
999
1721
|
}
|
|
1722
|
+
function signpostBig(){
|
|
1723
|
+
if(setupMode && cityComparison==='with') return cityMetricReadout().value;
|
|
1724
|
+
if(timelineProgress>=0.999) return cityMetric==='tokens'?signpostTokens : cityMetric==='cost'?signpostCost : signpostHours;
|
|
1725
|
+
const totals=all.reduce((sum,o)=>{
|
|
1726
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1727
|
+
sum.tokens+=(Number(o.tokens)||0)*fraction;
|
|
1728
|
+
sum.cost+=(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE);
|
|
1729
|
+
sum.hours+=(Number(o.hours)||0)*fraction;
|
|
1730
|
+
return sum;
|
|
1731
|
+
},{tokens:0,cost:0,hours:0});
|
|
1732
|
+
return cityMetric==='tokens'?fmtTok(totals.tokens):cityMetric==='cost'?money(totals.cost):`${totals.hours.toFixed(1)}h`;
|
|
1733
|
+
}
|
|
1734
|
+
function signpostSub(){
|
|
1735
|
+
if(setupMode && cityComparison==='with') return cityMetric==='tokens'?'MODELED TOKENS' : cityMetric==='cost'?'MODELED API COST' : 'MODELED TIME';
|
|
1736
|
+
return cityMetric==='tokens'?'TOTAL TOKENS' : cityMetric==='cost'?'TOTAL SPENT' : 'TOTAL TIME';
|
|
1737
|
+
}
|
|
1738
|
+
function signpostBottom(){
|
|
1739
|
+
if(cityComparison==='with') return `${comparisonAfter} CONTEXT SCORE`;
|
|
1740
|
+
if(timelineProgress<0.999) return 'HISTORY AT DATE';
|
|
1741
|
+
return cityMetric==='tokens'?signpostWastedPct : cityMetric==='cost'?signpostWastedCost : signpostWastedTime;
|
|
1742
|
+
}
|
|
1743
|
+
function redrawCityMetric(){
|
|
1744
|
+
repoLabels.forEach(drawRepoLabel);
|
|
1745
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom(),cityComparison==='with'?'#2a8f7d':'#e8473f');
|
|
1746
|
+
dirty=true;
|
|
1747
|
+
}
|
|
1000
1748
|
(function(){
|
|
1001
1749
|
const W=640, H=320;
|
|
1002
1750
|
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
1003
1751
|
signpostCanvas=cv;
|
|
1004
1752
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1005
1753
|
signpostTexture=tex;
|
|
1006
|
-
drawSignpost(
|
|
1754
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
1007
1755
|
const SIGN_W=152, SIGN_H=76, SIGN_Y=TRAY_TOP+64;
|
|
1008
1756
|
const sign=new THREE.Mesh(
|
|
1009
1757
|
new THREE.PlaneGeometry(SIGN_W, SIGN_H),
|
|
@@ -1020,6 +1768,114 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
1020
1768
|
post.position.set(sx, TRAY_TOP+postH/2, sz); post.castShadow=true; scene.add(post);
|
|
1021
1769
|
})();
|
|
1022
1770
|
|
|
1771
|
+
(function(){
|
|
1772
|
+
const ct=document.getElementById('city-toggle');
|
|
1773
|
+
if(!ct) return;
|
|
1774
|
+
const thumb=ct.querySelector('.ct-thumb');
|
|
1775
|
+
const moveThumb=(btn)=>{ if(!thumb||!btn) return;
|
|
1776
|
+
thumb.style.width=btn.offsetWidth+'px'; thumb.style.height=btn.offsetHeight+'px';
|
|
1777
|
+
thumb.style.transform=`translate(${btn.offsetLeft}px, ${btn.offsetTop}px)`;
|
|
1778
|
+
};
|
|
1779
|
+
requestAnimationFrame(()=>{
|
|
1780
|
+
const on=ct.querySelector('button.on')||ct.querySelector('button[data-metric]');
|
|
1781
|
+
if(thumb) thumb.style.transition='none';
|
|
1782
|
+
moveThumb(on);
|
|
1783
|
+
requestAnimationFrame(()=>{ if(thumb) thumb.style.transition=''; });
|
|
1784
|
+
});
|
|
1785
|
+
ct.addEventListener('click', (e)=>{
|
|
1786
|
+
const b=e.target.closest('button[data-metric]'); if(!b) return;
|
|
1787
|
+
cityMetric=b.getAttribute('data-metric') || 'time';
|
|
1788
|
+
ct.querySelectorAll('button').forEach((x)=>x.classList.toggle('on', x===b));
|
|
1789
|
+
moveThumb(b);
|
|
1790
|
+
redrawCityMetric();
|
|
1791
|
+
updateFieldReportMetric();
|
|
1792
|
+
});
|
|
1793
|
+
})();
|
|
1794
|
+
|
|
1795
|
+
function setCityComparison(mode){
|
|
1796
|
+
cityComparison=mode==='with'?'with':'without';
|
|
1797
|
+
repairTarget=cityComparison==='with'?1:0;
|
|
1798
|
+
document.body.classList.toggle('with-echo',cityComparison==='with');
|
|
1799
|
+
updateFieldReportMetric();
|
|
1800
|
+
redrawCityMetric();
|
|
1801
|
+
dirty=true;
|
|
1802
|
+
}
|
|
1803
|
+
setCityComparison('without');
|
|
1804
|
+
// Parent page (setup report fullscreen modal) drives the dirty/clean comparison remotely.
|
|
1805
|
+
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();}});
|
|
1806
|
+
|
|
1807
|
+
const timelineStartMs=Date.parse(setupReport?.scanStartDate||'2024-12-01T00:00:00Z');
|
|
1808
|
+
const timelineEndMs=Math.max(timelineStartMs+86400000,Date.parse(setupReport?.scanEndDate||new Date().toISOString()));
|
|
1809
|
+
function repoFractionAt(o,cutoffMs){
|
|
1810
|
+
if(timelineProgress>=0.999) return 1;
|
|
1811
|
+
if(!Array.isArray(o.daily)||!o.daily.length) return timelineProgress;
|
|
1812
|
+
let total=0,visible=0;
|
|
1813
|
+
for(const point of o.daily){
|
|
1814
|
+
if(!Array.isArray(point)||point.length<2) continue;
|
|
1815
|
+
const amount=Math.max(0,Number(point[1])||0);
|
|
1816
|
+
const day=Date.parse(String(point[0])+'T23:59:59Z');
|
|
1817
|
+
total+=amount;
|
|
1818
|
+
if(Number.isFinite(day)&&day<=cutoffMs) visible+=amount;
|
|
1819
|
+
}
|
|
1820
|
+
return total>0?Math.max(0,Math.min(1,visible/total)):timelineProgress;
|
|
1821
|
+
}
|
|
1822
|
+
function setTimelineProgress(value){
|
|
1823
|
+
timelineProgress=Math.max(0,Math.min(1,Number(value)||0));
|
|
1824
|
+
const cutoffMs=timelineStartMs+(timelineEndMs-timelineStartMs)*timelineProgress;
|
|
1825
|
+
const handle=document.querySelector('#city-timeline .tl-handle');
|
|
1826
|
+
const progress=document.querySelector('#city-timeline .tl-progress');
|
|
1827
|
+
const selected=document.querySelector('#city-timeline .tl-selected');
|
|
1828
|
+
const pct=timelineProgress*100;
|
|
1829
|
+
if(handle){ handle.style.left=`${pct}%`; handle.setAttribute('aria-valuenow',String(Math.round(pct))); }
|
|
1830
|
+
if(progress) progress.style.width=`${pct}%`;
|
|
1831
|
+
if(selected){
|
|
1832
|
+
selected.style.left=`${Math.max(7,Math.min(93,pct))}%`;
|
|
1833
|
+
selected.textContent=timelineProgress>=0.995?'Today':new Date(cutoffMs).toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',timeZone:'UTC'});
|
|
1834
|
+
}
|
|
1835
|
+
for(const o of all){
|
|
1836
|
+
const fraction=repoFractionAt(o,cutoffMs);
|
|
1837
|
+
o.timelineFraction=fraction;
|
|
1838
|
+
if(o.mesh){
|
|
1839
|
+
o.mesh.visible=fraction>0.002;
|
|
1840
|
+
o.mesh.scale.y=Math.max(0.001,fraction);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
for(const entry of repoLabels){
|
|
1844
|
+
const fraction=Number.isFinite(entry.o.timelineFraction)?entry.o.timelineFraction:1;
|
|
1845
|
+
entry.sprite.visible=fraction>0.08;
|
|
1846
|
+
entry.sprite.material.opacity=Math.max(0,Math.min(1,(fraction-0.08)/0.18));
|
|
1847
|
+
entry.sprite.position.y=TRAY_TOP+entry.o.h*fraction+entry.labelOffset;
|
|
1848
|
+
}
|
|
1849
|
+
redrawCityMetric();
|
|
1850
|
+
key.shadow.needsUpdate=true;
|
|
1851
|
+
dirty=true;
|
|
1852
|
+
}
|
|
1853
|
+
(function(){
|
|
1854
|
+
const timeline=document.getElementById('city-timeline');
|
|
1855
|
+
const track=timeline?.querySelector('.tl-track');
|
|
1856
|
+
const handle=timeline?.querySelector('.tl-handle');
|
|
1857
|
+
if(!timeline||!track||!handle) return;
|
|
1858
|
+
let dragging=false;
|
|
1859
|
+
const updateFromPointer=(event)=>{
|
|
1860
|
+
const rect=track.getBoundingClientRect();
|
|
1861
|
+
setTimelineProgress((event.clientX-rect.left)/Math.max(1,rect.width));
|
|
1862
|
+
};
|
|
1863
|
+
track.addEventListener('pointerdown',(event)=>{
|
|
1864
|
+
dragging=true; timeline.classList.add('dragging'); updateFromPointer(event); event.preventDefault();
|
|
1865
|
+
});
|
|
1866
|
+
window.addEventListener('pointermove',(event)=>{ if(dragging){ updateFromPointer(event); event.preventDefault(); } });
|
|
1867
|
+
window.addEventListener('pointerup',()=>{ dragging=false; timeline.classList.remove('dragging'); });
|
|
1868
|
+
handle.addEventListener('keydown',(event)=>{
|
|
1869
|
+
if(event.key==='ArrowLeft'||event.key==='ArrowDown') setTimelineProgress(timelineProgress-0.025);
|
|
1870
|
+
else if(event.key==='ArrowRight'||event.key==='ArrowUp') setTimelineProgress(timelineProgress+0.025);
|
|
1871
|
+
else if(event.key==='Home') setTimelineProgress(0);
|
|
1872
|
+
else if(event.key==='End') setTimelineProgress(1);
|
|
1873
|
+
else return;
|
|
1874
|
+
event.preventDefault();
|
|
1875
|
+
});
|
|
1876
|
+
setTimelineProgress(1);
|
|
1877
|
+
})();
|
|
1878
|
+
|
|
1023
1879
|
const MINZ=0.4, MAXZ=2.8; // lower floor → can zoom out to ~2/3 of the previous limit
|
|
1024
1880
|
const controls=new OrbitControls(camera, renderer.domElement);
|
|
1025
1881
|
controls.target.copy(TARGET);
|
|
@@ -1027,16 +1883,63 @@ controls.enableDamping=true; controls.dampingFactor=0.08; controls.enablePan=fal
|
|
|
1027
1883
|
controls.minPolarAngle=0.18; controls.maxPolarAngle=Math.PI*0.47; controls.rotateSpeed=0.85;
|
|
1028
1884
|
controls.update();
|
|
1029
1885
|
let targetZoom=camera.zoom;
|
|
1886
|
+
// Hover auto-focus: wheel + drag behave differently over the city, so cue it —
|
|
1887
|
+
// the camera eases into a slight zoom when the pointer enters, back out on leave.
|
|
1888
|
+
let hoverFocus=1, hoverFocusTarget=1;
|
|
1889
|
+
renderer.domElement.addEventListener('pointerenter',()=>{ hoverFocusTarget=1.05; });
|
|
1890
|
+
renderer.domElement.addEventListener('pointerleave',()=>{ hoverFocusTarget=1; });
|
|
1030
1891
|
renderer.domElement.addEventListener('wheel', (e)=>{
|
|
1031
1892
|
e.preventDefault();
|
|
1032
1893
|
const dy=e.deltaY * (e.deltaMode===1 ? 16 : 1);
|
|
1033
1894
|
targetZoom=Math.min(MAXZ, Math.max(MINZ, targetZoom * Math.pow(0.999, dy)));
|
|
1034
1895
|
}, {passive:false});
|
|
1035
1896
|
|
|
1036
|
-
let dirty=true;
|
|
1037
1897
|
function render(){ renderer.render(scene,camera); }
|
|
1038
1898
|
function loop(){ requestAnimationFrame(loop); let need=controls.update();
|
|
1039
|
-
|
|
1899
|
+
let shadowNeedsUpdate=false;
|
|
1900
|
+
if(Math.abs(hoverFocusTarget-hoverFocus)>0.0004){ hoverFocus+=(hoverFocusTarget-hoverFocus)*0.1; if(Math.abs(hoverFocusTarget-hoverFocus)<0.0005) hoverFocus=hoverFocusTarget; }
|
|
1901
|
+
const zoomGoal=Math.min(MAXZ, Math.max(MINZ, targetZoom*hoverFocus));
|
|
1902
|
+
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; }
|
|
1903
|
+
if(Math.abs(repairTarget-repairProgress)>0.0005){
|
|
1904
|
+
repairProgress+=(repairTarget-repairProgress)*0.085;
|
|
1905
|
+
if(Math.abs(repairTarget-repairProgress)<0.001) repairProgress=repairTarget;
|
|
1906
|
+
need=true; shadowNeedsUpdate=true;
|
|
1907
|
+
}
|
|
1908
|
+
const repairEase=repairProgress*repairProgress*(3-2*repairProgress);
|
|
1909
|
+
for(const entry of repairGroups){
|
|
1910
|
+
entry.group.rotation.x=entry.damageX*(1-repairEase);
|
|
1911
|
+
entry.group.rotation.z=entry.damageZ*(1-repairEase);
|
|
1912
|
+
}
|
|
1913
|
+
for(const entry of repairPieces){
|
|
1914
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1915
|
+
entry.mesh.rotation.y=entry.damageRotY*(1-repairEase);
|
|
1916
|
+
}
|
|
1917
|
+
for(const entry of buildingMaterials){
|
|
1918
|
+
entry.material.color.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1919
|
+
entry.material.attenuationColor.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1920
|
+
entry.material.roughness=0.9-(0.2*repairEase);
|
|
1921
|
+
entry.material.transmission=0.05+(0.11*repairEase);
|
|
1922
|
+
entry.material.clearcoat=0.015+(0.035*repairEase);
|
|
1923
|
+
entry.material.clearcoatRoughness=0.94-(0.14*repairEase);
|
|
1924
|
+
entry.material.envMapIntensity=0.24+(0.21*repairEase);
|
|
1925
|
+
}
|
|
1926
|
+
for(const entry of corruptionFades){
|
|
1927
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1928
|
+
const scale=1-repairEase*0.92;
|
|
1929
|
+
entry.mesh.scale.copy(entry.baseScale).multiplyScalar(scale);
|
|
1930
|
+
entry.material.opacity=1-repairEase;
|
|
1931
|
+
entry.mesh.visible=repairEase<0.985;
|
|
1932
|
+
}
|
|
1933
|
+
for(const material of corruptionSurfaces) material.opacity=1-repairEase;
|
|
1934
|
+
const now=performance.now()*0.001;
|
|
1935
|
+
for(const floater of corruptionFloaters){
|
|
1936
|
+
if(repairEase<0.985){
|
|
1937
|
+
floater.mesh.position.y+=Math.sin(now*floater.speed+floater.phase)*3.2*(1-repairEase);
|
|
1938
|
+
floater.mesh.rotation.y+=0.0025*floater.speed*(1-repairEase);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if(corruptionFloaters.length && repairEase<0.985) need=true;
|
|
1942
|
+
if(shadowNeedsUpdate && (++shadowFrame%3===0 || repairProgress===repairTarget)) key.shadow.needsUpdate=true;
|
|
1040
1943
|
if(need||dirty){ render(); dirty=false; }
|
|
1041
1944
|
}
|
|
1042
1945
|
loop();
|
|
@@ -1045,7 +1948,7 @@ loop();
|
|
|
1045
1948
|
const cardEl=document.querySelector('.share-card');
|
|
1046
1949
|
function fit(){
|
|
1047
1950
|
if(setupMode){
|
|
1048
|
-
VIZ_W=window.innerWidth; VIZ_H=window.innerHeight;
|
|
1951
|
+
VIZ_W=figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620)); VIZ_H=window.innerHeight;
|
|
1049
1952
|
cardEl.style.transform='none';
|
|
1050
1953
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1051
1954
|
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
@@ -1063,51 +1966,79 @@ function fit(){
|
|
|
1063
1966
|
fit();
|
|
1064
1967
|
window.addEventListener('resize', fit);
|
|
1065
1968
|
|
|
1066
|
-
// Loading lives inside the report:
|
|
1067
|
-
//
|
|
1969
|
+
// Loading lives inside the report: reveal only after a provenance-checked report has
|
|
1970
|
+
// populated at least one real repo. Empty, failed, and invalid setup states stay closed.
|
|
1068
1971
|
function finishSetupReveal(){
|
|
1972
|
+
document.querySelectorAll('[data-setup-pending-hidden]').forEach((el)=>{ el.removeAttribute('aria-hidden'); el.removeAttribute('data-setup-pending-hidden'); });
|
|
1069
1973
|
document.body.classList.remove('scanning');
|
|
1070
1974
|
}
|
|
1071
|
-
if(setupMode){
|
|
1975
|
+
if(setupMode && setupReport && setupLiveRepos.length && all.length){
|
|
1072
1976
|
requestAnimationFrame(()=>requestAnimationFrame(finishSetupReveal));
|
|
1073
1977
|
window.setTimeout(finishSetupReveal, 1200);
|
|
1074
1978
|
}
|
|
1075
1979
|
|
|
1076
|
-
|
|
1980
|
+
(function(){
|
|
1981
|
+
const btn=document.getElementById('fr-read-btn');
|
|
1982
|
+
const panel=document.getElementById('fr-persona-hover');
|
|
1983
|
+
if(!btn || !panel) return;
|
|
1984
|
+
const setOpen=(open)=>{
|
|
1985
|
+
panel.classList.toggle('open', open);
|
|
1986
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
1987
|
+
};
|
|
1988
|
+
btn.addEventListener('click', (event)=>{
|
|
1989
|
+
event.stopPropagation();
|
|
1990
|
+
setOpen(!panel.classList.contains('open'));
|
|
1991
|
+
});
|
|
1992
|
+
document.addEventListener('click', (event)=>{
|
|
1993
|
+
if(event.target!==btn && !panel.contains(event.target)) setOpen(false);
|
|
1994
|
+
});
|
|
1995
|
+
window.addEventListener('keydown', (event)=>{ if(event.key==='Escape') setOpen(false); });
|
|
1996
|
+
})();
|
|
1997
|
+
|
|
1998
|
+
document.querySelectorAll('#setup-connect,#v7-connect,#fr-connect').forEach((connectButton)=>{
|
|
1077
1999
|
connectButton.addEventListener('click', (event)=>{
|
|
1078
2000
|
event.stopPropagation();
|
|
1079
2001
|
if(setupMode) window.parent?.postMessage({type:'echomem:connect-click'}, window.location.origin);
|
|
1080
2002
|
});
|
|
1081
2003
|
});
|
|
1082
2004
|
|
|
1083
|
-
// ---- rotating
|
|
2005
|
+
// ---- rotating context-health readout: HUD score, share, time, and API-equivalent waste ----
|
|
1084
2006
|
const parseScalar=(value)=>{
|
|
1085
2007
|
const match=String(value||'').match(/[0-9][0-9,]*(?:\.[0-9]+)?/);
|
|
1086
2008
|
return match ? Number(match[0].replace(/,/g,''))||0 : 0;
|
|
1087
2009
|
};
|
|
1088
2010
|
const formatWasteMoney=(value)=>`$${Math.round(value).toLocaleString('en-US')}`;
|
|
1089
|
-
function buildMetricSlides(dirtyPct, attentionHours, computeCost){
|
|
1090
|
-
const
|
|
2011
|
+
function buildMetricSlides(dirtyPct, attentionHours, computeCost, report){
|
|
2012
|
+
const wastePct=goldenWastePct(report);
|
|
2013
|
+
const usefulPct=goldenUsefulPct(report);
|
|
2014
|
+
const dirtyShare=Math.max(0, Number.isFinite(wastePct) ? wastePct : parseScalar(dirtyPct))/100;
|
|
1091
2015
|
const wastedHours=Math.max(0, parseScalar(attentionHours))*dirtyShare;
|
|
1092
2016
|
const wastedDays=wastedHours/8;
|
|
1093
2017
|
const wastedCost=Math.max(0, parseScalar(computeCost))*dirtyShare;
|
|
1094
|
-
|
|
2018
|
+
const slides=[];
|
|
2019
|
+
slides.push(
|
|
2020
|
+
{
|
|
2021
|
+
value:`${oneDec(wastePct)}%`,
|
|
2022
|
+
label:'wasteful context',
|
|
2023
|
+
say:goldenWasteSay(report)
|
|
2024
|
+
},
|
|
1095
2025
|
{
|
|
1096
|
-
value:`${
|
|
1097
|
-
label:'
|
|
1098
|
-
say:
|
|
2026
|
+
value:`${oneDec(usefulPct)}%`,
|
|
2027
|
+
label:'useful context',
|
|
2028
|
+
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.'
|
|
1099
2029
|
},
|
|
1100
2030
|
{
|
|
1101
2031
|
value:`≈${Math.round(wastedHours)}h`,
|
|
1102
|
-
label:'
|
|
1103
|
-
say:`That is roughly <b>${wastedDays.toFixed(1)} workdays</b> of agent attention
|
|
2032
|
+
label:'waste-weighted time',
|
|
2033
|
+
say:`That is roughly <b>${wastedDays.toFixed(1)} workdays</b> of agent attention weighted by the golden-standard wasteful-context share.`
|
|
1104
2034
|
},
|
|
1105
2035
|
{
|
|
1106
2036
|
value:`≈${formatWasteMoney(wastedCost)}`,
|
|
1107
2037
|
label:'API-equiv waste',
|
|
1108
|
-
say:`At list API-equivalent rates,
|
|
2038
|
+
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.`
|
|
1109
2039
|
},
|
|
1110
|
-
|
|
2040
|
+
);
|
|
2041
|
+
return slides;
|
|
1111
2042
|
}
|
|
1112
2043
|
function bindMetricCarousel({metric,value,label,say,slides}){
|
|
1113
2044
|
if(!metric || !value || !label || !say || !slides?.length) return;
|
|
@@ -1124,7 +2055,7 @@ function bindMetricCarousel({metric,value,label,say,slides}){
|
|
|
1124
2055
|
say.classList.remove('is-swapping');
|
|
1125
2056
|
}, 520);
|
|
1126
2057
|
}
|
|
1127
|
-
if(slides.length>1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
|
|
2058
|
+
if(!setupMode && slides.length>1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
|
|
1128
2059
|
window.setInterval(()=>{
|
|
1129
2060
|
index=(index+1)%slides.length;
|
|
1130
2061
|
show(index);
|
|
@@ -1137,11 +2068,40 @@ bindMetricCarousel({
|
|
|
1137
2068
|
label:document.getElementById('v7-metric-label'),
|
|
1138
2069
|
say:document.getElementById('v7-metric-say'),
|
|
1139
2070
|
slides:buildMetricSlides(
|
|
1140
|
-
|
|
2071
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
1141
2072
|
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
1142
|
-
document.getElementById('v7-cost')?.textContent || '$13,689'
|
|
2073
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2074
|
+
setupReport
|
|
1143
2075
|
),
|
|
1144
2076
|
});
|
|
2077
|
+
bindMetricCarousel({
|
|
2078
|
+
metric:document.getElementById('fr-metric'),
|
|
2079
|
+
value:document.getElementById('fr-metric-value'),
|
|
2080
|
+
label:document.getElementById('fr-metric-label'),
|
|
2081
|
+
say:document.getElementById('fr-metric-say'),
|
|
2082
|
+
slides:buildMetricSlides(
|
|
2083
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
2084
|
+
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
2085
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2086
|
+
setupReport
|
|
2087
|
+
),
|
|
2088
|
+
});
|
|
2089
|
+
function reserveFrSayHeight(){
|
|
2090
|
+
if(!setupMode) return;
|
|
2091
|
+
const say=document.getElementById('fr-metric-say'); if(!say) return;
|
|
2092
|
+
const slides=buildMetricSlides(
|
|
2093
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
2094
|
+
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
2095
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2096
|
+
setupReport
|
|
2097
|
+
);
|
|
2098
|
+
const prev=say.innerHTML; say.style.minHeight='0px'; let max=0;
|
|
2099
|
+
for(const slide of slides){ say.innerHTML=slide.say; max=Math.max(max, say.offsetHeight); }
|
|
2100
|
+
say.innerHTML=prev;
|
|
2101
|
+
if(max>0) say.style.minHeight=max+'px';
|
|
2102
|
+
}
|
|
2103
|
+
reserveFrSayHeight();
|
|
2104
|
+
window.addEventListener('resize', reserveFrSayHeight);
|
|
1145
2105
|
|
|
1146
2106
|
// ---- repeated-reads info sheet: v7 "how we read your city" disclosure, moved behind the info button ----
|
|
1147
2107
|
const v7InfoBtns=Array.from(document.querySelectorAll('[aria-controls="v7-info-sheet"]'));
|