@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
|
@@ -30,6 +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";
|
|
34
|
+
import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
|
|
35
|
+
import { bandLabelFor, statsSummaryLine, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
|
|
33
36
|
|
|
34
37
|
const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
|
|
35
38
|
|
|
@@ -115,25 +118,37 @@ export function loadProgressLine(parts) {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
118
|
-
* The exported transcript as
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* The exported transcript as ONE Markdown document, in the SAME shape the
|
|
122
|
+
* Node CLI/TUI's own .tmct/session-<id>.md writes (session-log-format.mjs,
|
|
123
|
+
* spliced in beside this function below): a title naming the version and a
|
|
124
|
+
* short session id, one heading per turn at millisecond time-of-day
|
|
125
|
+
* precision, the question as a verbatim blockquote, the answer in a fenced
|
|
126
|
+
* block. No closing session-end line — unlike a CLI session's close(), an
|
|
127
|
+
* export can happen mid-conversation, before anything has actually ended.
|
|
124
128
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
129
|
+
* Reads the page's transcript MODEL (an array alternating { role: "you" |
|
|
130
|
+
* "tmct", text, chipTier, ts }, one entry per submit and per settled
|
|
131
|
+
* reply), never the DOM — the message column may virtualize long chats
|
|
132
|
+
* someday, and an export must still carry every turn.
|
|
133
|
+
*
|
|
134
|
+
* `headerMd`/`turnMd` are the injected session-log-format.mjs builders
|
|
135
|
+
* (spliced in as their own consts alongside this function) — injected
|
|
136
|
+
* rather than imported so this function stays `.toString()`-splice safe,
|
|
137
|
+
* the same discipline provenanceChipFor's injected `bucketFor` holds.
|
|
127
138
|
*/
|
|
128
|
-
export function transcriptMarkdown(turns, meta) {
|
|
139
|
+
export function transcriptMarkdown(turns, meta, headerMd, turnMd) {
|
|
129
140
|
const version = (meta && meta.version) || "dev";
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
const sessionId = (meta && meta.sessionId) || "";
|
|
142
|
+
const list = turns || [];
|
|
143
|
+
let doc = headerMd({ version: version, sessionId: sessionId, startedAt: list.length ? list[0].ts : Date.now() });
|
|
144
|
+
let turnNumber = 0;
|
|
145
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
146
|
+
if (list[i].role !== "you") continue;
|
|
147
|
+
turnNumber += 1;
|
|
148
|
+
const reply = list[i + 1] && list[i + 1].role === "tmct" ? list[i + 1] : null;
|
|
149
|
+
doc += turnMd({ startedAt: list[i].ts, turnNumber: turnNumber, query: list[i].text, answer: reply ? reply.text : "" });
|
|
135
150
|
}
|
|
136
|
-
return
|
|
151
|
+
return doc;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
/** The self-contained "talk to it" full-screen page. Pure — the same output
|
|
@@ -221,25 +236,37 @@ ${THEME_TOKENS_CSS}
|
|
|
221
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; }
|
|
222
237
|
.composer-inner button[type="submit"]:disabled { opacity: .4; cursor: default; }
|
|
223
238
|
|
|
224
|
-
/* the
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.composer-
|
|
230
|
-
.
|
|
231
|
-
.
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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; }
|
|
236
263
|
|
|
237
264
|
.statusline { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .6rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
238
265
|
|
|
239
|
-
/* the composer's small utility row — right-aligned mono controls
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.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; }
|
|
243
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); }
|
|
244
271
|
.tool-btn:hover { color: var(--ink); }
|
|
245
272
|
|
|
@@ -303,18 +330,33 @@ ${THEME_TOKENS_CSS}
|
|
|
303
330
|
placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
|
|
304
331
|
<button type="submit" id="composerSend" aria-label="Send" disabled>→</button>
|
|
305
332
|
</div>
|
|
306
|
-
<div class="composer-
|
|
307
|
-
<
|
|
308
|
-
<
|
|
309
|
-
<
|
|
310
|
-
<
|
|
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">
|
|
311
343
|
</label>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
</
|
|
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>
|
|
318
360
|
</div>
|
|
319
361
|
</form>
|
|
320
362
|
<div class="statusline" id="status">loading the engine…</div>
|
|
@@ -329,7 +371,16 @@ ${THEME_TOKENS_CSS}
|
|
|
329
371
|
const provBucketFor = ${provBucketFor.toString()};
|
|
330
372
|
const provenanceChipFor = ${provenanceChipFor.toString()};
|
|
331
373
|
const loadProgressLine = ${loadProgressLine.toString()};
|
|
374
|
+
const sessionLogTimeOfDay = ${sessionLogTimeOfDay.toString()};
|
|
375
|
+
const sessionLogHeaderMarkdown = ${sessionLogHeaderMarkdown.toString()};
|
|
376
|
+
const sessionLogTurnMarkdown = ${sessionLogTurnMarkdown.toString()};
|
|
332
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()};
|
|
333
384
|
const el = (id) => document.getElementById(id);
|
|
334
385
|
|
|
335
386
|
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
@@ -340,20 +391,54 @@ ${THEME_TOKENS_CSS}
|
|
|
340
391
|
const sendBtn = el("composerSend");
|
|
341
392
|
const statusEl = el("status");
|
|
342
393
|
const statsPanelEl = el("statsPanel");
|
|
343
|
-
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");
|
|
344
398
|
|
|
345
|
-
// The
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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 */ }
|
|
351
420
|
}
|
|
352
|
-
|
|
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() {
|
|
353
434
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} 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 */ }
|
|
357
442
|
}
|
|
358
443
|
|
|
359
444
|
function scrollToEnd() {
|
|
@@ -445,31 +530,6 @@ ${THEME_TOKENS_CSS}
|
|
|
445
530
|
if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
446
531
|
}
|
|
447
532
|
|
|
448
|
-
// Fetch the url reading the body as a stream, reporting (loadedBytes,
|
|
449
|
-
// totalBytes) after every chunk — total is 0 when the response carries no
|
|
450
|
-
// Content-Length. Resolves to a Blob of the whole body.
|
|
451
|
-
async function fetchWithProgress(url, onProgress) {
|
|
452
|
-
const res = await fetch(url);
|
|
453
|
-
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
454
|
-
const total = Number(res.headers.get("content-length")) || 0;
|
|
455
|
-
if (!res.body || !res.body.getReader) {
|
|
456
|
-
const blob = await res.blob();
|
|
457
|
-
onProgress(blob.size, total || blob.size);
|
|
458
|
-
return blob;
|
|
459
|
-
}
|
|
460
|
-
const reader = res.body.getReader();
|
|
461
|
-
const chunks = [];
|
|
462
|
-
let loaded = 0;
|
|
463
|
-
for (;;) {
|
|
464
|
-
const step = await reader.read();
|
|
465
|
-
if (step.done) break;
|
|
466
|
-
chunks.push(step.value);
|
|
467
|
-
loaded += step.value.byteLength;
|
|
468
|
-
onProgress(loaded, total);
|
|
469
|
-
}
|
|
470
|
-
return new Blob(chunks);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
533
|
let winkStatus = "pending";
|
|
474
534
|
async function tryLoadWink() {
|
|
475
535
|
// The bounded race guards the same failure the CDN era did — a load that
|
|
@@ -551,7 +611,8 @@ ${THEME_TOKENS_CSS}
|
|
|
551
611
|
return window.tmctChat.createChatSession({
|
|
552
612
|
seedPayload: cloneSeed(),
|
|
553
613
|
vocabSeeded: Boolean(seedPayload),
|
|
554
|
-
liveReference:
|
|
614
|
+
liveReference: liveReferenceForMode(checkedWikiMode()),
|
|
615
|
+
synthesisBudget: readSynthBudget(),
|
|
555
616
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
556
617
|
});
|
|
557
618
|
}
|
|
@@ -618,7 +679,7 @@ ${THEME_TOKENS_CSS}
|
|
|
618
679
|
restoredCount = 0;
|
|
619
680
|
window.tmctChatSession = newSession();
|
|
620
681
|
const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
|
|
621
|
-
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) + ").");
|
|
622
683
|
await renderStatsPanel(stats);
|
|
623
684
|
}
|
|
624
685
|
|
|
@@ -626,48 +687,8 @@ ${THEME_TOKENS_CSS}
|
|
|
626
687
|
// Both read window.tmctChat.memoryStats(memoryDir) (chat-browser-entry.mjs)
|
|
627
688
|
// — one computation, reused, so the boot line and the panel can never
|
|
628
689
|
// disagree with each other about what this session's memory holds.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
"human-medium": "human persona (medium)",
|
|
632
|
-
"human-large": "human persona (large)",
|
|
633
|
-
seon: "seon ontology",
|
|
634
|
-
conceptnet: "ConceptNet",
|
|
635
|
-
"tier2-aws": "AWS",
|
|
636
|
-
"tier2-python": "Python",
|
|
637
|
-
"tier2-java": "Java",
|
|
638
|
-
"wordnet-xl": "WordNet",
|
|
639
|
-
};
|
|
640
|
-
const BAND_ORDER = [
|
|
641
|
-
"human", "human-medium", "human-large", "seon", "conceptnet",
|
|
642
|
-
"tier2-aws", "tier2-python", "tier2-java", "wordnet-xl",
|
|
643
|
-
"taught this session", "other",
|
|
644
|
-
];
|
|
645
|
-
const bandLabel = (key) => BAND_LABELS[key] || key;
|
|
646
|
-
|
|
647
|
-
/** The boot system line's own memory summary — every seed band this
|
|
648
|
-
* session actually loaded, named with its real count, left-to-right in
|
|
649
|
-
* BAND_ORDER; a session with nothing seeded says so plainly instead of
|
|
650
|
-
* naming zero facts. */
|
|
651
|
-
function statsSummaryLine(stats) {
|
|
652
|
-
if (!stats || !stats.total) return "no starter memory; starting empty";
|
|
653
|
-
const parts = BAND_ORDER.filter((k) => stats.bandCounts[k]).map((k) => stats.bandCounts[k] + " " + bandLabel(k));
|
|
654
|
-
return parts.length
|
|
655
|
-
? "starter memory: " + parts.join(" + ") + " (" + stats.total + " facts total)"
|
|
656
|
-
: stats.total + " starter facts loaded";
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function bandRow(label, count) {
|
|
660
|
-
const row = document.createElement("p");
|
|
661
|
-
row.className = "band-row";
|
|
662
|
-
const l = document.createElement("span");
|
|
663
|
-
l.textContent = label;
|
|
664
|
-
const c = document.createElement("span");
|
|
665
|
-
c.className = "band-count";
|
|
666
|
-
c.textContent = String(count);
|
|
667
|
-
row.appendChild(l);
|
|
668
|
-
row.appendChild(c);
|
|
669
|
-
return row;
|
|
670
|
-
}
|
|
690
|
+
// bandLabelFor/statsSummaryLine/renderStatsPanelInto are the shared
|
|
691
|
+
// memory-panel-viz.mjs helpers, spliced in above.
|
|
671
692
|
|
|
672
693
|
/** (Re)render the docked panel from a memoryStats() result — stats may be
|
|
673
694
|
* passed in already-computed (boot reuses its own call rather than asking
|
|
@@ -680,46 +701,18 @@ ${THEME_TOKENS_CSS}
|
|
|
680
701
|
try { stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir); }
|
|
681
702
|
catch { return; }
|
|
682
703
|
}
|
|
683
|
-
statsPanelEl
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "taught this session" }));
|
|
691
|
-
if (!stats.taught.length) {
|
|
692
|
-
const empty = document.createElement("p");
|
|
693
|
-
empty.className = "empty";
|
|
694
|
-
empty.textContent = 'nothing yet \\u2014 teach it something ("a dog is a kind of animal") and it lands here, with its source.';
|
|
695
|
-
statsPanelEl.appendChild(empty);
|
|
696
|
-
} else {
|
|
697
|
-
for (const fact of stats.taught.slice(-8).reverse()) {
|
|
698
|
-
const item = document.createElement("p");
|
|
699
|
-
item.className = "taught-item";
|
|
700
|
-
item.appendChild(document.createTextNode(fact.subject + " " + fact.predicate + " " + fact.object));
|
|
701
|
-
const tag = document.createElement("span");
|
|
702
|
-
tag.className = "taught-tag";
|
|
703
|
-
tag.textContent = fact.tag;
|
|
704
|
-
item.appendChild(tag);
|
|
705
|
-
statsPanelEl.appendChild(item);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
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
|
+
}
|
|
708
710
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
forget.textContent = "forget everything";
|
|
715
|
-
forget.title = "clear what this device has saved and restart from the fresh seed";
|
|
716
|
-
forget.addEventListener("click", forgetEverything);
|
|
717
|
-
statsPanelEl.appendChild(forget);
|
|
718
|
-
const note = document.createElement("p");
|
|
719
|
-
note.className = "persist-note";
|
|
720
|
-
note.textContent = "taught facts are kept best-effort on this device (IndexedDB), never sent anywhere.";
|
|
721
|
-
statsPanelEl.appendChild(note);
|
|
722
|
-
}
|
|
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";
|
|
723
716
|
}
|
|
724
717
|
|
|
725
718
|
function renderStatus() {
|
|
@@ -731,18 +724,42 @@ ${THEME_TOKENS_CSS}
|
|
|
731
724
|
: winkStatus === "unavailable"
|
|
732
725
|
? "wink-nlp unavailable — curated + fuzzy tiers only (still zero guesses, zero LLM)"
|
|
733
726
|
: "wink-nlp: loading\\u2026";
|
|
734
|
-
const
|
|
727
|
+
const liveReference = window.tmctChatSession ? window.tmctChatSession.liveReference : liveReferenceForMode(checkedWikiMode());
|
|
728
|
+
const livePart = "live wikipedia: " + liveStatusWord(liveReference);
|
|
735
729
|
statusEl.textContent = seedPart + " \\u00b7 " + winkPart + " \\u00b7 " + livePart;
|
|
736
730
|
}
|
|
737
731
|
|
|
738
|
-
|
|
739
|
-
|
|
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);
|
|
740
748
|
if (window.tmctChatSession && window.tmctChatSession.setLiveReference) {
|
|
741
|
-
window.tmctChatSession.setLiveReference(
|
|
749
|
+
window.tmctChatSession.setLiveReference(liveReferenceForMode(mode));
|
|
742
750
|
}
|
|
743
751
|
renderStatus();
|
|
744
752
|
});
|
|
745
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
|
+
|
|
746
763
|
let busy = true;
|
|
747
764
|
function setBusy(v) {
|
|
748
765
|
busy = v;
|
|
@@ -751,38 +768,117 @@ ${THEME_TOKENS_CSS}
|
|
|
751
768
|
sendBtn.disabled = v || !ready;
|
|
752
769
|
}
|
|
753
770
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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;
|
|
759
778
|
addUserBubble(q);
|
|
760
779
|
transcript.push({ role: "you", text: q, chipTier: null, ts: Date.now() });
|
|
761
780
|
const pendingRow = addPendingAssistantBubble();
|
|
762
781
|
setBusy(true);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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,
|
|
770
796
|
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
771
|
-
{ miss: true })
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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();
|
|
786
882
|
});
|
|
787
883
|
|
|
788
884
|
// ---- export + print: whole-conversation controls ------------------------
|
|
@@ -791,7 +887,8 @@ ${THEME_TOKENS_CSS}
|
|
|
791
887
|
// @media print stylesheet above to un-pin the message column so every
|
|
792
888
|
// turn reaches paper.
|
|
793
889
|
el("exportMd").addEventListener("click", () => {
|
|
794
|
-
const
|
|
890
|
+
const sessionId = (window.tmctChatSession && window.tmctChatSession.sessionId) || "";
|
|
891
|
+
const md = transcriptMarkdown(transcript, { version: siteVersion, sessionId: sessionId }, sessionLogHeaderMarkdown, sessionLogTurnMarkdown);
|
|
795
892
|
const blob = new Blob([md], { type: "text/markdown" });
|
|
796
893
|
const url = URL.createObjectURL(blob);
|
|
797
894
|
const link = document.createElement("a");
|
|
@@ -829,6 +926,49 @@ ${THEME_TOKENS_CSS}
|
|
|
829
926
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
830
927
|
});
|
|
831
928
|
|
|
929
|
+
// "ingest file" feeds a whole .txt/.md through the SAME session, one
|
|
930
|
+
// sentence at a time (window.tmctChat.splitSentences, then session.turn),
|
|
931
|
+
// teaching every sentence the recognizer grounds and skipping the rest
|
|
932
|
+
// honestly — the same pipeline the ingest page runs, reaching the chat's own
|
|
933
|
+
// memory so the taught facts answer questions straight away.
|
|
934
|
+
el("ingestFile").addEventListener("click", () => el("ingestInput").click());
|
|
935
|
+
el("ingestInput").addEventListener("change", async (e) => {
|
|
936
|
+
const file = e.target.files && e.target.files[0];
|
|
937
|
+
e.target.value = "";
|
|
938
|
+
const session = window.tmctChatSession;
|
|
939
|
+
if (!file || busy || !session || !window.tmctChat.splitSentences) return;
|
|
940
|
+
let text;
|
|
941
|
+
try {
|
|
942
|
+
text = await file.text();
|
|
943
|
+
} catch (err) {
|
|
944
|
+
addSystemLine("couldn't read that file (" + (err && err.message ? err.message : err) + ").");
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const sentences = window.tmctChat.splitSentences(text);
|
|
948
|
+
if (!sentences.length) { addSystemLine("nothing to ingest in " + file.name + "."); return; }
|
|
949
|
+
setBusy(true);
|
|
950
|
+
statusEl.textContent = "ingesting " + file.name + "\\u2026";
|
|
951
|
+
let grounded = 0;
|
|
952
|
+
try {
|
|
953
|
+
for (const sentence of sentences) {
|
|
954
|
+
const result = await session.turn(sentence);
|
|
955
|
+
if (result.record && result.record.via === "assert" && !result.record.miss) grounded += 1;
|
|
956
|
+
}
|
|
957
|
+
} catch (err) {
|
|
958
|
+
addSystemLine("something went wrong ingesting " + file.name + " (" + (err && err.message ? err.message : err) + ").");
|
|
959
|
+
}
|
|
960
|
+
if (grounded) scheduleSave();
|
|
961
|
+
const skipped = sentences.length - grounded;
|
|
962
|
+
addSystemLine("ingested " + file.name + " \\u2014 " + sentences.length + " sentence"
|
|
963
|
+
+ (sentences.length === 1 ? "" : "s") + " read, " + grounded + " fact"
|
|
964
|
+
+ (grounded === 1 ? "" : "s") + " added"
|
|
965
|
+
+ (skipped ? ", " + skipped + " skipped (not a recognized fact shape)" : "") + ".");
|
|
966
|
+
await renderStatsPanel();
|
|
967
|
+
renderStatus();
|
|
968
|
+
setBusy(false);
|
|
969
|
+
inputEl.focus();
|
|
970
|
+
});
|
|
971
|
+
|
|
832
972
|
// "reset to seed" is the full re-initialisation: drop the persisted payload
|
|
833
973
|
// outright and reload, so boot re-seeds from the page's shipped seed as if on
|
|
834
974
|
// a first visit. Harder than "forget everything", which only swaps the live
|
|
@@ -853,25 +993,29 @@ ${THEME_TOKENS_CSS}
|
|
|
853
993
|
persist = window.tmctChat.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts });
|
|
854
994
|
}
|
|
855
995
|
const savedRecord = persist ? await persist.load() : null;
|
|
856
|
-
|
|
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;
|
|
857
1001
|
if (savedRecord && savedRecord.payload) {
|
|
858
1002
|
window.tmctChatSession = window.tmctChat.createChatSession({
|
|
859
1003
|
seedPayload: savedRecord.payload,
|
|
860
1004
|
vocabSeeded: true,
|
|
861
|
-
liveReference:
|
|
1005
|
+
liveReference: liveReferenceForMode(initialMode),
|
|
1006
|
+
synthesisBudget: readSynthBudget(),
|
|
862
1007
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
863
1008
|
});
|
|
864
1009
|
} else {
|
|
865
1010
|
window.tmctChatSession = newSession();
|
|
866
1011
|
}
|
|
867
|
-
if (window.tmctChatSession.setLiveReference) window.tmctChatSession.setLiveReference(liveToggleEl.checked);
|
|
868
1012
|
const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
|
|
869
1013
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
870
1014
|
const restoredNote = savedRecord
|
|
871
1015
|
? " Restored " + restoredCount + " taught fact" + (restoredCount === 1 ? "" : "s")
|
|
872
1016
|
+ " from your last visit \\u2014 state kept best-effort on this device."
|
|
873
1017
|
: "";
|
|
874
|
-
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)
|
|
875
1019
|
+ "." + restoredNote + " Ask it something, or teach it a fact of your own.");
|
|
876
1020
|
await renderStatsPanel(stats);
|
|
877
1021
|
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmctChat.vocabExampleHint(false);
|