@echomem/mcp 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/city/_live.html +37 -0
- package/dist/city/_serve.mjs +45 -0
- package/dist/city/card-data.json +15 -0
- package/dist/city/city-data.json +248 -0
- package/dist/city/echo-ai-city-only.html +1862 -0
- package/dist/city/echo-ai-city-only.template.html +1254 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/generate-echo-city-only.mjs +112 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/forensics.js +46 -5
- package/dist/migrate.js +46 -4
- package/dist/setup-page.js +767 -266
- package/dist/setup.js +64 -4
- package/package.json +2 -2
package/dist/setup-page.js
CHANGED
|
@@ -18,6 +18,9 @@ export function renderSetupPage() {
|
|
|
18
18
|
--echo-paper-white: #ffffff;
|
|
19
19
|
--echo-paper-soft: #f0efe9;
|
|
20
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);
|
|
21
24
|
--echo-font-brand: "Avenir Next", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
22
25
|
--echo-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
23
26
|
--echo-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
@@ -111,52 +114,82 @@ export function renderSetupPage() {
|
|
|
111
114
|
font-size: 16px;
|
|
112
115
|
text-align: center;
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.loading {
|
|
117
|
-
display: grid;
|
|
118
|
-
gap: 22px;
|
|
119
|
-
padding: clamp(24px, 6vw, 64px) 0;
|
|
117
|
+
body.cityMode {
|
|
118
|
+
background: #eef0f3;
|
|
120
119
|
}
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
body.cityMode main {
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: 0;
|
|
123
|
+
margin: 0;
|
|
125
124
|
}
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
border-radius: 6px;
|
|
129
|
-
background: linear-gradient(90deg, #ece9e0, #d7dbe6, #ece9e0);
|
|
130
|
-
background-size: 220% 100%;
|
|
131
|
-
animation: pulse 1.4s ease-in-out infinite;
|
|
132
|
-
}
|
|
133
|
-
.sk.title { height: 46px; width: 78%; }
|
|
134
|
-
.sk.w70 { width: 70%; }
|
|
135
|
-
.sk.w52 { width: 52%; }
|
|
136
|
-
.sk.w38 { width: 38%; }
|
|
137
|
-
.sk-grid {
|
|
138
|
-
display: grid;
|
|
139
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
140
|
-
gap: 10px;
|
|
141
|
-
max-width: 720px;
|
|
125
|
+
body.cityMode header {
|
|
126
|
+
display: none;
|
|
142
127
|
}
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
.
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
.loadCaption { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
|
|
128
|
+
body.cityMode #app {
|
|
129
|
+
display: block;
|
|
130
|
+
min-height: 100vh;
|
|
131
|
+
color: var(--echo-ink-text);
|
|
132
|
+
text-align: left;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* clean, consistent loader for the post-connect "working/starting" states */
|
|
136
|
+
.miniLoad { display: grid; gap: 12px; max-width: 520px; padding: clamp(24px, 6vw, 64px) 0; }
|
|
137
|
+
.miniLoad h2 { margin: 0; font-family: var(--echo-font-brand); font-size: clamp(22px, 3.6vw, 32px); color: var(--echo-ink-primary); }
|
|
138
|
+
.miniLoad p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.5; }
|
|
139
|
+
.miniBar { height: 6px; border-radius: 999px; background: var(--echo-paper-soft); overflow: hidden; max-width: 360px; margin-top: 4px; }
|
|
140
|
+
.miniBar i { display: block; height: 100%; width: 36%; border-radius: 999px; background: var(--echo-ink-primary); animation: miniIndet 1.3s ease-in-out infinite; }
|
|
141
|
+
@keyframes miniIndet { 0% { transform: translateX(-115%); } 100% { transform: translateX(330%); } }
|
|
158
142
|
|
|
159
143
|
/* ---- forensic report (ContextDoctorTwo) ---- */
|
|
144
|
+
.cityHero {
|
|
145
|
+
--city-display-w: min(calc(100vw - 28px), calc((100vh - 28px) * 0.7797));
|
|
146
|
+
--city-display-h: min(calc(100vh - 28px), calc((100vw - 28px) * 1.2826));
|
|
147
|
+
position: relative;
|
|
148
|
+
min-height: 100vh;
|
|
149
|
+
display: block;
|
|
150
|
+
padding: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
background: #eef0f3;
|
|
153
|
+
}
|
|
154
|
+
.cityShell {
|
|
155
|
+
position: relative;
|
|
156
|
+
width: 100vw;
|
|
157
|
+
height: 100vh;
|
|
158
|
+
border-radius: 0;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
.cityShell iframe {
|
|
162
|
+
width: 100%;
|
|
163
|
+
height: 100%;
|
|
164
|
+
border: 0;
|
|
165
|
+
display: block;
|
|
166
|
+
border-radius: 0;
|
|
167
|
+
background: transparent;
|
|
168
|
+
}
|
|
169
|
+
.cityArrow {
|
|
170
|
+
position: absolute;
|
|
171
|
+
z-index: 5;
|
|
172
|
+
right: clamp(22px, 3vw, 42px);
|
|
173
|
+
bottom: clamp(22px, 3.5vh, 42px);
|
|
174
|
+
width: 44px;
|
|
175
|
+
height: 44px;
|
|
176
|
+
min-height: 44px;
|
|
177
|
+
padding: 0;
|
|
178
|
+
border-radius: 999px;
|
|
179
|
+
border-color: rgba(26, 58, 143, 0.2);
|
|
180
|
+
background: rgba(255, 255, 255, 0.92);
|
|
181
|
+
color: var(--echo-ink-primary);
|
|
182
|
+
font-size: 24px;
|
|
183
|
+
line-height: 1;
|
|
184
|
+
box-shadow: 0 10px 28px rgba(28, 38, 60, 0.12);
|
|
185
|
+
}
|
|
186
|
+
.detailsWrap {
|
|
187
|
+
width: 100%;
|
|
188
|
+
margin: 0;
|
|
189
|
+
padding: clamp(42px, 6vw, 86px) clamp(28px, 7vw, 104px);
|
|
190
|
+
background: #fff;
|
|
191
|
+
}
|
|
192
|
+
#setup-details { scroll-margin-top: 0; }
|
|
160
193
|
.hero { padding: 4px 0 24px; }
|
|
161
194
|
.privacy {
|
|
162
195
|
display: grid;
|
|
@@ -328,7 +361,7 @@ export function renderSetupPage() {
|
|
|
328
361
|
}
|
|
329
362
|
|
|
330
363
|
/* reread / repo rows */
|
|
331
|
-
.
|
|
364
|
+
.row2 {
|
|
332
365
|
display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
|
|
333
366
|
padding: 11px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
334
367
|
}
|
|
@@ -374,6 +407,186 @@ export function renderSetupPage() {
|
|
|
374
407
|
.nightCard p { margin: 10px 0 0; font-size: 17px; line-height: 1.5; color: var(--echo-ink-text); }
|
|
375
408
|
.nightCard b { color: var(--echo-ink-primary); }
|
|
376
409
|
|
|
410
|
+
/* optional city explanation page */
|
|
411
|
+
.explainPage {
|
|
412
|
+
max-width: 1260px;
|
|
413
|
+
margin: 0 auto;
|
|
414
|
+
}
|
|
415
|
+
.explainHero {
|
|
416
|
+
display: grid;
|
|
417
|
+
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
|
418
|
+
gap: clamp(28px, 5vw, 70px);
|
|
419
|
+
align-items: end;
|
|
420
|
+
padding-bottom: clamp(28px, 5vw, 58px);
|
|
421
|
+
border-bottom: 1px solid var(--echo-line-soft);
|
|
422
|
+
}
|
|
423
|
+
.explainCopy {
|
|
424
|
+
max-width: 760px;
|
|
425
|
+
}
|
|
426
|
+
.explainCopy h2 {
|
|
427
|
+
margin: 10px 0 0;
|
|
428
|
+
font-size: clamp(38px, 5.4vw, 74px);
|
|
429
|
+
line-height: 0.98;
|
|
430
|
+
color: var(--echo-ink-primary);
|
|
431
|
+
}
|
|
432
|
+
.explainCopy p:not(.kicker-mono) {
|
|
433
|
+
max-width: 680px;
|
|
434
|
+
margin: 20px 0 0;
|
|
435
|
+
font-size: clamp(16px, 1.35vw, 20px);
|
|
436
|
+
line-height: 1.55;
|
|
437
|
+
color: var(--echo-ink-text);
|
|
438
|
+
}
|
|
439
|
+
.cleanReadout {
|
|
440
|
+
border: 1px solid var(--echo-line-soft);
|
|
441
|
+
border-radius: 8px;
|
|
442
|
+
padding: 22px;
|
|
443
|
+
background: #fff;
|
|
444
|
+
}
|
|
445
|
+
.cleanReadout span {
|
|
446
|
+
display: block;
|
|
447
|
+
font-family: var(--echo-font-mono);
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
font-weight: 800;
|
|
450
|
+
letter-spacing: 0.08em;
|
|
451
|
+
text-transform: uppercase;
|
|
452
|
+
color: var(--echo-ink-mute);
|
|
453
|
+
}
|
|
454
|
+
.cleanReadout strong {
|
|
455
|
+
display: block;
|
|
456
|
+
margin-top: 8px;
|
|
457
|
+
font-family: var(--echo-font-brand);
|
|
458
|
+
font-size: clamp(54px, 6vw, 88px);
|
|
459
|
+
font-weight: 900;
|
|
460
|
+
line-height: 0.9;
|
|
461
|
+
color: var(--echo-ink-seal);
|
|
462
|
+
font-variant-numeric: tabular-nums;
|
|
463
|
+
}
|
|
464
|
+
.cleanReadout p {
|
|
465
|
+
margin: 14px 0 0;
|
|
466
|
+
color: var(--echo-ink-mute);
|
|
467
|
+
font-size: 15px;
|
|
468
|
+
line-height: 1.55;
|
|
469
|
+
}
|
|
470
|
+
.insightPills {
|
|
471
|
+
display: grid;
|
|
472
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
473
|
+
gap: 10px;
|
|
474
|
+
margin-top: clamp(18px, 3vw, 32px);
|
|
475
|
+
}
|
|
476
|
+
.insightPill {
|
|
477
|
+
border: 1px solid var(--echo-line-soft);
|
|
478
|
+
border-radius: 8px;
|
|
479
|
+
padding: 14px 16px;
|
|
480
|
+
background: #fff;
|
|
481
|
+
}
|
|
482
|
+
.insightPill strong {
|
|
483
|
+
display: block;
|
|
484
|
+
font-family: var(--echo-font-brand);
|
|
485
|
+
font-size: clamp(24px, 2.2vw, 34px);
|
|
486
|
+
font-weight: 900;
|
|
487
|
+
line-height: 1;
|
|
488
|
+
color: var(--echo-ink-primary);
|
|
489
|
+
font-variant-numeric: tabular-nums;
|
|
490
|
+
}
|
|
491
|
+
.insightPill.alert strong { color: var(--echo-ink-seal); }
|
|
492
|
+
.insightPill span {
|
|
493
|
+
display: block;
|
|
494
|
+
margin-top: 8px;
|
|
495
|
+
font-family: var(--echo-font-mono);
|
|
496
|
+
font-size: 10px;
|
|
497
|
+
font-weight: 800;
|
|
498
|
+
letter-spacing: 0.07em;
|
|
499
|
+
text-transform: uppercase;
|
|
500
|
+
color: var(--echo-ink-mute);
|
|
501
|
+
}
|
|
502
|
+
.calcNote {
|
|
503
|
+
margin-top: 18px;
|
|
504
|
+
border-top: 1px solid var(--echo-line-soft);
|
|
505
|
+
padding-top: 14px;
|
|
506
|
+
color: var(--echo-ink-faint);
|
|
507
|
+
font-family: var(--echo-font-mono);
|
|
508
|
+
font-size: 11px;
|
|
509
|
+
line-height: 1.6;
|
|
510
|
+
}
|
|
511
|
+
.calcNote summary {
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
color: var(--echo-ink-primary);
|
|
514
|
+
font-weight: 800;
|
|
515
|
+
text-transform: uppercase;
|
|
516
|
+
letter-spacing: 0.06em;
|
|
517
|
+
}
|
|
518
|
+
.calcNote p { margin: 10px 0 0; }
|
|
519
|
+
.explainSections {
|
|
520
|
+
display: grid;
|
|
521
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
522
|
+
gap: 14px;
|
|
523
|
+
margin-top: clamp(28px, 4vw, 52px);
|
|
524
|
+
}
|
|
525
|
+
.explainCard {
|
|
526
|
+
border: 1px solid var(--echo-line-soft);
|
|
527
|
+
border-radius: 8px;
|
|
528
|
+
background: #fff;
|
|
529
|
+
padding: 18px;
|
|
530
|
+
min-height: 230px;
|
|
531
|
+
display: grid;
|
|
532
|
+
gap: 14px;
|
|
533
|
+
align-content: start;
|
|
534
|
+
}
|
|
535
|
+
.explainCard .kicker-mono { margin: 0; color: var(--echo-ink-mute); }
|
|
536
|
+
.explainCard h3 {
|
|
537
|
+
margin: 0;
|
|
538
|
+
font-family: var(--echo-font-brand);
|
|
539
|
+
font-size: clamp(21px, 2vw, 28px);
|
|
540
|
+
line-height: 1.08;
|
|
541
|
+
color: var(--echo-ink-primary);
|
|
542
|
+
}
|
|
543
|
+
.explainCard p {
|
|
544
|
+
margin: 0;
|
|
545
|
+
color: var(--echo-ink-text);
|
|
546
|
+
font-size: 15px;
|
|
547
|
+
line-height: 1.55;
|
|
548
|
+
}
|
|
549
|
+
.explainCard .panel {
|
|
550
|
+
padding: 0;
|
|
551
|
+
border: 0;
|
|
552
|
+
background: transparent;
|
|
553
|
+
}
|
|
554
|
+
.explainCta {
|
|
555
|
+
display: grid;
|
|
556
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
557
|
+
gap: 24px;
|
|
558
|
+
align-items: center;
|
|
559
|
+
margin-top: 16px;
|
|
560
|
+
padding: 22px;
|
|
561
|
+
border: 1px solid var(--echo-line-soft);
|
|
562
|
+
border-radius: 8px;
|
|
563
|
+
background: var(--echo-paper-canvas);
|
|
564
|
+
}
|
|
565
|
+
.explainCta h3 {
|
|
566
|
+
margin: 0;
|
|
567
|
+
font-family: var(--echo-font-brand);
|
|
568
|
+
font-size: clamp(24px, 2.5vw, 34px);
|
|
569
|
+
line-height: 1.08;
|
|
570
|
+
color: var(--echo-ink-primary);
|
|
571
|
+
}
|
|
572
|
+
.explainCta p {
|
|
573
|
+
margin: 8px 0 0;
|
|
574
|
+
max-width: 700px;
|
|
575
|
+
color: var(--echo-ink-mute);
|
|
576
|
+
font-size: 15px;
|
|
577
|
+
line-height: 1.5;
|
|
578
|
+
}
|
|
579
|
+
.explainCta .primary, .explainCta .secondary {
|
|
580
|
+
min-width: 180px;
|
|
581
|
+
min-height: 50px;
|
|
582
|
+
border-radius: 8px;
|
|
583
|
+
font-family: var(--echo-font-mono);
|
|
584
|
+
font-size: 13px;
|
|
585
|
+
font-weight: 800;
|
|
586
|
+
letter-spacing: 0.08em;
|
|
587
|
+
text-transform: uppercase;
|
|
588
|
+
}
|
|
589
|
+
|
|
377
590
|
/* ---- dashboard (post-auth extraction) ---- */
|
|
378
591
|
.loading-panel, .doctor-copy, .doctor-score, .metric, .extract-panel {
|
|
379
592
|
border: 1px solid var(--line);
|
|
@@ -460,11 +673,157 @@ export function renderSetupPage() {
|
|
|
460
673
|
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); }
|
|
461
674
|
.hidden { display: none; }
|
|
462
675
|
@media (max-width: 820px) {
|
|
463
|
-
.heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .
|
|
676
|
+
.heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .doctor-stat-strip,
|
|
677
|
+
.explainHero, .insightPills, .explainSections, .explainCta { grid-template-columns: 1fr; }
|
|
464
678
|
.doctor-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
465
679
|
.heroCopy h2 { font-size: 34px; }
|
|
466
680
|
.scoreCard { min-height: 220px; }
|
|
681
|
+
.explainCopy h2 { font-size: 38px; }
|
|
682
|
+
.explainCta .promiseActions { width: 100%; }
|
|
683
|
+
.explainCta .primary, .explainCta .secondary { width: 100%; }
|
|
684
|
+
.cityHero { min-height: auto; }
|
|
685
|
+
.cityShell {
|
|
686
|
+
width: 100vw;
|
|
687
|
+
height: 100vh;
|
|
688
|
+
}
|
|
467
689
|
}
|
|
690
|
+
/* extraction dashboard — plate layout */
|
|
691
|
+
.exHero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.95fr); gap: clamp(20px, 3vw, 40px); align-items: center; margin-top: 8px; }
|
|
692
|
+
.exCopy h2 { max-width: 560px; margin: 8px 0 0; font-size: clamp(30px, 4vw, 48px); line-height: 1.03; }
|
|
693
|
+
.exCopy .exSub { margin: 14px 0 0; max-width: 520px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.55; color: var(--echo-ink-mute); }
|
|
694
|
+
.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); }
|
|
695
|
+
.exLegend .lgDot { width: 12px; height: 12px; border-radius: 3px; display: inline-block; margin-left: 10px; }
|
|
696
|
+
.exLegend .lgDot:first-child { margin-left: 0; }
|
|
697
|
+
.lgNew { background: #2f6df0; }
|
|
698
|
+
.lgDim { background: #cfc8b8; }
|
|
699
|
+
.exHero .actions { margin-top: 22px; }
|
|
700
|
+
.exHint { margin: 14px 0 0; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
|
|
701
|
+
.exPlate { width: 100%; }
|
|
702
|
+
/* WebGL clay city (echo-extraction-plate.html) — the only plate; no SVG layer */
|
|
703
|
+
.plateStack { position: relative; width: 100%; aspect-ratio: 640 / 470; }
|
|
704
|
+
.plateStack .plateGl { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
|
|
705
|
+
.recon { margin-top: 16px; 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; }
|
|
706
|
+
@media (max-width: 820px) { .exHero { grid-template-columns: 1fr; } }
|
|
707
|
+
|
|
708
|
+
/* ===== workspace readout dashboard (Part 2 — replaces explainPage) ===== */
|
|
709
|
+
@keyframes rdoRise { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
|
|
710
|
+
@keyframes rdoGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
|
|
711
|
+
@keyframes rdoDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
|
|
712
|
+
@keyframes rdoWipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
|
|
713
|
+
body.cityMode .detailsWrap { background: linear-gradient(180deg, rgba(245,241,231,0.92), rgba(255,255,255,0.96)), var(--echo-paper-canvas); }
|
|
714
|
+
.echo-rdo { width: min(1280px, 100%); margin: 0 auto; color: var(--echo-ink-text); font-family: var(--echo-font-body); }
|
|
715
|
+
.echo-rdo .rise { opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) forwards; animation-delay: var(--d, 0ms); }
|
|
716
|
+
.echo-rdo .rdo-head { border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 20px; margin-bottom: 22px; }
|
|
717
|
+
.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); }
|
|
718
|
+
.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); }
|
|
719
|
+
.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; }
|
|
720
|
+
.echo-rdo .rdo-lead b { color: var(--echo-ink-primary); }
|
|
721
|
+
.echo-rdo .rdo-meta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
722
|
+
.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); }
|
|
723
|
+
.echo-rdo .rdo-chip b { color: var(--echo-ink-primary); }
|
|
724
|
+
.echo-rdo .rdo-chip .live { width: 7px; height: 7px; border-radius: 50%; background: var(--echo-ink-accent); }
|
|
725
|
+
.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); }
|
|
726
|
+
.echo-rdo .rdo-card:hover { box-shadow: 0 14px 30px -16px rgba(26,58,143,0.28); transform: translateY(-2px); }
|
|
727
|
+
.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); }
|
|
728
|
+
.echo-rdo .csub { margin-top: 4px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
|
|
729
|
+
.echo-rdo .csub b { color: var(--echo-ink-primary); }
|
|
730
|
+
.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; }
|
|
731
|
+
.echo-rdo .rdo-help:hover { color: #fff; background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
732
|
+
.echo-rdo .rdo-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 14px; }
|
|
733
|
+
.echo-rdo .rdo-kpi { min-height: 132px; display: flex; flex-direction: column; justify-content: flex-end; }
|
|
734
|
+
.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; }
|
|
735
|
+
.echo-rdo .rdo-kpi.hero { border-color: rgba(199,55,47,0.30); }
|
|
736
|
+
.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); }
|
|
737
|
+
.echo-rdo .rdo-kpi.hero .num { color: var(--echo-ink-seal); }
|
|
738
|
+
.echo-rdo .rdo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
739
|
+
.echo-rdo .figbox { margin-top: 16px; position: relative; }
|
|
740
|
+
.echo-rdo .rdo-histo { display: flex; align-items: flex-end; gap: 3px; height: 132px; border-bottom: 2px solid var(--echo-line-soft); }
|
|
741
|
+
.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); }
|
|
742
|
+
.echo-rdo .rdo-histo .bar.night { background: var(--echo-night); }
|
|
743
|
+
.echo-rdo .rdo-histo .bar:hover { filter: brightness(1.12); }
|
|
744
|
+
.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); }
|
|
745
|
+
.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); }
|
|
746
|
+
.echo-rdo .rdo-legend i { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
|
|
747
|
+
.echo-rdo .terr { display: grid; grid-template-columns: 150px 1fr; gap: 22px; align-items: center; margin-top: 16px; }
|
|
748
|
+
.echo-rdo .donut { width: 144px; height: 144px; position: relative; margin: 0 auto; }
|
|
749
|
+
.echo-rdo .donut .donutSvg { width: 100%; height: 100%; display: block; opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) 0.2s forwards; }
|
|
750
|
+
.echo-rdo .donut .seg { transition: opacity 0.15s ease, stroke-width 0.15s ease; }
|
|
751
|
+
.echo-rdo .donut.hovering .seg { opacity: 0.3; }
|
|
752
|
+
.echo-rdo .donut.hovering .seg.on { opacity: 1; stroke-width: 7.5; }
|
|
753
|
+
.echo-rdo .donut .seg { cursor: pointer; }
|
|
754
|
+
.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); }
|
|
755
|
+
.echo-rdo .donut-tip.show, .echo-rdo .histo-tip.show { opacity: 1; }
|
|
756
|
+
.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); }
|
|
757
|
+
.echo-rdo .donut .center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; pointer-events: none; }
|
|
758
|
+
.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; }
|
|
759
|
+
.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); }
|
|
760
|
+
.echo-rdo .rrow { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: baseline; padding: 7px 0; }
|
|
761
|
+
.echo-rdo .rrow .nm { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
762
|
+
.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; }
|
|
763
|
+
.echo-rdo .rbar { grid-column: 1 / -1; height: 5px; border-radius: 999px; background: var(--echo-paper-soft); margin-top: 3px; overflow: hidden; }
|
|
764
|
+
.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); }
|
|
765
|
+
.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; }
|
|
766
|
+
.echo-rdo .stack { display: flex; height: 28px; border-radius: 7px; overflow: hidden; background: var(--echo-paper-soft); }
|
|
767
|
+
.echo-rdo .stack .seg { min-width: 0; display: grid; place-items: center; font-family: var(--echo-font-mono); font-size: 10px; font-weight: 800; }
|
|
768
|
+
.echo-rdo .ledgerCols { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
|
|
769
|
+
.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); }
|
|
770
|
+
.echo-rdo .ex-row:first-of-type { border-top: 0; padding-top: 16px; }
|
|
771
|
+
.echo-rdo .collapse-inner .ex-row:first-of-type { border-top: 1px solid var(--echo-line-soft); padding-top: 11px; }
|
|
772
|
+
.echo-rdo .ex-file { font-family: var(--echo-font-mono); font-size: 12.5px; font-weight: 700; color: var(--echo-ink-primary); }
|
|
773
|
+
.echo-rdo .ex-meta { margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
|
|
774
|
+
.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; }
|
|
775
|
+
.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); }
|
|
776
|
+
.echo-rdo .collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.34s var(--rdo-ease); }
|
|
777
|
+
.echo-rdo .collapse.open { grid-template-rows: 1fr; }
|
|
778
|
+
.echo-rdo .collapse-inner { overflow: hidden; min-height: 0; }
|
|
779
|
+
.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); }
|
|
780
|
+
.echo-rdo .moreBtn:hover { opacity: 0.65; }
|
|
781
|
+
.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); }
|
|
782
|
+
.echo-rdo .mrow2:first-child { border-top: 0; }
|
|
783
|
+
.echo-rdo .mrow2 b { font-size: 13px; color: var(--echo-ink-primary); }
|
|
784
|
+
.echo-rdo .mrow2 strong { font-family: var(--echo-font-brand); font-size: 16px; color: var(--echo-ink-text); }
|
|
785
|
+
.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; }
|
|
786
|
+
.echo-rdo .outcome .rdo-kicker { color: var(--echo-ink-accent-deep); }
|
|
787
|
+
.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); }
|
|
788
|
+
.echo-rdo .outcome .rdo-lead { margin-top: 12px; max-width: 46ch; }
|
|
789
|
+
.echo-rdo .targetbar { position: relative; margin-top: 36px; }
|
|
790
|
+
.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; }
|
|
791
|
+
.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; }
|
|
792
|
+
.echo-rdo .tb-goal { position: absolute; top: 0; bottom: 0; width: 2px; background: rgba(255,255,255,0.9); border-radius: 2px; }
|
|
793
|
+
.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); }
|
|
794
|
+
.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); }
|
|
795
|
+
.echo-rdo .oc-targets { background: var(--echo-paper-white); border: 1px solid var(--echo-line-soft); border-radius: 10px; padding: 18px; }
|
|
796
|
+
.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); }
|
|
797
|
+
.echo-rdo .ot-note { margin: -2px 0 10px; font-size: 12px; line-height: 1.45; color: var(--echo-ink-mute); }
|
|
798
|
+
.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); }
|
|
799
|
+
.echo-rdo .ot-row:last-of-type { border-bottom: 0; }
|
|
800
|
+
.echo-rdo .ot-row .otk { display: flex; flex-direction: column; gap: 2px; }
|
|
801
|
+
.echo-rdo .ot-row .k { font-size: 13px; color: var(--echo-ink-text); }
|
|
802
|
+
.echo-rdo .ot-row .ks { font-family: var(--echo-font-mono); font-size: 10px; color: var(--echo-ink-faint); }
|
|
803
|
+
.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; }
|
|
804
|
+
.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; }
|
|
805
|
+
.echo-rdo .rdo-cta:hover { filter: brightness(1.12); }
|
|
806
|
+
.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); }
|
|
807
|
+
.echo-rdo .rdo-foot code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
|
|
808
|
+
.echo-rdo .rdo-foot .priv { color: var(--echo-ink-accent-deep); font-weight: 700; }
|
|
809
|
+
@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; } }
|
|
810
|
+
@media (max-width: 620px) { .echo-rdo .ledgerCols, .echo-rdo .terr { grid-template-columns: 1fr; } }
|
|
811
|
+
@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; } }
|
|
812
|
+
/* annotation dialog */
|
|
813
|
+
.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); }
|
|
814
|
+
.echo-note-dlg[open] { animation: rdoRise 0.24s var(--rdo-ease); }
|
|
815
|
+
.echo-note-dlg::backdrop { background: rgba(26,32,46,0.4); }
|
|
816
|
+
.echo-note-dlg .nk, .echo-note-dlg .nt, .echo-note-dlg .nb { padding-left: 26px; padding-right: 26px; }
|
|
817
|
+
.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); }
|
|
818
|
+
.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); }
|
|
819
|
+
.echo-note-dlg .nb { padding-top: 14px; padding-bottom: 26px; }
|
|
820
|
+
.echo-note-dlg .nb p { margin: 0 0 12px; font-size: 14px; line-height: 1.6; }
|
|
821
|
+
.echo-note-dlg .nb p:last-child { margin-bottom: 0; }
|
|
822
|
+
.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; }
|
|
823
|
+
.echo-note-dlg .nb .echo b { color: var(--echo-ink-primary); }
|
|
824
|
+
.echo-note-dlg .nb code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
|
|
825
|
+
.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; }
|
|
826
|
+
.echo-note-dlg .nclose:hover { color: var(--echo-ink-seal); border-color: var(--echo-ink-seal); }
|
|
468
827
|
</style>
|
|
469
828
|
</head>
|
|
470
829
|
<body>
|
|
@@ -472,12 +831,18 @@ export function renderSetupPage() {
|
|
|
472
831
|
<header>
|
|
473
832
|
<div>
|
|
474
833
|
<p class="kicker">EchoMem device setup</p>
|
|
475
|
-
<h1 id="title">
|
|
834
|
+
<h1 id="title">Turn history into memory</h1>
|
|
476
835
|
</div>
|
|
477
836
|
<div class="pill" id="status">Local</div>
|
|
478
837
|
</header>
|
|
479
838
|
<section id="app" class="state">Starting local scan...</section>
|
|
480
839
|
</main>
|
|
840
|
+
<dialog id="echo-note" class="echo-note-dlg">
|
|
841
|
+
<button class="nclose" aria-label="Close">×</button>
|
|
842
|
+
<p class="nk" id="echo-note-k"></p>
|
|
843
|
+
<h2 class="nt" id="echo-note-t"></h2>
|
|
844
|
+
<div class="nb" id="echo-note-b"></div>
|
|
845
|
+
</dialog>
|
|
481
846
|
<script>
|
|
482
847
|
(function () {
|
|
483
848
|
var params = new URLSearchParams(location.search);
|
|
@@ -490,6 +855,9 @@ export function renderSetupPage() {
|
|
|
490
855
|
var statsSlow = false;
|
|
491
856
|
var decisionMade = false;
|
|
492
857
|
var connected = false;
|
|
858
|
+
var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
|
|
859
|
+
var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
|
|
860
|
+
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
493
861
|
var authUrl = "";
|
|
494
862
|
var authWindow = null;
|
|
495
863
|
var connectionPollStarted = false;
|
|
@@ -506,6 +874,9 @@ export function renderSetupPage() {
|
|
|
506
874
|
title.textContent = nextTitle;
|
|
507
875
|
status.textContent = nextStatus;
|
|
508
876
|
}
|
|
877
|
+
function setCityMode(enabled) {
|
|
878
|
+
document.body.classList.toggle("cityMode", !!enabled);
|
|
879
|
+
}
|
|
509
880
|
function number(value) {
|
|
510
881
|
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
511
882
|
}
|
|
@@ -613,10 +984,13 @@ export function renderSetupPage() {
|
|
|
613
984
|
} catch (_) { return false; }
|
|
614
985
|
}
|
|
615
986
|
function showPopupFallback(message, url) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
987
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-help]"), function (hint) {
|
|
988
|
+
hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
|
|
989
|
+
});
|
|
990
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-fallback]"), function (fallback) {
|
|
991
|
+
fallback.setAttribute("href", url || authUrl);
|
|
992
|
+
fallback.classList.remove("hidden");
|
|
993
|
+
});
|
|
620
994
|
}
|
|
621
995
|
function onConnectClick() {
|
|
622
996
|
if (connected) { void waitForStats(); return; }
|
|
@@ -624,33 +998,38 @@ export function renderSetupPage() {
|
|
|
624
998
|
startConnectionPoll();
|
|
625
999
|
}
|
|
626
1000
|
function bindConnect() {
|
|
627
|
-
|
|
628
|
-
|
|
1001
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-connect-echo]"), function (signin) {
|
|
1002
|
+
signin.onclick = onConnectClick;
|
|
1003
|
+
});
|
|
629
1004
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
? "Reading your local history — " + number(prog.scanned) + " / " + number(prog.total) + " sessions"
|
|
636
|
-
: "Reading your local history…";
|
|
637
|
-
// Update the bar in place on later polls so the skeleton animation doesn't restart/flicker.
|
|
638
|
-
var fill = document.getElementById("loadFill");
|
|
639
|
-
if (app.className === "loading" && fill) {
|
|
640
|
-
fill.style.width = pct + "%";
|
|
641
|
-
var cap = document.getElementById("loadCaption");
|
|
642
|
-
if (cap) cap.textContent = caption;
|
|
643
|
-
return;
|
|
1005
|
+
function bindDetailsArrow() {
|
|
1006
|
+
var arrow = document.getElementById("details-arrow");
|
|
1007
|
+
var target = document.getElementById("setup-details");
|
|
1008
|
+
if (arrow && target) {
|
|
1009
|
+
arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
|
|
644
1010
|
}
|
|
645
|
-
|
|
646
|
-
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* ---------- report shell (city iframe owns its own loading) ---------- */
|
|
1014
|
+
// The report page (full-width city hero + detail sections below) is mounted ONCE. The city iframe
|
|
1015
|
+
// shows its OWN in-canvas loading state while it scans, then fades into the rendered report — one
|
|
1016
|
+
// surface, no separate loading page, no reload. The parent only fills the detail sections on /report.
|
|
1017
|
+
function mountReportShell() {
|
|
1018
|
+
setCityMode(true);
|
|
1019
|
+
app.className = "";
|
|
647
1020
|
app.innerHTML =
|
|
648
|
-
'<div class="
|
|
649
|
-
'<
|
|
650
|
-
|
|
651
|
-
'
|
|
652
|
-
'<div
|
|
653
|
-
|
|
1021
|
+
'<section class="cityHero"><div class="cityShell">' +
|
|
1022
|
+
'<iframe title="AI Coding City" src="/city/echo-ai-city-only.html?setup=1&nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
|
|
1023
|
+
'<button id="details-arrow" class="cityArrow hidden" aria-label="Show detailed report">↓</button>' +
|
|
1024
|
+
'</div></section>' +
|
|
1025
|
+
'<div id="setup-details" class="detailsWrap"></div>';
|
|
1026
|
+
bindDetailsArrow();
|
|
1027
|
+
reportMounted = true;
|
|
1028
|
+
}
|
|
1029
|
+
function renderLoading() {
|
|
1030
|
+
if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
|
|
1031
|
+
setCityMode(true);
|
|
1032
|
+
setHead("Turn history into memory", "Reading");
|
|
654
1033
|
}
|
|
655
1034
|
|
|
656
1035
|
/* ---------- forensic report (pic 1/2) ---------- */
|
|
@@ -688,86 +1067,219 @@ export function renderSetupPage() {
|
|
|
688
1067
|
'<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>' +
|
|
689
1068
|
'<div style="margin-top:14px">' + rows + '</div>';
|
|
690
1069
|
}
|
|
1070
|
+
/* ---------- workspace readout dashboard (Part 2) ---------- */
|
|
1071
|
+
var RDO_NIGHT = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
1072
|
+
var RDO_PIE = ["#1a3a8f", "#4ecdc4", "#c7372f", "#7a86c2", "#d29a3f", "#7d9a6d", "#cf6f4e"]; // major-repo colors; "#b3aea0" (taupe) is reserved for "Others" only
|
|
1073
|
+
var rdoReduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
1074
|
+
function rdoDec(v) { return typeof v === "number" && isFinite(v) ? (Math.round(v * 10) / 10) : "-"; }
|
|
1075
|
+
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); }
|
|
1076
|
+
function rdoHourLabel(h) { var x = h % 24, ap = x < 12 ? "am" : "pm", hr = x % 12; if (hr === 0) hr = 12; return hr + ap; }
|
|
1077
|
+
function rdoHisto(hours) {
|
|
1078
|
+
var list = Array.isArray(hours) ? hours : [], n = list.length || 1, max = Math.max.apply(null, [1].concat(list));
|
|
1079
|
+
var bars = list.map(function (v, h) {
|
|
1080
|
+
var ht = Math.max(3, Math.round((v / max) * 100)), hour = Math.round(h * (24 / n));
|
|
1081
|
+
return '<div class="bar' + (RDO_NIGHT[hour] ? ' night' : '') + '" style="height:' + ht + '%;--bd:' + (300 + h * 14) + 'ms" data-tip="' + rdoHourLabel(hour) + ' · ' + number(v) + ' prompts"></div>';
|
|
1082
|
+
}).join("");
|
|
1083
|
+
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>' +
|
|
1084
|
+
'<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–4am)</span></div><div class="histo-tip"></div></div>';
|
|
1085
|
+
}
|
|
1086
|
+
function rdoTerritory(repos) {
|
|
1087
|
+
var list = (repos || []).slice().sort(function (a, b) { return (b.attentionHours || 0) - (a.attentionHours || 0); });
|
|
1088
|
+
if (!list.length) return '<div class="terr"><div class="donut"></div><div></div></div>';
|
|
1089
|
+
var total = list.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0) || 1;
|
|
1090
|
+
var maxH = Math.max.apply(null, [1].concat(list.map(function (r) { return r.attentionHours || 0; })));
|
|
1091
|
+
var threshold = total * 0.05; // repos over 5% shown individually; the rest combine into "Others"
|
|
1092
|
+
var major = list.filter(function (r) { return (r.attentionHours || 0) > threshold; });
|
|
1093
|
+
var minor = list.filter(function (r) { return (r.attentionHours || 0) <= threshold; });
|
|
1094
|
+
var minorH = minor.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0);
|
|
1095
|
+
var slices = major.map(function (r, i) { return { i: i, name: r.name, hours: (r.attentionHours || 0), col: RDO_PIE[i % RDO_PIE.length] }; });
|
|
1096
|
+
if (minorH > 0) slices.push({ i: major.length, name: "Others (" + number(minor.length) + (minor.length === 1 ? " repo)" : " repos)"), hours: minorH, col: "#b3aea0" });
|
|
1097
|
+
var c0 = 0;
|
|
1098
|
+
var segs = slices.map(function (sl) {
|
|
1099
|
+
var pct = sl.hours / total * 100;
|
|
1100
|
+
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>';
|
|
1101
|
+
c0 += pct; return seg;
|
|
1102
|
+
}).join("");
|
|
1103
|
+
var donut = '<div class="donut"><svg viewBox="0 0 42 42" class="donutSvg">' + segs + '</svg>' +
|
|
1104
|
+
'<div class="center"><div><b>' + number(list.length) + '</b><span>repos</span></div></div><div class="donut-tip"></div></div>';
|
|
1105
|
+
var rows = slices.map(function (sl) {
|
|
1106
|
+
var pct = Math.round(sl.hours / total * 100), w = Math.round(sl.hours / maxH * 100);
|
|
1107
|
+
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 · ' + pct + '%</span>' +
|
|
1108
|
+
'<span class="rbar"><i style="width:' + w + '%;background:' + sl.col + '"></i></span></div>';
|
|
1109
|
+
}).join("");
|
|
1110
|
+
return '<div class="terr">' + donut + '<div>' + rows + '</div></div>';
|
|
1111
|
+
}
|
|
1112
|
+
function rdoTokenStack(ip) {
|
|
1113
|
+
ip = Math.max(0, Math.min(100, ip || 0)); var op = 100 - ip;
|
|
1114
|
+
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>' +
|
|
1115
|
+
'<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>';
|
|
1116
|
+
}
|
|
1117
|
+
function rdoCostStack(c) {
|
|
1118
|
+
var t = c.total || 1; function s(col, v) { return '<div class="seg" style="width:' + Math.max(0, (v / t) * 100) + '%;background:' + col + '"></div>'; }
|
|
1119
|
+
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>' +
|
|
1120
|
+
'<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>' +
|
|
1121
|
+
'<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>';
|
|
1122
|
+
}
|
|
1123
|
+
function rdoModelRows(byModel) {
|
|
1124
|
+
return Object.keys(byModel || {}).map(function (k) { return [k, byModel[k]]; }).sort(function (a, b) { return b[1].cost - a[1].cost; })
|
|
1125
|
+
.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("");
|
|
1126
|
+
}
|
|
1127
|
+
function rdoEvidence(files) {
|
|
1128
|
+
var list = files || [];
|
|
1129
|
+
function rowHtml(f) {
|
|
1130
|
+
var tag = f.everChanged ? '' : '<span class="rdo-tag">never changed</span>';
|
|
1131
|
+
return '<div class="ex-row"><div><div class="ex-file">' + esc(f.file) + '</div><div class="ex-meta">' + esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + tag + '</div></div><div class="ex-reads">' + number(f.totalReads) + '×</div></div>';
|
|
1132
|
+
}
|
|
1133
|
+
var firstN = 5;
|
|
1134
|
+
var head = list.slice(0, firstN).map(rowHtml).join("");
|
|
1135
|
+
var rest = list.slice(firstN).map(rowHtml).join("");
|
|
1136
|
+
var moreBtn = list.length > firstN ? '<button class="moreBtn" data-rdo-toggle>Show all ' + number(list.length) + ' files ↓</button>' : '';
|
|
1137
|
+
return head + '<div class="collapse"><div class="collapse-inner">' + rest + '</div></div>' + moreBtn;
|
|
1138
|
+
}
|
|
1139
|
+
function rdoNotes() {
|
|
1140
|
+
var r = report || {}, s = r.scale || {}, c = r.cost || {}, cl = r.cleanliness || {}, rh = r.rhythm || {}, uwh = r.userWorkingHours || {};
|
|
1141
|
+
return {
|
|
1142
|
+
"repeated-reads": { k: "Cleanliness", t: "Repeated reads", b: '<p>' + number(cl.staleReadSharePct) + '% of file reads reopened a file already seen <b>unchanged</b> — ' + 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>' },
|
|
1143
|
+
"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>' },
|
|
1144
|
+
"tokens-moved": { k: "Scale", t: "Tokens moved", b: '<p>' + big(s.totalTokens) + ' tokens read and written. ' + number(s.inputPct) + '% was <b>input</b> — context fed in, not output generated.</p>' },
|
|
1145
|
+
"api-value": { k: "Cost", t: "API-equivalent", b: '<p>' + money(c.total) + ' at API list prices — a measure of scale, not a charge. Subscriptions were not billed this.</p>' },
|
|
1146
|
+
"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–4am); ' + number(rh.lateNightShare) + '% of your prompts land there.</p>' },
|
|
1147
|
+
"time-by-repo": { k: "Focus", t: "Where your time goes", b: '<p>Agent attention hours split by repository — where your coding actually happened.</p>' },
|
|
1148
|
+
"cost-went": { k: "Cost", t: "Where the tokens & cost go", b: '<p>Most volume is input — 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>' },
|
|
1149
|
+
"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>' }
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function rdoOpenNote(key) {
|
|
1153
|
+
var d = rdoNotes()[key]; if (!d) return; var dlg = document.getElementById("echo-note"); if (!dlg) return;
|
|
1154
|
+
document.getElementById("echo-note-k").textContent = d.k;
|
|
1155
|
+
document.getElementById("echo-note-t").textContent = d.t;
|
|
1156
|
+
document.getElementById("echo-note-b").innerHTML = d.b;
|
|
1157
|
+
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
|
|
1158
|
+
}
|
|
1159
|
+
function rdoCountUp(el) {
|
|
1160
|
+
var target = parseFloat(el.getAttribute("data-count")), kind = el.getAttribute("data-fmt");
|
|
1161
|
+
if (rdoReduce || !isFinite(target)) { el.textContent = rdoFmt(target, kind); return; }
|
|
1162
|
+
var dur = 950, start = null;
|
|
1163
|
+
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); }
|
|
1164
|
+
requestAnimationFrame(tick);
|
|
1165
|
+
}
|
|
1166
|
+
function rdoKpi(cls, value, kind, label, sub, key) {
|
|
1167
|
+
return '<div class="rdo-card rdo-kpi ' + cls + '"><button class="rdo-help" data-info="' + key + '">?</button>' +
|
|
1168
|
+
'<div class="num" data-count="' + value + '" data-fmt="' + kind + '">' + rdoFmt(value, kind) + '</div>' +
|
|
1169
|
+
'<p class="clab" style="margin-top:11px">' + esc(label) + '</p><div class="csub">' + sub + '</div></div>';
|
|
1170
|
+
}
|
|
1171
|
+
function rdoWire() {
|
|
1172
|
+
var root = document.getElementById("setup-details"); if (!root) return;
|
|
1173
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-count]"), function (el, i) { setTimeout(function () { rdoCountUp(el); }, rdoReduce ? 0 : 260 + i * 60); });
|
|
1174
|
+
Array.prototype.forEach.call(root.querySelectorAll(".rdo-help"), function (el) { el.onclick = function () { rdoOpenNote(el.getAttribute("data-info")); }; });
|
|
1175
|
+
var dlg = document.getElementById("echo-note");
|
|
1176
|
+
if (dlg) { var cl = dlg.querySelector(".nclose"); if (cl) cl.onclick = function () { dlg.close(); }; dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); }; }
|
|
1177
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-rdo-toggle]"), function (mb) {
|
|
1178
|
+
var box = mb.previousElementSibling, tx = mb.textContent;
|
|
1179
|
+
if (!box || !box.classList.contains("collapse")) return;
|
|
1180
|
+
mb.onclick = function () { var o = !box.classList.contains("open"); box.classList.toggle("open", o); mb.textContent = o ? "Show less ↑" : tx; };
|
|
1181
|
+
});
|
|
1182
|
+
var donut = root.querySelector(".terr .donut");
|
|
1183
|
+
if (donut) {
|
|
1184
|
+
var segEls = donut.querySelectorAll(".seg");
|
|
1185
|
+
var tip = donut.querySelector(".donut-tip");
|
|
1186
|
+
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"; } }
|
|
1187
|
+
Array.prototype.forEach.call(segEls, function (s) {
|
|
1188
|
+
s.addEventListener("mouseenter", function (e) {
|
|
1189
|
+
donut.classList.add("hovering");
|
|
1190
|
+
Array.prototype.forEach.call(segEls, function (x) { x.classList.toggle("on", x === s); });
|
|
1191
|
+
if (tip) { tip.textContent = s.getAttribute("data-name") + " · " + s.getAttribute("data-pct"); tip.classList.add("show"); placeTip(e); }
|
|
1192
|
+
});
|
|
1193
|
+
s.addEventListener("mousemove", placeTip);
|
|
1194
|
+
s.addEventListener("mouseleave", function () {
|
|
1195
|
+
donut.classList.remove("hovering");
|
|
1196
|
+
s.classList.remove("on");
|
|
1197
|
+
if (tip) tip.classList.remove("show");
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
var histo = root.querySelector(".rdo-histo");
|
|
1202
|
+
if (histo) {
|
|
1203
|
+
var figbox = histo.closest(".figbox");
|
|
1204
|
+
var htip = figbox ? figbox.querySelector(".histo-tip") : null;
|
|
1205
|
+
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"; }
|
|
1206
|
+
Array.prototype.forEach.call(histo.querySelectorAll(".bar"), function (b) {
|
|
1207
|
+
b.addEventListener("mouseenter", function (e) { if (htip) { htip.textContent = b.getAttribute("data-tip"); htip.classList.add("show"); placeHTip(e); } });
|
|
1208
|
+
b.addEventListener("mousemove", placeHTip);
|
|
1209
|
+
b.addEventListener("mouseleave", function () { if (htip) htip.classList.remove("show"); });
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
691
1213
|
function renderForensicReport(message) {
|
|
692
1214
|
var r = report;
|
|
693
1215
|
if (!r) { showReport(); return; }
|
|
1216
|
+
if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
|
|
1217
|
+
setCityMode(true);
|
|
694
1218
|
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 || {};
|
|
695
1219
|
var night = rf.nightExample;
|
|
696
|
-
setHead("
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
'<
|
|
1220
|
+
setHead("AI Coding City", connected ? "Signed in" : "Local");
|
|
1221
|
+
// The city iframe is already mounted by the shell and shows its own loading; just reveal the
|
|
1222
|
+
// details arrow and fill the sections, so the city never reloads from loading to full report.
|
|
1223
|
+
var dArrow = document.getElementById("details-arrow");
|
|
1224
|
+
if (dArrow) dArrow.classList.remove("hidden");
|
|
1225
|
+
var files = rf.topFiles || [], repos = r.repos || [];
|
|
1226
|
+
var topRepoHours = repos.reduce(function (t, x) { return t + (x.attentionHours || 0); }, 0);
|
|
1227
|
+
var weeks = (new Date(r.scanEndDate) - new Date(r.scanStartDate)) / (7 * 86400000);
|
|
1228
|
+
if (!isFinite(weeks) || weeks < 1) weeks = 1;
|
|
1229
|
+
var sharePct = cl.staleReadSharePct || 0;
|
|
1230
|
+
var savedCost = Math.round((c.total / weeks) * (sharePct / 100));
|
|
1231
|
+
var savedTokens = (s.totalTokens / weeks) * (sharePct / 100);
|
|
1232
|
+
var weeklyRereads = Math.round((cl.staleRereads || 0) / weeks);
|
|
1233
|
+
var targetShare = 10;
|
|
1234
|
+
document.getElementById("setup-details").innerHTML =
|
|
1235
|
+
'<div class="echo-rdo">' +
|
|
1236
|
+
(message ? '<div class="notice" style="margin-bottom:18px">' + esc(message) + '</div>' : '') +
|
|
1237
|
+
'<div class="rdo-head rise" style="--d:0ms">' +
|
|
1238
|
+
'<p class="rdo-kicker">EchoMem · Local Workspace Scan</p>' +
|
|
1239
|
+
'<h2 class="rdo-h1">Where your agents spent their attention</h2>' +
|
|
1240
|
+
'<p class="rdo-lead">A local scan of ' + number(s.sessionCount) + ' coding sessions across ' + number(s.repoCount) + ' repositories. Most of the load wasn't new thinking — it was <b>reloading context</b> your agents had already seen.</p>' +
|
|
1241
|
+
'<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) + ' – ' + esc(r.scanEndDate) + '</span></div>' +
|
|
715
1242
|
'</div>' +
|
|
716
|
-
'<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
'<div class="storyText">' +
|
|
722
|
-
'<p>On an active day you average ' + (uwh.averageDailyAttentionHours || 0) + ' hours of focus. Your longest single day was ' + (uwh.maxDailyAttentionHours || 0) + ' hours, on ' + esc(uwh.peakDay && uwh.peakDay.date) + '. You are a <b>' + esc(mu.identityLabel) + '</b> — ' + esc(mu.topModel) + ' carried ' + number(mu.topModelShare) + '% of the load.</p>' +
|
|
723
|
-
'<p>And I can see when you work. ' + number(rh.lateNightShare) + '% of your prompts land after 10pm — ' + number(rh.lateNightSessionCount) + ' sessions ran deep into the night. Your busiest day is ' + esc(rh.busiestWeekday) + '. The red bars are the late hours you kept going.</p>' +
|
|
724
|
-
'</div>' +
|
|
725
|
-
'<div class="panel">' + histogram(rh.hourHistogram) + '<div class="chips">' + (r.repos || []).slice(0, 6).map(function (x) { return '<span>' + esc(x.name) + ' · ' + (x.attentionHours || 0) + 'h</span>'; }).join("") + '</div></div>' +
|
|
1243
|
+
'<div class="rdo-kpis">' +
|
|
1244
|
+
'<div class="rise" style="--d:60ms">' + rdoKpi("hero", cl.staleReadSharePct, "pct", "repeated reads", number(cl.staleRereads) + ' stale rereads', "repeated-reads") + '</div>' +
|
|
1245
|
+
'<div class="rise" style="--d:110ms">' + rdoKpi("", uwh.userAttentionHours, "hours", "agent attention", rdoDec(uwh.averageDailyAttentionHours) + 'h/day avg', "agent-attention") + '</div>' +
|
|
1246
|
+
'<div class="rise" style="--d:160ms">' + rdoKpi("", s.totalTokens, "big", "tokens moved", number(s.inputPct) + '% input', "tokens-moved") + '</div>' +
|
|
1247
|
+
'<div class="rise" style="--d:210ms">' + rdoKpi("", c.total, "money", "API-equivalent", 'list price, not billed', "api-value") + '</div>' +
|
|
726
1248
|
'</div>' +
|
|
727
|
-
'<div class="
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
'<p>' + number(s.inputPct) + '% of your tokens are input. Output — the part you picture paying for — is ' + money(c.outputCost) + '. Cold input is only ' + money(c.coldInputCost) + '.</p>' +
|
|
734
|
-
'<p>Cache is cheap per token, so you stop worrying about it. But the volume is enormous: cache reads alone cost ' + money(c.cacheReadCost) + ', cache writes ' + money(c.cacheWriteCost) + '. You paid ' + money(c.total) + ' in API-equivalent worth, and almost all of it is the agent re-loading context, turn after turn.</p>' +
|
|
1249
|
+
'<div class="rdo-grid">' +
|
|
1250
|
+
'<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 · <b>' + rdoDec(uwh.averageDailyAttentionHours) + 'h</b> avg active day</div>' + rdoHisto(rh.hourHistogram) + '</div>' +
|
|
1251
|
+
'<div class="rdo-card rise" style="--d:310ms"><button class="rdo-help" data-info="cost-went">?</button><p class="clab">Where the tokens & cost go</p><div class="csub"><b>' + number(s.inputPct) + '%</b> input · <b>' + money(c.total) + '</b> API-equivalent</div>' +
|
|
1252
|
+
'<div class="ledgerCols"><div><div class="lblrow">Tokens — in vs. out</div>' + rdoTokenStack(s.inputPct) + '</div><div><div class="lblrow">Cost by token type</div>' + rdoCostStack(c) + '</div></div>' +
|
|
1253
|
+
'<div class="collapse"><div class="collapse-inner" style="padding-top:6px">' + rdoModelRows(c.byModel) + '</div></div>' +
|
|
1254
|
+
'<button class="moreBtn" data-rdo-toggle>Break down by model ↓</button>' +
|
|
735
1255
|
'</div>' +
|
|
736
|
-
'<div class="
|
|
1256
|
+
'<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>' +
|
|
1257
|
+
'<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>' +
|
|
737
1258
|
'</div>' +
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
'<p>That is the pattern everywhere. ' + number(cl.staleReadSharePct) + '% of the content the agent pulled in was re-reading files that never changed since the last look. It re-learns your repo, your design rules, your structure — over and over — instead of remembering them.</p>' +
|
|
745
|
-
'<p>This is not your fault. The agent is stateless. Every new session is a blank page; every long session pushes earlier reads out of the window. So it reads the same files again, and you wait.</p>' +
|
|
1259
|
+
'<section class="outcome rise" style="--d:460ms">' +
|
|
1260
|
+
'<div class="oc-copy">' +
|
|
1261
|
+
'<p class="rdo-kicker">What changes next week</p>' +
|
|
1262
|
+
'<h2>Echo remembers, so your agents stop re-reading.</h2>' +
|
|
1263
|
+
'<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 — and it compounds every session.</p>' +
|
|
1264
|
+
'<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 ≤ ' + targetShare + '%</div></div>' +
|
|
746
1265
|
'</div>' +
|
|
747
|
-
'<div class="
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
'<div class="storyText">' +
|
|
755
|
-
'<p>I do not touch the real work — the building, the judgment, the direction. I remove the part that should never repeat: warm-up, unchanged-file rereads, re-learning your repo and your rules every session.</p>' +
|
|
756
|
-
'<p>Direct estimate from your own data: about ' + (aw.hours || 0) + ' hours of avoidable wait, roughly ' + (aw.days || 0) + ' focused days. Every file I hold becomes a compounding asset across every future session and every tool. You stop paying the re-reading tax twice.</p>' +
|
|
757
|
-
'<p id="signin-help">Connect the same EchoMem account you want memories saved to. After sign-in returns here, you choose what to extract.</p>' +
|
|
1266
|
+
'<div class="oc-targets">' +
|
|
1267
|
+
'<p class="ot-title">Next week’s target</p>' +
|
|
1268
|
+
'<p class="ot-note">What Echo saves you in a week, once it recalls instead of re-reading:</p>' +
|
|
1269
|
+
'<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>' +
|
|
1270
|
+
'<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>' +
|
|
1271
|
+
'<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>' +
|
|
1272
|
+
'<button class="rdo-cta" data-connect-echo>' + (connected ? "Continue →" : "Connect Echo →") + '</button>' +
|
|
758
1273
|
'</div>' +
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
'<div class="promiseActions">' +
|
|
764
|
-
'<button id="signin" class="primary">' + (connected ? "Continue" : "Connect Echo") + '</button>' +
|
|
765
|
-
'<a id="signin-fallback" class="button secondary hidden" href="' + esc(authUrl) + '">Open sign-in in this tab</a>' +
|
|
766
|
-
'</div>' +
|
|
767
|
-
'</aside>' +
|
|
1274
|
+
'</section>' +
|
|
1275
|
+
'<div class="rdo-foot rise" style="--d:520ms">' +
|
|
1276
|
+
'<span>Compiled locally from <code>~/.codex/sessions</code> and <code>~/.claude/projects</code> · API-equivalent list pricing, not a subscription bill.</span>' +
|
|
1277
|
+
'<span class="priv">No transcripts leave this machine.</span>' +
|
|
768
1278
|
'</div>' +
|
|
769
|
-
'</
|
|
1279
|
+
'</div>';
|
|
1280
|
+
rdoWire();
|
|
770
1281
|
bindConnect();
|
|
1282
|
+
bindDetailsArrow();
|
|
771
1283
|
}
|
|
772
1284
|
|
|
773
1285
|
/* ---------- dashboard (post-auth extraction) ---------- */
|
|
@@ -775,6 +1287,7 @@ export function renderSetupPage() {
|
|
|
775
1287
|
return '<div class="status-strip ' + esc(kind || "") + '"><span class="dot"></span><div><strong>' + esc(title) + '</strong><p>' + esc(detail) + '</p></div></div>';
|
|
776
1288
|
}
|
|
777
1289
|
function renderBridgeIssue(error) {
|
|
1290
|
+
setCityMode(false);
|
|
778
1291
|
setHead("Local bridge issue", "Retrying");
|
|
779
1292
|
app.className = "loading-panel";
|
|
780
1293
|
app.innerHTML =
|
|
@@ -783,11 +1296,13 @@ export function renderSetupPage() {
|
|
|
783
1296
|
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') + '</div>';
|
|
784
1297
|
}
|
|
785
1298
|
function renderLogoutPending() {
|
|
1299
|
+
setCityMode(false);
|
|
786
1300
|
setHead("Signing out locally", "Working");
|
|
787
1301
|
app.className = "loading-panel";
|
|
788
1302
|
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>';
|
|
789
1303
|
}
|
|
790
1304
|
function renderLogoutIssue(error) {
|
|
1305
|
+
setCityMode(false);
|
|
791
1306
|
setHead("Sign out did not finish", "Needs attention");
|
|
792
1307
|
app.className = "loading-panel";
|
|
793
1308
|
app.innerHTML =
|
|
@@ -816,9 +1331,10 @@ export function renderSetupPage() {
|
|
|
816
1331
|
async function waitForStats() {
|
|
817
1332
|
if (statsPollStarted) return;
|
|
818
1333
|
statsPollStarted = true;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
app.
|
|
1334
|
+
setCityMode(false);
|
|
1335
|
+
setHead("Turn history into memory", "Working");
|
|
1336
|
+
app.className = "";
|
|
1337
|
+
app.innerHTML = '<div class="miniLoad"><h2>Preparing your conversations</h2><p>Sizing what is new since your last run.</p><div class="miniBar"><i></i></div></div>';
|
|
822
1338
|
try {
|
|
823
1339
|
var started = Date.now();
|
|
824
1340
|
var lastError = "";
|
|
@@ -843,7 +1359,16 @@ export function renderSetupPage() {
|
|
|
843
1359
|
}
|
|
844
1360
|
} finally { statsPollStarted = false; }
|
|
845
1361
|
}
|
|
1362
|
+
// The plate slot: ONLY the WebGL clay city (echo-extraction-plate.html). It self-fetches /report
|
|
1363
|
+
// (your repos) and polls /progress (extraction state), growing as memories are created. The old SVG
|
|
1364
|
+
// fallback board was removed — it was bleeding through behind the transparent canvas (the double board).
|
|
1365
|
+
function plateBlock() {
|
|
1366
|
+
return '<div class="exPlate"><div class="plateStack">' +
|
|
1367
|
+
'<iframe class="plateGl" title="Extraction plate" src="/city/echo-extraction-plate.html?nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
|
|
1368
|
+
'</div></div>';
|
|
1369
|
+
}
|
|
846
1370
|
function renderDashboard(error) {
|
|
1371
|
+
setCityMode(false);
|
|
847
1372
|
var migratable = stats && stats.migratable ? stats.migratable : {};
|
|
848
1373
|
var pending = stats && stats.migratable ? stats.migratable.pending : null;
|
|
849
1374
|
var hasPendingCount = typeof pending === "number";
|
|
@@ -851,76 +1376,81 @@ export function renderSetupPage() {
|
|
|
851
1376
|
var isPartial = !!(stats && stats.partial);
|
|
852
1377
|
var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
|
|
853
1378
|
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
854
|
-
var buckets = migrationBuckets(migratable);
|
|
855
1379
|
var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
|
|
856
|
-
var knownDone = hasPendingCount && pending === 0;
|
|
857
|
-
var canExtract = hasPendingCount && pending > 0;
|
|
858
1380
|
var sessions = stats && stats.sessions ? stats.sessions : {};
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
var
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
var
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1381
|
+
// The "quick" phase counts only this device's local ledger; it does not know what the account
|
|
1382
|
+
// already imported elsewhere, so it over-counts (e.g. 10). The count is only trustworthy after the
|
|
1383
|
+
// account check (phase "account"/"exact"/"full"), which corrects it (e.g. 3). Until then we show a
|
|
1384
|
+
// "counting" state with no number, so the user never sees the count jump down.
|
|
1385
|
+
var pendingTrusted = hasPendingCount && !!discovery.phase && discovery.phase !== "quick";
|
|
1386
|
+
var knownDone = pendingTrusted && pending === 0;
|
|
1387
|
+
var canExtract = pendingTrusted && pending > 0;
|
|
1388
|
+
var pendN = hasPendingCount ? pending : 0;
|
|
1389
|
+
var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
|
|
1390
|
+
var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
|
|
1391
|
+
setHead("Turn history into memory", localScanReady ? "Ready" : "Scanning");
|
|
1392
|
+
var headline = !pendingTrusted
|
|
1393
|
+
? "Counting your new conversations…"
|
|
1394
|
+
: (knownDone ? "You are all caught up." : (pendN === 1 ? "1 new conversation to turn into memory." : number(pendN) + " new conversations to turn into memory."));
|
|
1395
|
+
var sub = !pendingTrusted
|
|
1396
|
+
? "Echo is matching your local history against what is already in memory."
|
|
1397
|
+
: (knownDone
|
|
1398
|
+
? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing — Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
|
|
1399
|
+
: "Already-processed conversations are skipped, so re-running stays incremental. About " + etaLabel + ".");
|
|
1400
|
+
var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? ("Extract " + number(pendN) + " conversation" + (pendN === 1 ? "" : "s")) : "All caught up");
|
|
1401
|
+
// Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
|
|
1402
|
+
// (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
|
|
1403
|
+
// Later polls only patch the text/counts in place; the iframe stays mounted.
|
|
1404
|
+
var live = dashMounted && document.getElementById("exHeadline");
|
|
1405
|
+
if (!live) {
|
|
1406
|
+
extractMounted = false;
|
|
1407
|
+
app.className = "";
|
|
1408
|
+
app.innerHTML =
|
|
1409
|
+
'<div id="exBanner"></div>' +
|
|
1410
|
+
'<section class="exHero">' +
|
|
1411
|
+
'<div class="exCopy">' +
|
|
1412
|
+
'<p class="kicker-mono" id="exKicker"></p>' +
|
|
1413
|
+
'<h2 id="exHeadline"></h2>' +
|
|
1414
|
+
'<p class="exSub" id="exSub"></p>' +
|
|
1415
|
+
'<div class="exLegend" id="exLegend"></div>' +
|
|
1416
|
+
'<div class="actions">' +
|
|
1417
|
+
'<button id="migrate" class="primary"></button>' +
|
|
1418
|
+
'<button id="skip" class="secondary"></button>' +
|
|
1419
|
+
'</div>' +
|
|
1420
|
+
'<p class="exHint">No transcripts leave this machine until you start extraction.</p>' +
|
|
1421
|
+
'</div>' +
|
|
1422
|
+
plateBlock() +
|
|
1423
|
+
'</section>';
|
|
1424
|
+
document.getElementById("migrate").onclick = startMigrate;
|
|
1425
|
+
document.getElementById("skip").onclick = skip;
|
|
1426
|
+
dashMounted = true;
|
|
886
1427
|
}
|
|
887
|
-
|
|
888
|
-
app.className = "";
|
|
889
|
-
app.innerHTML =
|
|
1428
|
+
document.getElementById("exBanner").innerHTML =
|
|
890
1429
|
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
|
|
891
|
-
(error ? '<div class="error">' + esc(error) + '</div>' : '')
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
metric("Sessions found", number(sessions.total), number(sessions.codex) + " Codex + " + number(sessions.claudeCode) + " Claude Code") +
|
|
905
|
-
metric("Memories captured", countOrChecking(stats && stats.memoriesCaptured), "already in EchoMem") +
|
|
906
|
-
'</div>' +
|
|
907
|
-
'<div class="actions doctor-actions">' +
|
|
908
|
-
'<button id="migrate" class="primary" ' + (canExtract ? "" : "disabled") + '>Extract conversations</button>' +
|
|
909
|
-
'<button id="skip" class="secondary">' + (knownDone ? "Finish setup" : "Not today") + '</button>' +
|
|
910
|
-
'<button id="logout" class="secondary">Sign out locally</button>' +
|
|
911
|
-
'</div>';
|
|
912
|
-
document.getElementById("migrate").onclick = startMigrate;
|
|
913
|
-
document.getElementById("skip").onclick = skip;
|
|
914
|
-
document.getElementById("logout").onclick = localLogout;
|
|
1430
|
+
(error ? '<div class="error">' + esc(error) + '</div>' : '');
|
|
1431
|
+
document.getElementById("exKicker").textContent = "Local scan · " + number(sessions.total) + " sessions found";
|
|
1432
|
+
document.getElementById("exHeadline").textContent = headline;
|
|
1433
|
+
document.getElementById("exSub").textContent = sub;
|
|
1434
|
+
document.getElementById("exLegend").innerHTML = pendingTrusted
|
|
1435
|
+
? '<span class="lgDot lgNew"></span>new (' + esc(number(pendN)) + ')<span class="lgDot lgDim"></span>already in memory (' + esc(number(alreadyN)) + ')'
|
|
1436
|
+
: '<span class="lgDot lgDim"></span>matching your history against saved memory…';
|
|
1437
|
+
// While still counting, hide the primary button entirely (a disabled "Counting…" button looked
|
|
1438
|
+
// half-baked) — the headline + legend already say it is working; only "Not now" stays available.
|
|
1439
|
+
var migrateBtn = document.getElementById("migrate");
|
|
1440
|
+
if (pendingTrusted) { migrateBtn.style.display = ""; migrateBtn.textContent = extractLabel; migrateBtn.disabled = !canExtract; }
|
|
1441
|
+
else { migrateBtn.style.display = "none"; }
|
|
1442
|
+
document.getElementById("skip").textContent = knownDone ? "Finish setup" : "Not now";
|
|
915
1443
|
}
|
|
916
1444
|
async function startMigrate() {
|
|
917
1445
|
if (decisionMade) return;
|
|
918
1446
|
decisionMade = true;
|
|
1447
|
+
setCityMode(false);
|
|
919
1448
|
setHead("Extracting", "Starting");
|
|
920
1449
|
app.className = "";
|
|
921
|
-
app.innerHTML = '<
|
|
1450
|
+
app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically — keep this terminal open.</p><div class="miniBar"><i></i></div></div>';
|
|
922
1451
|
try {
|
|
923
1452
|
var started = await postJson("/migrate", {});
|
|
1453
|
+
extractMounted = false;
|
|
924
1454
|
renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
|
|
925
1455
|
await pollProgress();
|
|
926
1456
|
} catch (e) {
|
|
@@ -937,90 +1467,56 @@ export function renderSetupPage() {
|
|
|
937
1467
|
}
|
|
938
1468
|
}
|
|
939
1469
|
function renderProgress(progress) {
|
|
1470
|
+
setCityMode(false);
|
|
940
1471
|
var status = progress.status;
|
|
941
1472
|
var done = status === "completed";
|
|
942
1473
|
var stopped = status === "failed";
|
|
943
1474
|
var total = progress.total || progress.jobCount || 0;
|
|
944
1475
|
var completed = progress.completed || 0;
|
|
945
1476
|
var failed = progress.failed || 0;
|
|
946
|
-
var running = progress.running || 0;
|
|
947
|
-
var queued = progress.queued || 0;
|
|
948
1477
|
var extracted = progress.extracted || 0;
|
|
949
1478
|
var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
|
|
1479
|
+
var preparing = (status === "starting" || status === "preparing");
|
|
950
1480
|
app.className = "";
|
|
951
|
-
|
|
952
|
-
|
|
1481
|
+
// Mount the shell ONCE so the WebGL plate iframe survives every poll (it self-polls /progress);
|
|
1482
|
+
// each poll only rebuilds the text column.
|
|
1483
|
+
if (!extractMounted) {
|
|
953
1484
|
app.innerHTML =
|
|
954
|
-
'<section class="
|
|
955
|
-
|
|
956
|
-
'<strong class="finishBig">' + esc(number(extracted)) + '</strong>' +
|
|
957
|
-
'<span class="finishLabel">' + (extracted === 1 ? "memory" : "memories") + ' captured</span>' +
|
|
958
|
-
'<p>' + (done
|
|
959
|
-
? "from " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session will recall instead of re-reading — and it keeps compounding."
|
|
960
|
-
: (String(progress.error) === "key-expired"
|
|
961
|
-
? "Your encryption key wasn't accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
|
|
962
|
-
: esc(progress.error || "Some conversations did not finish.") + " Re-run <code>echomem-mcp migrate</code> to finish the rest; already-done conversations are skipped.")) + '</p>' +
|
|
963
|
-
(done && failed ? '<p class="helper">' + esc(number(failed)) + ' could not be extracted.</p>' : "") +
|
|
1485
|
+
'<section class="exHero"><div class="exCopy" id="exText"></div>' +
|
|
1486
|
+
plateBlock() +
|
|
964
1487
|
'</section>';
|
|
965
|
-
|
|
1488
|
+
extractMounted = true;
|
|
966
1489
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
setHead("
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
"
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
var stepCls = i < activeStep ? "done" : (i === activeStep ? "active" : "");
|
|
981
|
-
stepsHtml += '<li class="' + stepCls + '"><span class="dot"></span><span>' + esc(prepLabels[i]) + '</span></li>';
|
|
982
|
-
}
|
|
983
|
-
app.innerHTML =
|
|
984
|
-
'<section class="privacy"><span>Preparing</span><p>Nothing has left your machine yet — Echo is getting your sessions ready locally. This takes a few seconds before extraction starts.</p></section>' +
|
|
985
|
-
'<section class="doctor-hero">' +
|
|
986
|
-
'<div class="doctor-copy">' +
|
|
987
|
-
'<p class="label">Now working on</p>' +
|
|
988
|
-
'<h2>' + esc(prepLabels[activeStep]) + '…</h2>' +
|
|
989
|
-
'<div class="track indef"><div class="fill"></div></div>' +
|
|
990
|
-
'<ul class="prep-steps">' + stepsHtml + '</ul>' +
|
|
991
|
-
'</div>' +
|
|
992
|
-
'<aside class="doctor-score">' +
|
|
993
|
-
'<span>Conversations</span>' +
|
|
994
|
-
'<strong>' + (total ? esc(number(total)) : "…") + '</strong>' +
|
|
995
|
-
'<p>found locally</p>' +
|
|
996
|
-
'</aside>' +
|
|
997
|
-
'</section>';
|
|
1490
|
+
var t = document.getElementById("exText");
|
|
1491
|
+
if (!t) return;
|
|
1492
|
+
if (done || stopped) {
|
|
1493
|
+
setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
|
|
1494
|
+
t.innerHTML =
|
|
1495
|
+
'<p class="kicker-mono">' + (done ? "Extraction complete" : "Extraction stopped") + '</p>' +
|
|
1496
|
+
'<h2>' + (done ? esc(number(extracted)) + ' new ' + (extracted === 1 ? "memory" : "memories") + '.' : "Some conversations did not finish.") + '</h2>' +
|
|
1497
|
+
'<p class="exSub">' + (done
|
|
1498
|
+
? "From " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session recalls instead of re-reading — and it keeps compounding."
|
|
1499
|
+
: (String(progress.error) === "key-expired"
|
|
1500
|
+
? "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."
|
|
1501
|
+
: esc(progress.error || "Re-run extraction to finish the rest — already-done conversations are skipped."))) + '</p>' +
|
|
1502
|
+
'<div class="recon">✓ ' + esc(number(completed)) + ' extracted · ' + esc(number(extracted)) + ' memories · ' + esc(number(failed)) + ' error' + (failed === 1 ? "" : "s") + '</div>';
|
|
998
1503
|
return;
|
|
999
1504
|
}
|
|
1000
|
-
setHead("Extracting", "Extracting");
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
'<
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
'<div class="counts"><span>' + esc(number(completed)) + ' done</span><span>' + esc(number(running)) + ' running</span><span>' + esc(number(queued)) + ' queued</span>' + (failed ? '<span>' + esc(number(failed)) + ' failed</span>' : "") + '</div>' +
|
|
1009
|
-
(progress.latest ? '<p class="helper" style="margin-top:10px">Now: ' + esc(progress.latest) + '</p>' : "") +
|
|
1010
|
-
(progress.capped ? '<p class="helper">Importing the newest ' + esc(number(progress.capped)) + '; re-run <code>echomem-mcp migrate</code> later for older sessions.</p>' : "") +
|
|
1011
|
-
'</div>' +
|
|
1012
|
-
'<aside class="doctor-score">' +
|
|
1013
|
-
'<span>Memories captured</span>' +
|
|
1014
|
-
'<strong>' + esc(number(extracted)) + '</strong>' +
|
|
1015
|
-
'<p>' + (extracted ? "and growing" : "starting…") + '</p>' +
|
|
1016
|
-
'</aside>' +
|
|
1017
|
-
'</section>';
|
|
1505
|
+
setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
|
|
1506
|
+
var statusLine = progress.latest ? ("Now: " + progress.latest) : (preparing ? "Getting your sessions ready locally…" : "");
|
|
1507
|
+
t.innerHTML =
|
|
1508
|
+
'<p class="kicker-mono">' + (preparing ? "Preparing" : "Extraction progress") + '</p>' +
|
|
1509
|
+
'<h2>' + (preparing ? "Sizing your sessions…" : (total ? esc(number(completed)) + ' of ' + esc(number(total)) : "Starting…")) + '</h2>' +
|
|
1510
|
+
'<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
|
|
1511
|
+
'<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (failed ? ' · ' + esc(number(failed)) + ' failed' : "") + '.' + (statusLine ? ' ' + esc(statusLine) : "") + '</p>' +
|
|
1512
|
+
(progress.capped ? '<p class="exHint">Importing the newest ' + esc(number(progress.capped)) + ' — re-run later for older sessions.</p>' : '<p class="exHint">Keep this terminal open — nothing leaves your machine beyond what you extract. You can use EchoMem in your editor now.</p>');
|
|
1018
1513
|
}
|
|
1019
1514
|
async function skip() {
|
|
1020
1515
|
if (decisionMade) return;
|
|
1021
1516
|
decisionMade = true;
|
|
1022
1517
|
try {
|
|
1023
1518
|
await postJson("/skip", {});
|
|
1519
|
+
setCityMode(false);
|
|
1024
1520
|
setHead("Setup complete", "Done");
|
|
1025
1521
|
app.className = "notice";
|
|
1026
1522
|
app.innerHTML = '<h2 style="color:#203820">All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
|
|
@@ -1060,8 +1556,7 @@ export function renderSetupPage() {
|
|
|
1060
1556
|
if (res.status === 200) { report = await res.json(); renderForensicReport(); return; }
|
|
1061
1557
|
if (res.status === 202) {
|
|
1062
1558
|
fails = 0;
|
|
1063
|
-
|
|
1064
|
-
renderLoading(prog);
|
|
1559
|
+
renderLoading();
|
|
1065
1560
|
} else {
|
|
1066
1561
|
throw new Error(await res.text() || ("HTTP " + res.status));
|
|
1067
1562
|
}
|
|
@@ -1083,6 +1578,8 @@ export function renderSetupPage() {
|
|
|
1083
1578
|
connected = true;
|
|
1084
1579
|
closeAuthWindow();
|
|
1085
1580
|
void waitForStats();
|
|
1581
|
+
} else if (data.type === "echomem:connect-click") {
|
|
1582
|
+
onConnectClick();
|
|
1086
1583
|
}
|
|
1087
1584
|
});
|
|
1088
1585
|
async function init() {
|
|
@@ -1094,6 +1591,10 @@ export function renderSetupPage() {
|
|
|
1094
1591
|
setHead("EchoMem connected", "Done");
|
|
1095
1592
|
app.className = "notice";
|
|
1096
1593
|
app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
|
|
1594
|
+
// Fallback: if window.close() was blocked (auth opened as a tab, or the browser refused),
|
|
1595
|
+
// this window would otherwise hang on "Returning..." forever. After a beat, take over here
|
|
1596
|
+
// so the user always lands on the dashboard instead of a dead page.
|
|
1597
|
+
setTimeout(function () { if (!window.closed) void waitForStats(); }, 1500);
|
|
1097
1598
|
} else {
|
|
1098
1599
|
// Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
|
|
1099
1600
|
void waitForStats();
|