@echomem/mcp 1.4.44 → 1.4.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -25
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +2232 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/context-analysis/claude-native-canonical.js +2 -2
- package/dist/context-analysis/vendored-canonical.js +2 -2
- package/dist/context-analysis/workspace-report.js +3 -3
- package/dist/forensics.js +1531 -0
- package/dist/hud/hooks.js +31 -43
- package/dist/index.js +79 -15
- package/dist/local-data-paths.js +38 -0
- package/dist/migrate.js +140 -70
- package/dist/report.js +721 -0
- package/dist/save-checkpoint-hook.js +1 -1
- package/dist/setup-page/client-core.js +372 -18
- package/dist/setup-page/client-extraction.js +204 -37
- package/dist/setup-page/client-lifecycle.js +101 -31
- package/dist/setup-page/client-report-audit.js +819 -0
- package/dist/setup-page/client-report-city.js +356 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +2 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +31 -1
- package/dist/setup-page/styles-foundation.js +89 -0
- package/dist/setup-page/styles-mvp.js +155 -10
- package/dist/setup-page/styles-website-alignment.js +204 -0
- package/dist/setup-page/styles.js +4 -0
- package/dist/setup-page.js +4 -4
- package/dist/setup-preview.js +212 -4
- package/dist/setup.js +793 -300
- package/dist/source-session.js +3 -9
- package/dist/v1-contract.js +8 -0
- package/package.json +7 -9
- package/dist/config-files.js +0 -63
- package/dist/local-jsonl.js +0 -87
- package/dist/onboarding-stats.js +0 -16
|
@@ -0,0 +1,2232 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
Runtime City source. The setup parent loads this file into an iframe and sends each local scan
|
|
4
|
+
report with postMessage. Do not generate user-specific HTML files: this renderer updates in place.
|
|
5
|
+
The retired static share-card template pipeline lives in artifacts/archive/.
|
|
6
|
+
-->
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8" />
|
|
10
|
+
<title>I scanned your AI coding city — Echo</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root{
|
|
13
|
+
--ink:#1b1c1f; --muted:#8b8d94; --line:#ececef;
|
|
14
|
+
--mono:"IBM Plex Mono","SFMono-Regular",ui-monospace,Menlo,monospace;
|
|
15
|
+
--sans:"Avenir Next","Helvetica Neue","Segoe UI",system-ui,-apple-system,sans-serif;
|
|
16
|
+
--blue:#1a3a8f; --red:#C7372F; --amber:#f0a83f;
|
|
17
|
+
}
|
|
18
|
+
*{margin:0;padding:0;box-sizing:border-box;}
|
|
19
|
+
html,body{background:#FAF7F0;}
|
|
20
|
+
body{display:flex;justify-content:center;align-items:center;height:100vh;overflow:hidden;
|
|
21
|
+
font-family:var(--sans);color:var(--ink);}
|
|
22
|
+
|
|
23
|
+
/* ===== the whole shareable card ===== */
|
|
24
|
+
.share-card{position:relative;width:920px;height:1180px;background:#ffffff;border-radius:26px;
|
|
25
|
+
box-shadow:0 30px 90px rgba(20,24,40,.16);overflow:hidden;transform-origin:center center;}
|
|
26
|
+
|
|
27
|
+
/* ===== central repo visualizer (WebGL clay city) — full-bleed background ===== */
|
|
28
|
+
#repo-visualizer{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:grab;z-index:0;}
|
|
29
|
+
#repo-visualizer:active{cursor:grabbing;}
|
|
30
|
+
|
|
31
|
+
/* overlay zones float above the visualizer (non-interactive → whole city stays draggable) */
|
|
32
|
+
.card-zone{position:absolute;z-index:5;pointer-events:none;}
|
|
33
|
+
body:not(.setup-mode) .compute-cost,
|
|
34
|
+
body:not(.setup-mode) .tool-split,
|
|
35
|
+
body:not(.setup-mode) .persona-card,
|
|
36
|
+
body:not(.setup-mode) .dirty-context,
|
|
37
|
+
body:not(.setup-mode) .counterfactual,
|
|
38
|
+
body:not(.setup-mode) .card-footer{display:none;}
|
|
39
|
+
|
|
40
|
+
/* v7 mirror-first card chrome: keep the WebGL city, replace the old poster furniture */
|
|
41
|
+
.v7-z{position:absolute;z-index:8;}
|
|
42
|
+
.v7-persona{left:56px;top:48px;width:330px;cursor:help;}
|
|
43
|
+
.v7-head{right:56px;top:48px;width:410px;text-align:right;}
|
|
44
|
+
.v7-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
45
|
+
.v7-title{font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.05;margin-top:8px;}
|
|
46
|
+
/* Persona is identity flavor (a badge), not the page headline — one display-size element per
|
|
47
|
+
screen, and that's "Here's what you built." */
|
|
48
|
+
.v7-personaName{font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:.92;margin-top:8px;text-transform:lowercase;}
|
|
49
|
+
.v7-rank{margin-top:12px;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
|
|
50
|
+
.v7-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
|
|
51
|
+
.v7-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
|
|
52
|
+
border-radius:999px;padding:5px 11px;background:rgba(255,255,255,.82);}
|
|
53
|
+
.v7-personaHover{position:absolute;left:0;top:calc(100% + 12px);width:360px;background:rgba(255,255,255,.98);
|
|
54
|
+
border:1px solid var(--line);border-radius:16px;padding:15px 17px;box-shadow:0 18px 46px rgba(20,24,40,.18);
|
|
55
|
+
opacity:0;visibility:hidden;transform:translateY(-4px);transition:opacity .14s ease,transform .14s ease;
|
|
56
|
+
pointer-events:none;z-index:32;}
|
|
57
|
+
.v7-persona:hover .v7-personaHover,
|
|
58
|
+
.v7-persona:focus-within .v7-personaHover{opacity:1;visibility:visible;transform:none;}
|
|
59
|
+
.v7-personaHover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
60
|
+
.v7-personaHover .ph-line:last-of-type{margin-bottom:0;}
|
|
61
|
+
.v7-personaHover .ph-line b{color:var(--ink);font-weight:800;}
|
|
62
|
+
.v7-personaHover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
63
|
+
padding-top:9px;border-top:1px solid var(--line);}
|
|
64
|
+
.v7-personaHover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
65
|
+
.v7-statline{margin-top:16px;display:flex;gap:22px;flex-wrap:wrap;justify-content:flex-end;}
|
|
66
|
+
.v7-stat .v{font-size:23px;font-weight:800;letter-spacing:-.01em;line-height:1;}
|
|
67
|
+
.v7-stat .k{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9296a0;margin-top:4px;}
|
|
68
|
+
.v7-band{left:90px;right:90px;bottom:250px;text-align:center;}
|
|
69
|
+
/* Echo delivers the verdict: the mascot makes the band a character speaking, not a floating stat. */
|
|
70
|
+
.v7-echo{width:56px;height:56px;object-fit:contain;margin:10px auto 2px;display:block;
|
|
71
|
+
filter:drop-shadow(0 12px 20px rgba(26,58,143,.22));}
|
|
72
|
+
.v7-band .eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
|
|
73
|
+
.v7-band .bignum{font-size:64px;font-weight:800;letter-spacing:-.03em;line-height:.9;margin-top:10px;color:var(--red);
|
|
74
|
+
min-height:64px;transition:opacity .52s ease,transform .52s ease;}
|
|
75
|
+
.v7-band .bignum.is-swapping,.v7-band .say.is-swapping{opacity:0;transform:translateY(6px);}
|
|
76
|
+
#v7-metric-value{font-size:64px;font-weight:800;color:var(--red);letter-spacing:-.03em;}
|
|
77
|
+
#v7-metric-label{font-size:27px;font-weight:700;margin-left:12px;color:var(--ink);letter-spacing:-.01em;}
|
|
78
|
+
.v7-infoBtn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;margin-left:10px;
|
|
79
|
+
border:1px solid #f3cfcc;border-radius:50%;background:rgba(255,255,255,.96);color:#b03a33;
|
|
80
|
+
font-family:var(--mono);font-size:15px;font-weight:800;line-height:1;vertical-align:middle;cursor:pointer;
|
|
81
|
+
box-shadow:0 8px 22px rgba(176,58,51,.10);transition:transform .15s ease,box-shadow .15s ease,background .15s ease;}
|
|
82
|
+
.v7-infoBtn:hover,.v7-infoBtn:focus-visible{background:#fff;box-shadow:0 10px 26px rgba(176,58,51,.18);transform:translateY(-1px);outline:none;}
|
|
83
|
+
.v7-band .say{font-size:18px;color:#46484f;margin:15px auto 0;line-height:1.55;max-width:680px;
|
|
84
|
+
min-height:56px;transition:opacity .52s ease,transform .52s ease;}
|
|
85
|
+
.v7-band .say b{color:var(--ink);font-weight:700;}
|
|
86
|
+
.v7-ctaWrap{left:0;right:0;bottom:112px;text-align:center;}
|
|
87
|
+
.v7-ctaWrap .pre{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#9296a0;margin-bottom:15px;}
|
|
88
|
+
.v7-cta{display:inline-flex;align-items:center;gap:11px;background:#1a3a8f;color:#fff;border:0;border-radius:999px;
|
|
89
|
+
font-family:var(--mono);font-size:17px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;padding:21px 48px;
|
|
90
|
+
box-shadow:0 20px 46px rgba(26,58,143,.32);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
91
|
+
.v7-cta:hover,.v7-cta:focus-visible{background:#223f9b;transform:translateY(-2px);outline:none;}
|
|
92
|
+
.v7-ctaWrap .micro{font-size:13px;color:#8b8d94;margin-top:14px;line-height:1.5;}
|
|
93
|
+
.v7-foot{left:60px;right:60px;bottom:26px;display:flex;justify-content:space-between;
|
|
94
|
+
font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:#b6b8bd;text-transform:uppercase;}
|
|
95
|
+
.v7-infoSheet{position:absolute;inset:0;z-index:60;display:none;background:rgba(20,24,40,.42);}
|
|
96
|
+
.v7-infoSheet.open{display:block;}
|
|
97
|
+
.v7-infoCard{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:28px 28px 26px 26px;
|
|
98
|
+
padding:34px 46px 40px;box-shadow:0 -26px 70px rgba(20,24,40,.26);transform:translateY(18px);
|
|
99
|
+
transition:transform .28s ease;}
|
|
100
|
+
.v7-infoSheet.open .v7-infoCard{transform:translateY(0);}
|
|
101
|
+
.v7-infoClose{position:absolute;right:30px;top:26px;width:38px;height:38px;border-radius:50%;
|
|
102
|
+
border:1px solid var(--line);background:#fff;font-size:22px;line-height:1;color:#8d909c;cursor:pointer;}
|
|
103
|
+
.v7-infoClose:hover,.v7-infoClose:focus-visible{color:var(--ink);outline:none;}
|
|
104
|
+
.v7-infoTitle{font-size:26px;font-weight:800;letter-spacing:-.01em;margin-bottom:6px;}
|
|
105
|
+
.v7-infoSub{font-size:14px;color:#8b8d94;margin-bottom:22px;}
|
|
106
|
+
.v7-infoRow{display:grid;grid-template-columns:150px 1fr;gap:24px;padding:15px 0;border-top:1px solid var(--line);}
|
|
107
|
+
.v7-infoRow .sk{font-family:var(--mono);font-size:13px;font-weight:800;letter-spacing:.04em;color:#1b1c1f;text-transform:uppercase;}
|
|
108
|
+
.v7-infoRow .sv{font-size:15.5px;color:#46484f;line-height:1.55;}
|
|
109
|
+
.v7-infoRow .sv b{color:var(--ink);font-weight:700;}
|
|
110
|
+
.v7-infoNote{margin-top:20px;font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:#9296a0;}
|
|
111
|
+
|
|
112
|
+
/* compute cost (headline $) */
|
|
113
|
+
.compute-cost{right:46px;top:42px;width:400px;text-align:right;}
|
|
114
|
+
.compute-cost .label{font-family:var(--mono);font-size:11px;letter-spacing:.2em;color:#a4a7af;text-transform:uppercase;}
|
|
115
|
+
.compute-cost .amount{font-size:60px;font-weight:800;letter-spacing:-.02em;color:var(--ink);line-height:.96;margin-top:4px;}
|
|
116
|
+
.compute-cost .detail{font-size:14px;color:#56585f;margin-top:10px;line-height:1.5;}
|
|
117
|
+
.compute-cost .detail b{color:var(--ink);}
|
|
118
|
+
|
|
119
|
+
/* tool split (Codex vs Claude chips) */
|
|
120
|
+
.tool-split{right:46px;top:212px;display:flex;gap:12px;justify-content:flex-end;}
|
|
121
|
+
.tool-chip{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:14px;
|
|
122
|
+
padding:10px 14px;background:#fff;box-shadow:0 6px 18px rgba(20,24,40,.05);}
|
|
123
|
+
.tool-chip svg{width:22px;height:22px;display:block;}
|
|
124
|
+
.tool-chip .pct{font-size:20px;font-weight:800;letter-spacing:-.01em;}
|
|
125
|
+
.tool-chip .name{font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:#80838c;text-transform:uppercase;line-height:1.3;}
|
|
126
|
+
|
|
127
|
+
/* persona card */
|
|
128
|
+
.persona-card{left:46px;top:38px;width:300px;background:#fff;border:1px solid var(--line);border-radius:20px;
|
|
129
|
+
box-shadow:0 18px 44px rgba(20,24,40,.12);padding:16px 18px 18px;}
|
|
130
|
+
/* big mascot: grows UP out of the card (negative top), bottom margin cancels the extra
|
|
131
|
+
height so the name/header below does NOT move. head can overflow the card's top edge. */
|
|
132
|
+
.persona-card .avatar-rive{width:280px;height:280px;display:block;margin:-129px 0 -63px -100px;
|
|
133
|
+
background:transparent;position:relative;z-index:6;pointer-events:none;}
|
|
134
|
+
.persona-card .name{font-size:34px;font-weight:800;color:var(--ink);letter-spacing:-.01em;line-height:1;text-transform:lowercase;}
|
|
135
|
+
/* Coding Persona Card: primary handle + rank·rhythm + side chips + one funny verdict */
|
|
136
|
+
.persona-card .rankrhythm{font-family:var(--mono);font-size:11px;letter-spacing:.16em;color:#9296a0;text-transform:uppercase;margin-top:8px;}
|
|
137
|
+
.persona-card .combo{display:flex;flex-wrap:wrap;gap:7px;margin-top:14px;padding-top:15px;border-top:1px solid var(--line);}
|
|
138
|
+
.persona-card .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;
|
|
139
|
+
border:1px solid var(--line);border-radius:999px;padding:5px 11px;background:#fafafb;}
|
|
140
|
+
.persona-card .verdict{font-size:14px;color:#42444b;margin-top:13px;line-height:1.55;}
|
|
141
|
+
.persona-card .verdict b{color:var(--ink);font-weight:800;}
|
|
142
|
+
|
|
143
|
+
/* dirty-context callout — 85% dirty → ~5% with Echo */
|
|
144
|
+
.dirty-context{left:46px;top:850px;width:430px;}
|
|
145
|
+
.dirty-context .pct{font-size:58px;font-weight:800;color:var(--red);letter-spacing:-.02em;line-height:.95;
|
|
146
|
+
min-height:58px;transition:opacity .52s ease,transform .52s ease;}
|
|
147
|
+
.dirty-context .pct.is-swapping,.dirty-context .desc.is-swapping{opacity:0;transform:translateY(6px);}
|
|
148
|
+
.dirty-context .pct .word{font-size:26px;font-weight:700;margin-left:8px;}
|
|
149
|
+
.dirty-context .desc{font-size:15px;color:#42444b;margin-top:12px;line-height:1.5;max-width:380px;
|
|
150
|
+
min-height:48px;transition:opacity .52s ease,transform .52s ease;}
|
|
151
|
+
.dirty-context .desc b{color:var(--ink);}
|
|
152
|
+
|
|
153
|
+
/* counterfactual — one line: the 18-days payoff */
|
|
154
|
+
.counterfactual{left:46px;top:1026px;width:828px;display:flex;align-items:center;gap:24px;}
|
|
155
|
+
.counterfactual .cal-icon{width:62px;height:65px;display:block;flex:none;
|
|
156
|
+
filter:drop-shadow(0 4px 9px rgba(20,24,40,.14));}
|
|
157
|
+
.cf-hero{font-size:38px;font-weight:800;letter-spacing:-.015em;color:var(--ink);line-height:1.04;}
|
|
158
|
+
.cf-hero b{color:var(--ink);font-weight:800;}
|
|
159
|
+
.cf-say{font-size:15px;color:#56585f;margin-top:8px;line-height:1.5;}
|
|
160
|
+
.cf-say b{color:var(--ink);font-weight:700;}
|
|
161
|
+
|
|
162
|
+
/* footer */
|
|
163
|
+
.card-footer{left:46px;right:46px;bottom:22px;display:flex;justify-content:space-between;
|
|
164
|
+
font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:#b6b8bd;text-transform:uppercase;}
|
|
165
|
+
|
|
166
|
+
/* setup flow CTA: hidden in share-card export, visible only in ?setup=1 mode */
|
|
167
|
+
.setup-connect{display:none;position:absolute;z-index:20;right:46px;top:930px;width:238px;min-height:58px;
|
|
168
|
+
border:1px solid #1a3a8f;border-radius:999px;background:#1a3a8f;color:#fff;align-items:center;justify-content:center;
|
|
169
|
+
font-family:var(--mono);font-size:13px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
|
|
170
|
+
box-shadow:0 16px 36px rgba(20,24,40,.18);cursor:pointer;}
|
|
171
|
+
.setup-connect:hover{background:#223f9b;}
|
|
172
|
+
body.setup-mode{background:#fff;}
|
|
173
|
+
/* figure-mode: embedded as an inline article figure — city only, chrome hidden, paper shows through */
|
|
174
|
+
body.figure-mode{background:transparent;}
|
|
175
|
+
body.figure-mode .share-card{background:transparent;box-shadow:none;}
|
|
176
|
+
body.figure-mode .fr-panel,body.figure-mode .corner-ribbon,body.figure-mode .city-toggle,
|
|
177
|
+
body.figure-mode .city-split,body.figure-mode .city-timeline,body.figure-mode .v7-band{display:none!important;}
|
|
178
|
+
body.setup-mode .share-card{width:100vw;height:100vh;border-radius:0;box-shadow:none;overflow:hidden;}
|
|
179
|
+
body.setup-mode .compute-cost,
|
|
180
|
+
body.setup-mode .tool-split,
|
|
181
|
+
body.setup-mode .persona-card,
|
|
182
|
+
body.setup-mode .dirty-context,
|
|
183
|
+
body.setup-mode .counterfactual,
|
|
184
|
+
body.setup-mode .setup-connect,
|
|
185
|
+
body.setup-mode .setup-header,
|
|
186
|
+
body.setup-mode .card-footer{display:none;}
|
|
187
|
+
/* bottom-left: dirty-context score */
|
|
188
|
+
body.setup-mode .dirty-context{left:clamp(28px,5vw,72px);top:auto;bottom:clamp(30px,6vh,64px);width:min(440px,42vw);}
|
|
189
|
+
body.setup-mode .dirty-context .pct{font-size:clamp(64px,8vw,116px);line-height:.82;}
|
|
190
|
+
body.setup-mode .dirty-context .pct .word{font-size:clamp(26px,3.4vw,46px);}
|
|
191
|
+
body.setup-mode .dirty-context .desc{font-size:clamp(13px,1.1vw,16px);max-width:min(420px,42vw);}
|
|
192
|
+
/* bottom-right: the "Echo saves you X days" payoff stacked directly above the Connect Echo CTA */
|
|
193
|
+
body.setup-mode .setup-connect{display:flex;left:auto;right:clamp(28px,5vw,64px);top:auto;bottom:clamp(30px,5vh,56px);}
|
|
194
|
+
body.setup-mode .counterfactual{left:auto;right:clamp(28px,5vw,64px);top:auto;
|
|
195
|
+
bottom:calc(clamp(30px,5vh,56px) + 78px);width:min(440px,46vw);justify-content:flex-end;gap:16px;}
|
|
196
|
+
body.setup-mode .counterfactual .cf-body{text-align:right;}
|
|
197
|
+
body.setup-mode .counterfactual .cal-icon{width:54px;height:56px;}
|
|
198
|
+
body.setup-mode .cf-hero{font-size:clamp(26px,2.5vw,36px);}
|
|
199
|
+
body.setup-mode .cf-say{font-size:clamp(13px,1vw,15px);}
|
|
200
|
+
.setup-header{display:none;}
|
|
201
|
+
body.setup-mode .setup-header{position:absolute;z-index:18;left:clamp(34px,5vw,86px);right:clamp(34px,5vw,86px);
|
|
202
|
+
top:clamp(26px,4.8vh,58px);display:flex;align-items:flex-start;justify-content:space-between;gap:28px;pointer-events:none;}
|
|
203
|
+
.setup-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:#9aa0aa;}
|
|
204
|
+
.setup-persona-name{margin-top:8px;font-size:clamp(42px,5.8vw,86px);font-weight:800;letter-spacing:-.02em;line-height:.88;color:var(--ink);}
|
|
205
|
+
.setup-rank{margin-top:11px;font-family:var(--mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
|
|
206
|
+
.setup-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:15px;}
|
|
207
|
+
.setup-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
|
|
208
|
+
border-radius:999px;padding:5px 11px;background:rgba(255,255,255,.82);}
|
|
209
|
+
.setup-stats{text-align:right;max-width:430px;}
|
|
210
|
+
.setup-stats .amount{margin-top:7px;font-size:clamp(46px,5.2vw,78px);font-weight:800;letter-spacing:-.02em;line-height:.9;color:var(--ink);}
|
|
211
|
+
.setup-stats .detail{margin-top:10px;font-size:15px;color:#56585f;}
|
|
212
|
+
.setup-split{display:flex;justify-content:flex-end;gap:10px;margin-top:16px;}
|
|
213
|
+
.setup-split span{border:1px solid var(--line);border-radius:12px;background:rgba(255,255,255,.82);padding:8px 11px;
|
|
214
|
+
font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#646a74;}
|
|
215
|
+
/* persona hover: derived coding-personality card revealed on hover/focus of the top-left persona */
|
|
216
|
+
body.setup-mode .setup-persona{pointer-events:auto;position:relative;cursor:help;}
|
|
217
|
+
body.setup-mode .persona-cue{margin-top:11px;font-family:var(--mono);font-size:10px;font-weight:700;
|
|
218
|
+
letter-spacing:.12em;text-transform:uppercase;color:#b0b4bd;}
|
|
219
|
+
body.setup-mode .persona-cue::after{content:" \2193";}
|
|
220
|
+
body.setup-mode .persona-hover{position:absolute;left:0;top:calc(100% + 10px);width:min(380px,80vw);
|
|
221
|
+
background:rgba(255,255,255,.98);border:1px solid var(--line);border-radius:16px;padding:15px 17px;
|
|
222
|
+
box-shadow:0 18px 46px rgba(20,24,40,.18);opacity:0;visibility:hidden;transform:translateY(-4px);
|
|
223
|
+
transition:opacity .14s ease,transform .14s ease;z-index:30;pointer-events:none;}
|
|
224
|
+
body.setup-mode .setup-persona:hover .persona-hover,
|
|
225
|
+
body.setup-mode .setup-persona:focus-within .persona-hover{opacity:1;visibility:visible;transform:none;}
|
|
226
|
+
body.setup-mode .persona-hover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
227
|
+
body.setup-mode .persona-hover .ph-line:last-of-type{margin-bottom:0;}
|
|
228
|
+
body.setup-mode .persona-hover .ph-line b{color:var(--ink);font-weight:800;}
|
|
229
|
+
body.setup-mode .persona-hover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
230
|
+
padding-top:9px;border-top:1px solid var(--line);}
|
|
231
|
+
body.setup-mode .persona-hover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
232
|
+
/* short viewports (e.g. 1366×768 laptops): shrink the headline numbers so nothing crowds the edges */
|
|
233
|
+
@media (max-height:760px){
|
|
234
|
+
body.setup-mode .setup-persona-name{font-size:clamp(34px,7vh,60px);}
|
|
235
|
+
body.setup-mode .setup-stats .amount{font-size:clamp(38px,7vh,60px);}
|
|
236
|
+
body.setup-mode .dirty-context .pct{font-size:clamp(52px,11vh,92px);}
|
|
237
|
+
body.setup-mode .counterfactual{bottom:calc(clamp(24px,4vh,48px) + 70px);}
|
|
238
|
+
body.setup-mode .setup-connect{bottom:clamp(24px,4vh,48px);}
|
|
239
|
+
}
|
|
240
|
+
body.setup-mode .setup-header,
|
|
241
|
+
body.setup-mode .dirty-context,
|
|
242
|
+
body.setup-mode .counterfactual,
|
|
243
|
+
body.setup-mode .setup-connect{display:none;}
|
|
244
|
+
body.setup-mode .v7-persona{left:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(360px,32vw);}
|
|
245
|
+
body.setup-mode .v7-personaName{font-size:clamp(38px,3.4vw,54px);}
|
|
246
|
+
body.setup-mode .v7-head{right:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(520px,38vw);}
|
|
247
|
+
body.setup-mode .v7-head .v7-eyebrow{font-size:clamp(10px,.72vw,12px);}
|
|
248
|
+
body.setup-mode .v7-title{font-size:clamp(28px,2.1vw,40px);}
|
|
249
|
+
body.setup-mode .v7-statline{gap:clamp(16px,1.55vw,26px);}
|
|
250
|
+
body.setup-mode .v7-stat .v{font-size:clamp(20px,1.45vw,28px);}
|
|
251
|
+
body.setup-mode .v7-stat .k{font-size:clamp(9px,.72vw,11px);}
|
|
252
|
+
body.setup-mode .v7-band{left:clamp(48px,5vw,86px);right:auto;bottom:clamp(82px,8vh,130px);
|
|
253
|
+
width:min(640px,42vw);text-align:left;display:grid;grid-template-columns:72px minmax(0,1fr);
|
|
254
|
+
column-gap:16px;align-items:start;}
|
|
255
|
+
body.setup-mode .v7-echo{width:72px;height:72px;margin:2px 0 0;}
|
|
256
|
+
body.setup-mode .v7-bandCopy{min-width:0;}
|
|
257
|
+
body.setup-mode .v7-band .eyebrow{display:none;}
|
|
258
|
+
body.setup-mode .v7-band .bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
259
|
+
margin-top:0;min-height:0;line-height:.96;}
|
|
260
|
+
body.setup-mode #v7-metric-value{font-size:clamp(38px,4vw,62px);}
|
|
261
|
+
body.setup-mode #v7-metric-label{font-size:clamp(17px,1.45vw,24px);margin-left:0;}
|
|
262
|
+
body.setup-mode .v7-infoBtn{width:30px;height:30px;margin-left:0;flex:none;}
|
|
263
|
+
body.setup-mode .v7-band .say{margin:14px 0 0;max-width:520px;font-size:clamp(15px,1.05vw,18px);
|
|
264
|
+
min-height:58px;}
|
|
265
|
+
body.setup-mode .v7-ctaWrap{left:auto;right:clamp(48px,5vw,86px);bottom:clamp(86px,8vh,132px);
|
|
266
|
+
width:min(430px,31vw);text-align:right;}
|
|
267
|
+
body.setup-mode .v7-cta{padding:20px 48px;font-size:clamp(14px,1.1vw,17px);}
|
|
268
|
+
body.setup-mode .v7-ctaWrap .micro{max-width:430px;margin-left:auto;}
|
|
269
|
+
body.setup-mode .v7-foot{left:clamp(48px,5vw,86px);right:clamp(48px,5vw,86px);}
|
|
270
|
+
@media (max-height:820px){
|
|
271
|
+
body.setup-mode .v7-personaName{font-size:clamp(32px,4.4vh,46px);}
|
|
272
|
+
body.setup-mode .v7-title{font-size:clamp(30px,5vh,44px);}
|
|
273
|
+
body.setup-mode .v7-band{bottom:58px;width:min(500px,40vw);}
|
|
274
|
+
body.setup-mode .v7-ctaWrap{bottom:58px;width:min(390px,32vw);}
|
|
275
|
+
body.setup-mode .v7-foot{display:none;}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* setup split report: city on the left, field report on the right */
|
|
279
|
+
/* Old full-screen setup overlay reserves 620px for the right-hand text panel.
|
|
280
|
+
figure-mode embeds (city as its own column) must NOT reserve anything: the
|
|
281
|
+
canvas fills the iframe, matching the full-width render buffer (VIZ_W). */
|
|
282
|
+
body.setup-mode:not(.figure-mode) #repo-visualizer{right:auto;width:calc(100vw - min(40vw,620px));}
|
|
283
|
+
body.with-echo .fr-metric .fr-v{color:#2a8f7d;}
|
|
284
|
+
body.with-echo .fr-pull{border-left-color:#2a8f7d;}
|
|
285
|
+
.city-toggle{display:none;transition:opacity .4s ease;}
|
|
286
|
+
body.setup-mode .city-toggle{display:inline-flex;position:absolute;z-index:9;
|
|
287
|
+
left:calc((100vw - min(40vw,620px)) / 2);top:clamp(92px,12.5vh,104px);transform:translateX(-50%);gap:2px;
|
|
288
|
+
background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:999px;padding:4px;
|
|
289
|
+
box-shadow:0 8px 22px rgba(20,24,40,.10);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);}
|
|
290
|
+
.ct-thumb{position:absolute;left:0;top:0;z-index:0;background:#1a3a8f;border-radius:999px;
|
|
291
|
+
transition:transform .32s cubic-bezier(.4,0,.2,1),width .32s cubic-bezier(.4,0,.2,1),height .32s ease;}
|
|
292
|
+
.city-toggle button{position:relative;z-index:1;appearance:none;border:0;background:transparent;cursor:pointer;
|
|
293
|
+
font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
|
|
294
|
+
color:#80838c;padding:7px 14px;border-radius:999px;transition:color .25s ease;}
|
|
295
|
+
.city-toggle button.on{color:#fff;}
|
|
296
|
+
.city-toggle button:not(.on):hover{color:#1b1c1f;}
|
|
297
|
+
.corner-ribbon{display:none;}
|
|
298
|
+
body.setup-mode .corner-ribbon{display:block;position:absolute;top:0;left:0;width:180px;height:180px;
|
|
299
|
+
overflow:hidden;z-index:12;pointer-events:none;}
|
|
300
|
+
.corner-ribbon>span{position:absolute;left:-70px;top:51px;width:280px;transform:rotate(-45deg);
|
|
301
|
+
background:#1a3a8f;color:#fff;text-align:center;padding:7px 0;line-height:1.2;
|
|
302
|
+
box-shadow:0 6px 16px rgba(20,24,40,.18);}
|
|
303
|
+
.corner-ribbon .l1{display:block;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;}
|
|
304
|
+
.corner-ribbon .l2{display:block;font-family:var(--mono);font-size:8.5px;font-weight:700;letter-spacing:.08em;
|
|
305
|
+
text-transform:uppercase;color:rgba(255,255,255,.86);margin-top:2px;}
|
|
306
|
+
.city-split{display:none;transition:opacity .4s ease;}
|
|
307
|
+
body.setup-mode .city-split{display:block;position:absolute;z-index:9;
|
|
308
|
+
left:clamp(28px,3vw,48px);bottom:clamp(72px,9vh,104px);width:min(260px,24vw);}
|
|
309
|
+
.city-split .cs-bar{display:flex;height:8px;border-radius:999px;overflow:hidden;background:#eceef2;
|
|
310
|
+
box-shadow:inset 0 0 0 1px rgba(20,24,40,.05);}
|
|
311
|
+
.city-split .cs-bar i{height:100%;display:block;transition:width .4s cubic-bezier(.4,0,.2,1);}
|
|
312
|
+
.city-split .cs-claude{background:#d97757;}
|
|
313
|
+
.city-split .cs-codex{background:#10a37f;}
|
|
314
|
+
.city-split .cs-legend{display:flex;justify-content:space-between;margin-top:9px;
|
|
315
|
+
font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#80838c;}
|
|
316
|
+
.city-split .cs-l{display:flex;align-items:center;gap:6px;}
|
|
317
|
+
.city-split .cs-l i{width:8px;height:8px;border-radius:50%;}
|
|
318
|
+
.city-split .cs-l.claude i{background:#d97757;}
|
|
319
|
+
.city-split .cs-l.codex i{background:#10a37f;}
|
|
320
|
+
.city-split .cs-l b{color:#1b1c1f;font-weight:800;margin-left:2px;}
|
|
321
|
+
.city-timeline{display:none;transition:opacity .4s ease;}
|
|
322
|
+
body.setup-mode .city-timeline{display:block;position:absolute;z-index:9;
|
|
323
|
+
left:clamp(28px,3vw,48px);bottom:clamp(20px,2.6vh,32px);
|
|
324
|
+
width:calc(100vw - min(40vw,620px) - 2 * clamp(28px,3vw,48px));touch-action:none;user-select:none;}
|
|
325
|
+
.city-timeline .tl-track{position:relative;height:18px;cursor:pointer;}
|
|
326
|
+
.city-timeline .tl-line{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
|
|
327
|
+
height:3px;border-radius:3px;background:#d8dbe1;}
|
|
328
|
+
.city-timeline .tl-progress{position:absolute;left:0;top:50%;transform:translateY(-50%);
|
|
329
|
+
width:100%;height:3px;border-radius:3px;background:#1a3a8f;pointer-events:none;}
|
|
330
|
+
.city-timeline .tl-dot{position:absolute;top:50%;transform:translate(-50%,-50%);width:9px;height:9px;border-radius:50%;}
|
|
331
|
+
.city-timeline .tl-dot.start{left:0;background:#9aa0aa;}
|
|
332
|
+
.city-timeline .tl-dot.end{left:100%;background:#1a3a8f;}
|
|
333
|
+
.city-timeline .tl-dot.end::after{content:'';position:absolute;inset:-4px;border-radius:50%;
|
|
334
|
+
border:2px solid rgba(26,58,143,.4);animation:tlPulse 2.2s ease-out infinite;}
|
|
335
|
+
@keyframes tlPulse{0%{transform:scale(.6);opacity:.9}100%{transform:scale(1.85);opacity:0}}
|
|
336
|
+
.city-timeline .tl-handle{position:absolute;left:100%;top:50%;transform:translate(-50%,-50%);
|
|
337
|
+
width:16px;height:16px;border-radius:50%;background:#1a3a8f;border:3px solid #fff;
|
|
338
|
+
box-shadow:0 3px 10px rgba(20,24,40,.28);z-index:3;cursor:grab;}
|
|
339
|
+
.city-timeline.dragging .tl-handle{cursor:grabbing;box-shadow:0 3px 14px rgba(26,58,143,.42);}
|
|
340
|
+
.city-timeline .tl-selected{position:absolute;left:100%;top:-17px;transform:translateX(-50%);
|
|
341
|
+
font-family:var(--mono);font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
|
|
342
|
+
color:#1a3a8f;white-space:nowrap;pointer-events:none;}
|
|
343
|
+
.city-timeline .tl-labels{display:flex;justify-content:space-between;align-items:baseline;margin-top:8px;
|
|
344
|
+
font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#80838c;}
|
|
345
|
+
.city-timeline .tl-labels .tl-span{color:#1b1c1f;font-weight:800;}
|
|
346
|
+
body.setup-mode .v7-persona,
|
|
347
|
+
body.setup-mode .v7-head,
|
|
348
|
+
body.setup-mode .v7-band,
|
|
349
|
+
body.setup-mode .v7-ctaWrap,
|
|
350
|
+
body.setup-mode .v7-foot{display:none;}
|
|
351
|
+
|
|
352
|
+
.fr-panel{display:none;transition:opacity .6s ease;}
|
|
353
|
+
body.setup-mode .fr-panel{display:flex;flex-direction:column;align-items:center;position:absolute;z-index:8;
|
|
354
|
+
right:0;top:0;width:min(40vw,620px);height:100vh;border-left:1px solid var(--line);background:#fff;
|
|
355
|
+
padding:clamp(44px,6vh,90px) clamp(36px,3.2vw,60px);}
|
|
356
|
+
.fr{width:min(54ch,100%);flex:1;display:flex;flex-direction:column;justify-content:space-between;
|
|
357
|
+
gap:clamp(22px,3.4vh,46px);}
|
|
358
|
+
.fr-dateline{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:11px;
|
|
359
|
+
font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
|
|
360
|
+
.fr-dateline .fr-rule{flex:1;height:1px;background:var(--line);}
|
|
361
|
+
.fr-intro{display:flex;flex-direction:column;position:relative;
|
|
362
|
+
padding-bottom:clamp(20px,3vh,38px);border-bottom:1px solid var(--line);}
|
|
363
|
+
.fr-avatar{width:clamp(92px,9.5vw,128px);height:auto;display:block;margin-bottom:10px;}
|
|
364
|
+
.fr-greet{font-size:clamp(34px,3.2vw,52px);font-weight:800;letter-spacing:-.035em;line-height:.95;
|
|
365
|
+
text-transform:lowercase;}
|
|
366
|
+
.fr-greet .fr-name{color:#1a3a8f;}
|
|
367
|
+
.fr-greet .fr-dot{color:var(--amber);}
|
|
368
|
+
.fr-rank{margin-top:13px;font-family:var(--mono);font-size:clamp(12px,1vw,14px);font-weight:600;
|
|
369
|
+
letter-spacing:.04em;color:#9296a0;}
|
|
370
|
+
.fr-rank b{font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#3a3c43;}
|
|
371
|
+
.fr-readBtn{position:absolute;z-index:9;top:clamp(2px,0.5vw,8px);right:0;
|
|
372
|
+
display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
|
|
373
|
+
cursor:pointer;font-family:var(--mono);font-size:14px;font-weight:800;line-height:1;
|
|
374
|
+
color:#5a5e66;background:#fafafb;border:1px solid var(--line);border-radius:50%;
|
|
375
|
+
transition:background .15s ease,border-color .15s ease,color .15s ease;}
|
|
376
|
+
.fr-readBtn:hover,.fr-readBtn:focus-visible{background:#fff;border-color:#d7dade;color:var(--ink);outline:none;}
|
|
377
|
+
.fr-readBtn[aria-expanded="true"]{background:#1a3a8f;border-color:#1a3a8f;color:#fff;}
|
|
378
|
+
.fr-personaHover{position:absolute;z-index:32;top:clamp(42px,4.6vw,56px);right:0;
|
|
379
|
+
width:min(340px,100%);max-height:min(72vh,540px);overflow:auto;
|
|
380
|
+
background:rgba(255,255,255,.98);border:1px solid var(--line);border-radius:16px;padding:15px 17px;
|
|
381
|
+
box-shadow:0 18px 46px rgba(20,24,40,.18);opacity:0;visibility:hidden;transform:translateY(-4px);
|
|
382
|
+
transition:opacity .14s ease,transform .14s ease;pointer-events:none;}
|
|
383
|
+
.fr-personaHover.open{opacity:1;visibility:visible;transform:none;pointer-events:auto;}
|
|
384
|
+
.fr-personaHover .ph-line{font-size:13.5px;line-height:1.5;color:#42444b;margin-bottom:9px;}
|
|
385
|
+
.fr-personaHover .ph-line:last-of-type{margin-bottom:0;}
|
|
386
|
+
.fr-personaHover .ph-line b{color:var(--ink);font-weight:800;}
|
|
387
|
+
.fr-personaHover .ph-meta{font-size:12px;line-height:1.45;color:#73767e;margin-top:9px;
|
|
388
|
+
padding-top:9px;border-top:1px solid var(--line);}
|
|
389
|
+
.fr-personaHover .ph-meta b{color:var(--blue);font-weight:700;}
|
|
390
|
+
.fr-mid{display:flex;flex-direction:column;gap:clamp(20px,3vh,40px);}
|
|
391
|
+
.fr-lede{font-size:clamp(16px,1.1vw,18px);line-height:1.68;color:#3c3e45;}
|
|
392
|
+
.fr-lede mark{background:linear-gradient(transparent 58%, rgba(240,168,63,.42) 58%);
|
|
393
|
+
color:var(--ink);font-weight:800;padding:0 1px;}
|
|
394
|
+
.fr-pull{padding-left:20px;border-left:3px solid var(--red);}
|
|
395
|
+
.fr-metric{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
|
|
396
|
+
transition:opacity .5s ease,transform .5s ease;}
|
|
397
|
+
.fr-metric .fr-v{font-size:clamp(32px,2.8vw,44px);font-weight:800;color:var(--red);letter-spacing:-.03em;line-height:.9;}
|
|
398
|
+
.fr-metric .fr-l{font-size:clamp(16px,1.2vw,19px);font-weight:700;color:var(--ink);letter-spacing:-.01em;}
|
|
399
|
+
.fr-say{font-size:clamp(14px,.9vw,15px);color:#56585f;margin-top:9px;line-height:1.5;max-width:46ch;
|
|
400
|
+
min-height:4.6em;transition:opacity .5s ease,transform .5s ease;}
|
|
401
|
+
.fr-say b{color:var(--ink);font-weight:700;}
|
|
402
|
+
.fr-metric.is-swapping,.fr-say.is-swapping{opacity:0;transform:translateY(6px);}
|
|
403
|
+
.fr-signoff{display:flex;align-items:center;gap:18px;flex-wrap:wrap;}
|
|
404
|
+
.fr-cta{display:inline-flex;align-items:center;gap:10px;background:#1a3a8f;color:#fff;border:0;
|
|
405
|
+
border-radius:13px;font-family:var(--mono);font-size:clamp(13px,1vw,15px);font-weight:800;
|
|
406
|
+
letter-spacing:.08em;text-transform:uppercase;padding:16px 34px;
|
|
407
|
+
box-shadow:0 18px 40px rgba(26,58,143,.30);cursor:pointer;transition:background .18s ease,transform .18s ease;}
|
|
408
|
+
.fr-cta:hover{background:#223f9b;transform:translateY(-2px);}
|
|
409
|
+
.fr-micro{font-size:12.5px;color:#8b8d94;line-height:1.45;max-width:24ch;}
|
|
410
|
+
|
|
411
|
+
/* ===== in-canvas scan loader (setup mode) — loading lives inside the report, matches its type ===== */
|
|
412
|
+
.scan-loading{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
413
|
+
text-align:center;gap:14px;padding:24px;opacity:1;transition:opacity .5s ease;pointer-events:none;}
|
|
414
|
+
body:not(.scanning) .scan-loading{opacity:0;visibility:hidden;}
|
|
415
|
+
.scan-loading .eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
|
|
416
|
+
.scan-loading .title{font-family:var(--sans);font-weight:800;font-size:clamp(28px,4.4vw,46px);letter-spacing:-.02em;color:var(--ink);}
|
|
417
|
+
.scan-loading .bar{width:min(360px,60vw);height:6px;border-radius:999px;background:#e2e4ea;overflow:hidden;margin-top:10px;}
|
|
418
|
+
.scan-loading .bar i{display:block;height:100%;width:36%;border-radius:999px;background:var(--blue);animation:scanIndet 1.3s ease-in-out infinite;}
|
|
419
|
+
@keyframes scanIndet{0%{transform:translateX(-115%)}100%{transform:translateX(330%)}}
|
|
420
|
+
.scan-loading .count{font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:#8b8d94;min-height:15px;margin-top:8px;}
|
|
421
|
+
/* during scan: hide the city + the value-bearing chrome (so demo numbers never flash), then fade in together */
|
|
422
|
+
#repo-visualizer{transition:opacity .7s ease;}
|
|
423
|
+
body.scanning #repo-visualizer{opacity:0;}
|
|
424
|
+
.setup-header,.dirty-context,.counterfactual,.setup-connect,.v7-z,.city-toggle,.city-split,.city-timeline,.fr-panel{transition:opacity .6s ease;}
|
|
425
|
+
body.scanning .setup-header,body.scanning .dirty-context,body.scanning .counterfactual,body.scanning .setup-connect,body.scanning .v7-z,body.scanning .city-toggle,body.scanning .city-split,body.scanning .city-timeline,body.scanning .fr-panel{opacity:0;pointer-events:none;}
|
|
426
|
+
body.setup-mode:not(.scanning) #repo-visualizer,
|
|
427
|
+
body.setup-mode:not(.scanning) .v7-z,
|
|
428
|
+
body.setup-mode:not(.scanning) .city-toggle,
|
|
429
|
+
body.setup-mode:not(.scanning) .city-split,
|
|
430
|
+
body.setup-mode:not(.scanning) .city-timeline,
|
|
431
|
+
body.setup-mode:not(.scanning) .fr-panel{opacity:1;}
|
|
432
|
+
|
|
433
|
+
/* hover-to-preview repo detail (interaction overlay, not part of the static export) */
|
|
434
|
+
#repo-detail{position:fixed;display:none;z-index:50;width:286px;background:rgba(255,255,255,.96);
|
|
435
|
+
border:1px solid rgba(197,203,214,.78);border-radius:18px;box-shadow:0 18px 46px rgba(20,24,40,.20);
|
|
436
|
+
padding:17px 18px 15px;pointer-events:none;backdrop-filter:blur(10px);}
|
|
437
|
+
#repo-detail .rd-kicker{font-family:var(--mono);font-size:10px;font-weight:800;letter-spacing:.18em;
|
|
438
|
+
text-transform:uppercase;color:#969aa4;margin-bottom:7px;}
|
|
439
|
+
#repo-detail .rd-name{font-family:var(--sans);font-weight:800;font-size:21px;color:var(--blue);
|
|
440
|
+
word-break:break-word;line-height:1.08;letter-spacing:-.01em;margin-bottom:14px;}
|
|
441
|
+
#repo-detail .rd-metrics{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:13px;}
|
|
442
|
+
#repo-detail .rd-label{font-family:var(--mono);font-size:9px;font-weight:800;letter-spacing:.12em;
|
|
443
|
+
text-transform:uppercase;color:#9a9da6;margin-bottom:6px;}
|
|
444
|
+
#repo-detail .rd-value{font-family:var(--mono);font-size:21px;font-weight:800;letter-spacing:.01em;
|
|
445
|
+
color:#1f2024;white-space:nowrap;}
|
|
446
|
+
#repo-detail .rd-meta{font-family:var(--mono);font-size:10.5px;font-weight:700;letter-spacing:.04em;
|
|
447
|
+
color:#858994;margin:1px 0 12px;}
|
|
448
|
+
#repo-detail .rd-split{height:8px;border-radius:999px;overflow:hidden;background:#edf0f3;display:flex;
|
|
449
|
+
box-shadow:inset 0 0 0 1px rgba(20,24,40,.04);}
|
|
450
|
+
#repo-detail .rd-seg{height:100%;min-width:0;}
|
|
451
|
+
#repo-detail .rd-seg.claude{background:#d97757;} #repo-detail .rd-seg.codex{background:#10a37f;}
|
|
452
|
+
#repo-detail .rd-legend{display:flex;justify-content:space-between;gap:12px;margin-top:11px;
|
|
453
|
+
font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.02em;}
|
|
454
|
+
#repo-detail .rd-legend .claude{color:#d97757;} #repo-detail .rd-legend .codex{color:#10a37f;text-align:right;}
|
|
455
|
+
</style>
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
<script>try{const p=new URLSearchParams(location.search);if(p.get('setup')==='1'){document.body.classList.add('setup-mode','scanning');}if(p.get('figure')==='1'){document.body.classList.add('figure-mode');window.addEventListener('message',function(e){if(e.origin!==location.origin)return;var d=e.data||{};if(d.type==='echomem:city-figure')document.body.classList.toggle('figure-mode',!d.expanded);});}}catch(e){}</script>
|
|
459
|
+
<div class="share-card">
|
|
460
|
+
<canvas id="repo-visualizer"></canvas>
|
|
461
|
+
|
|
462
|
+
<div class="city-toggle" id="city-toggle" role="group" aria-label="City metric">
|
|
463
|
+
<span class="ct-thumb" aria-hidden="true"></span>
|
|
464
|
+
<button type="button" data-metric="time" class="on">Time</button>
|
|
465
|
+
<button type="button" data-metric="tokens">Tokens</button>
|
|
466
|
+
<button type="button" data-metric="cost">API $</button>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div class="corner-ribbon" aria-hidden="true"><span><span class="l1">Local Scan</span><span class="l2">100% private & safe</span></span></div>
|
|
470
|
+
|
|
471
|
+
<div class="city-split" id="city-split">
|
|
472
|
+
<div class="cs-bar"><i class="cs-claude" style="width:74%"></i><i class="cs-codex" style="width:26%"></i></div>
|
|
473
|
+
<div class="cs-legend">
|
|
474
|
+
<span class="cs-l claude"><i></i>Claude <b>74%</b></span>
|
|
475
|
+
<span class="cs-l codex"><i></i>Codex <b>26%</b></span>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="city-timeline" id="city-timeline">
|
|
480
|
+
<div class="tl-track">
|
|
481
|
+
<span class="tl-line"></span>
|
|
482
|
+
<span class="tl-progress"></span>
|
|
483
|
+
<span class="tl-dot start"></span><span class="tl-dot end"></span>
|
|
484
|
+
<span class="tl-selected">Today</span>
|
|
485
|
+
<span class="tl-handle" role="slider" tabindex="0" aria-label="Coding history date" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"></span>
|
|
486
|
+
</div>
|
|
487
|
+
<div class="tl-labels"><span class="tl-start">Dec 2024</span><span class="tl-span">AI coding history</span><span class="tl-end">Today</span></div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div class="v7-z v7-persona" tabindex="0" aria-describedby="v7-persona-hover">
|
|
491
|
+
<div class="v7-eyebrow">AI Coding City</div>
|
|
492
|
+
<div class="v7-personaName">bossy</div>
|
|
493
|
+
<div class="v7-rank">the architect · marathoner</div>
|
|
494
|
+
<div class="v7-combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
495
|
+
<div class="v7-personaHover" id="v7-persona-hover" role="tooltip"></div>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="v7-z v7-head">
|
|
498
|
+
<div class="v7-eyebrow">Echo scanned your AI coding city</div>
|
|
499
|
+
<div class="v7-title">Here's what you built.</div>
|
|
500
|
+
<div class="v7-statline">
|
|
501
|
+
<div class="v7-stat"><div class="v" id="v7-repo-count">13</div><div class="k">repos</div></div>
|
|
502
|
+
<div class="v7-stat"><div class="v" id="v7-attention">191.1h</div><div class="k">attention</div></div>
|
|
503
|
+
<div class="v7-stat"><div class="v" id="v7-cost">$13,689</div><div class="k">API-equiv</div></div>
|
|
504
|
+
<div class="v7-stat"><div class="v" id="v7-total-tokens">7.49B</div><div class="k">tokens</div></div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="v7-z v7-band">
|
|
508
|
+
<div class="eyebrow">What we found</div>
|
|
509
|
+
<img class="v7-echo" src="echo-face-cutout.png" alt="" />
|
|
510
|
+
<div class="v7-bandCopy">
|
|
511
|
+
<div class="bignum" id="v7-metric"><span id="v7-metric-value">85.0%</span><span id="v7-metric-label">wasteful context</span><button class="v7-infoBtn" id="v7-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
|
|
512
|
+
<div class="say" id="v7-metric-say">The golden-standard model separates useful context from wasteful context, then attributes the waste to concrete problem types across your workspace.</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="v7-z v7-ctaWrap">
|
|
516
|
+
<div class="pre">Ready when you are</div>
|
|
517
|
+
<button class="v7-cta" id="v7-connect" type="button">Connect Echo →</button>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="v7-z v7-foot"><span>Generated by Echo · No transcripts shown</span><span>Local scan · Share-safe city mode</span></div>
|
|
520
|
+
|
|
521
|
+
<div class="fr-panel">
|
|
522
|
+
<div class="fr">
|
|
523
|
+
<div class="fr-dateline"><span>Echo field report</span><span class="fr-rule"></span></div>
|
|
524
|
+
<div class="fr-intro">
|
|
525
|
+
<button class="fr-readBtn" id="fr-read-btn" type="button" aria-expanded="false" aria-controls="fr-persona-hover" aria-label="Your full coding-personality read">?</button>
|
|
526
|
+
<div class="fr-personaHover" id="fr-persona-hover" role="region" aria-label="Your full coding-personality read"></div>
|
|
527
|
+
<img class="fr-avatar" id="fr-avatar" src="./personality_stickers/bossy.png" alt="" />
|
|
528
|
+
<h1 class="fr-greet">hey, <span class="fr-name" id="fr-name">bossy</span><span class="fr-dot">.</span></h1>
|
|
529
|
+
<div class="fr-rank" id="fr-rank">You are <b>the architect · marathoner</b></div>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="fr-mid">
|
|
532
|
+
<p class="fr-lede">Across <mark id="fr-repos">13 repos</mark> you poured in <mark id="fr-hours">191.1 hours</mark> and <mark id="fr-tokens">7.49B tokens</mark> — about <mark id="fr-cost">$13,689</mark> of API-equivalent compute. That whole skyline on the left? You built it.</p>
|
|
533
|
+
<div class="fr-pull">
|
|
534
|
+
<div class="fr-metric" id="fr-metric"><span class="fr-v" id="fr-metric-value">85.0%</span><span class="fr-l" id="fr-metric-label">wasteful context</span></div>
|
|
535
|
+
<div class="fr-say" id="fr-metric-say">The golden-standard model separates useful context from wasteful context, then attributes the waste to concrete problem types.</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="fr-signoff">
|
|
539
|
+
<button class="fr-cta" id="fr-connect" type="button">Connect Echo →</button>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div class="v7-infoSheet" id="v7-info-sheet" aria-hidden="true">
|
|
545
|
+
<div class="v7-infoCard" role="dialog" aria-modal="true" aria-labelledby="v7-info-title">
|
|
546
|
+
<button class="v7-infoClose" id="v7-info-close" type="button" aria-label="Close">×</button>
|
|
547
|
+
<div class="v7-infoTitle" id="v7-info-title">How we read your city</div>
|
|
548
|
+
<div class="v7-infoSub">Every number comes from your local Codex + Claude Code logs. Nothing here is an estimate of you — it's a readout of the tools.</div>
|
|
549
|
+
<div class="v7-infoRow"><div class="sk">Wasteful context</div><div class="sv">The headline comes from the golden-standard workspace rollup: waste tokens divided by official input tokens.</div></div>
|
|
550
|
+
<div class="v7-infoRow"><div class="sk">Useful context</div><div class="sv">Useful context is the portion worth keeping: active task state, product facts, current files, and required overhead.</div></div>
|
|
551
|
+
<div class="v7-infoRow"><div class="sk">$13,689</div><div class="sv"><b>API-equivalent</b> — what this usage would cost at list API rates. Most of it is cache / input reads, so it's a measure of <b>volume, not a bill you paid</b>.</div></div>
|
|
552
|
+
<div class="v7-infoRow"><div class="sk">Evidence</div><div class="sv">Repeated reads are still evidence, but the top-line waste number now includes duplicate context, re-finding, dead work, and residue.</div></div>
|
|
553
|
+
<div class="v7-infoNote">Computed locally · no transcripts leave your machine.</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<!-- in-canvas loading state (setup mode): the report loads in place, no separate page -->
|
|
558
|
+
<div class="scan-loading" id="scan-loading">
|
|
559
|
+
<div class="eyebrow">Local scan</div>
|
|
560
|
+
<div class="title">Reading your local history</div>
|
|
561
|
+
<div class="bar"><i></i></div>
|
|
562
|
+
<div class="count" id="scan-count"></div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div class="setup-header">
|
|
566
|
+
<div class="setup-persona" tabindex="0" aria-describedby="persona-hover">
|
|
567
|
+
<div class="setup-eyebrow">AI Coding City</div>
|
|
568
|
+
<div class="setup-persona-name">bossy</div>
|
|
569
|
+
<div class="setup-rank">the architect · marathoner</div>
|
|
570
|
+
<div class="setup-combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
571
|
+
<div class="persona-cue">hover for the read</div>
|
|
572
|
+
<div class="persona-hover" id="persona-hover" role="tooltip"></div>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="setup-stats">
|
|
575
|
+
<div class="setup-eyebrow">API-equivalent compute spent</div>
|
|
576
|
+
<div class="amount">$13,689</div>
|
|
577
|
+
<div class="detail">You spent <b>191.1h</b> of AI coding.</div>
|
|
578
|
+
<div class="setup-split">
|
|
579
|
+
<span>26% Codex</span>
|
|
580
|
+
<span>74% Claude</span>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<!-- compute cost -->
|
|
586
|
+
<div class="card-zone compute-cost">
|
|
587
|
+
<div class="label">API-equivalent compute spent</div>
|
|
588
|
+
<div class="amount">$13,689</div>
|
|
589
|
+
<div class="detail">You spent <b>191.1h</b> of AI coding.</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- tool split chips -->
|
|
593
|
+
<div class="card-zone tool-split" id="tool-split"></div>
|
|
594
|
+
|
|
595
|
+
<!-- persona -->
|
|
596
|
+
<div class="card-zone persona-card">
|
|
597
|
+
<canvas class="avatar-rive" id="persona-mascot"></canvas>
|
|
598
|
+
<div class="name">bossy</div>
|
|
599
|
+
<div class="rankrhythm">the architect · marathoner</div>
|
|
600
|
+
<div class="combo"><span class="ct">loopy</span><span class="ct">lusty</span><span class="ct">maxxy</span></div>
|
|
601
|
+
<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>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<!-- dirty context -->
|
|
605
|
+
<div class="card-zone dirty-context">
|
|
606
|
+
<div class="pct" id="setup-metric"><span id="setup-metric-value">85.0%</span><span class="word" id="setup-metric-label">wasteful context</span><button class="v7-infoBtn" id="setup-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
|
|
607
|
+
<div class="desc" id="setup-metric-say">The golden-standard model separates useful context from wasteful context across your workspace.</div>
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
<!-- counterfactual: one line — 18 days saved -->
|
|
611
|
+
<div class="card-zone counterfactual">
|
|
612
|
+
<svg class="cal-icon" viewBox="0 0 80 84" xmlns="http://www.w3.org/2000/svg">
|
|
613
|
+
<rect x="24" y="2" width="5" height="13" rx="2.5" fill="#1b1c1f"/>
|
|
614
|
+
<rect x="51" y="2" width="5" height="13" rx="2.5" fill="#1b1c1f"/>
|
|
615
|
+
<rect x="8" y="10" width="64" height="66" rx="12" fill="#ffffff" stroke="#e3e4e7" stroke-width="2"/>
|
|
616
|
+
<path d="M8 22 a12 12 0 0 1 12 -12 h40 a12 12 0 0 1 12 12 v8 H8 Z" fill="#1b1c1f"/>
|
|
617
|
+
<text x="40" y="24" text-anchor="middle" font-family="'IBM Plex Mono', monospace" font-size="11" font-weight="700" fill="#ffffff" letter-spacing="1.2">JUN</text>
|
|
618
|
+
<text x="40" y="64" text-anchor="middle" font-family="'Avenir Next', sans-serif" font-size="36" font-weight="800" fill="#1b1c1f">2</text>
|
|
619
|
+
</svg>
|
|
620
|
+
<div class="cf-body">
|
|
621
|
+
<div class="cf-hero">Echo saves you <b>18 days</b></div>
|
|
622
|
+
<div class="cf-say">You'd have reached today's workspace on <b>Jun 2</b>, not Jun 20.</div>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<button id="setup-connect" class="setup-connect" type="button">Connect Echo</button>
|
|
627
|
+
|
|
628
|
+
<div class="card-zone card-footer">
|
|
629
|
+
<span>Generated by Echo · No transcripts shown</span>
|
|
630
|
+
<span>Local scan · Share-safe city mode</span>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<div id="repo-detail"></div>
|
|
635
|
+
|
|
636
|
+
<script>
|
|
637
|
+
// The poster shares this artifact, but setup must never expose poster labels while it is waiting or
|
|
638
|
+
// failed. Scrub personalized defaults after the DOM exists; the validated report repopulates them.
|
|
639
|
+
try{
|
|
640
|
+
if(new URLSearchParams(location.search).get('setup')==='1'){
|
|
641
|
+
const pendingText={
|
|
642
|
+
'v7-repo-count':'—','v7-attention':'—','v7-cost':'—','v7-total-tokens':'—',
|
|
643
|
+
'v7-metric-value':'—','fr-name':'—','fr-rank':'Waiting for local scan',
|
|
644
|
+
'fr-repos':'— repos','fr-hours':'— hours','fr-tokens':'— tokens','fr-cost':'—',
|
|
645
|
+
'fr-metric-value':'—','setup-metric-value':'—'
|
|
646
|
+
};
|
|
647
|
+
for(const [id,value] of Object.entries(pendingText)){ const el=document.getElementById(id); if(el) el.textContent=value; }
|
|
648
|
+
document.querySelectorAll('.v7-personaName,.setup-persona-name,.persona-card .name').forEach((el)=>{ el.textContent='—'; });
|
|
649
|
+
document.querySelectorAll('.v7-rank,.setup-rank,.persona-card .rankrhythm').forEach((el)=>{ el.textContent='Waiting for local scan'; });
|
|
650
|
+
document.querySelectorAll('.v7-combo,.setup-combo,.persona-card .combo,.setup-split,#tool-split').forEach((el)=>{ el.replaceChildren(); });
|
|
651
|
+
document.querySelectorAll('.setup-stats .amount,.compute-cost .amount').forEach((el)=>{ el.textContent='—'; });
|
|
652
|
+
document.querySelectorAll('.setup-stats .detail,.compute-cost .detail').forEach((el)=>{ el.textContent='Waiting for validated local data.'; });
|
|
653
|
+
document.querySelectorAll('#v7-metric-say,#fr-metric-say,#setup-metric-say').forEach((el)=>{ el.textContent='Waiting for a validated local report.'; });
|
|
654
|
+
const frAvatar=document.getElementById('fr-avatar'); if(frAvatar) frAvatar.removeAttribute('src');
|
|
655
|
+
document.querySelectorAll('.share-card > :not(#scan-loading)').forEach((el)=>{ el.setAttribute('aria-hidden','true'); el.setAttribute('data-setup-pending-hidden',''); });
|
|
656
|
+
}
|
|
657
|
+
}catch(_){}
|
|
658
|
+
</script>
|
|
659
|
+
|
|
660
|
+
<script type="importmap">
|
|
661
|
+
{ "imports": { "three": "./vendor/three.module.min.js" } }
|
|
662
|
+
</script>
|
|
663
|
+
<script type="module">
|
|
664
|
+
import * as THREE from 'three';
|
|
665
|
+
import { RoundedBoxGeometry } from './vendor/RoundedBoxGeometry.js';
|
|
666
|
+
import { OrbitControls } from './vendor/OrbitControls.js';
|
|
667
|
+
|
|
668
|
+
const pageParams=new URLSearchParams(location.search);
|
|
669
|
+
const setupMode=pageParams.get('setup')==='1';
|
|
670
|
+
const previewMode=pageParams.get('preview')==='1';
|
|
671
|
+
const figureModeEmbed=pageParams.get('figure')==='1';
|
|
672
|
+
if(setupMode) document.body.classList.add('setup-mode');
|
|
673
|
+
// figure-mode embeds the city as its own column beside the text: the canvas owns the whole
|
|
674
|
+
// iframe. The 620px text reservation only applies to the old full-screen setup overlay.
|
|
675
|
+
let VIZ_W=setupMode ? (figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620))) : 920;
|
|
676
|
+
let VIZ_H=setupMode ? window.innerHeight : 1180; // repo-visualizer canvas = full card in share mode, left city in setup mode
|
|
677
|
+
const TRAY_COL = 0xd9d5ca; // worn warm concrete; keeps the corrupted city distinct from the white report panel
|
|
678
|
+
const TOOL = { codex:0x10a37f, claude:0xd97757 }; // ONLY two cube colours — exact OpenAI / Claude chip-icon hexes
|
|
679
|
+
const CORRUPTION = 0.82;
|
|
680
|
+
const corruptionFloaters=[];
|
|
681
|
+
const repairGroups=[];
|
|
682
|
+
const repairPieces=[];
|
|
683
|
+
const corruptionFades=[];
|
|
684
|
+
const corruptionSurfaces=[];
|
|
685
|
+
const buildingMaterials=[];
|
|
686
|
+
|
|
687
|
+
function hash01(value, salt=0){
|
|
688
|
+
let h=2166136261 ^ salt;
|
|
689
|
+
for(let i=0;i<String(value).length;i++){
|
|
690
|
+
h^=String(value).charCodeAt(i);
|
|
691
|
+
h=Math.imul(h,16777619);
|
|
692
|
+
}
|
|
693
|
+
return (h>>>0)/4294967295;
|
|
694
|
+
}
|
|
695
|
+
function corruptionProfile(o){
|
|
696
|
+
const pressure=Math.min(1, Math.max(0.28, Math.log10(Math.max(10, o.tokens||10))/10));
|
|
697
|
+
return {
|
|
698
|
+
amount:CORRUPTION*(0.58+pressure*0.42),
|
|
699
|
+
leanX:(hash01(o.name,11)-0.5)*0.25,
|
|
700
|
+
leanZ:(hash01(o.name,19)-0.5)*0.25,
|
|
701
|
+
phase:hash01(o.name,29)*Math.PI*2,
|
|
702
|
+
side:hash01(o.name,37)<0.5?-1:1,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── volume normalization ─────────────────────────────────────────────────────
|
|
707
|
+
// Each cube's VOLUME is proportional to that repo's token usage, measured in a fixed unit so
|
|
708
|
+
// city size is comparable across users. 1 unit = one "coding-day": ~2h of agentic AI coding,
|
|
709
|
+
// which drives ≈80M total tokens (anchored on 7.49B tokens / 191.1h ≈ 39M tok/h, corroborated).
|
|
710
|
+
const TOKENS_PER_UNIT = 80e6; // tokens in one standard 2h coding-day = 1 volume unit
|
|
711
|
+
const MIN_TOKENS = 4e6; // hide repos below ~0.05 coding-days (declutter the long tail)
|
|
712
|
+
const BASE_EDGE = 36; // footprint of a 1-unit cube (world units)
|
|
713
|
+
const TOWER = 1.4; // gentle tower: height = TOWER × base, volume preserved
|
|
714
|
+
const COST_SCALE = 13689/19043; // reconcile forensic per-repo $ (sum ≈ $19,043) to the $13,689 headline
|
|
715
|
+
const LABEL_MIN_BASE = 24; // cubes with footprint ≥ this get a top-face label; smaller ones are click-only
|
|
716
|
+
const fmtTok=(n)=> n>=1e9?`${(n/1e9).toFixed(2)}B` : n>=1e6?`${Math.round(n/1e6)}M` : `${Math.round(n/1e3)}K`;
|
|
717
|
+
const money=(n)=>'$'+Math.round(Number(n)||0).toLocaleString('en-US');
|
|
718
|
+
const oneDec=(n)=>Number(n||0).toFixed(1).replace(/\.0$/,'');
|
|
719
|
+
const REPEATED_READS_SAY="Your agents re-read unchanged files, re-run the same searches, and check git again before they can move. Echo turns those repeated proofs into recall.";
|
|
720
|
+
const GOLDEN_WASTE_SAY="The golden-standard model labels every analyzed input token as useful or wasteful, then attributes waste to duplicate context, re-finding, dead work, and residue across Codex and Claude Code.";
|
|
721
|
+
const monthDay=(iso)=>{
|
|
722
|
+
const d=new Date(iso);
|
|
723
|
+
if(Number.isNaN(d.getTime())) return {month:'JUN', day:'2', label:'Jun 2'};
|
|
724
|
+
const month=d.toLocaleString('en-US',{month:'short', timeZone:'UTC'});
|
|
725
|
+
return {month:month.toUpperCase(), day:String(d.getUTCDate()), label:`${month} ${d.getUTCDate()}`};
|
|
726
|
+
};
|
|
727
|
+
function providerSplit(report){
|
|
728
|
+
let codex=0, claude=0;
|
|
729
|
+
for(const [model, v] of Object.entries(report?.cost?.byModel || {})){
|
|
730
|
+
const tok=Number(v?.total)||0;
|
|
731
|
+
if(/claude|opus|sonnet|haiku|anthropic/i.test(model)) claude+=tok;
|
|
732
|
+
else if(/gpt|codex|openai|o\d/i.test(model)) codex+=tok;
|
|
733
|
+
}
|
|
734
|
+
const total=codex+claude || 1;
|
|
735
|
+
const codexPct=Math.round(codex/total*100);
|
|
736
|
+
return {codexPct, claudePct:100-codexPct, dominant:codex>=claude?'codex':'claude'};
|
|
737
|
+
}
|
|
738
|
+
function clampScore(value){
|
|
739
|
+
return Math.max(0, Math.min(100, Math.round(Number(value)||0)));
|
|
740
|
+
}
|
|
741
|
+
function reportContextMetrics(report){
|
|
742
|
+
const metrics=report?.contextWindow?.metrics;
|
|
743
|
+
return metrics && typeof metrics==='object' ? metrics : null;
|
|
744
|
+
}
|
|
745
|
+
function contextHealthScore(report){
|
|
746
|
+
const metrics=reportContextMetrics(report);
|
|
747
|
+
const direct=Number(metrics?.healthScorePct);
|
|
748
|
+
if(Number.isFinite(direct)) return clampScore(direct);
|
|
749
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
750
|
+
return report && Number.isFinite(stale) ? clampScore(100-stale) : null;
|
|
751
|
+
}
|
|
752
|
+
function pctText(value){
|
|
753
|
+
const n=Number(value);
|
|
754
|
+
return Number.isFinite(n) ? `${Math.round(Math.max(0, Math.min(1, n))*100)}%` : null;
|
|
755
|
+
}
|
|
756
|
+
function contextPressureText(metrics){
|
|
757
|
+
const projected=pctText(metrics?.projectedNextFullnessPct);
|
|
758
|
+
if(projected) return `${projected} projected window pressure`;
|
|
759
|
+
const fullness=pctText(metrics?.contextFullnessPct);
|
|
760
|
+
if(fullness) return `${fullness} window full`;
|
|
761
|
+
const latest=Number(metrics?.latestInputTokens);
|
|
762
|
+
return Number.isFinite(latest) && latest>0 ? `${fmtTok(latest)} latest input` : 'current window pressure';
|
|
763
|
+
}
|
|
764
|
+
function contextNoiseText(report){
|
|
765
|
+
const metrics=reportContextMetrics(report);
|
|
766
|
+
const noise=pctText(metrics?.noisePct);
|
|
767
|
+
if(noise) return `${noise} resident noise`;
|
|
768
|
+
const stale=Number(report?.cleanliness?.staleReadSharePct);
|
|
769
|
+
return Number.isFinite(stale) ? `${oneDec(stale)}% repeated-read signal` : 'resident noise';
|
|
770
|
+
}
|
|
771
|
+
function contextScoreSay(report){
|
|
772
|
+
const score=contextHealthScore(report);
|
|
773
|
+
const metrics=reportContextMetrics(report);
|
|
774
|
+
return `This is the same <b>${score}% context score</b> the HUD uses: ${contextNoiseText(report)} and ${contextPressureText(metrics)}, folded into one live setup readout.`;
|
|
775
|
+
}
|
|
776
|
+
function goldenReport(report){
|
|
777
|
+
const gs=report?.canonicalGoldenStandard || report?.goldenStandard;
|
|
778
|
+
return gs && typeof gs==='object' && !gs.error && gs.summary ? gs : null;
|
|
779
|
+
}
|
|
780
|
+
function goldenSummary(report){
|
|
781
|
+
return goldenReport(report)?.summary || null;
|
|
782
|
+
}
|
|
783
|
+
function goldenWastePct(report){
|
|
784
|
+
const pct=Number(goldenSummary(report)?.wastePct);
|
|
785
|
+
return Number.isFinite(pct) ? pct : Number(report?.cleanliness?.staleReadSharePct)||0;
|
|
786
|
+
}
|
|
787
|
+
function goldenUsefulPct(report){
|
|
788
|
+
const pct=Number(goldenSummary(report)?.usefulPct);
|
|
789
|
+
return Number.isFinite(pct) ? pct : Math.max(0, 100-goldenWastePct(report));
|
|
790
|
+
}
|
|
791
|
+
function goldenWasteSay(report){
|
|
792
|
+
const s=goldenSummary(report);
|
|
793
|
+
if(!s) return REPEATED_READS_SAY;
|
|
794
|
+
const p=report?.billedWasteProjection;
|
|
795
|
+
if(p && p.method==='canonical-window-share-over-provider-input'){
|
|
796
|
+
return `Across <b>${Number(s.sessionsAnalyzed||0).toLocaleString('en-US')} sessions</b>, the canonical windows found <b>${oneDec(s.wastePct)}% wasteful context</b>: a provider-billed projection of ${fmtTok(Number(p.projectedWasteTokens)||0)} waste tokens out of ${fmtTok(Number(p.billedInputTokens)||0)} input.`;
|
|
797
|
+
}
|
|
798
|
+
return `Across <b>${Number(s.sessionsAnalyzed||0).toLocaleString('en-US')} sessions</b>, the canonical windows found <b>${oneDec(s.wastePct)}% wasteful context</b>.`;
|
|
799
|
+
}
|
|
800
|
+
// ---- coding-persona engine (Phase 1: only signals the scan actually has — no commits / accept-all yet) ----
|
|
801
|
+
const PERSONA_TRAITS={
|
|
802
|
+
loopy:{label:'loopy', blurb:"opened the same file 14 times and still had to prove it again. not a skill issue — the session forgot what it had already seen."},
|
|
803
|
+
lusty:{label:'lusty', blurb:"burned a car payment in tokens for demos that'll never ship. zero regrets. Anthropic's about to send a thank-you card. 💸"},
|
|
804
|
+
maxxy:{label:'maxxy', blurb:"Opus to rename a variable, Opus to fix a typo. the small models are right there — but you don't do small models, you do consequences."},
|
|
805
|
+
tabby:{label:'tabby', blurb:"tabs everywhere, half-built features, breadth over depth. your brain is a browser with a memory leak. 🫠"},
|
|
806
|
+
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."},
|
|
807
|
+
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. 🧍"},
|
|
808
|
+
};
|
|
809
|
+
const PERSONA_RANKS={
|
|
810
|
+
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."},
|
|
811
|
+
agent:{label:'agent-pilled', blurb:"saw it one-shot something once and never looked back. drives one agent, still watches its every move."},
|
|
812
|
+
high:{label:'high-agency', blurb:"fluent with the fleet, right-sizes the model, takes the wheel back when it matters."},
|
|
813
|
+
architect:{label:'the architect', blurb:"runs the whole system from above — agents in parallel worktrees, just nods and merges."},
|
|
814
|
+
enlightened:{label:'enlightened', blurb:"transcended the keyboard. hasn't hand-written a line in months and the code's never been cleaner. 🗿"},
|
|
815
|
+
};
|
|
816
|
+
const PERSONA_RHYTHMS={
|
|
817
|
+
marathoner:{label:'marathoner', blurb:"locked in since noon, sun set and rose, did not move. shipped though."},
|
|
818
|
+
raider:{label:'raider', blurb:"25-minute strikes, then gone to think about it in the shower. the breaks are the method."},
|
|
819
|
+
streak:{label:'the streak', blurb:"here every single day, small commits, never misses. the graph must stay green."},
|
|
820
|
+
binger:{label:'binger', blurb:"dark for a week, then a whole app in one unhinged weekend. not a routine — episodes."},
|
|
821
|
+
nightowl:{label:'night owl', blurb:"unreachable at 9am, unstoppable at 1am. the moon is my standup. 🌙"},
|
|
822
|
+
};
|
|
823
|
+
function daysBetween(a,b){ const x=Date.parse(a), y=Date.parse(b); if(isNaN(x)||isNaN(y)) return 1; return Math.max(1, Math.round((y-x)/86400000)+1); }
|
|
824
|
+
function derivePersona(report){
|
|
825
|
+
const cost=report?.cost||{}, scale=report?.scale||{}, uwh=report?.userWorkingHours||{}, rh=report?.rhythm||{}, cl=report?.cleanliness||{};
|
|
826
|
+
const repos=Array.isArray(report?.repos)?report.repos:[];
|
|
827
|
+
const totalTok=Number(scale.totalTokens)||1;
|
|
828
|
+
let opusTok=0; for(const [m,v] of Object.entries(cost.byModel||{})){ if(/opus/i.test(m)) opusTok+=Number(v?.total)||0; }
|
|
829
|
+
const opusShare=opusTok/totalTok;
|
|
830
|
+
const assistant=repos.reduce((s,r)=>s+(Number(r.assistantMessages)||0),0);
|
|
831
|
+
const turns=Math.max(1, Number(uwh.realUserTurns)||0);
|
|
832
|
+
const autonomy=assistant/turns; // AI turns per human prompt → how hands-off you run
|
|
833
|
+
const repoCount=Number(scale.repoCount)||repos.length;
|
|
834
|
+
const stale=goldenWastePct(report), spend=Number(cost.total)||0;
|
|
835
|
+
// Phase-2 git signal: commits landed vs building activity. Gated on commits>0 so a missing git pass can't false-fire ghosty.
|
|
836
|
+
const commits=Number(scale.totalCommits)||0, attention=Number(uwh.userAttentionHours)||0;
|
|
837
|
+
const activeDaysG=Number(uwh.activeAttentionDays)||0;
|
|
838
|
+
const commitsPerDay=activeDaysG>0?commits/activeDaysG:0;
|
|
839
|
+
const ghostyOn=commits>0 && attention>=8 && commitsPerDay<0.5;
|
|
840
|
+
// measurable traits only — primary = strongest firing signal, sides = the next few
|
|
841
|
+
const scored=[
|
|
842
|
+
{key:'loopy', on:stale>=45, s:stale/100},
|
|
843
|
+
{key:'lusty', on:spend>=1500, s:Math.min(1,spend/12000)},
|
|
844
|
+
{key:'maxxy', on:opusShare>=0.5, s:opusShare},
|
|
845
|
+
{key:'bossy', on:autonomy>=5, s:Math.min(1,autonomy/20)},
|
|
846
|
+
{key:'tabby', on:repoCount>=10, s:Math.min(1,repoCount/30)},
|
|
847
|
+
{key:'ghosty', on:ghostyOn, s:ghostyOn?Math.min(0.8, 1-Math.min(1,commitsPerDay/0.5)):0},
|
|
848
|
+
].sort((a,b)=>b.s-a.s);
|
|
849
|
+
const primaryKey=scored[0].key; // most characteristic trait (highest signal)
|
|
850
|
+
const sideKeys=scored.filter(t=>t.on && t.key!==primaryKey).slice(0,3).map(t=>t.key);
|
|
851
|
+
// rank — rough proxy from autonomy + breadth (the true rank signals like re-roll aren't scanned yet)
|
|
852
|
+
let rankKey='tourist';
|
|
853
|
+
if(autonomy>=14&&repoCount>=12) rankKey='enlightened';
|
|
854
|
+
else if(autonomy>=8) rankKey='architect';
|
|
855
|
+
else if(autonomy>=4) rankKey='high';
|
|
856
|
+
else if(autonomy>=2) rankKey='agent';
|
|
857
|
+
// rhythm — fully measurable from work blocks + active-day density + late-night share
|
|
858
|
+
const lateShare=Number(rh.lateNightShare)||0;
|
|
859
|
+
const avgBlock=(Number(uwh.userAttentionHours)||0)/Math.max(1,Number(uwh.workBlocks)||0);
|
|
860
|
+
const density=(Number(uwh.activeAttentionDays)||0)/daysBetween(report?.scanStartDate, report?.scanEndDate);
|
|
861
|
+
const maxDay=Number(uwh.maxDailyAttentionHours)||0;
|
|
862
|
+
let rhythmKey='marathoner';
|
|
863
|
+
if(lateShare>=25) rhythmKey='nightowl';
|
|
864
|
+
else if(density<=0.35&&maxDay>=6) rhythmKey='binger';
|
|
865
|
+
else if(density>=0.6) rhythmKey='streak';
|
|
866
|
+
else if(avgBlock<=0.75) rhythmKey='raider';
|
|
867
|
+
return { primary:PERSONA_TRAITS[primaryKey], sides:sideKeys.map(k=>PERSONA_TRAITS[k]),
|
|
868
|
+
rank:PERSONA_RANKS[rankKey], rhythm:PERSONA_RHYTHMS[rhythmKey] };
|
|
869
|
+
}
|
|
870
|
+
function renderPersonaHover(persona){
|
|
871
|
+
const host=document.getElementById('persona-hover');
|
|
872
|
+
if(!host) return;
|
|
873
|
+
const esc=(t)=>String(t).replace(/[&<>]/g,(c)=>({'&':'&','<':'<','>':'>'}[c]));
|
|
874
|
+
const line=(t)=>`<div class="ph-line"><b>${esc(t.label)}</b> — ${esc(t.blurb)}</div>`;
|
|
875
|
+
host.innerHTML = line(persona.primary) + persona.sides.map(line).join('') +
|
|
876
|
+
`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>` +
|
|
877
|
+
`<div class="ph-meta"><b>${esc(persona.rhythm.label)}</b> · ${esc(persona.rhythm.blurb)}</div>`;
|
|
878
|
+
}
|
|
879
|
+
function renderV7PersonaHover(persona){
|
|
880
|
+
const host=document.getElementById('v7-persona-hover');
|
|
881
|
+
if(!host) return;
|
|
882
|
+
const esc=(t)=>String(t).replace(/[&<>]/g,(c)=>({'&':'&','<':'<','>':'>'}[c]));
|
|
883
|
+
const line=(t)=>t?`<div class="ph-line"><b>${esc(t.label)}</b> — ${esc(t.blurb)}</div>`:'';
|
|
884
|
+
if(!persona){
|
|
885
|
+
const byLabel=(table,label)=>Object.values(table).find((v)=>v.label.toLowerCase()===String(label||'').trim().toLowerCase());
|
|
886
|
+
const rankBits=String(document.querySelector('.v7-rank')?.textContent||'').split('·').map((s)=>s.trim());
|
|
887
|
+
persona={
|
|
888
|
+
primary:byLabel(PERSONA_TRAITS, document.querySelector('.v7-personaName')?.textContent),
|
|
889
|
+
sides:Array.from(document.querySelectorAll('.v7-combo .ct')).map((el)=>byLabel(PERSONA_TRAITS, el.textContent)).filter(Boolean),
|
|
890
|
+
rank:byLabel(PERSONA_RANKS, rankBits[0]),
|
|
891
|
+
rhythm:byLabel(PERSONA_RHYTHMS, rankBits[1]),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
host.innerHTML = line(persona.primary) + (persona.sides||[]).map(line).join('') +
|
|
895
|
+
(persona.rank?`<div class="ph-meta"><b>${esc(persona.rank.label)}</b> · ${esc(persona.rank.blurb)}</div>`:'') +
|
|
896
|
+
(persona.rhythm?`<div class="ph-meta"><b>${esc(persona.rhythm.label)}</b> · ${esc(persona.rhythm.blurb)}</div>`:'');
|
|
897
|
+
const frHost=document.getElementById('fr-persona-hover');
|
|
898
|
+
if(frHost) frHost.innerHTML=host.innerHTML;
|
|
899
|
+
}
|
|
900
|
+
function buildSetupRepos(report){
|
|
901
|
+
const split=providerSplit(report);
|
|
902
|
+
const source=Array.isArray(report?.repos) ? report.repos : [];
|
|
903
|
+
const usable=source
|
|
904
|
+
.filter((repo)=>Number(repo?.tokens)>0)
|
|
905
|
+
.sort((a,b)=>(Number(b.tokens)||0)-(Number(a.tokens)||0))
|
|
906
|
+
.slice(0, 18);
|
|
907
|
+
return usable.map((repo, i)=>{
|
|
908
|
+
let x, z;
|
|
909
|
+
if(i===0){ x=-36; z=-58; }
|
|
910
|
+
else if(i===1){ x=128; z=-12; }
|
|
911
|
+
else {
|
|
912
|
+
const angle=i*2.399963229728653;
|
|
913
|
+
const radius=72+Math.sqrt(i)*42;
|
|
914
|
+
x=Math.cos(angle)*radius;
|
|
915
|
+
z=Math.sin(angle)*radius+28;
|
|
916
|
+
}
|
|
917
|
+
const cxTok=Number(repo.codexTokens)||0, clTok=Number(repo.claudeTokens)||0, tTok=cxTok+clTok;
|
|
918
|
+
const codexPct = tTok ? Math.round(cxTok/tTok*100) : ((repo.dominantProvider==='codex')?100:0);
|
|
919
|
+
return {
|
|
920
|
+
name:String(repo.name || `Repo ${i+1}`),
|
|
921
|
+
x, z,
|
|
922
|
+
tool:(repo.dominantProvider==='codex'||repo.dominantProvider==='claude')?repo.dominantProvider:split.dominant,
|
|
923
|
+
tokens:Number(repo.tokens)||0,
|
|
924
|
+
cost:Number(repo.cost)||0,
|
|
925
|
+
days:Number(repo.equivalentFocusDays)||0,
|
|
926
|
+
hours:Number(repo.attentionHours)||0,
|
|
927
|
+
sessions:Number(repo.sessions)||0,
|
|
928
|
+
codexPct, claudePct:100-codexPct,
|
|
929
|
+
pin:i<2,
|
|
930
|
+
daily:Array.isArray(repo.daily)?repo.daily:null,
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function applySetupReport(report){
|
|
935
|
+
if(!setupMode || !report) return;
|
|
936
|
+
const split=providerSplit(report);
|
|
937
|
+
const persona=derivePersona(report);
|
|
938
|
+
const cost=money(report?.cost?.total);
|
|
939
|
+
const attention=oneDec(report?.userWorkingHours?.userAttentionHours);
|
|
940
|
+
const dirty=oneDec(goldenWastePct(report));
|
|
941
|
+
const metricValue=`${dirty}%`;
|
|
942
|
+
const metricLabel='wasteful context';
|
|
943
|
+
const metricSay=goldenWasteSay(report);
|
|
944
|
+
const cf=report?.counterfactual || {};
|
|
945
|
+
const reach=monthDay(cf.counterfactualDate);
|
|
946
|
+
const instead=monthDay(report?.scanEndDate).label;
|
|
947
|
+
const days=Math.max(0, Math.round(Number(cf.daysGained)||0));
|
|
948
|
+
document.querySelectorAll('.setup-persona-name,.persona-card .name,.v7-personaName').forEach((el)=>{ el.textContent=persona.primary.label; });
|
|
949
|
+
document.querySelectorAll('.setup-rank,.persona-card .rankrhythm,.v7-rank').forEach((el)=>{ el.textContent=`${persona.rank.label} · ${persona.rhythm.label}`; });
|
|
950
|
+
const frRank=document.getElementById('fr-rank'); if(frRank) frRank.innerHTML=`You are <b>${persona.rank.label}</b> on <b>${persona.rhythm.label}</b>`;
|
|
951
|
+
document.querySelectorAll('.setup-combo,.persona-card .combo,.v7-combo').forEach((el)=>{ el.innerHTML=persona.sides.map((s)=>`<span class="ct">${s.label}</span>`).join(''); });
|
|
952
|
+
renderPersonaHover(persona);
|
|
953
|
+
renderV7PersonaHover(persona);
|
|
954
|
+
document.querySelectorAll('.setup-stats .amount,.compute-cost .amount').forEach((el)=>{ el.textContent=cost; });
|
|
955
|
+
document.querySelectorAll('.setup-stats .detail,.compute-cost .detail').forEach((el)=>{ el.innerHTML=`You spent <b>${attention}h</b> of AI coding.`; });
|
|
956
|
+
const v7Cost=document.getElementById('v7-cost'); if(v7Cost) v7Cost.textContent=cost;
|
|
957
|
+
const v7Attention=document.getElementById('v7-attention'); if(v7Attention) v7Attention.textContent=`${attention}h`;
|
|
958
|
+
const repoCount=Number(report?.scale?.repoCount) || (Array.isArray(report?.repos) ? report.repos.length : 0);
|
|
959
|
+
const v7RepoCount=document.getElementById('v7-repo-count'); if(v7RepoCount && repoCount) v7RepoCount.textContent=String(repoCount);
|
|
960
|
+
const v7Tokens=document.getElementById('v7-total-tokens'); if(v7Tokens) v7Tokens.textContent=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
961
|
+
const v7MetricValue=document.getElementById('v7-metric-value'); if(v7MetricValue) v7MetricValue.textContent=metricValue;
|
|
962
|
+
const v7MetricLabel=document.getElementById('v7-metric-label'); if(v7MetricLabel) v7MetricLabel.textContent=metricLabel;
|
|
963
|
+
const v7MetricSay=document.getElementById('v7-metric-say');
|
|
964
|
+
if(v7MetricSay) v7MetricSay.innerHTML=metricSay;
|
|
965
|
+
const frName=document.getElementById('fr-name'); if(frName) frName.textContent=persona.primary.label;
|
|
966
|
+
const frAvatar=document.getElementById('fr-avatar');
|
|
967
|
+
if(frAvatar){
|
|
968
|
+
const key=String(persona.primary?.label||'').toLowerCase();
|
|
969
|
+
if(PERSONA_TRAITS[key]){ frAvatar.src=`./personality_stickers/${key}.png`; frAvatar.alt=`${key} avatar`; }
|
|
970
|
+
}
|
|
971
|
+
const frRepos=document.getElementById('fr-repos'); if(frRepos && repoCount) frRepos.textContent=`${repoCount} ${repoCount===1?'repo':'repos'}`;
|
|
972
|
+
const frHours=document.getElementById('fr-hours'); if(frHours) frHours.textContent=`${attention} hours`;
|
|
973
|
+
const frTokens=document.getElementById('fr-tokens'); if(frTokens) frTokens.textContent=`${fmtTok(Number(report?.scale?.totalTokens)||0)} tokens`;
|
|
974
|
+
const frCost=document.getElementById('fr-cost'); if(frCost) frCost.textContent=cost;
|
|
975
|
+
const frMetricValue=document.getElementById('fr-metric-value'); if(frMetricValue) frMetricValue.textContent=metricValue;
|
|
976
|
+
const frMetricLabel=document.getElementById('fr-metric-label'); if(frMetricLabel) frMetricLabel.textContent=metricLabel;
|
|
977
|
+
const frMetricSay=document.getElementById('fr-metric-say'); if(frMetricSay) frMetricSay.innerHTML=metricSay;
|
|
978
|
+
try{ window.parent?.postMessage({type:'echomem:city-fieldnote',
|
|
979
|
+
avatar:(frAvatar&&frAvatar.src)||'',
|
|
980
|
+
name:String(persona?.primary?.label||''),
|
|
981
|
+
rank:(document.getElementById('fr-rank')||{}).innerHTML||'',
|
|
982
|
+
lede:(document.querySelector('.fr-lede')||{}).innerHTML||'',
|
|
983
|
+
guide:{
|
|
984
|
+
current:{primary:String(persona?.primary?.label||''), rank:String(persona?.rank?.label||''), rhythm:String(persona?.rhythm?.label||'')},
|
|
985
|
+
traits:Object.values(PERSONA_TRAITS).map(({label,blurb})=>({label,blurb})),
|
|
986
|
+
ranks:Object.values(PERSONA_RANKS).map(({label,blurb})=>({label,blurb})),
|
|
987
|
+
rhythms:Object.values(PERSONA_RHYTHMS).map(({label,blurb})=>({label,blurb}))
|
|
988
|
+
}
|
|
989
|
+
}, location.origin); }catch(_){}
|
|
990
|
+
const splitHtml=`<span>${split.codexPct}% Codex</span><span>${split.claudePct}% Claude</span>`;
|
|
991
|
+
const setupSplit=document.querySelector('.setup-split'); if(setupSplit) setupSplit.innerHTML=splitHtml;
|
|
992
|
+
const csClaude=document.querySelector('#city-split .cs-claude'); if(csClaude) csClaude.style.width=`${split.claudePct}%`;
|
|
993
|
+
const csCodex=document.querySelector('#city-split .cs-codex'); if(csCodex) csCodex.style.width=`${split.codexPct}%`;
|
|
994
|
+
const csClB=document.querySelector('#city-split .cs-l.claude b'); if(csClB) csClB.textContent=`${split.claudePct}%`;
|
|
995
|
+
const csCdB=document.querySelector('#city-split .cs-l.codex b'); if(csCdB) csCdB.textContent=`${split.codexPct}%`;
|
|
996
|
+
const tlStartDate=new Date(report?.scanStartDate);
|
|
997
|
+
if(!Number.isNaN(tlStartDate.getTime())){
|
|
998
|
+
const tlStartEl=document.querySelector('#city-timeline .tl-start');
|
|
999
|
+
if(tlStartEl) tlStartEl.textContent=tlStartDate.toLocaleString('en-US',{month:'short', year:'numeric', timeZone:'UTC'});
|
|
1000
|
+
}
|
|
1001
|
+
const spanDays=daysBetween(report?.scanStartDate, report?.scanEndDate);
|
|
1002
|
+
const spanLabel=spanDays>=60 ? `${Math.round(spanDays/30.44)} months with AI` : `${spanDays} day${spanDays===1?'':'s'} with AI`;
|
|
1003
|
+
const tlSpanEl=document.querySelector('#city-timeline .tl-span'); if(tlSpanEl) tlSpanEl.textContent=spanLabel;
|
|
1004
|
+
const dirtyPct=document.querySelector('.dirty-context .pct');
|
|
1005
|
+
if(dirtyPct) dirtyPct.innerHTML=`<span id="setup-metric-value">${metricValue}</span><span class="word" id="setup-metric-label">${metricLabel}</span><button class="v7-infoBtn" id="setup-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button>`;
|
|
1006
|
+
const dirtyDesc=document.querySelector('.dirty-context .desc');
|
|
1007
|
+
if(dirtyDesc){ dirtyDesc.id='setup-metric-say'; dirtyDesc.innerHTML=metricSay; }
|
|
1008
|
+
const monthEl=document.querySelector('.counterfactual text:first-of-type'); if(monthEl) monthEl.textContent=reach.month;
|
|
1009
|
+
const dayEl=document.querySelector('.counterfactual text:last-of-type'); if(dayEl) dayEl.textContent=reach.day;
|
|
1010
|
+
const cfHero=document.querySelector('.cf-hero'); if(cfHero) cfHero.innerHTML=`Echo saves you <b>${days} days</b>`;
|
|
1011
|
+
const cfSay=document.querySelector('.cf-say'); if(cfSay) cfSay.innerHTML=`You'd have reached today's workspace on <b>${reach.label}</b>, not ${instead}.`;
|
|
1012
|
+
signpostTokens=fmtTok(Number(report?.scale?.totalTokens)||0);
|
|
1013
|
+
signpostHours=`${attention}h`;
|
|
1014
|
+
signpostCost=cost;
|
|
1015
|
+
const dirtyShare=Math.max(0, Number(dirty)||0)/100;
|
|
1016
|
+
signpostWastedTime=`≈${Math.round((Number(attention)||0)*dirtyShare)}h WASTED`;
|
|
1017
|
+
signpostWastedPct=`${Math.round(Number(dirty))||0}% WASTED`;
|
|
1018
|
+
signpostWastedCost=`≈${money((Number(report?.cost?.total)||0)*dirtyShare)} WASTED`;
|
|
1019
|
+
}
|
|
1020
|
+
async function loadSetupReport(){
|
|
1021
|
+
const nonce=pageParams.get('nonce');
|
|
1022
|
+
if(!setupMode) return null;
|
|
1023
|
+
const countEl=document.getElementById('scan-count');
|
|
1024
|
+
const loadingEl=document.getElementById('scan-loading');
|
|
1025
|
+
const setLoadState=(kind, title, detail)=>{
|
|
1026
|
+
const titleEl=loadingEl?.querySelector('.title');
|
|
1027
|
+
const eyebrowEl=loadingEl?.querySelector('.eyebrow');
|
|
1028
|
+
const barEl=loadingEl?.querySelector('.bar');
|
|
1029
|
+
if(titleEl) titleEl.textContent=title;
|
|
1030
|
+
if(eyebrowEl && kind!=='running') eyebrowEl.textContent=kind==='empty'?'Local scan complete':'Local scan error';
|
|
1031
|
+
if(barEl) barEl.hidden=kind!=='running';
|
|
1032
|
+
if(countEl) countEl.textContent=String(detail||'').slice(0, 240);
|
|
1033
|
+
};
|
|
1034
|
+
const reportShape=(report)=>{
|
|
1035
|
+
if(!report || typeof report!=='object') return {ok:false, code:'REPORT_NOT_OBJECT'};
|
|
1036
|
+
if(report.schemaVersion!==1) return {ok:false, code:'REPORT_SCHEMA_UNSUPPORTED'};
|
|
1037
|
+
const origin=report.dataOrigin;
|
|
1038
|
+
if((previewMode && origin!=='design-preview') || (!previewMode && origin!=='local-workspace-scan')){
|
|
1039
|
+
return {ok:false, code:'REPORT_ORIGIN_REJECTED'};
|
|
1040
|
+
}
|
|
1041
|
+
const finiteNonNegative=(value)=>typeof value==='number' && Number.isFinite(value) && value>=0;
|
|
1042
|
+
const scale=report.scale;
|
|
1043
|
+
const cost=report.cost;
|
|
1044
|
+
const hours=report.userWorkingHours;
|
|
1045
|
+
const summary=goldenSummary(report);
|
|
1046
|
+
const projection=report.billedWasteProjection;
|
|
1047
|
+
const cf=report.counterfactual;
|
|
1048
|
+
if(!scale || typeof scale!=='object' || !finiteNonNegative(scale.totalTokens) || Number(scale.totalTokens)<=0 ||
|
|
1049
|
+
!finiteNonNegative(scale.totalInputTokens) || Number(scale.totalInputTokens)<=0 ||
|
|
1050
|
+
!finiteNonNegative(scale.sessionCount) || Number(scale.sessionCount)<=0 ||
|
|
1051
|
+
!finiteNonNegative(scale.repoCount) || Number(scale.repoCount)<=0){
|
|
1052
|
+
return {ok:false, code:'REPORT_SCALE_INVALID'};
|
|
1053
|
+
}
|
|
1054
|
+
if(!cost || typeof cost!=='object' || !finiteNonNegative(cost.total) || !cost.byModel || typeof cost.byModel!=='object'){
|
|
1055
|
+
return {ok:false, code:'REPORT_COST_INVALID'};
|
|
1056
|
+
}
|
|
1057
|
+
if(!hours || typeof hours!=='object' || !finiteNonNegative(hours.userAttentionHours) ||
|
|
1058
|
+
!finiteNonNegative(hours.averageDailyAttentionHours)){
|
|
1059
|
+
return {ok:false, code:'REPORT_ATTENTION_INVALID'};
|
|
1060
|
+
}
|
|
1061
|
+
if(!summary || !finiteNonNegative(summary.sessionsAnalyzed) || Number(summary.sessionsAnalyzed)<=0 ||
|
|
1062
|
+
!finiteNonNegative(summary.officialInputTokens) || Number(summary.officialInputTokens)<=0 ||
|
|
1063
|
+
!finiteNonNegative(summary.wasteTokens) || !finiteNonNegative(summary.usefulTokens) ||
|
|
1064
|
+
!finiteNonNegative(summary.wastePct) || Number(summary.wastePct)>100){
|
|
1065
|
+
return {ok:false, code:'REPORT_CANONICAL_INVALID'};
|
|
1066
|
+
}
|
|
1067
|
+
if(!projection || typeof projection!=='object' || projection.method!=='canonical-window-share-over-provider-input' ||
|
|
1068
|
+
!finiteNonNegative(projection.billedInputTokens) || projection.billedInputTokens!==scale.totalInputTokens ||
|
|
1069
|
+
!finiteNonNegative(projection.canonicalInputTokens) || projection.canonicalInputTokens!==summary.officialInputTokens ||
|
|
1070
|
+
!finiteNonNegative(projection.canonicalWasteTokens) || projection.canonicalWasteTokens!==summary.wasteTokens ||
|
|
1071
|
+
!finiteNonNegative(projection.projectedUsefulTokens) || !finiteNonNegative(projection.projectedWasteTokens) ||
|
|
1072
|
+
projection.projectedUsefulTokens+projection.projectedWasteTokens!==projection.billedInputTokens){
|
|
1073
|
+
return {ok:false, code:'REPORT_PROJECTION_INVALID'};
|
|
1074
|
+
}
|
|
1075
|
+
if(!Array.isArray(report.repos) || !report.repos.some((repo)=>repo && typeof repo.name==='string' && repo.name.trim() &&
|
|
1076
|
+
typeof repo.tokens==='number' && Number.isFinite(repo.tokens) && repo.tokens>0)){
|
|
1077
|
+
return {ok:false, code:'REPORT_REPOS_EMPTY'};
|
|
1078
|
+
}
|
|
1079
|
+
if(!Number.isFinite(contextHealthScore(report)) || !cf || typeof cf!=='object' ||
|
|
1080
|
+
!finiteNonNegative(cf.daysGained) || !finiteNonNegative(cf.targetScore) || cf.targetScore>100 ||
|
|
1081
|
+
typeof report.scanStartDate!=='string' || Number.isNaN(Date.parse(report.scanStartDate)) ||
|
|
1082
|
+
typeof report.scanEndDate!=='string' || Number.isNaN(Date.parse(report.scanEndDate)) ||
|
|
1083
|
+
typeof cf.counterfactualDate!=='string' || Number.isNaN(Date.parse(cf.counterfactualDate))){
|
|
1084
|
+
return {ok:false, code:'REPORT_DERIVED_FIELDS_INVALID'};
|
|
1085
|
+
}
|
|
1086
|
+
return {ok:true};
|
|
1087
|
+
};
|
|
1088
|
+
const classifyEnvelope=(envelope)=>{
|
|
1089
|
+
if(!envelope || typeof envelope!=='object' || envelope.schemaVersion!==1 ||
|
|
1090
|
+
typeof envelope.scanId!=='string' || !envelope.scanId.trim()){
|
|
1091
|
+
return {kind:'invalid', code:'REPORT_ENVELOPE_INVALID'};
|
|
1092
|
+
}
|
|
1093
|
+
if(envelope.kind==='scanning') return {kind:'scanning', progress:envelope.progress||{}};
|
|
1094
|
+
if(envelope.kind==='empty') return {kind:'empty'};
|
|
1095
|
+
if(envelope.kind==='failed'){
|
|
1096
|
+
const error=envelope.error && typeof envelope.error==='object' ? envelope.error : {};
|
|
1097
|
+
return {kind:'failed', code:String(error.code||'REPORT_BUILD_FAILED'), message:String(error.message||'The local report could not be built.')};
|
|
1098
|
+
}
|
|
1099
|
+
if(envelope.kind!=='ready') return {kind:'invalid', code:'REPORT_ENVELOPE_KIND_INVALID'};
|
|
1100
|
+
const shape=reportShape(envelope.report);
|
|
1101
|
+
if(!shape.ok) return {kind:'invalid', code:shape.code};
|
|
1102
|
+
const origin=envelope.report.dataOrigin;
|
|
1103
|
+
if(origin==='local-workspace-scan' && envelope.mode!=='production') return {kind:'invalid', code:'REPORT_MODE_INVALID'};
|
|
1104
|
+
if(origin==='design-preview' && (!previewMode || envelope.mode!=='preview')) return {kind:'invalid', code:'REPORT_PREVIEW_REJECTED'};
|
|
1105
|
+
return {kind:'ready', report:envelope.report};
|
|
1106
|
+
};
|
|
1107
|
+
if(!nonce){
|
|
1108
|
+
setLoadState('failed', 'Your local report could not be shown', 'REPORT_NONCE_MISSING: Rerun Echo setup to open a verified local scan.');
|
|
1109
|
+
return new Promise(()=>{});
|
|
1110
|
+
}
|
|
1111
|
+
const renderScanProgress=(progress)=>{
|
|
1112
|
+
const scanned=Number(progress?.scanned||0);
|
|
1113
|
+
const total=Number(progress?.total||0);
|
|
1114
|
+
const elapsed=Number(progress?.stageElapsedMs||0)>0 ? ' · '+Math.round(Number(progress.stageElapsedMs)/1000)+'s' : '';
|
|
1115
|
+
if(total>0){
|
|
1116
|
+
const detail=progress?.stage==='classifying-repeated-context'
|
|
1117
|
+
? (progress.label || 'classifying repeated context')+' '+scanned.toLocaleString()+' / '+total.toLocaleString()+elapsed
|
|
1118
|
+
: (progress?.stage && progress.stage!=='reading-transcripts'
|
|
1119
|
+
? (progress.label || 'building report')+elapsed
|
|
1120
|
+
: 'read '+scanned.toLocaleString()+' / '+total.toLocaleString()+' transcript files');
|
|
1121
|
+
setLoadState('running', 'Reading your local history', detail);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
// The parent owns /report polling and validation. The city only renders the
|
|
1125
|
+
// parent-provided state, keeping loading, failure, and ready state in one place.
|
|
1126
|
+
return await new Promise((resolve)=>{
|
|
1127
|
+
let closed=false;
|
|
1128
|
+
const close=()=>{
|
|
1129
|
+
if(closed) return;
|
|
1130
|
+
closed=true;
|
|
1131
|
+
window.removeEventListener('message', onMessage);
|
|
1132
|
+
};
|
|
1133
|
+
const stop=(kind, code, message)=>{
|
|
1134
|
+
close();
|
|
1135
|
+
if(kind==='empty'){
|
|
1136
|
+
setLoadState('empty', 'No local workspace data found', 'No demo data was substituted.');
|
|
1137
|
+
}else{
|
|
1138
|
+
setLoadState('failed', 'Your local report could not be shown', String(code||'REPORT_BUILD_FAILED')+': '+(message||'Rerun Echo setup to try again.'));
|
|
1139
|
+
}
|
|
1140
|
+
// Keep the city on its terminal loading/error surface instead of drawing a city without a report.
|
|
1141
|
+
};
|
|
1142
|
+
const onMessage=(event)=>{
|
|
1143
|
+
if(event.origin!==location.origin) return;
|
|
1144
|
+
const data=event.data||{};
|
|
1145
|
+
if(data.type!=='echomem:setup-report' || data.nonce!==nonce || !Object.prototype.hasOwnProperty.call(data,'envelope')) return;
|
|
1146
|
+
const classified=classifyEnvelope(data.envelope);
|
|
1147
|
+
if(classified.kind==='scanning'){
|
|
1148
|
+
renderScanProgress(classified.progress);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if(classified.kind==='ready'){
|
|
1152
|
+
close();
|
|
1153
|
+
resolve(data.envelope);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if(classified.kind==='empty'){
|
|
1157
|
+
stop('empty');
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
stop('failed', classified.code, classified.message || 'The report failed provenance or structure validation.');
|
|
1161
|
+
};
|
|
1162
|
+
window.addEventListener('message', onMessage);
|
|
1163
|
+
try{ window.parent?.postMessage({type:'echomem:city-ready', nonce}, location.origin); }catch(_){}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
let signpostTokens=setupMode?'—':'7.49B';
|
|
1167
|
+
let signpostHours=setupMode?'—':'191.1h';
|
|
1168
|
+
let signpostCost=setupMode?'—':'$13,689';
|
|
1169
|
+
let signpostWastedTime=setupMode?'—':'≈162h WASTED';
|
|
1170
|
+
let signpostWastedPct=setupMode?'—':'85% WASTED';
|
|
1171
|
+
let signpostWastedCost=setupMode?'—':'≈$11,636 WASTED';
|
|
1172
|
+
const setupEnvelope=await loadSetupReport();
|
|
1173
|
+
const setupReport=setupMode ? setupEnvelope.report : null;
|
|
1174
|
+
const setupLiveRepos=setupMode ? buildSetupRepos(setupReport) : [];
|
|
1175
|
+
applySetupReport(setupReport);
|
|
1176
|
+
renderV7PersonaHover();
|
|
1177
|
+
let comparisonBefore=contextHealthScore(setupReport);
|
|
1178
|
+
if(!Number.isFinite(comparisonBefore)) comparisonBefore=setupMode?0:58;
|
|
1179
|
+
const reportTargetScore=Number(setupReport?.counterfactual?.targetScore);
|
|
1180
|
+
let comparisonAfter=Number.isFinite(reportTargetScore) ? clampScore(reportTargetScore) : (setupMode?comparisonBefore:clampScore(comparisonBefore+28));
|
|
1181
|
+
comparisonAfter=Math.max(comparisonBefore,comparisonAfter);
|
|
1182
|
+
let cityComparison='without';
|
|
1183
|
+
let repairTarget=0;
|
|
1184
|
+
let repairProgress=0;
|
|
1185
|
+
let dirty=true;
|
|
1186
|
+
let shadowFrame=0;
|
|
1187
|
+
|
|
1188
|
+
function setupWasteShare(){
|
|
1189
|
+
const pct=Number(goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || 0);
|
|
1190
|
+
if(Number.isFinite(pct) && pct>0) return Math.max(0, Math.min(0.95, pct/100));
|
|
1191
|
+
return Math.max(0, Math.min(0.95, (100-comparisonBefore)/100));
|
|
1192
|
+
}
|
|
1193
|
+
function setupAttentionHours(){
|
|
1194
|
+
const direct=Number(setupReport?.userWorkingHours?.userAttentionHours);
|
|
1195
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1196
|
+
return all.reduce((sum,o)=>sum+(Number(o.hours)||0),0);
|
|
1197
|
+
}
|
|
1198
|
+
function setupInputTokens(){
|
|
1199
|
+
const direct=Number(setupReport?.billedWasteProjection?.billedInputTokens);
|
|
1200
|
+
if(Number.isFinite(direct) && direct>0) return direct;
|
|
1201
|
+
return 0;
|
|
1202
|
+
}
|
|
1203
|
+
function setupUsefulTokens(){
|
|
1204
|
+
const direct=Number(setupReport?.billedWasteProjection?.projectedUsefulTokens);
|
|
1205
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1206
|
+
return 0;
|
|
1207
|
+
}
|
|
1208
|
+
function setupApiCost(){
|
|
1209
|
+
const direct=Number(setupReport?.cost?.total);
|
|
1210
|
+
if(Number.isFinite(direct) && direct>=0) return direct;
|
|
1211
|
+
return all.reduce((sum,o)=>sum+(Number(o.cost)||0),0);
|
|
1212
|
+
}
|
|
1213
|
+
function fmtInt(n){ return Math.round(Number(n)||0).toLocaleString('en-US'); }
|
|
1214
|
+
function cityMetricReadout(){
|
|
1215
|
+
const waste=setupWasteShare();
|
|
1216
|
+
const attention=setupAttentionHours();
|
|
1217
|
+
const reportedDaily=Number(setupReport?.userWorkingHours?.averageDailyAttentionHours);
|
|
1218
|
+
const daily=Number.isFinite(reportedDaily) && reportedDaily>0 ? reportedDaily : (attention>0?attention:0);
|
|
1219
|
+
const currentDays=daily>0?Math.ceil(attention/daily):0;
|
|
1220
|
+
const withHours=Math.max(0, attention*(1-waste));
|
|
1221
|
+
const withDays=daily>0?Math.ceil(withHours/daily):0;
|
|
1222
|
+
const inputTokens=setupInputTokens();
|
|
1223
|
+
const usefulTokens=Math.max(0, Math.min(inputTokens, setupUsefulTokens()));
|
|
1224
|
+
const apiCost=setupApiCost();
|
|
1225
|
+
const withCost=Math.max(0, apiCost*(1-waste));
|
|
1226
|
+
const savedDays=Math.max(0, currentDays-withDays);
|
|
1227
|
+
const savedTokens=Math.max(0, inputTokens-usefulTokens);
|
|
1228
|
+
const savedCost=Math.max(0, apiCost-withCost);
|
|
1229
|
+
const mode=cityComparison==='with'?'with':'without';
|
|
1230
|
+
const metric=cityMetric==='tokens'||cityMetric==='cost'?cityMetric:'time';
|
|
1231
|
+
const data={
|
|
1232
|
+
time:{
|
|
1233
|
+
without:{value:`${fmtInt(currentDays)} days`,label:'time without Echo',say:`${fmtInt(attention)}h of agent work at <b>${oneDec(daily)}h per active day</b>. This is the pace that built the current city.`},
|
|
1234
|
+
with:{value:`${fmtInt(withDays)} days`,label:'time with Echo',say:`Same project state, with repeat context cleared. That frees about <b>${fmtInt(savedDays)} working days</b> for everything else.`}
|
|
1235
|
+
},
|
|
1236
|
+
tokens:{
|
|
1237
|
+
without:{value:fmtTok(inputTokens),label:'tokens without Echo',say:`The current scan analyzed <b>${fmtTok(inputTokens)} input tokens</b>, including context the agent had to reload again.`},
|
|
1238
|
+
with:{value:fmtTok(usefulTokens),label:'tokens with Echo',say:`Echo carries the useful checkpoint forward, so about <b>${fmtTok(savedTokens)}</b> repeat tokens do not need to be reloaded.`}
|
|
1239
|
+
},
|
|
1240
|
+
cost:{
|
|
1241
|
+
without:{value:money(apiCost),label:'API cost without Echo',say:`API-equivalent list-price scale for the context volume in this scan. This is a volume estimate, not your subscription bill.`},
|
|
1242
|
+
with:{value:money(withCost),label:'API cost with Echo',say:`Modeled after removing repeat context from the scan, avoiding about <b>${money(savedCost)}</b> of reloaded context volume.`}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
return data[metric][mode];
|
|
1246
|
+
}
|
|
1247
|
+
function updateFieldReportMetric(){
|
|
1248
|
+
if(!setupMode) return;
|
|
1249
|
+
const readout=cityMetricReadout();
|
|
1250
|
+
const metricValue=document.getElementById('fr-metric-value'); if(metricValue) metricValue.textContent=readout.value;
|
|
1251
|
+
const metricLabel=document.getElementById('fr-metric-label'); if(metricLabel) metricLabel.textContent=readout.label;
|
|
1252
|
+
const metricSay=document.getElementById('fr-metric-say'); if(metricSay) metricSay.innerHTML=readout.say;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const renderer = new THREE.WebGLRenderer({canvas:document.getElementById('repo-visualizer'), antialias:true, alpha:true});
|
|
1256
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1257
|
+
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
1258
|
+
renderer.setClearColor(0xffffff,0); // transparent → card white shows through
|
|
1259
|
+
renderer.shadowMap.enabled=true;
|
|
1260
|
+
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
|
|
1261
|
+
renderer.outputColorSpace=THREE.SRGBColorSpace;
|
|
1262
|
+
renderer.toneMapping=THREE.ACESFilmicToneMapping; // soft highlight roll-off — stops light surfaces clipping to flat white
|
|
1263
|
+
renderer.toneMappingExposure=1.05; // master brightness for the whole city
|
|
1264
|
+
|
|
1265
|
+
const scene = new THREE.Scene();
|
|
1266
|
+
|
|
1267
|
+
// soft studio environment (image-based lighting) — gives the frosted glass its
|
|
1268
|
+
// translucent sheen + the bright internal highlight; generated once, no external HDRI.
|
|
1269
|
+
function buildEnv(){
|
|
1270
|
+
const c=document.createElement('canvas'); c.width=512; c.height=256;
|
|
1271
|
+
const g=c.getContext('2d');
|
|
1272
|
+
const grd=g.createLinearGradient(0,0,0,256);
|
|
1273
|
+
grd.addColorStop(0,'#ffffff'); grd.addColorStop(0.5,'#eef1f6'); grd.addColorStop(1,'#d2d7e0');
|
|
1274
|
+
g.fillStyle=grd; g.fillRect(0,0,512,256);
|
|
1275
|
+
const spot=g.createRadialGradient(150,62,8,150,62,150);
|
|
1276
|
+
spot.addColorStop(0,'rgba(255,255,255,0.95)'); spot.addColorStop(1,'rgba(255,255,255,0)');
|
|
1277
|
+
g.fillStyle=spot; g.fillRect(0,0,512,256);
|
|
1278
|
+
const tex=new THREE.CanvasTexture(c); tex.mapping=THREE.EquirectangularReflectionMapping; tex.colorSpace=THREE.SRGBColorSpace;
|
|
1279
|
+
const pm=new THREE.PMREMGenerator(renderer); pm.compileEquirectangularShader();
|
|
1280
|
+
const env=pm.fromEquirectangular(tex).texture; tex.dispose(); pm.dispose();
|
|
1281
|
+
return env;
|
|
1282
|
+
}
|
|
1283
|
+
scene.environment = buildEnv();
|
|
1284
|
+
|
|
1285
|
+
// Frozen poster data is available only outside setup. Setup starts empty and can be
|
|
1286
|
+
// populated solely by the validated local report envelope above.
|
|
1287
|
+
const POSTER_REPOS=[
|
|
1288
|
+
{name:'WebPageReactVersion', x:-30, z:-60, tool:'claude', pin:true, tokens:3436946364, cost:8896, days:18.6, hours:92.8, sessions:74},
|
|
1289
|
+
{name:'EchoMem-Chrome', x:140, z:-14, tool:'claude', pin:true, tokens:2863590896, cost:6959, days:14.0, hours:70.1, sessions:38},
|
|
1290
|
+
{name:'echo-design-system', x:30, z:30, tool:'codex', tokens:382642012, cost:1106, days:4.2, hours:21.2, sessions:29},
|
|
1291
|
+
{name:'sleepy-merkle-3d0470',x:90, z:88, tool:'codex', tokens:307339018, cost:651, days:0.8, hours:3.8, sessions:1},
|
|
1292
|
+
{name:'DG-Web', x:-128,z:6, tool:'claude', tokens:253239297, cost:621, days:1.4, hours:6.8, sessions:4},
|
|
1293
|
+
{name:'Kobe Life', x:182, z:80, tool:'codex', tokens:122566296, cost:336, days:1.9, hours:9.6, sessions:12},
|
|
1294
|
+
{name:'public', x:-66, z:96, tool:'claude', tokens:102828178, cost:290, days:0.4, hours:1.9, sessions:1},
|
|
1295
|
+
{name:'EchoMemory_log', x:40, z:150, tool:'claude', tokens:43937440, cost:111, days:0.4, hours:1.8, sessions:8},
|
|
1296
|
+
{name:'production-prompt', x:-188,z:64, tool:'claude', tokens:11092185, cost:19, days:0, hours:0, sessions:1},
|
|
1297
|
+
{name:'brands', x:-140,z:120, tool:'codex', tokens:8010799, cost:18, days:0, hours:0.1, sessions:1},
|
|
1298
|
+
{name:'components', x:-6, z:190, tool:'claude', tokens:7978639, cost:13, days:0, hours:0, sessions:1},
|
|
1299
|
+
{name:'dist', x:-210,z:118, tool:'claude', tokens:4550877, cost:8, days:0, hours:0, sessions:2},
|
|
1300
|
+
{name:'Daimler Engineering', x:-168,z:160, tool:'codex', tokens:4439902, cost:15, days:0, hours:0.2, sessions:2},
|
|
1301
|
+
];
|
|
1302
|
+
let REPOS=setupMode ? [] : POSTER_REPOS;
|
|
1303
|
+
// Per-repo Codex/Claude split for the retired standalone poster fallback.
|
|
1304
|
+
const REPO_SPLIT={'WebPageReactVersion':[31,69],'EchoMem-Chrome':[19,81],'echo-design-system':[44,56],'sleepy-merkle-3d0470':[0,100],'DG-Web':[0,100],'Kobe Life':[100,0],'public':[0,100],'EchoMemory_log':[63,37],'production-prompt':[0,100],'brands':[0,100],'components':[0,100],'dist':[0,100],'Daimler Engineering':[100,0]};
|
|
1305
|
+
for(const o of REPOS){ const s=REPO_SPLIT[o.name]; if(s){ o.codexPct=s[0]; o.claudePct=s[1]; } else { o.codexPct=o.tool==='codex'?100:0; o.claudePct=100-o.codexPct; } }
|
|
1306
|
+
if(setupMode){
|
|
1307
|
+
REPOS=setupLiveRepos;
|
|
1308
|
+
window.__ECHOMEM_CITY_REPORT_REPO_COUNT=Array.isArray(setupReport?.repos) ? setupReport.repos.length : -1;
|
|
1309
|
+
window.__ECHOMEM_CITY_REPOS=REPOS.map(({name,tokens,cost,hours,sessions,tool})=>({name,tokens,cost,hours,sessions,tool}));
|
|
1310
|
+
}
|
|
1311
|
+
// VOLUME ∝ tokens: units = tokens/TOKENS_PER_UNIT; volume = units·BASE_EDGE³; edge = cbrt(volume) (kept sane),
|
|
1312
|
+
// reshaped into a gentle tower (height = TOWER·base) with the volume preserved.
|
|
1313
|
+
for(const o of REPOS){
|
|
1314
|
+
const units=o.tokens/TOKENS_PER_UNIT;
|
|
1315
|
+
const base=Math.cbrt(units*BASE_EDGE*BASE_EDGE*BASE_EDGE/TOWER);
|
|
1316
|
+
o.w=base; o.d=base; o.h=base*TOWER;
|
|
1317
|
+
}
|
|
1318
|
+
const all=REPOS.filter(o=>o.tokens>=MIN_TOKENS); // drop the negligible long tail
|
|
1319
|
+
|
|
1320
|
+
const GAP=16;
|
|
1321
|
+
function separate(items, iters=400){
|
|
1322
|
+
for(let k=0;k<iters;k++){ let moved=false;
|
|
1323
|
+
for(let i=0;i<items.length;i++) for(let j=i+1;j<items.length;j++){
|
|
1324
|
+
const a=items[i], b=items[j];
|
|
1325
|
+
const hx=(a.w+b.w)/2+GAP, hz=(a.d+b.d)/2+GAP;
|
|
1326
|
+
const dx=b.x-a.x, dz=b.z-a.z;
|
|
1327
|
+
const ox=hx-Math.abs(dx), oz=hz-Math.abs(dz);
|
|
1328
|
+
if(ox>0 && oz>0){ moved=true;
|
|
1329
|
+
if(ox<oz){ const s=(dx<0?-1:1);
|
|
1330
|
+
if(a.pin&&b.pin){} else if(a.pin){ b.x+=ox*s; } else if(b.pin){ a.x-=ox*s; } else { a.x-=ox*0.5*s; b.x+=ox*0.5*s; }
|
|
1331
|
+
}else{ const s=(dz<0?-1:1);
|
|
1332
|
+
if(a.pin&&b.pin){} else if(a.pin){ b.z+=oz*s; } else if(b.pin){ a.z-=oz*s; } else { a.z-=oz*0.5*s; b.z+=oz*0.5*s; }
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if(!moved) break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
separate(all);
|
|
1340
|
+
|
|
1341
|
+
let minX=1e9,maxX=-1e9,minZ=1e9,maxZ=-1e9;
|
|
1342
|
+
for(const o of all){ minX=Math.min(minX,o.x-o.w/2); maxX=Math.max(maxX,o.x+o.w/2);
|
|
1343
|
+
minZ=Math.min(minZ,o.z-o.d/2); maxZ=Math.max(maxZ,o.z+o.d/2); }
|
|
1344
|
+
const MARGIN=56;
|
|
1345
|
+
const CX=(minX+maxX)/2, CZ=(minZ+maxZ)/2;
|
|
1346
|
+
const traySize=Math.max(maxX-minX, maxZ-minZ)+2*MARGIN;
|
|
1347
|
+
const trayW=traySize, trayD=traySize; // SQUARE footprint — reads as a square, not a skewed rectangle
|
|
1348
|
+
|
|
1349
|
+
const TRAY_TOP=25; // tray slab thickness — declared early so the setup-mode auto-fit can size the city
|
|
1350
|
+
const Vh = 620; // share-card mode: zoom out for the v7 chrome
|
|
1351
|
+
const PAN_X = 0.10, PAN_Y = -0.18; // share-card mode pan: keep city clear of the diagnosis band
|
|
1352
|
+
const SETUP_PAN_Y = -0.08; // setup mode: lift the city slightly above optical center
|
|
1353
|
+
const camera = new THREE.OrthographicCamera(-1,1,1,-1, 1, 6000);
|
|
1354
|
+
const TARGET = new THREE.Vector3(CX, 78, CZ);
|
|
1355
|
+
const OFFSET = new THREE.Vector3(760, 660, 760); // symmetric 45° azimuth → clean isometric square (was skewed → looked like a trapezoid)
|
|
1356
|
+
camera.position.copy(TARGET).add(OFFSET);
|
|
1357
|
+
if(setupMode) camera.lookAt(TARGET); // setup mode only: orient before the auto-fit. Share-card path left untouched — OrbitControls sets this same angle at update().
|
|
1358
|
+
// Setup mode: frame the WHOLE city so no cube or label is ever clipped, at any viewport size or
|
|
1359
|
+
// repo count. Project the city's world bounding box into camera space and size the orthographic
|
|
1360
|
+
// window to contain it (plus padding), instead of a fixed zoom that only fits the frozen demo.
|
|
1361
|
+
const FIT_PAD = 1.16; // breathing room so cubes never touch the viewport edge
|
|
1362
|
+
function cityFit(){
|
|
1363
|
+
camera.updateMatrixWorld(true);
|
|
1364
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1365
|
+
const inv = camera.matrixWorldInverse;
|
|
1366
|
+
const halfT = traySize / 2;
|
|
1367
|
+
const maxH = all.length ? Math.max.apply(null, all.map(function(o){ return o.h; })) : 60;
|
|
1368
|
+
const topY = TRAY_TOP + maxH + 150; // headroom for the token/$ labels that fly above each cube
|
|
1369
|
+
const xs=[CX-halfT, CX+halfT], ys=[0, topY], zs=[CZ-halfT, CZ+halfT];
|
|
1370
|
+
let minXc=1e9,maxXc=-1e9,minYc=1e9,maxYc=-1e9; const v=new THREE.Vector3();
|
|
1371
|
+
for(const x of xs) for(const y of ys) for(const z of zs){
|
|
1372
|
+
v.set(x,y,z).applyMatrix4(inv);
|
|
1373
|
+
if(v.x<minXc)minXc=v.x; if(v.x>maxXc)maxXc=v.x;
|
|
1374
|
+
if(v.y<minYc)minYc=v.y; if(v.y>maxYc)maxYc=v.y;
|
|
1375
|
+
}
|
|
1376
|
+
return {cx:(minXc+maxXc)/2, cy:(minYc+maxYc)/2, halfW:(maxXc-minXc)/2, halfH:(maxYc-minYc)/2};
|
|
1377
|
+
}
|
|
1378
|
+
function frame(){
|
|
1379
|
+
const aspect = VIZ_W/VIZ_H;
|
|
1380
|
+
if(setupMode){
|
|
1381
|
+
const f = cityFit();
|
|
1382
|
+
const half = Math.max(f.halfH, f.halfW/aspect) * FIT_PAD;
|
|
1383
|
+
const Vw = half*aspect;
|
|
1384
|
+
const cy = f.cy + half*SETUP_PAN_Y;
|
|
1385
|
+
camera.left = f.cx - Vw; camera.right = f.cx + Vw;
|
|
1386
|
+
camera.top = cy + half; camera.bottom = cy - half;
|
|
1387
|
+
camera.updateProjectionMatrix();
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const Vw=Vh*aspect, px=Vw*PAN_X, py=Vh*PAN_Y;
|
|
1391
|
+
camera.left=-Vw-px; camera.right=Vw-px; camera.top=Vh+py; camera.bottom=-Vh+py;
|
|
1392
|
+
camera.updateProjectionMatrix();
|
|
1393
|
+
}
|
|
1394
|
+
frame();
|
|
1395
|
+
|
|
1396
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0xdadce2, 0.7));
|
|
1397
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.2));
|
|
1398
|
+
const key = new THREE.DirectionalLight(0xffffff, 2.3);
|
|
1399
|
+
key.position.set(CX-360, 560, CZ+360); key.target.position.copy(TARGET);
|
|
1400
|
+
key.castShadow=true; key.shadow.mapSize.set(4096,4096); key.shadow.radius=16; key.shadow.bias=-0.0004;
|
|
1401
|
+
const shalf=Math.max(trayW,trayD)*0.8+140; const sc=key.shadow.camera;
|
|
1402
|
+
sc.left=-shalf; sc.right=shalf; sc.top=shalf; sc.bottom=-shalf; sc.near=100; sc.far=1800;
|
|
1403
|
+
key.shadow.autoUpdate=false; key.shadow.needsUpdate=true;
|
|
1404
|
+
scene.add(key); scene.add(key.target);
|
|
1405
|
+
const fill = new THREE.DirectionalLight(0xffffff, 0.4); fill.position.set(CX+420, 300, CZ+260); scene.add(fill);
|
|
1406
|
+
|
|
1407
|
+
const floor = new THREE.Mesh(new THREE.PlaneGeometry(6000,6000), new THREE.ShadowMaterial({opacity:0.22}));
|
|
1408
|
+
floor.rotation.x=-Math.PI/2; floor.position.y=0; floor.receiveShadow=true; scene.add(floor);
|
|
1409
|
+
|
|
1410
|
+
// Comparison material: corrupted buildings are dark and chalky; repaired buildings regain colour
|
|
1411
|
+
// without returning to the glossy toy-plastic finish.
|
|
1412
|
+
function clayGlass(hex){
|
|
1413
|
+
const clean=new THREE.Color(hex).multiplyScalar(0.82);
|
|
1414
|
+
const damaged=clean.clone().multiplyScalar(0.58);
|
|
1415
|
+
const material=new THREE.MeshPhysicalMaterial({
|
|
1416
|
+
color:damaged,
|
|
1417
|
+
roughness:0.9, metalness:0.01,
|
|
1418
|
+
transmission:0.05, ior:1.25, thickness:45,
|
|
1419
|
+
attenuationColor:damaged.clone(), attenuationDistance:58,
|
|
1420
|
+
clearcoat:0.015, clearcoatRoughness:0.94,
|
|
1421
|
+
envMapIntensity:0.24, transparent:true,
|
|
1422
|
+
});
|
|
1423
|
+
buildingMaterials.push({material,damaged,clean});
|
|
1424
|
+
return material;
|
|
1425
|
+
}
|
|
1426
|
+
function selectiveRoundedBoxGeometry(width, height, depth, segments=8, radius=0.1, opts={}){
|
|
1427
|
+
const roundTop=opts.roundTop!==false, roundBottom=opts.roundBottom!==false;
|
|
1428
|
+
const s=segments*2+1;
|
|
1429
|
+
const r=Math.min(width/2, height/2, depth/2, radius);
|
|
1430
|
+
const base=new THREE.BoxGeometry(1,1,1,s,s,s);
|
|
1431
|
+
const geo=base.toNonIndexed(); base.dispose();
|
|
1432
|
+
const positions=geo.attributes.position.array;
|
|
1433
|
+
const normals=geo.attributes.normal.array;
|
|
1434
|
+
const pos=new THREE.Vector3();
|
|
1435
|
+
const n=new THREE.Vector3();
|
|
1436
|
+
const faceSpan=positions.length/6;
|
|
1437
|
+
const halfStep=0.5/s;
|
|
1438
|
+
const bx=width/2-r, by=height/2-r, bz=depth/2-r;
|
|
1439
|
+
for(let i=0;i<positions.length;i+=3){
|
|
1440
|
+
pos.fromArray(positions,i);
|
|
1441
|
+
const sx=Math.sign(pos.x)||1, sy=Math.sign(pos.y)||1, sz=Math.sign(pos.z)||1;
|
|
1442
|
+
const side=Math.floor(i/faceSpan);
|
|
1443
|
+
const roundY=sy>0 ? roundTop : roundBottom;
|
|
1444
|
+
n.copy(pos);
|
|
1445
|
+
n.x-=sx*halfStep;
|
|
1446
|
+
n.z-=sz*halfStep;
|
|
1447
|
+
if(roundY) n.y-=sy*halfStep; else n.y=0;
|
|
1448
|
+
if(n.lengthSq()>0) n.normalize();
|
|
1449
|
+
positions[i]=bx*sx+n.x*r;
|
|
1450
|
+
positions[i+1]=roundY ? by*sy+n.y*r : height/2*sy;
|
|
1451
|
+
positions[i+2]=bz*sz+n.z*r;
|
|
1452
|
+
if(!roundY && (side===2 || side===3)){
|
|
1453
|
+
normals[i]=0; normals[i+1]=sy; normals[i+2]=0;
|
|
1454
|
+
}else{
|
|
1455
|
+
normals[i]=n.x; normals[i+1]=n.y; normals[i+2]=n.z;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
geo.attributes.position.needsUpdate=true;
|
|
1459
|
+
geo.attributes.normal.needsUpdate=true;
|
|
1460
|
+
geo.computeBoundingBox();
|
|
1461
|
+
geo.computeBoundingSphere();
|
|
1462
|
+
return geo;
|
|
1463
|
+
}
|
|
1464
|
+
function addCube(o){
|
|
1465
|
+
const {x,z,w,h,d,tool}=o;
|
|
1466
|
+
const damage=corruptionProfile(o);
|
|
1467
|
+
const r=Math.min(Math.min(w,h,d)*0.12, 12);
|
|
1468
|
+
// proportional split: Claude (orange) on the bottom, Codex (green) on top; segment heights ∝ each tool's token share
|
|
1469
|
+
let cl=Number(o.claudePct), cx=Number(o.codexPct);
|
|
1470
|
+
if(!(cl>=0 && cx>=0 && cl+cx>0)){ cl=tool==='codex'?0:100; cx=100-cl; } // fallback to the dominant tool
|
|
1471
|
+
const total=cl+cx, segs=[];
|
|
1472
|
+
if(cl>0) segs.push([h*cl/total, TOOL.claude]);
|
|
1473
|
+
if(cx>0) segs.push([h*cx/total, TOOL.codex]);
|
|
1474
|
+
const slices=[];
|
|
1475
|
+
for(const [hSeg,color] of segs){
|
|
1476
|
+
const count=hSeg>78?4:hSeg>44?3:hSeg>24?2:1;
|
|
1477
|
+
const gap=count>1?Math.min(3.2,hSeg*0.055):0;
|
|
1478
|
+
const sliceH=(hSeg-gap*(count-1))/count;
|
|
1479
|
+
for(let i=0;i<count;i++) slices.push({h:sliceH,color,gap:i===count-1?0:gap});
|
|
1480
|
+
}
|
|
1481
|
+
const group=new THREE.Group();
|
|
1482
|
+
group.position.set(x,0,z);
|
|
1483
|
+
group.rotation.x=damage.leanX*damage.amount;
|
|
1484
|
+
group.rotation.z=damage.leanZ*damage.amount;
|
|
1485
|
+
repairGroups.push({group,damageX:group.rotation.x,damageZ:group.rotation.z});
|
|
1486
|
+
let yBase=TRAY_TOP - 3;
|
|
1487
|
+
let cleanY=TRAY_TOP - 3;
|
|
1488
|
+
const fractured=slices.length>1;
|
|
1489
|
+
for(let i=0;i<slices.length;i++){
|
|
1490
|
+
const {h:hSeg,color,gap}=slices[i];
|
|
1491
|
+
const mat=clayGlass(color);
|
|
1492
|
+
const geom=fractured
|
|
1493
|
+
? selectiveRoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45), {roundBottom:i===0, roundTop:i===slices.length-1})
|
|
1494
|
+
: new RoundedBoxGeometry(w,hSeg,d,8,Math.min(r,hSeg*0.45));
|
|
1495
|
+
const m=new THREE.Mesh(geom, mat);
|
|
1496
|
+
const stagger=i===0?0:(hash01(o.name,i+61)-0.5)*2;
|
|
1497
|
+
const splitShift=stagger*Math.min(w*0.24, 13)*damage.amount;
|
|
1498
|
+
m.position.set(splitShift, yBase + hSeg/2, -splitShift*(0.25+hash01(o.name,i+67)*0.45));
|
|
1499
|
+
if(i>0) m.rotation.y=(hash01(o.name,i+71)-0.5)*0.30*damage.amount;
|
|
1500
|
+
repairPieces.push({
|
|
1501
|
+
mesh:m,
|
|
1502
|
+
damagePos:m.position.clone(),
|
|
1503
|
+
cleanPos:new THREE.Vector3(0,cleanY+hSeg/2,0),
|
|
1504
|
+
damageRotY:m.rotation.y,
|
|
1505
|
+
});
|
|
1506
|
+
m.castShadow=true; m.receiveShadow=true; m.userData=o;
|
|
1507
|
+
group.add(m);
|
|
1508
|
+
yBase+=hSeg+gap;
|
|
1509
|
+
cleanY+=hSeg;
|
|
1510
|
+
}
|
|
1511
|
+
const scarMat=new THREE.MeshStandardMaterial({color:0x343238,roughness:0.92,metalness:0.08,transparent:true});
|
|
1512
|
+
const scarCount=Math.max(1,Math.round(2+damage.amount*3));
|
|
1513
|
+
for(let i=0;i<scarCount;i++){
|
|
1514
|
+
const sw=Math.max(3,w*(0.10+hash01(o.name,101+i)*0.20));
|
|
1515
|
+
const sh=Math.max(2,Math.min(6,h*0.045));
|
|
1516
|
+
const sd=Math.max(2,d*0.06);
|
|
1517
|
+
const scar=new THREE.Mesh(new THREE.BoxGeometry(sw,sh,sd),scarMat);
|
|
1518
|
+
scar.position.set((hash01(o.name,131+i)-0.5)*w*0.72,TRAY_TOP+h*(0.18+hash01(o.name,151+i)*0.67),d*0.5+1.2);
|
|
1519
|
+
scar.rotation.z=(hash01(o.name,171+i)-0.5)*0.55;
|
|
1520
|
+
scar.castShadow=true; scar.userData=o; group.add(scar);
|
|
1521
|
+
corruptionFades.push({mesh:scar,material:scar.material,damagePos:scar.position.clone(),cleanPos:scar.position.clone(),baseScale:scar.scale.clone()});
|
|
1522
|
+
}
|
|
1523
|
+
const chunkCount=Math.max(4,Math.round(4+damage.amount*5));
|
|
1524
|
+
for(let i=0;i<chunkCount;i++){
|
|
1525
|
+
const size=Math.max(3,Math.min(w,d)*(0.07+hash01(o.name,201+i)*0.10));
|
|
1526
|
+
const chunk=new THREE.Mesh(
|
|
1527
|
+
i%3===0?new THREE.TetrahedronGeometry(size,0):new THREE.BoxGeometry(size,size*(0.5+hash01(o.name,221+i)),size*0.72),
|
|
1528
|
+
i%2?scarMat:clayGlass(tool==='codex'?TOOL.codex:TOOL.claude)
|
|
1529
|
+
);
|
|
1530
|
+
const angle=hash01(o.name,241+i)*Math.PI*2;
|
|
1531
|
+
const radius=Math.max(w,d)*(0.62+hash01(o.name,261+i)*1.05);
|
|
1532
|
+
const baseY=TRAY_TOP+2+hash01(o.name,281+i)*Math.min(h*0.72,58);
|
|
1533
|
+
chunk.position.set(Math.cos(angle)*radius,baseY,Math.sin(angle)*radius);
|
|
1534
|
+
chunk.rotation.set(hash01(o.name,301+i)*2.4,hash01(o.name,321+i)*2.4,hash01(o.name,341+i)*2.4);
|
|
1535
|
+
chunk.castShadow=true; chunk.receiveShadow=true; chunk.userData=o; group.add(chunk);
|
|
1536
|
+
chunk.material.transparent=true;
|
|
1537
|
+
corruptionFades.push({
|
|
1538
|
+
mesh:chunk,
|
|
1539
|
+
material:chunk.material,
|
|
1540
|
+
damagePos:chunk.position.clone(),
|
|
1541
|
+
cleanPos:new THREE.Vector3(0,Math.max(TRAY_TOP+4,Math.min(TRAY_TOP+h-5,baseY)),0),
|
|
1542
|
+
baseScale:chunk.scale.clone(),
|
|
1543
|
+
});
|
|
1544
|
+
if(i===0 && h>42) corruptionFloaters.push({mesh:chunk,baseY,phase:damage.phase,speed:0.65+hash01(o.name,361)*0.55});
|
|
1545
|
+
}
|
|
1546
|
+
group.userData=o; scene.add(group); return group;
|
|
1547
|
+
}
|
|
1548
|
+
// repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
|
|
1549
|
+
// setup mode uses the selected city metric; share-card mode keeps the original tokens + $ poster readout.
|
|
1550
|
+
let cityMetric=setupMode ? 'time' : 'tokens';
|
|
1551
|
+
let timelineProgress=1;
|
|
1552
|
+
const repoLabels=[];
|
|
1553
|
+
function fmtMetric(hours, tokens, cost){
|
|
1554
|
+
if(cityMetric==='tokens') return fmtTok(tokens);
|
|
1555
|
+
if(cityMetric==='cost') return '$'+Math.round(cost).toLocaleString('en-US');
|
|
1556
|
+
return `${(Number(hours)||0).toFixed(1)}h`;
|
|
1557
|
+
}
|
|
1558
|
+
function repoMetricText(o){
|
|
1559
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1560
|
+
return fmtMetric((Number(o.hours)||0)*fraction,(Number(o.tokens)||0)*fraction,(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE));
|
|
1561
|
+
}
|
|
1562
|
+
// GitHub octocat mark (24x24) — same glyph as the report's repo chips.
|
|
1563
|
+
const GH_MARK=new Path2D('M12 .7a11.5 11.5 0 0 0-3.64 22.41c.58.11.79-.25.79-.56v-2.23c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.17.08 1.78 1.2 1.78 1.2 1.04 1.78 2.72 1.27 3.38.97.1-.75.41-1.27.74-1.56-2.57-.29-5.27-1.29-5.27-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.47.11-3.05 0 0 .97-.31 3.16 1.18A10.97 10.97 0 0 1 12 6.12c.98 0 1.96.13 2.88.39 2.2-1.49 3.16-1.18 3.16-1.18.63 1.58.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.4-2.71 5.38-5.29 5.67.42.36.79 1.06.79 2.14v3.27c0 .31.21.68.8.56A11.5 11.5 0 0 0 12 .7Z');
|
|
1564
|
+
function drawRepoLabel(e){
|
|
1565
|
+
const {o, g, tex, PW, PH, numbers}=e;
|
|
1566
|
+
g.clearRect(0,0,PW,PH);
|
|
1567
|
+
g.textAlign='center';
|
|
1568
|
+
if(numbers){
|
|
1569
|
+
// dark ink + white halo: stays readable over the light grid AND the coloured roofs
|
|
1570
|
+
g.save();
|
|
1571
|
+
g.shadowColor='rgba(255,255,255,0.92)'; g.shadowBlur=12; g.shadowOffsetY=0;
|
|
1572
|
+
g.fillStyle='#171717'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
|
|
1573
|
+
g.fillText(setupMode ? repoMetricText(o) : fmtTok(o.tokens), PW/2, setupMode ? PH*0.66 : PH*0.18);
|
|
1574
|
+
if(!setupMode){
|
|
1575
|
+
g.fillStyle='rgba(23,23,23,0.85)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
|
|
1576
|
+
g.fillText('$'+Math.round(o.cost*COST_SCALE).toLocaleString('en-US'), PW/2, PH*0.40);
|
|
1577
|
+
}
|
|
1578
|
+
g.restore();
|
|
1579
|
+
}
|
|
1580
|
+
// repo name rides a white chip with the octocat, matching .audit-repo-chip in the report
|
|
1581
|
+
const fs=Math.round(PW*0.075);
|
|
1582
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1583
|
+
const icon=Math.round(fs*1.25), gap=Math.round(fs*0.45), padX=Math.round(fs*0.7);
|
|
1584
|
+
const tw=g.measureText(o.name).width;
|
|
1585
|
+
const maxText=PW*0.94-icon-gap-padX*2;
|
|
1586
|
+
const drawTw=Math.min(tw,maxText);
|
|
1587
|
+
const chipW=drawTw+icon+gap+padX*2;
|
|
1588
|
+
const chipH=Math.round(icon+fs*0.85);
|
|
1589
|
+
const cx=PW/2, cyMid=PH*0.86-fs*0.35;
|
|
1590
|
+
const rx=cx-chipW/2, ry=cyMid-chipH/2;
|
|
1591
|
+
g.save();
|
|
1592
|
+
g.shadowColor='rgba(31,35,40,0.25)'; g.shadowBlur=8; g.shadowOffsetY=2;
|
|
1593
|
+
g.beginPath();
|
|
1594
|
+
if(g.roundRect) g.roundRect(rx,ry,chipW,chipH,chipH/2); else g.rect(rx,ry,chipW,chipH);
|
|
1595
|
+
g.fillStyle='rgba(255,255,255,0.96)'; g.fill();
|
|
1596
|
+
g.shadowColor='transparent';
|
|
1597
|
+
g.lineWidth=1.5; g.strokeStyle='rgba(24,23,23,0.32)'; g.stroke();
|
|
1598
|
+
g.translate(rx+padX, cyMid-icon/2); g.scale(icon/24, icon/24);
|
|
1599
|
+
g.fillStyle='#181717'; g.fill(GH_MARK);
|
|
1600
|
+
g.restore();
|
|
1601
|
+
g.save();
|
|
1602
|
+
g.fillStyle='#171717'; g.textAlign='left'; g.textBaseline='middle';
|
|
1603
|
+
g.font=`700 ${fs}px "IBM Plex Mono", monospace`;
|
|
1604
|
+
g.translate(rx+padX+icon+gap, cyMid+fs*0.06);
|
|
1605
|
+
if(tw>maxText) g.scale(maxText/tw,1);
|
|
1606
|
+
g.fillText(o.name,0,0);
|
|
1607
|
+
g.restore();
|
|
1608
|
+
tex.needsUpdate=true;
|
|
1609
|
+
}
|
|
1610
|
+
function addTopLabel(o, numbers=true, lift=0){
|
|
1611
|
+
const PW=320, PH=280;
|
|
1612
|
+
const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
|
|
1613
|
+
const g=cv.getContext('2d');
|
|
1614
|
+
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1615
|
+
const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
|
|
1616
|
+
const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
|
|
1617
|
+
const labelOffset=-3+6+0.36*hWorld+lift;
|
|
1618
|
+
sprite.scale.set(wWorld, hWorld, 1);
|
|
1619
|
+
sprite.position.set(o.x, TRAY_TOP + o.h + labelOffset, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
|
|
1620
|
+
scene.add(sprite);
|
|
1621
|
+
const entry={o, g, tex, PW, PH, sprite, numbers, labelOffset};
|
|
1622
|
+
repoLabels.push(entry);
|
|
1623
|
+
drawRepoLabel(entry);
|
|
1624
|
+
}
|
|
1625
|
+
// fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
|
|
1626
|
+
const grain=(function(){
|
|
1627
|
+
const S=256; const cv=document.createElement('canvas'); cv.width=cv.height=S;
|
|
1628
|
+
const g=cv.getContext('2d'); const img=g.createImageData(S,S); const d=img.data;
|
|
1629
|
+
for(let i=0;i<d.length;i+=4){ const n=128+(Math.random()*2-1)*38; d[i]=d[i+1]=d[i+2]=n; d[i+3]=255; }
|
|
1630
|
+
g.putImageData(img,0,0);
|
|
1631
|
+
const t=new THREE.CanvasTexture(cv); t.wrapS=t.wrapT=THREE.RepeatWrapping; t.anisotropy=8;
|
|
1632
|
+
t.generateMipmaps=false; t.minFilter=THREE.LinearFilter; // don't let the grain mip-blur away to flat
|
|
1633
|
+
t.repeat.set(trayW/70, trayD/70);
|
|
1634
|
+
return t;
|
|
1635
|
+
})();
|
|
1636
|
+
const trayGeo=new RoundedBoxGeometry(trayW, TRAY_TOP, trayD, 8, 12);
|
|
1637
|
+
const tray=new THREE.Mesh(trayGeo, new THREE.MeshPhysicalMaterial({
|
|
1638
|
+
color:TRAY_COL, roughness:0.84, metalness:0.02,
|
|
1639
|
+
clearcoat:0.08, clearcoatRoughness:0.82, envMapIntensity:0.4,
|
|
1640
|
+
bumpMap:grain, bumpScale:0.9,
|
|
1641
|
+
}));
|
|
1642
|
+
tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=true; scene.add(tray);
|
|
1643
|
+
// grid + perimeter border on ONE decal: the grid is CLIPPED to the border's rounded rect, so
|
|
1644
|
+
// every line runs right up to the border and stops — no gap, and nothing leaks past it onto the rim.
|
|
1645
|
+
(function(){
|
|
1646
|
+
const scale=3;
|
|
1647
|
+
const W=Math.round(trayW*scale), H=Math.round(trayD*scale);
|
|
1648
|
+
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
1649
|
+
const g=cv.getContext('2d');
|
|
1650
|
+
const m=14*scale, rad=26*scale, rw=W-2*m, rh=H-2*m; // border rounded-rect (inset 14, radius 26)
|
|
1651
|
+
// grid, clipped to the border shape → fills exactly inside, ending at the border line
|
|
1652
|
+
g.save();
|
|
1653
|
+
g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.clip();
|
|
1654
|
+
const cell=21*scale; // half the previous cell size
|
|
1655
|
+
g.strokeStyle='rgba(74,80,96,0.45)'; g.lineWidth=1.8;
|
|
1656
|
+
g.beginPath();
|
|
1657
|
+
for(let x=m; x<=m+rw+0.5; x+=cell){ g.moveTo(x,m); g.lineTo(x,m+rh); }
|
|
1658
|
+
for(let y=m; y<=m+rh+0.5; y+=cell){ g.moveTo(m,y); g.lineTo(m+rw,y); }
|
|
1659
|
+
g.stroke();
|
|
1660
|
+
g.restore();
|
|
1661
|
+
// the border line itself, sitting on the boundary the grid ends at
|
|
1662
|
+
g.strokeStyle='rgba(120,126,140,0.24)'; g.lineWidth=0.7*scale; g.lineJoin='round';
|
|
1663
|
+
g.beginPath(); g.roundRect(m, m, rw, rh, rad); g.stroke();
|
|
1664
|
+
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1665
|
+
const plane=new THREE.Mesh(new THREE.PlaneGeometry(trayW, trayD), new THREE.MeshBasicMaterial({map:tex, transparent:true, depthWrite:false, toneMapped:false}));
|
|
1666
|
+
plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
|
|
1667
|
+
|
|
1668
|
+
// Keep corruption on its own decal so the same city can visibly heal without losing its clean grid.
|
|
1669
|
+
const damageCv=document.createElement('canvas'); damageCv.width=W; damageCv.height=H;
|
|
1670
|
+
const dg=damageCv.getContext('2d');
|
|
1671
|
+
dg.save(); dg.beginPath(); dg.roundRect(m,m,rw,rh,rad); dg.clip();
|
|
1672
|
+
let seed=0xdecafbad;
|
|
1673
|
+
const rnd=()=>{ seed=(Math.imul(seed,1664525)+1013904223)>>>0; return seed/4294967296; };
|
|
1674
|
+
for(let i=0;i<16;i++){
|
|
1675
|
+
const x=m+rnd()*rw, y=m+rnd()*rh, ww=(18+rnd()*54)*scale, hh=(8+rnd()*24)*scale;
|
|
1676
|
+
dg.save(); dg.translate(x,y); dg.rotate((rnd()-0.5)*0.7);
|
|
1677
|
+
dg.fillStyle=i%3===0?'rgba(170,59,51,0.18)':'rgba(43,43,49,0.16)';
|
|
1678
|
+
dg.fillRect(-ww/2,-hh/2,ww,hh); dg.restore();
|
|
1679
|
+
}
|
|
1680
|
+
dg.lineCap='square';
|
|
1681
|
+
for(let i=0;i<22;i++){
|
|
1682
|
+
let x=m+rnd()*rw, y=m+rnd()*rh;
|
|
1683
|
+
dg.strokeStyle=i%5===0?'rgba(176,58,51,0.62)':'rgba(45,44,49,0.55)';
|
|
1684
|
+
dg.lineWidth=(0.9+rnd()*1.5)*scale; dg.beginPath(); dg.moveTo(x,y);
|
|
1685
|
+
const steps=2+Math.floor(rnd()*4);
|
|
1686
|
+
for(let j=0;j<steps;j++){
|
|
1687
|
+
x+=((rnd()-0.5)*42)*scale; y+=((rnd()-0.5)*42)*scale; dg.lineTo(x,y);
|
|
1688
|
+
if(j===1 && rnd()>0.45){ dg.moveTo(x,y); dg.lineTo(x+(rnd()-0.5)*24*scale,y+(rnd()-0.5)*24*scale); dg.moveTo(x,y); }
|
|
1689
|
+
}
|
|
1690
|
+
dg.stroke();
|
|
1691
|
+
}
|
|
1692
|
+
dg.restore();
|
|
1693
|
+
const damageTex=new THREE.CanvasTexture(damageCv); damageTex.colorSpace=THREE.SRGBColorSpace; damageTex.anisotropy=8;
|
|
1694
|
+
const damageMat=new THREE.MeshBasicMaterial({map:damageTex,transparent:true,depthWrite:false,toneMapped:false});
|
|
1695
|
+
const damagePlane=new THREE.Mesh(new THREE.PlaneGeometry(trayW,trayD),damageMat);
|
|
1696
|
+
damagePlane.rotation.x=-Math.PI/2; damagePlane.position.set(CX,TRAY_TOP+0.72,CZ); scene.add(damagePlane);
|
|
1697
|
+
corruptionSurfaces.push(damageMat);
|
|
1698
|
+
})();
|
|
1699
|
+
|
|
1700
|
+
// De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
|
|
1701
|
+
// its name tag (hover still shows the full repo detail card). Kills the mid-air label collisions.
|
|
1702
|
+
const NUMBERED_LABELS=4;
|
|
1703
|
+
const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0,NUMBERED_LABELS);
|
|
1704
|
+
const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
|
|
1705
|
+
all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o, numberedLift.has(o.name), numberedLift.get(o.name)||0); });
|
|
1706
|
+
|
|
1707
|
+
// bottom-right standing "signpost": tray summary follows the selected city metric.
|
|
1708
|
+
let signpostCanvas=null, signpostTexture=null;
|
|
1709
|
+
function drawSignpost(bigText, subLabel, bottomText, accent='#e8473f'){
|
|
1710
|
+
if(!signpostCanvas) return;
|
|
1711
|
+
const W=signpostCanvas.width, H=signpostCanvas.height;
|
|
1712
|
+
const g=signpostCanvas.getContext('2d');
|
|
1713
|
+
g.clearRect(0,0,W,H);
|
|
1714
|
+
g.fillStyle='rgba(244,241,233,0.97)'; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.fill();
|
|
1715
|
+
g.strokeStyle='rgba(120,126,140,0.30)'; g.lineWidth=3; g.beginPath(); g.roundRect(8,8,W-16,H-16,30); g.stroke();
|
|
1716
|
+
g.textAlign='center';
|
|
1717
|
+
g.fillStyle='#1a1a1a'; g.font='800 124px "IBM Plex Mono", monospace'; g.fillText(bigText, W/2, 150);
|
|
1718
|
+
g.fillStyle='#6b6b6b'; g.font='600 36px "IBM Plex Mono", monospace'; g.fillText(subLabel, W/2, 204);
|
|
1719
|
+
g.fillStyle=accent; g.font='800 62px "IBM Plex Mono", monospace'; g.fillText(bottomText, W/2, 280);
|
|
1720
|
+
if(signpostTexture) signpostTexture.needsUpdate=true;
|
|
1721
|
+
}
|
|
1722
|
+
function signpostBig(){
|
|
1723
|
+
if(setupMode && cityComparison==='with') return cityMetricReadout().value;
|
|
1724
|
+
if(timelineProgress>=0.999) return cityMetric==='tokens'?signpostTokens : cityMetric==='cost'?signpostCost : signpostHours;
|
|
1725
|
+
const totals=all.reduce((sum,o)=>{
|
|
1726
|
+
const fraction=Number.isFinite(o.timelineFraction)?o.timelineFraction:1;
|
|
1727
|
+
sum.tokens+=(Number(o.tokens)||0)*fraction;
|
|
1728
|
+
sum.cost+=(Number(o.cost)||0)*fraction*(setupMode?1:COST_SCALE);
|
|
1729
|
+
sum.hours+=(Number(o.hours)||0)*fraction;
|
|
1730
|
+
return sum;
|
|
1731
|
+
},{tokens:0,cost:0,hours:0});
|
|
1732
|
+
return cityMetric==='tokens'?fmtTok(totals.tokens):cityMetric==='cost'?money(totals.cost):`${totals.hours.toFixed(1)}h`;
|
|
1733
|
+
}
|
|
1734
|
+
function signpostSub(){
|
|
1735
|
+
if(setupMode && cityComparison==='with') return cityMetric==='tokens'?'MODELED TOKENS' : cityMetric==='cost'?'MODELED API COST' : 'MODELED TIME';
|
|
1736
|
+
return cityMetric==='tokens'?'TOTAL TOKENS' : cityMetric==='cost'?'TOTAL SPENT' : 'TOTAL TIME';
|
|
1737
|
+
}
|
|
1738
|
+
function signpostBottom(){
|
|
1739
|
+
if(cityComparison==='with') return `${comparisonAfter} CONTEXT SCORE`;
|
|
1740
|
+
if(timelineProgress<0.999) return 'HISTORY AT DATE';
|
|
1741
|
+
return cityMetric==='tokens'?signpostWastedPct : cityMetric==='cost'?signpostWastedCost : signpostWastedTime;
|
|
1742
|
+
}
|
|
1743
|
+
function redrawCityMetric(){
|
|
1744
|
+
repoLabels.forEach(drawRepoLabel);
|
|
1745
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom(),cityComparison==='with'?'#2a8f7d':'#e8473f');
|
|
1746
|
+
dirty=true;
|
|
1747
|
+
}
|
|
1748
|
+
(function(){
|
|
1749
|
+
const W=640, H=320;
|
|
1750
|
+
const cv=document.createElement('canvas'); cv.width=W; cv.height=H;
|
|
1751
|
+
signpostCanvas=cv;
|
|
1752
|
+
const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
|
|
1753
|
+
signpostTexture=tex;
|
|
1754
|
+
drawSignpost(signpostBig(), signpostSub(), signpostBottom());
|
|
1755
|
+
const SIGN_W=152, SIGN_H=76, SIGN_Y=TRAY_TOP+64;
|
|
1756
|
+
const sign=new THREE.Mesh(
|
|
1757
|
+
new THREE.PlaneGeometry(SIGN_W, SIGN_H),
|
|
1758
|
+
new THREE.MeshBasicMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false, side:THREE.DoubleSide})
|
|
1759
|
+
);
|
|
1760
|
+
const sx=CX + trayW*0.48, sz=CZ + trayD*0.48;
|
|
1761
|
+
sign.position.set(sx, SIGN_Y, sz);
|
|
1762
|
+
sign.rotation.y=Math.PI/4; // fixed in the city, facing the default isometric camera instead of billboarding
|
|
1763
|
+
scene.add(sign);
|
|
1764
|
+
const POST_OVERLAP=4; // tiny tuck into the board so the pole feels attached, not floating
|
|
1765
|
+
const postH=Math.max(4, SIGN_Y - SIGN_H/2 - TRAY_TOP + POST_OVERLAP);
|
|
1766
|
+
const post=new THREE.Mesh(new THREE.CylinderGeometry(2.2,2.2,postH,12),
|
|
1767
|
+
new THREE.MeshStandardMaterial({color:0xddd9cc, roughness:0.7, metalness:0}));
|
|
1768
|
+
post.position.set(sx, TRAY_TOP+postH/2, sz); post.castShadow=true; scene.add(post);
|
|
1769
|
+
})();
|
|
1770
|
+
|
|
1771
|
+
(function(){
|
|
1772
|
+
const ct=document.getElementById('city-toggle');
|
|
1773
|
+
if(!ct) return;
|
|
1774
|
+
const thumb=ct.querySelector('.ct-thumb');
|
|
1775
|
+
const moveThumb=(btn)=>{ if(!thumb||!btn) return;
|
|
1776
|
+
thumb.style.width=btn.offsetWidth+'px'; thumb.style.height=btn.offsetHeight+'px';
|
|
1777
|
+
thumb.style.transform=`translate(${btn.offsetLeft}px, ${btn.offsetTop}px)`;
|
|
1778
|
+
};
|
|
1779
|
+
requestAnimationFrame(()=>{
|
|
1780
|
+
const on=ct.querySelector('button.on')||ct.querySelector('button[data-metric]');
|
|
1781
|
+
if(thumb) thumb.style.transition='none';
|
|
1782
|
+
moveThumb(on);
|
|
1783
|
+
requestAnimationFrame(()=>{ if(thumb) thumb.style.transition=''; });
|
|
1784
|
+
});
|
|
1785
|
+
ct.addEventListener('click', (e)=>{
|
|
1786
|
+
const b=e.target.closest('button[data-metric]'); if(!b) return;
|
|
1787
|
+
cityMetric=b.getAttribute('data-metric') || 'time';
|
|
1788
|
+
ct.querySelectorAll('button').forEach((x)=>x.classList.toggle('on', x===b));
|
|
1789
|
+
moveThumb(b);
|
|
1790
|
+
redrawCityMetric();
|
|
1791
|
+
updateFieldReportMetric();
|
|
1792
|
+
});
|
|
1793
|
+
})();
|
|
1794
|
+
|
|
1795
|
+
function setCityComparison(mode){
|
|
1796
|
+
cityComparison=mode==='with'?'with':'without';
|
|
1797
|
+
repairTarget=cityComparison==='with'?1:0;
|
|
1798
|
+
document.body.classList.toggle('with-echo',cityComparison==='with');
|
|
1799
|
+
updateFieldReportMetric();
|
|
1800
|
+
redrawCityMetric();
|
|
1801
|
+
dirty=true;
|
|
1802
|
+
}
|
|
1803
|
+
setCityComparison('without');
|
|
1804
|
+
// Parent page (setup report fullscreen modal) drives the dirty/clean comparison remotely.
|
|
1805
|
+
window.addEventListener('message',(e)=>{if(e.origin!==location.origin)return;const d=e.data||{};if(d.type==='echomem:city-comparison')setCityComparison(d.mode==='with'?'with':'without');if(d.type==='echomem:city-metric'){const m=({time:1,tokens:1,cost:1})[d.metric]?d.metric:'time';const b=document.querySelector('#city-toggle button[data-metric="'+m+'"]');if(b)b.click();}});
|
|
1806
|
+
|
|
1807
|
+
const timelineStartMs=Date.parse(setupReport?.scanStartDate||'2024-12-01T00:00:00Z');
|
|
1808
|
+
const timelineEndMs=Math.max(timelineStartMs+86400000,Date.parse(setupReport?.scanEndDate||new Date().toISOString()));
|
|
1809
|
+
function repoFractionAt(o,cutoffMs){
|
|
1810
|
+
if(timelineProgress>=0.999) return 1;
|
|
1811
|
+
if(!Array.isArray(o.daily)||!o.daily.length) return timelineProgress;
|
|
1812
|
+
let total=0,visible=0;
|
|
1813
|
+
for(const point of o.daily){
|
|
1814
|
+
if(!Array.isArray(point)||point.length<2) continue;
|
|
1815
|
+
const amount=Math.max(0,Number(point[1])||0);
|
|
1816
|
+
const day=Date.parse(String(point[0])+'T23:59:59Z');
|
|
1817
|
+
total+=amount;
|
|
1818
|
+
if(Number.isFinite(day)&&day<=cutoffMs) visible+=amount;
|
|
1819
|
+
}
|
|
1820
|
+
return total>0?Math.max(0,Math.min(1,visible/total)):timelineProgress;
|
|
1821
|
+
}
|
|
1822
|
+
function setTimelineProgress(value){
|
|
1823
|
+
timelineProgress=Math.max(0,Math.min(1,Number(value)||0));
|
|
1824
|
+
const cutoffMs=timelineStartMs+(timelineEndMs-timelineStartMs)*timelineProgress;
|
|
1825
|
+
const handle=document.querySelector('#city-timeline .tl-handle');
|
|
1826
|
+
const progress=document.querySelector('#city-timeline .tl-progress');
|
|
1827
|
+
const selected=document.querySelector('#city-timeline .tl-selected');
|
|
1828
|
+
const pct=timelineProgress*100;
|
|
1829
|
+
if(handle){ handle.style.left=`${pct}%`; handle.setAttribute('aria-valuenow',String(Math.round(pct))); }
|
|
1830
|
+
if(progress) progress.style.width=`${pct}%`;
|
|
1831
|
+
if(selected){
|
|
1832
|
+
selected.style.left=`${Math.max(7,Math.min(93,pct))}%`;
|
|
1833
|
+
selected.textContent=timelineProgress>=0.995?'Today':new Date(cutoffMs).toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',timeZone:'UTC'});
|
|
1834
|
+
}
|
|
1835
|
+
for(const o of all){
|
|
1836
|
+
const fraction=repoFractionAt(o,cutoffMs);
|
|
1837
|
+
o.timelineFraction=fraction;
|
|
1838
|
+
if(o.mesh){
|
|
1839
|
+
o.mesh.visible=fraction>0.002;
|
|
1840
|
+
o.mesh.scale.y=Math.max(0.001,fraction);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
for(const entry of repoLabels){
|
|
1844
|
+
const fraction=Number.isFinite(entry.o.timelineFraction)?entry.o.timelineFraction:1;
|
|
1845
|
+
entry.sprite.visible=fraction>0.08;
|
|
1846
|
+
entry.sprite.material.opacity=Math.max(0,Math.min(1,(fraction-0.08)/0.18));
|
|
1847
|
+
entry.sprite.position.y=TRAY_TOP+entry.o.h*fraction+entry.labelOffset;
|
|
1848
|
+
}
|
|
1849
|
+
redrawCityMetric();
|
|
1850
|
+
key.shadow.needsUpdate=true;
|
|
1851
|
+
dirty=true;
|
|
1852
|
+
}
|
|
1853
|
+
(function(){
|
|
1854
|
+
const timeline=document.getElementById('city-timeline');
|
|
1855
|
+
const track=timeline?.querySelector('.tl-track');
|
|
1856
|
+
const handle=timeline?.querySelector('.tl-handle');
|
|
1857
|
+
if(!timeline||!track||!handle) return;
|
|
1858
|
+
let dragging=false;
|
|
1859
|
+
const updateFromPointer=(event)=>{
|
|
1860
|
+
const rect=track.getBoundingClientRect();
|
|
1861
|
+
setTimelineProgress((event.clientX-rect.left)/Math.max(1,rect.width));
|
|
1862
|
+
};
|
|
1863
|
+
track.addEventListener('pointerdown',(event)=>{
|
|
1864
|
+
dragging=true; timeline.classList.add('dragging'); updateFromPointer(event); event.preventDefault();
|
|
1865
|
+
});
|
|
1866
|
+
window.addEventListener('pointermove',(event)=>{ if(dragging){ updateFromPointer(event); event.preventDefault(); } });
|
|
1867
|
+
window.addEventListener('pointerup',()=>{ dragging=false; timeline.classList.remove('dragging'); });
|
|
1868
|
+
handle.addEventListener('keydown',(event)=>{
|
|
1869
|
+
if(event.key==='ArrowLeft'||event.key==='ArrowDown') setTimelineProgress(timelineProgress-0.025);
|
|
1870
|
+
else if(event.key==='ArrowRight'||event.key==='ArrowUp') setTimelineProgress(timelineProgress+0.025);
|
|
1871
|
+
else if(event.key==='Home') setTimelineProgress(0);
|
|
1872
|
+
else if(event.key==='End') setTimelineProgress(1);
|
|
1873
|
+
else return;
|
|
1874
|
+
event.preventDefault();
|
|
1875
|
+
});
|
|
1876
|
+
setTimelineProgress(1);
|
|
1877
|
+
})();
|
|
1878
|
+
|
|
1879
|
+
const MINZ=0.4, MAXZ=2.8; // lower floor → can zoom out to ~2/3 of the previous limit
|
|
1880
|
+
const controls=new OrbitControls(camera, renderer.domElement);
|
|
1881
|
+
controls.target.copy(TARGET);
|
|
1882
|
+
controls.enableDamping=true; controls.dampingFactor=0.08; controls.enablePan=false; controls.enableZoom=false;
|
|
1883
|
+
controls.minPolarAngle=0.18; controls.maxPolarAngle=Math.PI*0.47; controls.rotateSpeed=0.85;
|
|
1884
|
+
controls.update();
|
|
1885
|
+
let targetZoom=camera.zoom;
|
|
1886
|
+
// Hover auto-focus: wheel + drag behave differently over the city, so cue it —
|
|
1887
|
+
// the camera eases into a slight zoom when the pointer enters, back out on leave.
|
|
1888
|
+
let hoverFocus=1, hoverFocusTarget=1;
|
|
1889
|
+
renderer.domElement.addEventListener('pointerenter',()=>{ hoverFocusTarget=1.05; });
|
|
1890
|
+
renderer.domElement.addEventListener('pointerleave',()=>{ hoverFocusTarget=1; });
|
|
1891
|
+
renderer.domElement.addEventListener('wheel', (e)=>{
|
|
1892
|
+
e.preventDefault();
|
|
1893
|
+
const dy=e.deltaY * (e.deltaMode===1 ? 16 : 1);
|
|
1894
|
+
targetZoom=Math.min(MAXZ, Math.max(MINZ, targetZoom * Math.pow(0.999, dy)));
|
|
1895
|
+
}, {passive:false});
|
|
1896
|
+
|
|
1897
|
+
function render(){ renderer.render(scene,camera); }
|
|
1898
|
+
function loop(){ requestAnimationFrame(loop); let need=controls.update();
|
|
1899
|
+
let shadowNeedsUpdate=false;
|
|
1900
|
+
if(Math.abs(hoverFocusTarget-hoverFocus)>0.0004){ hoverFocus+=(hoverFocusTarget-hoverFocus)*0.1; if(Math.abs(hoverFocusTarget-hoverFocus)<0.0005) hoverFocus=hoverFocusTarget; }
|
|
1901
|
+
const zoomGoal=Math.min(MAXZ, Math.max(MINZ, targetZoom*hoverFocus));
|
|
1902
|
+
if(Math.abs(zoomGoal-camera.zoom) > 0.0006){ camera.zoom += (zoomGoal-camera.zoom)*0.16; if(Math.abs(zoomGoal-camera.zoom)<0.001) camera.zoom=zoomGoal; camera.updateProjectionMatrix(); need=true; }
|
|
1903
|
+
if(Math.abs(repairTarget-repairProgress)>0.0005){
|
|
1904
|
+
repairProgress+=(repairTarget-repairProgress)*0.085;
|
|
1905
|
+
if(Math.abs(repairTarget-repairProgress)<0.001) repairProgress=repairTarget;
|
|
1906
|
+
need=true; shadowNeedsUpdate=true;
|
|
1907
|
+
}
|
|
1908
|
+
const repairEase=repairProgress*repairProgress*(3-2*repairProgress);
|
|
1909
|
+
for(const entry of repairGroups){
|
|
1910
|
+
entry.group.rotation.x=entry.damageX*(1-repairEase);
|
|
1911
|
+
entry.group.rotation.z=entry.damageZ*(1-repairEase);
|
|
1912
|
+
}
|
|
1913
|
+
for(const entry of repairPieces){
|
|
1914
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1915
|
+
entry.mesh.rotation.y=entry.damageRotY*(1-repairEase);
|
|
1916
|
+
}
|
|
1917
|
+
for(const entry of buildingMaterials){
|
|
1918
|
+
entry.material.color.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1919
|
+
entry.material.attenuationColor.copy(entry.damaged).lerp(entry.clean,repairEase);
|
|
1920
|
+
entry.material.roughness=0.9-(0.2*repairEase);
|
|
1921
|
+
entry.material.transmission=0.05+(0.11*repairEase);
|
|
1922
|
+
entry.material.clearcoat=0.015+(0.035*repairEase);
|
|
1923
|
+
entry.material.clearcoatRoughness=0.94-(0.14*repairEase);
|
|
1924
|
+
entry.material.envMapIntensity=0.24+(0.21*repairEase);
|
|
1925
|
+
}
|
|
1926
|
+
for(const entry of corruptionFades){
|
|
1927
|
+
entry.mesh.position.copy(entry.damagePos).lerp(entry.cleanPos,repairEase);
|
|
1928
|
+
const scale=1-repairEase*0.92;
|
|
1929
|
+
entry.mesh.scale.copy(entry.baseScale).multiplyScalar(scale);
|
|
1930
|
+
entry.material.opacity=1-repairEase;
|
|
1931
|
+
entry.mesh.visible=repairEase<0.985;
|
|
1932
|
+
}
|
|
1933
|
+
for(const material of corruptionSurfaces) material.opacity=1-repairEase;
|
|
1934
|
+
const now=performance.now()*0.001;
|
|
1935
|
+
for(const floater of corruptionFloaters){
|
|
1936
|
+
if(repairEase<0.985){
|
|
1937
|
+
floater.mesh.position.y+=Math.sin(now*floater.speed+floater.phase)*3.2*(1-repairEase);
|
|
1938
|
+
floater.mesh.rotation.y+=0.0025*floater.speed*(1-repairEase);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if(corruptionFloaters.length && repairEase<0.985) need=true;
|
|
1942
|
+
if(shadowNeedsUpdate && (++shadowFrame%3===0 || repairProgress===repairTarget)) key.shadow.needsUpdate=true;
|
|
1943
|
+
if(need||dirty){ render(); dirty=false; }
|
|
1944
|
+
}
|
|
1945
|
+
loop();
|
|
1946
|
+
|
|
1947
|
+
// ---- scale the whole card to fill the viewport, keeping the visualizer crisp ----
|
|
1948
|
+
const cardEl=document.querySelector('.share-card');
|
|
1949
|
+
function fit(){
|
|
1950
|
+
if(setupMode){
|
|
1951
|
+
VIZ_W=figureModeEmbed ? window.innerWidth : Math.round(window.innerWidth - Math.min(window.innerWidth*0.4, 620)); VIZ_H=window.innerHeight;
|
|
1952
|
+
cardEl.style.transform='none';
|
|
1953
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1954
|
+
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
1955
|
+
frame();
|
|
1956
|
+
dirty=true;
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const contain=Math.min((window.innerWidth-28)/920, (window.innerHeight-28)/1180);
|
|
1960
|
+
const s=contain;
|
|
1961
|
+
cardEl.style.transform=`scale(${s})`;
|
|
1962
|
+
renderer.setPixelRatio(Math.min(Math.max(window.devicePixelRatio*s, 1), 3)); // match displayed res
|
|
1963
|
+
renderer.setSize(VIZ_W,VIZ_H,false);
|
|
1964
|
+
dirty=true;
|
|
1965
|
+
}
|
|
1966
|
+
fit();
|
|
1967
|
+
window.addEventListener('resize', fit);
|
|
1968
|
+
|
|
1969
|
+
// Loading lives inside the report: reveal only after a provenance-checked report has
|
|
1970
|
+
// populated at least one real repo. Empty, failed, and invalid setup states stay closed.
|
|
1971
|
+
function finishSetupReveal(){
|
|
1972
|
+
document.querySelectorAll('[data-setup-pending-hidden]').forEach((el)=>{ el.removeAttribute('aria-hidden'); el.removeAttribute('data-setup-pending-hidden'); });
|
|
1973
|
+
document.body.classList.remove('scanning');
|
|
1974
|
+
}
|
|
1975
|
+
if(setupMode && setupReport && setupLiveRepos.length && all.length){
|
|
1976
|
+
requestAnimationFrame(()=>requestAnimationFrame(finishSetupReveal));
|
|
1977
|
+
window.setTimeout(finishSetupReveal, 1200);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
(function(){
|
|
1981
|
+
const btn=document.getElementById('fr-read-btn');
|
|
1982
|
+
const panel=document.getElementById('fr-persona-hover');
|
|
1983
|
+
if(!btn || !panel) return;
|
|
1984
|
+
const setOpen=(open)=>{
|
|
1985
|
+
panel.classList.toggle('open', open);
|
|
1986
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
1987
|
+
};
|
|
1988
|
+
btn.addEventListener('click', (event)=>{
|
|
1989
|
+
event.stopPropagation();
|
|
1990
|
+
setOpen(!panel.classList.contains('open'));
|
|
1991
|
+
});
|
|
1992
|
+
document.addEventListener('click', (event)=>{
|
|
1993
|
+
if(event.target!==btn && !panel.contains(event.target)) setOpen(false);
|
|
1994
|
+
});
|
|
1995
|
+
window.addEventListener('keydown', (event)=>{ if(event.key==='Escape') setOpen(false); });
|
|
1996
|
+
})();
|
|
1997
|
+
|
|
1998
|
+
document.querySelectorAll('#setup-connect,#v7-connect,#fr-connect').forEach((connectButton)=>{
|
|
1999
|
+
connectButton.addEventListener('click', (event)=>{
|
|
2000
|
+
event.stopPropagation();
|
|
2001
|
+
if(setupMode) window.parent?.postMessage({type:'echomem:connect-click'}, window.location.origin);
|
|
2002
|
+
});
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// ---- rotating context-health readout: HUD score, share, time, and API-equivalent waste ----
|
|
2006
|
+
const parseScalar=(value)=>{
|
|
2007
|
+
const match=String(value||'').match(/[0-9][0-9,]*(?:\.[0-9]+)?/);
|
|
2008
|
+
return match ? Number(match[0].replace(/,/g,''))||0 : 0;
|
|
2009
|
+
};
|
|
2010
|
+
const formatWasteMoney=(value)=>`$${Math.round(value).toLocaleString('en-US')}`;
|
|
2011
|
+
function buildMetricSlides(dirtyPct, attentionHours, computeCost, report){
|
|
2012
|
+
const wastePct=goldenWastePct(report);
|
|
2013
|
+
const usefulPct=goldenUsefulPct(report);
|
|
2014
|
+
const dirtyShare=Math.max(0, Number.isFinite(wastePct) ? wastePct : parseScalar(dirtyPct))/100;
|
|
2015
|
+
const wastedHours=Math.max(0, parseScalar(attentionHours))*dirtyShare;
|
|
2016
|
+
const wastedDays=wastedHours/8;
|
|
2017
|
+
const wastedCost=Math.max(0, parseScalar(computeCost))*dirtyShare;
|
|
2018
|
+
const slides=[];
|
|
2019
|
+
slides.push(
|
|
2020
|
+
{
|
|
2021
|
+
value:`${oneDec(wastePct)}%`,
|
|
2022
|
+
label:'wasteful context',
|
|
2023
|
+
say:goldenWasteSay(report)
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
value:`${oneDec(usefulPct)}%`,
|
|
2027
|
+
label:'useful context',
|
|
2028
|
+
say:'Useful context is the portion of the analyzed window that still helps the agent: active task state, product facts, current files, and required overhead.'
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
value:`≈${Math.round(wastedHours)}h`,
|
|
2032
|
+
label:'waste-weighted time',
|
|
2033
|
+
say:`That is roughly <b>${wastedDays.toFixed(1)} workdays</b> of agent attention weighted by the golden-standard wasteful-context share.`
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
value:`≈${formatWasteMoney(wastedCost)}`,
|
|
2037
|
+
label:'API-equiv waste',
|
|
2038
|
+
say:`At list API-equivalent rates, the wasteful-context share represents about <b>${formatWasteMoney(wastedCost)}</b> of the compute volume shown above. Not your bill — the scale of the waste.`
|
|
2039
|
+
},
|
|
2040
|
+
);
|
|
2041
|
+
return slides;
|
|
2042
|
+
}
|
|
2043
|
+
function bindMetricCarousel({metric,value,label,say,slides}){
|
|
2044
|
+
if(!metric || !value || !label || !say || !slides?.length) return;
|
|
2045
|
+
let index=0;
|
|
2046
|
+
function show(indexToShow){
|
|
2047
|
+
const slide=slides[indexToShow%slides.length];
|
|
2048
|
+
metric.classList.add('is-swapping');
|
|
2049
|
+
say.classList.add('is-swapping');
|
|
2050
|
+
window.setTimeout(()=>{
|
|
2051
|
+
value.textContent=slide.value;
|
|
2052
|
+
label.textContent=slide.label;
|
|
2053
|
+
say.innerHTML=slide.say;
|
|
2054
|
+
metric.classList.remove('is-swapping');
|
|
2055
|
+
say.classList.remove('is-swapping');
|
|
2056
|
+
}, 520);
|
|
2057
|
+
}
|
|
2058
|
+
if(!setupMode && slides.length>1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
|
|
2059
|
+
window.setInterval(()=>{
|
|
2060
|
+
index=(index+1)%slides.length;
|
|
2061
|
+
show(index);
|
|
2062
|
+
}, 6000);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
bindMetricCarousel({
|
|
2066
|
+
metric:document.getElementById('v7-metric'),
|
|
2067
|
+
value:document.getElementById('v7-metric-value'),
|
|
2068
|
+
label:document.getElementById('v7-metric-label'),
|
|
2069
|
+
say:document.getElementById('v7-metric-say'),
|
|
2070
|
+
slides:buildMetricSlides(
|
|
2071
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
2072
|
+
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
2073
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2074
|
+
setupReport
|
|
2075
|
+
),
|
|
2076
|
+
});
|
|
2077
|
+
bindMetricCarousel({
|
|
2078
|
+
metric:document.getElementById('fr-metric'),
|
|
2079
|
+
value:document.getElementById('fr-metric-value'),
|
|
2080
|
+
label:document.getElementById('fr-metric-label'),
|
|
2081
|
+
say:document.getElementById('fr-metric-say'),
|
|
2082
|
+
slides:buildMetricSlides(
|
|
2083
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
2084
|
+
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
2085
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2086
|
+
setupReport
|
|
2087
|
+
),
|
|
2088
|
+
});
|
|
2089
|
+
function reserveFrSayHeight(){
|
|
2090
|
+
if(!setupMode) return;
|
|
2091
|
+
const say=document.getElementById('fr-metric-say'); if(!say) return;
|
|
2092
|
+
const slides=buildMetricSlides(
|
|
2093
|
+
goldenWastePct(setupReport) || setupReport?.cleanliness?.staleReadSharePct || '85.0',
|
|
2094
|
+
document.getElementById('v7-attention')?.textContent || '191.1',
|
|
2095
|
+
document.getElementById('v7-cost')?.textContent || '$13,689',
|
|
2096
|
+
setupReport
|
|
2097
|
+
);
|
|
2098
|
+
const prev=say.innerHTML; say.style.minHeight='0px'; let max=0;
|
|
2099
|
+
for(const slide of slides){ say.innerHTML=slide.say; max=Math.max(max, say.offsetHeight); }
|
|
2100
|
+
say.innerHTML=prev;
|
|
2101
|
+
if(max>0) say.style.minHeight=max+'px';
|
|
2102
|
+
}
|
|
2103
|
+
reserveFrSayHeight();
|
|
2104
|
+
window.addEventListener('resize', reserveFrSayHeight);
|
|
2105
|
+
|
|
2106
|
+
// ---- repeated-reads info sheet: v7 "how we read your city" disclosure, moved behind the info button ----
|
|
2107
|
+
const v7InfoBtns=Array.from(document.querySelectorAll('[aria-controls="v7-info-sheet"]'));
|
|
2108
|
+
const v7InfoSheet=document.getElementById('v7-info-sheet');
|
|
2109
|
+
const v7InfoClose=document.getElementById('v7-info-close');
|
|
2110
|
+
function setV7InfoSheet(open){
|
|
2111
|
+
if(!v7InfoSheet) return;
|
|
2112
|
+
v7InfoSheet.classList.toggle('open', open);
|
|
2113
|
+
v7InfoSheet.setAttribute('aria-hidden', String(!open));
|
|
2114
|
+
v7InfoBtns.forEach((button)=>button.setAttribute('aria-expanded', String(open)));
|
|
2115
|
+
}
|
|
2116
|
+
if(v7InfoBtns.length && v7InfoSheet){
|
|
2117
|
+
v7InfoBtns.forEach((button)=>button.addEventListener('click', (event)=>{
|
|
2118
|
+
event.stopPropagation();
|
|
2119
|
+
setV7InfoSheet(!v7InfoSheet.classList.contains('open'));
|
|
2120
|
+
}));
|
|
2121
|
+
v7InfoSheet.addEventListener('click', (event)=>{
|
|
2122
|
+
if(event.target===v7InfoSheet) setV7InfoSheet(false);
|
|
2123
|
+
});
|
|
2124
|
+
v7InfoSheet.querySelector('.v7-infoCard')?.addEventListener('click', (event)=>event.stopPropagation());
|
|
2125
|
+
}
|
|
2126
|
+
if(v7InfoClose) v7InfoClose.addEventListener('click', (event)=>{ event.stopPropagation(); setV7InfoSheet(false); });
|
|
2127
|
+
|
|
2128
|
+
// ---- hover a cube → highlight it + open a clear detail panel (more detail than the top label) ----
|
|
2129
|
+
const detailEl=document.getElementById('repo-detail');
|
|
2130
|
+
const ray=new THREE.Raycaster();
|
|
2131
|
+
let downXY=null, selected=null, activeDetail=null, pointerIsDown=false;
|
|
2132
|
+
function highlight(grp){
|
|
2133
|
+
if(selected) selected.traverse(m=>{ if(m.material && m.material.emissive) m.material.emissive.setHex(0x000000); });
|
|
2134
|
+
selected=grp;
|
|
2135
|
+
if(grp) grp.traverse(m=>{ if(m.material && m.material.emissive) m.material.emissive.copy(m.material.color).multiplyScalar(0.22); });
|
|
2136
|
+
dirty=true;
|
|
2137
|
+
}
|
|
2138
|
+
function repoAtPointer(e){
|
|
2139
|
+
const rect=renderer.domElement.getBoundingClientRect();
|
|
2140
|
+
ray.setFromCamera({x:((e.clientX-rect.left)/rect.width)*2-1, y:-((e.clientY-rect.top)/rect.height)*2+1}, camera);
|
|
2141
|
+
const hits=ray.intersectObjects(all.map(o=>o.mesh), true);
|
|
2142
|
+
return hits.length ? hits[0].object.userData : null;
|
|
2143
|
+
}
|
|
2144
|
+
function updateHoverDetail(e){
|
|
2145
|
+
if(pointerIsDown) return;
|
|
2146
|
+
const repo=repoAtPointer(e);
|
|
2147
|
+
if(repo) showDetail(repo); else hideDetail();
|
|
2148
|
+
}
|
|
2149
|
+
renderer.domElement.addEventListener('pointermove', updateHoverDetail);
|
|
2150
|
+
renderer.domElement.addEventListener('pointerleave', hideDetail);
|
|
2151
|
+
renderer.domElement.addEventListener('pointerdown', e=>{ downXY=[e.clientX,e.clientY]; pointerIsDown=true; hideDetail(); });
|
|
2152
|
+
renderer.domElement.addEventListener('pointerup', e=>{
|
|
2153
|
+
if(!downXY) return;
|
|
2154
|
+
const moved=Math.hypot(e.clientX-downXY[0], e.clientY-downXY[1]); downXY=null;
|
|
2155
|
+
pointerIsDown=false;
|
|
2156
|
+
if(moved>6) return; // it was a drag-to-rotate, not a click
|
|
2157
|
+
const repo=repoAtPointer(e); // tap/click fallback for touch and keyboard-less devices
|
|
2158
|
+
if(repo) showDetail(repo); else hideDetail();
|
|
2159
|
+
});
|
|
2160
|
+
function showDetail(o){
|
|
2161
|
+
if(activeDetail===o && detailEl.style.display==='block') return;
|
|
2162
|
+
activeDetail=o;
|
|
2163
|
+
highlight(o.mesh);
|
|
2164
|
+
const dollars=Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US');
|
|
2165
|
+
const days=o.days>=0.1 ? `${o.days}` : '<0.1';
|
|
2166
|
+
const cxp=Number(o.codexPct)||0, clp=Number(o.claudePct)||0;
|
|
2167
|
+
const splitTotal=Math.max(1, cxp+clp);
|
|
2168
|
+
const codexW=Math.max(0, Math.min(100, cxp/splitTotal*100));
|
|
2169
|
+
const claudeW=Math.max(0, 100-codexW);
|
|
2170
|
+
const esc=(v)=>String(v).replace(/[&<>"]/g,(c)=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
2171
|
+
detailEl.innerHTML=
|
|
2172
|
+
`<div class="rd-kicker">Repository</div>`+
|
|
2173
|
+
`<div class="rd-name">${esc(o.name)}</div>`+
|
|
2174
|
+
`<div class="rd-metrics">`+
|
|
2175
|
+
`<div><div class="rd-label">Total tokens</div><div class="rd-value">${fmtTok(o.tokens)}</div></div>`+
|
|
2176
|
+
`<div><div class="rd-label">API-equiv cost</div><div class="rd-value">$${dollars}</div></div>`+
|
|
2177
|
+
`</div>`+
|
|
2178
|
+
`<div class="rd-meta">${days} focus days · ${Number(o.sessions)||0} sessions</div>`+
|
|
2179
|
+
`<div class="rd-split"><i class="rd-seg claude" style="width:${claudeW}%"></i><i class="rd-seg codex" style="width:${codexW}%"></i></div>`+
|
|
2180
|
+
`<div class="rd-legend"><span class="claude">Claude ${clp}%</span><span class="codex">Codex ${cxp}%</span></div>`;
|
|
2181
|
+
detailEl.style.display='block';
|
|
2182
|
+
const rect=renderer.domElement.getBoundingClientRect();
|
|
2183
|
+
const v=new THREE.Vector3(o.x, TRAY_TOP+o.h-3, o.z).project(camera); // anchor to the cube, not the cursor
|
|
2184
|
+
const cx=rect.left+(v.x*0.5+0.5)*rect.width, cy=rect.top+(-v.y*0.5+0.5)*rect.height;
|
|
2185
|
+
let x=cx+26, y=cy-detailEl.offsetHeight*0.5;
|
|
2186
|
+
if(x+detailEl.offsetWidth>window.innerWidth-8) x=cx-detailEl.offsetWidth-26;
|
|
2187
|
+
if(y<8) y=8; if(y+detailEl.offsetHeight>window.innerHeight-8) y=window.innerHeight-detailEl.offsetHeight-8;
|
|
2188
|
+
detailEl.style.left=x+'px'; detailEl.style.top=y+'px';
|
|
2189
|
+
}
|
|
2190
|
+
function hideDetail(){ activeDetail=null; highlight(null); detailEl.style.display='none'; }
|
|
2191
|
+
window.addEventListener('keydown', e=>{ if(e.key==='Escape'){ setV7InfoSheet(false); hideDetail(); } });
|
|
2192
|
+
|
|
2193
|
+
// ---- tool-split chips (official icons) ----
|
|
2194
|
+
const CLAUDE_SVG='<svg viewBox="0 0 100 100"><path fill="#d97757" d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>';
|
|
2195
|
+
const OPENAI_SVG='<svg viewBox="0 0 20 20"><path fill="#10a37f" d="M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z"/></svg>';
|
|
2196
|
+
document.getElementById('tool-split').innerHTML=
|
|
2197
|
+
`<div class="tool-chip">${OPENAI_SVG}<div><div class="pct">26%</div><div class="name">OpenAI Codex</div></div></div>`+
|
|
2198
|
+
`<div class="tool-chip">${CLAUDE_SVG}<div><div class="pct">74%</div><div class="name">Claude Code</div></div></div>`;
|
|
2199
|
+
</script>
|
|
2200
|
+
|
|
2201
|
+
<!-- Echo mascot (Rive) — the persona avatar, animated; replaces the old hand-drawn SVG -->
|
|
2202
|
+
<script src="./vendor/rive.js"></script>
|
|
2203
|
+
<script>
|
|
2204
|
+
(function(){
|
|
2205
|
+
if(!window.rive){ console.error('Rive runtime missing'); return; }
|
|
2206
|
+
const { Rive, Layout, Fit, Alignment, RuntimeLoader } = window.rive;
|
|
2207
|
+
RuntimeLoader.setWasmUrl('./vendor/rive.wasm');
|
|
2208
|
+
const canvas = document.getElementById('persona-mascot');
|
|
2209
|
+
if(!canvas) return;
|
|
2210
|
+
const mascot = new Rive({
|
|
2211
|
+
src: './vendor/echo_general-file-21.riv',
|
|
2212
|
+
canvas,
|
|
2213
|
+
autoplay: true,
|
|
2214
|
+
stateMachines: 'SM-General',
|
|
2215
|
+
layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
|
|
2216
|
+
onLoad: () => {
|
|
2217
|
+
const resize = () => { try { mascot.resizeDrawingSurfaceToCanvas(); } catch(e){} };
|
|
2218
|
+
resize();
|
|
2219
|
+
try {
|
|
2220
|
+
const inputs = mascot.stateMachineInputs('SM-General');
|
|
2221
|
+
const greet = inputs && inputs.find(i => i.name === 'introgreeting');
|
|
2222
|
+
if (greet) greet.fire();
|
|
2223
|
+
} catch(e){}
|
|
2224
|
+
setTimeout(resize, 300);
|
|
2225
|
+
window.addEventListener('resize', resize);
|
|
2226
|
+
},
|
|
2227
|
+
onLoadError: (e) => console.error('Echo mascot failed to load', e),
|
|
2228
|
+
});
|
|
2229
|
+
})();
|
|
2230
|
+
</script>
|
|
2231
|
+
</body>
|
|
2232
|
+
</html>
|