@echomem/mcp 1.4.44 → 1.4.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -25
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +2232 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/context-analysis/claude-native-canonical.js +2 -2
- package/dist/context-analysis/vendored-canonical.js +2 -2
- package/dist/context-analysis/workspace-report.js +3 -3
- package/dist/forensics.js +1531 -0
- package/dist/hud/hooks.js +31 -43
- package/dist/index.js +79 -15
- package/dist/local-data-paths.js +38 -0
- package/dist/migrate.js +140 -70
- package/dist/report.js +721 -0
- package/dist/save-checkpoint-hook.js +1 -1
- package/dist/setup-page/client-core.js +372 -18
- package/dist/setup-page/client-extraction.js +204 -37
- package/dist/setup-page/client-lifecycle.js +101 -31
- package/dist/setup-page/client-report-audit.js +819 -0
- package/dist/setup-page/client-report-city.js +356 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +2 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +31 -1
- package/dist/setup-page/styles-foundation.js +89 -0
- package/dist/setup-page/styles-mvp.js +155 -10
- package/dist/setup-page/styles-website-alignment.js +204 -0
- package/dist/setup-page/styles.js +4 -0
- package/dist/setup-page.js +4 -4
- package/dist/setup-preview.js +212 -4
- package/dist/setup.js +702 -321
- package/dist/source-session.js +3 -9
- package/dist/v1-contract.js +8 -0
- package/package.json +7 -9
- package/dist/config-files.js +0 -63
- package/dist/local-jsonl.js +0 -87
- package/dist/onboarding-stats.js +0 -16
|
@@ -10,7 +10,7 @@ const CHECKPOINT_REASON = [
|
|
|
10
10
|
"Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
|
|
11
11
|
"On the first request_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per groupId. Relay the tool's exact text question and call set_group_session_sharing only after an explicit Yes/No. Silence leaves that group's state unset. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
|
|
12
12
|
"Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
13
|
-
"If EchoMem reports that the encrypted vault is locked,
|
|
13
|
+
"If EchoMem reports that the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
|
|
14
14
|
].join(" ");
|
|
15
15
|
function currentTurnSlice(transcript) {
|
|
16
16
|
const lines = transcript.split(/\r?\n/);
|
|
@@ -6,28 +6,27 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
6
6
|
var app = document.getElementById("app");
|
|
7
7
|
var title = document.getElementById("title");
|
|
8
8
|
var status = document.getElementById("status");
|
|
9
|
-
|
|
9
|
+
var report = null;
|
|
10
|
+
var reportEnvelope = null;
|
|
11
|
+
// The local bridge is authoritative. Login never gets access to scan/import routes;
|
|
10
12
|
// onboarding may request the separate local-history permission.
|
|
11
13
|
var setupFlow = "onboarding";
|
|
14
|
+
var activeScanId = "";
|
|
12
15
|
var stats = null;
|
|
13
16
|
var statsSlow = false;
|
|
14
17
|
var decisionMade = false;
|
|
15
18
|
var connected = false;
|
|
16
19
|
var localHistoryConsentGranted = false;
|
|
17
|
-
var extractMounted = false; // extraction
|
|
20
|
+
var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
|
|
18
21
|
var lastExtractionProgress = null; // restores the live extraction view if ending fails
|
|
19
22
|
var extractionRecoveryMessage = ""; // persists pause failures across resumed progress updates
|
|
20
23
|
var exStartAt = 0; // extraction start (ms) — drives the countdown
|
|
21
24
|
var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
|
|
22
25
|
var extractionEnded = false; // user chose to leave the extraction flow before natural completion
|
|
23
26
|
var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
|
|
24
|
-
var dashMounted = false; // dashboard shell mounted once; persists across /stats polls
|
|
27
|
+
var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
|
|
28
|
+
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
25
29
|
var workspacePath = "";
|
|
26
|
-
var setupPlatform = "unknown";
|
|
27
|
-
// Preview states render before /config; production init replaces these with bridge-reported
|
|
28
|
-
// capabilities before rendering a user-facing state.
|
|
29
|
-
var canOpenClaudeDesktop = true;
|
|
30
|
-
var canOpenAgentSessions = true;
|
|
31
30
|
var billingStatus = null;
|
|
32
31
|
var billingStatusLoading = false;
|
|
33
32
|
var setupPlanChoice = "";
|
|
@@ -52,7 +51,17 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
52
51
|
// Invalidates async work started for a previously connected account.
|
|
53
52
|
var accountStateEpoch = 0;
|
|
54
53
|
var statsPollStarted = false;
|
|
54
|
+
var reportPollStarted = false;
|
|
55
|
+
var reportEtaDeadline = 0;
|
|
56
|
+
var reportEtaDisplaySeconds = 0;
|
|
57
|
+
var reportEtaUpdatedAt = 0;
|
|
58
|
+
var reportEtaStage = "";
|
|
59
|
+
var reportEtaStageDone = 0;
|
|
60
|
+
var reportEtaStageElapsed = 0;
|
|
61
|
+
var reportEtaObservedRate = 0;
|
|
62
|
+
var reportEtaScanId = "";
|
|
55
63
|
var readyStage = "";
|
|
64
|
+
var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
56
65
|
|
|
57
66
|
try {
|
|
58
67
|
var storedSetupPlanChoice = sessionStorage.getItem("echomem:setup-plan:" + nonce) || "";
|
|
@@ -98,6 +107,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
98
107
|
var paths = {
|
|
99
108
|
"shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
|
|
100
109
|
"folder-read": '<path d="M3 7.5h6l2-2h4.5A2.5 2.5 0 0 1 18 8v1"/><path d="M3 7.5V18a2 2 0 0 0 2 2h7"/><circle cx="17" cy="15" r="3.5"/><path d="m19.5 17.5 2 2"/>',
|
|
110
|
+
"report": '<path d="M5 20V10"/><path d="M12 20V4"/><path d="M19 20v-7"/><path d="M3 20h18"/>',
|
|
101
111
|
"cloud-off": '<path d="m3 3 18 18"/><path d="M6.7 6.7A5.5 5.5 0 0 0 6.5 17H17"/><path d="M10.7 5.2A7 7 0 0 1 19 12.1 4 4 0 0 1 19.5 19"/>',
|
|
102
112
|
"list-check": '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m7.5 9 1.4 1.4L11.5 8"/><path d="M13.5 9h3"/><path d="m7.5 14 1.4 1.4 2.6-2.4"/><path d="M13.5 14h3"/>',
|
|
103
113
|
"pause": '<path d="M9 7v10"/><path d="M15 7v10"/>',
|
|
@@ -108,7 +118,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
108
118
|
return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
|
|
109
119
|
}
|
|
110
120
|
function setupJourney(currentStep) {
|
|
111
|
-
var labels = ["
|
|
121
|
+
var labels = ["Scan", "Report", "Connect", "Import", "Try Echo"];
|
|
112
122
|
var items = labels.map(function (label, index) {
|
|
113
123
|
var step = index + 1;
|
|
114
124
|
var state = step < currentStep ? " is-complete" : (step === currentStep ? " is-current" : "");
|
|
@@ -123,13 +133,21 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
123
133
|
title.textContent = nextTitle;
|
|
124
134
|
status.textContent = nextStatus;
|
|
125
135
|
}
|
|
136
|
+
function setCityMode(enabled) {
|
|
137
|
+
document.body.classList.toggle("cityMode", !!enabled);
|
|
138
|
+
if (enabled) document.body.classList.remove("scanMode", "extractMode", "readyMode");
|
|
139
|
+
}
|
|
140
|
+
function setScanMode(enabled) {
|
|
141
|
+
document.body.classList.toggle("scanMode", !!enabled);
|
|
142
|
+
if (enabled) document.body.classList.remove("cityMode", "extractMode", "readyMode");
|
|
143
|
+
}
|
|
126
144
|
function setExtractMode(enabled) {
|
|
127
145
|
document.body.classList.toggle("extractMode", !!enabled);
|
|
128
|
-
if (enabled) document.body.classList.remove("readyMode");
|
|
146
|
+
if (enabled) document.body.classList.remove("scanMode", "readyMode");
|
|
129
147
|
}
|
|
130
148
|
function setReadyMode(enabled) {
|
|
131
149
|
document.body.classList.toggle("readyMode", !!enabled);
|
|
132
|
-
if (enabled) document.body.classList.remove("extractMode");
|
|
150
|
+
if (enabled) document.body.classList.remove("scanMode", "cityMode", "extractMode");
|
|
133
151
|
}
|
|
134
152
|
function number(value) {
|
|
135
153
|
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
@@ -141,6 +159,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
141
159
|
if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
142
160
|
if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
|
|
143
161
|
return String(Math.round(value));
|
|
162
|
+
}
|
|
163
|
+
function big(value) {
|
|
164
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
165
|
+
var n = Math.abs(value);
|
|
166
|
+
if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
|
|
167
|
+
if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
168
|
+
if (n >= 1e3) return Math.round(value / 1e3) + "K";
|
|
169
|
+
return String(Math.round(value));
|
|
144
170
|
}
|
|
145
171
|
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
146
172
|
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
@@ -174,6 +200,12 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
174
200
|
try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
|
|
175
201
|
if (!res.ok) throw new Error(data.message || data.error || raw || ("HTTP " + res.status));
|
|
176
202
|
return data;
|
|
203
|
+
}
|
|
204
|
+
function metric(label, value, detail) {
|
|
205
|
+
return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
|
|
206
|
+
}
|
|
207
|
+
function row(label, value) {
|
|
208
|
+
return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
|
|
177
209
|
}
|
|
178
210
|
function routeError(code) {
|
|
179
211
|
if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
|
|
@@ -227,18 +259,20 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
227
259
|
}
|
|
228
260
|
function renderLocalLogin(message) {
|
|
229
261
|
var onboardingConnect = setupFlow !== "login";
|
|
262
|
+
setCityMode(false);
|
|
263
|
+
setScanMode(false);
|
|
230
264
|
setExtractMode(false);
|
|
231
265
|
setReadyMode(true);
|
|
232
266
|
setHead("Connect EchoMem", "Local login");
|
|
233
267
|
app.className = "localAuthStage localAuthWelcomeStage localAuthEmailStage";
|
|
234
268
|
app.innerHTML =
|
|
235
|
-
setupJourney(
|
|
269
|
+
setupJourney(3) +
|
|
236
270
|
'<section class="localAuthWelcome" aria-labelledby="localAuthWelcomeTitle">' +
|
|
237
271
|
'<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
|
|
238
272
|
'<div class="localAuthWelcomeCopy">' +
|
|
239
273
|
'<h2 id="localAuthWelcomeTitle">' + (onboardingConnect ? 'Take your context with you.' : 'Welcome to Echo') + '</h2>' +
|
|
240
274
|
(onboardingConnect
|
|
241
|
-
? '<p class="localAuthConnectPurpose">Connect this
|
|
275
|
+
? '<p class="localAuthConnectPurpose">Connect this Mac to Echo to use the memories you choose across your AI tools.</p>'
|
|
242
276
|
: '') +
|
|
243
277
|
'</div>' +
|
|
244
278
|
'<form class="localAuthWelcomeForm" id="localAuthEmailForm">' +
|
|
@@ -263,6 +297,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
263
297
|
updateLocalAuthSendButton();
|
|
264
298
|
}
|
|
265
299
|
function renderLocalLoginComplete() {
|
|
300
|
+
setCityMode(false);
|
|
301
|
+
setScanMode(false);
|
|
266
302
|
setExtractMode(false);
|
|
267
303
|
setReadyMode(true);
|
|
268
304
|
setHead("EchoMem connected", "Done");
|
|
@@ -271,7 +307,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
271
307
|
'<section class="localAuthComplete" aria-labelledby="localAuthCompleteTitle">' +
|
|
272
308
|
'<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
|
|
273
309
|
'<h2 id="localAuthCompleteTitle">You\'re signed in.</h2>' +
|
|
274
|
-
'<p>Return to
|
|
310
|
+
'<p>Return to Terminal.</p>' +
|
|
275
311
|
'<p class="localAuthCompleteNext">Run <code>echomem-mcp init</code> when you\'re ready to start onboarding.</p>' +
|
|
276
312
|
'</section>';
|
|
277
313
|
}
|
|
@@ -311,12 +347,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
311
347
|
}
|
|
312
348
|
}
|
|
313
349
|
function renderLocalOtp(message) {
|
|
350
|
+
setCityMode(false);
|
|
351
|
+
setScanMode(false);
|
|
314
352
|
setExtractMode(false);
|
|
315
353
|
setReadyMode(true);
|
|
316
354
|
setHead("Check your email", "Local login");
|
|
317
355
|
app.className = "localAuthStage localAuthWelcomeStage localAuthOtpStage";
|
|
318
356
|
app.innerHTML =
|
|
319
|
-
setupJourney(
|
|
357
|
+
setupJourney(3) +
|
|
320
358
|
'<section class="localAuthWelcome localAuthOtpWelcome" aria-labelledby="localAuthOtpTitle">' +
|
|
321
359
|
'<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
|
|
322
360
|
'<div class="localAuthWelcomeCopy">' +
|
|
@@ -405,12 +443,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
405
443
|
function renderLocalPassphrase(mode, email, message) {
|
|
406
444
|
localAuthEmail = email || localAuthEmail;
|
|
407
445
|
var setupMode = mode === "setup";
|
|
446
|
+
setCityMode(false);
|
|
447
|
+
setScanMode(false);
|
|
408
448
|
setExtractMode(false);
|
|
409
449
|
setReadyMode(true);
|
|
410
450
|
setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
|
|
411
451
|
app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
|
|
412
452
|
app.innerHTML =
|
|
413
|
-
setupJourney(
|
|
453
|
+
setupJourney(3) +
|
|
414
454
|
'<section class="localAuthWelcome localAuthPassphraseWelcome">' +
|
|
415
455
|
'<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
|
|
416
456
|
'<div class="localAuthWelcomeCopy">' +
|
|
@@ -471,8 +511,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
471
511
|
}
|
|
472
512
|
function onConnectClick() {
|
|
473
513
|
if (!localHistoryConsentGranted) {
|
|
474
|
-
|
|
475
|
-
setConsentStatus("Allow local
|
|
514
|
+
renderLocalScanConsent();
|
|
515
|
+
setConsentStatus("Allow the local scan to continue.", "required");
|
|
476
516
|
return;
|
|
477
517
|
}
|
|
478
518
|
if (connected) { void waitForStats(); return; }
|
|
@@ -484,4 +524,318 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
484
524
|
signin.onclick = onConnectClick;
|
|
485
525
|
});
|
|
486
526
|
}
|
|
527
|
+
// Retained from the archived pencil-pie prototype: 3D pencil-sketch pies drawn on a
|
|
528
|
+
// transparent canvas so the graphite sits directly on the page background.
|
|
529
|
+
function pencilMulberry(seed) {
|
|
530
|
+
return function () {
|
|
531
|
+
var t = seed += 0x6D2B79F5;
|
|
532
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
533
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
534
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function drawPencilPie(cv, slices, R) {
|
|
538
|
+
var TILT = 0.48, u = R / 270;
|
|
539
|
+
var TH = 58 * u, SHLEN = 154 * u, SHANG = 14 * Math.PI / 180;
|
|
540
|
+
var density = Math.min(1.1, Math.max(0.2, u * u * 2.6));
|
|
541
|
+
cv.width = Math.round(R * 2.62);
|
|
542
|
+
cv.height = Math.round(R * 1.9);
|
|
543
|
+
var cx = R * 1.14, cy = R * 0.64;
|
|
544
|
+
var g = cv.getContext("2d");
|
|
545
|
+
var random = pencilMulberry(2117 + Math.round(R * 7) + slices.length * 131);
|
|
546
|
+
function ePoint(radius, angle, yOff) { return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius * TILT + (yOff || 0) }; }
|
|
547
|
+
function ePath(radius) { g.beginPath(); g.ellipse(cx, cy, radius, radius * TILT, 0, 0, Math.PI * 2); }
|
|
548
|
+
function wPath(start, sweep) {
|
|
549
|
+
var steps = Math.max(18, Math.ceil(Math.abs(sweep) / (Math.PI / 90)));
|
|
550
|
+
g.beginPath();
|
|
551
|
+
g.moveTo(cx, cy);
|
|
552
|
+
for (var i = 0; i <= steps; i++) { var p = ePoint(R, start + sweep * (i / steps), 0); g.lineTo(p.x, p.y); }
|
|
553
|
+
g.closePath();
|
|
554
|
+
}
|
|
555
|
+
function pStroke(pathFn, color, width, repeats) {
|
|
556
|
+
var rnd = pencilMulberry(808 + Math.round(width * 100) + repeats * 47);
|
|
557
|
+
for (var i = 0; i < repeats; i++) {
|
|
558
|
+
g.save();
|
|
559
|
+
g.translate((rnd() - 0.5) * 1.25, (rnd() - 0.5) * 0.8);
|
|
560
|
+
pathFn();
|
|
561
|
+
g.strokeStyle = color;
|
|
562
|
+
g.globalAlpha = 0.58 / Math.max(1, repeats * 0.64);
|
|
563
|
+
g.lineWidth = width + rnd() * 0.45;
|
|
564
|
+
g.stroke();
|
|
565
|
+
g.restore();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function hatchClipped(clipFn, angle, spacing, color, alpha) {
|
|
569
|
+
g.save();
|
|
570
|
+
clipFn();
|
|
571
|
+
g.clip();
|
|
572
|
+
g.translate(cx, cy);
|
|
573
|
+
g.rotate(angle);
|
|
574
|
+
g.translate(-cx, -cy);
|
|
575
|
+
g.strokeStyle = color;
|
|
576
|
+
g.globalAlpha = alpha;
|
|
577
|
+
g.lineWidth = 0.58;
|
|
578
|
+
g.lineCap = "round";
|
|
579
|
+
g.beginPath();
|
|
580
|
+
for (var x = cx - R * 1.7; x < cx + R * 1.7; x += spacing) {
|
|
581
|
+
var off = (random() - 0.5) * 2;
|
|
582
|
+
g.moveTo(x + off, cy - R);
|
|
583
|
+
g.lineTo(x + off + (random() - 0.5) * 2, cy + R);
|
|
584
|
+
}
|
|
585
|
+
g.stroke();
|
|
586
|
+
g.restore();
|
|
587
|
+
}
|
|
588
|
+
// Graphite shadow rubbed into the page beneath the disc.
|
|
589
|
+
var dx = Math.cos(SHANG) * SHLEN, dy = Math.sin(SHANG) * SHLEN;
|
|
590
|
+
var shX = cx + dx * 0.46, shY = cy + TH + 12 * u + dy * 0.42;
|
|
591
|
+
g.save();
|
|
592
|
+
g.filter = "blur(" + Math.max(2, 6 * u * 1.6).toFixed(1) + "px)";
|
|
593
|
+
g.fillStyle = "rgba(31,33,28,.05)";
|
|
594
|
+
g.beginPath();
|
|
595
|
+
g.ellipse(shX, shY, R * 1.08, R * TILT * 0.82, SHANG * 0.12, 0, Math.PI * 2);
|
|
596
|
+
g.fill();
|
|
597
|
+
g.restore();
|
|
598
|
+
var L = Math.max(0.8, u * 1.9);
|
|
599
|
+
var dust = Math.round(1380 * density), pass, i;
|
|
600
|
+
var dustTones = ["rgba(35,37,32,.028)", "rgba(35,37,32,.055)", "rgba(35,37,32,.09)"];
|
|
601
|
+
for (pass = 0; pass < 3; pass++) {
|
|
602
|
+
g.beginPath();
|
|
603
|
+
for (i = pass; i < dust; i += 3) {
|
|
604
|
+
var ring = Math.pow(random(), 0.58 + pass * 0.16), ang = random() * Math.PI * 2, trail = Math.pow(random(), 2.1);
|
|
605
|
+
g.rect(shX + Math.cos(ang) * R * 1.06 * ring + dx * trail * 0.48, shY + Math.sin(ang) * R * TILT * 0.76 * ring + dy * trail * 0.32, 0.35 + random() * (1.2 + pass * 0.28), (0.35 + random()) * (0.55 + random() * 0.8));
|
|
606
|
+
}
|
|
607
|
+
g.fillStyle = dustTones[pass];
|
|
608
|
+
g.fill();
|
|
609
|
+
}
|
|
610
|
+
var strokes = Math.round(1040 * density);
|
|
611
|
+
g.save();
|
|
612
|
+
g.lineCap = "round";
|
|
613
|
+
g.lineWidth = 0.72;
|
|
614
|
+
for (pass = 0; pass < 2; pass++) {
|
|
615
|
+
g.beginPath();
|
|
616
|
+
var cross = pass === 0 ? -0.38 : 0.46;
|
|
617
|
+
for (i = pass; i < strokes; i += 2) {
|
|
618
|
+
var ring2 = Math.pow(random(), 0.62), ang2 = random() * Math.PI * 2, trail2 = Math.pow(random(), 2.05);
|
|
619
|
+
var x0 = shX + Math.cos(ang2) * R * 1.04 * ring2 + dx * trail2 * 0.5;
|
|
620
|
+
var y0 = shY + Math.sin(ang2) * R * TILT * 0.75 * ring2 + dy * trail2 * 0.34;
|
|
621
|
+
var len = (3 + random() * (9 + (1 - ring2) * 14)) * L;
|
|
622
|
+
var sa = SHANG + cross + (random() - 0.5) * 0.15;
|
|
623
|
+
g.moveTo(x0, y0);
|
|
624
|
+
g.lineTo(x0 + Math.cos(sa) * len, y0 + Math.sin(sa) * len * 0.62);
|
|
625
|
+
}
|
|
626
|
+
g.strokeStyle = pass === 0 ? "rgba(32,34,29,.14)" : "rgba(32,34,29,.11)";
|
|
627
|
+
g.stroke();
|
|
628
|
+
}
|
|
629
|
+
g.restore();
|
|
630
|
+
// Contact crevice hugging the lower ellipse.
|
|
631
|
+
g.save();
|
|
632
|
+
g.lineCap = "round";
|
|
633
|
+
g.lineWidth = 0.9;
|
|
634
|
+
var contact = Math.round(300 * density);
|
|
635
|
+
for (pass = 0; pass < 3; pass++) {
|
|
636
|
+
g.beginPath();
|
|
637
|
+
for (i = pass; i < contact; i += 3) {
|
|
638
|
+
var cAng = random() * Math.PI;
|
|
639
|
+
var dark = 0.5 + Math.cos(cAng - SHANG) * 0.5;
|
|
640
|
+
var depth = Math.pow(random(), 2.6) * (5 + dark * 8) * L + pass * 1.2;
|
|
641
|
+
var cp = ePoint(R * (0.97 + random() * 0.045), cAng, TH + 1 + depth);
|
|
642
|
+
var tx = -Math.sin(cAng), ty = Math.cos(cAng) * TILT;
|
|
643
|
+
var norm = Math.hypot(tx, ty) || 1;
|
|
644
|
+
var cLen = (3 + random() * (8 + dark * 11)) * L;
|
|
645
|
+
g.moveTo(cp.x - tx / norm * cLen * 0.5, cp.y - ty / norm * cLen * 0.5);
|
|
646
|
+
g.lineTo(cp.x + tx / norm * cLen * 0.5, cp.y + ty / norm * cLen * 0.5);
|
|
647
|
+
}
|
|
648
|
+
g.strokeStyle = ["rgba(27,29,25,.36)", "rgba(27,29,25,.25)", "rgba(27,29,25,.16)"][pass];
|
|
649
|
+
g.stroke();
|
|
650
|
+
}
|
|
651
|
+
g.restore();
|
|
652
|
+
// Cylinder wall.
|
|
653
|
+
function sidePath() {
|
|
654
|
+
var steps = 120, k;
|
|
655
|
+
g.beginPath();
|
|
656
|
+
for (k = 0; k <= steps; k++) { var sp = ePoint(R, Math.PI * k / steps, 0); if (k === 0) g.moveTo(sp.x, sp.y); else g.lineTo(sp.x, sp.y); }
|
|
657
|
+
for (k = steps; k >= 0; k--) { var sp2 = ePoint(R, Math.PI * k / steps, TH); g.lineTo(sp2.x, sp2.y); }
|
|
658
|
+
g.closePath();
|
|
659
|
+
}
|
|
660
|
+
sidePath();
|
|
661
|
+
var sideTone = g.createLinearGradient(cx - Math.cos(SHANG) * R, cy - Math.sin(SHANG) * R * TILT, cx + Math.cos(SHANG) * R, cy + Math.sin(SHANG) * R * TILT);
|
|
662
|
+
sideTone.addColorStop(0, "#e6e1d6");
|
|
663
|
+
sideTone.addColorStop(0.47, "#d8d3c7");
|
|
664
|
+
sideTone.addColorStop(1, "#aaa79f");
|
|
665
|
+
g.fillStyle = sideTone;
|
|
666
|
+
g.fill();
|
|
667
|
+
g.strokeStyle = "rgba(39,41,35,.7)";
|
|
668
|
+
g.lineWidth = 1.5;
|
|
669
|
+
g.stroke();
|
|
670
|
+
var hatchCount = Math.round(138 * Math.max(density, 0.45));
|
|
671
|
+
g.save();
|
|
672
|
+
g.lineCap = "round";
|
|
673
|
+
g.lineWidth = 0.66;
|
|
674
|
+
for (pass = 0; pass < 3; pass++) {
|
|
675
|
+
g.beginPath();
|
|
676
|
+
for (i = 1; i < hatchCount; i++) {
|
|
677
|
+
var hAng = Math.PI * i / hatchCount;
|
|
678
|
+
var hDark = 0.5 + Math.cos(hAng - SHANG) * 0.5;
|
|
679
|
+
if (Math.min(2, Math.floor(hDark * 3)) !== pass) continue;
|
|
680
|
+
var top = ePoint(R, hAng, 0);
|
|
681
|
+
var jit = (random() - 0.5) * 4 * L;
|
|
682
|
+
g.moveTo(top.x + jit * 0.25, top.y + 2 + random() * 3);
|
|
683
|
+
g.lineTo(top.x + jit, top.y + TH - 2 - random() * 5);
|
|
684
|
+
}
|
|
685
|
+
g.strokeStyle = ["rgba(37,39,34,.13)", "rgba(37,39,34,.21)", "rgba(37,39,34,.31)"][pass];
|
|
686
|
+
g.stroke();
|
|
687
|
+
}
|
|
688
|
+
g.restore();
|
|
689
|
+
pStroke(function () { g.beginPath(); g.ellipse(cx, cy, R, R * TILT, 0, 0, Math.PI, false); }, "#272923", 1.05, 3);
|
|
690
|
+
pStroke(function () { g.beginPath(); g.ellipse(cx, cy + TH, R, R * TILT, 0, 0, Math.PI, false); }, "#22241f", 1.45, 4);
|
|
691
|
+
// Disc top.
|
|
692
|
+
ePath(R);
|
|
693
|
+
g.fillStyle = "#f1ede2";
|
|
694
|
+
g.fill();
|
|
695
|
+
hatchClipped(function () { ePath(R); }, -Math.PI / 4, Math.max(3, 8 * u * 2) / 1, "#4c4d47", 0.15);
|
|
696
|
+
pStroke(function () { ePath(R); }, "#22241f", 1.35, 3);
|
|
697
|
+
// Colored wedges: same clock as the CSS conic version (0% = 12 o'clock).
|
|
698
|
+
slices.forEach(function (slice) {
|
|
699
|
+
var pctSpan = Math.max(0, Number(slice.end || 0) - Number(slice.start || 0));
|
|
700
|
+
if (pctSpan < 0.5) return;
|
|
701
|
+
var start = (-90 + Number(slice.start || 0) * 3.6) * Math.PI / 180;
|
|
702
|
+
var sweep = pctSpan * 3.6 * Math.PI / 180;
|
|
703
|
+
wPath(start, sweep);
|
|
704
|
+
g.fillStyle = slice.color || "#778db8";
|
|
705
|
+
g.fill();
|
|
706
|
+
hatchClipped(function () { wPath(start, sweep); }, Math.PI / 5, Math.max(2.4, 5.5 * u * 2), "#26354d", 0.24);
|
|
707
|
+
pStroke(function () { wPath(start, sweep); }, "#20231f", 1.25, 3);
|
|
708
|
+
g.save();
|
|
709
|
+
wPath(start, sweep);
|
|
710
|
+
g.strokeStyle = "rgba(30,32,28,.92)";
|
|
711
|
+
g.lineWidth = 1.45;
|
|
712
|
+
g.stroke();
|
|
713
|
+
g.restore();
|
|
714
|
+
});
|
|
715
|
+
pStroke(function () { ePath(R); }, "#22241f", 1.15, 2);
|
|
716
|
+
}
|
|
717
|
+
function renderPencilPies() {
|
|
718
|
+
Array.prototype.forEach.call(document.querySelectorAll("canvas[data-pencil-pie]"), function (cv) {
|
|
719
|
+
if (cv.getAttribute("data-pie-drawn")) return;
|
|
720
|
+
cv.setAttribute("data-pie-drawn", "1");
|
|
721
|
+
var slices = [];
|
|
722
|
+
try { slices = JSON.parse(cv.getAttribute("data-slices") || "[]"); } catch (_) { slices = []; }
|
|
723
|
+
drawPencilPie(cv, slices, Number(cv.getAttribute("data-radius") || 100));
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function bindSessionLinks() {
|
|
727
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-open-session]"), function (button) {
|
|
728
|
+
button.onclick = async function () {
|
|
729
|
+
var scope = button.closest(".audit-example-body");
|
|
730
|
+
var status = scope && scope.querySelector(".audit-session-status");
|
|
731
|
+
if (status) status.textContent = "Opening original session…";
|
|
732
|
+
try {
|
|
733
|
+
var result = await postJson("/open-session", {
|
|
734
|
+
source: button.getAttribute("data-session-source"),
|
|
735
|
+
sessionId: button.getAttribute("data-session-id")
|
|
736
|
+
}, 8000);
|
|
737
|
+
if (status) status.textContent = result && result.message ? result.message : "Original session opened.";
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (status) status.textContent = error && error.message ? error.message : "Could not open the original session.";
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function bindDetailsArrow() {
|
|
745
|
+
var arrow = document.getElementById("details-arrow");
|
|
746
|
+
var target = document.getElementById("setup-details");
|
|
747
|
+
if (arrow && target) {
|
|
748
|
+
arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function bindCityFieldnote() {
|
|
752
|
+
window.addEventListener("message", function (event) {
|
|
753
|
+
if (event.origin !== location.origin) return;
|
|
754
|
+
var data = event.data || {};
|
|
755
|
+
if (data.type !== "echomem:city-fieldnote") return;
|
|
756
|
+
var note = document.getElementById("city-fieldnote");
|
|
757
|
+
if (!note) return;
|
|
758
|
+
var sig = JSON.stringify([data.name, data.rank, data.lede]);
|
|
759
|
+
if (note._fnSig === sig) return;
|
|
760
|
+
note._fnSig = sig;
|
|
761
|
+
var save = fnSavings();
|
|
762
|
+
var saveLine = save
|
|
763
|
+
? '<p class="fn-save">I can help you get the same things done with ' +
|
|
764
|
+
'<b class="fn-num" data-kind="days" data-target="' + save.days + '">0 days</b> and ' +
|
|
765
|
+
'<b class="fn-num" data-kind="tokens" data-target="' + save.tokens + '">0 tokens</b> <b class="fn-less">less</b>.</p>'
|
|
766
|
+
: '';
|
|
767
|
+
note.innerHTML =
|
|
768
|
+
'<div class="fn-speak">' +
|
|
769
|
+
(data.avatar ? '<img src="' + esc(data.avatar) + '" alt="" />' : '') +
|
|
770
|
+
'<div class="fn-bubble"><h2>hey, <b>' + esc(data.name || "") + '</b><span class="fn-dot">.</span></h2>' + saveLine + '</div>' +
|
|
771
|
+
'</div>' +
|
|
772
|
+
(data.rank ? '<p class="fn-rank">' + data.rank + (data.guide ? '<button class="fn-info" type="button" aria-label="What do these types mean?">i</button>' : '') + '</p>' : '') +
|
|
773
|
+
(data.lede ? '<p class="fn-lede">' + data.lede + '</p>' : '');
|
|
774
|
+
var infoBtn = note.querySelector(".fn-info");
|
|
775
|
+
if (infoBtn) infoBtn.onclick = function () { openPersonaGuide(data.guide); };
|
|
776
|
+
fnAnimateNums(note);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
// The taxonomy quote: every persona kind Echo can assign, with the user's own
|
|
780
|
+
// rank/rhythm/personality highlighted. Data rides in from the city iframe.
|
|
781
|
+
function openPersonaGuide(guide) {
|
|
782
|
+
var dlg = document.getElementById("echo-note");
|
|
783
|
+
if (!dlg || !guide) return;
|
|
784
|
+
dlg.classList.remove("example");
|
|
785
|
+
var cur = guide.current || {};
|
|
786
|
+
function rows(list, curLabel) {
|
|
787
|
+
return (list || []).map(function (t) {
|
|
788
|
+
var you = t.label === curLabel;
|
|
789
|
+
return '<div class="fn-guide-row' + (you ? ' is-you' : '') + '"><b>' + esc(t.label) + '</b>' + (you ? '<i>you</i>' : '') + '<span> — ' + esc(t.blurb) + '</span></div>';
|
|
790
|
+
}).join("");
|
|
791
|
+
}
|
|
792
|
+
document.getElementById("echo-note-k").textContent = "Persona guide";
|
|
793
|
+
document.getElementById("echo-note-t").textContent = "How Echo reads you";
|
|
794
|
+
document.getElementById("echo-note-b").innerHTML =
|
|
795
|
+
'<p class="fn-guide-sec">Personality · ' + (guide.traits || []).length + ' kinds</p>' + rows(guide.traits, cur.primary) +
|
|
796
|
+
'<p class="fn-guide-sec">Rank · ' + (guide.ranks || []).length + ' levels</p>' + rows(guide.ranks, cur.rank) +
|
|
797
|
+
'<p class="fn-guide-sec">Rhythm · ' + (guide.rhythms || []).length + ' kinds</p>' + rows(guide.rhythms, cur.rhythm);
|
|
798
|
+
var cl = dlg.querySelector(".nclose");
|
|
799
|
+
if (cl) cl.onclick = function () { dlg.close(); };
|
|
800
|
+
dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); };
|
|
801
|
+
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
|
|
802
|
+
}
|
|
803
|
+
// Savings projection. BOTH figures apply the canonical terminal-window noise share to the
|
|
804
|
+
// FULL ledger, so they share one basis: days = waste share of total attention hours; tokens =
|
|
805
|
+
// projected waste share of total provider-billed input. The absolute token figure is a
|
|
806
|
+
// calibrated projection because intermediate requests are billed but not independently scored.
|
|
807
|
+
function fnSavings() {
|
|
808
|
+
var r = report || {};
|
|
809
|
+
var gs = gsClean(r.canonicalGoldenStandard);
|
|
810
|
+
if (!gs) return null;
|
|
811
|
+
var pct = gsDisplayWastePct(gs);
|
|
812
|
+
var projection = gsBillingProjection(r, gs);
|
|
813
|
+
var tokens = Number(projection.projectedWasteTokens || 0);
|
|
814
|
+
var wh = r.userWorkingHours || {};
|
|
815
|
+
var hours = Number(wh.userAttentionHours || 0) * pct / 100;
|
|
816
|
+
var perDay = Number(wh.averageDailyAttentionHours || 0);
|
|
817
|
+
var days = Math.round(perDay > 0 ? hours / perDay : hours / 8);
|
|
818
|
+
if (!tokens && !days) return null;
|
|
819
|
+
return { tokens: tokens, days: days };
|
|
820
|
+
}
|
|
821
|
+
function fnAnimateNums(root) {
|
|
822
|
+
var els = root.querySelectorAll(".fn-num");
|
|
823
|
+
if (!els.length) return;
|
|
824
|
+
var DURATION = 1500, t0 = null;
|
|
825
|
+
function label(kind, value) {
|
|
826
|
+
if (kind === "days") return number(value) + " days";
|
|
827
|
+
return big(value) + " tokens";
|
|
828
|
+
}
|
|
829
|
+
function step(ts) {
|
|
830
|
+
if (t0 === null) t0 = ts;
|
|
831
|
+
var p = Math.min(1, (ts - t0) / DURATION);
|
|
832
|
+
var eased = 1 - Math.pow(1 - p, 3);
|
|
833
|
+
Array.prototype.forEach.call(els, function (el) {
|
|
834
|
+
var target = Number(el.getAttribute("data-target") || 0);
|
|
835
|
+
el.textContent = label(el.getAttribute("data-kind"), Math.round(target * eased));
|
|
836
|
+
});
|
|
837
|
+
if (p < 1) requestAnimationFrame(step);
|
|
838
|
+
}
|
|
839
|
+
requestAnimationFrame(step);
|
|
840
|
+
}
|
|
487
841
|
`;
|