@echomem/mcp 1.4.8 → 1.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +23 -3
  2. package/assets/canonical-scorer/README.md +18 -0
  3. package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
  4. package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
  5. package/assets/canonical-scorer/golden_anchors.mjs +83 -0
  6. package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
  7. package/assets/hud/github.svg +1 -0
  8. package/dist/city/README.md +9 -0
  9. package/dist/city/echo-ai-city-only.html +1067 -107
  10. package/dist/codex-session-files.js +283 -0
  11. package/dist/codex-sync.js +7 -2
  12. package/dist/context-analysis/canonical-golden.js +47 -0
  13. package/dist/context-analysis/claude-canonical-adapter.js +315 -0
  14. package/dist/context-analysis/claude-native-canonical.js +1216 -0
  15. package/dist/context-analysis/vendored-canonical.js +793 -0
  16. package/dist/context-analysis/workspace-report.js +1838 -0
  17. package/dist/context-metrics/calculate.js +43 -0
  18. package/dist/context-metrics/estimator.js +45 -0
  19. package/dist/context-metrics/ledger.js +507 -0
  20. package/dist/context-metrics/model-limits.js +26 -0
  21. package/dist/context-metrics/parse-claude.js +227 -0
  22. package/dist/context-metrics/parse-codex.js +276 -0
  23. package/dist/context-metrics/types.js +1 -0
  24. package/dist/forensics-10-problems.js +7 -6
  25. package/dist/forensics.js +863 -132
  26. package/dist/hud/adapters.js +77 -202
  27. package/dist/hud/cli.js +0 -0
  28. package/dist/hud/efficiency.js +447 -0
  29. package/dist/hud/electron-main.js +3 -2
  30. package/dist/hud/fs.js +14 -0
  31. package/dist/hud/metric.js +17 -102
  32. package/dist/hud/monitor.js +136 -28
  33. package/dist/hud/render.js +4 -3
  34. package/dist/hud/server.js +30 -0
  35. package/dist/hud/web.js +622 -353
  36. package/dist/index.js +7 -3
  37. package/dist/local-data-paths.js +87 -0
  38. package/dist/migrate.js +37 -29
  39. package/dist/report.js +101 -40
  40. package/dist/setup-page/client-core.js +475 -0
  41. package/dist/setup-page/client-extraction.js +550 -0
  42. package/dist/setup-page/client-lifecycle.js +116 -0
  43. package/dist/setup-page/client-report-audit.js +818 -0
  44. package/dist/setup-page/client-report-city.js +204 -0
  45. package/dist/setup-page/client-report.js +6 -0
  46. package/dist/setup-page/client.js +15 -0
  47. package/dist/setup-page/document.js +37 -0
  48. package/dist/setup-page/styles-city-report.js +880 -0
  49. package/dist/setup-page/styles-context-audit.js +470 -0
  50. package/dist/setup-page/styles-extraction.js +821 -0
  51. package/dist/setup-page/styles-foundation.js +231 -0
  52. package/dist/setup-page/styles.js +11 -0
  53. package/dist/setup-page.js +15 -2538
  54. package/dist/setup-preview.js +245 -0
  55. package/dist/setup.js +456 -44
  56. package/package.json +8 -7
  57. package/templates/echomem-recall.md +2 -2
  58. package/dist/city/10-problems-report.html +0 -649
  59. package/dist/city/_live.html +0 -37
  60. package/dist/city/_serve.mjs +0 -45
  61. package/dist/city/card-data.json +0 -15
  62. package/dist/city/city-data.json +0 -248
  63. package/dist/city/echo-ai-city-only.template.html +0 -1272
  64. package/dist/city/generate-echo-city-only.mjs +0 -112
