@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.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 +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
|
@@ -19,12 +19,34 @@
|
|
|
19
19
|
// renderResearchHtml() is pure: no I/O, deterministic output for identical
|
|
20
20
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result to
|
|
21
21
|
// public/research.html, after research-browser.bundle.js already exists.
|
|
22
|
-
import { THEME_TOKENS_CSS,
|
|
23
|
-
import { fetchWithProgress } from "./memory-panel-viz.mjs";
|
|
22
|
+
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
23
|
+
import { fetchWithProgress, loadProgressLine, factTripleParts } from "./memory-panel-viz.mjs";
|
|
24
24
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
25
|
+
import { loadWinkVendor } from "./viz-boot.mjs";
|
|
26
|
+
import { cloneMemoryPayload } from "../adapters/memory/core.mjs";
|
|
25
27
|
|
|
26
28
|
const DEFAULT_TITLE = "the-mechanical-code-talker — research";
|
|
27
29
|
|
|
30
|
+
// research.html reads as ledger.html's dashboard sibling (dense stat/control
|
|
31
|
+
// panels, monospace metrics), so it runs the same system sans + forced dark
|
|
32
|
+
// chrome — see ledger-viz.mjs's own DASH_SANS_STACK/DASH_DARK_CHROME_CSS for
|
|
33
|
+
// the fuller rationale. Kept as a separate copy rather than a shared import:
|
|
34
|
+
// each generated-page builder is a standalone, self-contained module (no
|
|
35
|
+
// cross-page runtime dependency), matching every sibling page builder here.
|
|
36
|
+
const DASH_SANS_STACK = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
|
|
37
|
+
const DASH_DARK_CHROME_CSS = `
|
|
38
|
+
:root { color-scheme: dark; }
|
|
39
|
+
body {
|
|
40
|
+
--bg: #15181C; --ink: #E7E5DF; --muted: #9A9E95; --line: #2B3036; --card: #1C2126;
|
|
41
|
+
--taught: #5FBE8B; --corpus: #6C93BF; --entail: #D9A554; --alert: #D08070;
|
|
42
|
+
--taught-t1: rgba(95,190,139,.35); --taught-t2: rgba(95,190,139,.65); --taught-t3: rgba(95,190,139,1);
|
|
43
|
+
--corpus-t1: rgba(108,147,191,.35); --corpus-t2: rgba(108,147,191,.65); --corpus-t3: rgba(108,147,191,1);
|
|
44
|
+
--entail-t1: rgba(217,165,84,.35); --entail-t2: rgba(217,165,84,.65); --entail-t3: rgba(217,165,84,1);
|
|
45
|
+
--taught-soft: rgba(95,190,139,.14); --corpus-soft: rgba(108,147,191,.14);
|
|
46
|
+
--entail-soft: rgba(217,165,84,.16); --alert-soft: rgba(208,128,112,.16);
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
28
50
|
/** The human label + short colour key for one source snapshot entry
|
|
29
51
|
* ({ key, band }). A seed band folds to a readable corpus name; the three
|
|
30
52
|
* growth sources get their own words. Pure and `.toString()`-splice safe. */
|
|
@@ -43,35 +65,6 @@ export function sourceLabelFor(source) {
|
|
|
43
65
|
return { label: (BANDS[band] || band || "seed") + " (seed corpus)", tone: "seed" };
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
/** One learned fact as its three canonical cells plus a source tone, for the
|
|
47
|
-
* highlights and history lists. Pure, `.toString()`-splice safe. */
|
|
48
|
-
export function factTripleParts(fact) {
|
|
49
|
-
return {
|
|
50
|
-
subject: String((fact && fact.subject) || ""),
|
|
51
|
-
predicate: String((fact && fact.predicate) || ""),
|
|
52
|
-
object: String((fact && fact.object) || ""),
|
|
53
|
-
source: String((fact && fact.source) || ""),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The boot statusline while the big assets stream in — the same aggregator
|
|
58
|
-
* ingest-viz.mjs's own loadProgressLine is. `parts` is an array of
|
|
59
|
-
* { loaded, total } byte counts. Self-contained, `.toString()`-splice safe. */
|
|
60
|
-
export function loadProgressLine(parts) {
|
|
61
|
-
const mb = (n) => (n / 1048576).toFixed(1);
|
|
62
|
-
let loaded = 0;
|
|
63
|
-
let total = 0;
|
|
64
|
-
let totalKnown = true;
|
|
65
|
-
for (const p of parts || []) {
|
|
66
|
-
loaded += (p && p.loaded) || 0;
|
|
67
|
-
if (p && p.total > 0) total += p.total;
|
|
68
|
-
else totalKnown = false;
|
|
69
|
-
}
|
|
70
|
-
return totalKnown && total > 0
|
|
71
|
-
? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
|
|
72
|
-
: "loading the engine… " + mb(loaded) + " MB";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
68
|
/** The self-contained research page. Pure — the same output for the same
|
|
76
69
|
* input every time; every piece of state is computed live in the browser once
|
|
77
70
|
* the sibling research bundle loads. `digestStructures` are the pre-parsed
|
|
@@ -79,7 +72,7 @@ export function loadProgressLine(parts) {
|
|
|
79
72
|
* page can digest a term client-side over its grown store (no TOML parser in
|
|
80
73
|
* the browser); an empty list leaves the digest panel degrading to an honest
|
|
81
74
|
* "no digest available" the same way the node stub does. */
|
|
82
|
-
export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
|
|
75
|
+
export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "" } = {}) {
|
|
83
76
|
const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
|
|
84
77
|
return `<!doctype html>
|
|
85
78
|
<html lang="en">
|
|
@@ -99,34 +92,52 @@ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [
|
|
|
99
92
|
-->
|
|
100
93
|
<style>
|
|
101
94
|
${THEME_TOKENS_CSS}
|
|
95
|
+
${DASH_DARK_CHROME_CSS}
|
|
102
96
|
html, body { min-height: 100%; }
|
|
103
|
-
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${
|
|
97
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
|
|
104
98
|
.mono { font-family: ${MONO_STACK}; }
|
|
105
99
|
button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
|
|
106
|
-
button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--
|
|
100
|
+
button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
|
|
107
101
|
a { color: var(--corpus); }
|
|
108
102
|
|
|
109
|
-
.wrap { max-width:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
.
|
|
103
|
+
.wrap { max-width: 1200px; margin: 0 auto; padding: 1.1rem 1.1rem 3rem; }
|
|
104
|
+
|
|
105
|
+
/* A brand block on the left and a small stat-readout panel on the right —
|
|
106
|
+
the readout fills the row instead of one status line stranded beside a
|
|
107
|
+
wide gap, and it doubles as the boot-progress display. */
|
|
108
|
+
header.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.4rem; flex-wrap: wrap; padding: .6rem 0 1rem; border-bottom: 1px solid var(--line); margin-bottom: 1.3rem; }
|
|
109
|
+
.brand { display: flex; flex-direction: column; gap: .3rem; max-width: 640px; }
|
|
110
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
111
|
+
.subtitle { font-size: .92rem; color: var(--ink); opacity: .82; max-width: 58ch; }
|
|
112
|
+
.statuspanel { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .9rem; }
|
|
113
|
+
.statuspanel .stat { display: flex; flex-direction: column; gap: .14rem; min-width: 7rem; }
|
|
114
|
+
.statuspanel .stat-label { font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
115
|
+
.statuspanel .stat-value { font-family: ${MONO_STACK}; font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--ink); }
|
|
116
|
+
/* the live total is what this page is actually growing, so it carries the
|
|
117
|
+
panel's emphasis — the seed count beside it never moves after boot. */
|
|
118
|
+
.statuspanel .stat-facts { padding-right: 1.1rem; border-right: 1px solid var(--line); }
|
|
119
|
+
.statuspanel .stat-facts .stat-value { font-size: 1.15rem; font-weight: 600; }
|
|
120
|
+
.enginenote { margin: -.5rem 0 1.2rem; padding: .55rem .8rem; border: 1px solid var(--alert); border-radius: 6px; background: var(--alert-soft); color: var(--alert); font-family: ${MONO_STACK}; font-size: .78rem; }
|
|
121
|
+
.enginenote[hidden] { display: none; }
|
|
116
122
|
|
|
117
123
|
h2.band { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); margin: 1.6rem 0 .7rem; border-bottom: 1px solid var(--line); padding-bottom: .35rem; }
|
|
118
124
|
|
|
119
|
-
/* the three ways to grow the graph
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.grow
|
|
125
|
+
/* the three ways to grow the graph, as one control-bar row of panels: a
|
|
126
|
+
hairline grid gap (painted by the shared background) separates them
|
|
127
|
+
instead of individual card borders, and each carries a top accent in
|
|
128
|
+
its own tone color. Stacks on a narrow screen. */
|
|
129
|
+
.grow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
130
|
+
.grow .card { background: var(--card); border-top: 2px solid var(--line); padding: .9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
|
|
131
|
+
.grow .card.card-research { border-top-color: var(--corpus); }
|
|
132
|
+
.grow .card.card-taught { border-top-color: var(--taught); }
|
|
133
|
+
.grow .card.card-ingest { border-top-color: var(--entail); }
|
|
134
|
+
.grow .card h3 { margin: 0; font-size: .95rem; font-weight: 600; }
|
|
124
135
|
.grow .card .hint { font-size: .76rem; color: var(--muted); margin: 0; }
|
|
125
136
|
.grow .card .tone { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .35rem; vertical-align: baseline; }
|
|
126
137
|
.tone-taught { background: var(--taught); } .tone-ingest { background: var(--entail); }
|
|
127
138
|
.tone-research { background: var(--corpus); } .tone-seed { background: var(--muted); }
|
|
128
139
|
|
|
129
|
-
.grow input[type="text"], .grow textarea { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .8rem; background: var(--
|
|
140
|
+
.grow input[type="text"], .grow textarea { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .8rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .4rem .55rem; }
|
|
130
141
|
.grow textarea { resize: vertical; min-height: 5.5rem; line-height: 1.5; }
|
|
131
142
|
.grow input::placeholder, .grow textarea::placeholder { color: var(--muted); }
|
|
132
143
|
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
|
@@ -138,35 +149,45 @@ ${THEME_TOKENS_CSS}
|
|
|
138
149
|
.optionToggle input { margin: 0; accent-color: var(--corpus); }
|
|
139
150
|
.knobs { display: flex; gap: .9rem; align-items: center; flex-wrap: wrap; }
|
|
140
151
|
.knob { display: inline-flex; align-items: center; gap: .4rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
141
|
-
.knob input[type="number"] { width: 3.4rem; font-family: ${MONO_STACK}; font-size: .74rem; background: var(--
|
|
152
|
+
.knob input[type="number"] { width: 3.4rem; font-family: ${MONO_STACK}; font-size: .74rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .25rem .35rem; text-align: right; }
|
|
142
153
|
|
|
143
154
|
/* highlights + ask, two columns */
|
|
144
155
|
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; align-items: start; }
|
|
145
156
|
|
|
146
|
-
.panel { border: 1px solid var(--line); border-radius:
|
|
147
|
-
.panel h3 { margin: 0 0 .6rem; font-size: .9rem; }
|
|
157
|
+
.panel { border: 1px solid var(--line); border-radius: 6px; padding: .9rem 1rem 1rem; background: var(--card); }
|
|
158
|
+
.panel h3 { margin: 0 0 .6rem; font-size: .9rem; font-weight: 600; }
|
|
148
159
|
.panel .empty { color: var(--muted); font-size: .8rem; margin: .3rem 0; }
|
|
149
160
|
|
|
150
|
-
|
|
161
|
+
/* recently-learned: a dense feed of the facts just grounded. */
|
|
162
|
+
.fact { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: .45rem; align-items: baseline; padding: .3rem .3rem; margin: 0 -.3rem; border-radius: 4px; border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .76rem; }
|
|
151
163
|
.fact:last-child { border-bottom: none; }
|
|
164
|
+
.fact:hover { background: var(--line); }
|
|
152
165
|
.fact .dot { width: 7px; height: 7px; border-radius: 50%; align-self: center; }
|
|
153
166
|
.fact .subj { color: var(--ink); word-break: break-word; }
|
|
154
167
|
.fact .pred { color: var(--corpus); white-space: nowrap; }
|
|
155
168
|
.fact .obj { color: var(--ink); word-break: break-word; }
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
/* best-connected terms: a top-N ranked list (a mini bar chart), styled as
|
|
171
|
+
ledger.html's own predicate.top panel so the two pages read as siblings.
|
|
172
|
+
Each row keeps the "chip tapchip" contract the page's click handling and
|
|
173
|
+
its own e2e coverage rely on; the ranked-bar look rides inside it. */
|
|
174
|
+
.chips { display: flex; flex-direction: column; gap: .2rem; }
|
|
158
175
|
.chip { font-family: ${MONO_STACK}; font-size: .74rem; border: 1px solid var(--line); border-radius: 99px; padding: .2rem .6rem; background: var(--bg); }
|
|
159
|
-
.chip.tapchip { cursor: pointer; }
|
|
160
|
-
.chip.tapchip:hover {
|
|
161
|
-
.chip .
|
|
176
|
+
.chip.tapchip { cursor: pointer; display: grid; grid-template-columns: 1.5rem minmax(0,1fr) 4.6rem 3.6rem; align-items: center; gap: .5rem; width: 100%; text-align: left; border: none; border-radius: 4px; padding: .3rem .4rem; background: none; }
|
|
177
|
+
.chip.tapchip:hover { background: var(--line); }
|
|
178
|
+
.chip .rank { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
179
|
+
.chip .term { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
180
|
+
.chip .track { background: var(--line); border-radius: 99px; height: 5px; overflow: hidden; }
|
|
181
|
+
.chip .fill { display: block; height: 100%; background: var(--ink); opacity: .6; border-radius: 99px; }
|
|
182
|
+
.chip .deg { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
|
|
162
183
|
|
|
163
184
|
/* the term digest: a narrative card, its sources, and the flat fact list one
|
|
164
185
|
click away behind "show the facts". */
|
|
165
186
|
.digestpanel .hint { font-size: .78rem; color: var(--muted); margin: 0 0 .55rem; }
|
|
166
187
|
.digestout { min-height: 1.4rem; }
|
|
167
188
|
.digestout .empty { color: var(--muted); font-size: .82rem; margin: .2rem 0; }
|
|
168
|
-
.digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius:
|
|
169
|
-
.dgcard { border: 1px solid var(--line); border-radius:
|
|
189
|
+
.digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 6px; padding: .55rem .7rem; }
|
|
190
|
+
.dgcard { border: 1px solid var(--line); border-radius: 6px; padding: .7rem .85rem .8rem; background: var(--bg); }
|
|
170
191
|
.dgterm { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
|
|
171
192
|
.dgcard p { margin: 0 0 .5rem; font-size: .95rem; }
|
|
172
193
|
.dgcard p:last-of-type { margin-bottom: 0; }
|
|
@@ -178,15 +199,19 @@ ${THEME_TOKENS_CSS}
|
|
|
178
199
|
|
|
179
200
|
/* ask, scoped by source */
|
|
180
201
|
.askRow { display: flex; gap: .5rem; margin: .2rem 0 .7rem; }
|
|
181
|
-
.askRow input { flex: 1; min-width: 0; font-family:
|
|
182
|
-
#answer { font-size: .9rem; white-space: pre-wrap; word-break: break-word; padding: .55rem .7rem; border-radius:
|
|
202
|
+
.askRow input { flex: 1; min-width: 0; font-family: inherit; font-size: .92rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .45rem .7rem; }
|
|
203
|
+
#answer { font-size: .9rem; white-space: pre-wrap; word-break: break-word; padding: .55rem .7rem; border-radius: 6px; background: var(--bg); border: 1px solid var(--line); min-height: 1.4rem; }
|
|
183
204
|
#answer.miss { color: var(--muted); border-style: dashed; }
|
|
184
205
|
.sourcesHead { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; margin: .2rem 0 .5rem; }
|
|
185
206
|
.sourcesHead .toggleAll { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .12rem .5rem; background: var(--bg); }
|
|
186
207
|
|
|
208
|
+
/* scope by source: a filterable table — a header row names the columns,
|
|
209
|
+
then one row per source with its own checkbox, count, and history. */
|
|
210
|
+
.sourceTableHead { display: flex; justify-content: space-between; padding: 0 .3rem .3rem; font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); border-bottom: 1px solid var(--line); margin-bottom: .2rem; }
|
|
187
211
|
details.source { border-bottom: 1px solid var(--line); }
|
|
188
212
|
details.source:last-of-type { border-bottom: none; }
|
|
189
|
-
details.source > summary { list-style: none; display: flex; align-items: center; gap: .5rem; padding: .35rem 0; cursor: pointer; font-family: ${MONO_STACK}; font-size: .78rem; }
|
|
213
|
+
details.source > summary { list-style: none; display: flex; align-items: center; gap: .5rem; padding: .35rem .3rem; margin: 0 -.3rem; border-radius: 4px; cursor: pointer; font-family: ${MONO_STACK}; font-size: .78rem; }
|
|
214
|
+
details.source > summary:hover { background: var(--line); }
|
|
190
215
|
details.source > summary::-webkit-details-marker { display: none; }
|
|
191
216
|
details.source > summary .srcLabel { flex: 1; display: inline-flex; align-items: center; gap: .4rem; }
|
|
192
217
|
details.source > summary input { margin: 0; accent-color: var(--corpus); }
|
|
@@ -201,23 +226,34 @@ ${THEME_TOKENS_CSS}
|
|
|
201
226
|
@media (max-width: 820px) {
|
|
202
227
|
.grow { grid-template-columns: 1fr; }
|
|
203
228
|
.cols { grid-template-columns: 1fr; }
|
|
229
|
+
.chip.tapchip { grid-template-columns: 1.5rem minmax(0,1fr) 3.2rem; }
|
|
230
|
+
.chip.tapchip .track { display: none; }
|
|
231
|
+
.statuspanel .stat { min-width: 0; }
|
|
204
232
|
}
|
|
205
233
|
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
|
|
234
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
235
|
+
.fact, .chip.tapchip, details.source > summary { transition: background-color .1s ease; }
|
|
236
|
+
}
|
|
206
237
|
</style>
|
|
207
238
|
</head>
|
|
208
239
|
<body>
|
|
209
240
|
<div class="wrap">
|
|
210
241
|
<header class="topbar">
|
|
211
242
|
<div class="brand">
|
|
212
|
-
<span class="eyebrow">the-mechanical-code-talker</span>
|
|
213
|
-
<span class="subtitle">
|
|
243
|
+
<span class="eyebrow">the-mechanical-code-talker · research</span>
|
|
244
|
+
<span class="subtitle">Grow one graph three ways. Watch what it learns, then ask a question scoped to the sources you trust.</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="statuspanel" id="statusPanel" aria-live="polite">
|
|
247
|
+
<div class="stat stat-facts"><span class="stat-label">facts in the graph</span><span class="stat-value" id="statFacts">—</span></div>
|
|
248
|
+
<div class="stat"><span class="stat-label">seed</span><span class="stat-value" id="statSeed">—</span></div>
|
|
249
|
+
<div class="stat"><span class="stat-label">engine</span><span class="stat-value" id="statEngine">booting…</span></div>
|
|
214
250
|
</div>
|
|
215
|
-
<span class="status" id="status">loading the engine…</span>
|
|
216
251
|
</header>
|
|
252
|
+
<div class="enginenote" id="engineNote" hidden></div>
|
|
217
253
|
|
|
218
254
|
<h2 class="band">grow the graph</h2>
|
|
219
255
|
<section class="grow">
|
|
220
|
-
<div class="card">
|
|
256
|
+
<div class="card card-research">
|
|
221
257
|
<h3><span class="tone tone-research"></span>research a term</h3>
|
|
222
258
|
<p class="hint">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 consent for these fetches.</p>
|
|
223
259
|
<div class="row">
|
|
@@ -236,24 +272,24 @@ ${THEME_TOKENS_CSS}
|
|
|
236
272
|
</div>
|
|
237
273
|
<p class="note" id="researchNote"></p>
|
|
238
274
|
</div>
|
|
239
|
-
<div class="card">
|
|
275
|
+
<div class="card card-taught">
|
|
240
276
|
<h3><span class="tone tone-taught"></span>teach by telling</h3>
|
|
241
|
-
<p class="hint">Type a plain fact and it
|
|
277
|
+
<p class="hint">Type a plain fact and it's stored if the recognizer can ground it, for example “a beagle is a kind of dog” or “a dog has a tail”. An unrecognized sentence is skipped.</p>
|
|
242
278
|
<div class="row">
|
|
243
279
|
<input id="teachInput" type="text" autocomplete="off" spellcheck="false" placeholder="a beagle is a kind of dog" aria-label="A fact to teach">
|
|
244
280
|
<button type="button" class="btn primary" id="teachGo" disabled>teach</button>
|
|
245
281
|
</div>
|
|
246
282
|
<p class="note" id="teachNote"></p>
|
|
247
283
|
</div>
|
|
248
|
-
<div class="card">
|
|
284
|
+
<div class="card card-ingest">
|
|
249
285
|
<h3><span class="tone tone-ingest"></span>ingest documents</h3>
|
|
250
|
-
<p class="hint">Paste or drop text
|
|
286
|
+
<p class="hint">Paste or drop text. It keeps only the sentences it can ground as facts and skips the rest, using the same recognizer the ingest page runs.</p>
|
|
251
287
|
<textarea id="ingestText" spellcheck="false" placeholder="Paste a paragraph or drop a .txt/.md file here." aria-label="Text to ingest"></textarea>
|
|
252
288
|
<div class="row">
|
|
253
289
|
<button type="button" class="btn primary" id="ingestGo" disabled>ingest</button>
|
|
254
290
|
<button type="button" class="btn" id="ingestBrowse">browse…</button>
|
|
255
291
|
<input type="file" id="ingestFile" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
256
|
-
<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, tagged optimistic-extract
|
|
292
|
+
<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, tagged optimistic-extract, below every curated source.">
|
|
257
293
|
<input type="checkbox" id="fuzzyToggle"> fuzzy tier
|
|
258
294
|
</label>
|
|
259
295
|
</div>
|
|
@@ -275,7 +311,7 @@ ${THEME_TOKENS_CSS}
|
|
|
275
311
|
|
|
276
312
|
<h2 class="band">read a term back</h2>
|
|
277
313
|
<div class="panel digestpanel">
|
|
278
|
-
<p class="hint">Pick a term the graph knows
|
|
314
|
+
<p class="hint">Pick a term the graph knows. It reads back a short narrative built from the facts it holds, with each source named. The narrative is deterministic and never a guess. The full fact list stays one click away behind “show the facts”.</p>
|
|
279
315
|
<div class="askRow">
|
|
280
316
|
<input id="digestInput" type="text" autocomplete="off" spellcheck="false" placeholder="a term the graph knows, e.g. dog" aria-label="A term to read back as a digest" disabled>
|
|
281
317
|
<button type="button" class="btn primary" id="digestGo" disabled>digest</button>
|
|
@@ -298,6 +334,7 @@ ${THEME_TOKENS_CSS}
|
|
|
298
334
|
<h3 style="margin:0">scope by source</h3>
|
|
299
335
|
<button type="button" class="toggleAll" id="toggleAll">all / none</button>
|
|
300
336
|
</div>
|
|
337
|
+
<div class="sourceTableHead" aria-hidden="true"><span>source</span><span>facts</span></div>
|
|
301
338
|
<div id="sourcesList"><p class="empty">loading sources…</p></div>
|
|
302
339
|
</div>
|
|
303
340
|
</div>
|
|
@@ -318,6 +355,8 @@ ${THEME_TOKENS_CSS}
|
|
|
318
355
|
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
319
356
|
const createTicker = ${createTicker.toString()};
|
|
320
357
|
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
358
|
+
const loadWinkVendor = ${loadWinkVendor.toString()};
|
|
359
|
+
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
321
360
|
const el = (id) => document.getElementById(id);
|
|
322
361
|
// The digest sentence-structure bank, pre-parsed at build time — the browser
|
|
323
362
|
// has no TOML parser, so the page carries the table the client-side digest
|
|
@@ -326,40 +365,40 @@ ${THEME_TOKENS_CSS}
|
|
|
326
365
|
|
|
327
366
|
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
328
367
|
|
|
329
|
-
|
|
368
|
+
// The header's stat panel replaces one status line: "seed" carries the
|
|
369
|
+
// fact-count readout (boot progress, then the final count), "engine"
|
|
370
|
+
// carries the wink-nlp load result. engineNote is a separate banner for
|
|
371
|
+
// the one case that needs more room than a stat value: the whole engine
|
|
372
|
+
// bundle failing to load.
|
|
373
|
+
// The build's own content hash for the seed. It rides in the URL this page
|
|
374
|
+
// fetches the seed by, so the service worker's cache-first read can only
|
|
375
|
+
// ever return the copy this page asked for. Empty in a build with no seed.
|
|
376
|
+
const SEED_STAMP = ${JSON.stringify(seedStamp)};
|
|
377
|
+
const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
|
|
378
|
+
|
|
379
|
+
const statFactsEl = el("statFacts");
|
|
380
|
+
const statSeedEl = el("statSeed");
|
|
381
|
+
const statEngineEl = el("statEngine");
|
|
382
|
+
const engineNoteEl = el("engineNote");
|
|
330
383
|
let session = null;
|
|
331
384
|
let researchQueue = null; // the engine's latest research snapshot, null when no run stands
|
|
332
385
|
const checkedSources = new Set(); // source keys currently checked for the ask
|
|
333
386
|
|
|
334
387
|
// ---- boot --------------------------------------------------------------
|
|
335
|
-
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
336
388
|
const progressParts = {};
|
|
337
389
|
let progressActive = true;
|
|
338
390
|
function noteProgress(key, loaded, total) {
|
|
339
391
|
progressParts[key] = { loaded: loaded, total: total };
|
|
340
|
-
if (progressActive)
|
|
392
|
+
if (progressActive) statEngineEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
341
393
|
}
|
|
342
394
|
|
|
343
|
-
|
|
344
|
-
let settled = false;
|
|
345
|
-
const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
|
|
346
|
-
try {
|
|
347
|
-
const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
|
|
348
|
-
settled = true;
|
|
349
|
-
window.tmctResearch.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
350
|
-
return "loaded";
|
|
351
|
-
} catch (err) {
|
|
352
|
-
settled = true;
|
|
353
|
-
console.warn("tmct research: the wink vendor asset failed to load", err);
|
|
354
|
-
return "unavailable";
|
|
355
|
-
}
|
|
356
|
-
}
|
|
395
|
+
const loadWink = loadWinkVendor({ register: (factory) => window.tmctResearch.registerWinkModel(factory) });
|
|
357
396
|
|
|
358
397
|
let seedPayload = null;
|
|
359
398
|
let seedFacts = 0;
|
|
360
399
|
async function fetchSeed() {
|
|
361
400
|
try {
|
|
362
|
-
const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
|
|
401
|
+
const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, (loaded, total) => noteProgress("seed", loaded, total));
|
|
363
402
|
seedPayload = JSON.parse(await blob.text());
|
|
364
403
|
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
365
404
|
} catch (err) {
|
|
@@ -367,12 +406,8 @@ ${THEME_TOKENS_CSS}
|
|
|
367
406
|
console.warn("tmct research: chat-seed.json unavailable — starting unseeded", err);
|
|
368
407
|
}
|
|
369
408
|
}
|
|
370
|
-
const cloneSeed = () => {
|
|
371
|
-
if (!seedPayload) return null;
|
|
372
|
-
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
373
|
-
};
|
|
374
409
|
function newSession() {
|
|
375
|
-
return window.tmctResearch.createResearchSession({ seedPayload:
|
|
410
|
+
return window.tmctResearch.createResearchSession({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
|
|
376
411
|
}
|
|
377
412
|
|
|
378
413
|
// The curated reference pack provider, same fetch seam chat.html registers,
|
|
@@ -398,15 +433,19 @@ ${THEME_TOKENS_CSS}
|
|
|
398
433
|
|
|
399
434
|
async function boot() {
|
|
400
435
|
if (!window.tmctResearch) {
|
|
401
|
-
|
|
436
|
+
engineNoteEl.hidden = false;
|
|
437
|
+
engineNoteEl.textContent = "The research engine did not load. Run npm run demo:build, then reload this page.";
|
|
438
|
+
statSeedEl.textContent = "—";
|
|
439
|
+
statEngineEl.textContent = "unavailable";
|
|
402
440
|
return;
|
|
403
441
|
}
|
|
404
|
-
const [winkStatus] = await Promise.all([
|
|
442
|
+
const [winkStatus] = await Promise.all([loadWink(), fetchSeed()]);
|
|
405
443
|
progressActive = false;
|
|
406
444
|
window.tmctResearch.registerReferencePackProvider(fetchPackProvider);
|
|
407
445
|
session = newSession();
|
|
408
|
-
const winkPart = winkStatus === "loaded" ? "wink-nlp
|
|
409
|
-
|
|
446
|
+
const winkPart = winkStatus === "loaded" ? "wink-nlp loaded" : "wink-nlp unavailable, curated tiers only";
|
|
447
|
+
statSeedEl.textContent = seedPayload ? seedFacts.toLocaleString() + " facts" : "no seed";
|
|
448
|
+
statEngineEl.textContent = winkPart + " · ready";
|
|
410
449
|
enableInputs();
|
|
411
450
|
await refresh();
|
|
412
451
|
}
|
|
@@ -428,6 +467,8 @@ ${THEME_TOKENS_CSS}
|
|
|
428
467
|
let snap;
|
|
429
468
|
try { snap = await window.tmctResearch.researchSnapshot(session.memoryDir, session.sessionIds); }
|
|
430
469
|
catch (err) { console.warn("tmct research: snapshot failed", err); return; }
|
|
470
|
+
const total = (snap.sources || []).reduce((sum, source) => sum + (Number(source.count) || 0), 0);
|
|
471
|
+
statFactsEl.textContent = total.toLocaleString();
|
|
431
472
|
renderRecent(snap.recent);
|
|
432
473
|
renderHubs(snap.hubs);
|
|
433
474
|
renderSources(snap.sources, snap.history);
|
|
@@ -459,6 +500,11 @@ ${THEME_TOKENS_CSS}
|
|
|
459
500
|
for (const fact of recent) box.appendChild(factRow(fact));
|
|
460
501
|
}
|
|
461
502
|
|
|
503
|
+
// A top-N ranked bar list, styled after ledger.html's own predicate.top
|
|
504
|
+
// panel so the two pages' widgets read as siblings. Each row keeps the
|
|
505
|
+
// page's "chip tapchip" contract (its click-to-digest behaviour and its
|
|
506
|
+
// own e2e coverage key off those two classes); the rank/track/fill bar
|
|
507
|
+
// rides inside it.
|
|
462
508
|
function renderHubs(hubs) {
|
|
463
509
|
const box = el("hubsList");
|
|
464
510
|
box.textContent = "";
|
|
@@ -468,19 +514,24 @@ ${THEME_TOKENS_CSS}
|
|
|
468
514
|
box.appendChild(p);
|
|
469
515
|
return;
|
|
470
516
|
}
|
|
471
|
-
|
|
517
|
+
const max = hubs.reduce((m, h) => Math.max(m, h.degree), 0) || 1;
|
|
518
|
+
hubs.forEach((hub, i) => {
|
|
472
519
|
const chip = document.createElement("button");
|
|
473
520
|
chip.type = "button";
|
|
474
521
|
chip.className = "chip tapchip";
|
|
475
522
|
chip.setAttribute("aria-label", "read a digest of " + hub.term);
|
|
476
|
-
|
|
523
|
+
const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = String(i + 1).padStart(2, "0");
|
|
524
|
+
const term = document.createElement("span"); term.className = "term"; term.textContent = hub.term;
|
|
525
|
+
const track = document.createElement("span"); track.className = "track";
|
|
526
|
+
const fill = document.createElement("span"); fill.className = "fill"; fill.style.width = ((hub.degree / max) * 100).toFixed(1) + "%";
|
|
527
|
+
track.appendChild(fill);
|
|
477
528
|
const deg = document.createElement("span");
|
|
478
529
|
deg.className = "deg";
|
|
479
530
|
deg.textContent = hub.degree + (hub.degree === 1 ? " fact" : " facts");
|
|
480
|
-
chip.appendChild(deg);
|
|
531
|
+
chip.appendChild(rank); chip.appendChild(term); chip.appendChild(track); chip.appendChild(deg);
|
|
481
532
|
chip.addEventListener("click", () => { el("digestInput").value = hub.term; digest(); });
|
|
482
533
|
box.appendChild(chip);
|
|
483
|
-
}
|
|
534
|
+
});
|
|
484
535
|
}
|
|
485
536
|
|
|
486
537
|
function renderSources(sources, history) {
|
|
@@ -499,7 +550,7 @@ ${THEME_TOKENS_CSS}
|
|
|
499
550
|
}
|
|
500
551
|
if (!sources || !sources.length) {
|
|
501
552
|
const p = document.createElement("p"); p.className = "empty";
|
|
502
|
-
p.textContent = "No facts yet
|
|
553
|
+
p.textContent = "No facts yet. Grow the graph above, then scope your question here.";
|
|
503
554
|
box.appendChild(p);
|
|
504
555
|
return;
|
|
505
556
|
}
|
|
@@ -569,7 +620,7 @@ ${THEME_TOKENS_CSS}
|
|
|
569
620
|
const answerEl = el("answer");
|
|
570
621
|
if (boxes.length && checked.length === 0) {
|
|
571
622
|
answerEl.className = "miss";
|
|
572
|
-
answerEl.textContent = "No source is checked
|
|
623
|
+
answerEl.textContent = "No source is checked. Check at least one on the right to ask against it.";
|
|
573
624
|
return;
|
|
574
625
|
}
|
|
575
626
|
answerEl.className = "";
|
|
@@ -605,7 +656,7 @@ ${THEME_TOKENS_CSS}
|
|
|
605
656
|
box.textContent = "";
|
|
606
657
|
if (!view) {
|
|
607
658
|
const p = document.createElement("p"); p.className = "miss";
|
|
608
|
-
p.textContent = 'No grounded digest for "' + term + '"
|
|
659
|
+
p.textContent = 'No grounded digest for "' + term + '". Nothing is stored about it, or nothing the digest could compose. It abstains rather than guess.';
|
|
609
660
|
box.appendChild(p);
|
|
610
661
|
return;
|
|
611
662
|
}
|
|
@@ -659,7 +710,7 @@ ${THEME_TOKENS_CSS}
|
|
|
659
710
|
note.textContent = "stored: " + (res.answer || "remembered.");
|
|
660
711
|
el("teachInput").value = "";
|
|
661
712
|
} else {
|
|
662
|
-
note.textContent = res && res.answer ? res.answer : "not a recognized fact shape
|
|
713
|
+
note.textContent = res && res.answer ? res.answer : "not a recognized fact shape, nothing stored.";
|
|
663
714
|
}
|
|
664
715
|
await refresh();
|
|
665
716
|
}
|
|
@@ -711,9 +762,9 @@ ${THEME_TOKENS_CSS}
|
|
|
711
762
|
const depth = researchQueue.maxDepth || 1;
|
|
712
763
|
const budget = researchQueue.maxTopics || 0;
|
|
713
764
|
const knobs = " (depth " + depth + (budget ? ", budget " + budget : "") + ")";
|
|
714
|
-
const capped = researchQueue.nodeCapReached ? "
|
|
765
|
+
const capped = researchQueue.nodeCapReached ? " · node budget reached" : "";
|
|
715
766
|
if (researchQueue.complete) {
|
|
716
|
-
note.textContent = 'research "' + researchQueue.topic + '" complete
|
|
767
|
+
note.textContent = 'research "' + researchQueue.topic + '" complete · '
|
|
717
768
|
+ researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s") + " grounded" + knobs + capped + ".";
|
|
718
769
|
} else {
|
|
719
770
|
note.textContent = 'research "' + researchQueue.topic + '": '
|
|
@@ -793,7 +844,8 @@ ${THEME_TOKENS_CSS}
|
|
|
793
844
|
|
|
794
845
|
window.tmctResearchReady = boot().catch((err) => {
|
|
795
846
|
console.error("tmct research failed to boot", err);
|
|
796
|
-
|
|
847
|
+
engineNoteEl.hidden = false;
|
|
848
|
+
engineNoteEl.textContent = "The research page failed to start (" + (err && err.message ? err.message : err) + ").";
|
|
797
849
|
});
|
|
798
850
|
})();
|
|
799
851
|
</script>
|