@polycode-projects/the-mechanical-code-talker 2.10.3 → 2.11.0
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 +68 -12
- package/bin/tmct.mjs +5 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +7 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/memory/core.mjs +8 -1
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/memory/trust.mjs +32 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +341 -197
- package/src/services/chat-session.mjs +24 -9
- package/src/services/chat.mjs +580 -47
- package/src/services/code-explorer-viz.mjs +198 -76
- package/src/services/extract-facts.mjs +384 -82
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +637 -0
- package/src/services/ledger-viz.mjs +209 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +54 -1
- package/src/services/spider-fly-viz.mjs +41 -23
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +32 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +208 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -5
- package/src/surfaces/web/memory-ask-browser.bundle.js +134 -125
- package/src/surfaces/web/memory-stats.mjs +53 -0
- package/src/tools/definitions.mjs +14 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-ingest.mjs +43 -0
- package/src/tools/server.mjs +5 -2
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { loadMemory, readFactRows, findContradictions, normFactTerm } from "../adapters/memory/core.mjs";
|
|
18
18
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
19
|
+
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
19
20
|
import { readFile } from "node:fs/promises";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
import { dirname, join } from "node:path";
|
|
@@ -524,6 +525,34 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
524
525
|
const placeholder = termSet.has("ishmael")
|
|
525
526
|
? "who is the grandfather of ishmael"
|
|
526
527
|
: (exampleTerm ? `ask the graph… e.g. what is ${exampleTerm.term}` : "ask the graph…");
|
|
528
|
+
// The paste-and-drop ingest panel plus the JSONL export ride the LIVE
|
|
529
|
+
// engine (window.tmctLedger): both teach into and read the dock's own
|
|
530
|
+
// session store, so they appear only where that bundle is offered — the
|
|
531
|
+
// demo site, never the CLI's self-contained tmct viz page.
|
|
532
|
+
const ingestHtml = ledgerBundleAvailable
|
|
533
|
+
? `<div class="docktools">
|
|
534
|
+
<button type="button" id="ingestToggle" class="dockbtn">ingest text…</button>
|
|
535
|
+
<button type="button" id="exportFacts" class="dockbtn">export facts</button>
|
|
536
|
+
</div>
|
|
537
|
+
<div class="researchrow" title="Fetches the topic from Simple English Wikipedia into this graph, then queues the topics its lead section links to — each queued topic runs as its own dock turn, paced politely. Asking is the network consent for these fetches.">
|
|
538
|
+
<label for="researchTopic" class="mono">research:</label>
|
|
539
|
+
<input id="researchTopic" type="text" autocomplete="off" spellcheck="false"
|
|
540
|
+
placeholder="a topic, e.g. owls" aria-label="Topic to research on Simple English Wikipedia">
|
|
541
|
+
<button type="button" class="dockbtn" id="researchGo">go</button>
|
|
542
|
+
<button type="button" class="dockbtn" id="researchPlay" aria-pressed="false" hidden>play</button>
|
|
543
|
+
<span class="ingeststatus mono" id="researchStatus" aria-live="polite"></span>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="ingestpanel" id="ingestPanel" hidden>
|
|
546
|
+
<textarea id="ingestText" spellcheck="false" aria-label="Text to ingest into the graph"
|
|
547
|
+
placeholder="Paste text or drop a .txt/.md file. Each sentence it recognizes as a fact is added to the graph; the rest are skipped honestly."></textarea>
|
|
548
|
+
<div class="ingestrow">
|
|
549
|
+
<button type="button" class="dockbtn" id="ingestBrowse">browse…</button>
|
|
550
|
+
<input type="file" id="ingestFile" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
551
|
+
<button type="button" class="dockbtn primary" id="ingestRun">ingest</button>
|
|
552
|
+
<span class="ingeststatus mono" id="ingestStatus" aria-live="polite"></span>
|
|
553
|
+
</div>
|
|
554
|
+
</div>`
|
|
555
|
+
: "";
|
|
527
556
|
const dockHtml = hasMemChat
|
|
528
557
|
? `<div class="chat">
|
|
529
558
|
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
@@ -531,6 +560,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
531
560
|
<span class="prompt mono">tmct></span>
|
|
532
561
|
<input id="chatq" type="text" placeholder="${escapeHtml(placeholder)}" aria-label="Ask the graph">
|
|
533
562
|
</form>
|
|
563
|
+
${ingestHtml}
|
|
534
564
|
</div>`
|
|
535
565
|
: `<div class="chat chat-off"><p class="chatnote">chat unavailable — run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
|
|
536
566
|
|
|
@@ -655,6 +685,20 @@ ${THEME_TOKENS_CSS}
|
|
|
655
685
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .6rem; min-width: 0; }
|
|
656
686
|
.chatask input:disabled { opacity: .6; }
|
|
657
687
|
.chatnote { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin: 0; }
|
|
688
|
+
.docktools { display: flex; gap: .5rem; margin-top: .55rem; }
|
|
689
|
+
.dockbtn { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 6px; padding: .22rem .6rem; background: var(--bg); cursor: pointer; }
|
|
690
|
+
.dockbtn:hover { color: var(--ink); border-color: var(--ink); }
|
|
691
|
+
.dockbtn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
692
|
+
.dockbtn[aria-pressed="true"] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
693
|
+
.researchrow { display: flex; align-items: center; gap: .5rem; margin-top: .45rem; font-size: .72rem; color: var(--muted); flex-wrap: wrap; }
|
|
694
|
+
.researchrow input { flex: 1 1 8rem; min-width: 6rem; font-family: ${MONO_STACK}; font-size: .72rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .25rem .55rem; }
|
|
695
|
+
.researchrow input::placeholder { color: var(--muted); }
|
|
696
|
+
.ingestpanel { margin-top: .55rem; display: flex; flex-direction: column; gap: .45rem; }
|
|
697
|
+
.ingestpanel[hidden] { display: none; }
|
|
698
|
+
.ingestpanel textarea { width: 100%; box-sizing: border-box; min-height: 96px; resize: vertical; font-family: ${MONO_STACK}; font-size: .76rem; line-height: 1.45; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .6rem; }
|
|
699
|
+
.ingestpanel textarea.drag { outline: 2px dashed var(--corpus); outline-offset: -4px; }
|
|
700
|
+
.ingestrow { display: flex; align-items: center; gap: .5rem; }
|
|
701
|
+
.ingeststatus { font-size: .68rem; color: var(--muted); }
|
|
658
702
|
@media (prefers-reduced-motion: no-preference) {
|
|
659
703
|
.seg, .chip, .look { transition: border-color .12s ease, background-color .12s ease; }
|
|
660
704
|
/* A live teach re-render swaps the dash section wholesale (dashboardHtml
|
|
@@ -975,6 +1019,8 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
975
1019
|
// (renderLedgerHtml's own ledgerBundleAvailable defaults false there), so
|
|
976
1020
|
// this branch is simply never reachable on a CLI-generated page.
|
|
977
1021
|
const resolveAnsweredTerm = ${resolveAnsweredTerm.toString()};
|
|
1022
|
+
const createTicker = ${createTicker.toString()};
|
|
1023
|
+
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
978
1024
|
const chatForm = el("chatform");
|
|
979
1025
|
if (chatForm && typeof tmctLedger !== "undefined" && typeof tmctLedger.createLedgerSession === "function") {
|
|
980
1026
|
const log = el("chatlog");
|
|
@@ -1067,6 +1113,169 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1067
1113
|
}
|
|
1068
1114
|
});
|
|
1069
1115
|
});
|
|
1116
|
+
|
|
1117
|
+
// ---- ingest + export: bring your own text, take the graph away ---------
|
|
1118
|
+
// The ingest panel runs pasted/dropped/browsed text through the SAME dock
|
|
1119
|
+
// session, one sentence at a time (tmctLedger.splitSentences, then
|
|
1120
|
+
// s.turn), keeping only the sentences the recognizer grounds — then
|
|
1121
|
+
// re-derives the whole ledger so the new facts can be examined in place.
|
|
1122
|
+
// Export serializes that same session store to canonical JSONL.
|
|
1123
|
+
const ingestToggle = el("ingestToggle");
|
|
1124
|
+
if (ingestToggle) {
|
|
1125
|
+
const panel = el("ingestPanel");
|
|
1126
|
+
const ta = el("ingestText");
|
|
1127
|
+
const runBtn = el("ingestRun");
|
|
1128
|
+
const statusEl = el("ingestStatus");
|
|
1129
|
+
const fileInput = el("ingestFile");
|
|
1130
|
+
ingestToggle.addEventListener("click", () => {
|
|
1131
|
+
panel.hidden = !panel.hidden;
|
|
1132
|
+
if (!panel.hidden) ta.focus();
|
|
1133
|
+
});
|
|
1134
|
+
el("ingestBrowse").addEventListener("click", () => fileInput.click());
|
|
1135
|
+
fileInput.addEventListener("change", async () => {
|
|
1136
|
+
const f = fileInput.files && fileInput.files[0];
|
|
1137
|
+
if (f) { try { ta.value = await f.text(); } catch { statusEl.textContent = "couldn't read that file"; } }
|
|
1138
|
+
fileInput.value = "";
|
|
1139
|
+
});
|
|
1140
|
+
["dragenter", "dragover"].forEach((ev) => ta.addEventListener(ev, (e) => { e.preventDefault(); ta.classList.add("drag"); }));
|
|
1141
|
+
["dragleave", "drop"].forEach((ev) => ta.addEventListener(ev, () => ta.classList.remove("drag")));
|
|
1142
|
+
ta.addEventListener("drop", async (e) => {
|
|
1143
|
+
e.preventDefault();
|
|
1144
|
+
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
|
1145
|
+
if (f) { try { ta.value = await f.text(); } catch { statusEl.textContent = "couldn't read that file"; } }
|
|
1146
|
+
});
|
|
1147
|
+
runBtn.addEventListener("click", () => {
|
|
1148
|
+
const text = ta.value.trim();
|
|
1149
|
+
if (!text) return;
|
|
1150
|
+
statusEl.textContent = "reading\\u2026";
|
|
1151
|
+
runBtn.disabled = true;
|
|
1152
|
+
withLock(async () => {
|
|
1153
|
+
try {
|
|
1154
|
+
const s = await ensureSession();
|
|
1155
|
+
const sentences = tmctLedger.splitSentences(text);
|
|
1156
|
+
let grounded = 0;
|
|
1157
|
+
for (const sentence of sentences) {
|
|
1158
|
+
const r = await s.turn(sentence);
|
|
1159
|
+
if (r.record && r.record.miss === false && r.record.via === "assert") grounded += 1;
|
|
1160
|
+
}
|
|
1161
|
+
const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
|
|
1162
|
+
applyLedgerData(fresh);
|
|
1163
|
+
const skipped = sentences.length - grounded;
|
|
1164
|
+
statusEl.textContent = sentences.length + " sentence" + (sentences.length === 1 ? "" : "s")
|
|
1165
|
+
+ ", " + grounded + " added" + (skipped ? ", " + skipped + " skipped" : "");
|
|
1166
|
+
} catch {
|
|
1167
|
+
statusEl.textContent = "something went wrong ingesting that";
|
|
1168
|
+
} finally {
|
|
1169
|
+
runBtn.disabled = false;
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const exportBtn = el("exportFacts");
|
|
1176
|
+
if (exportBtn) {
|
|
1177
|
+
exportBtn.addEventListener("click", () => {
|
|
1178
|
+
withLock(async () => {
|
|
1179
|
+
try {
|
|
1180
|
+
const s = await ensureSession();
|
|
1181
|
+
const jsonl = await tmctLedger.exportFactsJsonl(s.memoryDir);
|
|
1182
|
+
const blob = new Blob([jsonl], { type: "application/x-ndjson" });
|
|
1183
|
+
const url = URL.createObjectURL(blob);
|
|
1184
|
+
const link = document.createElement("a");
|
|
1185
|
+
link.href = url;
|
|
1186
|
+
link.download = "tmct-facts.jsonl";
|
|
1187
|
+
document.body.appendChild(link);
|
|
1188
|
+
link.click();
|
|
1189
|
+
link.remove();
|
|
1190
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1191
|
+
} catch { /* a failed export leaves the page untouched */ }
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ---- research: a Simple English Wikipedia queue through the SAME dock
|
|
1197
|
+
// session. The engine owns the queue (each turn's result.research is its
|
|
1198
|
+
// snapshot); this dock decides WHEN "research next" is asked, through
|
|
1199
|
+
// the shared viz-ticker verbs, and re-derives the whole ledger after a
|
|
1200
|
+
// step that grounded facts so the new rows can be examined in place —
|
|
1201
|
+
// the same refresh a successful teach performs.
|
|
1202
|
+
const researchTopicEl = el("researchTopic");
|
|
1203
|
+
if (researchTopicEl) {
|
|
1204
|
+
const researchGoBtn = el("researchGo");
|
|
1205
|
+
const researchPlayBtn = el("researchPlay");
|
|
1206
|
+
const researchStatusEl = el("researchStatus");
|
|
1207
|
+
let researchQueue = null; // the engine's latest snapshot, null when no run stands
|
|
1208
|
+
|
|
1209
|
+
function renderResearchControls(tickState) {
|
|
1210
|
+
const st = tickState || researchTicker.getState();
|
|
1211
|
+
researchPlayBtn.hidden = !(researchQueue && !researchQueue.complete);
|
|
1212
|
+
researchPlayBtn.textContent = st.playing ? "pause" : "play";
|
|
1213
|
+
researchPlayBtn.setAttribute("aria-pressed", String(st.playing));
|
|
1214
|
+
researchStatusEl.textContent = !researchQueue ? ""
|
|
1215
|
+
: researchQueue.complete
|
|
1216
|
+
? 'research "' + researchQueue.topic + '" complete \\u2014 ' + researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s")
|
|
1217
|
+
: researchQueue.done.length + " done \\u00b7 " + researchQueue.pending.length + " queued";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// One research turn through the dock — rendered exactly like a typed
|
|
1221
|
+
// line, so the transcript reads as if the visitor asked each search.
|
|
1222
|
+
function researchLine(q) {
|
|
1223
|
+
addLine("u", esc(q));
|
|
1224
|
+
const pending = addLine("a pending", "researching\\u2026");
|
|
1225
|
+
return withLock(async () => {
|
|
1226
|
+
try {
|
|
1227
|
+
const s = await ensureSession();
|
|
1228
|
+
const result = await s.turn(q);
|
|
1229
|
+
const missed = !result.record || Boolean(result.record.miss);
|
|
1230
|
+
pending.className = "a" + (missed ? " miss" : "");
|
|
1231
|
+
pending.innerHTML = esc(result.answer).replace(/\\n/g, "<br>");
|
|
1232
|
+
if (result.research !== undefined) {
|
|
1233
|
+
researchQueue = result.research;
|
|
1234
|
+
renderResearchControls();
|
|
1235
|
+
if (!missed) {
|
|
1236
|
+
const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
|
|
1237
|
+
applyLedgerData(fresh);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return result;
|
|
1241
|
+
} catch {
|
|
1242
|
+
pending.className = "a miss";
|
|
1243
|
+
pending.textContent = "Something went wrong with that research step. Try again, or reload the page.";
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const researchTicker = createTicker({
|
|
1250
|
+
onTick: async () => { await researchLine("research next"); },
|
|
1251
|
+
hasNext: () => Boolean(researchQueue && !researchQueue.complete),
|
|
1252
|
+
onRender: renderResearchControls,
|
|
1253
|
+
waitMs: 2400,
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
researchGoBtn.addEventListener("click", () => {
|
|
1257
|
+
const topic = researchTopicEl.value.trim();
|
|
1258
|
+
if (!topic) return;
|
|
1259
|
+
researchTopicEl.value = "";
|
|
1260
|
+
researchLine("research " + topic).then(() => {
|
|
1261
|
+
// A fresh run with topics queued auto-plays, unless the visitor
|
|
1262
|
+
// asked for reduced motion — the play button covers them too.
|
|
1263
|
+
if (researchQueue && !researchQueue.complete && !prefersReducedMotion() && !researchTicker.getState().playing) {
|
|
1264
|
+
researchTicker.play();
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
researchTopicEl.addEventListener("keydown", (e) => {
|
|
1269
|
+
if (e.key === "Enter") { e.preventDefault(); researchGoBtn.click(); }
|
|
1270
|
+
});
|
|
1271
|
+
// pause() directly, not play()'s own toggle: play() declines while a
|
|
1272
|
+
// step is mid-animation, and a pause pressed exactly then must not be
|
|
1273
|
+
// dropped — the in-flight step still settles, then the loop stops.
|
|
1274
|
+
researchPlayBtn.addEventListener("click", () => {
|
|
1275
|
+
if (researchTicker.getState().playing) researchTicker.pause();
|
|
1276
|
+
else researchTicker.play();
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1070
1279
|
} else if (chatForm && typeof tmctMemoryAsk !== "undefined") {
|
|
1071
1280
|
const memHandle = tmctMemoryAsk.createInMemoryStore();
|
|
1072
1281
|
memHandle.payload = PAYLOAD;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// memory-panel-viz.mjs — the docked "this session's memory" panel shared by
|
|
2
|
+
// chat.html and ingest.html: both pages seed from the same chat-seed.json
|
|
3
|
+
// bands and want the identical band-count/taught-list/forget-everything
|
|
4
|
+
// chrome, so the rendering lives once here instead of twice.
|
|
5
|
+
//
|
|
6
|
+
// Every export below is pure and `.toString()`-splice safe (no references
|
|
7
|
+
// outside its own parameters), the same discipline chat-page-viz.mjs's own
|
|
8
|
+
// provenanceChipFor/loadProgressLine hold — a page's inline script splices
|
|
9
|
+
// these in as text, so a closure over this module's top-level scope would
|
|
10
|
+
// silently vanish at splice time. Collaborators a caller may want to vary
|
|
11
|
+
// (the band-label lookup, a forget hook) are passed in as parameters, not
|
|
12
|
+
// imported, mirroring provenanceChipFor's own injected `bucketFor`.
|
|
13
|
+
|
|
14
|
+
/** The human label for one memoryStats() band key — the same band names
|
|
15
|
+
* build-chat-seed.mjs/extensions.mjs seed under, plus the two synthetic
|
|
16
|
+
* buckets memoryStats mints for anything outside a seed band
|
|
17
|
+
* ("taught this session", "other"). An unrecognized key renders as itself
|
|
18
|
+
* rather than disappearing, so a future band never goes unlabeled. */
|
|
19
|
+
export function bandLabelFor(key) {
|
|
20
|
+
const BAND_LABELS = {
|
|
21
|
+
human: "human persona",
|
|
22
|
+
"human-medium": "human persona (medium)",
|
|
23
|
+
"human-large": "human persona (large)",
|
|
24
|
+
seon: "seon ontology",
|
|
25
|
+
conceptnet: "ConceptNet",
|
|
26
|
+
"tier2-aws": "AWS",
|
|
27
|
+
"tier2-python": "Python",
|
|
28
|
+
"tier2-java": "Java",
|
|
29
|
+
"wordnet-xl": "WordNet",
|
|
30
|
+
};
|
|
31
|
+
return BAND_LABELS[key] || key;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The boot line's own memory summary — every seed band a session actually
|
|
35
|
+
* loaded, named with its real count, left to right in the fixed band order;
|
|
36
|
+
* a session with nothing seeded says so plainly instead of naming zero
|
|
37
|
+
* facts. `bandLabel` is the label lookup to use for each band key (pass
|
|
38
|
+
* bandLabelFor, or a page's own override). */
|
|
39
|
+
export function statsSummaryLine(stats, bandLabel) {
|
|
40
|
+
const BAND_ORDER = [
|
|
41
|
+
"human", "human-medium", "human-large", "seon", "conceptnet",
|
|
42
|
+
"tier2-aws", "tier2-python", "tier2-java", "wordnet-xl",
|
|
43
|
+
"taught this session", "other",
|
|
44
|
+
];
|
|
45
|
+
if (!stats || !stats.total) return "no starter memory; starting empty";
|
|
46
|
+
const parts = BAND_ORDER.filter((k) => stats.bandCounts[k]).map((k) => stats.bandCounts[k] + " " + bandLabel(k));
|
|
47
|
+
return parts.length
|
|
48
|
+
? "starter memory: " + parts.join(" + ") + " (" + stats.total + " facts total)"
|
|
49
|
+
: stats.total + " starter facts loaded";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Fetch `url` reading the body as a stream, reporting (loadedBytes,
|
|
53
|
+
* totalBytes) after every chunk — total is 0 when the response carries no
|
|
54
|
+
* Content-Length. Resolves to a Blob of the whole body. Falls back to a
|
|
55
|
+
* single-shot blob() read when the runtime has no streaming body reader. */
|
|
56
|
+
export async function fetchWithProgress(url, onProgress) {
|
|
57
|
+
const res = await fetch(url);
|
|
58
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
59
|
+
const total = Number(res.headers.get("content-length")) || 0;
|
|
60
|
+
if (!res.body || !res.body.getReader) {
|
|
61
|
+
const blob = await res.blob();
|
|
62
|
+
onProgress(blob.size, total || blob.size);
|
|
63
|
+
return blob;
|
|
64
|
+
}
|
|
65
|
+
const reader = res.body.getReader();
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let loaded = 0;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const step = await reader.read();
|
|
70
|
+
if (step.done) break;
|
|
71
|
+
chunks.push(step.value);
|
|
72
|
+
loaded += step.value.byteLength;
|
|
73
|
+
onProgress(loaded, total);
|
|
74
|
+
}
|
|
75
|
+
return new Blob(chunks);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* (Re)render the docked "this session's memory" panel into `panelEl` from a
|
|
80
|
+
* memoryStats() result: a total-facts row, one row per seed band the session
|
|
81
|
+
* actually holds, then the last 8 taught facts (most recent first), most
|
|
82
|
+
* recently taught last shown first. Clears and rebuilds `panelEl`'s children
|
|
83
|
+
* every call — cheap at this row count, and it means a stale row can never
|
|
84
|
+
* survive a re-render.
|
|
85
|
+
*
|
|
86
|
+
* opts.bandLabel required — the band-key -> label lookup (bandLabelFor,
|
|
87
|
+
* or a page's own override).
|
|
88
|
+
* opts.taughtHint the empty-state copy under "taught this session" when
|
|
89
|
+
* nothing has been taught yet. Defaults to the plain
|
|
90
|
+
* teach-a-fact prompt every page can show verbatim.
|
|
91
|
+
* opts.onForget when set, renders a "forget everything" button wired
|
|
92
|
+
* to this callback; omitted, no button renders (a page
|
|
93
|
+
* with no persistence to forget).
|
|
94
|
+
* opts.persistNote when set (and onForget is set), a short note under the
|
|
95
|
+
* forget button naming where the state lives.
|
|
96
|
+
*/
|
|
97
|
+
export function renderStatsPanelInto(panelEl, stats, opts) {
|
|
98
|
+
const { bandLabel, taughtHint, onForget = null, persistNote = null } = opts;
|
|
99
|
+
const BAND_ORDER = [
|
|
100
|
+
"human", "human-medium", "human-large", "seon", "conceptnet",
|
|
101
|
+
"tier2-aws", "tier2-python", "tier2-java", "wordnet-xl",
|
|
102
|
+
"taught this session", "other",
|
|
103
|
+
];
|
|
104
|
+
function bandRow(label, count) {
|
|
105
|
+
const row = document.createElement("p");
|
|
106
|
+
row.className = "band-row";
|
|
107
|
+
const l = document.createElement("span");
|
|
108
|
+
l.textContent = label;
|
|
109
|
+
const c = document.createElement("span");
|
|
110
|
+
c.className = "band-count";
|
|
111
|
+
c.textContent = String(count);
|
|
112
|
+
row.appendChild(l);
|
|
113
|
+
row.appendChild(c);
|
|
114
|
+
return row;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
panelEl.textContent = "";
|
|
118
|
+
panelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "this session's memory" }));
|
|
119
|
+
panelEl.appendChild(bandRow("total facts", stats.total));
|
|
120
|
+
for (const key of BAND_ORDER) {
|
|
121
|
+
if (stats.bandCounts[key]) panelEl.appendChild(bandRow(bandLabel(key), stats.bandCounts[key]));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
panelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "taught this session" }));
|
|
125
|
+
if (!stats.taught.length) {
|
|
126
|
+
const empty = document.createElement("p");
|
|
127
|
+
empty.className = "empty";
|
|
128
|
+
empty.textContent = taughtHint || 'nothing yet — teach it something ("a dog is a kind of animal") and it lands here, with its source.';
|
|
129
|
+
panelEl.appendChild(empty);
|
|
130
|
+
} else {
|
|
131
|
+
for (const fact of stats.taught.slice(-8).reverse()) {
|
|
132
|
+
const item = document.createElement("p");
|
|
133
|
+
item.className = "taught-item";
|
|
134
|
+
item.appendChild(document.createTextNode(fact.subject + " " + fact.predicate + " " + fact.object));
|
|
135
|
+
const tag = document.createElement("span");
|
|
136
|
+
tag.className = "taught-tag";
|
|
137
|
+
tag.textContent = fact.tag;
|
|
138
|
+
item.appendChild(tag);
|
|
139
|
+
panelEl.appendChild(item);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (onForget) {
|
|
144
|
+
const forget = document.createElement("button");
|
|
145
|
+
forget.type = "button";
|
|
146
|
+
forget.id = "forgetEverything";
|
|
147
|
+
forget.className = "forget-btn";
|
|
148
|
+
forget.textContent = "forget everything";
|
|
149
|
+
forget.title = "clear what this device has saved and restart from the fresh seed";
|
|
150
|
+
forget.addEventListener("click", onForget);
|
|
151
|
+
panelEl.appendChild(forget);
|
|
152
|
+
if (persistNote) {
|
|
153
|
+
const note = document.createElement("p");
|
|
154
|
+
note.className = "persist-note";
|
|
155
|
+
note.textContent = persistNote;
|
|
156
|
+
panelEl.appendChild(note);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|