@echomem/mcp 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/city/echo-ai-city-only.html +643 -35
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/forensics.js +9 -4
- package/dist/setup-page.js +1 -2
- package/package.json +1 -1
|
@@ -3,11 +3,14 @@
|
|
|
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">
|
|
6
8
|
<style>
|
|
7
9
|
:root{
|
|
8
10
|
--ink:#1b1c1f; --muted:#8b8d94; --line:#ececef;
|
|
9
11
|
--mono:"IBM Plex Mono","SFMono-Regular",ui-monospace,Menlo,monospace;
|
|
10
12
|
--sans:"Avenir Next","Helvetica Neue","Segoe UI",system-ui,-apple-system,sans-serif;
|
|
13
|
+
--hand:"Caveat",cursive;
|
|
11
14
|
--blue:#2f6df0; --red:#e8473f; --amber:#f0a83f;
|
|
12
15
|
}
|
|
13
16
|
*{margin:0;padding:0;box-sizing:border-box;}
|
|
@@ -257,6 +260,214 @@
|
|
|
257
260
|
body.setup-mode .v7-foot{display:none;}
|
|
258
261
|
}
|
|
259
262
|
|
|
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
|
+
|
|
260
471
|
/* ===== in-canvas scan loader (setup mode) — loading lives inside the report, matches its type ===== */
|
|
261
472
|
.scan-loading{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
262
473
|
text-align:center;gap:14px;padding:24px;opacity:1;transition:opacity .5s ease;pointer-events:none;}
|
|
@@ -304,11 +515,41 @@
|
|
|
304
515
|
<div class="share-card">
|
|
305
516
|
<canvas id="repo-visualizer"></canvas>
|
|
306
517
|
|
|
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
|
+
|
|
307
548
|
<div class="v7-z v7-persona" tabindex="0" aria-describedby="v7-persona-hover">
|
|
308
549
|
<div class="v7-eyebrow">AI Coding City</div>
|
|
309
550
|
<div class="v7-personaName">bossy</div>
|
|
310
551
|
<div class="v7-rank">the architect · marathoner</div>
|
|
311
|
-
<div class="v7-combo"><span class="ct">loopy</span><span class="ct">
|
|
552
|
+
<div class="v7-combo"><span class="ct">loopy</span><span class="ct">tabby</span><span class="ct">maxxy</span></div>
|
|
312
553
|
<div class="v7-personaHover" id="v7-persona-hover" role="tooltip"></div>
|
|
313
554
|
</div>
|
|
314
555
|
<div class="v7-z v7-head">
|
|
@@ -332,6 +573,31 @@
|
|
|
332
573
|
<div class="micro">Clears the dirty context, so agents stop relearning your workspace.</div>
|
|
333
574
|
</div>
|
|
334
575
|
<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>
|
|
335
601
|
<div class="v7-infoSheet" id="v7-info-sheet" aria-hidden="true">
|
|
336
602
|
<div class="v7-infoCard" role="dialog" aria-modal="true" aria-labelledby="v7-info-title">
|
|
337
603
|
<button class="v7-infoClose" id="v7-info-close" type="button" aria-label="Close">×</button>
|
|
@@ -358,7 +624,7 @@
|
|
|
358
624
|
<div class="setup-eyebrow">AI Coding City</div>
|
|
359
625
|
<div class="setup-persona-name">bossy</div>
|
|
360
626
|
<div class="setup-rank">the architect · marathoner</div>
|
|
361
|
-
<div class="setup-combo"><span class="ct">loopy</span><span class="ct">
|
|
627
|
+
<div class="setup-combo"><span class="ct">loopy</span><span class="ct">tabby</span><span class="ct">maxxy</span></div>
|
|
362
628
|
<div class="persona-cue">hover for the read</div>
|
|
363
629
|
<div class="persona-hover" id="persona-hover" role="tooltip"></div>
|
|
364
630
|
</div>
|
|
@@ -388,7 +654,7 @@
|
|
|
388
654
|
<canvas class="avatar-rive" id="persona-mascot"></canvas>
|
|
389
655
|
<div class="name">bossy</div>
|
|
390
656
|
<div class="rankrhythm">the architect · marathoner</div>
|
|
391
|
-
<div class="combo"><span class="ct">loopy</span><span class="ct">
|
|
657
|
+
<div class="combo"><span class="ct">loopy</span><span class="ct">tabby</span><span class="ct">maxxy</span></div>
|
|
392
658
|
<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>
|
|
393
659
|
</div>
|
|
394
660
|
|
|
@@ -435,8 +701,8 @@ import { OrbitControls } from './vendor/OrbitControls.js';
|
|
|
435
701
|
const pageParams=new URLSearchParams(location.search);
|
|
436
702
|
const setupMode=pageParams.get('setup')==='1';
|
|
437
703
|
if(setupMode) document.body.classList.add('setup-mode');
|
|
438
|
-
let VIZ_W=setupMode ? window.innerWidth : 920;
|
|
439
|
-
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode,
|
|
704
|
+
let VIZ_W=setupMode ? Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620)) : 920; // setup split: city = viewport minus the capped report panel
|
|
705
|
+
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode, left half in setup mode
|
|
440
706
|
const TRAY_COL = 0xece5d3; // warm IVORY — not pure white, not cool grey; separates from the white card
|
|
441
707
|
const TOOL = { codex:0x10a37f, claude:0xd97757 }; // ONLY two cube colours — exact OpenAI / Claude chip-icon hexes
|
|
442
708
|
|
|
@@ -472,13 +738,14 @@ function providerSplit(report){
|
|
|
472
738
|
}
|
|
473
739
|
// ---- coding-persona engine (Phase 1: only signals the scan actually has — no commits / accept-all yet) ----
|
|
474
740
|
const PERSONA_TRAITS={
|
|
475
|
-
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. 🗿"},
|
|
476
|
-
lusty:{label:'lusty', blurb:"burned a car payment in tokens for demos that'll never ship. zero regrets. Anthropic's about to send a thank-you card. 💸"},
|
|
477
|
-
maxxy:{label:'maxxy', blurb:"Opus to rename a variable, Opus to fix a typo. the small models are right there — but you don't do small models, you do consequences."},
|
|
478
|
-
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features, breadth over depth. your brain is a browser with a memory leak. 🫠"},
|
|
479
741
|
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."},
|
|
480
|
-
|
|
742
|
+
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. 💸"},
|
|
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."},
|
|
481
746
|
};
|
|
747
|
+
// maxxy (merged from lusty) wears the lusty character; every other trait uses its own sticker
|
|
748
|
+
const stickerFor=(traitKey)=> traitKey==='maxxy' ? 'lusty' : traitKey;
|
|
482
749
|
const PERSONA_RANKS={
|
|
483
750
|
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."},
|
|
484
751
|
agent:{label:'agent-pilled', blurb:"saw it one-shot something once and never looked back. drives one agent, still watches its every move."},
|
|
@@ -513,8 +780,7 @@ function derivePersona(report){
|
|
|
513
780
|
// measurable traits only — primary = strongest firing signal, sides = the next few
|
|
514
781
|
const scored=[
|
|
515
782
|
{key:'loopy', on:stale>=45, s:stale/100},
|
|
516
|
-
{key:'
|
|
517
|
-
{key:'maxxy', on:opusShare>=0.5, s:opusShare},
|
|
783
|
+
{key:'maxxy', on:opusShare>=0.5 || spend>=1500, s:Math.max(opusShare, Math.min(1,spend/12000))}, // merged: Opus-heavy OR big spender
|
|
518
784
|
{key:'bossy', on:autonomy>=5, s:Math.min(1,autonomy/20)},
|
|
519
785
|
{key:'tabby', on:repoCount>=10, s:Math.min(1,repoCount/30)},
|
|
520
786
|
{key:'ghosty', on:ghostyOn, s:ghostyOn?Math.min(0.8, 1-Math.min(1,commitsPerDay/0.5)):0},
|
|
@@ -551,7 +817,6 @@ function renderPersonaHover(persona){
|
|
|
551
817
|
}
|
|
552
818
|
function renderV7PersonaHover(persona){
|
|
553
819
|
const host=document.getElementById('v7-persona-hover');
|
|
554
|
-
if(!host) return;
|
|
555
820
|
const esc=(t)=>String(t).replace(/[&<>]/g,(c)=>({'&':'&','<':'<','>':'>'}[c]));
|
|
556
821
|
const line=(t)=>t?`<div class="ph-line"><b>${esc(t.label)}</b> — ${esc(t.blurb)}</div>`:'';
|
|
557
822
|
if(!persona){
|
|
@@ -564,9 +829,73 @@ function renderV7PersonaHover(persona){
|
|
|
564
829
|
rhythm:byLabel(PERSONA_RHYTHMS, rankBits[1]),
|
|
565
830
|
};
|
|
566
831
|
}
|
|
567
|
-
|
|
832
|
+
const youHtml = line(persona.primary) + (persona.sides||[]).map(line).join('') +
|
|
568
833
|
(persona.rank?`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>`:'') +
|
|
569
834
|
(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
|
+
}
|
|
570
899
|
}
|
|
571
900
|
function buildSetupRepos(report){
|
|
572
901
|
const split=providerSplit(report);
|
|
@@ -598,6 +927,7 @@ function buildSetupRepos(report){
|
|
|
598
927
|
sessions:Number(repo.sessions)||0,
|
|
599
928
|
codexPct, claudePct:100-codexPct,
|
|
600
929
|
pin:i<2,
|
|
930
|
+
daily:Array.isArray(repo.daily)?repo.daily:null, // [[localDay, tokens], ...] for grow-over-time
|
|
601
931
|
};
|
|
602
932
|
});
|
|
603
933
|
}
|
|
@@ -614,6 +944,7 @@ function applySetupReport(report){
|
|
|
614
944
|
const days=Math.max(1, Math.round(Number(cf.daysGained)||0));
|
|
615
945
|
document.querySelectorAll('.setup-persona-name,.persona-card .name,.v7-personaName').forEach((el)=>{ el.textContent=persona.primary.label; });
|
|
616
946
|
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>`;
|
|
617
948
|
document.querySelectorAll('.setup-combo,.persona-card .combo,.v7-combo').forEach((el)=>{ el.innerHTML=persona.sides.map((s)=>`<span class="ct">${s.label}</span>`).join(''); });
|
|
618
949
|
renderPersonaHover(persona);
|
|
619
950
|
renderV7PersonaHover(persona);
|
|
@@ -626,10 +957,37 @@ function applySetupReport(report){
|
|
|
626
957
|
const v7Tokens=document.getElementById('v7-total-tokens'); if(v7Tokens) v7Tokens.textContent=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
627
958
|
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent=`${dirty}%`;
|
|
628
959
|
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}%`;
|
|
629
973
|
const v7MetricSay=document.getElementById('v7-metric-say');
|
|
630
974
|
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.";
|
|
631
975
|
const splitHtml=`<span>${split.codexPct}% Codex</span><span>${split.claudePct}% Claude</span>`;
|
|
632
976
|
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;
|
|
633
991
|
const dirtyPct=document.querySelector('.dirty-context .pct');
|
|
634
992
|
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>`;
|
|
635
993
|
const dirtyDesc=document.querySelector('.dirty-context .desc');
|
|
@@ -639,8 +997,13 @@ function applySetupReport(report){
|
|
|
639
997
|
const cfHero=document.querySelector('.cf-hero'); if(cfHero) cfHero.innerHTML=`Echo saves you <b>${days} days</b>`;
|
|
640
998
|
const cfSay=document.querySelector('.cf-say'); if(cfSay) cfSay.innerHTML=`You'd have reached today's workspace on <b>${reach.label}</b>, not ${instead}.`;
|
|
641
999
|
signpostTokens=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
642
|
-
|
|
643
|
-
|
|
1000
|
+
signpostHours=`${attention}h`;
|
|
1001
|
+
signpostCost=cost;
|
|
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`;
|
|
644
1007
|
}
|
|
645
1008
|
async function loadSetupReport(){
|
|
646
1009
|
const nonce=pageParams.get('nonce');
|
|
@@ -659,7 +1022,11 @@ async function loadSetupReport(){
|
|
|
659
1022
|
return null;
|
|
660
1023
|
}
|
|
661
1024
|
let signpostTokens='7.49B';
|
|
662
|
-
let
|
|
1025
|
+
let signpostHours='191.1h';
|
|
1026
|
+
let signpostCost='$13,689';
|
|
1027
|
+
let signpostWastedTime='≈162h WASTED';
|
|
1028
|
+
let signpostWastedPct='85% WASTED';
|
|
1029
|
+
let signpostWastedCost='≈$11,636 WASTED';
|
|
663
1030
|
const setupReport=await loadSetupReport();
|
|
664
1031
|
applySetupReport(setupReport);
|
|
665
1032
|
renderV7PersonaHover();
|
|
@@ -895,28 +1262,44 @@ function addCube(o){
|
|
|
895
1262
|
group.userData=o; scene.add(group); return group;
|
|
896
1263
|
}
|
|
897
1264
|
// repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
|
|
898
|
-
// name = small tag just above the bar top; tokens
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1265
|
+
// name = small tag just above the bar top; the chosen metric (time / tokens / $) sits just above it.
|
|
1266
|
+
let cityMetric = setupMode ? 'time' : 'tokens'; // which number the city shows; toggled by the city control
|
|
1267
|
+
const repoLabels=[]; // {o,cv,g,tex,PW,PH} so every label can be redrawn on toggle
|
|
1268
|
+
function fmtMetric(hours, tokens, cost){
|
|
1269
|
+
if(cityMetric==='tokens') return fmtTok(tokens);
|
|
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';
|
|
903
1280
|
g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
|
|
904
|
-
//
|
|
1281
|
+
// the metric — the headline, sits just above the name tag
|
|
905
1282
|
g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
906
|
-
g.fillText(
|
|
907
|
-
// dollars — just under the tokens, also up high
|
|
908
|
-
g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
909
|
-
g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
1283
|
+
g.fillText(valueText!=null?valueText:repoMetricText(o), PW/2, PH*0.66);
|
|
910
1284
|
// name — small tag sitting just above the bar top
|
|
911
1285
|
g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
|
|
912
1286
|
const fw=g.measureText(o.name).width, maxw=PW*0.94;
|
|
913
1287
|
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');
|
|
914
1294
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
915
1295
|
const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
|
|
916
1296
|
const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
|
|
917
1297
|
sprite.scale.set(wWorld, hWorld, 1);
|
|
918
|
-
sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld, o.z); // name tags the top;
|
|
1298
|
+
sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld, o.z); // name tags the top; metric flies higher
|
|
919
1299
|
scene.add(sprite);
|
|
1300
|
+
const e={o, cv, g, tex, PW, PH, sprite, hWorld};
|
|
1301
|
+
repoLabels.push(e);
|
|
1302
|
+
drawRepoLabel(e);
|
|
920
1303
|
}
|
|
921
1304
|
// fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
|
|
922
1305
|
const grain=(function(){
|
|
@@ -965,8 +1348,8 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
|
|
|
965
1348
|
all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o); });
|
|
966
1349
|
|
|
967
1350
|
// bottom-right standing "signpost": tray summary — total tokens + how much is input
|
|
968
|
-
let signpostCanvas=null, signpostTexture=null;
|
|
969
|
-
function drawSignpost(
|
|
1351
|
+
let signpostCanvas=null, signpostTexture=null, signpostMesh=null;
|
|
1352
|
+
function drawSignpost(bigText, subLabel, bottomText, accent='#e8473f'){
|
|
970
1353
|
if(!signpostCanvas) return;
|
|
971
1354
|
const W=signpostCanvas.width, H=signpostCanvas.height;
|
|
972
1355
|
const g=signpostCanvas.getContext('2d');
|
|
@@ -974,18 +1357,48 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
974
1357
|
g.fillStyle='rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.fill();
|
|
975
1358
|
g.strokeStyle='rgba(120,126,140,0.30)'; g.lineWidth=3; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.stroke();
|
|
976
1359
|
g.textAlign='center';
|
|
977
|
-
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(
|
|
978
|
-
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(
|
|
1360
|
+
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(bigText, W/2, 150);
|
|
1361
|
+
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(subLabel, W/2, 204);
|
|
979
1362
|
g.fillStyle=accent; g.font='800 62px "IBM Plex Mono", monospace'; g.fillText(bottomText, W/2, 280);
|
|
980
1363
|
if(signpostTexture) signpostTexture.needsUpdate=true;
|
|
981
1364
|
}
|
|
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
|
+
}
|
|
982
1395
|
(function(){
|
|
983
1396
|
const W=640, H=320;
|
|
984
1397
|
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
985
1398
|
signpostCanvas=cv;
|
|
986
1399
|
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
987
1400
|
signpostTexture=tex;
|
|
988
|
-
drawSignpost(
|
|
1401
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
989
1402
|
const SIGN_W=152, SIGN_H=76, SIGN_Y=TRAY_TOP+64;
|
|
990
1403
|
const sign=new THREE.Mesh(
|
|
991
1404
|
new THREE.PlaneGeometry(SIGN_W, SIGN_H),
|
|
@@ -995,6 +1408,7 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
995
1408
|
sign.position.set(sx, SIGN_Y, sz);
|
|
996
1409
|
sign.rotation.y=Math.PI/4; // fixed in the city, facing the default isometric camera instead of billboarding
|
|
997
1410
|
scene.add(sign);
|
|
1411
|
+
signpostMesh=sign;
|
|
998
1412
|
const POST_OVERLAP=4; // tiny tuck into the board so the pole feels attached, not floating
|
|
999
1413
|
const postH=Math.max(4, SIGN_Y - SIGN_H/2 - TRAY_TOP + POST_OVERLAP);
|
|
1000
1414
|
const post=new THREE.Mesh(new THREE.CylinderGeometry(2.2,2.2,postH,12),
|
|
@@ -1002,6 +1416,133 @@ function drawSignpost(tokenText, bottomText, accent='#e8473f'){
|
|
|
1002
1416
|
post.position.set(sx, TRAY_TOP+postH/2, sz); post.castShadow=true; scene.add(post);
|
|
1003
1417
|
})();
|
|
1004
1418
|
|
|
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
|
+
|
|
1005
1546
|
const MINZ=0.4, MAXZ=2.8; // lower floor → can zoom out to ~2/3 of the previous limit
|
|
1006
1547
|
const controls=new OrbitControls(camera, renderer.domElement);
|
|
1007
1548
|
controls.target.copy(TARGET);
|
|
@@ -1027,7 +1568,7 @@ loop();
|
|
|
1027
1568
|
const cardEl=document.querySelector('.share-card');
|
|
1028
1569
|
function fit(){
|
|
1029
1570
|
if(setupMode){
|
|
1030
|
-
VIZ_W=window.innerWidth; VIZ_H=window.innerHeight;
|
|
1571
|
+
VIZ_W=Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620)); VIZ_H=window.innerHeight; // city = viewport minus capped report panel
|
|
1031
1572
|
cardEl.style.transform='none';
|
|
1032
1573
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1033
1574
|
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
@@ -1055,7 +1596,46 @@ if(setupMode){
|
|
|
1055
1596
|
window.setTimeout(finishSetupReveal, 1200);
|
|
1056
1597
|
}
|
|
1057
1598
|
|
|
1058
|
-
|
|
1599
|
+
// field-report persona breakdown: click to toggle (no hover); catalog items reveal their blurb on hover
|
|
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)=>{
|
|
1059
1639
|
connectButton.addEventListener('click', (event)=>{
|
|
1060
1640
|
event.stopPropagation();
|
|
1061
1641
|
if(setupMode) window.parent?.postMessage({type:'echomem:connect-click'}, window.location.origin);
|
|
@@ -1124,6 +1704,34 @@ bindMetricCarousel({
|
|
|
1124
1704
|
document.getElementById('v7-cost')?.textContent || '$13,689'
|
|
1125
1705
|
),
|
|
1126
1706
|
});
|
|
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);
|
|
1127
1735
|
|
|
1128
1736
|
// ---- repeated-reads info sheet: v7 "how we read your city" disclosure, moved behind the info button ----
|
|
1129
1737
|
const v7InfoBtns=Array.from(document.querySelectorAll('[aria-controls="v7-info-sheet"]'));
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/forensics.js
CHANGED
|
@@ -191,7 +191,7 @@ class Forensics {
|
|
|
191
191
|
const name = repoLabel(cwd);
|
|
192
192
|
let r = this.repos.get(name);
|
|
193
193
|
if (!r) {
|
|
194
|
-
r = { name, cwd: null, sessions: new Set(), assistantMessages: 0, cold: 0, cacheWrite: 0, cacheRead: 0, output: 0, codexTokens: 0, claudeTokens: 0, reads: 0, rereads: 0, staleRereads: 0, userTimestamps: [] };
|
|
194
|
+
r = { name, cwd: null, sessions: new Set(), assistantMessages: 0, cold: 0, cacheWrite: 0, cacheRead: 0, output: 0, codexTokens: 0, claudeTokens: 0, reads: 0, rereads: 0, staleRereads: 0, userTimestamps: [], byDay: new Map() };
|
|
195
195
|
this.repos.set(name, r);
|
|
196
196
|
}
|
|
197
197
|
if (cwd && !r.cwd)
|
|
@@ -222,7 +222,7 @@ class Forensics {
|
|
|
222
222
|
this.allSessionIds.add(id);
|
|
223
223
|
}
|
|
224
224
|
/** assistant-turn token usage (already split into cold/cacheWrite/cacheRead/output). */
|
|
225
|
-
recordUsage(model, cwd, session, u, provider) {
|
|
225
|
+
recordUsage(model, cwd, session, u, provider, ms) {
|
|
226
226
|
const m = this.modelBucket(model);
|
|
227
227
|
m.messages += 1;
|
|
228
228
|
m.cold += u.cold;
|
|
@@ -244,6 +244,10 @@ class Forensics {
|
|
|
244
244
|
r.codexTokens += turnTokens;
|
|
245
245
|
else
|
|
246
246
|
r.claudeTokens += turnTokens;
|
|
247
|
+
if (ms != null && turnTokens > 0) {
|
|
248
|
+
const d = localDay(ms);
|
|
249
|
+
r.byDay.set(d, (r.byDay.get(d) || 0) + turnTokens);
|
|
250
|
+
}
|
|
247
251
|
}
|
|
248
252
|
}
|
|
249
253
|
recordEdit(target, cwd) {
|
|
@@ -414,6 +418,7 @@ class Forensics {
|
|
|
414
418
|
dominantProvider: repo.codexTokens >= repo.claudeTokens ? "codex" : "claude",
|
|
415
419
|
codexTokens: repo.codexTokens, claudeTokens: repo.claudeTokens,
|
|
416
420
|
commits,
|
|
421
|
+
daily: [...repo.byDay.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)),
|
|
417
422
|
});
|
|
418
423
|
for (const b of wb.blocks) {
|
|
419
424
|
const d = localDay(b.start);
|
|
@@ -528,7 +533,7 @@ function feedClaude(file, eng) {
|
|
|
528
533
|
eng.recordUsage(o.message.model || "unknown", cwd, session, {
|
|
529
534
|
cold: u.input_tokens || 0, cacheWrite: u.cache_creation_input_tokens || 0,
|
|
530
535
|
cacheRead: u.cache_read_input_tokens || 0, output: u.output_tokens || 0,
|
|
531
|
-
}, "claude");
|
|
536
|
+
}, "claude", Number.isFinite(ts) ? ts : undefined);
|
|
532
537
|
const blocks = Array.isArray(o.message.content) ? o.message.content : [];
|
|
533
538
|
for (const b of blocks) {
|
|
534
539
|
if (b?.type !== "tool_use")
|
|
@@ -663,7 +668,7 @@ function replayFile(eng, fe) {
|
|
|
663
668
|
eng.noteSession(fe.session);
|
|
664
669
|
const provider = fe.source === "codex" ? "codex" : "claude";
|
|
665
670
|
for (const u of fe.usage)
|
|
666
|
-
eng.recordUsage(u.model, fe.cwd, fe.session, { cold: u.cold, cacheWrite: 0, cacheRead: u.cacheRead, output: u.output }, provider);
|
|
671
|
+
eng.recordUsage(u.model, fe.cwd, fe.session, { cold: u.cold, cacheWrite: 0, cacheRead: u.cacheRead, output: u.output }, provider, fe.firstTs ?? undefined);
|
|
667
672
|
for (const e of fe.ev) {
|
|
668
673
|
if (e.t === "m")
|
|
669
674
|
eng.recordUserMsg(fe.cwd, fe.session, e.ms);
|
package/dist/setup-page.js
CHANGED