@echomem/mcp 1.4.6 → 1.4.8
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 +14 -8
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +1734 -0
- package/dist/city/10-problems-report.html +649 -0
- package/dist/city/echo-ai-city-only.html +63 -653
- package/dist/city/echo-ai-city-only.template.html +35 -17
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/forensics-10-problems.js +632 -0
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +229 -22
- package/dist/hud/monitor.js +36 -68
- package/dist/hud/preload.cjs +12 -0
- package/dist/hud/server.js +413 -3
- package/dist/hud/web.js +713 -74
- package/dist/index.js +355 -28
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +353 -40
- package/dist/v1-contract.js +154 -5
- package/package.json +3 -2
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<title>I scanned your AI coding city — Echo</title>
|
|
6
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
7
|
-
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600;700&display=swap" rel="stylesheet">
|
|
8
6
|
<style>
|
|
9
7
|
:root{
|
|
10
8
|
--ink:#1b1c1f; --muted:#8b8d94; --line:#ececef;
|
|
11
9
|
--mono:"IBM Plex Mono","SFMono-Regular",ui-monospace,Menlo,monospace;
|
|
12
10
|
--sans:"Avenir Next","Helvetica Neue","Segoe UI",system-ui,-apple-system,sans-serif;
|
|
13
|
-
--
|
|
14
|
-
--blue:#2f6df0; --red:#e8473f; --amber:#f0a83f;
|
|
11
|
+
--blue:#1a3a8f; --red:#C7372F; --amber:#f0a83f;
|
|
15
12
|
}
|
|
16
13
|
*{margin:0;padding:0;box-sizing:border-box;}
|
|
17
|
-
html,body{background:#
|
|
14
|
+
html,body{background:#FAF7F0;}
|
|
18
15
|
body{display:flex;justify-content:center;align-items:center;height:100vh;overflow:hidden;
|
|
19
16
|
font-family:var(--sans);color:var(--ink);}
|
|
20
17
|
|
|
@@ -41,7 +38,9 @@
|
|
|
41
38
|
.v7-head{right:56px;top:48px;width:410px;text-align:right;}
|
|
42
39
|
.v7-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
43
40
|
.v7-title{font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.05;margin-top:8px;}
|
|
44
|
-
|
|
41
|
+
/* Persona is identity flavor (a badge), not the page headline — one display-size element per
|
|
42
|
+
screen, and that's "Here's what you built." */
|
|
43
|
+
.v7-personaName{font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:.92;margin-top:8px;text-transform:lowercase;}
|
|
45
44
|
.v7-rank{margin-top:12px;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
|
|
46
45
|
.v7-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
|
|
47
46
|
.v7-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
|
|
@@ -62,6 +61,9 @@
|
|
|
62
61
|
.v7-stat .v{font-size:23px;font-weight:800;letter-spacing:-.01em;line-height:1;}
|
|
63
62
|
.v7-stat .k{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9296a0;margin-top:4px;}
|
|
64
63
|
.v7-band{left:90px;right:90px;bottom:250px;text-align:center;}
|
|
64
|
+
/* Echo delivers the verdict: the mascot makes the band a character speaking, not a floating stat. */
|
|
65
|
+
.v7-echo{width:56px;height:56px;object-fit:contain;margin:10px auto 2px;display:block;
|
|
66
|
+
filter:drop-shadow(0 12px 20px rgba(26,58,143,.22));}
|
|
65
67
|
.v7-band .eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
|
|
66
68
|
.v7-band .bignum{font-size:64px;font-weight:800;letter-spacing:-.03em;line-height:.9;margin-top:10px;color:var(--red);
|
|
67
69
|
min-height:64px;transition:opacity .52s ease,transform .52s ease;}
|
|
@@ -78,7 +80,7 @@
|
|
|
78
80
|
.v7-band .say b{color:var(--ink);font-weight:700;}
|
|
79
81
|
.v7-ctaWrap{left:0;right:0;bottom:112px;text-align:center;}
|
|
80
82
|
.v7-ctaWrap .pre{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#9296a0;margin-bottom:15px;}
|
|
81
|
-
.v7-cta{display:inline-flex;align-items:center;gap:11px;background:#1a3a8f;color:#fff;border:0;border-radius:
|
|
83
|
+
.v7-cta{display:inline-flex;align-items:center;gap:11px;background:#1a3a8f;color:#fff;border:0;border-radius:999px;
|
|
82
84
|
font-family:var(--mono);font-size:17px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;padding:21px 48px;
|
|
83
85
|
box-shadow:0 20px 46px rgba(26,58,143,.32);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
84
86
|
.v7-cta:hover,.v7-cta:focus-visible{background:#223f9b;transform:translateY(-2px);outline:none;}
|
|
@@ -158,7 +160,7 @@
|
|
|
158
160
|
|
|
159
161
|
/* setup flow CTA: hidden in share-card export, visible only in ?setup=1 mode */
|
|
160
162
|
.setup-connect{display:none;position:absolute;z-index:20;right:46px;top:930px;width:238px;min-height:58px;
|
|
161
|
-
border:1px solid #1a3a8f;border-radius:
|
|
163
|
+
border:1px solid #1a3a8f;border-radius:999px;background:#1a3a8f;color:#fff;align-items:center;justify-content:center;
|
|
162
164
|
font-family:var(--mono);font-size:13px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
|
|
163
165
|
box-shadow:0 16px 36px rgba(20,24,40,.18);cursor:pointer;}
|
|
164
166
|
.setup-connect:hover{background:#223f9b;}
|
|
@@ -230,7 +232,7 @@
|
|
|
230
232
|
body.setup-mode .counterfactual,
|
|
231
233
|
body.setup-mode .setup-connect{display:none;}
|
|
232
234
|
body.setup-mode .v7-persona{left:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(360px,32vw);}
|
|
233
|
-
body.setup-mode .v7-personaName{font-size:clamp(
|
|
235
|
+
body.setup-mode .v7-personaName{font-size:clamp(38px,3.4vw,54px);}
|
|
234
236
|
body.setup-mode .v7-head{right:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(520px,38vw);}
|
|
235
237
|
body.setup-mode .v7-head .v7-eyebrow{font-size:clamp(10px,.72vw,12px);}
|
|
236
238
|
body.setup-mode .v7-title{font-size:clamp(28px,2.1vw,40px);}
|
|
@@ -238,7 +240,10 @@
|
|
|
238
240
|
body.setup-mode .v7-stat .v{font-size:clamp(20px,1.45vw,28px);}
|
|
239
241
|
body.setup-mode .v7-stat .k{font-size:clamp(9px,.72vw,11px);}
|
|
240
242
|
body.setup-mode .v7-band{left:clamp(48px,5vw,86px);right:auto;bottom:clamp(82px,8vh,130px);
|
|
241
|
-
width:min(
|
|
243
|
+
width:min(640px,42vw);text-align:left;display:grid;grid-template-columns:72px minmax(0,1fr);
|
|
244
|
+
column-gap:16px;align-items:start;}
|
|
245
|
+
body.setup-mode .v7-echo{width:72px;height:72px;margin:2px 0 0;}
|
|
246
|
+
body.setup-mode .v7-bandCopy{min-width:0;}
|
|
242
247
|
body.setup-mode .v7-band .eyebrow{display:none;}
|
|
243
248
|
body.setup-mode .v7-band .bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
244
249
|
margin-top:0;min-height:0;line-height:.96;}
|
|
@@ -253,221 +258,13 @@
|
|
|
253
258
|
body.setup-mode .v7-ctaWrap .micro{max-width:430px;margin-left:auto;}
|
|
254
259
|
body.setup-mode .v7-foot{left:clamp(48px,5vw,86px);right:clamp(48px,5vw,86px);}
|
|
255
260
|
@media (max-height:820px){
|
|
256
|
-
body.setup-mode .v7-personaName{font-size:clamp(
|
|
261
|
+
body.setup-mode .v7-personaName{font-size:clamp(32px,4.4vh,46px);}
|
|
257
262
|
body.setup-mode .v7-title{font-size:clamp(30px,5vh,44px);}
|
|
258
263
|
body.setup-mode .v7-band{bottom:58px;width:min(500px,40vw);}
|
|
259
264
|
body.setup-mode .v7-ctaWrap{bottom:58px;width:min(390px,32vw);}
|
|
260
265
|
body.setup-mode .v7-foot{display:none;}
|
|
261
266
|
}
|
|
262
267
|
|
|
263
|
-
/* ===== setup SPLIT layout: report panel capped ~620px, city takes the rest (responsive on wide screens) ===== */
|
|
264
|
-
body.setup-mode #repo-visualizer{right:auto;width:calc(100vw - min(40vw,620px));}
|
|
265
|
-
/* city metric toggle — centered over the city (clear of the corner ribbon), setup mode only */
|
|
266
|
-
.city-toggle{display:none;transition:opacity .4s ease;}
|
|
267
|
-
body.setup-mode .city-toggle{display:inline-flex;position:absolute;z-index:9;
|
|
268
|
-
left:calc((100vw - min(40vw,620px)) / 2);top:clamp(20px,3vh,36px);transform:translateX(-50%);gap:2px;
|
|
269
|
-
background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:999px;padding:4px;
|
|
270
|
-
box-shadow:0 8px 22px rgba(20,24,40,.10);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);}
|
|
271
|
-
body.scanning .city-toggle{opacity:0;pointer-events:none;}
|
|
272
|
-
/* Flutter-debug-style two-line corner ribbon (top-left) — "local scan, private & safe" */
|
|
273
|
-
.corner-ribbon{display:none;}
|
|
274
|
-
body.setup-mode .corner-ribbon{display:block;position:absolute;top:0;left:0;width:180px;height:180px;
|
|
275
|
-
overflow:hidden;z-index:12;pointer-events:none;}
|
|
276
|
-
.corner-ribbon>span{position:absolute;left:-70px;top:51px;width:280px;transform:rotate(-45deg);
|
|
277
|
-
background:#1a3a8f;color:#fff;text-align:center;padding:7px 0;line-height:1.2;
|
|
278
|
-
box-shadow:0 6px 16px rgba(20,24,40,.18);}
|
|
279
|
-
.corner-ribbon .l1{display:block;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;}
|
|
280
|
-
.corner-ribbon .l2{display:block;font-family:var(--mono);font-size:8.5px;font-weight:700;letter-spacing:.08em;
|
|
281
|
-
text-transform:uppercase;color:rgba(255,255,255,.86);margin-top:2px;}
|
|
282
|
-
/* Claude vs Codex token split — bottom-left of the city */
|
|
283
|
-
.city-split{display:none;transition:opacity .4s ease;}
|
|
284
|
-
body.setup-mode .city-split{display:block;position:absolute;z-index:9;
|
|
285
|
-
left:clamp(28px,3vw,48px);bottom:clamp(72px,9vh,104px);width:min(260px,24vw);}
|
|
286
|
-
body.scanning .city-split{opacity:0;pointer-events:none;}
|
|
287
|
-
/* timeline: first Claude/Codex session -> today, full width along the bottom of the city */
|
|
288
|
-
.city-timeline{display:none;transition:opacity .4s ease;}
|
|
289
|
-
body.setup-mode .city-timeline{display:block;position:absolute;z-index:9;
|
|
290
|
-
left:clamp(28px,3vw,48px);bottom:clamp(20px,2.6vh,32px);
|
|
291
|
-
width:calc(100vw - min(40vw,620px) - 2 * clamp(28px,3vw,48px));}
|
|
292
|
-
body.scanning .city-timeline{opacity:0;pointer-events:none;}
|
|
293
|
-
.city-timeline .tl-track{position:relative;height:16px;cursor:pointer;touch-action:none;}
|
|
294
|
-
.city-timeline .tl-line{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
|
|
295
|
-
height:2px;border-radius:2px;background:linear-gradient(90deg,#d8dbe1,#aab0bb);}
|
|
296
|
-
.city-timeline .tl-dot{position:absolute;top:50%;transform:translate(-50%,-50%);width:9px;height:9px;border-radius:50%;}
|
|
297
|
-
.city-timeline .tl-dot.start{left:0;background:#9aa0aa;}
|
|
298
|
-
.city-timeline .tl-dot.end{left:100%;background:#1a3a8f;}
|
|
299
|
-
.city-timeline .tl-dot.end::after{content:'';position:absolute;inset:-4px;border-radius:50%;
|
|
300
|
-
border:2px solid rgba(26,58,143,.4);animation:tlPulse 2.2s ease-out infinite;}
|
|
301
|
-
@keyframes tlPulse{0%{transform:scale(.6);opacity:.9}100%{transform:scale(1.85);opacity:0}}
|
|
302
|
-
.city-timeline .tl-handle{position:absolute;left:100%;top:50%;transform:translate(-50%,-50%);
|
|
303
|
-
width:16px;height:16px;border-radius:50%;background:#1a3a8f;border:3px solid #fff;
|
|
304
|
-
box-shadow:0 3px 10px rgba(20,24,40,.28);cursor:grab;z-index:3;}
|
|
305
|
-
.city-timeline .tl-handle:active,.city-timeline.dragging .tl-handle{cursor:grabbing;}
|
|
306
|
-
.city-timeline .tl-bubble{position:absolute;bottom:calc(100% + 9px);left:50%;transform:translateX(-50%);
|
|
307
|
-
white-space:nowrap;background:#1b1c1f;color:#fff;font-family:var(--mono);font-size:10px;font-weight:700;
|
|
308
|
-
letter-spacing:.04em;padding:4px 8px;border-radius:6px;opacity:0;transition:opacity .15s ease;pointer-events:none;
|
|
309
|
-
box-shadow:0 6px 16px rgba(20,24,40,.24);}
|
|
310
|
-
.city-timeline.dragging .tl-bubble{opacity:1;}
|
|
311
|
-
.city-timeline .tl-labels{display:flex;justify-content:space-between;align-items:baseline;margin-top:8px;
|
|
312
|
-
font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#80838c;}
|
|
313
|
-
.city-timeline .tl-labels .tl-span{color:#1b1c1f;font-weight:800;}
|
|
314
|
-
.city-split .cs-bar{display:flex;height:8px;border-radius:999px;overflow:hidden;background:#eceef2;
|
|
315
|
-
box-shadow:inset 0 0 0 1px rgba(20,24,40,.05);}
|
|
316
|
-
.city-split .cs-bar i{height:100%;display:block;transition:width .4s cubic-bezier(.4,0,.2,1);}
|
|
317
|
-
.city-split .cs-claude{background:#d97757;}
|
|
318
|
-
.city-split .cs-codex{background:#10a37f;}
|
|
319
|
-
.city-split .cs-legend{display:flex;justify-content:space-between;margin-top:9px;
|
|
320
|
-
font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#80838c;}
|
|
321
|
-
.city-split .cs-l{display:flex;align-items:center;gap:6px;}
|
|
322
|
-
.city-split .cs-l i{width:8px;height:8px;border-radius:50%;}
|
|
323
|
-
.city-split .cs-l.claude i{background:#d97757;}
|
|
324
|
-
.city-split .cs-l.codex i{background:#10a37f;}
|
|
325
|
-
.city-split .cs-l b{color:#1b1c1f;font-weight:800;margin-left:2px;}
|
|
326
|
-
.ct-thumb{position:absolute;left:0;top:0;z-index:0;background:#1a3a8f;border-radius:999px;
|
|
327
|
-
transition:transform .32s cubic-bezier(.4,0,.2,1),width .32s cubic-bezier(.4,0,.2,1),height .32s ease;}
|
|
328
|
-
.city-toggle button{position:relative;z-index:1;appearance:none;border:0;background:transparent;cursor:pointer;
|
|
329
|
-
font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
|
|
330
|
-
color:#80838c;padding:7px 14px;border-radius:999px;transition:color .25s ease;}
|
|
331
|
-
.city-toggle button.on{color:#fff;}
|
|
332
|
-
.city-toggle button:not(.on):hover{color:#1b1c1f;}
|
|
333
|
-
/* the old four-corner chrome is replaced by the right-hand field report */
|
|
334
|
-
body.setup-mode .v7-persona,
|
|
335
|
-
body.setup-mode .v7-head,
|
|
336
|
-
body.setup-mode .v7-band,
|
|
337
|
-
body.setup-mode .v7-ctaWrap{display:none;}
|
|
338
|
-
/* old footer reassurances are replaced by the top-left corner ribbon */
|
|
339
|
-
body.setup-mode .v7-foot{display:none;}
|
|
340
|
-
|
|
341
|
-
.fr-panel{display:none;transition:opacity .6s ease;}
|
|
342
|
-
body.setup-mode .fr-panel{display:flex;flex-direction:column;align-items:center;position:absolute;z-index:8;
|
|
343
|
-
right:0;top:0;width:min(40vw,620px);height:100vh;border-left:1px solid var(--line);background:#fff;
|
|
344
|
-
padding:clamp(44px,6vh,90px) clamp(36px,3.2vw,60px);}
|
|
345
|
-
body.scanning .fr-panel{opacity:0;pointer-events:none;}
|
|
346
|
-
/* centered content column that fills the column height and distributes the beats top -> bottom */
|
|
347
|
-
.fr{width:min(54ch,100%);flex:1;display:flex;flex-direction:column;justify-content:space-between;
|
|
348
|
-
gap:clamp(22px,3.4vh,46px);}
|
|
349
|
-
.fr-dateline{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:11px;
|
|
350
|
-
font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
351
|
-
.fr-dateline .fr-no{color:var(--ink);font-weight:800;}
|
|
352
|
-
.fr-dateline .fr-rule{flex:1;height:1px;background:var(--line);}
|
|
353
|
-
.fr-intro{display:flex;flex-direction:column;position:relative;
|
|
354
|
-
padding-bottom:clamp(20px,3vh,38px);border-bottom:1px solid var(--line);}
|
|
355
|
-
.fr-avatar{width:clamp(92px,9.5vw,128px);height:auto;display:block;margin-bottom:10px;}
|
|
356
|
-
.fr-greet{font-size:clamp(34px,3.2vw,52px);font-weight:800;letter-spacing:-.035em;line-height:.95;
|
|
357
|
-
text-transform:lowercase;}
|
|
358
|
-
.fr-greet .fr-name{color:#1a3a8f;}
|
|
359
|
-
.fr-greet .fr-dot{color:var(--amber);}
|
|
360
|
-
.fr-rank{margin-top:13px;font-family:var(--mono);font-size:clamp(12px,1vw,14px);font-weight:600;
|
|
361
|
-
letter-spacing:.04em;color:#9296a0;}
|
|
362
|
-
.fr-rank b{font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#3a3c43;}
|
|
363
|
-
/* persona breakdown: small "?" pinned top-right, click to reveal the rest of the read */
|
|
364
|
-
.fr-readBtn{position:absolute;z-index:9;top:clamp(2px,0.5vw,8px);right:0;
|
|
365
|
-
display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
|
|
366
|
-
cursor:pointer;font-family:var(--mono);font-size:14px;font-weight:800;line-height:1;
|
|
367
|
-
color:#5a5e66;background:#fafafb;border:1px solid var(--line);border-radius:50%;
|
|
368
|
-
transition:background .15s ease,border-color .15s ease,color .15s ease;}
|
|
369
|
-
.fr-readBtn:hover,.fr-readBtn:focus-visible{background:#fff;border-color:#d7dade;color:var(--ink);outline:none;}
|
|
370
|
-
.fr-readBtn[aria-expanded="true"]{background:#1a3a8f;border-color:#1a3a8f;color:#fff;}
|
|
371
|
-
.fr-personaHover{position:absolute;z-index:32;top:clamp(42px,4.6vw,56px);right:0;
|
|
372
|
-
width:min(340px,100%);max-height:min(72vh,540px);overflow:auto;
|
|
373
|
-
background:rgba(255,255,255,.98);border:1px solid var(--line);border-radius:16px;padding:15px 17px;
|
|
374
|
-
box-shadow:0 18px 46px rgba(20,24,40,.18);opacity:0;visibility:hidden;transform:translateY(-4px);
|
|
375
|
-
transition:opacity .14s ease,transform .14s ease;pointer-events:none;}
|
|
376
|
-
.fr-personaHover.open{opacity:1;visibility:visible;transform:none;pointer-events:auto;}
|
|
377
|
-
/* "this is you" — editorial: hairline rows, mono labels, amber highlighter (no boxes) */
|
|
378
|
-
.ph-kicker{font-family:var(--mono);font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
379
|
-
.ph-title{font-size:30px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:6px;text-transform:lowercase;color:#1a3a8f;}
|
|
380
|
-
.ph-lead{font-size:13.5px;line-height:1.55;color:#42444b;margin-top:10px;}
|
|
381
|
-
.ph-row{margin-top:14px;padding-top:13px;border-top:1px solid var(--line);}
|
|
382
|
-
.ph-row-h{display:flex;align-items:center;justify-content:space-between;
|
|
383
|
-
font-family:var(--mono);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:#9aa0aa;}
|
|
384
|
-
.ph-row-v{font-size:17px;font-weight:800;letter-spacing:-.015em;line-height:1.05;margin-top:6px;
|
|
385
|
-
text-transform:lowercase;color:#1b1c1f;}
|
|
386
|
-
.ph-row-d{font-size:12px;line-height:1.45;color:#73767e;margin-top:4px;}
|
|
387
|
-
.ph-meter{display:inline-flex;gap:4px;align-items:center;}
|
|
388
|
-
.ph-meter i{width:6px;height:6px;border-radius:50%;background:#e0e3e8;}
|
|
389
|
-
.ph-meter i.on{background:#1a3a8f;}
|
|
390
|
-
.ph-also{margin-top:8px;display:flex;flex-wrap:wrap;gap:9px;}
|
|
391
|
-
.ph-also mark{background:linear-gradient(transparent 60%, rgba(240,168,63,.42) 60%);color:#1b1c1f;
|
|
392
|
-
font-weight:800;font-size:13.5px;padding:0 1px;text-transform:lowercase;}
|
|
393
|
-
/* "see all types" catalog inside the popover */
|
|
394
|
-
.ph-toggle{margin-top:13px;width:100%;cursor:pointer;font-family:var(--mono);font-size:10.5px;font-weight:800;
|
|
395
|
-
letter-spacing:.1em;text-transform:uppercase;color:#1a3a8f;background:#f3f5fb;border:1px solid #e1e6f3;
|
|
396
|
-
border-radius:10px;padding:9px 12px;transition:background .15s ease;}
|
|
397
|
-
.ph-toggle:hover{background:#e9edf8;}
|
|
398
|
-
/* section headers: mono label + hairline rule, matching the "you" rows */
|
|
399
|
-
.ph-cat-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
|
|
400
|
-
font-family:var(--mono);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
|
|
401
|
-
color:#9aa0aa;margin-top:14px;padding-top:13px;border-top:1px solid var(--line);}
|
|
402
|
-
.ph-cat-h:first-child{margin-top:0;padding-top:0;border-top:0;}
|
|
403
|
-
.ph-cat-h span{color:#bcc0c7;font-weight:700;text-transform:none;letter-spacing:0;}
|
|
404
|
-
/* indexed list: mono numbers + light labels; only YOUR picks are emphasized */
|
|
405
|
-
.ph-list{display:flex;flex-direction:column;margin-top:6px;}
|
|
406
|
-
.ph-item{display:flex;align-items:baseline;gap:11px;padding:4px 0;cursor:default;}
|
|
407
|
-
.ph-item .ph-ix{font-family:var(--mono);font-size:10px;font-weight:600;color:#cfd3d9;flex:none;width:15px;}
|
|
408
|
-
.ph-item .ph-nm{font-size:14px;font-weight:600;letter-spacing:-.005em;text-transform:lowercase;color:#a4a8af;}
|
|
409
|
-
.ph-item.is-you .ph-ix,.ph-item.is-primary .ph-ix{color:#1a3a8f;}
|
|
410
|
-
.ph-item.is-you .ph-nm{font-weight:700;color:#1b1c1f;background:linear-gradient(transparent 64%, rgba(240,168,63,.38) 64%);padding:0 1px;}
|
|
411
|
-
.ph-item.is-primary .ph-nm{font-weight:700;color:#1a3a8f;background:linear-gradient(transparent 62%, rgba(240,168,63,.44) 62%);padding:0 1px;}
|
|
412
|
-
.ph-item .ph-you-tag{margin-left:auto;align-self:center;font-family:var(--mono);font-size:9px;font-weight:700;
|
|
413
|
-
letter-spacing:.12em;text-transform:uppercase;color:#1a3a8f;}
|
|
414
|
-
/* rank PODIUM — ascending steps you climb; your rung is the tall blue bar with a YOU flag */
|
|
415
|
-
.ph-podium{display:flex;align-items:flex-end;gap:6px;margin-top:14px;}
|
|
416
|
-
.ph-pod{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;cursor:default;min-width:0;}
|
|
417
|
-
.ph-pod-you{font-family:var(--mono);font-size:8px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#1a3a8f;margin-bottom:5px;}
|
|
418
|
-
.ph-pod-bar{position:relative;width:100%;border-radius:7px 7px 0 0;background:#ecedf1;transition:background .2s ease;}
|
|
419
|
-
.ph-pod-lvl{position:absolute;top:5px;left:0;right:0;text-align:center;font-family:var(--mono);font-size:9px;font-weight:700;color:#aab0bd;}
|
|
420
|
-
.ph-pod-nm{margin-top:7px;width:100%;text-align:center;font-size:8.5px;line-height:1.15;color:#a8acb4;
|
|
421
|
-
text-transform:lowercase;word-break:break-word;}
|
|
422
|
-
.ph-pod.reached .ph-pod-bar{background:#c9cfe2;}
|
|
423
|
-
.ph-pod.reached .ph-pod-lvl{color:#5a6486;}
|
|
424
|
-
.ph-pod.reached .ph-pod-nm{color:#6a7290;}
|
|
425
|
-
.ph-pod.is-you .ph-pod-bar{background:#1a3a8f;box-shadow:0 10px 20px rgba(26,58,143,.30);}
|
|
426
|
-
.ph-pod.is-you .ph-pod-lvl{color:#fff;}
|
|
427
|
-
.ph-pod.is-you .ph-pod-nm{color:#1b1c1f;font-weight:800;}
|
|
428
|
-
/* detail tooltip shown on hover of a catalog item (fixed, so the scrolling popover never clips it) */
|
|
429
|
-
/* trait/rank/rhythm detail card — sticker (traits only) + name + blurb */
|
|
430
|
-
.fr-catTip{position:fixed;z-index:60;width:288px;background:#fff;border:1px solid var(--line);border-radius:16px;
|
|
431
|
-
padding:14px 16px;box-shadow:0 22px 54px rgba(20,24,40,.22);pointer-events:none;opacity:0;transform:translateY(5px);
|
|
432
|
-
transition:opacity .14s ease,transform .14s ease;display:flex;gap:13px;align-items:flex-start;}
|
|
433
|
-
.fr-catTip.show{opacity:1;transform:none;}
|
|
434
|
-
.fr-catTip .ct-img{width:62px;height:62px;flex:none;object-fit:contain;display:none;
|
|
435
|
-
border-radius:14px;background:radial-gradient(120% 120% at 50% 30%,#f6f7f9,#eceef2);
|
|
436
|
-
filter:drop-shadow(0 8px 16px rgba(20,24,40,.14));}
|
|
437
|
-
.fr-catTip.has-img .ct-img{display:block;}
|
|
438
|
-
.fr-catTip .ct-body{min-width:0;}
|
|
439
|
-
.fr-catTip .tl{display:block;font-size:18px;font-weight:800;letter-spacing:-.02em;text-transform:lowercase;
|
|
440
|
-
color:#1a3a8f;line-height:1;margin-bottom:7px;}
|
|
441
|
-
.fr-catTip .bd{display:block;font-size:13px;line-height:1.5;color:#42444b;}
|
|
442
|
-
.fr-personaHover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
443
|
-
.fr-personaHover .ph-line:last-of-type{margin-bottom:0;}
|
|
444
|
-
.fr-personaHover .ph-line b{color:var(--ink);font-weight:800;}
|
|
445
|
-
.fr-personaHover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
446
|
-
padding-top:9px;border-top:1px solid var(--line);}
|
|
447
|
-
.fr-personaHover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
448
|
-
/* lede + cost grouped into one section (so they stay together on tall screens), with a divider above */
|
|
449
|
-
.fr-mid{display:flex;flex-direction:column;gap:clamp(20px,3vh,40px);}
|
|
450
|
-
.fr-lede{font-size:clamp(16px,1.1vw,18px);line-height:1.68;color:#3c3e45;}
|
|
451
|
-
.fr-lede mark{background:linear-gradient(transparent 58%, rgba(240,168,63,.42) 58%);
|
|
452
|
-
color:var(--ink);font-weight:800;padding:0 1px;}
|
|
453
|
-
.fr-pull{padding-left:20px;border-left:3px solid var(--red);}
|
|
454
|
-
.fr-metric{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
455
|
-
transition:opacity .5s ease,transform .5s ease;}
|
|
456
|
-
.fr-metric .fr-v{font-size:clamp(32px,2.8vw,44px);font-weight:800;color:var(--red);letter-spacing:-.03em;line-height:.9;}
|
|
457
|
-
.fr-metric .fr-l{font-size:clamp(16px,1.2vw,19px);font-weight:700;color:var(--ink);letter-spacing:-.01em;}
|
|
458
|
-
/* reserve the tallest slide's height so the rotating swap never reflows the column */
|
|
459
|
-
.fr-say{font-size:clamp(14px,.9vw,15px);color:#56585f;margin-top:9px;line-height:1.5;max-width:46ch;
|
|
460
|
-
min-height:4.6em;transition:opacity .5s ease,transform .5s ease;}
|
|
461
|
-
.fr-say b{color:var(--ink);font-weight:700;}
|
|
462
|
-
.fr-metric.is-swapping,.fr-say.is-swapping{opacity:0;transform:translateY(6px);}
|
|
463
|
-
.fr-signoff{display:flex;align-items:center;gap:18px;flex-wrap:wrap;}
|
|
464
|
-
.fr-cta{display:inline-flex;align-items:center;gap:10px;background:#1a3a8f;color:#fff;border:0;
|
|
465
|
-
border-radius:13px;font-family:var(--mono);font-size:clamp(13px,1vw,15px);font-weight:800;
|
|
466
|
-
letter-spacing:.08em;text-transform:uppercase;padding:16px 34px;
|
|
467
|
-
box-shadow:0 18px 40px rgba(26,58,143,.30);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
468
|
-
.fr-cta:hover{background:#223f9b;transform:translateY(-2px);}
|
|
469
|
-
.fr-micro{font-size:12.5px;color:#8b8d94;line-height:1.45;max-width:24ch;}
|
|
470
|
-
|
|
471
268
|
/* ===== in-canvas scan loader (setup mode) — loading lives inside the report, matches its type ===== */
|
|
472
269
|
.scan-loading{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
473
270
|
text-align:center;gap:14px;padding:24px;opacity:1;transition:opacity .5s ease;pointer-events:none;}
|
|
@@ -515,41 +312,11 @@
|
|
|
515
312
|
<div class="share-card">
|
|
516
313
|
<canvas id="repo-visualizer"></canvas>
|
|
517
314
|
|
|
518
|
-
<!-- city metric toggle (setup mode): swaps every number on the city among time / tokens / $ -->
|
|
519
|
-
<div class="city-toggle" id="city-toggle" role="group" aria-label="City metric">
|
|
520
|
-
<span class="ct-thumb" aria-hidden="true"></span>
|
|
521
|
-
<button type="button" data-metric="time" class="on">Time</button>
|
|
522
|
-
<button type="button" data-metric="tokens">Tokens</button>
|
|
523
|
-
<button type="button" data-metric="cost">API $</button>
|
|
524
|
-
</div>
|
|
525
|
-
|
|
526
|
-
<!-- corner ribbon (Flutter-debug style): two-line, makes clear this is a safe local scan -->
|
|
527
|
-
<div class="corner-ribbon" aria-hidden="true"><span><span class="l1">Local Scan</span><span class="l2">100% private & safe</span></span></div>
|
|
528
|
-
|
|
529
|
-
<!-- Claude vs Codex token split (matches the cube colors: orange = Claude, green = Codex) -->
|
|
530
|
-
<div class="city-split" id="city-split">
|
|
531
|
-
<div class="cs-bar"><i class="cs-claude" style="width:74%"></i><i class="cs-codex" style="width:26%"></i></div>
|
|
532
|
-
<div class="cs-legend">
|
|
533
|
-
<span class="cs-l claude"><i></i>Claude <b>74%</b></span>
|
|
534
|
-
<span class="cs-l codex"><i></i>Codex <b>26%</b></span>
|
|
535
|
-
</div>
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
<!-- timeline: drag to grow the city from the first Claude/Codex session to today -->
|
|
539
|
-
<div class="city-timeline" id="city-timeline">
|
|
540
|
-
<div class="tl-track" id="tl-track">
|
|
541
|
-
<span class="tl-line"></span>
|
|
542
|
-
<span class="tl-dot start"></span><span class="tl-dot end"></span>
|
|
543
|
-
<span class="tl-handle" id="tl-handle"><span class="tl-bubble" id="tl-bubble"></span></span>
|
|
544
|
-
</div>
|
|
545
|
-
<div class="tl-labels"><span class="tl-start">Dec 2024</span><span class="tl-span">drag to replay your build</span><span class="tl-end">Today</span></div>
|
|
546
|
-
</div>
|
|
547
|
-
|
|
548
315
|
<div class="v7-z v7-persona" tabindex="0" aria-describedby="v7-persona-hover">
|
|
549
316
|
<div class="v7-eyebrow">AI Coding City</div>
|
|
550
317
|
<div class="v7-personaName">bossy</div>
|
|
551
318
|
<div class="v7-rank">the architect · marathoner</div>
|
|
552
|
-
<div class="v7-combo"><span class="ct">loopy</span><span class="ct">
|
|
319
|
+
<div class="v7-combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
553
320
|
<div class="v7-personaHover" id="v7-persona-hover" role="tooltip"></div>
|
|
554
321
|
</div>
|
|
555
322
|
<div class="v7-z v7-head">
|
|
@@ -564,8 +331,11 @@
|
|
|
564
331
|
</div>
|
|
565
332
|
<div class="v7-z v7-band">
|
|
566
333
|
<div class="eyebrow">What we found</div>
|
|
334
|
+
<img class="v7-echo" src="echo-face-cutout.png" alt="" />
|
|
335
|
+
<div class="v7-bandCopy">
|
|
567
336
|
<div class="bignum" id="v7-metric"><span id="v7-metric-value">85.0%</span><span id="v7-metric-label">repeated reads</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>
|
|
568
337
|
<div class="say" id="v7-metric-say">Your agents re-load context they've already seen — unchanged files, the same rules, stale maps — every session. That's context rot. It's <b>how coding agents work today</b>, and it's the haze on your city.</div>
|
|
338
|
+
</div>
|
|
569
339
|
</div>
|
|
570
340
|
<div class="v7-z v7-ctaWrap">
|
|
571
341
|
<div class="pre">Ready when you are</div>
|
|
@@ -573,31 +343,6 @@
|
|
|
573
343
|
<div class="micro">Clears the dirty context, so agents stop relearning your workspace.</div>
|
|
574
344
|
</div>
|
|
575
345
|
<div class="v7-z v7-foot"><span>Generated by Echo · No transcripts shown</span><span>Local scan · Share-safe city mode</span></div>
|
|
576
|
-
|
|
577
|
-
<!-- setup-mode field report (right half) — the narrative beside the city -->
|
|
578
|
-
<div class="fr-panel">
|
|
579
|
-
<div class="fr">
|
|
580
|
-
<div class="fr-dateline"><span>Echo field report</span><span class="fr-rule"></span></div>
|
|
581
|
-
<div class="fr-intro">
|
|
582
|
-
<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>
|
|
583
|
-
<div class="fr-personaHover" id="fr-persona-hover" role="region" aria-label="Your full coding-personality read"></div>
|
|
584
|
-
<img class="fr-avatar" id="fr-avatar" src="./personality_stickers/bossy.png" alt="" />
|
|
585
|
-
<h1 class="fr-greet">hey, <span class="fr-name" id="fr-name">bossy</span><span class="fr-dot">.</span></h1>
|
|
586
|
-
<div class="fr-rank" id="fr-rank">You are <b>the architect</b> on <b>the streak</b></div>
|
|
587
|
-
</div>
|
|
588
|
-
<div class="fr-mid">
|
|
589
|
-
<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>
|
|
590
|
-
<div class="fr-pull">
|
|
591
|
-
<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">repeated reads</span></div>
|
|
592
|
-
<div class="fr-say" id="fr-metric-say">Your agents re-load context they've already seen — unchanged files, the same rules, stale maps — every session. That's context rot.</div>
|
|
593
|
-
</div>
|
|
594
|
-
</div>
|
|
595
|
-
<div class="fr-signoff">
|
|
596
|
-
<button class="fr-cta" id="fr-connect" type="button">Connect Echo →</button>
|
|
597
|
-
<div class="fr-micro">Clears the dirty context, so agents stop relearning your workspace.</div>
|
|
598
|
-
</div>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
346
|
<div class="v7-infoSheet" id="v7-info-sheet" aria-hidden="true">
|
|
602
347
|
<div class="v7-infoCard" role="dialog" aria-modal="true" aria-labelledby="v7-info-title">
|
|
603
348
|
<button class="v7-infoClose" id="v7-info-close" type="button" aria-label="Close">×</button>
|
|
@@ -624,7 +369,7 @@
|
|
|
624
369
|
<div class="setup-eyebrow">AI Coding City</div>
|
|
625
370
|
<div class="setup-persona-name">bossy</div>
|
|
626
371
|
<div class="setup-rank">the architect · marathoner</div>
|
|
627
|
-
<div class="setup-combo"><span class="ct">loopy</span><span class="ct">
|
|
372
|
+
<div class="setup-combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
628
373
|
<div class="persona-cue">hover for the read</div>
|
|
629
374
|
<div class="persona-hover" id="persona-hover" role="tooltip"></div>
|
|
630
375
|
</div>
|
|
@@ -654,7 +399,7 @@
|
|
|
654
399
|
<canvas class="avatar-rive" id="persona-mascot"></canvas>
|
|
655
400
|
<div class="name">bossy</div>
|
|
656
401
|
<div class="rankrhythm">the architect · marathoner</div>
|
|
657
|
-
<div class="combo"><span class="ct">loopy</span><span class="ct">
|
|
402
|
+
<div class="combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
658
403
|
<div class="verdict">you don't write code, you run a daycare for <b>163</b> sleepless interns who reread the same file into amnesia and bill every typo to Opus — <b>$13,689</b> later, you wanted a helper and became a dad. 🗿</div>
|
|
659
404
|
</div>
|
|
660
405
|
|
|
@@ -701,8 +446,8 @@ import { OrbitControls } from './vendor/OrbitControls.js';
|
|
|
701
446
|
const pageParams=new URLSearchParams(location.search);
|
|
702
447
|
const setupMode=pageParams.get('setup')==='1';
|
|
703
448
|
if(setupMode) document.body.classList.add('setup-mode');
|
|
704
|
-
let VIZ_W=setupMode ?
|
|
705
|
-
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode,
|
|
449
|
+
let VIZ_W=setupMode ? window.innerWidth : 920;
|
|
450
|
+
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode, viewport in setup mode
|
|
706
451
|
const TRAY_COL = 0xece5d3; // warm IVORY — not pure white, not cool grey; separates from the white card
|
|
707
452
|
const TOOL = { codex:0x10a37f, claude:0xd97757 }; // ONLY two cube colours — exact OpenAI / Claude chip-icon hexes
|
|
708
453
|
|
|
@@ -738,14 +483,13 @@ function providerSplit(report){
|
|
|
738
483
|
}
|
|
739
484
|
// ---- coding-persona engine (Phase 1: only signals the scan actually has — no commits / accept-all yet) ----
|
|
740
485
|
const PERSONA_TRAITS={
|
|
486
|
+
loopy:{label:'loopy', blurb:"opened the same file 14 times and still doesn't know what's in it. it's not working, it's re-remembering. context rot undefeated — and the bill's yours. 🗿"},
|
|
487
|
+
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
|
+
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
|
+
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features, breadth over depth. your brain is a browser with a memory leak. 🫠"},
|
|
741
490
|
bossy:{label:'bossy', blurb:"you don't write code anymore — you run a daycare for agents who never sleep and occasionally lie. wanted a helper, became a dad."},
|
|
742
|
-
|
|
743
|
-
loopy:{label:'loopy', blurb:"opened the same file 14 times and still doesn't know what's in it. it's not working, it's re-remembering. context rot undefeated. 🗿"},
|
|
744
|
-
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features in every direction. your brain is a browser with a memory leak. 🫠"},
|
|
745
|
-
ghosty:{label:'ghosty', blurb:"you edit a lot but commit little — branches get opened, worked on, then abandoned before they ship. lots of motion, not much landed."},
|
|
491
|
+
ghosty:{label:'ghosty', blurb:"opened three files, made eight edits, then simply vanished. another one for the graveyard, abandoned at exactly 73%. we don't talk about that branch. 🧍"},
|
|
746
492
|
};
|
|
747
|
-
// maxxy (merged from lusty) wears the lusty character; every other trait uses its own sticker
|
|
748
|
-
const stickerFor=(traitKey)=> traitKey==='maxxy' ? 'lusty' : traitKey;
|
|
749
493
|
const PERSONA_RANKS={
|
|
750
494
|
tourist:{label:'vibe tourist', blurb:"accepts all on blind faith, googles the error the agent already explained two lines up. bless — we all start here."},
|
|
751
495
|
agent:{label:'agent-pilled', blurb:"saw it one-shot something once and never looked back. drives one agent, still watches its every move."},
|
|
@@ -780,7 +524,8 @@ function derivePersona(report){
|
|
|
780
524
|
// measurable traits only — primary = strongest firing signal, sides = the next few
|
|
781
525
|
const scored=[
|
|
782
526
|
{key:'loopy', on:stale>=45, s:stale/100},
|
|
783
|
-
{key:'
|
|
527
|
+
{key:'lusty', on:spend>=1500, s:Math.min(1,spend/12000)},
|
|
528
|
+
{key:'maxxy', on:opusShare>=0.5, s:opusShare},
|
|
784
529
|
{key:'bossy', on:autonomy>=5, s:Math.min(1,autonomy/20)},
|
|
785
530
|
{key:'tabby', on:repoCount>=10, s:Math.min(1,repoCount/30)},
|
|
786
531
|
{key:'ghosty', on:ghostyOn, s:ghostyOn?Math.min(0.8, 1-Math.min(1,commitsPerDay/0.5)):0},
|
|
@@ -817,6 +562,7 @@ function renderPersonaHover(persona){
|
|
|
817
562
|
}
|
|
818
563
|
function renderV7PersonaHover(persona){
|
|
819
564
|
const host=document.getElementById('v7-persona-hover');
|
|
565
|
+
if(!host) return;
|
|
820
566
|
const esc=(t)=>String(t).replace(/[&<>]/g,(c)=>({'&':'&','<':'<','>':'>'}[c]));
|
|
821
567
|
const line=(t)=>t?`<div class="ph-line"><b>${esc(t.label)}</b> — ${esc(t.blurb)}</div>`:'';
|
|
822
568
|
if(!persona){
|
|
@@ -829,73 +575,9 @@ function renderV7PersonaHover(persona){
|
|
|
829
575
|
rhythm:byLabel(PERSONA_RHYTHMS, rankBits[1]),
|
|
830
576
|
};
|
|
831
577
|
}
|
|
832
|
-
|
|
578
|
+
host.innerHTML = line(persona.primary) + (persona.sides||[]).map(line).join('') +
|
|
833
579
|
(persona.rank?`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>`:'') +
|
|
834
580
|
(persona.rhythm?`<div class="ph-meta"><b>${esc(persona.rhythm.label)}</b> · ${esc(persona.rhythm.blurb)}</div>`:'');
|
|
835
|
-
if(host) host.innerHTML = youHtml;
|
|
836
|
-
renderFrPersona(persona, youHtml, esc);
|
|
837
|
-
}
|
|
838
|
-
// field-report popover: "this is you" + a togglable catalog of ALL the trait/rank/rhythm options
|
|
839
|
-
function renderFrPersona(persona, youHtml, esc){
|
|
840
|
-
const frHost=document.getElementById('fr-persona-hover');
|
|
841
|
-
if(!frHost || !persona) return;
|
|
842
|
-
esc = esc || ((t)=>String(t).replace(/[&<>]/g,(c)=>({'&':'&','<':'<','>':'>'}[c])));
|
|
843
|
-
const da=(t)=>esc(t).replace(/"/g,'"');
|
|
844
|
-
const primaryLabel=persona.primary?persona.primary.label:'';
|
|
845
|
-
const primaryBlurb=persona.primary?persona.primary.blurb:'';
|
|
846
|
-
const sides=(persona.sides||[]).filter(Boolean);
|
|
847
|
-
const sideLabels=new Set(sides.map((s)=>s.label));
|
|
848
|
-
const rankLabel=persona.rank?persona.rank.label:'';
|
|
849
|
-
const rhythmLabel=persona.rhythm?persona.rhythm.label:'';
|
|
850
|
-
const rankList=Object.values(PERSONA_RANKS);
|
|
851
|
-
const rankIdx=rankList.findIndex((r)=>r.label===rankLabel);
|
|
852
|
-
const dots=rankList.map((r,i)=>`<i class="${i<=rankIdx?'on':''}"></i>`).join('');
|
|
853
|
-
// ---- "this is you": primary is the star, rank as a level meter, rhythm as a badge, sides demoted ----
|
|
854
|
-
const youPaneHtml =
|
|
855
|
-
`<div class="ph-kicker">your coding personality</div>`+
|
|
856
|
-
`<div class="ph-title">${esc(primaryLabel)}</div>`+
|
|
857
|
-
`<div class="ph-lead">${esc(primaryBlurb)}</div>`+
|
|
858
|
-
`<div class="ph-row"><div class="ph-row-h"><span>Rank · lv ${rankIdx+1}/${rankList.length}</span><span class="ph-meter">${dots}</span></div>`+
|
|
859
|
-
`<div class="ph-row-v">${esc(rankLabel)}</div>`+
|
|
860
|
-
(persona.rank?`<div class="ph-row-d">${esc(persona.rank.blurb)}</div>`:'')+
|
|
861
|
-
`</div>`+
|
|
862
|
-
`<div class="ph-row"><div class="ph-row-h"><span>Rhythm</span></div>`+
|
|
863
|
-
`<div class="ph-row-v">${esc(rhythmLabel)}</div>`+
|
|
864
|
-
(persona.rhythm?`<div class="ph-row-d">${esc(persona.rhythm.blurb)}</div>`:'')+
|
|
865
|
-
`</div>`+
|
|
866
|
-
(sides.length?`<div class="ph-row"><div class="ph-row-h"><span>Also leaning</span></div>`+
|
|
867
|
-
`<div class="ph-also">${sides.map((s)=>`<mark>${esc(s.label)}</mark>`).join('')}</div></div>`:'');
|
|
868
|
-
// ---- "all types" catalog (indexed lists; each item shows its blurb on hover) ----
|
|
869
|
-
const item=(t,o)=>`<div class="ph-item${o.primary?' is-primary':o.you?' is-you':''}" data-label="${da(t.label)}" data-blurb="${da(t.blurb)}"><span class="ph-ix">${String(o.i+1).padStart(2,'0')}</span><span class="ph-nm">${esc(t.label)}</span>${(o.tag&&(o.you||o.primary))?'<span class="ph-you-tag">you</span>':''}</div>`;
|
|
870
|
-
const traitItems=Object.values(PERSONA_TRAITS).map((t,i)=>item(t,{you:sideLabels.has(t.label),primary:t.label===primaryLabel,i,tag:false})).join('');
|
|
871
|
-
const rhythmItems=Object.values(PERSONA_RHYTHMS).map((t,i)=>item(t,{you:t.label===rhythmLabel,primary:false,i,tag:true})).join('');
|
|
872
|
-
// rank as a podium — ascending steps you climb; your rung is the tall blue bar
|
|
873
|
-
const pod=(t,i)=>{ const you=t.label===rankLabel, cls=you?'is-you':(i<=rankIdx?'reached':'locked'), h=34+i*15;
|
|
874
|
-
return `<div class="ph-pod ${cls}" data-label="${da(t.label)}" data-blurb="${da(t.blurb)}">`+
|
|
875
|
-
(you?`<span class="ph-pod-you">you</span>`:'')+
|
|
876
|
-
`<span class="ph-pod-bar" style="height:${h}px"><span class="ph-pod-lvl">${i+1}</span></span>`+
|
|
877
|
-
`<span class="ph-pod-nm">${esc(t.label)}</span></div>`; };
|
|
878
|
-
const rankPods=rankList.map((t,i)=>pod(t,i)).join('');
|
|
879
|
-
frHost.innerHTML =
|
|
880
|
-
`<div class="ph-you">${youPaneHtml}</div>`+
|
|
881
|
-
`<div class="ph-all" hidden>`+
|
|
882
|
-
`<div class="ph-cat-h">Traits <span>hover to read</span></div><div class="ph-list">${traitItems}</div>`+
|
|
883
|
-
`<div class="ph-cat-h">Rank <span>level ${rankIdx+1} / ${rankList.length}</span></div><div class="ph-podium">${rankPods}</div>`+
|
|
884
|
-
`<div class="ph-cat-h">Rhythm <span>your cadence</span></div><div class="ph-list">${rhythmItems}</div>`+
|
|
885
|
-
`</div>`+
|
|
886
|
-
`<button class="ph-toggle" type="button">see all types →</button>`;
|
|
887
|
-
const toggle=frHost.querySelector('.ph-toggle');
|
|
888
|
-
const youPane=frHost.querySelector('.ph-you');
|
|
889
|
-
const allPane=frHost.querySelector('.ph-all');
|
|
890
|
-
if(toggle && youPane && allPane){
|
|
891
|
-
toggle.addEventListener('click', (e)=>{
|
|
892
|
-
e.stopPropagation();
|
|
893
|
-
const showAll=allPane.hasAttribute('hidden');
|
|
894
|
-
allPane.toggleAttribute('hidden', !showAll);
|
|
895
|
-
youPane.toggleAttribute('hidden', showAll);
|
|
896
|
-
toggle.textContent = showAll ? '← back to you' : 'see all types →';
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
581
|
}
|
|
900
582
|
function buildSetupRepos(report){
|
|
901
583
|
const split=providerSplit(report);
|
|
@@ -927,7 +609,6 @@ function buildSetupRepos(report){
|
|
|
927
609
|
sessions:Number(repo.sessions)||0,
|
|
928
610
|
codexPct, claudePct:100-codexPct,
|
|
929
611
|
pin:i<2,
|
|
930
|
-
daily:Array.isArray(repo.daily)?repo.daily:null, // [[localDay, tokens], ...] for grow-over-time
|
|
931
612
|
};
|
|
932
613
|
});
|
|
933
614
|
}
|
|
@@ -944,7 +625,6 @@ function applySetupReport(report){
|
|
|
944
625
|
const days=Math.max(1, Math.round(Number(cf.daysGained)||0));
|
|
945
626
|
document.querySelectorAll('.setup-persona-name,.persona-card .name,.v7-personaName').forEach((el)=>{ el.textContent=persona.primary.label; });
|
|
946
627
|
document.querySelectorAll('.setup-rank,.persona-card .rankrhythm,.v7-rank').forEach((el)=>{ el.textContent=`${persona.rank.label} · ${persona.rhythm.label}`; });
|
|
947
|
-
const frRank=document.getElementById('fr-rank'); if(frRank) frRank.innerHTML=`You are <b>${persona.rank.label}</b> on <b>${persona.rhythm.label}</b>`;
|
|
948
628
|
document.querySelectorAll('.setup-combo,.persona-card .combo,.v7-combo').forEach((el)=>{ el.innerHTML=persona.sides.map((s)=>`<span class="ct">${s.label}</span>`).join(''); });
|
|
949
629
|
renderPersonaHover(persona);
|
|
950
630
|
renderV7PersonaHover(persona);
|
|
@@ -957,37 +637,10 @@ function applySetupReport(report){
|
|
|
957
637
|
const v7Tokens=document.getElementById('v7-total-tokens'); if(v7Tokens) v7Tokens.textContent=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
958
638
|
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent=`${dirty}%`;
|
|
959
639
|
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent='repeated reads';
|
|
960
|
-
// ---- setup-split field report (right half) ----
|
|
961
|
-
const frName=document.getElementById('fr-name'); if(frName) frName.textContent=persona.primary.label;
|
|
962
|
-
const frAvatar=document.getElementById('fr-avatar');
|
|
963
|
-
if(frAvatar){
|
|
964
|
-
const key=String(persona.primary&&persona.primary.label||'').toLowerCase();
|
|
965
|
-
if(PERSONA_TRAITS[key]){ frAvatar.src=`./personality_stickers/${stickerFor(key)}.png`; frAvatar.alt=`${key} avatar`; }
|
|
966
|
-
}
|
|
967
|
-
const frNo=document.getElementById('fr-no'); if(frNo && repoCount) frNo.textContent=`№ ${repoCount}`;
|
|
968
|
-
const frRepos=document.getElementById('fr-repos'); if(frRepos && repoCount) frRepos.textContent=`${repoCount} ${repoCount===1?'repo':'repos'}`;
|
|
969
|
-
const frHours=document.getElementById('fr-hours'); if(frHours) frHours.textContent=`${attention} hours`;
|
|
970
|
-
const frTokens=document.getElementById('fr-tokens'); if(frTokens) frTokens.textContent=`${fmtTok(Number(report?.scale?.totalTokens)||0)} tokens`;
|
|
971
|
-
const frCost=document.getElementById('fr-cost'); if(frCost) frCost.textContent=cost;
|
|
972
|
-
const frMetricValue=document.getElementById('fr-metric-value'); if(frMetricValue) frMetricValue.textContent=`${dirty}%`;
|
|
973
640
|
const v7MetricSay=document.getElementById('v7-metric-say');
|
|
974
641
|
if(v7MetricSay) v7MetricSay.innerHTML="Your agents re-load context they've already seen — unchanged files, the same rules, stale maps — every session. That's context rot. It's <b>how coding agents work today</b>, and it's the haze on your city.";
|
|
975
642
|
const splitHtml=`<span>${split.codexPct}% Codex</span><span>${split.claudePct}% Claude</span>`;
|
|
976
643
|
const setupSplit=document.querySelector('.setup-split'); if(setupSplit) setupSplit.innerHTML=splitHtml;
|
|
977
|
-
// city Claude/Codex split bar + legend
|
|
978
|
-
const csClaude=document.querySelector('#city-split .cs-claude'); if(csClaude) csClaude.style.width=`${split.claudePct}%`;
|
|
979
|
-
const csCodex=document.querySelector('#city-split .cs-codex'); if(csCodex) csCodex.style.width=`${split.codexPct}%`;
|
|
980
|
-
const csClB=document.querySelector('#city-split .cs-l.claude b'); if(csClB) csClB.textContent=`${split.claudePct}%`;
|
|
981
|
-
const csCdB=document.querySelector('#city-split .cs-l.codex b'); if(csCdB) csCdB.textContent=`${split.codexPct}%`;
|
|
982
|
-
// timeline: first session -> today
|
|
983
|
-
const tlStartDate=new Date(report?.scanStartDate);
|
|
984
|
-
if(!Number.isNaN(tlStartDate.getTime())){
|
|
985
|
-
const tlStartEl=document.querySelector('#city-timeline .tl-start');
|
|
986
|
-
if(tlStartEl) tlStartEl.textContent=tlStartDate.toLocaleString('en-US',{month:'short', year:'numeric', timeZone:'UTC'});
|
|
987
|
-
}
|
|
988
|
-
const spanDays=daysBetween(report?.scanStartDate, report?.scanEndDate);
|
|
989
|
-
const spanLabel = spanDays>=60 ? `${Math.round(spanDays/30.44)} months with AI` : `${spanDays} day${spanDays===1?'':'s'} with AI`;
|
|
990
|
-
const tlSpanEl=document.querySelector('#city-timeline .tl-span'); if(tlSpanEl) tlSpanEl.textContent=spanLabel;
|
|
991
644
|
const dirtyPct=document.querySelector('.dirty-context .pct');
|
|
992
645
|
if(dirtyPct) dirtyPct.innerHTML=`<span id="setup-metric-value">${dirty}%</span><span class="word" id="setup-metric-label">repeated reads</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>`;
|
|
993
646
|
const dirtyDesc=document.querySelector('.dirty-context .desc');
|
|
@@ -997,13 +650,8 @@ function applySetupReport(report){
|
|
|
997
650
|
const cfHero=document.querySelector('.cf-hero'); if(cfHero) cfHero.innerHTML=`Echo saves you <b>${days} days</b>`;
|
|
998
651
|
const cfSay=document.querySelector('.cf-say'); if(cfSay) cfSay.innerHTML=`You'd have reached today's workspace on <b>${reach.label}</b>, not ${instead}.`;
|
|
999
652
|
signpostTokens=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
// the "WASTED" line follows the metric too: hours / % / $ of repeated-read waste (same numbers as the cost quote)
|
|
1003
|
-
const dirtyShare=Math.max(0, Number(dirty)||0)/100;
|
|
1004
|
-
signpostWastedTime=`≈${Math.round((Number(attention)||0)*dirtyShare)}h WASTED`;
|
|
1005
|
-
signpostWastedPct=`${Math.round(Number(dirty))||0}% WASTED`;
|
|
1006
|
-
signpostWastedCost=`≈${money((Number(report?.cost?.total)||0)*dirtyShare)} WASTED`;
|
|
653
|
+
const inputPct=Number(report?.scale?.totalTokens) ? (Number(report.scale.totalInputTokens||0)/Number(report.scale.totalTokens)*100).toFixed(2) : '0.00';
|
|
654
|
+
signpostInputPct=`${inputPct}% INPUT`;
|
|
1007
655
|
}
|
|
1008
656
|
async function loadSetupReport(){
|
|
1009
657
|
const nonce=pageParams.get('nonce');
|
|
@@ -1022,11 +670,7 @@ async function loadSetupReport(){
|
|
|
1022
670
|
return null;
|
|
1023
671
|
}
|
|
1024
672
|
let signpostTokens='7.49B';
|
|
1025
|
-
let
|
|
1026
|
-
let signpostCost='$13,689';
|
|
1027
|
-
let signpostWastedTime='≈162h WASTED';
|
|
1028
|
-
let signpostWastedPct='85% WASTED';
|
|
1029
|
-
let signpostWastedCost='≈$11,636 WASTED';
|
|
673
|
+
let signpostInputPct='99.39% INPUT';
|
|
1030
674
|
const setupReport=await loadSetupReport();
|
|
1031
675
|
applySetupReport(setupReport);
|
|
1032
676
|
renderV7PersonaHover();
|
|
@@ -1262,44 +906,30 @@ function addCube(o){
|
|
|
1262
906
|
group.userData=o; scene.add(group); return group;
|
|
1263
907
|
}
|
|
1264
908
|
// repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
|
|
1265
|
-
// name = small tag just above the bar top;
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if(cityMetric==='cost') return '$'+Math.round(cost).toLocaleString('en-US');
|
|
1271
|
-
return `${(Number(hours)||0).toFixed(1)}h`; // time (default)
|
|
1272
|
-
}
|
|
1273
|
-
function repoMetricText(o){ return fmtMetric(Number(o.hours)||0, Number(o.tokens)||0, (Number(o.cost)||0)*(setupMode?1:COST_SCALE)); }
|
|
1274
|
-
// the same metric scaled to a point in time (ratio = cumulative tokens / total) — used by the timeline scrubber
|
|
1275
|
-
function repoMetricTextAt(o, ratio){ return fmtMetric((Number(o.hours)||0)*ratio, (Number(o.tokens)||0)*ratio, (Number(o.cost)||0)*(setupMode?1:COST_SCALE)*ratio); }
|
|
1276
|
-
function drawRepoLabel(e, valueText){
|
|
1277
|
-
const {o, cv, g, tex, PW, PH}=e;
|
|
1278
|
-
g.clearRect(0,0,PW,PH);
|
|
1279
|
-
g.textAlign='center';
|
|
909
|
+
// name = small tag just above the bar top; tokens + $ fly higher above it, clear of the cube.
|
|
910
|
+
function addTopLabel(o, numbers=true, lift=0){
|
|
911
|
+
const PW=320, PH=280;
|
|
912
|
+
const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
|
|
913
|
+
const g=cv.getContext('2d'); g.textAlign='center';
|
|
1280
914
|
g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
+
}
|
|
1284
923
|
// name — small tag sitting just above the bar top
|
|
1285
924
|
g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
|
|
1286
925
|
const fw=g.measureText(o.name).width, maxw=PW*0.94;
|
|
1287
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();
|
|
1288
|
-
tex.needsUpdate=true;
|
|
1289
|
-
}
|
|
1290
|
-
function addTopLabel(o){
|
|
1291
|
-
const PW=320, PH=280;
|
|
1292
|
-
const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
|
|
1293
|
-
const g=cv.getContext('2d');
|
|
1294
927
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1295
928
|
const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
|
|
1296
929
|
const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
|
|
1297
930
|
sprite.scale.set(wWorld, hWorld, 1);
|
|
1298
|
-
sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld, o.z); // name tags the top;
|
|
931
|
+
sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld + lift, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
|
|
1299
932
|
scene.add(sprite);
|
|
1300
|
-
const e={o, cv, g, tex, PW, PH, sprite, hWorld};
|
|
1301
|
-
repoLabels.push(e);
|
|
1302
|
-
drawRepoLabel(e);
|
|
1303
933
|
}
|
|
1304
934
|
// fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
|
|
1305
935
|
const grain=(function(){
|
|
@@ -1345,11 +975,16 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
|
|
|
1345
975
|
plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
|
|
1346
976
|
})();
|
|
1347
977
|
|
|
1348
|
-
|
|
978
|
+
// De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
|
|
979
|
+
// its name tag (hover still shows the full repo detail card). Kills the mid-air label collisions.
|
|
980
|
+
const NUMBERED_LABELS=4;
|
|
981
|
+
const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0,NUMBERED_LABELS);
|
|
982
|
+
const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
|
|
983
|
+
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); });
|
|
1349
984
|
|
|
1350
985
|
// bottom-right standing "signpost": tray summary — total tokens + how much is input
|
|
1351
|
-
let signpostCanvas=null, signpostTexture=null
|
|
1352
|
-
function drawSignpost(
|
|
986
|
+
let signpostCanvas=null, signpostTexture=null;
|
|
987
|
+
function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
1353
988
|
if(!signpostCanvas) return;
|
|
1354
989
|
const W=signpostCanvas.width, H=signpostCanvas.height;
|
|
1355
990
|
const g=signpostCanvas.getContext('2d');
|
|
@@ -1357,48 +992,18 @@ function drawSignpost(bigText, subLabel, bottomText, accent='#e8473f'){
|
|
|
1357
992
|
g.fillStyle='rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.fill();
|
|
1358
993
|
g.strokeStyle='rgba(120,126,140,0.30)'; g.lineWidth=3; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.stroke();
|
|
1359
994
|
g.textAlign='center';
|
|
1360
|
-
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(
|
|
1361
|
-
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(
|
|
995
|
+
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(tokenText, W/2, 150);
|
|
996
|
+
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText('TOTAL TOKENS', W/2, 204);
|
|
1362
997
|
g.fillStyle=accent; g.font='800 62px "IBM Plex Mono", monospace'; g.fillText(bottomText, W/2, 280);
|
|
1363
998
|
if(signpostTexture) signpostTexture.needsUpdate=true;
|
|
1364
999
|
}
|
|
1365
|
-
// the signpost headline + sublabel follow the same city metric as the cube labels
|
|
1366
|
-
function signpostBig(){ return cityMetric==='tokens'?signpostTokens : cityMetric==='cost'?signpostCost : signpostHours; }
|
|
1367
|
-
function signpostSub(){ return cityMetric==='tokens'?'TOTAL TOKENS' : cityMetric==='cost'?'TOTAL SPENT' : 'TOTAL TIME'; }
|
|
1368
|
-
function signpostBottom(){ return cityMetric==='tokens'?signpostWastedPct : cityMetric==='cost'?signpostWastedCost : signpostWastedTime; }
|
|
1369
|
-
function redrawCityMetric(){
|
|
1370
|
-
for(const e of repoLabels) drawRepoLabel(e);
|
|
1371
|
-
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
1372
|
-
dirty=true;
|
|
1373
|
-
}
|
|
1374
|
-
// animated metric switch: dip the labels + signpost to 0, swap the numbers at the bottom, fade back up
|
|
1375
|
-
let animId=0;
|
|
1376
|
-
function setCityMetric(m){
|
|
1377
|
-
if(m===cityMetric) return;
|
|
1378
|
-
const id=++animId;
|
|
1379
|
-
const items=repoLabels.map((e)=>e.sprite).filter(Boolean);
|
|
1380
|
-
if(signpostMesh) items.push(signpostMesh);
|
|
1381
|
-
const dur=420, start=performance.now();
|
|
1382
|
-
let swapped=false;
|
|
1383
|
-
function step(now){
|
|
1384
|
-
if(id!==animId) return; // a newer switch superseded this one
|
|
1385
|
-
const t=Math.min(1, (now-start)/dur);
|
|
1386
|
-
const o = t<0.5 ? 1-t/0.5 : (t-0.5)/0.5; // opacity dips to 0 at the midpoint, back to 1
|
|
1387
|
-
const eased = o*o*(3-2*o); // smoothstep
|
|
1388
|
-
items.forEach((s)=>{ if(s.material) s.material.opacity=eased; });
|
|
1389
|
-
if(!swapped && t>=0.5){ swapped=true; cityMetric=m; redrawCityMetric(); }
|
|
1390
|
-
dirty=true;
|
|
1391
|
-
if(t<1) requestAnimationFrame(step);
|
|
1392
|
-
}
|
|
1393
|
-
requestAnimationFrame(step);
|
|
1394
|
-
}
|
|
1395
1000
|
(function(){
|
|
1396
1001
|
const W=640, H=320;
|
|
1397
1002
|
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
1398
1003
|
signpostCanvas=cv;
|
|
1399
1004
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1400
1005
|
signpostTexture=tex;
|
|
1401
|
-
drawSignpost(
|
|
1006
|
+
drawSignpost(signpostTokens, signpostInputPct);
|
|
1402
1007
|
const SIGN_W=152, SIGN_H=76, SIGN_Y=TRAY_TOP+64;
|
|
1403
1008
|
const sign=new THREE.Mesh(
|
|
1404
1009
|
new THREE.PlaneGeometry(SIGN_W, SIGN_H),
|
|
@@ -1408,7 +1013,6 @@ function setCityMetric(m){
|
|
|
1408
1013
|
sign.position.set(sx, SIGN_Y, sz);
|
|
1409
1014
|
sign.rotation.y=Math.PI/4; // fixed in the city, facing the default isometric camera instead of billboarding
|
|
1410
1015
|
scene.add(sign);
|
|
1411
|
-
signpostMesh=sign;
|
|
1412
1016
|
const POST_OVERLAP=4; // tiny tuck into the board so the pole feels attached, not floating
|
|
1413
1017
|
const postH=Math.max(4, SIGN_Y - SIGN_H/2 - TRAY_TOP + POST_OVERLAP);
|
|
1414
1018
|
const post=new THREE.Mesh(new THREE.CylinderGeometry(2.2,2.2,postH,12),
|
|
@@ -1416,133 +1020,6 @@ function setCityMetric(m){
|
|
|
1416
1020
|
post.position.set(sx, TRAY_TOP+postH/2, sz); post.castShadow=true; scene.add(post);
|
|
1417
1021
|
})();
|
|
1418
1022
|
|
|
1419
|
-
// city metric toggle: sliding pill + animated metric swap on the city
|
|
1420
|
-
(function(){
|
|
1421
|
-
const ct=document.getElementById('city-toggle');
|
|
1422
|
-
if(!ct) return;
|
|
1423
|
-
const thumb=ct.querySelector('.ct-thumb');
|
|
1424
|
-
const moveThumb=(btn)=>{ if(!thumb||!btn) return;
|
|
1425
|
-
thumb.style.width=btn.offsetWidth+'px'; thumb.style.height=btn.offsetHeight+'px';
|
|
1426
|
-
thumb.style.transform=`translate(${btn.offsetLeft}px, ${btn.offsetTop}px)`; };
|
|
1427
|
-
// place the pill under the default button without animating in
|
|
1428
|
-
requestAnimationFrame(()=>{
|
|
1429
|
-
const on=ct.querySelector('button.on')||ct.querySelector('button[data-metric]');
|
|
1430
|
-
if(thumb) thumb.style.transition='none';
|
|
1431
|
-
moveThumb(on);
|
|
1432
|
-
requestAnimationFrame(()=>{ if(thumb) thumb.style.transition=''; });
|
|
1433
|
-
});
|
|
1434
|
-
ct.addEventListener('click', (e)=>{
|
|
1435
|
-
const b=e.target.closest('button[data-metric]'); if(!b) return;
|
|
1436
|
-
ct.querySelectorAll('button').forEach((x)=>x.classList.toggle('on', x===b));
|
|
1437
|
-
moveThumb(b);
|
|
1438
|
-
setCityMetric(b.getAttribute('data-metric'));
|
|
1439
|
-
});
|
|
1440
|
-
})();
|
|
1441
|
-
|
|
1442
|
-
// ===== timeline scrubber: drag the handle to grow the city from the first session to today =====
|
|
1443
|
-
(function(){
|
|
1444
|
-
const tl=document.getElementById('city-timeline');
|
|
1445
|
-
const track=document.getElementById('tl-track');
|
|
1446
|
-
const handle=document.getElementById('tl-handle');
|
|
1447
|
-
const bubble=document.getElementById('tl-bubble');
|
|
1448
|
-
if(!tl||!track||!handle) return;
|
|
1449
|
-
const parseDay=(s)=>{ if(!s) return null; const d=new Date(s); return Number.isNaN(d.getTime())?null:d.getTime(); };
|
|
1450
|
-
let axisStart=parseDay(setupReport&&setupReport.scanStartDate);
|
|
1451
|
-
let axisEnd=parseDay(setupReport&&setupReport.scanEndDate);
|
|
1452
|
-
if(axisStart==null||axisEnd==null||axisEnd<=axisStart){ axisEnd=Date.parse(new Date().toISOString().slice(0,10)); axisStart=axisEnd-240*864e5; }
|
|
1453
|
-
const span=Math.max(1, axisEnd-axisStart);
|
|
1454
|
-
// per-cube cumulative model: prefix-sum of the daily series; fallback = staggered linear growth (demo / missing data)
|
|
1455
|
-
const cubes=all.map((o,i)=>{
|
|
1456
|
-
const total=Number(o.tokens)||0;
|
|
1457
|
-
let cum=null;
|
|
1458
|
-
if(Array.isArray(o.daily)&&o.daily.length){
|
|
1459
|
-
const rows=o.daily.map((p)=>[parseDay(p[0]), Number(p[1])||0]).filter((r)=>r[0]!=null).sort((a,b)=>a[0]-b[0]);
|
|
1460
|
-
let s=0; cum=rows.map((r)=>{ s+=r[1]; return [r[0], s]; });
|
|
1461
|
-
}
|
|
1462
|
-
return {o, i, total, cum};
|
|
1463
|
-
});
|
|
1464
|
-
const fallbackStart=(i)=>(i*0.137)%0.66; // each repo "appears" at a staggered point along the span
|
|
1465
|
-
function ratioAt(c, t){
|
|
1466
|
-
if(c.total<=0) return 0;
|
|
1467
|
-
if(!c.cum){ const f=(t-axisStart)/span, st=fallbackStart(c.i); return Math.max(0, Math.min(1,(f-st)/(1-st))); }
|
|
1468
|
-
let lo=0, hi=c.cum.length-1, idx=-1;
|
|
1469
|
-
while(lo<=hi){ const mid=(lo+hi)>>1; if(c.cum[mid][0]<=t){ idx=mid; lo=mid+1; } else hi=mid-1; }
|
|
1470
|
-
let cumTok;
|
|
1471
|
-
if(idx<0){ cumTok=0; } // before the repo's first active day → not built yet
|
|
1472
|
-
else {
|
|
1473
|
-
const cur=c.cum[idx], nxt=c.cum[idx+1];
|
|
1474
|
-
if(!nxt){ cumTok=cur[1]; } // at/after the last recorded day → final size
|
|
1475
|
-
else { // interpolate cumulative across the gap → buildings grow smoothly, not in steps
|
|
1476
|
-
const f=Math.max(0, Math.min(1, (t-cur[0])/Math.max(1, nxt[0]-cur[0])));
|
|
1477
|
-
cumTok=cur[1]+(nxt[1]-cur[1])*f;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
return Math.max(0, Math.min(1, cumTok/c.total));
|
|
1481
|
-
}
|
|
1482
|
-
const fmtDate=(t)=>new Date(t).toLocaleString('en-US',{month:'short', day:'numeric', year:'numeric', timeZone:'UTC'});
|
|
1483
|
-
const labelByO=new Map(repoLabels.map((e)=>[e.o, e]));
|
|
1484
|
-
// drop the shadow map to a cheap resolution while scrubbing (re-renders ~16× cheaper at 49-repo scale), crisp on release
|
|
1485
|
-
function setShadowRes(n){ key.shadow.mapSize.set(n,n); if(key.shadow.map){ key.shadow.map.dispose(); key.shadow.map=null; } key.shadow.needsUpdate=true; }
|
|
1486
|
-
const labelY=(le, s)=> TRAY_TOP + s*((Number(le.o.h)||0)-3) + 6 + 0.36*le.hWorld; // label tracks the scaled cube top
|
|
1487
|
-
let lastDayKey=null, lastTextT=-1;
|
|
1488
|
-
function applyAt(frac){
|
|
1489
|
-
const atToday = frac>=0.999;
|
|
1490
|
-
const t=axisStart + frac*span;
|
|
1491
|
-
const now=performance.now();
|
|
1492
|
-
const redrawText = atToday || (now - lastTextT > 100); // throttle label canvas + shadow redraws to ~10fps (cube scaling stays 60fps)
|
|
1493
|
-
if(redrawText){ lastTextT=now; key.shadow.needsUpdate=true; } // re-render the shadow map so shadows grow with the buildings
|
|
1494
|
-
let cumH=0, cumTok=0, cumCost=0;
|
|
1495
|
-
for(const c of cubes){
|
|
1496
|
-
const r = atToday ? 1 : ratioAt(c, t);
|
|
1497
|
-
cumH += (Number(c.o.hours)||0)*r;
|
|
1498
|
-
cumTok += (Number(c.o.tokens)||0)*r;
|
|
1499
|
-
cumCost += (Number(c.o.cost)||0)*(setupMode?1:COST_SCALE)*r;
|
|
1500
|
-
const grp=c.o.mesh, le=labelByO.get(c.o);
|
|
1501
|
-
if(!grp) continue;
|
|
1502
|
-
if(r<=0.0008){ grp.visible=false; if(le&&le.sprite) le.sprite.visible=false; continue; }
|
|
1503
|
-
grp.visible=true;
|
|
1504
|
-
const s=Math.cbrt(r);
|
|
1505
|
-
grp.scale.set(s,s,s);
|
|
1506
|
-
grp.position.set(c.o.x*(1-s), TRAY_TOP*(1-s), c.o.z*(1-s)); // scale about each cube's base, so it rises in place
|
|
1507
|
-
if(le&&le.sprite){ le.sprite.visible=true; le.sprite.position.y=labelY(le, s); if(redrawText) drawRepoLabel(le, repoMetricTextAt(c.o, r)); }
|
|
1508
|
-
}
|
|
1509
|
-
const dayKey=new Date(t).toISOString().slice(0,10);
|
|
1510
|
-
if(redrawText || dayKey!==lastDayKey){ lastDayKey=dayKey; drawSignpost(fmtMetric(cumH,cumTok,cumCost), signpostSub(), fmtDate(t).toUpperCase()); }
|
|
1511
|
-
if(bubble) bubble.textContent=fmtDate(t);
|
|
1512
|
-
dirty=true;
|
|
1513
|
-
}
|
|
1514
|
-
function restore(){
|
|
1515
|
-
for(const c of cubes){
|
|
1516
|
-
const grp=c.o.mesh; if(grp){ grp.visible=true; grp.scale.set(1,1,1); grp.position.set(0,0,0); }
|
|
1517
|
-
const le=labelByO.get(c.o); if(le&&le.sprite){ le.sprite.visible=true; le.sprite.position.y=labelY(le, 1); drawRepoLabel(le); }
|
|
1518
|
-
}
|
|
1519
|
-
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
1520
|
-
setShadowRes(4096); // settle crisp shadows on the full-size city
|
|
1521
|
-
dirty=true;
|
|
1522
|
-
}
|
|
1523
|
-
const fracFromX=(clientX)=>{ const r=track.getBoundingClientRect(); return Math.max(0, Math.min(1,(clientX-r.left)/Math.max(1,r.width))); };
|
|
1524
|
-
const setHandle=(frac)=>{ handle.style.left=(frac*100)+'%'; };
|
|
1525
|
-
let dragging=false;
|
|
1526
|
-
function onMove(e){ if(!dragging) return; const f=fracFromX(e.clientX); setHandle(f); applyAt(f); }
|
|
1527
|
-
function onUp(){
|
|
1528
|
-
if(!dragging) return; dragging=false; tl.classList.remove('dragging');
|
|
1529
|
-
window.removeEventListener('pointermove',onMove); window.removeEventListener('pointerup',onUp);
|
|
1530
|
-
const from=Math.max(0, Math.min(1,(parseFloat(handle.style.left)||100)/100)); // spring back to today
|
|
1531
|
-
const start=performance.now(), dur=480;
|
|
1532
|
-
(function back(now){
|
|
1533
|
-
const t=Math.min(1,(now-start)/dur), e=1-Math.pow(1-t,3);
|
|
1534
|
-
const f=from+(1-from)*e; setHandle(f); applyAt(f);
|
|
1535
|
-
if(t<1) requestAnimationFrame(back); else { setHandle(1); restore(); }
|
|
1536
|
-
})(performance.now());
|
|
1537
|
-
}
|
|
1538
|
-
track.addEventListener('pointerdown', (e)=>{
|
|
1539
|
-
dragging=true; tl.classList.add('dragging'); lastDayKey=null; lastTextT=-1; setShadowRes(1024);
|
|
1540
|
-
const f=fracFromX(e.clientX); setHandle(f); applyAt(f);
|
|
1541
|
-
window.addEventListener('pointermove',onMove); window.addEventListener('pointerup',onUp);
|
|
1542
|
-
e.preventDefault();
|
|
1543
|
-
});
|
|
1544
|
-
})();
|
|
1545
|
-
|
|
1546
1023
|
const MINZ=0.4, MAXZ=2.8; // lower floor → can zoom out to ~2/3 of the previous limit
|
|
1547
1024
|
const controls=new OrbitControls(camera, renderer.domElement);
|
|
1548
1025
|
controls.target.copy(TARGET);
|
|
@@ -1568,7 +1045,7 @@ loop();
|
|
|
1568
1045
|
const cardEl=document.querySelector('.share-card');
|
|
1569
1046
|
function fit(){
|
|
1570
1047
|
if(setupMode){
|
|
1571
|
-
VIZ_W=
|
|
1048
|
+
VIZ_W=window.innerWidth; VIZ_H=window.innerHeight;
|
|
1572
1049
|
cardEl.style.transform='none';
|
|
1573
1050
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1574
1051
|
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
@@ -1596,46 +1073,7 @@ if(setupMode){
|
|
|
1596
1073
|
window.setTimeout(finishSetupReveal, 1200);
|
|
1597
1074
|
}
|
|
1598
1075
|
|
|
1599
|
-
|
|
1600
|
-
(function(){
|
|
1601
|
-
const btn=document.getElementById('fr-read-btn');
|
|
1602
|
-
const panel=document.getElementById('fr-persona-hover');
|
|
1603
|
-
if(!btn || !panel) return;
|
|
1604
|
-
const tip=document.createElement('div'); tip.className='fr-catTip';
|
|
1605
|
-
tip.innerHTML='<img class="ct-img" alt=""><div class="ct-body"><span class="tl"></span><span class="bd"></span></div>';
|
|
1606
|
-
document.body.appendChild(tip);
|
|
1607
|
-
const hideTip=()=>tip.classList.remove('show');
|
|
1608
|
-
const showTip=(el)=>{
|
|
1609
|
-
const blurb=el.getAttribute('data-blurb'); if(!blurb) return;
|
|
1610
|
-
const label=el.getAttribute('data-label')||'', key=label.toLowerCase();
|
|
1611
|
-
const img=tip.querySelector('.ct-img');
|
|
1612
|
-
if(PERSONA_TRAITS[key]){ img.src=`./personality_stickers/${stickerFor(key)}.png`; tip.classList.add('has-img'); }
|
|
1613
|
-
else tip.classList.remove('has-img');
|
|
1614
|
-
tip.querySelector('.tl').textContent=label;
|
|
1615
|
-
tip.querySelector('.bd').textContent=blurb;
|
|
1616
|
-
const r=el.getBoundingClientRect(), tw=tip.offsetWidth, th=tip.offsetHeight;
|
|
1617
|
-
let left=r.left - tw - 12; if(left<8) left=Math.min(r.right + 12, window.innerWidth - tw - 8);
|
|
1618
|
-
let top=Math.max(8, Math.min(r.top + r.height/2 - th/2, window.innerHeight - th - 8));
|
|
1619
|
-
tip.style.left=left+'px'; tip.style.top=top+'px';
|
|
1620
|
-
tip.classList.add('show');
|
|
1621
|
-
};
|
|
1622
|
-
const setOpen=(open)=>{
|
|
1623
|
-
panel.classList.toggle('open', open);
|
|
1624
|
-
btn.setAttribute('aria-expanded', String(open));
|
|
1625
|
-
if(open){ // always reopen on the "this is you" pane
|
|
1626
|
-
const youPane=panel.querySelector('.ph-you'), allPane=panel.querySelector('.ph-all'), tg=panel.querySelector('.ph-toggle');
|
|
1627
|
-
if(youPane && allPane && tg){ youPane.removeAttribute('hidden'); allPane.setAttribute('hidden',''); tg.textContent='see all types →'; }
|
|
1628
|
-
} else { hideTip(); }
|
|
1629
|
-
};
|
|
1630
|
-
btn.addEventListener('click', (e)=>{ e.stopPropagation(); setOpen(!panel.classList.contains('open')); });
|
|
1631
|
-
document.addEventListener('click', (e)=>{ if(e.target!==btn && !panel.contains(e.target)) setOpen(false); });
|
|
1632
|
-
window.addEventListener('keydown', (e)=>{ if(e.key==='Escape') setOpen(false); });
|
|
1633
|
-
panel.addEventListener('mouseover', (e)=>{ const el=e.target.closest && e.target.closest('.ph-item,.ph-pod'); if(el) showTip(el); });
|
|
1634
|
-
panel.addEventListener('mouseout', (e)=>{ const el=e.target.closest && e.target.closest('.ph-item,.ph-pod'); if(el) hideTip(); });
|
|
1635
|
-
panel.addEventListener('scroll', hideTip);
|
|
1636
|
-
})();
|
|
1637
|
-
|
|
1638
|
-
document.querySelectorAll('#setup-connect,#v7-connect,#fr-connect').forEach((connectButton)=>{
|
|
1076
|
+
document.querySelectorAll('#setup-connect,#v7-connect').forEach((connectButton)=>{
|
|
1639
1077
|
connectButton.addEventListener('click', (event)=>{
|
|
1640
1078
|
event.stopPropagation();
|
|
1641
1079
|
if(setupMode) window.parent?.postMessage({type:'echomem:connect-click'}, window.location.origin);
|
|
@@ -1704,34 +1142,6 @@ bindMetricCarousel({
|
|
|
1704
1142
|
document.getElementById('v7-cost')?.textContent || '$13,689'
|
|
1705
1143
|
),
|
|
1706
1144
|
});
|
|
1707
|
-
// setup-split field report shares the same rotating cost readout
|
|
1708
|
-
bindMetricCarousel({
|
|
1709
|
-
metric:document.getElementById('fr-metric'),
|
|
1710
|
-
value:document.getElementById('fr-metric-value'),
|
|
1711
|
-
label:document.getElementById('fr-metric-label'),
|
|
1712
|
-
say:document.getElementById('fr-metric-say'),
|
|
1713
|
-
slides:buildMetricSlides(
|
|
1714
|
-
document.getElementById('v7-metric-value')?.textContent || '85.0',
|
|
1715
|
-
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
1716
|
-
document.getElementById('v7-cost')?.textContent || '$13,689'
|
|
1717
|
-
),
|
|
1718
|
-
});
|
|
1719
|
-
// reserve the TALLEST slide's height (measured at the real panel width) so the loop never reflows the column
|
|
1720
|
-
function reserveFrSayHeight(){
|
|
1721
|
-
if(!setupMode) return;
|
|
1722
|
-
const say=document.getElementById('fr-metric-say'); if(!say) return;
|
|
1723
|
-
const slides=buildMetricSlides(
|
|
1724
|
-
document.getElementById('v7-metric-value')?.textContent || '85.0',
|
|
1725
|
-
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
1726
|
-
document.getElementById('v7-cost')?.textContent || '$13,689'
|
|
1727
|
-
);
|
|
1728
|
-
const prev=say.innerHTML; say.style.minHeight='0px'; let max=0;
|
|
1729
|
-
for(const s of slides){ say.innerHTML=s.say; max=Math.max(max, say.offsetHeight); }
|
|
1730
|
-
say.innerHTML=prev;
|
|
1731
|
-
if(max>0) say.style.minHeight=max+'px';
|
|
1732
|
-
}
|
|
1733
|
-
reserveFrSayHeight();
|
|
1734
|
-
window.addEventListener('resize', reserveFrSayHeight);
|
|
1735
1145
|
|
|
1736
1146
|
// ---- repeated-reads info sheet: v7 "how we read your city" disclosure, moved behind the info button ----
|
|
1737
1147
|
const v7InfoBtns=Array.from(document.querySelectorAll('[aria-controls="v7-info-sheet"]'));
|