@echomem/mcp 1.4.9 → 1.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/hud/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +54 -93
- package/dist/context-analysis/claude-canonical-adapter.js +315 -0
- package/dist/context-analysis/claude-native-canonical.js +35 -12
- package/dist/context-metrics/calculate.js +2 -15
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -0
- package/dist/hud/adapters.js +69 -198
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +4 -98
- package/dist/hud/monitor.js +1 -12
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +409 -186
- package/dist/index.js +0 -0
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -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 +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +6 -5623
- package/dist/setup.js +24 -10
- package/package.json +4 -4
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/chaos-to-clarity-pencil.html +0 -582
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -2271
- package/dist/city/generate-echo-city-only.mjs +0 -112
- package/dist/city/pencil-pie-generator.html +0 -883
- package/dist/city/pencil-webgl-landscape.html +0 -1239
- package/dist/city/spatial-fan-story.html +0 -479
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// Generated by splitting the previous inline setup page. Keep this source readable: it is emitted into one browser IIFE.
|
|
2
|
+
export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSearchParams(location.search);
|
|
3
|
+
// Preview is a server-authorized render option, never a URL-controlled client mode.
|
|
4
|
+
var previewState = __ECHO_SETUP_PREVIEW_STATE__;
|
|
5
|
+
var nonce = params.get("nonce") || "";
|
|
6
|
+
var app = document.getElementById("app");
|
|
7
|
+
var title = document.getElementById("title");
|
|
8
|
+
var status = document.getElementById("status");
|
|
9
|
+
var report = null;
|
|
10
|
+
var reportEnvelope = null;
|
|
11
|
+
var activeScanId = "";
|
|
12
|
+
var stats = null;
|
|
13
|
+
var statsSlow = false;
|
|
14
|
+
var decisionMade = false;
|
|
15
|
+
var connected = false;
|
|
16
|
+
var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
|
|
17
|
+
var exStartAt = 0; // extraction start (ms) — drives the countdown
|
|
18
|
+
var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
|
|
19
|
+
var extractionEnded = false; // user chose to leave the extraction flow before natural completion
|
|
20
|
+
var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
|
|
21
|
+
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
22
|
+
var authUrl = "";
|
|
23
|
+
var workspacePath = "";
|
|
24
|
+
var authWindow = null;
|
|
25
|
+
var connectionPollStarted = false;
|
|
26
|
+
var statsPollStarted = false;
|
|
27
|
+
var reportPollStarted = false;
|
|
28
|
+
var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
29
|
+
|
|
30
|
+
function esc(value) {
|
|
31
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
|
|
32
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function setHead(nextTitle, nextStatus) {
|
|
36
|
+
title.textContent = nextTitle;
|
|
37
|
+
status.textContent = nextStatus;
|
|
38
|
+
}
|
|
39
|
+
function setCityMode(enabled) {
|
|
40
|
+
document.body.classList.toggle("cityMode", !!enabled);
|
|
41
|
+
if (enabled) document.body.classList.remove("scanMode", "extractMode", "readyMode");
|
|
42
|
+
}
|
|
43
|
+
function setScanMode(enabled) {
|
|
44
|
+
document.body.classList.toggle("scanMode", !!enabled);
|
|
45
|
+
if (enabled) document.body.classList.remove("cityMode", "extractMode", "readyMode");
|
|
46
|
+
}
|
|
47
|
+
function setExtractMode(enabled) {
|
|
48
|
+
document.body.classList.toggle("extractMode", !!enabled);
|
|
49
|
+
if (enabled) document.body.classList.remove("scanMode", "readyMode");
|
|
50
|
+
}
|
|
51
|
+
function setReadyMode(enabled) {
|
|
52
|
+
document.body.classList.toggle("readyMode", !!enabled);
|
|
53
|
+
if (enabled) document.body.classList.remove("scanMode", "cityMode", "extractMode");
|
|
54
|
+
}
|
|
55
|
+
function number(value) {
|
|
56
|
+
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
57
|
+
}
|
|
58
|
+
function compact(value) {
|
|
59
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
60
|
+
var abs = Math.abs(value);
|
|
61
|
+
if (abs >= 1e9) return (value / 1e9).toFixed(1) + "B";
|
|
62
|
+
if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
63
|
+
if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
|
|
64
|
+
return String(Math.round(value));
|
|
65
|
+
}
|
|
66
|
+
function big(value) {
|
|
67
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
68
|
+
var n = Math.abs(value);
|
|
69
|
+
if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
|
|
70
|
+
if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
71
|
+
if (n >= 1e3) return Math.round(value / 1e3) + "K";
|
|
72
|
+
return String(Math.round(value));
|
|
73
|
+
}
|
|
74
|
+
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
75
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
76
|
+
var controller = new AbortController();
|
|
77
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
return await fetch(url, Object.assign({}, options || {}, { signal: controller.signal }));
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function getJson(path) {
|
|
85
|
+
var sep = path.indexOf("?") >= 0 ? "&" : "?";
|
|
86
|
+
var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
|
|
87
|
+
if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
88
|
+
return await res.json();
|
|
89
|
+
}
|
|
90
|
+
async function postJson(path, body, timeoutMs) {
|
|
91
|
+
var options = {
|
|
92
|
+
method: "POST",
|
|
93
|
+
credentials: "omit",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
|
|
96
|
+
};
|
|
97
|
+
var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
|
|
98
|
+
var raw = await res.text();
|
|
99
|
+
var data = {};
|
|
100
|
+
try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
|
|
101
|
+
if (!res.ok) throw new Error(data.message || data.error || raw || ("HTTP " + res.status));
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
function metric(label, value, detail) {
|
|
105
|
+
return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
|
|
106
|
+
}
|
|
107
|
+
function row(label, value) {
|
|
108
|
+
return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
|
|
109
|
+
}
|
|
110
|
+
function routeError(code) {
|
|
111
|
+
if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
|
|
112
|
+
if (code === "FORBIDDEN_SCOPE") return "This device token cannot import history. Re-connect this device to mint an import-capable token.";
|
|
113
|
+
if (code === "VAULT_LOCKED") return "Your local vault is locked. Run echomem-mcp unlock, then retry.";
|
|
114
|
+
if (code === "IMPORT_START_TIMEOUT") return "Starting the import timed out. Retry when the connection settles.";
|
|
115
|
+
if (code === "NOT_LOGGED_IN") return "Echo on this machine is not signed in yet. Sign in again and retry.";
|
|
116
|
+
return code || "Something went wrong. Retry in a moment.";
|
|
117
|
+
}
|
|
118
|
+
function closeAuthWindow() {
|
|
119
|
+
try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
|
|
120
|
+
authWindow = null;
|
|
121
|
+
}
|
|
122
|
+
function notifyOpenerConnected() {
|
|
123
|
+
try {
|
|
124
|
+
if (window.opener && window.opener !== window && !window.opener.closed) {
|
|
125
|
+
window.opener.postMessage({ type: "echomem:connected", nonce: nonce }, location.origin);
|
|
126
|
+
setTimeout(function () { window.close(); }, 250);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
function openAuthWindow(url) {
|
|
133
|
+
var targetUrl = url || authUrl;
|
|
134
|
+
if (!targetUrl) return false;
|
|
135
|
+
try {
|
|
136
|
+
authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
|
|
137
|
+
if (!authWindow) return false;
|
|
138
|
+
authWindow.focus();
|
|
139
|
+
return true;
|
|
140
|
+
} catch (_) { return false; }
|
|
141
|
+
}
|
|
142
|
+
function showPopupFallback(message, url) {
|
|
143
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-help]"), function (hint) {
|
|
144
|
+
hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
|
|
145
|
+
});
|
|
146
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-fallback]"), function (fallback) {
|
|
147
|
+
fallback.setAttribute("href", url || authUrl);
|
|
148
|
+
fallback.classList.remove("hidden");
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function onConnectClick() {
|
|
152
|
+
if (connected) { void waitForStats(); return; }
|
|
153
|
+
if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
|
|
154
|
+
startConnectionPoll();
|
|
155
|
+
}
|
|
156
|
+
function bindConnect() {
|
|
157
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-connect-echo]"), function (signin) {
|
|
158
|
+
signin.onclick = onConnectClick;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Retained from the archived pencil-pie prototype: 3D pencil-sketch pies drawn on a
|
|
162
|
+
// transparent canvas so the graphite sits directly on the page background.
|
|
163
|
+
function pencilMulberry(seed) {
|
|
164
|
+
return function () {
|
|
165
|
+
var t = seed += 0x6D2B79F5;
|
|
166
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
167
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
168
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function drawPencilPie(cv, slices, R) {
|
|
172
|
+
var TILT = 0.48, u = R / 270;
|
|
173
|
+
var TH = 58 * u, SHLEN = 154 * u, SHANG = 14 * Math.PI / 180;
|
|
174
|
+
var density = Math.min(1.1, Math.max(0.2, u * u * 2.6));
|
|
175
|
+
cv.width = Math.round(R * 2.62);
|
|
176
|
+
cv.height = Math.round(R * 1.9);
|
|
177
|
+
var cx = R * 1.14, cy = R * 0.64;
|
|
178
|
+
var g = cv.getContext("2d");
|
|
179
|
+
var random = pencilMulberry(2117 + Math.round(R * 7) + slices.length * 131);
|
|
180
|
+
function ePoint(radius, angle, yOff) { return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius * TILT + (yOff || 0) }; }
|
|
181
|
+
function ePath(radius) { g.beginPath(); g.ellipse(cx, cy, radius, radius * TILT, 0, 0, Math.PI * 2); }
|
|
182
|
+
function wPath(start, sweep) {
|
|
183
|
+
var steps = Math.max(18, Math.ceil(Math.abs(sweep) / (Math.PI / 90)));
|
|
184
|
+
g.beginPath();
|
|
185
|
+
g.moveTo(cx, cy);
|
|
186
|
+
for (var i = 0; i <= steps; i++) { var p = ePoint(R, start + sweep * (i / steps), 0); g.lineTo(p.x, p.y); }
|
|
187
|
+
g.closePath();
|
|
188
|
+
}
|
|
189
|
+
function pStroke(pathFn, color, width, repeats) {
|
|
190
|
+
var rnd = pencilMulberry(808 + Math.round(width * 100) + repeats * 47);
|
|
191
|
+
for (var i = 0; i < repeats; i++) {
|
|
192
|
+
g.save();
|
|
193
|
+
g.translate((rnd() - 0.5) * 1.25, (rnd() - 0.5) * 0.8);
|
|
194
|
+
pathFn();
|
|
195
|
+
g.strokeStyle = color;
|
|
196
|
+
g.globalAlpha = 0.58 / Math.max(1, repeats * 0.64);
|
|
197
|
+
g.lineWidth = width + rnd() * 0.45;
|
|
198
|
+
g.stroke();
|
|
199
|
+
g.restore();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function hatchClipped(clipFn, angle, spacing, color, alpha) {
|
|
203
|
+
g.save();
|
|
204
|
+
clipFn();
|
|
205
|
+
g.clip();
|
|
206
|
+
g.translate(cx, cy);
|
|
207
|
+
g.rotate(angle);
|
|
208
|
+
g.translate(-cx, -cy);
|
|
209
|
+
g.strokeStyle = color;
|
|
210
|
+
g.globalAlpha = alpha;
|
|
211
|
+
g.lineWidth = 0.58;
|
|
212
|
+
g.lineCap = "round";
|
|
213
|
+
g.beginPath();
|
|
214
|
+
for (var x = cx - R * 1.7; x < cx + R * 1.7; x += spacing) {
|
|
215
|
+
var off = (random() - 0.5) * 2;
|
|
216
|
+
g.moveTo(x + off, cy - R);
|
|
217
|
+
g.lineTo(x + off + (random() - 0.5) * 2, cy + R);
|
|
218
|
+
}
|
|
219
|
+
g.stroke();
|
|
220
|
+
g.restore();
|
|
221
|
+
}
|
|
222
|
+
// Graphite shadow rubbed into the page beneath the disc.
|
|
223
|
+
var dx = Math.cos(SHANG) * SHLEN, dy = Math.sin(SHANG) * SHLEN;
|
|
224
|
+
var shX = cx + dx * 0.46, shY = cy + TH + 12 * u + dy * 0.42;
|
|
225
|
+
g.save();
|
|
226
|
+
g.filter = "blur(" + Math.max(2, 6 * u * 1.6).toFixed(1) + "px)";
|
|
227
|
+
g.fillStyle = "rgba(31,33,28,.05)";
|
|
228
|
+
g.beginPath();
|
|
229
|
+
g.ellipse(shX, shY, R * 1.08, R * TILT * 0.82, SHANG * 0.12, 0, Math.PI * 2);
|
|
230
|
+
g.fill();
|
|
231
|
+
g.restore();
|
|
232
|
+
var L = Math.max(0.8, u * 1.9);
|
|
233
|
+
var dust = Math.round(1380 * density), pass, i;
|
|
234
|
+
var dustTones = ["rgba(35,37,32,.028)", "rgba(35,37,32,.055)", "rgba(35,37,32,.09)"];
|
|
235
|
+
for (pass = 0; pass < 3; pass++) {
|
|
236
|
+
g.beginPath();
|
|
237
|
+
for (i = pass; i < dust; i += 3) {
|
|
238
|
+
var ring = Math.pow(random(), 0.58 + pass * 0.16), ang = random() * Math.PI * 2, trail = Math.pow(random(), 2.1);
|
|
239
|
+
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));
|
|
240
|
+
}
|
|
241
|
+
g.fillStyle = dustTones[pass];
|
|
242
|
+
g.fill();
|
|
243
|
+
}
|
|
244
|
+
var strokes = Math.round(1040 * density);
|
|
245
|
+
g.save();
|
|
246
|
+
g.lineCap = "round";
|
|
247
|
+
g.lineWidth = 0.72;
|
|
248
|
+
for (pass = 0; pass < 2; pass++) {
|
|
249
|
+
g.beginPath();
|
|
250
|
+
var cross = pass === 0 ? -0.38 : 0.46;
|
|
251
|
+
for (i = pass; i < strokes; i += 2) {
|
|
252
|
+
var ring2 = Math.pow(random(), 0.62), ang2 = random() * Math.PI * 2, trail2 = Math.pow(random(), 2.05);
|
|
253
|
+
var x0 = shX + Math.cos(ang2) * R * 1.04 * ring2 + dx * trail2 * 0.5;
|
|
254
|
+
var y0 = shY + Math.sin(ang2) * R * TILT * 0.75 * ring2 + dy * trail2 * 0.34;
|
|
255
|
+
var len = (3 + random() * (9 + (1 - ring2) * 14)) * L;
|
|
256
|
+
var sa = SHANG + cross + (random() - 0.5) * 0.15;
|
|
257
|
+
g.moveTo(x0, y0);
|
|
258
|
+
g.lineTo(x0 + Math.cos(sa) * len, y0 + Math.sin(sa) * len * 0.62);
|
|
259
|
+
}
|
|
260
|
+
g.strokeStyle = pass === 0 ? "rgba(32,34,29,.14)" : "rgba(32,34,29,.11)";
|
|
261
|
+
g.stroke();
|
|
262
|
+
}
|
|
263
|
+
g.restore();
|
|
264
|
+
// Contact crevice hugging the lower ellipse.
|
|
265
|
+
g.save();
|
|
266
|
+
g.lineCap = "round";
|
|
267
|
+
g.lineWidth = 0.9;
|
|
268
|
+
var contact = Math.round(300 * density);
|
|
269
|
+
for (pass = 0; pass < 3; pass++) {
|
|
270
|
+
g.beginPath();
|
|
271
|
+
for (i = pass; i < contact; i += 3) {
|
|
272
|
+
var cAng = random() * Math.PI;
|
|
273
|
+
var dark = 0.5 + Math.cos(cAng - SHANG) * 0.5;
|
|
274
|
+
var depth = Math.pow(random(), 2.6) * (5 + dark * 8) * L + pass * 1.2;
|
|
275
|
+
var cp = ePoint(R * (0.97 + random() * 0.045), cAng, TH + 1 + depth);
|
|
276
|
+
var tx = -Math.sin(cAng), ty = Math.cos(cAng) * TILT;
|
|
277
|
+
var norm = Math.hypot(tx, ty) || 1;
|
|
278
|
+
var cLen = (3 + random() * (8 + dark * 11)) * L;
|
|
279
|
+
g.moveTo(cp.x - tx / norm * cLen * 0.5, cp.y - ty / norm * cLen * 0.5);
|
|
280
|
+
g.lineTo(cp.x + tx / norm * cLen * 0.5, cp.y + ty / norm * cLen * 0.5);
|
|
281
|
+
}
|
|
282
|
+
g.strokeStyle = ["rgba(27,29,25,.36)", "rgba(27,29,25,.25)", "rgba(27,29,25,.16)"][pass];
|
|
283
|
+
g.stroke();
|
|
284
|
+
}
|
|
285
|
+
g.restore();
|
|
286
|
+
// Cylinder wall.
|
|
287
|
+
function sidePath() {
|
|
288
|
+
var steps = 120, k;
|
|
289
|
+
g.beginPath();
|
|
290
|
+
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); }
|
|
291
|
+
for (k = steps; k >= 0; k--) { var sp2 = ePoint(R, Math.PI * k / steps, TH); g.lineTo(sp2.x, sp2.y); }
|
|
292
|
+
g.closePath();
|
|
293
|
+
}
|
|
294
|
+
sidePath();
|
|
295
|
+
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);
|
|
296
|
+
sideTone.addColorStop(0, "#e6e1d6");
|
|
297
|
+
sideTone.addColorStop(0.47, "#d8d3c7");
|
|
298
|
+
sideTone.addColorStop(1, "#aaa79f");
|
|
299
|
+
g.fillStyle = sideTone;
|
|
300
|
+
g.fill();
|
|
301
|
+
g.strokeStyle = "rgba(39,41,35,.7)";
|
|
302
|
+
g.lineWidth = 1.5;
|
|
303
|
+
g.stroke();
|
|
304
|
+
var hatchCount = Math.round(138 * Math.max(density, 0.45));
|
|
305
|
+
g.save();
|
|
306
|
+
g.lineCap = "round";
|
|
307
|
+
g.lineWidth = 0.66;
|
|
308
|
+
for (pass = 0; pass < 3; pass++) {
|
|
309
|
+
g.beginPath();
|
|
310
|
+
for (i = 1; i < hatchCount; i++) {
|
|
311
|
+
var hAng = Math.PI * i / hatchCount;
|
|
312
|
+
var hDark = 0.5 + Math.cos(hAng - SHANG) * 0.5;
|
|
313
|
+
if (Math.min(2, Math.floor(hDark * 3)) !== pass) continue;
|
|
314
|
+
var top = ePoint(R, hAng, 0);
|
|
315
|
+
var jit = (random() - 0.5) * 4 * L;
|
|
316
|
+
g.moveTo(top.x + jit * 0.25, top.y + 2 + random() * 3);
|
|
317
|
+
g.lineTo(top.x + jit, top.y + TH - 2 - random() * 5);
|
|
318
|
+
}
|
|
319
|
+
g.strokeStyle = ["rgba(37,39,34,.13)", "rgba(37,39,34,.21)", "rgba(37,39,34,.31)"][pass];
|
|
320
|
+
g.stroke();
|
|
321
|
+
}
|
|
322
|
+
g.restore();
|
|
323
|
+
pStroke(function () { g.beginPath(); g.ellipse(cx, cy, R, R * TILT, 0, 0, Math.PI, false); }, "#272923", 1.05, 3);
|
|
324
|
+
pStroke(function () { g.beginPath(); g.ellipse(cx, cy + TH, R, R * TILT, 0, 0, Math.PI, false); }, "#22241f", 1.45, 4);
|
|
325
|
+
// Disc top.
|
|
326
|
+
ePath(R);
|
|
327
|
+
g.fillStyle = "#f1ede2";
|
|
328
|
+
g.fill();
|
|
329
|
+
hatchClipped(function () { ePath(R); }, -Math.PI / 4, Math.max(3, 8 * u * 2) / 1, "#4c4d47", 0.15);
|
|
330
|
+
pStroke(function () { ePath(R); }, "#22241f", 1.35, 3);
|
|
331
|
+
// Colored wedges: same clock as the CSS conic version (0% = 12 o'clock).
|
|
332
|
+
slices.forEach(function (slice) {
|
|
333
|
+
var pctSpan = Math.max(0, Number(slice.end || 0) - Number(slice.start || 0));
|
|
334
|
+
if (pctSpan < 0.5) return;
|
|
335
|
+
var start = (-90 + Number(slice.start || 0) * 3.6) * Math.PI / 180;
|
|
336
|
+
var sweep = pctSpan * 3.6 * Math.PI / 180;
|
|
337
|
+
wPath(start, sweep);
|
|
338
|
+
g.fillStyle = slice.color || "#778db8";
|
|
339
|
+
g.fill();
|
|
340
|
+
hatchClipped(function () { wPath(start, sweep); }, Math.PI / 5, Math.max(2.4, 5.5 * u * 2), "#26354d", 0.24);
|
|
341
|
+
pStroke(function () { wPath(start, sweep); }, "#20231f", 1.25, 3);
|
|
342
|
+
g.save();
|
|
343
|
+
wPath(start, sweep);
|
|
344
|
+
g.strokeStyle = "rgba(30,32,28,.92)";
|
|
345
|
+
g.lineWidth = 1.45;
|
|
346
|
+
g.stroke();
|
|
347
|
+
g.restore();
|
|
348
|
+
});
|
|
349
|
+
pStroke(function () { ePath(R); }, "#22241f", 1.15, 2);
|
|
350
|
+
}
|
|
351
|
+
function renderPencilPies() {
|
|
352
|
+
Array.prototype.forEach.call(document.querySelectorAll("canvas[data-pencil-pie]"), function (cv) {
|
|
353
|
+
if (cv.getAttribute("data-pie-drawn")) return;
|
|
354
|
+
cv.setAttribute("data-pie-drawn", "1");
|
|
355
|
+
var slices = [];
|
|
356
|
+
try { slices = JSON.parse(cv.getAttribute("data-slices") || "[]"); } catch (_) { slices = []; }
|
|
357
|
+
drawPencilPie(cv, slices, Number(cv.getAttribute("data-radius") || 100));
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function bindSessionLinks() {
|
|
361
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-open-session]"), function (button) {
|
|
362
|
+
button.onclick = async function () {
|
|
363
|
+
var scope = button.closest(".audit-example-body");
|
|
364
|
+
var status = scope && scope.querySelector(".audit-session-status");
|
|
365
|
+
if (status) status.textContent = "Opening original session…";
|
|
366
|
+
try {
|
|
367
|
+
var result = await postJson("/open-session", {
|
|
368
|
+
source: button.getAttribute("data-session-source"),
|
|
369
|
+
sessionId: button.getAttribute("data-session-id")
|
|
370
|
+
}, 8000);
|
|
371
|
+
if (status) status.textContent = result && result.message ? result.message : "Original session opened.";
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (status) status.textContent = error && error.message ? error.message : "Could not open the original session.";
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function bindDetailsArrow() {
|
|
379
|
+
var arrow = document.getElementById("details-arrow");
|
|
380
|
+
var target = document.getElementById("setup-details");
|
|
381
|
+
if (arrow && target) {
|
|
382
|
+
arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function bindCityFieldnote() {
|
|
386
|
+
window.addEventListener("message", function (event) {
|
|
387
|
+
if (event.origin !== location.origin) return;
|
|
388
|
+
var data = event.data || {};
|
|
389
|
+
if (data.type !== "echomem:city-fieldnote") return;
|
|
390
|
+
var note = document.getElementById("city-fieldnote");
|
|
391
|
+
if (!note) return;
|
|
392
|
+
var sig = JSON.stringify([data.name, data.rank, data.lede]);
|
|
393
|
+
if (note._fnSig === sig) return;
|
|
394
|
+
note._fnSig = sig;
|
|
395
|
+
var save = fnSavings();
|
|
396
|
+
var saveLine = save
|
|
397
|
+
? '<p class="fn-save">I can help you get the same things done with ' +
|
|
398
|
+
'<b class="fn-num" data-kind="days" data-target="' + save.days + '">0 days</b> and ' +
|
|
399
|
+
'<b class="fn-num" data-kind="tokens" data-target="' + save.tokens + '">0 tokens</b> <b class="fn-less">less</b>.</p>'
|
|
400
|
+
: '';
|
|
401
|
+
note.innerHTML =
|
|
402
|
+
'<div class="fn-speak">' +
|
|
403
|
+
(data.avatar ? '<img src="' + esc(data.avatar) + '" alt="" />' : '') +
|
|
404
|
+
'<div class="fn-bubble"><h2>hey, <b>' + esc(data.name || "") + '</b><span class="fn-dot">.</span></h2>' + saveLine + '</div>' +
|
|
405
|
+
'</div>' +
|
|
406
|
+
(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>' : '') +
|
|
407
|
+
(data.lede ? '<p class="fn-lede">' + data.lede + '</p>' : '');
|
|
408
|
+
var infoBtn = note.querySelector(".fn-info");
|
|
409
|
+
if (infoBtn) infoBtn.onclick = function () { openPersonaGuide(data.guide); };
|
|
410
|
+
fnAnimateNums(note);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// The taxonomy quote: every persona kind Echo can assign, with the user's own
|
|
414
|
+
// rank/rhythm/personality highlighted. Data rides in from the city iframe.
|
|
415
|
+
function openPersonaGuide(guide) {
|
|
416
|
+
var dlg = document.getElementById("echo-note");
|
|
417
|
+
if (!dlg || !guide) return;
|
|
418
|
+
dlg.classList.remove("example");
|
|
419
|
+
var cur = guide.current || {};
|
|
420
|
+
function rows(list, curLabel) {
|
|
421
|
+
return (list || []).map(function (t) {
|
|
422
|
+
var you = t.label === curLabel;
|
|
423
|
+
return '<div class="fn-guide-row' + (you ? ' is-you' : '') + '"><b>' + esc(t.label) + '</b>' + (you ? '<i>you</i>' : '') + '<span> — ' + esc(t.blurb) + '</span></div>';
|
|
424
|
+
}).join("");
|
|
425
|
+
}
|
|
426
|
+
document.getElementById("echo-note-k").textContent = "Persona guide";
|
|
427
|
+
document.getElementById("echo-note-t").textContent = "How Echo reads you";
|
|
428
|
+
document.getElementById("echo-note-b").innerHTML =
|
|
429
|
+
'<p class="fn-guide-sec">Personality · ' + (guide.traits || []).length + ' kinds</p>' + rows(guide.traits, cur.primary) +
|
|
430
|
+
'<p class="fn-guide-sec">Rank · ' + (guide.ranks || []).length + ' levels</p>' + rows(guide.ranks, cur.rank) +
|
|
431
|
+
'<p class="fn-guide-sec">Rhythm · ' + (guide.rhythms || []).length + ' kinds</p>' + rows(guide.rhythms, cur.rhythm);
|
|
432
|
+
var cl = dlg.querySelector(".nclose");
|
|
433
|
+
if (cl) cl.onclick = function () { dlg.close(); };
|
|
434
|
+
dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); };
|
|
435
|
+
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
|
|
436
|
+
}
|
|
437
|
+
// Savings projection. BOTH figures apply the canonical terminal-window noise share to the
|
|
438
|
+
// FULL ledger, so they share one basis: days = waste share of total attention hours; tokens =
|
|
439
|
+
// projected waste share of total provider-billed input. The absolute token figure is a
|
|
440
|
+
// calibrated projection because intermediate requests are billed but not independently scored.
|
|
441
|
+
function fnSavings() {
|
|
442
|
+
var r = report || {};
|
|
443
|
+
var gs = gsClean(r.canonicalGoldenStandard);
|
|
444
|
+
if (!gs) return null;
|
|
445
|
+
var pct = gsDisplayWastePct(gs);
|
|
446
|
+
var projection = gsBillingProjection(r, gs);
|
|
447
|
+
var tokens = Number(projection.projectedWasteTokens || 0);
|
|
448
|
+
var wh = r.userWorkingHours || {};
|
|
449
|
+
var hours = Number(wh.userAttentionHours || 0) * pct / 100;
|
|
450
|
+
var perDay = Number(wh.averageDailyAttentionHours || 0);
|
|
451
|
+
var days = Math.round(perDay > 0 ? hours / perDay : hours / 8);
|
|
452
|
+
if (!tokens && !days) return null;
|
|
453
|
+
return { tokens: tokens, days: days };
|
|
454
|
+
}
|
|
455
|
+
function fnAnimateNums(root) {
|
|
456
|
+
var els = root.querySelectorAll(".fn-num");
|
|
457
|
+
if (!els.length) return;
|
|
458
|
+
var DURATION = 1500, t0 = null;
|
|
459
|
+
function label(kind, value) {
|
|
460
|
+
if (kind === "days") return number(value) + " days";
|
|
461
|
+
return big(value) + " tokens";
|
|
462
|
+
}
|
|
463
|
+
function step(ts) {
|
|
464
|
+
if (t0 === null) t0 = ts;
|
|
465
|
+
var p = Math.min(1, (ts - t0) / DURATION);
|
|
466
|
+
var eased = 1 - Math.pow(1 - p, 3);
|
|
467
|
+
Array.prototype.forEach.call(els, function (el) {
|
|
468
|
+
var target = Number(el.getAttribute("data-target") || 0);
|
|
469
|
+
el.textContent = label(el.getAttribute("data-kind"), Math.round(target * eased));
|
|
470
|
+
});
|
|
471
|
+
if (p < 1) requestAnimationFrame(step);
|
|
472
|
+
}
|
|
473
|
+
requestAnimationFrame(step);
|
|
474
|
+
}
|
|
475
|
+
`;
|