@polycode-projects/the-mechanical-code-talker 2.10.5 → 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 +2 -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 +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/memory/trust.mjs +11 -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 +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +286 -43
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -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/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
- package/src/surfaces/web/memory-stats.mjs +53 -0
|
@@ -9,18 +9,24 @@
|
|
|
9
9
|
// (Text | Document) across the top, a roomy free-text area on the left that
|
|
10
10
|
// takes paste and drag-and-drop plus a browse-for-file control, and a
|
|
11
11
|
// soft-panel canonical facts pane on the right that fills LIVE as the
|
|
12
|
-
// recognizer grounds each sentence
|
|
13
|
-
//
|
|
12
|
+
// recognizer grounds each sentence, plus chat.html's own memory chrome:
|
|
13
|
+
// starter memory seeded by default, a right-docked "this session's memory"
|
|
14
|
+
// panel, and best-effort persistence across a reload. One options row above
|
|
15
|
+
// the panes (seed with general knowledge / fuzzy low-trust tier); one
|
|
16
|
+
// actions row under them (ingest, export facts, reset to seed, clear).
|
|
14
17
|
//
|
|
15
18
|
// Behind the panes is the ONE recognizer seam the browser bundle exposes —
|
|
16
19
|
// session.ingest(text) — so a wider ingest tier plugs in without this page
|
|
17
|
-
// changing. Grounded facts write to
|
|
18
|
-
//
|
|
20
|
+
// changing. Grounded facts write to a PERSISTENT session store that survives
|
|
21
|
+
// across ingest clicks (a second paste extends the same memory, it never
|
|
22
|
+
// starts over); the facts pane itself still shows only what THIS ingest just
|
|
23
|
+
// grounded, live.
|
|
19
24
|
//
|
|
20
25
|
// renderIngestHtml() is pure: no I/O, deterministic output for identical
|
|
21
26
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
22
27
|
// to public/ingest.html, after ingest-browser.bundle.js already exists.
|
|
23
28
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
29
|
+
import { bandLabelFor, statsSummaryLine, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
|
|
24
30
|
|
|
25
31
|
const DEFAULT_TITLE = "the-mechanical-code-talker — ingest";
|
|
26
32
|
|
|
@@ -37,6 +43,29 @@ export function factTripleParts(fact) {
|
|
|
37
43
|
};
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/** The boot statusline while the big assets stream in — the same aggregator
|
|
47
|
+
* chat-page-viz.mjs's own loadProgressLine is (kept as this page's own copy
|
|
48
|
+
* rather than a shared import — the two pages' boot lines diverge slightly
|
|
49
|
+
* and neither is a collaborator the other calls). `parts` is an array of
|
|
50
|
+
* { loaded, total } byte counts (total 0 when the response carried no
|
|
51
|
+
* Content-Length); with no usable total the line shows loaded bytes alone
|
|
52
|
+
* rather than inventing a denominator. Self-contained, `.toString()`-splice
|
|
53
|
+
* safe. */
|
|
54
|
+
export function loadProgressLine(parts) {
|
|
55
|
+
const mb = (n) => (n / 1048576).toFixed(1);
|
|
56
|
+
let loaded = 0;
|
|
57
|
+
let total = 0;
|
|
58
|
+
let totalKnown = true;
|
|
59
|
+
for (const p of parts || []) {
|
|
60
|
+
loaded += (p && p.loaded) || 0;
|
|
61
|
+
if (p && p.total > 0) total += p.total;
|
|
62
|
+
else totalKnown = false;
|
|
63
|
+
}
|
|
64
|
+
return totalKnown && total > 0
|
|
65
|
+
? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
|
|
66
|
+
: "loading the engine… " + mb(loaded) + " MB";
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
/** The self-contained ingest page. Pure — the same output for the same
|
|
41
70
|
* `title` every time; every piece of state (the session, each grounded fact)
|
|
42
71
|
* is computed live in the browser once the sibling ingest bundle loads. */
|
|
@@ -57,10 +86,14 @@ export function renderIngestHtml({ title = DEFAULT_TITLE } = {}) {
|
|
|
57
86
|
<style>
|
|
58
87
|
${THEME_TOKENS_CSS}
|
|
59
88
|
html, body { height: 100%; }
|
|
60
|
-
body
|
|
89
|
+
/* body is the OUTER row: the ingest column plus the stats panel docked to
|
|
90
|
+
its right, the same split chat.html's own body/.chatCol/.statsPanel
|
|
91
|
+
layout holds. */
|
|
92
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; display: flex; overflow: hidden; }
|
|
93
|
+
.ingestCol { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
|
61
94
|
.mono { font-family: ${MONO_STACK}; }
|
|
62
95
|
button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
|
|
63
|
-
button:focus-visible, textarea:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
96
|
+
button:focus-visible, textarea:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
64
97
|
|
|
65
98
|
header.topbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
66
99
|
.brand { display: flex; flex-direction: column; gap: .1rem; }
|
|
@@ -74,6 +107,12 @@ ${THEME_TOKENS_CSS}
|
|
|
74
107
|
.pills button:last-child { border-right: none; }
|
|
75
108
|
.pills button[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
|
|
76
109
|
|
|
110
|
+
/* the options row — seed with general knowledge / the fuzzy low-trust
|
|
111
|
+
tier — both off/on switches in the statusline's own quiet mono idiom. */
|
|
112
|
+
.optionsRow { flex: 0 0 auto; display: flex; align-items: center; gap: 1.2rem; padding: .5rem 1.1rem; border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); flex-wrap: wrap; }
|
|
113
|
+
.optionToggle { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; white-space: nowrap; }
|
|
114
|
+
.optionToggle input { margin: 0; accent-color: var(--corpus); }
|
|
115
|
+
|
|
77
116
|
/* the two panes: a roomy input on the left, a soft-panel facts render on the
|
|
78
117
|
right. A CSS grid that stacks on a phone. */
|
|
79
118
|
main.panes { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); overflow: hidden; }
|
|
@@ -106,6 +145,26 @@ ${THEME_TOKENS_CSS}
|
|
|
106
145
|
.actions .btn:disabled { opacity: .45; cursor: default; }
|
|
107
146
|
.actions .status { margin-left: auto; font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); }
|
|
108
147
|
|
|
148
|
+
/* the provenance stats panel: what this session's memory holds, docked to
|
|
149
|
+
the right of the ingest column (a real layout column, not an overlay) —
|
|
150
|
+
the same class names and breakpoint chat-page-viz.mjs's own docked panel
|
|
151
|
+
uses, re-rendered after boot and after every ingest from
|
|
152
|
+
window.tmctIngest's own memoryStats(). */
|
|
153
|
+
.statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; }
|
|
154
|
+
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
155
|
+
.statsPanel h2:first-child { margin-top: 0; }
|
|
156
|
+
.statsPanel .band-row { display: flex; justify-content: space-between; gap: .6rem; margin: 0; padding: .12rem 0; }
|
|
157
|
+
.statsPanel .band-count { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
158
|
+
.statsPanel .taught-item { margin: 0 0 .7rem; }
|
|
159
|
+
.statsPanel .taught-tag { display: block; color: var(--muted); font-size: .66rem; margin-top: .15rem; word-break: break-word; }
|
|
160
|
+
.statsPanel .empty { color: var(--muted); margin: 0; }
|
|
161
|
+
.statsPanel .forget-btn { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .18rem .55rem; margin-top: 1.1rem; background: var(--card); }
|
|
162
|
+
.statsPanel .forget-btn:hover { color: var(--ink); }
|
|
163
|
+
.statsPanel .persist-note { color: var(--muted); font-size: .64rem; margin: .4rem 0 0; }
|
|
164
|
+
|
|
165
|
+
@media (max-width: 860px) {
|
|
166
|
+
.statsPanel { display: none; }
|
|
167
|
+
}
|
|
109
168
|
@media (max-width: 720px) {
|
|
110
169
|
main.panes { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }
|
|
111
170
|
}
|
|
@@ -113,46 +172,67 @@ ${THEME_TOKENS_CSS}
|
|
|
113
172
|
</style>
|
|
114
173
|
</head>
|
|
115
174
|
<body>
|
|
116
|
-
<
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<div class="pills" role="group" aria-label="Input mode">
|
|
122
|
-
<button type="button" id="modeText" aria-pressed="true">Text</button>
|
|
123
|
-
<button type="button" id="modeDoc" aria-pressed="false">Document</button>
|
|
124
|
-
</div>
|
|
125
|
-
</header>
|
|
126
|
-
<main class="panes">
|
|
127
|
-
<section class="pane inPane" id="inPane" aria-label="Text to ingest">
|
|
128
|
-
<div class="pane-head">
|
|
129
|
-
<span id="srcLabel">your text</span>
|
|
130
|
-
<button type="button" class="browse" id="browseBtn">browse for a file…</button>
|
|
131
|
-
<input type="file" id="fileInput" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
175
|
+
<div class="ingestCol">
|
|
176
|
+
<header class="topbar">
|
|
177
|
+
<div class="brand">
|
|
178
|
+
<span class="eyebrow">the-mechanical-code-talker</span>
|
|
179
|
+
<span class="subtitle">ingest — paste or drop text; it keeps only the facts it can ground, and skips the rest honestly</span>
|
|
132
180
|
</div>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
</section>
|
|
137
|
-
<section class="pane outPane" aria-label="Grounded canonical facts">
|
|
138
|
-
<div class="pane-head">
|
|
139
|
-
<span>canonical facts</span>
|
|
140
|
-
<span id="factCount" class="mono"></span>
|
|
181
|
+
<div class="pills" role="group" aria-label="Input mode">
|
|
182
|
+
<button type="button" id="modeText" aria-pressed="true">Text</button>
|
|
183
|
+
<button type="button" id="modeDoc" aria-pressed="false">Document</button>
|
|
141
184
|
</div>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
</header>
|
|
186
|
+
<div class="optionsRow" id="optionsRow">
|
|
187
|
+
<label class="optionToggle" title="Loads chat.html's own starter memory (persona, ConceptNet, WordNet and the rest) before ingesting, so a taught fact can link into what it already knows. Off keeps the previous empty-store fast path.">
|
|
188
|
+
<input type="checkbox" id="seedToggle" checked>
|
|
189
|
+
seed with general knowledge
|
|
190
|
+
</label>
|
|
191
|
+
<label class="optionToggle" title="On a miss, also tries a copula or known relation verb flanked by two resolvable entities as a low-trust candidate fact, tagged optimistic-extract — below every curated source, and never able to corroborate one.">
|
|
192
|
+
<input type="checkbox" id="fuzzyToggle">
|
|
193
|
+
fuzzy tier (low-trust candidates)
|
|
194
|
+
</label>
|
|
195
|
+
</div>
|
|
196
|
+
<main class="panes">
|
|
197
|
+
<section class="pane inPane" id="inPane" aria-label="Text to ingest">
|
|
198
|
+
<div class="pane-head">
|
|
199
|
+
<span id="srcLabel">your text</span>
|
|
200
|
+
<button type="button" class="browse" id="browseBtn">browse for a file…</button>
|
|
201
|
+
<input type="file" id="fileInput" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
202
|
+
</div>
|
|
203
|
+
<textarea id="source" spellcheck="false" autocapitalize="off"
|
|
204
|
+
placeholder="Paste text here, drop a .txt/.md file, or browse for one. Each sentence it recognizes as a fact ("A beagle is a kind of dog.") is kept; every other sentence is skipped, never guessed at."></textarea>
|
|
205
|
+
<div class="dropHint">drop the file to load it</div>
|
|
206
|
+
</section>
|
|
207
|
+
<section class="pane outPane" aria-label="Grounded canonical facts">
|
|
208
|
+
<div class="pane-head">
|
|
209
|
+
<span>canonical facts</span>
|
|
210
|
+
<span id="factCount" class="mono"></span>
|
|
211
|
+
</div>
|
|
212
|
+
<div id="facts"><p class="empty">Nothing ingested yet. The facts it grounds will appear here as it reads.</p></div>
|
|
213
|
+
</section>
|
|
214
|
+
</main>
|
|
215
|
+
<div class="actions">
|
|
216
|
+
<button type="button" class="btn primary" id="ingestBtn" disabled>ingest</button>
|
|
217
|
+
<button type="button" class="btn" id="downloadBtn" disabled>export facts</button>
|
|
218
|
+
<button type="button" class="btn" id="reinitStore" title="drop everything saved on this device and reload from the shipped seed">reset to seed</button>
|
|
219
|
+
<button type="button" class="btn" id="clearBtn">clear</button>
|
|
220
|
+
<span class="status" id="status">loading the engine…</span>
|
|
221
|
+
</div>
|
|
150
222
|
</div>
|
|
223
|
+
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
|
|
224
|
+
<p class="empty">loading memory stats…</p>
|
|
225
|
+
</aside>
|
|
151
226
|
<script src="./ingest-browser.bundle.js"></script>
|
|
152
227
|
<script>
|
|
153
228
|
(function () {
|
|
154
229
|
"use strict";
|
|
155
230
|
const factTripleParts = ${factTripleParts.toString()};
|
|
231
|
+
const loadProgressLine = ${loadProgressLine.toString()};
|
|
232
|
+
const bandLabelFor = ${bandLabelFor.toString()};
|
|
233
|
+
const statsSummaryLine = ${statsSummaryLine.toString()};
|
|
234
|
+
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
235
|
+
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
156
236
|
const el = (id) => document.getElementById(id);
|
|
157
237
|
|
|
158
238
|
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
@@ -170,9 +250,12 @@ ${THEME_TOKENS_CSS}
|
|
|
170
250
|
const srcLabel = el("srcLabel");
|
|
171
251
|
const modeTextBtn = el("modeText");
|
|
172
252
|
const modeDocBtn = el("modeDoc");
|
|
253
|
+
const seedToggleEl = el("seedToggle");
|
|
254
|
+
const fuzzyToggleEl = el("fuzzyToggle");
|
|
255
|
+
const statsPanelEl = el("statsPanel");
|
|
173
256
|
|
|
174
257
|
let session = null;
|
|
175
|
-
let grounded = 0; // facts on show in the right pane
|
|
258
|
+
let grounded = 0; // facts on show in the right pane, from the CURRENT ingest only
|
|
176
259
|
let sourceTag = "pasted text"; // what the header names the current input
|
|
177
260
|
|
|
178
261
|
// ---- input mode: Text | Document ---------------------------------------
|
|
@@ -233,12 +316,15 @@ ${THEME_TOKENS_CSS}
|
|
|
233
316
|
ingestBtn.disabled = !session || !sourceEl.value.trim();
|
|
234
317
|
}
|
|
235
318
|
|
|
236
|
-
// ---- the canonical facts pane
|
|
319
|
+
// ---- the canonical facts pane -------------------------------------------
|
|
320
|
+
// Shows only what the CURRENT ingest grounds, live — the underlying session
|
|
321
|
+
// store is persistent across ingest clicks (see below), but this pane
|
|
322
|
+
// clears at the start of every ingest so it never shows a stale mix of runs.
|
|
237
323
|
function clearFactsPane() {
|
|
238
324
|
factsEl.textContent = "";
|
|
239
325
|
grounded = 0;
|
|
240
326
|
factCountEl.textContent = "";
|
|
241
|
-
downloadBtn.disabled =
|
|
327
|
+
downloadBtn.disabled = !session;
|
|
242
328
|
const empty = document.createElement("p");
|
|
243
329
|
empty.className = "empty";
|
|
244
330
|
empty.textContent = "Nothing ingested yet. The facts it grounds will appear here as it reads.";
|
|
@@ -275,20 +361,200 @@ ${THEME_TOKENS_CSS}
|
|
|
275
361
|
factsEl.scrollTop = factsEl.scrollHeight;
|
|
276
362
|
}
|
|
277
363
|
|
|
278
|
-
// ----
|
|
364
|
+
// ---- memory stats: the docked panel, same convention as chat.html --------
|
|
365
|
+
async function renderStatsPanel(stats) {
|
|
366
|
+
if (!stats) {
|
|
367
|
+
if (!session || !window.tmctIngest.memoryStats) return;
|
|
368
|
+
try { stats = await window.tmctIngest.memoryStats(session.memoryDir); }
|
|
369
|
+
catch { return; }
|
|
370
|
+
}
|
|
371
|
+
renderStatsPanelInto(statsPanelEl, stats, {
|
|
372
|
+
bandLabel: bandLabelFor,
|
|
373
|
+
taughtHint: "nothing yet \\u2014 ingest some text and its grounded facts land here, with their source.",
|
|
374
|
+
onForget: persist ? forgetEverything : null,
|
|
375
|
+
persistNote: "taught facts are kept best-effort on this device (IndexedDB), never sent anywhere.",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- seed: chat.html's own starter memory, on by default -----------------
|
|
380
|
+
const SEED_PREF_KEY = "tmct.ingest.seed";
|
|
381
|
+
function readSeedPref() {
|
|
382
|
+
try {
|
|
383
|
+
const stored = localStorage.getItem(SEED_PREF_KEY);
|
|
384
|
+
return stored === null ? true : stored === "on";
|
|
385
|
+
} catch { return true; }
|
|
386
|
+
}
|
|
387
|
+
function writeSeedPref(on) {
|
|
388
|
+
try { localStorage.setItem(SEED_PREF_KEY, on ? "on" : "off"); } catch { /* private mode — this visit still works */ }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let seedPayload = null;
|
|
392
|
+
let seedFacts = 0;
|
|
393
|
+
const progressParts = {};
|
|
394
|
+
let progressActive = true;
|
|
395
|
+
function noteProgress(key, loaded, total) {
|
|
396
|
+
progressParts[key] = { loaded: loaded, total: total };
|
|
397
|
+
if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// The one branch this page's seed choice makes: checked, fetch and parse
|
|
401
|
+
// the same chat-seed.json chat.html embeds; unchecked, skip the request
|
|
402
|
+
// outright and stay on the previous empty-store fast path.
|
|
403
|
+
async function fetchSeedIfWanted() {
|
|
404
|
+
if (!seedToggleEl.checked) { seedPayload = null; seedFacts = 0; return; }
|
|
405
|
+
try {
|
|
406
|
+
const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
|
|
407
|
+
seedPayload = JSON.parse(await blob.text());
|
|
408
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
seedPayload = null;
|
|
411
|
+
seedFacts = 0;
|
|
412
|
+
console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function cloneSeed() {
|
|
416
|
+
if (!seedPayload) return null;
|
|
417
|
+
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
418
|
+
}
|
|
419
|
+
function newSession() {
|
|
420
|
+
return window.tmctIngest.createIngestSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload) });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---- engine boot ---------------------------------------------------------
|
|
424
|
+
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
425
|
+
async function tryLoadWink() {
|
|
426
|
+
let settled = false;
|
|
427
|
+
const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
|
|
428
|
+
try {
|
|
429
|
+
const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
|
|
430
|
+
settled = true;
|
|
431
|
+
window.tmctIngest.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
432
|
+
return "loaded";
|
|
433
|
+
} catch (err) {
|
|
434
|
+
settled = true;
|
|
435
|
+
console.warn("tmct ingest: the wink vendor asset failed to load; the recognizer needs it to split and parse sentences", err);
|
|
436
|
+
return "unavailable";
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// The deploy's own version, read off the service worker file the build
|
|
441
|
+
// already stamps — the only same-origin place the number exists at runtime
|
|
442
|
+
// without a second build artifact. Best-effort: no worker file, no match,
|
|
443
|
+
// no network -> "dev".
|
|
444
|
+
async function fetchSiteVersion() {
|
|
445
|
+
try {
|
|
446
|
+
const res = await fetch("./tmct-sw.js");
|
|
447
|
+
if (!res.ok) return "dev";
|
|
448
|
+
const found = /tmct-precache-v(\\d+\\.\\d+\\.\\d+)/.exec(await res.text());
|
|
449
|
+
return found ? found[1] : "dev";
|
|
450
|
+
} catch {
|
|
451
|
+
return "dev";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---- persistence: taught facts survive a reload, on this device ----------
|
|
456
|
+
let persist = null;
|
|
457
|
+
let saveTimer = null;
|
|
458
|
+
let siteVersion = "dev";
|
|
459
|
+
window.tmctIngestLastSave = null;
|
|
460
|
+
|
|
461
|
+
function scheduleSave() {
|
|
462
|
+
if (!persist) return;
|
|
463
|
+
clearTimeout(saveTimer);
|
|
464
|
+
saveTimer = setTimeout(() => {
|
|
465
|
+
saveTimer = null;
|
|
466
|
+
if (!session) return;
|
|
467
|
+
const started = performance.now();
|
|
468
|
+
let snapshot;
|
|
469
|
+
try {
|
|
470
|
+
snapshot = structuredClone(session.memoryDir.payload);
|
|
471
|
+
} catch {
|
|
472
|
+
try { snapshot = JSON.parse(JSON.stringify(session.memoryDir.payload)); } catch { return; }
|
|
473
|
+
}
|
|
474
|
+
persist.save(snapshot).then((saved) => {
|
|
475
|
+
if (saved) window.tmctIngestLastSave = { at: Date.now(), ms: Math.round(performance.now() - started) };
|
|
476
|
+
});
|
|
477
|
+
}, 500);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function forgetEverything() {
|
|
481
|
+
clearTimeout(saveTimer);
|
|
482
|
+
saveTimer = null;
|
|
483
|
+
if (persist) await persist.clear();
|
|
484
|
+
session = newSession();
|
|
485
|
+
clearFactsPane();
|
|
486
|
+
updateIngestEnabled();
|
|
487
|
+
const stats = await window.tmctIngest.memoryStats(session.memoryDir);
|
|
488
|
+
statusEl.textContent = "forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
|
|
489
|
+
await renderStatsPanel(stats);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Flipping the seed switch rebuilds the session from scratch under the new
|
|
493
|
+
// choice — a seeded and an unseeded store are different enough that
|
|
494
|
+
// half-carrying one visit's typed facts across the flip would be more
|
|
495
|
+
// confusing than starting clean.
|
|
496
|
+
seedToggleEl.addEventListener("change", async () => {
|
|
497
|
+
writeSeedPref(seedToggleEl.checked);
|
|
498
|
+
ingestBtn.disabled = true;
|
|
499
|
+
statusEl.textContent = seedToggleEl.checked ? "loading starter memory\\u2026" : "starting unseeded\\u2026";
|
|
500
|
+
await fetchSeedIfWanted();
|
|
501
|
+
clearTimeout(saveTimer);
|
|
502
|
+
saveTimer = null;
|
|
503
|
+
session = newSession();
|
|
504
|
+
clearFactsPane();
|
|
505
|
+
const stats = await window.tmctIngest.memoryStats(session.memoryDir);
|
|
506
|
+
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + " \\u2014 ready.";
|
|
507
|
+
await renderStatsPanel(stats);
|
|
508
|
+
updateIngestEnabled();
|
|
509
|
+
sourceEl.focus();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
async function boot() {
|
|
513
|
+
if (!window.tmctIngest) {
|
|
514
|
+
statusEl.textContent = "the ingest engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
seedToggleEl.checked = readSeedPref();
|
|
518
|
+
const [winkStatus] = await Promise.all([
|
|
519
|
+
tryLoadWink(),
|
|
520
|
+
fetchSeedIfWanted(),
|
|
521
|
+
fetchSiteVersion().then((v) => { siteVersion = v; }),
|
|
522
|
+
]);
|
|
523
|
+
progressActive = false;
|
|
524
|
+
if (window.tmctIngest.openPersistedStore) {
|
|
525
|
+
persist = window.tmctIngest.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts });
|
|
526
|
+
}
|
|
527
|
+
const savedRecord = persist ? await persist.load() : null;
|
|
528
|
+
session = savedRecord && savedRecord.payload
|
|
529
|
+
? window.tmctIngest.createIngestSession({ seedPayload: savedRecord.payload, vocabSeeded: true })
|
|
530
|
+
: newSession();
|
|
531
|
+
setMode(false);
|
|
532
|
+
updateIngestEnabled();
|
|
533
|
+
const stats = await window.tmctIngest.memoryStats(session.memoryDir);
|
|
534
|
+
const winkPart = winkStatus === "loaded"
|
|
535
|
+
? "wink-nlp: loaded"
|
|
536
|
+
: "wink-nlp unavailable \\u2014 the recognizer can't split sentences without it";
|
|
537
|
+
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + " \\u00b7 " + winkPart
|
|
538
|
+
+ (savedRecord ? " \\u2014 restored from your last visit." : " \\u2014 paste or drop text, then ingest.");
|
|
539
|
+
await renderStatsPanel(stats);
|
|
540
|
+
sourceEl.focus();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---- ingest: the one seam call -------------------------------------------
|
|
544
|
+
// The session is PERSISTENT across ingest clicks — a second paste extends
|
|
545
|
+
// the same memory rather than starting over — so only the facts pane
|
|
546
|
+
// clears per click, never the underlying store.
|
|
279
547
|
let busy = false;
|
|
280
548
|
ingestBtn.addEventListener("click", async () => {
|
|
281
549
|
const text = sourceEl.value.trim();
|
|
282
550
|
if (!text || busy || !session) return;
|
|
283
551
|
busy = true;
|
|
284
552
|
ingestBtn.disabled = true;
|
|
285
|
-
// A fresh ingest starts a fresh store, so the right pane and the canonical
|
|
286
|
-
// download always describe exactly the text now in the box.
|
|
287
|
-
session = window.tmctIngest.createIngestSession();
|
|
288
553
|
clearFactsPane();
|
|
289
554
|
statusEl.textContent = "reading\\u2026";
|
|
290
555
|
try {
|
|
291
556
|
const summary = await session.ingest(text, {
|
|
557
|
+
optimistic: fuzzyToggleEl.checked,
|
|
292
558
|
onFact: (fact) => { appendFactRow(fact); return new Promise((r) => setTimeout(r, 0)); },
|
|
293
559
|
});
|
|
294
560
|
statusEl.textContent = summary.sentences + " sentence" + (summary.sentences === 1 ? "" : "s")
|
|
@@ -303,7 +569,10 @@ ${THEME_TOKENS_CSS}
|
|
|
303
569
|
note.textContent = "No sentence here was a fact it could ground. Try a plain statement like \\u201cA beagle is a kind of dog.\\u201d";
|
|
304
570
|
factsEl.appendChild(note);
|
|
305
571
|
}
|
|
572
|
+
} else {
|
|
573
|
+
scheduleSave();
|
|
306
574
|
}
|
|
575
|
+
await renderStatsPanel();
|
|
307
576
|
} catch (err) {
|
|
308
577
|
statusEl.textContent = "something went wrong reading that (" + (err && err.message ? err.message : err) + ")";
|
|
309
578
|
} finally {
|
|
@@ -312,7 +581,7 @@ ${THEME_TOKENS_CSS}
|
|
|
312
581
|
}
|
|
313
582
|
});
|
|
314
583
|
|
|
315
|
-
// ---- download the canonical facts as JSONL
|
|
584
|
+
// ---- download the canonical facts as JSONL -------------------------------
|
|
316
585
|
downloadBtn.addEventListener("click", async () => {
|
|
317
586
|
if (!session || !window.tmctIngest.exportFactsJsonl) return;
|
|
318
587
|
let jsonl;
|
|
@@ -333,49 +602,29 @@ ${THEME_TOKENS_CSS}
|
|
|
333
602
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
334
603
|
});
|
|
335
604
|
|
|
605
|
+
// "reset to seed" is the full re-initialisation: drop the persisted payload
|
|
606
|
+
// outright and reload, so boot re-seeds from the page's shipped seed as if
|
|
607
|
+
// on a first visit.
|
|
608
|
+
el("reinitStore").addEventListener("click", async () => {
|
|
609
|
+
clearTimeout(saveTimer);
|
|
610
|
+
saveTimer = null;
|
|
611
|
+
if (persist) await persist.clear();
|
|
612
|
+
window.location.reload();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// "clear" only resets the UI (the textarea and this-run's facts pane) — the
|
|
616
|
+
// underlying session and everything it has grounded so far stays intact,
|
|
617
|
+
// matching the persistent-session contract above.
|
|
336
618
|
clearBtn.addEventListener("click", () => {
|
|
337
619
|
sourceEl.value = "";
|
|
338
620
|
sourceTag = "pasted text";
|
|
339
621
|
srcLabel.textContent = modeDocBtn.getAttribute("aria-pressed") === "true" ? "drop or browse for a file" : "pasted text";
|
|
340
|
-
session = window.tmctIngest ? window.tmctIngest.createIngestSession() : null;
|
|
341
622
|
clearFactsPane();
|
|
342
623
|
statusEl.textContent = "cleared";
|
|
343
624
|
updateIngestEnabled();
|
|
344
625
|
sourceEl.focus();
|
|
345
626
|
});
|
|
346
627
|
|
|
347
|
-
// ---- engine boot -------------------------------------------------------
|
|
348
|
-
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
349
|
-
async function tryLoadWink() {
|
|
350
|
-
let settled = false;
|
|
351
|
-
const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
|
|
352
|
-
try {
|
|
353
|
-
const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
|
|
354
|
-
settled = true;
|
|
355
|
-
window.tmctIngest.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
356
|
-
return "loaded";
|
|
357
|
-
} catch (err) {
|
|
358
|
-
settled = true;
|
|
359
|
-
console.warn("tmct ingest: the wink vendor asset failed to load; the recognizer needs it to split and parse sentences", err);
|
|
360
|
-
return "unavailable";
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function boot() {
|
|
365
|
-
if (!window.tmctIngest) {
|
|
366
|
-
statusEl.textContent = "the ingest engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const winkStatus = await tryLoadWink();
|
|
370
|
-
session = window.tmctIngest.createIngestSession();
|
|
371
|
-
setMode(false);
|
|
372
|
-
updateIngestEnabled();
|
|
373
|
-
statusEl.textContent = winkStatus === "loaded"
|
|
374
|
-
? "ready \\u2014 paste or drop text, then ingest"
|
|
375
|
-
: "wink-nlp unavailable \\u2014 the recognizer can't split sentences without it";
|
|
376
|
-
sourceEl.focus();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
628
|
window.tmctIngestReady = boot().catch((err) => {
|
|
380
629
|
console.error("tmct ingest failed to boot", err);
|
|
381
630
|
statusEl.textContent = "the ingest page failed to start (" + (err && err.message ? err.message : err) + ")";
|