@@ -1,2539 +1,16 @@
1
- export function renderSetupPage() {
2
- return `<!doctype html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <title>EchoMem Device Setup</title>
8
- <style>
9
- :root {
10
- color-scheme: light;
11
- --echo-ink-primary: #1a3a8f;
12
- --echo-ink-accent: #4ecdc4;
13
- --echo-ink-seal: #c7372f;
14
- --echo-ink-text: #2b2a26;
15
- --echo-ink-mute: #6b6f69;
16
- --echo-ink-faint: #9a9b94;
17
- --echo-paper-canvas: #faf7f0;
18
- --echo-paper-white: #ffffff;
19
- --echo-paper-soft: #f0efe9;
20
- --echo-line-soft: #e4e0d6;
21
- --echo-ink-accent-deep: #2f9d93;
22
- --echo-night: #8b95cf;
23
- --rdo-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
24
- --echo-font-brand: "Avenir Next", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
25
- --echo-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
- --echo-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
27
- --bg: var(--echo-paper-canvas);
28
- --ink: var(--echo-ink-text);
29
- --muted: var(--echo-ink-mute);
30
- --line: var(--echo-line-soft);
31
- --panel: var(--echo-paper-white);
32
- --good-bg: #eff7ec;
33
- --good-line: #cbd9c8;
34
- --warn-bg: #fff6dc;
35
- --warn-line: #edce79;
36
- --error-bg: #fff1ef;
37
- --error-line: #e7aaa2;
38
- }
39
- * { box-sizing: border-box; }
40
- body {
41
- margin: 0;
42
- min-height: 100vh;
43
- background:
44
- linear-gradient(180deg, rgba(245, 241, 231, 0.92), rgba(255, 255, 255, 0.96)),
45
- var(--echo-paper-canvas);
46
- color: var(--echo-ink-text);
47
- font-family: var(--echo-font-body);
48
- letter-spacing: 0;
49
- }
50
- main {
51
- width: min(1120px, 100%);
52
- margin: 0 auto;
53
- padding: clamp(18px, 4vw, 34px);
54
- }
55
- header {
56
- display: flex;
57
- align-items: center;
58
- justify-content: space-between;
59
- gap: 16px;
60
- flex-wrap: wrap;
61
- border-bottom: 1px solid var(--line);
62
- padding-bottom: 18px;
63
- margin-bottom: 24px;
64
- }
65
- .kicker {
66
- margin: 0;
67
- font-family: var(--echo-font-mono);
68
- color: var(--echo-ink-mute);
69
- font-size: 12px;
70
- font-weight: 700;
71
- letter-spacing: 0.08em;
72
- text-transform: uppercase;
73
- }
74
- h1 {
75
- margin: 4px 0 0;
76
- font-family: var(--echo-font-brand);
77
- font-size: clamp(30px, 5vw, 56px);
78
- font-weight: 900;
79
- line-height: 1;
80
- letter-spacing: 0;
81
- color: var(--echo-ink-primary);
82
- }
83
- h2 {
84
- margin: 0;
85
- font-family: var(--echo-font-brand);
86
- font-size: 22px;
87
- font-weight: 900;
88
- line-height: 1.2;
89
- letter-spacing: 0;
90
- color: var(--echo-ink-primary);
91
- }
92
- .pill {
93
- min-height: 36px;
94
- display: inline-flex;
95
- align-items: center;
96
- gap: 8px;
97
- border: 1px solid #c9ccc3;
98
- border-radius: 999px;
99
- padding: 0 14px;
100
- background: var(--panel);
101
- font-family: var(--echo-font-mono);
102
- font-size: 12px;
103
- font-weight: 700;
104
- letter-spacing: 0.04em;
105
- text-transform: uppercase;
106
- white-space: nowrap;
107
- }
108
- .state {
109
- min-height: 220px;
110
- display: flex;
111
- align-items: center;
112
- justify-content: center;
113
- color: var(--echo-ink-mute);
114
- font-size: 16px;
115
- text-align: center;
116
- }
117
- body.cityMode {
118
- background: #eef0f3;
119
- }
120
- body.cityMode main {
121
- width: 100%;
122
- padding: 0;
123
- margin: 0;
124
- }
125
- body.cityMode header {
126
- display: none;
127
- }
128
- body.extractMode header {
129
- display: none;
130
- }
131
- body.readyMode header {
132
- display: none;
133
- }
134
- body.extractMode main {
135
- min-height: 100vh;
136
- display: grid;
137
- align-items: center;
138
- justify-items: center;
139
- }
140
- body.readyMode main {
141
- min-height: 100vh;
142
- display: grid;
143
- align-items: center;
144
- justify-items: center;
145
- padding: clamp(24px, 5vw, 56px);
146
- }
147
- body.cityMode #app {
148
- display: block;
149
- min-height: 100vh;
150
- color: var(--echo-ink-text);
151
- text-align: left;
152
- }
153
- body.extractMode #app {
154
- width: 100%;
155
- display: grid;
156
- justify-items: center;
157
- }
158
- body.readyMode #app {
159
- width: 100%;
160
- display: grid;
161
- justify-items: center;
162
- }
163
-
164
- /* clean, consistent loader for the post-connect "working/starting" states */
165
- .miniLoad { display: grid; gap: 12px; max-width: 520px; padding: clamp(24px, 6vw, 64px) 0; }
166
- .miniLoad h2 { margin: 0; font-family: var(--echo-font-brand); font-size: clamp(22px, 3.6vw, 32px); color: var(--echo-ink-primary); }
167
- .miniLoad p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.5; }
168
- .miniBar { height: 6px; border-radius: 999px; background: var(--echo-paper-soft); overflow: hidden; max-width: 360px; margin-top: 4px; }
169
- .miniBar i { display: block; height: 100%; width: 36%; border-radius: 999px; background: var(--echo-ink-primary); animation: miniIndet 1.3s ease-in-out infinite; }
170
- @keyframes miniIndet { 0% { transform: translateX(-115%); } 100% { transform: translateX(330%); } }
171
-
172
- /* ---- forensic report (ContextDoctorTwo) ---- */
173
- .cityHero {
174
- --city-display-w: min(calc(100vw - 28px), calc((100vh - 28px) * 0.7797));
175
- --city-display-h: min(calc(100vh - 28px), calc((100vw - 28px) * 1.2826));
176
- position: relative;
177
- min-height: 100vh;
178
- display: block;
179
- padding: 0;
180
- overflow: hidden;
181
- background: #eef0f3;
182
- }
183
- .cityShell {
184
- position: relative;
185
- width: 100vw;
186
- height: 100vh;
187
- border-radius: 0;
188
- overflow: hidden;
189
- }
190
- .cityShell iframe {
191
- width: 100%;
192
- height: 100%;
193
- border: 0;
194
- display: block;
195
- border-radius: 0;
196
- background: transparent;
197
- }
198
- .cityArrow {
199
- position: absolute;
200
- z-index: 5;
201
- right: clamp(22px, 3vw, 42px);
202
- bottom: clamp(22px, 3.5vh, 42px);
203
- width: 44px;
204
- height: 44px;
205
- min-height: 44px;
206
- padding: 0;
207
- border-radius: 999px;
208
- border-color: rgba(26, 58, 143, 0.2);
209
- background: rgba(255, 255, 255, 0.92);
210
- color: var(--echo-ink-primary);
211
- font-size: 24px;
212
- line-height: 1;
213
- box-shadow: 0 10px 28px rgba(28, 38, 60, 0.12);
214
- }
215
- .detailsWrap {
216
- width: 100%;
217
- margin: 0;
218
- padding: clamp(42px, 6vw, 86px) clamp(28px, 7vw, 104px);
219
- background: #fff;
220
- }
221
- #setup-details { scroll-margin-top: 0; }
222
- .hero { padding: 4px 0 24px; }
223
- .privacy {
224
- display: grid;
225
- grid-template-columns: auto 1fr;
226
- gap: 10px;
227
- align-items: baseline;
228
- margin-bottom: 24px;
229
- padding: 10px 0;
230
- border-bottom: 1px solid var(--echo-line-soft);
231
- }
232
- .privacy span,
233
- .kicker-mono,
234
- .sectionHead span {
235
- font-family: var(--echo-font-mono);
236
- font-size: 12px;
237
- font-weight: 700;
238
- letter-spacing: 0.08em;
239
- text-transform: uppercase;
240
- color: var(--echo-ink-primary);
241
- }
242
- .privacy p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
243
- .heroGrid {
244
- display: grid;
245
- grid-template-columns: minmax(0, 1fr) 280px;
246
- gap: clamp(20px, 3vw, 36px);
247
- align-items: stretch;
248
- }
249
- .heroCopy { display: flex; flex-direction: column; justify-content: center; }
250
- .heroCopy .kicker-mono { margin: 0 0 14px; color: var(--echo-ink-primary); }
251
- .heroCopy h2 {
252
- max-width: 720px;
253
- margin: 0;
254
- font-size: clamp(34px, 5vw, 58px);
255
- line-height: 1.02;
256
- }
257
- .heroCopy p:not(.kicker-mono) {
258
- margin: 18px 0 0;
259
- font-size: clamp(15px, 1.25vw, 18px);
260
- line-height: 1.62;
261
- color: var(--echo-ink-text);
262
- }
263
- .scoreCard, .promiseCard, .panel {
264
- border: 1px solid var(--echo-line-soft);
265
- border-radius: 8px;
266
- background: var(--echo-paper-white);
267
- }
268
- .scoreCard { display: grid; align-content: center; min-height: 282px; padding: 22px; }
269
- .scoreCard span, .promiseCard span {
270
- font-family: var(--echo-font-mono);
271
- font-size: 12px;
272
- font-weight: 700;
273
- letter-spacing: 0.08em;
274
- text-transform: uppercase;
275
- color: var(--echo-ink-mute);
276
- }
277
- .scoreCard strong {
278
- margin-top: 12px;
279
- font-family: var(--echo-font-brand);
280
- font-size: clamp(72px, 8vw, 96px);
281
- font-weight: 900;
282
- line-height: 0.9;
283
- color: var(--echo-ink-seal);
284
- font-variant-numeric: tabular-nums;
285
- }
286
- .scoreCard em {
287
- margin-top: 4px;
288
- font-style: normal;
289
- font-weight: 900;
290
- font-size: 22px;
291
- color: var(--echo-ink-primary);
292
- }
293
- .scoreCard p, .promiseCard p { margin: 14px 0 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
294
- .statStrip {
295
- display: grid;
296
- grid-template-columns: repeat(4, minmax(0, 1fr));
297
- gap: 10px;
298
- margin-top: 22px;
299
- }
300
- .stat {
301
- min-height: 92px;
302
- padding: 14px;
303
- border: 1px solid var(--echo-line-soft);
304
- border-radius: 8px;
305
- background: var(--echo-paper-white);
306
- }
307
- .stat strong {
308
- display: block;
309
- font-family: var(--echo-font-brand);
310
- font-size: clamp(24px, 2.8vw, 34px);
311
- font-weight: 900;
312
- line-height: 1;
313
- color: var(--echo-ink-primary);
314
- font-variant-numeric: tabular-nums;
315
- }
316
- .stat.alert strong { color: var(--echo-ink-seal); }
317
- .stat span {
318
- display: block;
319
- margin-top: 8px;
320
- font-family: var(--echo-font-mono);
321
- font-size: 10px;
322
- font-weight: 700;
323
- letter-spacing: 0.06em;
324
- text-transform: uppercase;
325
- color: var(--echo-ink-mute);
326
- }
327
- .sourceNote {
328
- max-width: 980px;
329
- margin: 16px 0 0;
330
- font-family: var(--echo-font-mono);
331
- font-size: 11px;
332
- line-height: 1.55;
333
- color: var(--echo-ink-faint);
334
- }
335
- .sourceNote code {
336
- padding: 1px 5px;
337
- border-radius: 4px;
338
- background: var(--echo-paper-soft);
339
- color: var(--echo-ink-primary);
340
- }
341
- .section { padding: 30px 0 0; margin-top: 30px; border-top: 1px solid var(--echo-line-soft); }
342
- .sectionHead { display: flex; gap: 12px; align-items: baseline; margin-bottom: 18px; }
343
- .sectionHead h2 { margin: 0; font-size: clamp(24px, 3.4vw, 38px); line-height: 1.05; }
344
- .twoCol {
345
- display: grid;
346
- grid-template-columns: minmax(0, 1fr) minmax(340px, 0.8fr);
347
- gap: clamp(20px, 3.5vw, 36px);
348
- align-items: start;
349
- }
350
- .storyText p { margin: 0 0 18px; color: var(--echo-ink-text); font-size: clamp(15px, 1.25vw, 18px); line-height: 1.62; }
351
- .storyText p:last-child { margin-bottom: 0; }
352
- .storyText b { color: var(--echo-ink-primary); }
353
- .panel { display: grid; gap: 12px; padding: 16px; }
354
- .promiseCard { padding: 22px; }
355
- .promiseCard strong {
356
- display: block;
357
- margin-top: 12px;
358
- font-family: var(--echo-font-brand);
359
- font-size: clamp(34px, 4vw, 48px);
360
- font-weight: 900;
361
- line-height: 1;
362
- color: var(--echo-ink-primary);
363
- }
364
- .promiseActions { display: grid; gap: 10px; margin-top: 18px; }
365
- .promiseCard button, .promiseCard .button {
366
- width: 100%;
367
- min-height: 48px;
368
- border-radius: 6px;
369
- font-family: var(--echo-font-mono);
370
- font-size: 13px;
371
- font-weight: 800;
372
- letter-spacing: 0.08em;
373
- text-transform: uppercase;
374
- }
375
- .promiseCard .primary { border-color: var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; }
376
- .promiseCard .secondary { background: #fff; color: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
377
-
378
- /* histogram */
379
- .histo { display: flex; align-items: flex-end; gap: 3px; height: 120px; padding-top: 8px; }
380
- .histoBar { flex: 1; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--echo-ink-primary); }
381
- .histoBar.night { background: var(--echo-ink-seal); }
382
- .histoAxis {
383
- display: flex; justify-content: space-between; margin-top: 8px;
384
- font-family: var(--echo-font-mono); font-size: 10px; letter-spacing: 0.06em; color: var(--echo-ink-faint);
385
- }
386
- .chips { display: flex; flex-wrap: wrap; gap: 8px; }
387
- .chips span {
388
- padding: 6px 8px; border: 1px solid var(--echo-line-soft); border-radius: 6px;
389
- background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-primary);
390
- }
391
-
392
- /* reread / repo rows */
393
- .row2 {
394
- display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
395
- padding: 11px 0; border-bottom: 1px solid var(--echo-line-soft);
396
- }
397
- .row2:last-child { border-bottom: 0; }
398
- .row2 b { font-family: var(--echo-font-mono); font-size: 13px; color: var(--echo-ink-primary); }
399
- .row2 small { display: block; margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
400
- .row2 strong {
401
- font-family: var(--echo-font-brand); font-size: 24px; font-weight: 900;
402
- color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap;
403
- }
404
- .repoRow strong { color: var(--echo-ink-primary); }
405
- .tag {
406
- display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px;
407
- background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 10px;
408
- font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal);
409
- }
410
-
411
- /* cost split + model rows */
412
- .segBar { display: flex; width: 100%; height: 22px; overflow: hidden; border-radius: 5px; background: var(--echo-paper-soft); }
413
- .segBar span { display: block; min-width: 2px; }
414
- .seg-output { background: var(--echo-ink-primary); }
415
- .seg-cold { background: var(--echo-ink-mute); }
416
- .seg-cw { background: var(--echo-ink-accent); }
417
- .seg-cr { background: var(--echo-ink-seal); }
418
- .costLegend { display: grid; gap: 6px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
419
- .modelRow {
420
- display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: baseline;
421
- padding: 9px 0; border-bottom: 1px solid var(--echo-line-soft);
422
- font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute);
423
- }
424
- .modelRow:last-child { border-bottom: 0; }
425
- .modelRow b { font-size: 13px; color: var(--echo-ink-primary); }
426
- .modelRow strong { font-family: var(--echo-font-brand); font-size: 18px; font-weight: 900; color: var(--echo-ink-seal); }
427
-
428
- .nightCard {
429
- margin-top: 18px; padding: 20px 22px; border: 1px solid var(--echo-ink-seal);
430
- border-radius: 8px; background: var(--echo-paper-white);
431
- }
432
- .nightCard span {
433
- font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
434
- text-transform: uppercase; color: var(--echo-ink-seal);
435
- }
436
- .nightCard p { margin: 10px 0 0; font-size: 17px; line-height: 1.5; color: var(--echo-ink-text); }
437
- .nightCard b { color: var(--echo-ink-primary); }
438
-
439
- /* optional city explanation page */
440
- .explainPage {
441
- max-width: 1260px;
442
- margin: 0 auto;
443
- }
444
- .explainHero {
445
- display: grid;
446
- grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
447
- gap: clamp(28px, 5vw, 70px);
448
- align-items: end;
449
- padding-bottom: clamp(28px, 5vw, 58px);
450
- border-bottom: 1px solid var(--echo-line-soft);
451
- }
452
- .explainCopy {
453
- max-width: 760px;
454
- }
455
- .explainCopy h2 {
456
- margin: 10px 0 0;
457
- font-size: clamp(38px, 5.4vw, 74px);
458
- line-height: 0.98;
459
- color: var(--echo-ink-primary);
460
- }
461
- .explainCopy p:not(.kicker-mono) {
462
- max-width: 680px;
463
- margin: 20px 0 0;
464
- font-size: clamp(16px, 1.35vw, 20px);
465
- line-height: 1.55;
466
- color: var(--echo-ink-text);
467
- }
468
- .cleanReadout {
469
- border: 1px solid var(--echo-line-soft);
470
- border-radius: 8px;
471
- padding: 22px;
472
- background: #fff;
473
- }
474
- .cleanReadout span {
475
- display: block;
476
- font-family: var(--echo-font-mono);
477
- font-size: 11px;
478
- font-weight: 800;
479
- letter-spacing: 0.08em;
480
- text-transform: uppercase;
481
- color: var(--echo-ink-mute);
482
- }
483
- .cleanReadout strong {
484
- display: block;
485
- margin-top: 8px;
486
- font-family: var(--echo-font-brand);
487
- font-size: clamp(54px, 6vw, 88px);
488
- font-weight: 900;
489
- line-height: 0.9;
490
- color: var(--echo-ink-seal);
491
- font-variant-numeric: tabular-nums;
492
- }
493
- .cleanReadout p {
494
- margin: 14px 0 0;
495
- color: var(--echo-ink-mute);
496
- font-size: 15px;
497
- line-height: 1.55;
498
- }
499
- .insightPills {
500
- display: grid;
501
- grid-template-columns: repeat(3, minmax(0, 1fr));
502
- gap: 10px;
503
- margin-top: clamp(18px, 3vw, 32px);
504
- }
505
- .insightPill {
506
- border: 1px solid var(--echo-line-soft);
507
- border-radius: 8px;
508
- padding: 14px 16px;
509
- background: #fff;
510
- }
511
- .insightPill strong {
512
- display: block;
513
- font-family: var(--echo-font-brand);
514
- font-size: clamp(24px, 2.2vw, 34px);
515
- font-weight: 900;
516
- line-height: 1;
517
- color: var(--echo-ink-primary);
518
- font-variant-numeric: tabular-nums;
519
- }
520
- .insightPill.alert strong { color: var(--echo-ink-seal); }
521
- .insightPill span {
522
- display: block;
523
- margin-top: 8px;
524
- font-family: var(--echo-font-mono);
525
- font-size: 10px;
526
- font-weight: 800;
527
- letter-spacing: 0.07em;
528
- text-transform: uppercase;
529
- color: var(--echo-ink-mute);
530
- }
531
- .calcNote {
532
- margin-top: 18px;
533
- border-top: 1px solid var(--echo-line-soft);
534
- padding-top: 14px;
535
- color: var(--echo-ink-faint);
536
- font-family: var(--echo-font-mono);
537
- font-size: 11px;
538
- line-height: 1.6;
539
- }
540
- .calcNote summary {
541
- cursor: pointer;
542
- color: var(--echo-ink-primary);
543
- font-weight: 800;
544
- text-transform: uppercase;
545
- letter-spacing: 0.06em;
546
- }
547
- .calcNote p { margin: 10px 0 0; }
548
- .explainSections {
549
- display: grid;
550
- grid-template-columns: repeat(3, minmax(0, 1fr));
551
- gap: 14px;
552
- margin-top: clamp(28px, 4vw, 52px);
553
- }
554
- .explainCard {
555
- border: 1px solid var(--echo-line-soft);
556
- border-radius: 8px;
557
- background: #fff;
558
- padding: 18px;
559
- min-height: 230px;
560
- display: grid;
561
- gap: 14px;
562
- align-content: start;
563
- }
564
- .explainCard .kicker-mono { margin: 0; color: var(--echo-ink-mute); }
565
- .explainCard h3 {
566
- margin: 0;
567
- font-family: var(--echo-font-brand);
568
- font-size: clamp(21px, 2vw, 28px);
569
- line-height: 1.08;
570
- color: var(--echo-ink-primary);
571
- }
572
- .explainCard p {
573
- margin: 0;
574
- color: var(--echo-ink-text);
575
- font-size: 15px;
576
- line-height: 1.55;
577
- }
578
- .explainCard .panel {
579
- padding: 0;
580
- border: 0;
581
- background: transparent;
582
- }
583
- .explainCta {
584
- display: grid;
585
- grid-template-columns: minmax(0, 1fr) auto;
586
- gap: 24px;
587
- align-items: center;
588
- margin-top: 16px;
589
- padding: 22px;
590
- border: 1px solid var(--echo-line-soft);
591
- border-radius: 8px;
592
- background: var(--echo-paper-canvas);
593
- }
594
- .explainCta h3 {
595
- margin: 0;
596
- font-family: var(--echo-font-brand);
597
- font-size: clamp(24px, 2.5vw, 34px);
598
- line-height: 1.08;
599
- color: var(--echo-ink-primary);
600
- }
601
- .explainCta p {
602
- margin: 8px 0 0;
603
- max-width: 700px;
604
- color: var(--echo-ink-mute);
605
- font-size: 15px;
606
- line-height: 1.5;
607
- }
608
- .explainCta .primary, .explainCta .secondary {
609
- min-width: 180px;
610
- min-height: 50px;
611
- border-radius: 8px;
612
- font-family: var(--echo-font-mono);
613
- font-size: 13px;
614
- font-weight: 800;
615
- letter-spacing: 0.08em;
616
- text-transform: uppercase;
617
- }
618
-
619
- /* ---- dashboard (post-auth extraction) ---- */
620
- .loading-panel, .doctor-copy, .doctor-score, .metric, .extract-panel {
621
- border: 1px solid var(--line);
622
- border-radius: 8px;
623
- background: var(--panel);
624
- }
625
- .status-strip {
626
- display: flex; align-items: flex-start; gap: 12px;
627
- border: 1px solid #c7d6e5; border-radius: 8px; background: #eef5fb; color: #1e3449;
628
- padding: 13px 15px; margin-bottom: 18px;
629
- }
630
- .status-strip.good { border-color: var(--good-line); background: var(--good-bg); color: #203820; }
631
- .status-strip.warn { border-color: var(--warn-line); background: var(--warn-bg); color: #5e4300; }
632
- .status-strip .dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 999px; margin-top: 4px; background: currentColor; }
633
- .status-strip strong { display: block; line-height: 1.25; }
634
- .status-strip p { margin-top: 3px; color: currentColor; font-size: 14px; }
635
- .doctor-hero {
636
- display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
637
- gap: 14px; margin-bottom: 14px; align-items: stretch;
638
- }
639
- .doctor-copy, .doctor-score { padding: clamp(18px, 3vw, 26px); }
640
- .doctor-copy { display: flex; flex-direction: column; justify-content: center; min-height: 240px; }
641
- .doctor-copy h2 { max-width: 760px; margin-top: 10px; font-size: clamp(30px, 4.4vw, 52px); line-height: 1.02; }
642
- .doctor-copy p:not(.label):not(.helper) { max-width: 720px; margin-top: 14px; font-size: clamp(15px, 1.5vw, 18px); color: var(--echo-ink-text); }
643
- .doctor-score { display: grid; align-content: center; gap: 12px; }
644
- .doctor-score > span { font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
645
- .doctor-score > strong { display: block; font-family: var(--echo-font-brand); font-size: clamp(34px, 5.5vw, 60px); font-weight: 900; line-height: 0.95; color: var(--echo-ink-primary); }
646
- .doctor-mini { display: grid; gap: 8px; margin-top: 12px; }
647
- .mini-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 8px; }
648
- .mini-row span { color: var(--echo-ink-text); font-size: 14px; }
649
- .mini-row strong { font-family: var(--echo-font-brand); font-size: 16px; font-weight: 900; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; white-space: nowrap; }
650
- .doctor-stat-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 14px; }
651
- .doctor-actions { margin: 0 0 18px; }
652
- .metric { min-height: 122px; padding: 18px; display: flex; flex-direction: column; justify-content: space-between; }
653
- .label { margin: 0; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
654
- .value { display: block; font-family: var(--echo-font-brand); font-size: 34px; font-weight: 900; line-height: 1; color: var(--echo-ink-primary); }
655
- .detail { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.45; }
656
- .extract-panel { padding: 18px; }
657
- .bucket-row { align-items: center; display: grid; grid-template-columns: minmax(90px, 1fr) minmax(100px, auto); gap: 12px; }
658
- .bucket-value { text-align: right; font-weight: 800; white-space: nowrap; }
659
- .helper { margin-top: 10px; color: var(--echo-ink-mute); font-size: 14px; }
660
- .notice, .done, .warning, .error { border-radius: 8px; padding: 14px 16px; margin-bottom: 18px; }
661
- .notice, .done { border: 1px solid var(--good-line); background: var(--good-bg); color: #203820; }
662
- .warning { border: 1px solid var(--warn-line); background: var(--warn-bg); color: #5e4300; }
663
- .error { border: 1px solid var(--error-line); background: var(--error-bg); color: #7d2117; }
664
- .details { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); gap: 12px; margin-bottom: 18px; }
665
- dl { margin: 14px 0 0; display: grid; gap: 10px; }
666
- .row { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid #ecece7; padding-bottom: 8px; }
667
- dt { color: var(--echo-ink-text); }
668
- dd { margin: 0; text-align: right; color: var(--echo-ink-text); }
669
- ol { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px; }
670
- li { display: flex; justify-content: space-between; gap: 10px; }
671
- .actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
672
- button, .button {
673
- min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
674
- border-radius: 8px; border: 1px solid var(--echo-ink-primary); padding: 0 16px;
675
- font: inherit; font-weight: 800; text-decoration: none; cursor: pointer;
676
- }
677
- .primary { background: var(--echo-ink-primary); color: #fff; }
678
- .secondary { background: #fff; color: var(--echo-ink-primary); }
679
- button:disabled { cursor: not-allowed; opacity: 0.55; }
680
- .track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
681
- .fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
682
- /* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
683
- .track.indef .fill { width: 40%; transition: none; animation: prepSlide 1.2s ease-in-out infinite; }
684
- @keyframes prepSlide { 0% { transform: translateX(-110%); } 100% { transform: translateX(350%); } }
685
- .prep-steps { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
686
- .prep-steps li { display: grid; grid-template-columns: 16px 1fr; gap: 11px; align-items: center; font-size: 15px; color: var(--echo-ink-mute); }
687
- .prep-steps .dot { width: 13px; height: 13px; border-radius: 999px; border: 2px solid #cfcfc6; box-sizing: border-box; }
688
- .prep-steps li.done { color: var(--echo-ink-text); }
689
- .prep-steps li.done .dot { background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
690
- .prep-steps li.active { color: var(--echo-ink-primary); font-weight: 700; }
691
- .prep-steps li.active .dot { border-color: var(--echo-ink-primary); animation: prepDot 1.1s ease-in-out infinite; }
692
- @keyframes prepDot { 0%, 100% { box-shadow: 0 0 0 0 rgba(26,58,143,0.4); } 60% { box-shadow: 0 0 0 6px rgba(26,58,143,0); } }
693
- .counts { display: flex; gap: 14px; flex-wrap: wrap; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 13px; letter-spacing: 0.02em; margin-bottom: 6px; }
694
- .finish { display: grid; gap: 6px; place-items: center; text-align: center; padding: clamp(28px, 6vw, 64px) 0; }
695
- .finishMark { width: 54px; height: 54px; border-radius: 999px; display: grid; place-items: center; font-size: 26px; font-weight: 900; margin-bottom: 8px; }
696
- .finishMark.ok { background: var(--good-bg); color: #2f7f4f; border: 1px solid var(--good-line); }
697
- .finishMark.warn { background: var(--warn-bg); color: #5e4300; border: 1px solid var(--warn-line); }
698
- .finishBig { font-family: var(--echo-font-brand); font-size: clamp(48px, 8vw, 84px); font-weight: 900; line-height: 0.9; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; }
699
- .finishLabel { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-mute); }
700
- .finish p { max-width: 560px; margin: 12px 0 0; color: var(--echo-ink-text); font-size: 16px; line-height: 1.6; }
701
- .finish p.helper { color: var(--echo-ink-mute); font-size: 14px; margin-top: 6px; }
702
- code { font-family: var(--echo-font-mono); font-size: 0.9em; padding: 1px 5px; border-radius: 4px; background: var(--echo-paper-soft); color: var(--echo-ink-primary); }
703
- .hidden { display: none; }
704
- @media (max-width: 820px) {
705
- .heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .doctor-stat-strip,
706
- .explainHero, .insightPills, .explainSections, .explainCta { grid-template-columns: 1fr; }
707
- .doctor-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
708
- .heroCopy h2 { font-size: 34px; }
709
- .scoreCard { min-height: 220px; }
710
- .explainCopy h2 { font-size: 38px; }
711
- .explainCta .promiseActions { width: 100%; }
712
- .explainCta .primary, .explainCta .secondary { width: 100%; }
713
- .cityHero { min-height: auto; }
714
- .cityShell {
715
- width: 100vw;
716
- height: 100vh;
717
- }
718
- }
719
- /* extraction dashboard — plate layout */
720
- .exHero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.95fr); gap: clamp(20px, 3vw, 40px); align-items: center; margin-top: 8px; }
721
- /* Centered composition: mascot on top, then everything stacked and centered on one axis. */
722
- .readyDecision {
723
- width: 100%;
724
- max-width: min(720px, calc(100vw - clamp(32px, 8vw, 120px)));
725
- display: grid;
726
- justify-items: center;
727
- text-align: center;
728
- gap: 20px;
729
- margin: 0 auto;
730
- }
731
- .readyHero {
732
- display: grid;
733
- justify-items: center;
734
- gap: 16px;
735
- margin: 0 auto;
736
- }
737
- .readyMascot {
738
- width: 84px;
739
- height: 84px;
740
- object-fit: contain;
741
- filter: drop-shadow(0 12px 18px rgba(26,58,143,0.18));
742
- }
743
- .readyHero .exCopy h2 {
744
- margin: 0;
745
- font-size: clamp(32px, 4.6vw, 60px);
746
- line-height: 0.98;
747
- }
748
- .readyHero .exCopy .exSub {
749
- max-width: 560px;
750
- margin: 12px auto 0;
751
- font-size: clamp(16px, 1.45vw, 18px);
752
- }
753
- .readyAction {
754
- display: grid;
755
- grid-template-columns: minmax(0, 1fr) minmax(210px, auto);
756
- gap: clamp(18px, 3vw, 34px);
757
- align-items: end;
758
- margin-top: 2px;
759
- padding: 22px 0 24px;
760
- border-top: 1px solid var(--echo-line-soft);
761
- border-bottom: 1px solid var(--echo-line-soft);
762
- }
763
- .readyRun {
764
- display: grid;
765
- grid-template-columns: minmax(220px, 0.8fr) minmax(280px, 1.2fr);
766
- gap: clamp(18px, 3vw, 34px);
767
- align-items: start;
768
- }
769
- .exHeroReady {
770
- width: 100%;
771
- max-width: min(860px, calc(100vw - clamp(36px, 9vw, 140px)));
772
- grid-template-columns: minmax(0, 1fr);
773
- justify-content: center;
774
- justify-items: stretch;
775
- margin: clamp(8px, 2vh, 24px) auto 0;
776
- }
777
- .exHeroReady .exCopy { width: 100%; }
778
- .exHeroResult {
779
- width: 100%;
780
- max-width: min(920px, calc(100vw - clamp(36px, 8vw, 120px)));
781
- grid-template-columns: minmax(0, 760px);
782
- justify-content: center;
783
- justify-items: stretch;
784
- margin: 0 auto;
785
- }
786
- .exHeroResult .exPlate { display: none; }
787
- .exHeroResult .exCopy { width: 100%; margin: 0 auto; }
788
- .exCopy h2 { max-width: 620px; margin: 8px 0 0; font-size: clamp(34px, 4.8vw, 58px); line-height: 1.01; }
789
- .exCopy .exSub { margin: 14px 0 0; max-width: 590px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.6; color: var(--echo-ink-mute); }
790
- .exLegend { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 16px; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
791
- .exLegend .lgDot { width: 12px; height: 12px; border-radius: 3px; display: inline-block; margin-left: 10px; }
792
- .exLegend .lgDot:first-child { margin-left: 0; }
793
- .lgNew { background: #2f6df0; }
794
- .lgDim { background: #cfc8b8; }
795
- .exHero .actions { margin-top: 22px; }
796
- .readyDecision .actions {
797
- justify-content: center;
798
- margin-top: 0;
799
- }
800
- .readyDecision .actions .primary {
801
- min-width: 240px;
802
- min-height: 60px;
803
- padding: 0 30px;
804
- font-size: 16px;
805
- box-shadow: 0 16px 28px -20px rgba(26,58,143,0.55);
806
- }
807
- /* Proof strip: one quiet line of evidence (source split), not stat cards. Centered. */
808
- .proofStrip {
809
- display: flex;
810
- flex-wrap: wrap;
811
- justify-content: center;
812
- align-items: center;
813
- gap: 8px 12px;
814
- min-height: 26px;
815
- margin-top: 2px;
816
- font-family: var(--echo-font-mono);
817
- font-size: 13px;
818
- color: var(--echo-ink-mute);
819
- }
820
- .proofStrip .pf { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
821
- .proofStrip .pf strong { color: var(--echo-ink-primary); font-weight: 800; font-variant-numeric: tabular-nums; }
822
- .proofStrip .pfIcon { position: relative; width: 22px; height: 22px; border-radius: 6px; overflow: hidden; background: var(--echo-ink-primary); display: grid; place-items: center; flex: 0 0 auto; }
823
- .proofStrip .pfIcon img { width: 22px; height: 22px; object-fit: cover; display: block; }
824
- .proofStrip .pfFallback { display: none; place-items: center; width: 22px; height: 22px; color: #fff; font-size: 8px; font-weight: 900; }
825
- .proofStrip .pfDot { color: var(--echo-ink-faint); }
826
- .proofStrip .pfNote { color: var(--echo-ink-faint); font-size: 12px; }
827
- /* The action block: button first, reassurances attached right under it. Centered. */
828
- .readyGo { display: grid; gap: 11px; margin-top: 6px; justify-items: center; }
829
- .ctaMeta { margin: 0; max-width: 480px; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 4px 9px; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.5; color: var(--echo-ink-mute); }
830
- .ctaMeta .cm { white-space: nowrap; }
831
- .ctaMeta .cmDot { color: var(--echo-ink-faint); }
832
- .exHint { margin: 0; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
833
- .readyFoot {
834
- display: grid;
835
- justify-items: center;
836
- gap: 14px;
837
- margin-top: clamp(24px, 5vh, 52px);
838
- opacity: 0.82;
839
- }
840
- .readyTime {
841
- width: 100%;
842
- display: grid;
843
- gap: 7px;
844
- align-content: start;
845
- padding: 0;
846
- border: 0;
847
- background: transparent;
848
- box-shadow: none;
849
- }
850
- .readyTime span {
851
- font-family: var(--echo-font-mono);
852
- font-size: 11px;
853
- font-weight: 800;
854
- letter-spacing: 0.08em;
855
- text-transform: uppercase;
856
- color: var(--echo-ink-mute);
857
- }
858
- .readyTime strong {
859
- font-family: var(--echo-font-brand);
860
- font-size: clamp(36px, 4vw, 54px);
861
- font-weight: 900;
862
- line-height: 0.92;
863
- color: var(--echo-ink-primary);
864
- }
865
- .readyTime em {
866
- max-width: 390px;
867
- font-style: normal;
868
- color: var(--echo-ink-mute);
869
- font-size: 13px;
870
- line-height: 1.45;
871
- }
872
- .sourceGrid {
873
- display: grid;
874
- gap: 8px;
875
- margin-top: 8px;
876
- }
877
- .sourceHeading {
878
- margin: 0;
879
- font-family: var(--echo-font-mono);
880
- font-size: 11px;
881
- font-weight: 800;
882
- letter-spacing: 0.08em;
883
- text-transform: uppercase;
884
- color: var(--echo-ink-mute);
885
- }
886
- .sourceCard {
887
- display: grid;
888
- grid-template-columns: 30px minmax(0, 1fr);
889
- align-items: center;
890
- gap: 10px;
891
- border: 1px solid var(--echo-line-soft);
892
- border-radius: 8px;
893
- background: var(--echo-paper-soft);
894
- padding: 8px 11px 8px 8px;
895
- }
896
- .sourceIcon {
897
- width: 30px;
898
- height: 30px;
899
- border-radius: 7px;
900
- overflow: hidden;
901
- background: var(--echo-ink-primary);
902
- display: grid;
903
- place-items: center;
904
- flex: 0 0 auto;
905
- }
906
- .sourceIcon img { width: 30px; height: 30px; object-fit: cover; display: block; }
907
- .sourceFallback {
908
- display: none;
909
- color: #fff;
910
- font-family: var(--echo-font-mono);
911
- font-size: 9px;
912
- font-weight: 900;
913
- }
914
- .sourceCard > div {
915
- min-width: 0;
916
- display: grid;
917
- grid-template-columns: auto minmax(0, 1fr);
918
- align-items: baseline;
919
- gap: 2px 7px;
920
- }
921
- .sourceCard strong {
922
- font-family: var(--echo-font-brand);
923
- font-size: 24px;
924
- font-weight: 900;
925
- line-height: 1;
926
- color: var(--echo-ink-primary);
927
- font-variant-numeric: tabular-nums;
928
- }
929
- .sourceCard span {
930
- font-family: var(--echo-font-mono);
931
- font-size: 11px;
932
- font-weight: 800;
933
- letter-spacing: 0.04em;
934
- color: var(--echo-ink-mute);
935
- }
936
- .sourceCard small {
937
- display: block;
938
- grid-column: 1 / -1;
939
- color: var(--echo-ink-faint);
940
- font-size: 12px;
941
- line-height: 1.35;
942
- }
943
- .readyExplain {
944
- max-width: 760px;
945
- border-top: 1px solid var(--echo-line-soft);
946
- padding-top: 12px;
947
- color: var(--echo-ink-mute);
948
- }
949
- .readyExplain summary {
950
- cursor: pointer;
951
- font-family: var(--echo-font-mono);
952
- font-size: 11px;
953
- font-weight: 800;
954
- letter-spacing: 0.08em;
955
- text-transform: uppercase;
956
- color: var(--echo-ink-primary);
957
- }
958
- .readyExplain summary::marker { color: var(--echo-ink-faint); }
959
- .readyExplain p {
960
- margin: 7px 0 0;
961
- max-width: 640px;
962
- color: var(--echo-ink-mute);
963
- font-size: 14px;
964
- line-height: 1.55;
965
- }
966
- .readySteps {
967
- display: grid;
968
- grid-template-columns: repeat(3, minmax(0, 1fr));
969
- gap: 8px;
970
- margin-top: 11px;
971
- }
972
- .readyStep {
973
- border: 1px solid var(--echo-line-soft);
974
- border-radius: 8px;
975
- background: transparent;
976
- padding: 10px 11px;
977
- }
978
- .readyStep strong {
979
- display: block;
980
- font-family: var(--echo-font-mono);
981
- font-size: 10px;
982
- font-weight: 800;
983
- letter-spacing: 0.06em;
984
- text-transform: uppercase;
985
- color: var(--echo-ink-primary);
986
- }
987
- .readyStep span {
988
- display: block;
989
- margin-top: 5px;
990
- color: var(--echo-ink-faint);
991
- font-size: 12px;
992
- line-height: 1.35;
993
- }
994
- .exPlate { width: 100%; }
995
- /* WebGL clay city (echo-extraction-plate.html) — the only plate; no SVG layer */
996
- .plateStack { position: relative; width: 100%; aspect-ratio: 640 / 470; }
997
- .plateStack .plateGl { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
998
- .exHeroActive {
999
- grid-template-columns: minmax(0, 640px);
1000
- justify-content: center;
1001
- justify-items: stretch;
1002
- width: 100%;
1003
- max-width: min(920px, calc(100vw - clamp(36px, 8vw, 120px)));
1004
- margin: 0 auto;
1005
- }
1006
- .exHeroActive .exPlate { display: none; }
1007
- .exHeroActive .exCopy { width: 100%; }
1008
- .workbench { display: grid; gap: 14px; max-width: 640px; }
1009
- .workbench h2 {
1010
- max-width: 520px;
1011
- font-size: clamp(24px, 3vw, 34px);
1012
- line-height: 1.04;
1013
- }
1014
- .exMeter {
1015
- display: flex;
1016
- align-items: baseline;
1017
- justify-content: space-between;
1018
- gap: 14px;
1019
- margin-top: 10px;
1020
- padding-bottom: 8px;
1021
- border-bottom: 1px solid var(--echo-line-soft);
1022
- }
1023
- .exMeter strong {
1024
- font-family: var(--echo-font-brand);
1025
- font-size: clamp(38px, 5vw, 66px);
1026
- font-weight: 900;
1027
- line-height: 0.9;
1028
- color: var(--echo-ink-primary);
1029
- font-variant-numeric: tabular-nums;
1030
- }
1031
- .exMeter span {
1032
- font-family: var(--echo-font-mono);
1033
- font-size: 11px;
1034
- font-weight: 800;
1035
- letter-spacing: 0.07em;
1036
- text-transform: uppercase;
1037
- color: var(--echo-ink-mute);
1038
- }
1039
- .exCurrent {
1040
- display: grid;
1041
- grid-template-columns: auto 1fr;
1042
- gap: 10px;
1043
- align-items: center;
1044
- margin-top: 2px;
1045
- padding: 11px 0;
1046
- border-top: 1px solid var(--echo-line-soft);
1047
- border-bottom: 1px solid var(--echo-line-soft);
1048
- }
1049
- .exCurrent i {
1050
- width: 9px;
1051
- height: 9px;
1052
- border-radius: 999px;
1053
- background: var(--echo-ink-accent);
1054
- box-shadow: 0 0 0 4px rgba(78,205,196,0.15);
1055
- }
1056
- .exCurrent span {
1057
- font-family: var(--echo-font-mono);
1058
- font-size: 11px;
1059
- font-weight: 800;
1060
- letter-spacing: 0.07em;
1061
- text-transform: uppercase;
1062
- color: var(--echo-ink-primary);
1063
- }
1064
- .exCurrent strong {
1065
- display: block;
1066
- margin-top: 3px;
1067
- color: var(--echo-ink-text);
1068
- font-size: 14px;
1069
- line-height: 1.45;
1070
- font-weight: 700;
1071
- }
1072
- .extractFacts {
1073
- margin: 0;
1074
- font-family: var(--echo-font-mono);
1075
- font-size: 12px;
1076
- line-height: 1.5;
1077
- color: var(--echo-ink-mute);
1078
- }
1079
- .extractFacts strong {
1080
- color: var(--echo-ink-primary);
1081
- font-weight: 900;
1082
- }
1083
- .extractEndRow {
1084
- display: flex;
1085
- align-items: center;
1086
- gap: 10px;
1087
- flex-wrap: wrap;
1088
- margin-top: 2px;
1089
- }
1090
- .extractEndRow button {
1091
- min-height: 0;
1092
- border: 0;
1093
- border-radius: 0;
1094
- background: transparent;
1095
- color: var(--echo-ink-primary);
1096
- padding: 0;
1097
- font-family: var(--echo-font-mono);
1098
- font-size: 12px;
1099
- font-weight: 800;
1100
- letter-spacing: 0.04em;
1101
- text-transform: none;
1102
- }
1103
- .extractEndRow button:hover {
1104
- text-decoration: underline;
1105
- }
1106
- .extractEndRow span {
1107
- color: var(--echo-ink-faint);
1108
- font-size: 12px;
1109
- line-height: 1.45;
1110
- }
1111
- .skipStage {
1112
- display: grid;
1113
- gap: 14px;
1114
- max-width: 680px;
1115
- margin: 0 auto;
1116
- text-align: center;
1117
- justify-items: center;
1118
- }
1119
- .skipStage h2 {
1120
- max-width: 620px;
1121
- font-size: clamp(38px, 5vw, 64px);
1122
- line-height: 0.98;
1123
- }
1124
- .skipStage p {
1125
- max-width: 560px;
1126
- margin: 0;
1127
- color: var(--echo-ink-mute);
1128
- font-size: 16px;
1129
- line-height: 1.6;
1130
- }
1131
- .skipStage .seal {
1132
- width: fit-content;
1133
- border: 1px solid var(--echo-line-soft);
1134
- border-radius: 999px;
1135
- background: var(--echo-paper-white);
1136
- padding: 8px 13px;
1137
- font-family: var(--echo-font-mono);
1138
- font-size: 11px;
1139
- font-weight: 800;
1140
- letter-spacing: 0.07em;
1141
- text-transform: uppercase;
1142
- color: var(--echo-ink-primary);
1143
- }
1144
- .resultStage { display: grid; gap: 18px; max-width: 760px; padding: 8px 0 2px; }
1145
- .resultStage .kicker-mono { color: var(--echo-ink-seal); }
1146
- .resultTop {
1147
- display: grid;
1148
- gap: 12px;
1149
- align-items: start;
1150
- }
1151
- .resultTop h2 { margin-top: 6px; font-size: clamp(36px, 5vw, 58px); line-height: 0.98; }
1152
- .resultNumber {
1153
- width: fit-content;
1154
- display: inline-flex;
1155
- align-items: baseline;
1156
- gap: 8px;
1157
- border: 1px solid var(--echo-line-soft);
1158
- border-radius: 999px;
1159
- background: var(--echo-paper-white);
1160
- padding: 7px 12px;
1161
- }
1162
- .resultNumber strong {
1163
- font-family: var(--echo-font-brand);
1164
- font-size: 18px;
1165
- font-weight: 900;
1166
- line-height: 1;
1167
- color: var(--echo-ink-primary);
1168
- font-variant-numeric: tabular-nums;
1169
- }
1170
- .resultNumber span {
1171
- font-family: var(--echo-font-mono);
1172
- font-size: 10px;
1173
- font-weight: 800;
1174
- letter-spacing: 0.07em;
1175
- text-transform: uppercase;
1176
- color: var(--echo-ink-mute);
1177
- text-align: right;
1178
- }
1179
- .memoryReceipt {
1180
- display: grid;
1181
- gap: 10px;
1182
- max-width: 700px;
1183
- padding-top: 2px;
1184
- }
1185
- .memoryReceipt .receiptK {
1186
- margin: 0;
1187
- font-family: var(--echo-font-mono);
1188
- font-size: 10px;
1189
- font-weight: 800;
1190
- letter-spacing: 0.08em;
1191
- text-transform: uppercase;
1192
- color: var(--echo-ink-mute);
1193
- }
1194
- .finishStats {
1195
- display: flex;
1196
- flex-wrap: wrap;
1197
- gap: 6px 18px;
1198
- }
1199
- .finishStat {
1200
- display: inline-flex;
1201
- align-items: baseline;
1202
- gap: 6px;
1203
- border: 0;
1204
- background: transparent;
1205
- padding: 0;
1206
- }
1207
- .finishStat strong {
1208
- font-family: var(--echo-font-brand);
1209
- font-size: 17px;
1210
- font-weight: 900;
1211
- line-height: 1;
1212
- color: var(--echo-ink-primary);
1213
- font-variant-numeric: tabular-nums;
1214
- }
1215
- .finishStat span {
1216
- font-family: var(--echo-font-mono);
1217
- font-size: 10px;
1218
- font-weight: 800;
1219
- letter-spacing: 0.07em;
1220
- text-transform: uppercase;
1221
- color: var(--echo-ink-mute);
1222
- }
1223
- .retryNote {
1224
- margin: 0;
1225
- max-width: 640px;
1226
- font-size: 12px;
1227
- line-height: 1.5;
1228
- color: var(--echo-ink-faint);
1229
- }
1230
- .recon { margin-top: 0; display: inline-block; font-family: var(--echo-font-mono); font-size: 13px; font-weight: 700; color: var(--echo-ink-primary); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 8px 12px; }
1231
- .exCountdown { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
1232
- .exCountdown strong { color: var(--echo-ink-primary); font-weight: 700; }
1233
- .exOverdue { margin: 0; max-width: 620px; border: 1px solid rgba(196, 148, 42, 0.45); background: rgba(233, 185, 73, 0.12); border-radius: 8px; padding: 12px 14px; font-size: 13px; line-height: 1.5; color: var(--echo-ink-text); }
1234
- .exOverdue strong { display: block; margin-bottom: 4px; color: var(--echo-ink-primary); }
1235
- .tryCard { margin-top: 2px; max-width: 760px; border: 2px solid var(--echo-ink-primary); border-radius: 8px; background: var(--echo-paper-white); padding: clamp(18px, 3vw, 26px); box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45); }
1236
- .tryCard .tryK { margin: 0 0 10px; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
1237
- .tryCard .tryTitle {
1238
- margin: 0 0 12px;
1239
- font-family: var(--echo-font-brand);
1240
- font-size: clamp(18px, 1.9vw, 23px);
1241
- line-height: 1.15;
1242
- font-weight: 900;
1243
- color: var(--echo-ink-primary);
1244
- }
1245
- .tryCard pre { margin: 0; max-height: 72px; overflow: hidden; white-space: pre-wrap; word-break: break-word; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.5; color: var(--echo-ink-mute); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 11px 13px; -webkit-mask-image: linear-gradient(to bottom, #000 52%, transparent); mask-image: linear-gradient(to bottom, #000 52%, transparent); }
1246
- .agentLaunch {
1247
- display: grid;
1248
- grid-template-columns: repeat(3, minmax(0, 1fr));
1249
- gap: 10px;
1250
- margin-top: 18px;
1251
- }
1252
- .agentLaunch button {
1253
- min-height: 72px;
1254
- justify-content: flex-start;
1255
- border-color: var(--echo-line-soft);
1256
- background: var(--echo-paper-white);
1257
- color: var(--echo-ink-primary);
1258
- padding: 10px 14px;
1259
- text-align: left;
1260
- }
1261
- .agentLaunch button:hover { border-color: var(--echo-ink-primary); }
1262
- .agentIcon {
1263
- width: 46px;
1264
- height: 46px;
1265
- display: grid;
1266
- place-items: center;
1267
- flex: 0 0 auto;
1268
- border-radius: 11px;
1269
- background: var(--echo-ink-primary);
1270
- color: #fff;
1271
- font-family: var(--echo-font-mono);
1272
- font-size: 11px;
1273
- font-weight: 900;
1274
- letter-spacing: 0;
1275
- overflow: hidden;
1276
- box-shadow: 0 8px 16px -10px rgba(20,20,20,0.45);
1277
- }
1278
- .agentIcon img {
1279
- width: 100%;
1280
- height: 100%;
1281
- object-fit: cover;
1282
- display: block;
1283
- }
1284
- .agentIcon .agentFallback {
1285
- width: 100%;
1286
- height: 100%;
1287
- display: none;
1288
- place-items: center;
1289
- }
1290
- .copyGlyph {
1291
- position: relative;
1292
- width: 21px;
1293
- height: 25px;
1294
- display: block;
1295
- }
1296
- .copyGlyph::before,
1297
- .copyGlyph::after {
1298
- content: "";
1299
- position: absolute;
1300
- width: 15px;
1301
- height: 18px;
1302
- border: 2px solid #fff;
1303
- border-radius: 4px;
1304
- background: var(--echo-ink-primary);
1305
- }
1306
- .copyGlyph::before { left: 0; top: 0; opacity: 0.72; }
1307
- .copyGlyph::after { right: 0; bottom: 0; }
1308
- .agentText {
1309
- display: block;
1310
- min-width: 0;
1311
- }
1312
- .agentLaunch strong {
1313
- display: block;
1314
- font-family: var(--echo-font-brand);
1315
- font-size: 17px;
1316
- font-weight: 900;
1317
- line-height: 1.1;
1318
- }
1319
- .agentText small {
1320
- display: block;
1321
- margin-top: 2px;
1322
- color: var(--echo-ink-mute);
1323
- font-size: 11px;
1324
- line-height: 1.3;
1325
- font-weight: 600;
1326
- }
1327
- .launchStatus { margin: 9px 0 0; min-height: 18px; font-size: 12px; color: var(--echo-ink-mute); }
1328
- .hudTip { margin: 14px 0 0; max-width: 700px; display: grid; grid-template-columns: 52px 1fr; gap: 12px; align-items: center; font-size: 13px; line-height: 1.55; color: var(--echo-ink-mute); }
1329
- .hudTip img { width: 52px; height: 52px; object-fit: contain; filter: drop-shadow(0 10px 16px rgba(26,58,143,0.18)); }
1330
- .hudTip strong { color: var(--echo-ink-primary); }
1331
- .hudTip code { font-family: var(--echo-font-mono); font-size: 12px; background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 5px; padding: 1px 5px; }
1332
- .doneCloseNote { margin: clamp(30px, 5vh, 58px) 0 0; max-width: 700px; font-family: var(--echo-font-mono); font-size: 11px; line-height: 1.5; color: var(--echo-ink-faint); }
1333
-
1334
- /* ===== Calm redesign: one focal point per state, progressive disclosure ===== */
1335
- .calm { max-width: 640px; }
1336
- /* Ready: a single compact meta strip replaces the giant ETA box + source cards. */
1337
- .readyMeta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 20px 0 2px; font-family: var(--echo-font-mono); font-size: 12.5px; color: var(--echo-ink-mute); }
1338
- .readyMeta .rm { display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px; border: 1px solid var(--echo-line-soft); border-radius: 999px; background: var(--echo-paper-soft); white-space: nowrap; }
1339
- .readyMeta .rm strong { color: var(--echo-ink-primary); font-weight: 800; }
1340
- .readyMeta .rm img { width: 15px; height: 15px; border-radius: 4px; object-fit: contain; }
1341
- .readyMeta .rm .sf { display: none; width: 15px; height: 15px; border-radius: 4px; background: var(--echo-ink-primary); color: #fff; font-size: 8px; font-weight: 800; align-items: center; justify-content: center; }
1342
- /* Done: the number is the hero; a small ceremonial tick, one supporting line, no duplicate receipt. */
1343
- .doneMark { display: inline-flex; align-items: center; gap: 9px; margin: 0; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; color: var(--echo-ink-accent-deep); }
1344
- .doneMark .tick { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: var(--echo-ink-accent-deep); color: #fff; font-size: 13px; animation: donePop 0.5s cubic-bezier(0.2, 1.5, 0.4, 1) both; }
1345
- @keyframes donePop { from { transform: scale(0); } to { transform: scale(1); } }
1346
- .doneNum { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 14px; margin: 14px 0 0; }
1347
- .doneNum strong { font-family: var(--echo-font-brand); font-size: clamp(52px, 9vw, 96px); line-height: 0.9; color: var(--echo-ink-primary); letter-spacing: -0.01em; }
1348
- .doneNum span { font-size: clamp(15px, 1.6vw, 19px); font-weight: 700; color: var(--echo-ink-mute); }
1349
- .doneSub { margin: 16px 0 0; max-width: 560px; font-size: clamp(15px, 1.4vw, 17px); line-height: 1.5; color: var(--echo-ink-text); }
1350
- /* Collapse the long warm-up prompt behind a disclosure so the buttons lead. */
1351
- .promptPeek { margin: 14px 0 0; }
1352
- .promptPeek summary { cursor: pointer; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); list-style: none; }
1353
- .promptPeek summary::-webkit-details-marker { display: none; }
1354
- .promptPeek summary::before { content: "▸ "; color: var(--echo-ink-faint); }
1355
- .promptPeek[open] summary::before { content: "▾ "; }
1356
- .promptPeek pre { margin: 10px 0 0; white-space: pre-wrap; word-break: break-word; font-family: var(--echo-font-mono); font-size: 12px; line-height: 1.6; color: var(--echo-ink-mute); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 12px 14px; }
1357
-
1358
- @media (max-width: 820px) {
1359
- .exHero, .resultTop, .finishStats, .agentLaunch, .sourceGrid, .readySteps, .readyRun, .readyAction { grid-template-columns: 1fr; }
1360
- .readyHero { grid-template-columns: 1fr; }
1361
- .readyMascot { width: 76px; height: 76px; }
1362
- .readyDecision .actions { justify-content: stretch; }
1363
- .readyDecision .actions .primary { width: 100%; }
1364
- .resultNumber span { text-align: left; }
1365
- }
1366
-
1367
- /* ===== workspace readout dashboard (Part 2 — replaces explainPage) ===== */
1368
- @keyframes rdoRise { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
1369
- @keyframes rdoGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
1370
- @keyframes rdoDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
1371
- @keyframes rdoWipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
1372
- body.cityMode .detailsWrap { background: linear-gradient(180deg, rgba(245,241,231,0.92), rgba(255,255,255,0.96)), var(--echo-paper-canvas); }
1373
- .echo-rdo { width: min(1280px, 100%); margin: 0 auto; color: var(--echo-ink-text); font-family: var(--echo-font-body); }
1374
- .echo-rdo .rise { opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) forwards; animation-delay: var(--d, 0ms); }
1375
- .echo-rdo .rdo-head { border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 20px; margin-bottom: 22px; }
1376
- .echo-rdo .rdo-kicker { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
1377
- .echo-rdo .rdo-h1 { margin: 8px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(28px, 4vw, 46px); line-height: 1.02; letter-spacing: -0.01em; color: var(--echo-ink-primary); }
1378
- .echo-rdo .rdo-lead { margin: 12px 0 0; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.55; color: var(--echo-ink-text); max-width: 56ch; }
1379
- .echo-rdo .rdo-lead b { color: var(--echo-ink-primary); }
1380
- .echo-rdo .rdo-meta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; }
1381
- .echo-rdo .rdo-chip { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--echo-line-soft); border-radius: 999px; background: var(--echo-paper-white); padding: 6px 13px; font-family: var(--echo-font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-mute); }
1382
- .echo-rdo .rdo-chip b { color: var(--echo-ink-primary); }
1383
- .echo-rdo .rdo-chip .live { width: 7px; height: 7px; border-radius: 50%; background: var(--echo-ink-accent); }
1384
- .echo-rdo .rdo-card { position: relative; background: var(--echo-paper-white); border: 1px solid var(--echo-line-soft); border-radius: 10px; padding: 18px; transition: box-shadow 0.2s var(--rdo-ease), transform 0.2s var(--rdo-ease); }
1385
- .echo-rdo .rdo-card:hover { box-shadow: 0 14px 30px -16px rgba(26,58,143,0.28); transform: translateY(-2px); }
1386
- .echo-rdo .clab { margin: 0; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--echo-ink-mute); }
1387
- .echo-rdo .csub { margin-top: 4px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
1388
- .echo-rdo .csub b { color: var(--echo-ink-primary); }
1389
- .echo-rdo .rdo-help { position: absolute; top: 13px; right: 13px; width: 24px; height: 24px; min-height: 0; box-sizing: border-box; border-radius: 999px; border: 1px solid var(--echo-line-soft); background: var(--echo-paper-soft); color: var(--echo-ink-faint); font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; cursor: pointer; display: grid; place-items: center; padding: 0; line-height: 1; transition: all 0.15s ease; }
1390
- .echo-rdo .rdo-help:hover { color: #fff; background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
1391
- .echo-rdo .rdo-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 14px; }
1392
- .echo-rdo .rdo-kpi { min-height: 132px; display: flex; flex-direction: column; justify-content: flex-end; }
1393
- .echo-rdo .rdo-kpi .num { font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(34px, 4vw, 50px); line-height: 0.9; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; }
1394
- .echo-rdo .rdo-kpi.hero { border-color: rgba(199,55,47,0.30); }
1395
- .echo-rdo .rdo-kpi.hero::before { content: ""; position: absolute; left: 0; top: 14px; bottom: 14px; width: 3px; border-radius: 3px; background: var(--echo-ink-seal); }
1396
- .echo-rdo .rdo-kpi.hero .num { color: var(--echo-ink-seal); }
1397
- .echo-rdo .rdo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
1398
- .echo-rdo .figbox { margin-top: 16px; position: relative; }
1399
- .echo-rdo .rdo-histo { display: flex; align-items: flex-end; gap: 3px; height: 132px; border-bottom: 2px solid var(--echo-line-soft); }
1400
- .echo-rdo .rdo-histo .bar { flex: 1; align-self: flex-end; min-height: 3px; border-radius: 3px 3px 0 0; background: var(--echo-ink-primary); transform: scaleY(0); transform-origin: bottom; animation: rdoGrow 0.5s var(--rdo-ease) forwards; animation-delay: var(--bd, 0ms); }
1401
- .echo-rdo .rdo-histo .bar.night { background: var(--echo-night); }
1402
- .echo-rdo .rdo-histo .bar:hover { filter: brightness(1.12); }
1403
- .echo-rdo .rdo-axis { display: flex; justify-content: space-between; margin-top: 8px; font-family: var(--echo-font-mono); font-size: 10px; color: var(--echo-ink-faint); }
1404
- .echo-rdo .rdo-legend { display: flex; flex-wrap: wrap; gap: 7px 16px; margin-top: 13px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
1405
- .echo-rdo .rdo-legend i { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
1406
- .echo-rdo .terr { display: grid; grid-template-columns: 150px 1fr; gap: 22px; align-items: center; margin-top: 16px; }
1407
- .echo-rdo .donut { width: 144px; height: 144px; position: relative; margin: 0 auto; }
1408
- .echo-rdo .donut .donutSvg { width: 100%; height: 100%; display: block; opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) 0.2s forwards; }
1409
- .echo-rdo .donut .seg { transition: opacity 0.15s ease, stroke-width 0.15s ease; }
1410
- .echo-rdo .donut.hovering .seg { opacity: 0.3; }
1411
- .echo-rdo .donut.hovering .seg.on { opacity: 1; stroke-width: 7.5; }
1412
- .echo-rdo .donut .seg { cursor: pointer; }
1413
- .echo-rdo .donut-tip, .echo-rdo .histo-tip { position: absolute; left: 0; top: 0; pointer-events: none; transform: translate(-50%, -132%); background: var(--echo-ink-text); color: #fff; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; padding: 5px 9px; border-radius: 6px; white-space: nowrap; z-index: 6; opacity: 0; transition: opacity 0.12s ease; box-shadow: 0 6px 16px rgba(28,38,60,0.2); }
1414
- .echo-rdo .donut-tip.show, .echo-rdo .histo-tip.show { opacity: 1; }
1415
- .echo-rdo .donut-tip::after, .echo-rdo .histo-tip::after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--echo-ink-text); }
1416
- .echo-rdo .donut .center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; pointer-events: none; }
1417
- .echo-rdo .donut .center b { display: block; font-family: var(--echo-font-brand); font-weight: 900; font-size: 28px; color: var(--echo-ink-primary); line-height: 0.9; }
1418
- .echo-rdo .donut .center span { font-family: var(--echo-font-mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-mute); }
1419
- .echo-rdo .rrow { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: baseline; padding: 7px 0; }
1420
- .echo-rdo .rrow .nm { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1421
- .echo-rdo .rrow .vl { font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-text); white-space: nowrap; font-variant-numeric: tabular-nums; }
1422
- .echo-rdo .rbar { grid-column: 1 / -1; height: 5px; border-radius: 999px; background: var(--echo-paper-soft); margin-top: 3px; overflow: hidden; }
1423
- .echo-rdo .rbar i { display: block; height: 100%; border-radius: 999px; transform-origin: left; transform: scaleX(0); animation: rdoDraw 0.6s var(--rdo-ease) forwards; animation-delay: var(--rd, 0ms); }
1424
- .echo-rdo .lblrow { font-family: var(--echo-font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--echo-ink-mute); margin: 16px 0 7px; }
1425
- .echo-rdo .stack { display: flex; height: 28px; border-radius: 7px; overflow: hidden; background: var(--echo-paper-soft); }
1426
- .echo-rdo .stack .seg { min-width: 0; display: grid; place-items: center; font-family: var(--echo-font-mono); font-size: 10px; font-weight: 800; }
1427
- .echo-rdo .ledgerCols { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
1428
- .echo-rdo .ex-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline; padding: 11px 0; border-top: 1px solid var(--echo-line-soft); }
1429
- .echo-rdo .ex-row:first-of-type { border-top: 0; padding-top: 16px; }
1430
- .echo-rdo .collapse-inner .ex-row:first-of-type { border-top: 1px solid var(--echo-line-soft); padding-top: 11px; }
1431
- .echo-rdo .ex-file { font-family: var(--echo-font-mono); font-size: 12.5px; font-weight: 700; color: var(--echo-ink-primary); }
1432
- .echo-rdo .ex-meta { margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
1433
- .echo-rdo .ex-reads { font-family: var(--echo-font-brand); font-weight: 900; font-size: 20px; color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap; }
1434
- .echo-rdo .rdo-tag { display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px; background: rgba(199,55,47,0.08); font-family: var(--echo-font-mono); font-size: 9px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal); }
1435
- .echo-rdo .collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.34s var(--rdo-ease); }
1436
- .echo-rdo .collapse.open { grid-template-rows: 1fr; }
1437
- .echo-rdo .collapse-inner { overflow: hidden; min-height: 0; }
1438
- .echo-rdo .moreBtn { display: inline-block; min-height: 0; margin-top: 14px; background: none; border: 0; padding: 6px 0; cursor: pointer; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--echo-ink-primary); }
1439
- .echo-rdo .moreBtn:hover { opacity: 0.65; }
1440
- .echo-rdo .mrow2 { display: grid; grid-template-columns: 1fr auto auto; gap: 14px; align-items: baseline; padding: 10px 0; border-top: 1px solid var(--echo-line-soft); font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
1441
- .echo-rdo .mrow2:first-child { border-top: 0; }
1442
- .echo-rdo .mrow2 b { font-size: 13px; color: var(--echo-ink-primary); }
1443
- .echo-rdo .mrow2 strong { font-family: var(--echo-font-brand); font-size: 16px; color: var(--echo-ink-text); }
1444
- .echo-rdo .outcome { margin-top: 14px; border: 1px solid rgba(78,205,196,0.45); background: linear-gradient(180deg, rgba(78,205,196,0.10), rgba(78,205,196,0.04)); border-radius: 12px; padding: clamp(20px, 3vw, 30px); display: grid; grid-template-columns: 1.5fr 1fr; gap: clamp(22px, 3vw, 44px); align-items: center; }
1445
- .echo-rdo .outcome .rdo-kicker { color: var(--echo-ink-accent-deep); }
1446
- .echo-rdo .outcome h2 { margin: 9px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(22px, 2.7vw, 33px); line-height: 1.05; letter-spacing: -0.01em; color: var(--echo-ink-primary); }
1447
- .echo-rdo .outcome .rdo-lead { margin-top: 12px; max-width: 46ch; }
1448
- .echo-rdo .targetbar { position: relative; margin-top: 36px; }
1449
- .echo-rdo .tb-track { position: relative; height: 34px; border-radius: 999px; background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); overflow: hidden; }
1450
- .echo-rdo .tb-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #d6473e, var(--echo-ink-seal)); display: flex; align-items: center; padding: 0 14px; color: #fff; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; white-space: nowrap; animation: rdoWipe 0.85s var(--rdo-ease) 0.2s both; }
1451
- .echo-rdo .tb-goal { position: absolute; top: 0; bottom: 0; width: 2px; background: rgba(255,255,255,0.9); border-radius: 2px; }
1452
- .echo-rdo .tb-flag { position: absolute; top: -29px; transform: translateX(-50%); background: var(--echo-ink-accent-deep); color: #fff; font-family: var(--echo-font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 9px; border-radius: 6px; white-space: nowrap; box-shadow: 0 3px 8px rgba(47,157,147,0.3); }
1453
- .echo-rdo .tb-flag::after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--echo-ink-accent-deep); }
1454
- .echo-rdo .oc-targets { background: var(--echo-paper-white); border: 1px solid var(--echo-line-soft); border-radius: 10px; padding: 18px; }
1455
- .echo-rdo .ot-title { margin: 0 0 6px; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--echo-ink-mute); }
1456
- .echo-rdo .ot-note { margin: -2px 0 10px; font-size: 12px; line-height: 1.45; color: var(--echo-ink-mute); }
1457
- .echo-rdo .ot-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--echo-line-soft); }
1458
- .echo-rdo .ot-row:last-of-type { border-bottom: 0; }
1459
- .echo-rdo .ot-row .otk { display: flex; flex-direction: column; gap: 2px; }
1460
- .echo-rdo .ot-row .k { font-size: 13px; color: var(--echo-ink-text); }
1461
- .echo-rdo .ot-row .ks { font-family: var(--echo-font-mono); font-size: 10px; color: var(--echo-ink-faint); }
1462
- .echo-rdo .ot-row .v { font-family: var(--echo-font-brand); font-weight: 900; font-size: 22px; color: var(--echo-ink-accent-deep); font-variant-numeric: tabular-nums; white-space: nowrap; }
1463
- .echo-rdo .rdo-cta { margin-top: 16px; width: 100%; min-height: 46px; border-radius: 8px; border: 1px solid var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; cursor: pointer; transition: filter 0.15s ease; }
1464
- .echo-rdo .rdo-cta:hover { filter: brightness(1.12); }
1465
- .echo-rdo .rdo-foot { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--echo-line-soft); display: flex; flex-wrap: wrap; gap: 12px 24px; justify-content: space-between; font-family: var(--echo-font-mono); font-size: 11px; line-height: 1.6; color: var(--echo-ink-faint); }
1466
- .echo-rdo .rdo-foot code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
1467
- .echo-rdo .rdo-foot .priv { color: var(--echo-ink-accent-deep); font-weight: 700; }
1468
- @media (max-width: 860px) { .echo-rdo .rdo-kpis { grid-template-columns: repeat(2, 1fr); } .echo-rdo .rdo-grid { grid-template-columns: 1fr; } .echo-rdo .outcome { grid-template-columns: 1fr; } }
1469
- @media (max-width: 620px) { .echo-rdo .ledgerCols, .echo-rdo .terr { grid-template-columns: 1fr; } }
1470
- @media (prefers-reduced-motion: reduce) { .echo-rdo .rise, .echo-rdo .rdo-histo .bar, .echo-rdo .donut .donutSvg, .echo-rdo .rbar i, .echo-rdo .tb-fill { animation: none !important; opacity: 1 !important; transform: none !important; translate: none !important; clip-path: none !important; } }
1471
- /* annotation dialog */
1472
- .echo-note-dlg { border: 1px solid var(--echo-line-soft); border-radius: 14px; padding: 0; width: min(440px, calc(100vw - 32px)); background: var(--echo-paper-white); color: var(--echo-ink-text); box-shadow: 0 28px 60px -18px rgba(26,32,46,0.4); }
1473
- .echo-note-dlg[open] { animation: rdoRise 0.24s var(--rdo-ease); }
1474
- .echo-note-dlg::backdrop { background: rgba(26,32,46,0.4); }
1475
- .echo-note-dlg .nk, .echo-note-dlg .nt, .echo-note-dlg .nb { padding-left: 26px; padding-right: 26px; }
1476
- .echo-note-dlg .nk { margin: 24px 0 0; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-accent-deep); }
1477
- .echo-note-dlg .nt { margin: 6px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: 24px; line-height: 1.1; color: var(--echo-ink-primary); }
1478
- .echo-note-dlg .nb { padding-top: 14px; padding-bottom: 26px; }
1479
- .echo-note-dlg .nb p { margin: 0 0 12px; font-size: 14px; line-height: 1.6; }
1480
- .echo-note-dlg .nb p:last-child { margin-bottom: 0; }
1481
- .echo-note-dlg .nb .echo { margin-top: 14px; padding: 12px 14px; border-radius: 8px; background: rgba(78,205,196,0.10); border: 1px solid rgba(78,205,196,0.3); font-size: 13px; line-height: 1.55; }
1482
- .echo-note-dlg .nb .echo b { color: var(--echo-ink-primary); }
1483
- .echo-note-dlg .nb code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
1484
- .echo-note-dlg .nclose { position: absolute; top: 13px; right: 14px; width: 30px; height: 30px; border-radius: 999px; border: 1px solid var(--echo-line-soft); background: var(--echo-paper-white); color: var(--echo-ink-mute); font-size: 18px; cursor: pointer; display: grid; place-items: center; }
1485
- .echo-note-dlg .nclose:hover { color: var(--echo-ink-seal); border-color: var(--echo-ink-seal); }
1486
- </style>
1487
- </head>
1488
- <body>
1489
- <main>
1490
- <header>
1491
- <div>
1492
- <p class="kicker">EchoMem device setup</p>
1493
- <h1 id="title">Turn history into memory</h1>
1494
- </div>
1495
- <div class="pill" id="status">Local</div>
1496
- </header>
1497
- <section id="app" class="state">Starting local scan...</section>
1498
- </main>
1499
- <dialog id="echo-note" class="echo-note-dlg">
1500
- <button class="nclose" aria-label="Close">&times;</button>
1501
- <p class="nk" id="echo-note-k"></p>
1502
- <h2 class="nt" id="echo-note-t"></h2>
1503
- <div class="nb" id="echo-note-b"></div>
1504
- </dialog>
1505
- <script>
1506
- (function () {
1507
- var params = new URLSearchParams(location.search);
1508
- var nonce = params.get("nonce") || "";
1509
- var app = document.getElementById("app");
1510
- var title = document.getElementById("title");
1511
- var status = document.getElementById("status");
1512
- var report = null;
1513
- var stats = null;
1514
- var statsSlow = false;
1515
- var decisionMade = false;
1516
- var connected = false;
1517
- var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
1518
- var exStartAt = 0; // extraction start (ms) — drives the countdown
1519
- var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
1520
- var extractionEnded = false; // user chose to leave the extraction flow before natural completion
1521
- var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
1522
- var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
1523
- var authUrl = "";
1524
- var workspacePath = "";
1525
- var authWindow = null;
1526
- var connectionPollStarted = false;
1527
- var statsPollStarted = false;
1528
- var reportPollStarted = false;
1529
- var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
1530
-
1531
- function esc(value) {
1532
- return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
1533
- return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
1534
- });
1535
- }
1536
- function setHead(nextTitle, nextStatus) {
1537
- title.textContent = nextTitle;
1538
- status.textContent = nextStatus;
1539
- }
1540
- function setCityMode(enabled) {
1541
- document.body.classList.toggle("cityMode", !!enabled);
1542
- if (enabled) document.body.classList.remove("extractMode", "readyMode");
1543
- }
1544
- function setExtractMode(enabled) {
1545
- document.body.classList.toggle("extractMode", !!enabled);
1546
- if (enabled) document.body.classList.remove("readyMode");
1547
- }
1548
- function setReadyMode(enabled) {
1549
- document.body.classList.toggle("readyMode", !!enabled);
1550
- if (enabled) document.body.classList.remove("cityMode", "extractMode");
1551
- }
1552
- function number(value) {
1553
- return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
1554
- }
1555
- function countOrChecking(value) {
1556
- return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "checking";
1557
- }
1558
- function compact(value) {
1559
- if (typeof value !== "number" || !isFinite(value)) return "-";
1560
- var abs = Math.abs(value);
1561
- if (abs >= 1e9) return (value / 1e9).toFixed(1) + "B";
1562
- if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
1563
- if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
1564
- return String(Math.round(value));
1565
- }
1566
- function big(value) {
1567
- if (typeof value !== "number" || !isFinite(value)) return "-";
1568
- var n = Math.abs(value);
1569
- if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
1570
- if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
1571
- if (n >= 1e3) return Math.round(value / 1e3) + "K";
1572
- return String(Math.round(value));
1573
- }
1574
- function money(value) {
1575
- return "$" + number(value);
1576
- }
1577
- function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
1578
- async function fetchWithTimeout(url, options, timeoutMs) {
1579
- var controller = new AbortController();
1580
- var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
1581
- try {
1582
- return await fetch(url, Object.assign({}, options || {}, { signal: controller.signal }));
1583
- } finally {
1584
- clearTimeout(timer);
1585
- }
1586
- }
1587
- async function getJson(path) {
1588
- var sep = path.indexOf("?") >= 0 ? "&" : "?";
1589
- var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
1590
- if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
1591
- return await res.json();
1592
- }
1593
- async function postJson(path, body, timeoutMs) {
1594
- var options = {
1595
- method: "POST",
1596
- credentials: "omit",
1597
- headers: { "Content-Type": "application/json" },
1598
- body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
1599
- };
1600
- var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
1601
- var raw = await res.text();
1602
- var data = {};
1603
- try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
1604
- if (!res.ok) throw new Error(data.message || data.error || raw || ("HTTP " + res.status));
1605
- return data;
1606
- }
1607
- function metric(label, value, detail) {
1608
- return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
1609
- }
1610
- function row(label, value) {
1611
- return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
1612
- }
1613
- function migrationBuckets(migratable) {
1614
- var eta = migratable && migratable.eta ? migratable.eta : {};
1615
- return Array.isArray(migratable && migratable.buckets) ? migratable.buckets : (Array.isArray(eta.buckets) ? eta.buckets : []);
1616
- }
1617
- function bucketCount(buckets, key) {
1618
- var found = buckets.filter(function (b) { return b && b.key === key; })[0];
1619
- return found && typeof found.count === "number" ? found.count : 0;
1620
- }
1621
- function bucketList(buckets) {
1622
- var visible = buckets.filter(function (b) { return b && (b.count || b.chars); });
1623
- if (!visible.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
1624
- return '<ol>' + visible.map(function (b) {
1625
- return '<li class="bucket-row"><span>' + esc(b.label) + '</span><span class="bucket-value">' + esc(number(b.count)) + ' · ' + esc(compact(b.chars)) + '</span></li>';
1626
- }).join("") + '</ol>';
1627
- }
1628
- function routeError(code) {
1629
- if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
1630
- if (code === "FORBIDDEN_SCOPE") return "This device token cannot import history. Re-connect this device to mint an import-capable token.";
1631
- if (code === "VAULT_LOCKED") return "Your local vault is locked. Run echomem-mcp unlock, then retry.";
1632
- if (code === "IMPORT_START_TIMEOUT") return "Starting the import timed out. Retry when the connection settles.";
1633
- if (code === "NOT_LOGGED_IN") return "The local bridge is not logged in yet. Sign in again and retry.";
1634
- return code || "Something went wrong. Retry in a moment.";
1635
- }
1636
- function closeAuthWindow() {
1637
- try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
1638
- authWindow = null;
1639
- }
1640
- function notifyOpenerConnected() {
1641
- try {
1642
- if (window.opener && window.opener !== window && !window.opener.closed) {
1643
- window.opener.postMessage({ type: "echomem:connected", nonce: nonce }, location.origin);
1644
- setTimeout(function () { window.close(); }, 250);
1645
- return true;
1646
- }
1647
- } catch (_) {}
1648
- return false;
1649
- }
1650
- function openAuthWindow(url) {
1651
- var targetUrl = url || authUrl;
1652
- if (!targetUrl) return false;
1653
- try {
1654
- authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
1655
- if (!authWindow) return false;
1656
- authWindow.focus();
1657
- return true;
1658
- } catch (_) { return false; }
1659
- }
1660
- function showPopupFallback(message, url) {
1661
- Array.prototype.forEach.call(document.querySelectorAll("[data-signin-help]"), function (hint) {
1662
- hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
1663
- });
1664
- Array.prototype.forEach.call(document.querySelectorAll("[data-signin-fallback]"), function (fallback) {
1665
- fallback.setAttribute("href", url || authUrl);
1666
- fallback.classList.remove("hidden");
1667
- });
1668
- }
1669
- function onConnectClick() {
1670
- if (connected) { void waitForStats(); return; }
1671
- if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
1672
- startConnectionPoll();
1673
- }
1674
- function bindConnect() {
1675
- Array.prototype.forEach.call(document.querySelectorAll("[data-connect-echo]"), function (signin) {
1676
- signin.onclick = onConnectClick;
1677
- });
1678
- }
1679
- function bindDetailsArrow() {
1680
- var arrow = document.getElementById("details-arrow");
1681
- var target = document.getElementById("setup-details");
1682
- if (arrow && target) {
1683
- arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
1684
- }
1685
- }
1686
-
1687
- /* ---------- report shell (city iframe owns its own loading) ---------- */
1688
- // The report page (full-width city hero + detail sections below) is mounted ONCE. The city iframe
1689
- // shows its OWN in-canvas loading state while it scans, then fades into the rendered report — one
1690
- // surface, no separate loading page, no reload. The parent only fills the detail sections on /report.
1691
- function mountReportShell() {
1692
- setCityMode(true);
1693
- app.className = "";
1694
- app.innerHTML =
1695
- '<section class="cityHero"><div class="cityShell">' +
1696
- '<iframe title="AI Coding City" src="/city/echo-ai-city-only.html?setup=1&nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
1697
- '<button id="details-arrow" class="cityArrow hidden" aria-label="Show detailed report">&#8595;</button>' +
1698
- '</div></section>' +
1699
- '<div id="setup-details" class="detailsWrap"></div>';
1700
- bindDetailsArrow();
1701
- reportMounted = true;
1702
- }
1703
- function renderLoading() {
1704
- if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
1705
- setCityMode(true);
1706
- setHead("Turn history into memory", "Reading");
1707
- }
1708
-
1709
- /* ---------- forensic report (pic 1/2) ---------- */
1710
- function histogram(hours) {
1711
- var list = Array.isArray(hours) ? hours : [];
1712
- var max = Math.max.apply(null, [1].concat(list));
1713
- var bars = list.map(function (v, h) {
1714
- var height = Math.max(2, Math.round((v / max) * 100));
1715
- return '<span class="histoBar' + (NIGHT_HOURS[h] ? ' night' : '') + '" style="height:' + height + '%" title="' + h + ':00 — ' + number(v) + ' prompts"></span>';
1716
- }).join("");
1717
- return '<div class="histo">' + bars + '</div><div class="histoAxis"><span>12a</span><span>6a</span><span>12p</span><span>6p</span><span>11p</span></div>';
1718
- }
1719
- function rereadRows(files) {
1720
- return (files || []).slice(0, 6).map(function (f) {
1721
- var meta = esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + (f.nightReads ? ' · ' + number(f.nightReads) + ' at night' : '');
1722
- var tag = f.everChanged ? '' : '<span class="tag">never changed</span>';
1723
- return '<div class="row2"><div><b>' + esc(f.file) + '</b>' + tag + '<small>' + meta + '</small></div><strong>' + number(f.totalReads) + '&times;</strong></div>';
1724
- }).join("");
1725
- }
1726
- function repoRows(repos) {
1727
- return (repos || []).slice(0, 5).map(function (r) {
1728
- var meta = number(r.sessions) + ' sessions · ' + number(r.activeAttentionDays) + ' active days · ' + (r.dailyFocusHours || 0) + 'h/day · ' + big(r.tokens) + ' tokens';
1729
- return '<div class="row2 repoRow"><div><b style="font-family:var(--echo-font-brand);font-size:18px">' + esc(r.name) + '</b><small>' + esc(meta) + '</small></div><strong style="color:var(--echo-ink-primary)">' + (r.attentionHours || 0) + 'h</strong></div>';
1730
- }).join("");
1731
- }
1732
- function costSplit(cost) {
1733
- var total = cost.total || 1;
1734
- function seg(cls, v) { return '<span class="' + cls + '" style="width:' + Math.max(0, (v / total) * 100) + '%"></span>'; }
1735
- var byModel = cost.byModel || {};
1736
- var rows = Object.keys(byModel).map(function (k) { return [k, byModel[k]]; })
1737
- .sort(function (a, b) { return b[1].total - a[1].total; })
1738
- .map(function (e) { return '<div class="modelRow"><b>' + esc(e[0]) + '</b><span>' + big(e[1].total) + ' tok</span><strong>' + money(e[1].cost) + '</strong></div>'; })
1739
- .join("");
1740
- return '<div class="segBar">' + seg("seg-output", cost.outputCost) + seg("seg-cold", cost.coldInputCost) + seg("seg-cw", cost.cacheWriteCost) + seg("seg-cr", cost.cacheReadCost) + '</div>' +
1741
- '<div class="costLegend"><span>output ' + money(cost.outputCost) + '</span><span>cold input ' + money(cost.coldInputCost) + '</span><span>cache write ' + money(cost.cacheWriteCost) + '</span><span>cache read ' + money(cost.cacheReadCost) + '</span></div>' +
1742
- '<div style="margin-top:14px">' + rows + '</div>';
1743
- }
1744
- /* ---------- workspace readout dashboard (Part 2) ---------- */
1745
- var RDO_NIGHT = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
1746
- var RDO_PIE = ["#1a3a8f", "#4ecdc4", "#c7372f", "#7a86c2", "#d29a3f", "#7d9a6d", "#cf6f4e"]; // major-repo colors; "#b3aea0" (taupe) is reserved for "Others" only
1747
- var rdoReduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
1748
- function rdoDec(v) { return typeof v === "number" && isFinite(v) ? (Math.round(v * 10) / 10) : "-"; }
1749
- function rdoFmt(v, k) { if (k === "pct") return number(v) + "%"; if (k === "hours") return number(v) + "h"; if (k === "money") return money(v); if (k === "big") return big(v); return number(v); }
1750
- function rdoHourLabel(h) { var x = h % 24, ap = x < 12 ? "am" : "pm", hr = x % 12; if (hr === 0) hr = 12; return hr + ap; }
1751
- function rdoHisto(hours) {
1752
- var list = Array.isArray(hours) ? hours : [], n = list.length || 1, max = Math.max.apply(null, [1].concat(list));
1753
- var bars = list.map(function (v, h) {
1754
- var ht = Math.max(3, Math.round((v / max) * 100)), hour = Math.round(h * (24 / n));
1755
- return '<div class="bar' + (RDO_NIGHT[hour] ? ' night' : '') + '" style="height:' + ht + '%;--bd:' + (300 + h * 14) + 'ms" data-tip="' + rdoHourLabel(hour) + ' &middot; ' + number(v) + ' prompts"></div>';
1756
- }).join("");
1757
- return '<div class="figbox"><div class="rdo-histo">' + bars + '</div><div class="rdo-axis"><span>12am</span><span>6am</span><span>12pm</span><span>6pm</span><span>11pm</span></div>' +
1758
- '<div class="rdo-legend"><span><i style="background:var(--echo-ink-primary)"></i>daytime</span><span><i style="background:var(--echo-night)"></i>late night (10pm&ndash;4am)</span></div><div class="histo-tip"></div></div>';
1759
- }
1760
- function rdoTerritory(repos) {
1761
- var list = (repos || []).slice().sort(function (a, b) { return (b.attentionHours || 0) - (a.attentionHours || 0); });
1762
- if (!list.length) return '<div class="terr"><div class="donut"></div><div></div></div>';
1763
- var total = list.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0) || 1;
1764
- var maxH = Math.max.apply(null, [1].concat(list.map(function (r) { return r.attentionHours || 0; })));
1765
- var threshold = total * 0.05; // repos over 5% shown individually; the rest combine into "Others"
1766
- var major = list.filter(function (r) { return (r.attentionHours || 0) > threshold; });
1767
- var minor = list.filter(function (r) { return (r.attentionHours || 0) <= threshold; });
1768
- var minorH = minor.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0);
1769
- var slices = major.map(function (r, i) { return { i: i, name: r.name, hours: (r.attentionHours || 0), col: RDO_PIE[i % RDO_PIE.length] }; });
1770
- if (minorH > 0) slices.push({ i: major.length, name: "Others (" + number(minor.length) + (minor.length === 1 ? " repo)" : " repos)"), hours: minorH, col: "#b3aea0" });
1771
- var c0 = 0;
1772
- var segs = slices.map(function (sl) {
1773
- var pct = sl.hours / total * 100;
1774
- var seg = '<circle class="seg" data-i="' + sl.i + '" data-name="' + esc(sl.name) + '" data-pct="' + Math.round(pct) + '%" cx="21" cy="21" r="15.915" fill="none" stroke="' + sl.col + '" stroke-width="6" stroke-dasharray="' + pct.toFixed(2) + ' ' + (100 - pct).toFixed(2) + '" stroke-dashoffset="' + (25 - c0).toFixed(2) + '"></circle>';
1775
- c0 += pct; return seg;
1776
- }).join("");
1777
- var donut = '<div class="donut"><svg viewBox="0 0 42 42" class="donutSvg">' + segs + '</svg>' +
1778
- '<div class="center"><div><b>' + number(list.length) + '</b><span>repos</span></div></div><div class="donut-tip"></div></div>';
1779
- var rows = slices.map(function (sl) {
1780
- var pct = Math.round(sl.hours / total * 100), w = Math.round(sl.hours / maxH * 100);
1781
- return '<div class="rrow" data-i="' + sl.i + '" data-name="' + esc(sl.name) + '" data-pct="' + pct + '%"><span class="nm" style="color:' + sl.col + '">' + esc(sl.name) + '</span><span class="vl">' + rdoDec(sl.hours) + 'h &middot; ' + pct + '%</span>' +
1782
- '<span class="rbar"><i style="width:' + w + '%;background:' + sl.col + '"></i></span></div>';
1783
- }).join("");
1784
- return '<div class="terr">' + donut + '<div>' + rows + '</div></div>';
1785
- }
1786
- function rdoTokenStack(ip) {
1787
- ip = Math.max(0, Math.min(100, ip || 0)); var op = 100 - ip;
1788
- return '<div class="stack"><div class="seg" style="width:' + ip + '%;background:var(--echo-ink-primary);color:#fff">' + ip + '% in</div><div class="seg" style="width:' + op + '%;background:var(--echo-ink-accent);color:#0c3b37">' + op + '%</div></div>' +
1789
- '<div class="rdo-legend"><span><i style="background:var(--echo-ink-primary)"></i>context loaded in</span><span><i style="background:var(--echo-ink-accent)"></i>answers written out</span></div>';
1790
- }
1791
- function rdoCostStack(c) {
1792
- var t = c.total || 1; function s(col, v) { return '<div class="seg" style="width:' + Math.max(0, (v / t) * 100) + '%;background:' + col + '"></div>'; }
1793
- return '<div class="stack">' + s("var(--echo-ink-seal)", c.cacheReadCost) + s("var(--echo-ink-mute)", c.coldInputCost) + s("var(--echo-night)", c.cacheWriteCost) + s("var(--echo-ink-accent)", c.outputCost) + '</div>' +
1794
- '<div class="rdo-legend"><span><i style="background:var(--echo-ink-seal)"></i>cache read ' + money(c.cacheReadCost) + '</span><span><i style="background:var(--echo-ink-mute)"></i>cold input ' + money(c.coldInputCost) + '</span>' +
1795
- '<span><i style="background:var(--echo-night)"></i>cache write ' + money(c.cacheWriteCost) + '</span><span><i style="background:var(--echo-ink-accent)"></i>output ' + money(c.outputCost) + '</span></div>';
1796
- }
1797
- function rdoModelRows(byModel) {
1798
- return Object.keys(byModel || {}).map(function (k) { return [k, byModel[k]]; }).sort(function (a, b) { return b[1].cost - a[1].cost; })
1799
- .map(function (e) { return '<div class="mrow2"><b>' + esc(e[0]) + '</b><span>' + big(e[1].total) + ' tok</span><strong>' + money(e[1].cost) + '</strong></div>'; }).join("");
1800
- }
1801
- function rdoEvidence(files) {
1802
- var list = files || [];
1803
- function rowHtml(f) {
1804
- var tag = f.everChanged ? '' : '<span class="rdo-tag">never changed</span>';
1805
- return '<div class="ex-row"><div><div class="ex-file">' + esc(f.file) + '</div><div class="ex-meta">' + esc(f.repo) + ' &middot; ' + number(f.sessionsTouched) + ' sessions &middot; ' + number(f.daysTouched) + ' days' + tag + '</div></div><div class="ex-reads">' + number(f.totalReads) + '&times;</div></div>';
1806
- }
1807
- var firstN = 5;
1808
- var head = list.slice(0, firstN).map(rowHtml).join("");
1809
- var rest = list.slice(firstN).map(rowHtml).join("");
1810
- var moreBtn = list.length > firstN ? '<button class="moreBtn" data-rdo-toggle>Show all ' + number(list.length) + ' files &darr;</button>' : '';
1811
- return head + '<div class="collapse"><div class="collapse-inner">' + rest + '</div></div>' + moreBtn;
1812
- }
1813
- function rdoNotes() {
1814
- var r = report || {}, s = r.scale || {}, c = r.cost || {}, cl = r.cleanliness || {}, rh = r.rhythm || {}, uwh = r.userWorkingHours || {};
1815
- return {
1816
- "repeated-reads": { k: "Cleanliness", t: "Repeated reads", b: '<p>' + number(cl.staleReadSharePct) + '% of file reads reopened a file already seen <b>unchanged</b> &mdash; ' + number(cl.staleRereads) + ' stale rereads across ' + number(s.repoCount) + ' repos.</p><p>Every reopen re-pays the cost of loading that file, though nothing changed. It is the biggest source of avoidable token spend.</p><div class="echo"><b>What Echo does:</b> remembers each file so your agent recalls it instead of reading it again.</div>' },
1817
- "agent-attention": { k: "Activity", t: "Agent attention", b: '<p>' + number(uwh.userAttentionHours) + ' hours of active agent work across the scan. ' + rdoDec(uwh.averageDailyAttentionHours) + 'h/day average, peaking at ' + rdoDec(uwh.maxDailyAttentionHours) + 'h.</p>' },
1818
- "tokens-moved": { k: "Scale", t: "Tokens moved", b: '<p>' + big(s.totalTokens) + ' tokens read and written. ' + number(s.inputPct) + '% was <b>input</b> &mdash; context fed in, not output generated.</p>' },
1819
- "api-value": { k: "Cost", t: "API-equivalent", b: '<p>' + money(c.total) + ' at API list prices &mdash; a measure of scale, not a charge. Subscriptions were not billed this.</p>' },
1820
- "when-you-work": { k: "Rhythm", t: "When you work", b: '<p>Prompts by hour of day. The soft-indigo bars are late-night hours (10pm&ndash;4am); ' + number(rh.lateNightShare) + '% of your prompts land there.</p>' },
1821
- "time-by-repo": { k: "Focus", t: "Where your time goes", b: '<p>Agent attention hours split by repository &mdash; where your coding actually happened.</p>' },
1822
- "cost-went": { k: "Cost", t: "Where the tokens & cost go", b: '<p>Most volume is input &mdash; reloading context each turn. In cost, <b>cache read</b> + <b>cold input</b> are re-fed context.</p><div class="echo">That re-fed-context tax shrinks when agents recall instead of re-read.</div>' },
1823
- "rereads": { k: "Cleanliness", t: "Most re-read files", b: '<p>Files your agents kept reopening after seeing them unchanged. A <code>never changed</code> tag means many reads, zero edits.</p>' }
1824
- };
1825
- }
1826
- function rdoOpenNote(key) {
1827
- var d = rdoNotes()[key]; if (!d) return; var dlg = document.getElementById("echo-note"); if (!dlg) return;
1828
- document.getElementById("echo-note-k").textContent = d.k;
1829
- document.getElementById("echo-note-t").textContent = d.t;
1830
- document.getElementById("echo-note-b").innerHTML = d.b;
1831
- if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
1832
- }
1833
- function rdoCountUp(el) {
1834
- var target = parseFloat(el.getAttribute("data-count")), kind = el.getAttribute("data-fmt");
1835
- if (rdoReduce || !isFinite(target)) { el.textContent = rdoFmt(target, kind); return; }
1836
- var dur = 950, start = null;
1837
- function tick(now) { if (start === null) start = now; var t = Math.min(1, (now - start) / dur), e = 1 - Math.pow(1 - t, 3); el.textContent = rdoFmt(target * e, kind); if (t < 1) requestAnimationFrame(tick); else el.textContent = rdoFmt(target, kind); }
1838
- requestAnimationFrame(tick);
1839
- }
1840
- function rdoKpi(cls, value, kind, label, sub, key) {
1841
- return '<div class="rdo-card rdo-kpi ' + cls + '"><button class="rdo-help" data-info="' + key + '">?</button>' +
1842
- '<div class="num" data-count="' + value + '" data-fmt="' + kind + '">' + rdoFmt(value, kind) + '</div>' +
1843
- '<p class="clab" style="margin-top:11px">' + esc(label) + '</p><div class="csub">' + sub + '</div></div>';
1844
- }
1845
- function rdoWire() {
1846
- var root = document.getElementById("setup-details"); if (!root) return;
1847
- Array.prototype.forEach.call(root.querySelectorAll("[data-count]"), function (el, i) { setTimeout(function () { rdoCountUp(el); }, rdoReduce ? 0 : 260 + i * 60); });
1848
- Array.prototype.forEach.call(root.querySelectorAll(".rdo-help"), function (el) { el.onclick = function () { rdoOpenNote(el.getAttribute("data-info")); }; });
1849
- var dlg = document.getElementById("echo-note");
1850
- if (dlg) { var cl = dlg.querySelector(".nclose"); if (cl) cl.onclick = function () { dlg.close(); }; dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); }; }
1851
- Array.prototype.forEach.call(root.querySelectorAll("[data-rdo-toggle]"), function (mb) {
1852
- var box = mb.previousElementSibling, tx = mb.textContent;
1853
- if (!box || !box.classList.contains("collapse")) return;
1854
- mb.onclick = function () { var o = !box.classList.contains("open"); box.classList.toggle("open", o); mb.textContent = o ? "Show less ↑" : tx; };
1855
- });
1856
- var donut = root.querySelector(".terr .donut");
1857
- if (donut) {
1858
- var segEls = donut.querySelectorAll(".seg");
1859
- var tip = donut.querySelector(".donut-tip");
1860
- function placeTip(e) { var rc = donut.getBoundingClientRect(); if (tip) { tip.style.left = (e.clientX - rc.left) + "px"; tip.style.top = (e.clientY - rc.top) + "px"; } }
1861
- Array.prototype.forEach.call(segEls, function (s) {
1862
- s.addEventListener("mouseenter", function (e) {
1863
- donut.classList.add("hovering");
1864
- Array.prototype.forEach.call(segEls, function (x) { x.classList.toggle("on", x === s); });
1865
- if (tip) { tip.textContent = s.getAttribute("data-name") + " · " + s.getAttribute("data-pct"); tip.classList.add("show"); placeTip(e); }
1866
- });
1867
- s.addEventListener("mousemove", placeTip);
1868
- s.addEventListener("mouseleave", function () {
1869
- donut.classList.remove("hovering");
1870
- s.classList.remove("on");
1871
- if (tip) tip.classList.remove("show");
1872
- });
1873
- });
1874
- }
1875
- var histo = root.querySelector(".rdo-histo");
1876
- if (histo) {
1877
- var figbox = histo.closest(".figbox");
1878
- var htip = figbox ? figbox.querySelector(".histo-tip") : null;
1879
- function placeHTip(e) { if (!figbox || !htip) return; var rc = figbox.getBoundingClientRect(); htip.style.left = (e.clientX - rc.left) + "px"; htip.style.top = (e.clientY - rc.top) + "px"; }
1880
- Array.prototype.forEach.call(histo.querySelectorAll(".bar"), function (b) {
1881
- b.addEventListener("mouseenter", function (e) { if (htip) { htip.textContent = b.getAttribute("data-tip"); htip.classList.add("show"); placeHTip(e); } });
1882
- b.addEventListener("mousemove", placeHTip);
1883
- b.addEventListener("mouseleave", function () { if (htip) htip.classList.remove("show"); });
1884
- });
1885
- }
1886
- }
1887
- function renderForensicReport(message) {
1888
- var r = report;
1889
- if (!r) { showReport(); return; }
1890
- if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
1891
- setCityMode(true);
1892
- var s = r.scale || {}, c = r.cost || {}, mu = r.modelUsage || {}, uwh = r.userWorkingHours || {}, rh = r.rhythm || {}, cl = r.cleanliness || {}, rf = r.rereadForensics || {}, aw = r.avoidableWait || {}, cf = r.counterfactual || {};
1893
- var night = rf.nightExample;
1894
- setHead("AI Coding City", connected ? "Signed in" : "Local");
1895
- // The city iframe is already mounted by the shell and shows its own loading; just reveal the
1896
- // details arrow and fill the sections, so the city never reloads from loading to full report.
1897
- var dArrow = document.getElementById("details-arrow");
1898
- if (dArrow) dArrow.classList.remove("hidden");
1899
- var files = rf.topFiles || [], repos = r.repos || [];
1900
- var topRepoHours = repos.reduce(function (t, x) { return t + (x.attentionHours || 0); }, 0);
1901
- var weeks = (new Date(r.scanEndDate) - new Date(r.scanStartDate)) / (7 * 86400000);
1902
- if (!isFinite(weeks) || weeks < 1) weeks = 1;
1903
- var sharePct = cl.staleReadSharePct || 0;
1904
- var savedCost = Math.round((c.total / weeks) * (sharePct / 100));
1905
- var savedTokens = (s.totalTokens / weeks) * (sharePct / 100);
1906
- var weeklyRereads = Math.round((cl.staleRereads || 0) / weeks);
1907
- var targetShare = 10;
1908
- document.getElementById("setup-details").innerHTML =
1909
- '<div class="echo-rdo">' +
1910
- (message ? '<div class="notice" style="margin-bottom:18px">' + esc(message) + '</div>' : '') +
1911
- '<div class="rdo-head rise" style="--d:0ms">' +
1912
- '<p class="rdo-kicker">EchoMem &middot; Local Workspace Scan</p>' +
1913
- '<h2 class="rdo-h1">Where your agents spent their attention</h2>' +
1914
- '<p class="rdo-lead">A local scan of ' + number(s.sessionCount) + ' coding sessions across ' + number(s.repoCount) + ' repositories. Most of the load wasn&#39;t new thinking &mdash; it was <b>reloading context</b> your agents had already seen.</p>' +
1915
- '<div class="rdo-meta"><span class="rdo-chip"><span class="live"></span>Filed locally</span><span class="rdo-chip"><b>' + number(s.sessionCount) + '</b> sessions</span><span class="rdo-chip"><b>' + number(s.repoCount) + '</b> repos</span><span class="rdo-chip">' + esc(r.scanStartDate) + ' &ndash; ' + esc(r.scanEndDate) + '</span></div>' +
1916
- '</div>' +
1917
- '<div class="rdo-kpis">' +
1918
- '<div class="rise" style="--d:60ms">' + rdoKpi("hero", cl.staleReadSharePct, "pct", "repeated reads", number(cl.staleRereads) + ' stale rereads', "repeated-reads") + '</div>' +
1919
- '<div class="rise" style="--d:110ms">' + rdoKpi("", uwh.userAttentionHours, "hours", "agent attention", rdoDec(uwh.averageDailyAttentionHours) + 'h/day avg', "agent-attention") + '</div>' +
1920
- '<div class="rise" style="--d:160ms">' + rdoKpi("", s.totalTokens, "big", "tokens moved", number(s.inputPct) + '% input', "tokens-moved") + '</div>' +
1921
- '<div class="rise" style="--d:210ms">' + rdoKpi("", c.total, "money", "API-equivalent", 'list price, not billed', "api-value") + '</div>' +
1922
- '</div>' +
1923
- '<div class="rdo-grid">' +
1924
- '<div class="rdo-card rise" style="--d:260ms"><button class="rdo-help" data-info="when-you-work">?</button><p class="clab">When you work</p><div class="csub"><b>' + number(rh.lateNightShare) + '%</b> after 10pm &middot; <b>' + rdoDec(uwh.averageDailyAttentionHours) + 'h</b> avg active day</div>' + rdoHisto(rh.hourHistogram) + '</div>' +
1925
- '<div class="rdo-card rise" style="--d:310ms"><button class="rdo-help" data-info="cost-went">?</button><p class="clab">Where the tokens &amp; cost go</p><div class="csub"><b>' + number(s.inputPct) + '%</b> input &middot; <b>' + money(c.total) + '</b> API-equivalent</div>' +
1926
- '<div class="ledgerCols"><div><div class="lblrow">Tokens &mdash; in vs. out</div>' + rdoTokenStack(s.inputPct) + '</div><div><div class="lblrow">Cost by token type</div>' + rdoCostStack(c) + '</div></div>' +
1927
- '<div class="collapse"><div class="collapse-inner" style="padding-top:6px">' + rdoModelRows(c.byModel) + '</div></div>' +
1928
- '<button class="moreBtn" data-rdo-toggle>Break down by model &darr;</button>' +
1929
- '</div>' +
1930
- '<div class="rdo-card rise" style="--d:360ms"><button class="rdo-help" data-info="time-by-repo">?</button><p class="clab">Where your time goes</p><div class="csub"><b>' + rdoDec(topRepoHours) + 'h</b> across ' + number(repos.length) + ' repos</div>' + rdoTerritory(repos) + '</div>' +
1931
- '<div class="rdo-card rise" style="--d:410ms"><button class="rdo-help" data-info="rereads">?</button><p class="clab">Most re-read files</p><div class="csub">top 5 of <b>' + number(files.length) + '</b> by total reads</div>' + rdoEvidence(files) + '</div>' +
1932
- '</div>' +
1933
- '<section class="outcome rise" style="--d:460ms">' +
1934
- '<div class="oc-copy">' +
1935
- '<p class="rdo-kicker">What changes next week</p>' +
1936
- '<h2>Echo remembers, so your agents stop re-reading.</h2>' +
1937
- '<p class="rdo-lead" data-signin-help>Right now ' + number(sharePct) + '% of reads are repeats. As Echo fills in, your agents <b>recall</b> what they have already seen instead of reloading it &mdash; and it compounds every session.</p>' +
1938
- '<div class="targetbar"><div class="tb-track"><div class="tb-fill" style="width:' + sharePct + '%">' + number(sharePct) + '% repeats today</div><div class="tb-goal" style="left:' + targetShare + '%"></div></div><div class="tb-flag" style="left:' + targetShare + '%">target &le; ' + targetShare + '%</div></div>' +
1939
- '</div>' +
1940
- '<div class="oc-targets">' +
1941
- '<p class="ot-title">Next week&rsquo;s target</p>' +
1942
- '<p class="ot-note">What Echo saves you in a week, once it recalls instead of re-reading:</p>' +
1943
- '<div class="ot-row"><div class="otk"><span class="k">Cost saved</span><span class="ks">API list-price value</span></div><span class="v" data-count="' + savedCost + '" data-fmt="money">' + money(savedCost) + '</span></div>' +
1944
- '<div class="ot-row"><div class="otk"><span class="k">Tokens saved</span><span class="ks">context not reloaded</span></div><span class="v" data-count="' + savedTokens + '" data-fmt="big">' + big(savedTokens) + '</span></div>' +
1945
- '<div class="ot-row"><div class="otk"><span class="k">Re-reads avoided</span><span class="ks">unchanged files reopened</span></div><span class="v" data-count="' + weeklyRereads + '" data-fmt="int">' + number(weeklyRereads) + '</span></div>' +
1946
- '<button class="rdo-cta" data-connect-echo>' + (connected ? "Continue &rarr;" : "Connect Echo &rarr;") + '</button>' +
1947
- '</div>' +
1948
- '</section>' +
1949
- '<div class="rdo-foot rise" style="--d:520ms">' +
1950
- '<span>Compiled locally from <code>~/.codex/sessions</code> and <code>~/.claude/projects</code> &middot; API-equivalent list pricing, not a subscription bill.</span>' +
1951
- '<span class="priv">No transcripts leave this machine.</span>' +
1952
- '</div>' +
1953
- '</div>';
1954
- rdoWire();
1955
- bindConnect();
1956
- bindDetailsArrow();
1957
- }
1958
-
1959
- /* ---------- dashboard (post-auth extraction) ---------- */
1960
- function statusStrip(title, detail, kind) {
1961
- return '<div class="status-strip ' + esc(kind || "") + '"><span class="dot"></span><div><strong>' + esc(title) + '</strong><p>' + esc(detail) + '</p></div></div>';
1962
- }
1963
- function renderBridgeIssue(error) {
1964
- setCityMode(false);
1965
- setHead("Local bridge issue", "Retrying");
1966
- app.className = "loading-panel";
1967
- app.innerHTML =
1968
- '<div style="padding:24px"><h2>Still waiting for the local bridge</h2>' +
1969
- '<p class="helper">Keep the terminal running. If it already exited or printed an error, close this tab and run echomem-mcp setup again.</p>' +
1970
- (error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') + '</div>';
1971
- }
1972
- function renderLogoutPending() {
1973
- setCityMode(false);
1974
- setHead("Signing out locally", "Working");
1975
- app.className = "loading-panel";
1976
- app.innerHTML = '<div style="padding:24px"><h2>Clearing this device token</h2><p class="helper">If the bridge is scanning local files, this can take a few seconds to answer.</p></div>';
1977
- }
1978
- function renderLogoutIssue(error) {
1979
- setCityMode(false);
1980
- setHead("Sign out did not finish", "Needs attention");
1981
- app.className = "loading-panel";
1982
- app.innerHTML =
1983
- '<div style="padding:24px"><h2>The local bridge did not answer</h2>' +
1984
- '<p class="helper">Keep the terminal open and retry. If the command exited, close this tab and run setup again.</p>' +
1985
- (error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') +
1986
- '<div class="actions"><button id="logout" class="secondary">Retry sign out</button></div></div>';
1987
- var lo = document.getElementById("logout"); if (lo) lo.onclick = localLogout;
1988
- }
1989
- async function waitForConnection() {
1990
- if (connectionPollStarted) return;
1991
- connectionPollStarted = true;
1992
- try {
1993
- for (;;) {
1994
- if (decisionMade) return;
1995
- try {
1996
- var cfg = await getJson("/config");
1997
- authUrl = cfg.authUrl || authUrl;
1998
- if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
1999
- } catch (_) {}
2000
- await sleep(900);
2001
- }
2002
- } finally { connectionPollStarted = false; }
2003
- }
2004
- function startConnectionPoll() { void waitForConnection(); }
2005
- async function waitForStats() {
2006
- if (statsPollStarted) return;
2007
- statsPollStarted = true;
2008
- // Use the same one-reading-line stage as the partial scan state. This avoids a jarring
2009
- // old setup shell between device approval and the ready decision.
2010
- renderDashboard();
2011
- try {
2012
- var started = Date.now();
2013
- var lastError = "";
2014
- for (;;) {
2015
- if (decisionMade) return;
2016
- try {
2017
- var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
2018
- if (res.status === 200) {
2019
- stats = await res.json();
2020
- if (decisionMade) return;
2021
- renderDashboard();
2022
- if (stats && stats.partial) { await sleep(1200); continue; }
2023
- return;
2024
- }
2025
- if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
2026
- } catch (e) {
2027
- lastError = e && e.message ? e.message : String(e);
2028
- if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
2029
- if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
2030
- }
2031
- await sleep(statsSlow ? 1500 : 500);
2032
- }
2033
- } finally { statsPollStarted = false; }
2034
- }
2035
- // The plate slot: ONLY the WebGL clay city (echo-extraction-plate.html). It self-fetches /report
2036
- // (your repos) and polls /progress (extraction state), growing as memories are created. The old SVG
2037
- // fallback board was removed — it was bleeding through behind the transparent canvas (the double board).
2038
- function plateBlock() {
2039
- return '<div class="exPlate"><div class="plateStack">' +
2040
- '<iframe class="plateGl" title="Extraction plate" src="/city/echo-extraction-plate.html?nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
2041
- '</div></div>';
2042
- }
2043
- function renderDashboard(error) {
2044
- setCityMode(false);
2045
- setExtractMode(false);
2046
- setReadyMode(true);
2047
- var migratable = stats && stats.migratable ? stats.migratable : {};
2048
- var pending = stats && stats.migratable ? stats.migratable.pending : null;
2049
- var hasPendingCount = typeof pending === "number";
2050
- var discovery = stats && stats.discovery ? stats.discovery : {};
2051
- var isPartial = !!(stats && stats.partial);
2052
- var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
2053
- var eta = migratable && migratable.eta ? migratable.eta : {};
2054
- var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
2055
- var sessions = stats && stats.sessions ? stats.sessions : {};
2056
- // The "quick" phase counts only this device's local ledger; it does not know what the account
2057
- // already imported elsewhere, so it over-counts (e.g. 10). The count is only trustworthy after the
2058
- // account check (phase "account"/"exact"/"full"), which corrects it (e.g. 3). Until then we show a
2059
- // "counting" state with no number, so the user never sees the count jump down.
2060
- var pendingTrusted = hasPendingCount && !!discovery.phase && discovery.phase !== "quick";
2061
- var knownDone = pendingTrusted && pending === 0;
2062
- var canExtract = pendingTrusted && pending > 0;
2063
- var pendN = hasPendingCount ? pending : 0;
2064
- var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
2065
- var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
2066
- var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
2067
- setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
2068
- var headline = !pendingTrusted
2069
- ? "Counting your new conversations…"
2070
- : (knownDone ? "You are all caught up." : "Echo found " + number(pendN) + " new coding session" + (pendN === 1 ? "" : "s") + ".");
2071
- var sub = !pendingTrusted
2072
- ? "Echo is matching your local history against what is already in memory."
2073
- : (knownDone
2074
- ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
2075
- : "Turn them into memory your agents can recall: decisions, fixes, and where you left off. No more re-explaining your project every session.");
2076
- var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start extraction" : "All caught up");
2077
- // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
2078
- // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
2079
- // Later polls only patch the text/counts in place; the iframe stays mounted.
2080
- var live = dashMounted && document.getElementById("exHeadline");
2081
- if (!live) {
2082
- extractMounted = false;
2083
- app.className = "";
2084
- // One reading line, top to bottom: the find (headline) → the value (sub) → quiet proof
2085
- // (source strip) → the single action with its reassurances attached → optional detail.
2086
- // The 19/9 split is EVIDENCE ("Echo scanned MY machine"), not data to study — so it is a
2087
- // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
2088
- // not a display-size headline.
2089
- app.innerHTML =
2090
- '<div id="exBanner"></div>' +
2091
- '<section class="readyDecision">' +
2092
- '<div class="readyHero">' +
2093
- '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
2094
- '<div class="exCopy">' +
2095
- '<h2 id="exHeadline"></h2>' +
2096
- '<p class="exSub" id="exSub"></p>' +
2097
- '</div>' +
2098
- '</div>' +
2099
- '<div class="proofStrip" id="exSources"></div>' +
2100
- '<div class="readyGo">' +
2101
- '<div class="actions">' +
2102
- '<button id="migrate" class="primary"></button>' +
2103
- '</div>' +
2104
- '<p class="ctaMeta" id="exEta"></p>' +
2105
- '</div>' +
2106
- '<div class="readyFoot">' +
2107
- '<details class="readyExplain" id="readyExplain"></details>' +
2108
- '</div>' +
2109
- '</section>';
2110
- document.getElementById("migrate").onclick = startMigrate;
2111
- dashMounted = true;
2112
- }
2113
- document.getElementById("exBanner").innerHTML =
2114
- (statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
2115
- (error ? '<div class="error">' + esc(error) + '</div>' : '');
2116
- document.getElementById("exHeadline").textContent = headline;
2117
- document.getElementById("exSub").textContent = sub;
2118
- var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
2119
- var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
2120
- var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
2121
- // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
2122
- var srcIcon = function (id, fallback) {
2123
- return '<span class="pfIcon"><img src="/agent-icons/' + id + '.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="pfFallback">' + fallback + '</span></span>';
2124
- };
2125
- document.getElementById("exSources").innerHTML = !pendingTrusted
2126
- ? '<span class="pfNote">Scanning local history&hellip;</span>'
2127
- : (canExtract
2128
- ? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
2129
- '<span class="pfDot">&middot;</span>' +
2130
- '<span class="pf">' + srcIcon("claude-desktop", "CL") + '<strong>' + esc(number(claudeN)) + '</strong> Claude Code</span>' +
2131
- '<span class="pfNote">found on this Mac</span>'
2132
- : "");
2133
- // Reassurances live at the moment of commitment — right under the button.
2134
- document.getElementById("exEta").innerHTML = pendingTrusted
2135
- ? (canExtract
2136
- ? '<span class="cm">&asymp; ' + esc(etaLabel) + '</span><span class="cmDot">&middot;</span><span class="cm">runs locally</span>' + (alreadyN ? '<span class="cmDot">&middot;</span><span class="cm">' + esc(number(alreadyN)) + ' already-saved skipped</span>' : '')
2137
- : 'Nothing new to extract right now.')
2138
- : 'Echo is checking local history against saved memory&hellip;';
2139
- document.getElementById("readyExplain").innerHTML =
2140
- '<summary>What exactly happens?</summary>' +
2141
- '<p>A coding session is a local Codex or Claude Code transcript from this Mac. Echo extracts compact memories from it, not a full chat replay.</p>' +
2142
- '<div class="readySteps">' +
2143
- '<div class="readyStep"><strong>Read locally</strong><span>Echo scans session logs on this device.</span></div>' +
2144
- '<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
2145
- '<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
2146
- '</div>';
2147
- // While still counting, hide the primary button entirely; the headline + source split
2148
- // already say it is working, and the ready state should present one clear action.
2149
- var migrateBtn = document.getElementById("migrate");
2150
- if (pendingTrusted) {
2151
- migrateBtn.style.display = "";
2152
- migrateBtn.disabled = false;
2153
- migrateBtn.textContent = canExtract ? extractLabel : "Finish setup";
2154
- migrateBtn.onclick = canExtract ? startMigrate : skip;
2155
- } else {
2156
- migrateBtn.style.display = "none";
2157
- }
2158
- }
2159
- async function startMigrate() {
2160
- if (decisionMade) return;
2161
- decisionMade = true;
2162
- extractionEnded = false;
2163
- setCityMode(false);
2164
- setHead("Extracting", "Starting");
2165
- app.className = "";
2166
- app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically. Finished work is saved as it goes.</p><div class="miniBar"><i></i></div></div>';
2167
- try {
2168
- var started = await postJson("/migrate", {});
2169
- extractMounted = false;
2170
- renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
2171
- await pollProgress();
2172
- } catch (e) {
2173
- decisionMade = false;
2174
- renderDashboard(routeError(e && e.message));
2175
- }
2176
- }
2177
- async function pollProgress() {
2178
- for (;;) {
2179
- if (extractionEnded) return;
2180
- var progress = await getJson("/progress");
2181
- if (extractionEnded) return;
2182
- renderProgress(progress);
2183
- if (progress.status === "completed" || progress.status === "failed") return;
2184
- await sleep(1200);
2185
- }
2186
- }
2187
- function renderProgress(progress) {
2188
- if (extractionEnded) return;
2189
- setCityMode(false);
2190
- setExtractMode(true);
2191
- var status = progress.status;
2192
- var done = status === "completed";
2193
- var stopped = status === "failed";
2194
- var total = progress.total || progress.jobCount || 0;
2195
- var completed = progress.completed || 0;
2196
- var failed = progress.failed || 0;
2197
- var extracted = progress.extracted || 0;
2198
- var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
2199
- var preparing = (status === "starting" || status === "preparing");
2200
- app.className = "";
2201
- // Mount the shell ONCE so the WebGL plate iframe survives every poll (it self-polls /progress);
2202
- // each poll only rebuilds the text column.
2203
- if (!extractMounted) {
2204
- app.innerHTML =
2205
- '<section class="exHero"><div class="exCopy" id="exText"></div>' +
2206
- plateBlock() +
2207
- '</section>';
2208
- extractMounted = true;
2209
- }
2210
- var shell = app.querySelector(".exHero");
2211
- if (shell) {
2212
- shell.classList.toggle("exHeroResult", done || stopped);
2213
- shell.classList.toggle("exHeroActive", !done && !stopped);
2214
- }
2215
- var t = document.getElementById("exText");
2216
- if (!t) return;
2217
- if (done || stopped) {
2218
- setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
2219
- var memoryNoun = extracted === 1 ? "new memory" : "new memories";
2220
- var conversationNoun = completed === 1 ? "conversation" : "conversations";
2221
- var stopDetail = String(progress.error) === "key-expired"
2222
- ? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
2223
- : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
2224
- t.innerHTML =
2225
- '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
2226
- (done ? "" : '<p class="kicker-mono">Extraction stopped</p>') +
2227
- '<div class="resultTop">' +
2228
- '<div>' +
2229
- '<h2>' + (done ? "Memory is live." : "Some conversations need another pass.") + '</h2>' +
2230
- (done ? "" : '<p class="exSub">' + stopDetail + '</p>') +
2231
- '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>' +
2232
- '</div>' +
2233
- '</div>' +
2234
- (done ? tryItCard() : "") +
2235
- '<div class="memoryReceipt">' +
2236
- '<p class="receiptK">What just happened</p>' +
2237
- '<div class="finishStats">' +
2238
- '<div class="finishStat"><strong>' + esc(number(completed)) + '</strong><span>' + esc(conversationNoun) + ' extracted</span></div>' +
2239
- '<div class="finishStat"><strong>' + esc(number(extracted)) + '</strong><span>memories ready</span></div>' +
2240
- '</div>' +
2241
- '</div>' +
2242
- '</div>' +
2243
- (done ? hudTip() + doneCloseNote() : "");
2244
- var copyBtn = document.getElementById("tryCopy");
2245
- if (copyBtn) copyBtn.addEventListener("click", copyPromptOnly);
2246
- var codexBtn = document.getElementById("tryCodex");
2247
- if (codexBtn) codexBtn.addEventListener("click", tryInCodex);
2248
- var claudeBtn = document.getElementById("tryClaudeDesktop");
2249
- if (claudeBtn) claudeBtn.addEventListener("click", openClaudeDesktop);
2250
- return;
2251
- }
2252
- setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
2253
- var checkingEdgeCases = failed > 0 && !preparing;
2254
- var statusLine = checkingEdgeCases
2255
- ? "Finishing a few checks without stopping the import."
2256
- : (progress.latest ? (progress.latest + (progress.latestRepo ? " · " + progress.latestRepo : "")) : (preparing ? "Getting your sessions ready locally…" : "Importing the next batch…"));
2257
- // Honest countdown: first ETA from batch size (throughput is rate-capped ≈ 26/min), then
2258
- // refined from the observed rate. Blowing past the FIRST estimate flips on the reassurance banner.
2259
- var countdownHtml = "";
2260
- var overdueHtml = "";
2261
- if (!preparing && total > 0) {
2262
- var now = Date.now();
2263
- if (!exStartAt) { exStartAt = now; exDeadline = now + Math.max(60, total * 2.6) * 1000; }
2264
- var elapsed = (now - exStartAt) / 1000;
2265
- var remainSec;
2266
- if (completed >= 5 && elapsed > 20) remainSec = (total - completed) / (completed / elapsed);
2267
- else remainSec = Math.max(0, (exDeadline - now) / 1000);
2268
- if (now > exDeadline) {
2269
- overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected. Still working.</strong>' +
2270
- 'Everything already processed is saved. Ending now is safe; the next run resumes from the remaining conversations.</div>';
2271
- } else {
2272
- countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
2273
- }
2274
- }
2275
- t.innerHTML =
2276
- '<div class="workbench">' +
2277
- '<h2>' + (preparing ? "Setting up the import." : "Building your memory.") + '</h2>' +
2278
- '<div class="exMeter"><strong>' + (preparing ? "Local" : esc(number(pct)) + "%") + '</strong><span>' + (preparing ? "sizing sessions" : (total ? esc(number(completed)) + " of " + esc(number(total)) + " conversations" : "starting")) + '</span></div>' +
2279
- '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
2280
- '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
2281
- '<div class="exCurrent"><i></i><div><span>Now handling</span><strong>' + esc(statusLine) + '</strong></div></div>' +
2282
- countdownHtml + overdueHtml +
2283
- '<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button><span>Ends cleanly; next time continues from saved work.</span></div>' +
2284
- '<p class="exHint">You can step away from this page. Echo saves each finished conversation as it goes.</p>' +
2285
- '</div>';
2286
- var endBtn = document.getElementById("endExtraction");
2287
- if (endBtn) endBtn.onclick = endExtraction;
2288
- }
2289
- function fmtCountdown(sec) {
2290
- sec = Math.max(0, Math.round(sec));
2291
- var m = Math.floor(sec / 60), s = sec % 60;
2292
- if (m >= 60) { var h = Math.floor(m / 60); return h + "h " + (m % 60) + "m"; }
2293
- return m > 0 ? m + "m " + s + "s" : s + "s";
2294
- }
2295
- function recallPrompt() {
2296
- return "Warm up from EchoMem: call search_memories for what I have been working on recently, then give me a 5-line recap of my current projects and where each one stands, so we can pick up right where I left off. If this is Codex or Claude Code, open the EchoMem context HUD by running echomem-hud app. Use the HUD's context-health score to tell me when to renew into a fresh session and carry a checkpoint forward. As we work, when we make a decision, finish a fix, or hit a milestone, save it with save_conversation. Use this prompt in Codex, Claude Code, or Claude Desktop; if you are in another chat surface, tell me EchoMem tools may not be available here.";
2297
- }
2298
- function setLaunchStatus(message) {
2299
- var node = document.getElementById("launchStatus");
2300
- if (node) node.textContent = message || "";
2301
- }
2302
- async function copyRecallPrompt() {
2303
- var prompt = recallPrompt();
2304
- try {
2305
- await navigator.clipboard.writeText(prompt);
2306
- return true;
2307
- } catch (_) {
2308
- return false;
2309
- }
2310
- }
2311
- async function copyPromptOnly() {
2312
- var copied = await copyRecallPrompt();
2313
- setLaunchStatus(copied ? "Prompt copied." : "Could not copy automatically. Select the prompt and copy it.");
2314
- }
2315
- function codexDeepLink(prompt) {
2316
- var q = "prompt=" + encodeURIComponent(prompt);
2317
- if (workspacePath) q += "&path=" + encodeURIComponent(workspacePath);
2318
- return "codex://new?" + q;
2319
- }
2320
- async function tryInCodex() {
2321
- var prompt = recallPrompt();
2322
- var copied = await copyRecallPrompt();
2323
- setLaunchStatus(copied ? "Copied prompt. Opening Codex..." : "Opening Codex. If the prompt is empty, copy it manually.");
2324
- window.location.href = codexDeepLink(prompt);
2325
- }
2326
- async function openClaudeDesktop() {
2327
- var copied = await copyRecallPrompt();
2328
- setLaunchStatus(copied ? "Copied prompt. Opening Claude Desktop..." : "Opening Claude Desktop. If the prompt is empty, copy it manually.");
2329
- try {
2330
- var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
2331
- setLaunchStatus(res && res.message ? res.message : "Prompt copied. Paste it into Claude Desktop.");
2332
- } catch (error) {
2333
- var msg = error && error.message ? error.message : "Could not auto-open Claude.";
2334
- if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The prompt is copied - open Claude Desktop and paste it.";
2335
- setLaunchStatus(msg);
2336
- }
2337
- }
2338
- function tryItCard() {
2339
- var prompt = recallPrompt();
2340
- return '<div class="tryCard">' +
2341
- '<p class="tryK">Try it now</p>' +
2342
- '<h3 class="tryTitle">Ask your agent to pick up where you left off.</h3>' +
2343
- '<pre id="tryPrompt">' + esc(prompt) + '</pre>' +
2344
- '<div class="agentLaunch" aria-label="Open in a coding agent">' +
2345
- '<button type="button" id="tryCodex"><span class="agentIcon"><img src="/agent-icons/codex.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CX</span></span><span class="agentText"><strong>Try in Codex</strong><small>copy prompt + open session</small></span></button>' +
2346
- '<button type="button" id="tryClaudeDesktop"><span class="agentIcon"><img src="/agent-icons/claude-desktop.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CL</span></span><span class="agentText"><strong>Open Claude Desktop</strong><small>copy prompt + open app</small></span></button>' +
2347
- '<button type="button" id="tryCopy"><span class="agentIcon"><span class="copyGlyph" aria-hidden="true"></span></span><span class="agentText"><strong>Copy prompt</strong><small>paste anywhere</small></span></button>' +
2348
- '</div>' +
2349
- '<p class="launchStatus" id="launchStatus">Paste into Codex, Claude Code, or Claude Desktop. EchoMem tools need to be available there.</p>' +
2350
- '</div>';
2351
- }
2352
- function hudTip() {
2353
- return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches the context window while you work. When the score drifts low, start a fresh session and carry the checkpoint forward. In Codex or Claude Code, ask &ldquo;<strong>open the EchoMem HUD</strong>&rdquo; or run <code>echomem-hud app</code>.</span></p>';
2354
- }
2355
- function doneCloseNote() {
2356
- return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
2357
- }
2358
- function renderEnding() {
2359
- setCityMode(false);
2360
- setExtractMode(true);
2361
- setHead("Ending extraction", "Ending");
2362
- app.className = "";
2363
- app.innerHTML =
2364
- '<div class="miniLoad" style="margin:0 auto;text-align:center;place-items:center">' +
2365
- '<h2>Ending cleanly</h2>' +
2366
- '<p>Give Echo a moment to mark this setup as finished for now. Everything already captured stays saved.</p>' +
2367
- '<div class="miniBar"><i></i></div>' +
2368
- '</div>';
2369
- }
2370
- function renderSkipped() {
2371
- setCityMode(false);
2372
- setExtractMode(true);
2373
- setHead("Setup paused", "Skipped");
2374
- app.className = "";
2375
- app.innerHTML =
2376
- '<div class="skipStage">' +
2377
- '<span class="seal">Skipped for now</span>' +
2378
- '<h2>You can come back anytime.</h2>' +
2379
- '<p>Echo saved what already finished. When you are ready, run <code>echomem-mcp migrate</code> and it will continue from the remaining conversations.</p>' +
2380
- hudTip() +
2381
- '</div>';
2382
- }
2383
- async function endExtraction() {
2384
- if (extractionEnded) return;
2385
- var ok = window.confirm("End extraction for now? You can come back anytime. Everything already captured stays saved, and the next run continues from what remains.");
2386
- if (!ok) return;
2387
- extractionEnded = true;
2388
- renderEnding();
2389
- try {
2390
- await postJson("/skip", {});
2391
- } catch (_) {
2392
- /* The bridge may close while ending; the local page can still show the paused state. */
2393
- }
2394
- await sleep(650);
2395
- renderSkipped();
2396
- }
2397
- async function skip() {
2398
- if (decisionMade) return;
2399
- decisionMade = true;
2400
- try {
2401
- await postJson("/skip", {});
2402
- renderSkipped();
2403
- } catch (e) {
2404
- decisionMade = false;
2405
- renderDashboard(e && e.message);
2406
- }
2407
- }
2408
- async function localLogout() {
2409
- if (decisionMade) return;
2410
- decisionMade = true;
2411
- renderLogoutPending();
2412
- try {
2413
- await postJson("/logout", {}, 12000);
2414
- stats = null;
2415
- statsSlow = false;
2416
- connected = false;
2417
- decisionMade = false;
2418
- renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
2419
- } catch (e) {
2420
- decisionMade = false;
2421
- renderLogoutIssue(e && e.message ? e.message : String(e));
2422
- }
2423
- }
2424
-
2425
- /* ---------- scan-first poll for the forensic report ---------- */
2426
- async function showReport() {
2427
- if (reportPollStarted) return;
2428
- reportPollStarted = true;
2429
- if (!report) renderLoading();
2430
- try {
2431
- var fails = 0;
2432
- for (;;) {
2433
- if (decisionMade) return;
2434
- try {
2435
- var res = await fetchWithTimeout("/report?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 12000);
2436
- if (res.status === 200) { report = await res.json(); renderForensicReport(); return; }
2437
- if (res.status === 202) {
2438
- fails = 0;
2439
- renderLoading();
2440
- } else {
2441
- throw new Error(await res.text() || ("HTTP " + res.status));
2442
- }
2443
- } catch (e) {
2444
- // Only a RUN of consecutive failures is a real bridge problem. A long scan that keeps
2445
- // answering 202 must never trip this (that was the old false "Local bridge issue").
2446
- fails++;
2447
- if (!report && fails >= 8) renderBridgeIssue(e && e.message ? e.message : String(e));
2448
- }
2449
- await sleep(700);
2450
- }
2451
- } finally { reportPollStarted = false; }
2452
- }
2453
-
2454
- window.addEventListener("message", function (event) {
2455
- if (event.origin !== location.origin) return;
2456
- var data = event.data || {};
2457
- if (data.type === "echomem:connected" && data.nonce === nonce) {
2458
- connected = true;
2459
- closeAuthWindow();
2460
- void waitForStats();
2461
- } else if (data.type === "echomem:connect-click") {
2462
- onConnectClick();
2463
- }
2464
- });
2465
- async function init() {
2466
- if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp setup.");
2467
- if (params.get("connected") === "1") {
2468
- connected = true;
2469
- if (notifyOpenerConnected()) {
2470
- // Popup flow: hand the main tab back control, then this popup closes.
2471
- setHead("EchoMem connected", "Done");
2472
- app.className = "notice";
2473
- app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
2474
- // Fallback: if window.close() was blocked (auth opened as a tab, or the browser refused),
2475
- // this window would otherwise hang on "Returning..." forever. After a beat, take over here
2476
- // so the user always lands on the dashboard instead of a dead page.
2477
- setTimeout(function () { if (!window.closed) void waitForStats(); }, 1500);
2478
- } else {
2479
- // Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
2480
- void waitForStats();
2481
- }
2482
- return;
2483
- }
2484
- // Learn the auth URL + connection state, but don't block the local scan on it.
2485
- getJson("/config").then(function (cfg) {
2486
- authUrl = cfg.authUrl || authUrl;
2487
- connected = !!cfg.connected;
2488
- workspacePath = cfg.workspacePath || workspacePath;
2489
- }).catch(function () {});
2490
- // Scan-first: show the local forensic report before asking the user to connect.
2491
- await showReport();
2492
- }
2493
- // Dev/design preview: ?preview=extract-counting|extract-ready|extract-run|extract-overdue|extract-done|extract-ending|extract-skipped renders that extraction
2494
- // state directly with sample data (no server, no nonce). Lets us eyeball every state of the
2495
- // first-impression flow without running a real 3000-session import.
2496
- var previewState = params.get("preview");
2497
- if (previewState === "extract-counting") {
2498
- stats = null;
2499
- renderDashboard();
2500
- } else if (previewState === "extract-ready") {
2501
- stats = {
2502
- sessions: { total: 136, codex: 82, claudeCode: 54 },
2503
- migratable: {
2504
- pending: 28,
2505
- pendingTotal: 28,
2506
- pendingCodex: 19,
2507
- pendingClaudeCode: 9,
2508
- alreadyMigrated: 108,
2509
- skippedActive: 0,
2510
- eta: { estimatedLabel: "under 2 minutes" },
2511
- estimatedLabel: "under 2 minutes",
2512
- },
2513
- discovery: { phase: "account", exact: false },
2514
- };
2515
- renderDashboard();
2516
- } else if (previewState === "extract-run") {
2517
- renderProgress({ status: "running", total: 28, completed: 4, extracted: 19, failed: 1, latest: "codex 2026-07-01", latestRepo: "EchoMem-Chrome" });
2518
- } else if (previewState === "extract-overdue") {
2519
- exStartAt = Date.now() - 200000;
2520
- exDeadline = Date.now() - 1000;
2521
- renderProgress({ status: "running", total: 28, completed: 21, extracted: 96, failed: 2, latest: "claude-code 2026-06-28", latestRepo: "DG-Web" });
2522
- } else if (previewState === "extract-done") {
2523
- renderProgress({ status: "completed", total: 28, completed: 28, extracted: 128, failed: 0 });
2524
- } else if (previewState === "extract-ending") {
2525
- renderEnding();
2526
- } else if (previewState === "extract-skipped") {
2527
- renderSkipped();
2528
- } else {
2529
- init().catch(function (e) {
2530
- setHead("Connection issue", "Needs attention");
2531
- app.className = "error";
2532
- app.textContent = e && e.message ? e.message : String(e);
2533
- });
2534
- }
2535
- })();
2536
- </script>
2537
- </body>
2538
- </html>`;
1
+ import { renderSetupPageDocument } from "./setup-page/document.js";
2
+ import { renderSetupPreviewBootstrap } from "./setup-preview.js";
3
+ /**
4
+ * Setup page entry point. The implementation is split by product phase under ./setup-page/:
5
+ * core utilities, local report, post-auth extraction, and lifecycle polling.
6
+ */
7
+ export function renderSetupPage(options = {}) {
8
+ const previewState = options.previewState ?? null;
9
+ const startupScript = previewState
10
+ ? renderSetupPreviewBootstrap(previewState)
11
+ : `
12
+ init().catch(function () {
13
+ renderReportIssue("SETUP_INITIALIZATION_FAILED", "The local setup page could not start. Close this tab and run echomem-mcp init again.");
14
+ });`;
15
+ return renderSetupPageDocument({ previewState, startupScript });
2539
16
  }