@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
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
// exist (both built earlier in that same script, for the embedded widget).
|
|
31
31
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
32
32
|
import { provBucketFor } from "./ledger-viz.mjs";
|
|
33
|
+
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
33
34
|
import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
|
|
35
|
+
import { bandLabelFor, statsSummaryLine, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
|
|
34
36
|
|
|
35
37
|
const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
|
|
36
38
|
|
|
@@ -234,25 +236,37 @@ ${THEME_TOKENS_CSS}
|
|
|
234
236
|
.composer-inner button[type="submit"] { width: 2.3rem; height: 2.3rem; border-radius: 50%; background: var(--ink); color: var(--bg); display: flex; align-items: center; justify-content: center; font-size: 1rem; flex: 0 0 auto; }
|
|
235
237
|
.composer-inner button[type="submit"]:disabled { opacity: .4; cursor: default; }
|
|
236
238
|
|
|
237
|
-
/* the
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.composer-
|
|
243
|
-
.
|
|
244
|
-
.
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
239
|
+
/* the wikipedia-mode row, under the input: a plain radio group (off / on a
|
|
240
|
+
miss / always) plus the synthesis-budget slider, both in the
|
|
241
|
+
statusline's own quiet mono idiom. "supplement" (typed /wiki supplement
|
|
242
|
+
only) has no radio of its own — every radio clears when that mode is
|
|
243
|
+
active, and the statusline names it instead. */
|
|
244
|
+
.composer-wiki { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .4rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
245
|
+
fieldset.wikiMode { border: none; margin: 0; padding: 0; display: inline-flex; align-items: center; gap: .7rem; }
|
|
246
|
+
fieldset.wikiMode legend { padding: 0; margin-right: .15rem; font: inherit; color: inherit; }
|
|
247
|
+
fieldset.wikiMode label { display: inline-flex; align-items: center; gap: .28rem; cursor: pointer; white-space: nowrap; }
|
|
248
|
+
fieldset.wikiMode input[type="radio"] { margin: 0; accent-color: var(--corpus); }
|
|
249
|
+
.synthRow { display: inline-flex; align-items: center; gap: .5rem; white-space: nowrap; }
|
|
250
|
+
.synthRow input[type="range"] { width: 88px; accent-color: var(--corpus); }
|
|
251
|
+
|
|
252
|
+
/* the research row: type a topic, and the page asks "research <topic>"
|
|
253
|
+
then ticks "research next" turns through the queue — play/pause rides
|
|
254
|
+
the shared viz-ticker verbs. Same quiet mono idiom as the wiki row. */
|
|
255
|
+
.composer-research { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .4rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
256
|
+
.composer-research label[for="researchTopic"] { white-space: nowrap; }
|
|
257
|
+
.composer-research input[type="text"] { 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: .28rem .55rem; }
|
|
258
|
+
.composer-research input[type="text"]::placeholder { color: var(--muted); }
|
|
259
|
+
.research-btn { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .03em; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .16rem .55rem; background: var(--card); }
|
|
260
|
+
.research-btn:hover { color: var(--ink); }
|
|
261
|
+
.research-btn[aria-pressed="true"] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
262
|
+
#researchQueueStatus { white-space: nowrap; }
|
|
249
263
|
|
|
250
264
|
.statusline { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .6rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
251
265
|
|
|
252
|
-
/* the composer's small utility row — right-aligned mono controls
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.composer-tools { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .35rem; display: flex; justify-content: flex-end; align-items: center; gap: .5rem; }
|
|
266
|
+
/* the composer's small utility row — right-aligned mono controls, for
|
|
267
|
+
anything that acts on the conversation as a whole (export, print, reset)
|
|
268
|
+
rather than on one turn. */
|
|
269
|
+
.composer-tools { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .35rem; display: flex; justify-content: flex-end; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
256
270
|
.tool-btn { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .03em; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .16rem .55rem; background: var(--card); }
|
|
257
271
|
.tool-btn:hover { color: var(--ink); }
|
|
258
272
|
|
|
@@ -316,20 +330,33 @@ ${THEME_TOKENS_CSS}
|
|
|
316
330
|
placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
|
|
317
331
|
<button type="submit" id="composerSend" aria-label="Send" disabled>→</button>
|
|
318
332
|
</div>
|
|
319
|
-
<div class="composer-
|
|
320
|
-
<
|
|
321
|
-
<
|
|
322
|
-
<
|
|
323
|
-
<
|
|
333
|
+
<div class="composer-wiki">
|
|
334
|
+
<fieldset class="wikiMode" id="wikiMode" title="Off by default. "when I don't know": a question nothing local can answer also asks en.wikipedia.org — two small requests per lookup, cited (CC BY-SA). "always": every grounded answer also gets a cited Wikipedia read-out. Type /wiki supplement for that same read-out on grounded answers only, without switching this to always.">
|
|
335
|
+
<legend>ask wikipedia</legend>
|
|
336
|
+
<label><input type="radio" name="wikiMode" id="wikiOff" value="off" checked> off</label>
|
|
337
|
+
<label><input type="radio" name="wikiMode" id="wikiMiss" value="miss"> when I don’t know</label>
|
|
338
|
+
<label><input type="radio" name="wikiMode" id="wikiAlways" value="always"> always</label>
|
|
339
|
+
</fieldset>
|
|
340
|
+
<label class="synthRow" for="synthSlider" title="How many facts to work out and store, entailed, after each Wikipedia-sourced load. 0 stores the article's own stated facts only, with no entailment pass.">
|
|
341
|
+
synthesize from wikipedia: <span id="synthValue" class="mono">12</span>
|
|
342
|
+
<input type="range" id="synthSlider" min="0" max="24" step="4" value="12">
|
|
324
343
|
</label>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
</div>
|
|
345
|
+
<div class="composer-research">
|
|
346
|
+
<label for="researchTopic" title="Fetches the topic from Simple English Wikipedia and stores the facts it grounds, then queues the topics its lead section links to. Asking is the network consent for these fetches; each queued topic is asked as its own chat turn, paced politely.">research:</label>
|
|
347
|
+
<input id="researchTopic" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
348
|
+
placeholder="a topic, e.g. owls" aria-label="Topic to research on Simple English Wikipedia">
|
|
349
|
+
<button type="button" id="researchGo" class="research-btn">go</button>
|
|
350
|
+
<button type="button" id="researchPlay" class="research-btn" aria-pressed="false" hidden>play</button>
|
|
351
|
+
<span id="researchQueueStatus" aria-live="polite"></span>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="composer-tools">
|
|
354
|
+
<button type="button" id="ingestFile" class="tool-btn" title="load a .txt/.md file and teach every fact it recognizes into this session">ingest file</button>
|
|
355
|
+
<input type="file" id="ingestInput" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
356
|
+
<button type="button" id="exportMd" class="tool-btn" title="download this conversation as Markdown">export .md</button>
|
|
357
|
+
<button type="button" id="exportFacts" class="tool-btn" title="download this session's facts as JSONL (the tmct extract shape, provenance included)">export facts</button>
|
|
358
|
+
<button type="button" id="printChat" class="tool-btn" title="print the whole conversation">print</button>
|
|
359
|
+
<button type="button" id="reinitStore" class="tool-btn" title="drop everything saved on this device and reload from the shipped seed">reset to seed</button>
|
|
333
360
|
</div>
|
|
334
361
|
</form>
|
|
335
362
|
<div class="statusline" id="status">loading the engine…</div>
|
|
@@ -348,6 +375,12 @@ ${THEME_TOKENS_CSS}
|
|
|
348
375
|
const sessionLogHeaderMarkdown = ${sessionLogHeaderMarkdown.toString()};
|
|
349
376
|
const sessionLogTurnMarkdown = ${sessionLogTurnMarkdown.toString()};
|
|
350
377
|
const transcriptMarkdown = ${transcriptMarkdown.toString()};
|
|
378
|
+
const bandLabelFor = ${bandLabelFor.toString()};
|
|
379
|
+
const statsSummaryLine = ${statsSummaryLine.toString()};
|
|
380
|
+
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
381
|
+
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
382
|
+
const createTicker = ${createTicker.toString()};
|
|
383
|
+
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
351
384
|
const el = (id) => document.getElementById(id);
|
|
352
385
|
|
|
353
386
|
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
@@ -358,20 +391,54 @@ ${THEME_TOKENS_CSS}
|
|
|
358
391
|
const sendBtn = el("composerSend");
|
|
359
392
|
const statusEl = el("status");
|
|
360
393
|
const statsPanelEl = el("statsPanel");
|
|
361
|
-
const
|
|
394
|
+
const wikiModeFieldset = el("wikiMode");
|
|
395
|
+
const wikiModeRadios = Array.prototype.slice.call(wikiModeFieldset.querySelectorAll('input[type="radio"]'));
|
|
396
|
+
const synthSliderEl = el("synthSlider");
|
|
397
|
+
const synthValueEl = el("synthValue");
|
|
362
398
|
|
|
363
|
-
// The
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
399
|
+
// The wikipedia mode: "off" | "miss" (the radio's own value for what the
|
|
400
|
+
// session calls plain true, a rescue on a clean miss) | "always". "supplement"
|
|
401
|
+
// (typed /wiki supplement only) never lands here — a turn that sets it
|
|
402
|
+
// clears every radio instead, read back off the session's own getter.
|
|
403
|
+
// try/caught throughout — private-mode storage that throws must never
|
|
404
|
+
// break the page, it just forgets the preference between visits.
|
|
405
|
+
const WIKI_MODE_KEY = "tmct.chat.wikiMode";
|
|
406
|
+
const LEGACY_LIVE_PREF_KEY = "tmct.chat.liveWikipedia";
|
|
407
|
+
function readWikiMode() {
|
|
408
|
+
try {
|
|
409
|
+
const stored = localStorage.getItem(WIKI_MODE_KEY);
|
|
410
|
+
if (stored === "off" || stored === "miss" || stored === "always") return stored;
|
|
411
|
+
if (localStorage.getItem(LEGACY_LIVE_PREF_KEY) === "on") return "miss";
|
|
412
|
+
} catch { /* private mode — starts at the default this visit */ }
|
|
413
|
+
return "off";
|
|
414
|
+
}
|
|
415
|
+
function writeWikiMode(mode) {
|
|
416
|
+
try {
|
|
417
|
+
localStorage.setItem(WIKI_MODE_KEY, mode);
|
|
418
|
+
localStorage.removeItem(LEGACY_LIVE_PREF_KEY);
|
|
419
|
+
} catch { /* private mode — the choice still works this visit */ }
|
|
369
420
|
}
|
|
370
|
-
|
|
421
|
+
const liveReferenceForMode = (mode) => (mode === "always" ? "always" : mode === "miss");
|
|
422
|
+
function setWikiModeRadios(mode) {
|
|
423
|
+
for (const radio of wikiModeRadios) radio.checked = radio.value === mode;
|
|
424
|
+
}
|
|
425
|
+
function checkedWikiMode() {
|
|
426
|
+
const checked = wikiModeRadios.find((r) => r.checked);
|
|
427
|
+
return checked ? checked.value : "off";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The synthesis budget: how many facts an auto-synthesis pass may add,
|
|
431
|
+
// entailed, after each Wikipedia-sourced load. 0 disables it.
|
|
432
|
+
const SYNTH_BUDGET_KEY = "tmct.chat.synthBudget";
|
|
433
|
+
function readSynthBudget() {
|
|
371
434
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
} catch { /* private mode —
|
|
435
|
+
const n = Number(localStorage.getItem(SYNTH_BUDGET_KEY));
|
|
436
|
+
if (Number.isFinite(n) && n >= 0 && n <= 24) return n;
|
|
437
|
+
} catch { /* private mode — starts at the default this visit */ }
|
|
438
|
+
return 12;
|
|
439
|
+
}
|
|
440
|
+
function writeSynthBudget(n) {
|
|
441
|
+
try { localStorage.setItem(SYNTH_BUDGET_KEY, String(n)); } catch { /* private mode — this visit still works */ }
|
|
375
442
|
}
|
|
376
443
|
|
|
377
444
|
function scrollToEnd() {
|
|
@@ -463,31 +530,6 @@ ${THEME_TOKENS_CSS}
|
|
|
463
530
|
if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
464
531
|
}
|
|
465
532
|
|
|
466
|
-
// Fetch the url reading the body as a stream, reporting (loadedBytes,
|
|
467
|
-
// totalBytes) after every chunk — total is 0 when the response carries no
|
|
468
|
-
// Content-Length. Resolves to a Blob of the whole body.
|
|
469
|
-
async function fetchWithProgress(url, onProgress) {
|
|
470
|
-
const res = await fetch(url);
|
|
471
|
-
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
472
|
-
const total = Number(res.headers.get("content-length")) || 0;
|
|
473
|
-
if (!res.body || !res.body.getReader) {
|
|
474
|
-
const blob = await res.blob();
|
|
475
|
-
onProgress(blob.size, total || blob.size);
|
|
476
|
-
return blob;
|
|
477
|
-
}
|
|
478
|
-
const reader = res.body.getReader();
|
|
479
|
-
const chunks = [];
|
|
480
|
-
let loaded = 0;
|
|
481
|
-
for (;;) {
|
|
482
|
-
const step = await reader.read();
|
|
483
|
-
if (step.done) break;
|
|
484
|
-
chunks.push(step.value);
|
|
485
|
-
loaded += step.value.byteLength;
|
|
486
|
-
onProgress(loaded, total);
|
|
487
|
-
}
|
|
488
|
-
return new Blob(chunks);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
533
|
let winkStatus = "pending";
|
|
492
534
|
async function tryLoadWink() {
|
|
493
535
|
// The bounded race guards the same failure the CDN era did — a load that
|
|
@@ -569,7 +611,8 @@ ${THEME_TOKENS_CSS}
|
|
|
569
611
|
return window.tmctChat.createChatSession({
|
|
570
612
|
seedPayload: cloneSeed(),
|
|
571
613
|
vocabSeeded: Boolean(seedPayload),
|
|
572
|
-
liveReference:
|
|
614
|
+
liveReference: liveReferenceForMode(checkedWikiMode()),
|
|
615
|
+
synthesisBudget: readSynthBudget(),
|
|
573
616
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
574
617
|
});
|
|
575
618
|
}
|
|
@@ -636,7 +679,7 @@ ${THEME_TOKENS_CSS}
|
|
|
636
679
|
restoredCount = 0;
|
|
637
680
|
window.tmctChatSession = newSession();
|
|
638
681
|
const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
|
|
639
|
-
addSystemLine("forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats) + ").");
|
|
682
|
+
addSystemLine("forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").");
|
|
640
683
|
await renderStatsPanel(stats);
|
|
641
684
|
}
|
|
642
685
|
|
|
@@ -644,48 +687,8 @@ ${THEME_TOKENS_CSS}
|
|
|
644
687
|
// Both read window.tmctChat.memoryStats(memoryDir) (chat-browser-entry.mjs)
|
|
645
688
|
// — one computation, reused, so the boot line and the panel can never
|
|
646
689
|
// disagree with each other about what this session's memory holds.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
"human-medium": "human persona (medium)",
|
|
650
|
-
"human-large": "human persona (large)",
|
|
651
|
-
seon: "seon ontology",
|
|
652
|
-
conceptnet: "ConceptNet",
|
|
653
|
-
"tier2-aws": "AWS",
|
|
654
|
-
"tier2-python": "Python",
|
|
655
|
-
"tier2-java": "Java",
|
|
656
|
-
"wordnet-xl": "WordNet",
|
|
657
|
-
};
|
|
658
|
-
const BAND_ORDER = [
|
|
659
|
-
"human", "human-medium", "human-large", "seon", "conceptnet",
|
|
660
|
-
"tier2-aws", "tier2-python", "tier2-java", "wordnet-xl",
|
|
661
|
-
"taught this session", "other",
|
|
662
|
-
];
|
|
663
|
-
const bandLabel = (key) => BAND_LABELS[key] || key;
|
|
664
|
-
|
|
665
|
-
/** The boot system line's own memory summary — every seed band this
|
|
666
|
-
* session actually loaded, named with its real count, left-to-right in
|
|
667
|
-
* BAND_ORDER; a session with nothing seeded says so plainly instead of
|
|
668
|
-
* naming zero facts. */
|
|
669
|
-
function statsSummaryLine(stats) {
|
|
670
|
-
if (!stats || !stats.total) return "no starter memory; starting empty";
|
|
671
|
-
const parts = BAND_ORDER.filter((k) => stats.bandCounts[k]).map((k) => stats.bandCounts[k] + " " + bandLabel(k));
|
|
672
|
-
return parts.length
|
|
673
|
-
? "starter memory: " + parts.join(" + ") + " (" + stats.total + " facts total)"
|
|
674
|
-
: stats.total + " starter facts loaded";
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function bandRow(label, count) {
|
|
678
|
-
const row = document.createElement("p");
|
|
679
|
-
row.className = "band-row";
|
|
680
|
-
const l = document.createElement("span");
|
|
681
|
-
l.textContent = label;
|
|
682
|
-
const c = document.createElement("span");
|
|
683
|
-
c.className = "band-count";
|
|
684
|
-
c.textContent = String(count);
|
|
685
|
-
row.appendChild(l);
|
|
686
|
-
row.appendChild(c);
|
|
687
|
-
return row;
|
|
688
|
-
}
|
|
690
|
+
// bandLabelFor/statsSummaryLine/renderStatsPanelInto are the shared
|
|
691
|
+
// memory-panel-viz.mjs helpers, spliced in above.
|
|
689
692
|
|
|
690
693
|
/** (Re)render the docked panel from a memoryStats() result — stats may be
|
|
691
694
|
* passed in already-computed (boot reuses its own call rather than asking
|
|
@@ -698,46 +701,18 @@ ${THEME_TOKENS_CSS}
|
|
|
698
701
|
try { stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir); }
|
|
699
702
|
catch { return; }
|
|
700
703
|
}
|
|
701
|
-
statsPanelEl
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "taught this session" }));
|
|
709
|
-
if (!stats.taught.length) {
|
|
710
|
-
const empty = document.createElement("p");
|
|
711
|
-
empty.className = "empty";
|
|
712
|
-
empty.textContent = 'nothing yet \\u2014 teach it something ("a dog is a kind of animal") and it lands here, with its source.';
|
|
713
|
-
statsPanelEl.appendChild(empty);
|
|
714
|
-
} else {
|
|
715
|
-
for (const fact of stats.taught.slice(-8).reverse()) {
|
|
716
|
-
const item = document.createElement("p");
|
|
717
|
-
item.className = "taught-item";
|
|
718
|
-
item.appendChild(document.createTextNode(fact.subject + " " + fact.predicate + " " + fact.object));
|
|
719
|
-
const tag = document.createElement("span");
|
|
720
|
-
tag.className = "taught-tag";
|
|
721
|
-
tag.textContent = fact.tag;
|
|
722
|
-
item.appendChild(tag);
|
|
723
|
-
statsPanelEl.appendChild(item);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
704
|
+
renderStatsPanelInto(statsPanelEl, stats, {
|
|
705
|
+
bandLabel: bandLabelFor,
|
|
706
|
+
onForget: persist ? forgetEverything : null,
|
|
707
|
+
persistNote: "taught facts are kept best-effort on this device (IndexedDB), never sent anywhere.",
|
|
708
|
+
});
|
|
709
|
+
}
|
|
726
710
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
forget.textContent = "forget everything";
|
|
733
|
-
forget.title = "clear what this device has saved and restart from the fresh seed";
|
|
734
|
-
forget.addEventListener("click", forgetEverything);
|
|
735
|
-
statsPanelEl.appendChild(forget);
|
|
736
|
-
const note = document.createElement("p");
|
|
737
|
-
note.className = "persist-note";
|
|
738
|
-
note.textContent = "taught facts are kept best-effort on this device (IndexedDB), never sent anywhere.";
|
|
739
|
-
statsPanelEl.appendChild(note);
|
|
740
|
-
}
|
|
711
|
+
// "supplement" (typed /wiki supplement only) has no radio; the statusline
|
|
712
|
+
// still names it, read straight off the session's own liveReference getter
|
|
713
|
+
// rather than the last radio the page itself set.
|
|
714
|
+
function liveStatusWord(liveReference) {
|
|
715
|
+
return liveReference === "always" ? "always" : liveReference === "supplement" ? "supplement" : liveReference ? "on" : "off";
|
|
741
716
|
}
|
|
742
717
|
|
|
743
718
|
function renderStatus() {
|
|
@@ -749,18 +724,42 @@ ${THEME_TOKENS_CSS}
|
|
|
749
724
|
: winkStatus === "unavailable"
|
|
750
725
|
? "wink-nlp unavailable — curated + fuzzy tiers only (still zero guesses, zero LLM)"
|
|
751
726
|
: "wink-nlp: loading\\u2026";
|
|
752
|
-
const
|
|
727
|
+
const liveReference = window.tmctChatSession ? window.tmctChatSession.liveReference : liveReferenceForMode(checkedWikiMode());
|
|
728
|
+
const livePart = "live wikipedia: " + liveStatusWord(liveReference);
|
|
753
729
|
statusEl.textContent = seedPart + " \\u00b7 " + winkPart + " \\u00b7 " + livePart;
|
|
754
730
|
}
|
|
755
731
|
|
|
756
|
-
|
|
757
|
-
|
|
732
|
+
// A "/wiki on|off|supplement|always" turn flips the session's own state;
|
|
733
|
+
// this mirrors it back into the radio group and the stored preference.
|
|
734
|
+
// "supplement" clears every radio (it has none of its own) rather than
|
|
735
|
+
// leaving a stale mode checked.
|
|
736
|
+
function mirrorWikiModeFromSession() {
|
|
737
|
+
const session = window.tmctChatSession;
|
|
738
|
+
if (!session || typeof session.liveReference === "undefined") return;
|
|
739
|
+
const liveReference = session.liveReference;
|
|
740
|
+
const mode = liveReference === "always" ? "always" : liveReference === true ? "miss" : liveReference === false ? "off" : null;
|
|
741
|
+
setWikiModeRadios(mode || "");
|
|
742
|
+
if (mode) writeWikiMode(mode);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
wikiModeFieldset.addEventListener("change", function () {
|
|
746
|
+
const mode = checkedWikiMode();
|
|
747
|
+
writeWikiMode(mode);
|
|
758
748
|
if (window.tmctChatSession && window.tmctChatSession.setLiveReference) {
|
|
759
|
-
window.tmctChatSession.setLiveReference(
|
|
749
|
+
window.tmctChatSession.setLiveReference(liveReferenceForMode(mode));
|
|
760
750
|
}
|
|
761
751
|
renderStatus();
|
|
762
752
|
});
|
|
763
753
|
|
|
754
|
+
synthSliderEl.addEventListener("input", function () {
|
|
755
|
+
const n = Number(synthSliderEl.value);
|
|
756
|
+
synthValueEl.textContent = String(n);
|
|
757
|
+
writeSynthBudget(n);
|
|
758
|
+
if (window.tmctChatSession && window.tmctChatSession.setSynthesisBudget) {
|
|
759
|
+
window.tmctChatSession.setSynthesisBudget(n);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
764
763
|
let busy = true;
|
|
765
764
|
function setBusy(v) {
|
|
766
765
|
busy = v;
|
|
@@ -769,44 +768,117 @@ ${THEME_TOKENS_CSS}
|
|
|
769
768
|
sendBtn.disabled = v || !ready;
|
|
770
769
|
}
|
|
771
770
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
771
|
+
// ONE dispatched turn through the page — the composer form and the
|
|
772
|
+
// research ticker both submit here, so an auto-played "research next"
|
|
773
|
+
// renders exactly like a typed one: user bubble, transcript entry, pending
|
|
774
|
+
// bubble, settle, persist, stats. Resolves once the turn has settled (the
|
|
775
|
+
// ticker awaits it before pacing the next step).
|
|
776
|
+
async function submitLine(q) {
|
|
777
|
+
if (!q || busy || !window.tmctChatSession) return null;
|
|
777
778
|
addUserBubble(q);
|
|
778
779
|
transcript.push({ role: "you", text: q, chipTier: null, ts: Date.now() });
|
|
779
780
|
const pendingRow = addPendingAssistantBubble();
|
|
780
781
|
setBusy(true);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
782
|
+
let result = null;
|
|
783
|
+
try {
|
|
784
|
+
result = await window.tmctChatSession.turn(q);
|
|
785
|
+
settleAssistantBubble(pendingRow, result.answer, result.record);
|
|
786
|
+
// Persist on ANY store write, not just a teach turn: a learn-on-miss
|
|
787
|
+
// load (a child pack, a reference or live-Wikipedia article, a
|
|
788
|
+
// research step) and its auto-synthesis also append facts, and those
|
|
789
|
+
// were lost on reload when only via==="assert" saved. Commands write
|
|
790
|
+
// nothing, so they stay out. The save is debounced, so a read-through
|
|
791
|
+
// that changed nothing costs at most one coalesced write.
|
|
792
|
+
if (result.record && result.record.via !== "command") scheduleSave();
|
|
793
|
+
await renderStatsPanel(); // a teach or learned-load turn grew this session's memory; a plain ask leaves it unchanged either way
|
|
794
|
+
} catch (err) {
|
|
795
|
+
settleAssistantBubble(pendingRow,
|
|
794
796
|
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
795
|
-
{ miss: true })
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
797
|
+
{ miss: true });
|
|
798
|
+
} finally {
|
|
799
|
+
// A "/wiki on|off|supplement|always" turn flips the session's own
|
|
800
|
+
// state — mirror it back into the radio group and the stored
|
|
801
|
+
// preference, then settle the statusline (which the onLiveLookup
|
|
802
|
+
// hook may have overwritten with "searching wikipedia…" mid-turn).
|
|
803
|
+
mirrorWikiModeFromSession();
|
|
804
|
+
renderStatus();
|
|
805
|
+
setBusy(false);
|
|
806
|
+
}
|
|
807
|
+
if (result) noteResearchResult(result);
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
composerForm.addEventListener("submit", (e) => {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
const q = inputEl.value.trim();
|
|
814
|
+
if (!q || busy || !window.tmctChatSession) return;
|
|
815
|
+
inputEl.value = "";
|
|
816
|
+
submitLine(q).then(() => inputEl.focus());
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// ---- the research queue: play/pause over "research next" turns ----------
|
|
820
|
+
// The engine owns the queue (each turn's result.research is its snapshot);
|
|
821
|
+
// this page only decides WHEN the next step is asked, through the shared
|
|
822
|
+
// viz-ticker verbs, paced no faster than the adapter's own polite interval.
|
|
823
|
+
const researchTopicEl = el("researchTopic");
|
|
824
|
+
const researchGoBtn = el("researchGo");
|
|
825
|
+
const researchPlayBtn = el("researchPlay");
|
|
826
|
+
const researchQueueStatusEl = el("researchQueueStatus");
|
|
827
|
+
const RESEARCH_TICK_MS = 2400;
|
|
828
|
+
let researchQueue = null; // the engine's latest snapshot, null when no run stands
|
|
829
|
+
|
|
830
|
+
const researchTicker = createTicker({
|
|
831
|
+
onTick: async () => { await submitLine("research next"); },
|
|
832
|
+
hasNext: () => Boolean(researchQueue && !researchQueue.complete),
|
|
833
|
+
onRender: renderResearchControls,
|
|
834
|
+
waitMs: RESEARCH_TICK_MS,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
function renderResearchControls(tickState) {
|
|
838
|
+
const state = tickState || researchTicker.getState();
|
|
839
|
+
researchPlayBtn.hidden = !(researchQueue && !researchQueue.complete);
|
|
840
|
+
researchPlayBtn.textContent = state.playing ? "pause" : "play";
|
|
841
|
+
researchPlayBtn.setAttribute("aria-pressed", String(state.playing));
|
|
842
|
+
if (!researchQueue) {
|
|
843
|
+
researchQueueStatusEl.textContent = "";
|
|
844
|
+
} else if (researchQueue.complete) {
|
|
845
|
+
researchQueueStatusEl.textContent = 'research "' + researchQueue.topic + '" complete \\u2014 '
|
|
846
|
+
+ researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s") + " grounded";
|
|
847
|
+
} else {
|
|
848
|
+
researchQueueStatusEl.textContent = 'research "' + researchQueue.topic + '": '
|
|
849
|
+
+ researchQueue.done.length + " done \\u00b7 " + researchQueue.pending.length + " queued";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Fold one settled turn's research field into the controls. A snapshot
|
|
854
|
+
* (re)arms them; null (a run that ended) clears them; undefined (not a
|
|
855
|
+
* research turn) leaves them alone. A FRESH run with topics queued starts
|
|
856
|
+
* auto-play, unless the visitor asked for reduced motion — the play
|
|
857
|
+
* button is the same control either way. */
|
|
858
|
+
function noteResearchResult(result) {
|
|
859
|
+
if (result.research === undefined) return;
|
|
860
|
+
const previous = researchQueue;
|
|
861
|
+
researchQueue = result.research;
|
|
862
|
+
const freshRun = Boolean(researchQueue && !researchQueue.complete && (!previous || previous.complete || previous.topic !== researchQueue.topic));
|
|
863
|
+
renderResearchControls();
|
|
864
|
+
if (freshRun && !prefersReducedMotion() && !researchTicker.getState().playing) researchTicker.play();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
researchGoBtn.addEventListener("click", () => {
|
|
868
|
+
const topic = researchTopicEl.value.trim();
|
|
869
|
+
if (!topic || busy || !window.tmctChatSession) return;
|
|
870
|
+
researchTopicEl.value = "";
|
|
871
|
+
submitLine("research " + topic);
|
|
872
|
+
});
|
|
873
|
+
researchTopicEl.addEventListener("keydown", (e) => {
|
|
874
|
+
if (e.key === "Enter") { e.preventDefault(); researchGoBtn.click(); }
|
|
875
|
+
});
|
|
876
|
+
researchPlayBtn.addEventListener("click", () => {
|
|
877
|
+
// pause() directly, not play()'s own toggle: play() declines while a
|
|
878
|
+
// step is mid-animation, and a pause pressed exactly then must not be
|
|
879
|
+
// dropped — the in-flight step still settles, then the loop stops.
|
|
880
|
+
if (researchTicker.getState().playing) researchTicker.pause();
|
|
881
|
+
else researchTicker.play();
|
|
810
882
|
});
|
|
811
883
|
|
|
812
884
|
// ---- export + print: whole-conversation controls ------------------------
|
|
@@ -921,25 +993,29 @@ ${THEME_TOKENS_CSS}
|
|
|
921
993
|
persist = window.tmctChat.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts });
|
|
922
994
|
}
|
|
923
995
|
const savedRecord = persist ? await persist.load() : null;
|
|
924
|
-
|
|
996
|
+
const initialMode = readWikiMode();
|
|
997
|
+
setWikiModeRadios(initialMode);
|
|
998
|
+
writeWikiMode(initialMode); // settles a legacy-key migration under the new key immediately, not only on the next radio change
|
|
999
|
+
synthSliderEl.value = String(readSynthBudget());
|
|
1000
|
+
synthValueEl.textContent = synthSliderEl.value;
|
|
925
1001
|
if (savedRecord && savedRecord.payload) {
|
|
926
1002
|
window.tmctChatSession = window.tmctChat.createChatSession({
|
|
927
1003
|
seedPayload: savedRecord.payload,
|
|
928
1004
|
vocabSeeded: true,
|
|
929
|
-
liveReference:
|
|
1005
|
+
liveReference: liveReferenceForMode(initialMode),
|
|
1006
|
+
synthesisBudget: readSynthBudget(),
|
|
930
1007
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
931
1008
|
});
|
|
932
1009
|
} else {
|
|
933
1010
|
window.tmctChatSession = newSession();
|
|
934
1011
|
}
|
|
935
|
-
if (window.tmctChatSession.setLiveReference) window.tmctChatSession.setLiveReference(liveToggleEl.checked);
|
|
936
1012
|
const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
|
|
937
1013
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
938
1014
|
const restoredNote = savedRecord
|
|
939
1015
|
? " Restored " + restoredCount + " taught fact" + (restoredCount === 1 ? "" : "s")
|
|
940
1016
|
+ " from your last visit \\u2014 state kept best-effort on this device."
|
|
941
1017
|
: "";
|
|
942
|
-
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats)
|
|
1018
|
+
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats, bandLabelFor)
|
|
943
1019
|
+ "." + restoredNote + " Ask it something, or teach it a fact of your own.");
|
|
944
1020
|
await renderStatsPanel(stats);
|
|
945
1021
|
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmctChat.vocabExampleHint(false);
|