@polycode-projects/the-mechanical-code-talker 2.11.5 → 2.11.9
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/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +250 -53
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +140 -138
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
// research-viz.mjs — research.html, the graph-BUILDING page: a self-contained
|
|
2
|
+
// document shaped exactly like ingest-viz.mjs/chat-page-viz.mjs's own
|
|
3
|
+
// page-builders — one inlined <style> importing viz-theme.mjs's shared tokens,
|
|
4
|
+
// behaviour as an inlined IIFE — running the research engine
|
|
5
|
+
// (research-browser.bundle.js's globalThis.tmctResearch) by same-origin
|
|
6
|
+
// relative paths.
|
|
7
|
+
//
|
|
8
|
+
// One in-memory graph grows three ways, each visible on the page:
|
|
9
|
+
// 1. research a term — submits "research <topic>" (the Simple English
|
|
10
|
+
// Wikipedia lane) and steps its "research next" queue.
|
|
11
|
+
// 2. teach by telling — an ordinary teach turn ("a beagle is a kind of dog").
|
|
12
|
+
// 3. ingest documents — paste/drop text through the ingest recognizer.
|
|
13
|
+
// A highlights panel shows the facts just learned and the best-connected terms.
|
|
14
|
+
// The "ask the graph" box is scoped BY SOURCE: a checkbox per source (taught,
|
|
15
|
+
// ingested, research, each seeded corpus band), and the ask runs against only
|
|
16
|
+
// the checked sources — or the whole store when every box is checked. Each
|
|
17
|
+
// source lists the facts it added, so its learning history is on the page too.
|
|
18
|
+
//
|
|
19
|
+
// renderResearchHtml() is pure: no I/O, deterministic output for identical
|
|
20
|
+
// input. scripts/build-demo-site.mjs calls it directly and writes the result to
|
|
21
|
+
// public/research.html, after research-browser.bundle.js already exists.
|
|
22
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
23
|
+
import { fetchWithProgress } from "./memory-panel-viz.mjs";
|
|
24
|
+
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TITLE = "the-mechanical-code-talker — research";
|
|
27
|
+
|
|
28
|
+
/** The human label + short colour key for one source snapshot entry
|
|
29
|
+
* ({ key, band }). A seed band folds to a readable corpus name; the three
|
|
30
|
+
* growth sources get their own words. Pure and `.toString()`-splice safe. */
|
|
31
|
+
export function sourceLabelFor(source) {
|
|
32
|
+
const BANDS = {
|
|
33
|
+
human: "human persona", "human-medium": "human persona", "human-large": "human persona",
|
|
34
|
+
seon: "SEON ontology", conceptnet: "ConceptNet",
|
|
35
|
+
"tier2-aws": "AWS", "tier2-python": "Python", "tier2-java": "Java", "wordnet-xl": "WordNet",
|
|
36
|
+
};
|
|
37
|
+
const key = (source && source.key) || "";
|
|
38
|
+
if (key === "taught") return { label: "taught by telling", tone: "taught" };
|
|
39
|
+
if (key === "ingest") return { label: "ingested documents", tone: "ingest" };
|
|
40
|
+
if (key === "research") return { label: "wikipedia research", tone: "research" };
|
|
41
|
+
if (key === "other") return { label: "derived / other", tone: "seed" };
|
|
42
|
+
const band = (source && source.band) || "";
|
|
43
|
+
return { label: (BANDS[band] || band || "seed") + " (seed corpus)", tone: "seed" };
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
+
/** The self-contained research page. Pure — the same output for the same
|
|
76
|
+
* `title` every time; every piece of state is computed live in the browser
|
|
77
|
+
* once the sibling research bundle loads. */
|
|
78
|
+
export function renderResearchHtml({ title = DEFAULT_TITLE } = {}) {
|
|
79
|
+
return `<!doctype html>
|
|
80
|
+
<html lang="en">
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="utf-8">
|
|
83
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
84
|
+
<title>${escapeHtml(title)}</title>
|
|
85
|
+
<!--
|
|
86
|
+
The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
|
|
87
|
+
first-party bundle (built by scripts/build-wink-vendor.mjs), one cached copy
|
|
88
|
+
for every page, no CDN. The research engine needs wink to split and parse the
|
|
89
|
+
sentences it grounds; a failed load degrades to the curated tiers, never an
|
|
90
|
+
error.
|
|
91
|
+
-->
|
|
92
|
+
<style>
|
|
93
|
+
${THEME_TOKENS_CSS}
|
|
94
|
+
html, body { min-height: 100%; }
|
|
95
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
96
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
97
|
+
button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
|
|
98
|
+
button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
99
|
+
a { color: var(--corpus); }
|
|
100
|
+
|
|
101
|
+
.wrap { max-width: 1100px; margin: 0 auto; padding: 1.1rem 1.1rem 3rem; }
|
|
102
|
+
|
|
103
|
+
header.topbar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
104
|
+
.brand { display: flex; flex-direction: column; gap: .12rem; }
|
|
105
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
|
|
106
|
+
.subtitle { font-size: .86rem; color: var(--muted); max-width: 46ch; }
|
|
107
|
+
.status { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
108
|
+
|
|
109
|
+
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; }
|
|
110
|
+
|
|
111
|
+
/* the three ways to grow the graph — three cards in one row, stacking on a
|
|
112
|
+
narrow screen. */
|
|
113
|
+
.grow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
114
|
+
.grow .card { background: var(--bg); padding: .9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
|
|
115
|
+
.grow .card h3 { margin: 0; font-size: .95rem; }
|
|
116
|
+
.grow .card .hint { font-size: .76rem; color: var(--muted); margin: 0; }
|
|
117
|
+
.grow .card .tone { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .35rem; vertical-align: baseline; }
|
|
118
|
+
.tone-taught { background: var(--taught); } .tone-ingest { background: var(--entail); }
|
|
119
|
+
.tone-research { background: var(--corpus); } .tone-seed { background: var(--muted); }
|
|
120
|
+
|
|
121
|
+
.grow input[type="text"], .grow textarea { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .8rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .4rem .55rem; }
|
|
122
|
+
.grow textarea { resize: vertical; min-height: 5.5rem; line-height: 1.5; }
|
|
123
|
+
.grow input::placeholder, .grow textarea::placeholder { color: var(--muted); }
|
|
124
|
+
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
|
125
|
+
.btn { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .8rem; background: var(--card); }
|
|
126
|
+
.btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
127
|
+
.btn:disabled { opacity: .45; cursor: default; }
|
|
128
|
+
.card .note { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); min-height: 1rem; }
|
|
129
|
+
.optionToggle { display: inline-flex; align-items: center; gap: .35rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); cursor: pointer; }
|
|
130
|
+
.optionToggle input { margin: 0; accent-color: var(--corpus); }
|
|
131
|
+
|
|
132
|
+
/* highlights + ask, two columns */
|
|
133
|
+
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; align-items: start; }
|
|
134
|
+
|
|
135
|
+
.panel { border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem 1rem; background: var(--card); }
|
|
136
|
+
.panel h3 { margin: 0 0 .6rem; font-size: .9rem; }
|
|
137
|
+
.panel .empty { color: var(--muted); font-size: .8rem; margin: .3rem 0; }
|
|
138
|
+
|
|
139
|
+
.fact { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: .45rem; align-items: baseline; padding: .28rem 0; border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .76rem; }
|
|
140
|
+
.fact:last-child { border-bottom: none; }
|
|
141
|
+
.fact .dot { width: 7px; height: 7px; border-radius: 50%; align-self: center; }
|
|
142
|
+
.fact .subj { color: var(--ink); word-break: break-word; }
|
|
143
|
+
.fact .pred { color: var(--corpus); white-space: nowrap; }
|
|
144
|
+
.fact .obj { color: var(--ink); word-break: break-word; }
|
|
145
|
+
|
|
146
|
+
.chips { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
147
|
+
.chip { font-family: ${MONO_STACK}; font-size: .74rem; border: 1px solid var(--line); border-radius: 99px; padding: .2rem .6rem; background: var(--bg); }
|
|
148
|
+
.chip .deg { color: var(--muted); margin-left: .35rem; }
|
|
149
|
+
|
|
150
|
+
/* ask, scoped by source */
|
|
151
|
+
.askRow { display: flex; gap: .5rem; margin: .2rem 0 .7rem; }
|
|
152
|
+
.askRow input { flex: 1; min-width: 0; font-family: ${SERIF_STACK}; font-size: .92rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: .45rem .7rem; }
|
|
153
|
+
#answer { font-size: .9rem; white-space: pre-wrap; word-break: break-word; padding: .55rem .7rem; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); min-height: 1.4rem; }
|
|
154
|
+
#answer.miss { color: var(--muted); border-style: dashed; }
|
|
155
|
+
.sourcesHead { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; margin: .2rem 0 .5rem; }
|
|
156
|
+
.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); }
|
|
157
|
+
|
|
158
|
+
details.source { border-bottom: 1px solid var(--line); }
|
|
159
|
+
details.source:last-of-type { border-bottom: none; }
|
|
160
|
+
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; }
|
|
161
|
+
details.source > summary::-webkit-details-marker { display: none; }
|
|
162
|
+
details.source > summary .srcLabel { flex: 1; display: inline-flex; align-items: center; gap: .4rem; }
|
|
163
|
+
details.source > summary input { margin: 0; accent-color: var(--corpus); }
|
|
164
|
+
details.source > summary .count { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
165
|
+
details.source > summary .caret { color: var(--muted); font-size: .7rem; }
|
|
166
|
+
.srcHistory { padding: .1rem 0 .6rem 1.4rem; }
|
|
167
|
+
.srcHistory .empty { font-size: .74rem; }
|
|
168
|
+
|
|
169
|
+
.toolsRow { display: flex; gap: .5rem; margin-top: 1.2rem; flex-wrap: wrap; }
|
|
170
|
+
.toolsRow .btn { font-size: .7rem; }
|
|
171
|
+
|
|
172
|
+
@media (max-width: 820px) {
|
|
173
|
+
.grow { grid-template-columns: 1fr; }
|
|
174
|
+
.cols { grid-template-columns: 1fr; }
|
|
175
|
+
}
|
|
176
|
+
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
|
|
177
|
+
</style>
|
|
178
|
+
</head>
|
|
179
|
+
<body>
|
|
180
|
+
<div class="wrap">
|
|
181
|
+
<header class="topbar">
|
|
182
|
+
<div class="brand">
|
|
183
|
+
<span class="eyebrow">the-mechanical-code-talker</span>
|
|
184
|
+
<span class="subtitle">research — grow one graph three ways, watch what it learns, and ask it a question scoped to the sources you trust.</span>
|
|
185
|
+
</div>
|
|
186
|
+
<span class="status" id="status">loading the engine…</span>
|
|
187
|
+
</header>
|
|
188
|
+
|
|
189
|
+
<h2 class="band">grow the graph</h2>
|
|
190
|
+
<section class="grow">
|
|
191
|
+
<div class="card">
|
|
192
|
+
<h3><span class="tone tone-research"></span>research a term</h3>
|
|
193
|
+
<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>
|
|
194
|
+
<div class="row">
|
|
195
|
+
<input id="researchTopic" type="text" autocomplete="off" spellcheck="false" placeholder="a topic, e.g. owls" aria-label="Topic to research">
|
|
196
|
+
<button type="button" class="btn primary" id="researchGo" disabled>research</button>
|
|
197
|
+
<button type="button" class="btn" id="researchNext" hidden>research next</button>
|
|
198
|
+
<button type="button" class="btn" id="researchPlay" aria-pressed="false" hidden>play</button>
|
|
199
|
+
</div>
|
|
200
|
+
<p class="note" id="researchNote"></p>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="card">
|
|
203
|
+
<h3><span class="tone tone-taught"></span>teach by telling</h3>
|
|
204
|
+
<p class="hint">Type a plain fact and it is stored if the recognizer can ground it — “a beagle is a kind of dog”, “a dog has a tail”. No guessing: an unrecognized sentence is skipped, honestly.</p>
|
|
205
|
+
<div class="row">
|
|
206
|
+
<input id="teachInput" type="text" autocomplete="off" spellcheck="false" placeholder="a beagle is a kind of dog" aria-label="A fact to teach">
|
|
207
|
+
<button type="button" class="btn primary" id="teachGo" disabled>teach</button>
|
|
208
|
+
</div>
|
|
209
|
+
<p class="note" id="teachNote"></p>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="card">
|
|
212
|
+
<h3><span class="tone tone-ingest"></span>ingest documents</h3>
|
|
213
|
+
<p class="hint">Paste or drop text; it keeps only the sentences it can ground as facts and skips the rest. The same recognizer the ingest page runs.</p>
|
|
214
|
+
<textarea id="ingestText" spellcheck="false" placeholder="Paste a paragraph or drop a .txt/.md file here." aria-label="Text to ingest"></textarea>
|
|
215
|
+
<div class="row">
|
|
216
|
+
<button type="button" class="btn primary" id="ingestGo" disabled>ingest</button>
|
|
217
|
+
<button type="button" class="btn" id="ingestBrowse">browse…</button>
|
|
218
|
+
<input type="file" id="ingestFile" accept=".txt,.md,text/plain,text/markdown" hidden>
|
|
219
|
+
<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.">
|
|
220
|
+
<input type="checkbox" id="fuzzyToggle"> fuzzy tier
|
|
221
|
+
</label>
|
|
222
|
+
</div>
|
|
223
|
+
<p class="note" id="ingestNote"></p>
|
|
224
|
+
</div>
|
|
225
|
+
</section>
|
|
226
|
+
|
|
227
|
+
<h2 class="band">highlights</h2>
|
|
228
|
+
<div class="cols">
|
|
229
|
+
<div class="panel">
|
|
230
|
+
<h3>recently learned</h3>
|
|
231
|
+
<div id="recentList"><p class="empty">Nothing learned yet this session. Research a term, teach a fact, or ingest some text above.</p></div>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="panel">
|
|
234
|
+
<h3>best-connected terms</h3>
|
|
235
|
+
<div class="chips" id="hubsList"><p class="empty">The graph's hubs appear here as it grows.</p></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<h2 class="band">ask the graph</h2>
|
|
240
|
+
<div class="cols">
|
|
241
|
+
<div class="panel">
|
|
242
|
+
<h3>ask a question</h3>
|
|
243
|
+
<div class="askRow">
|
|
244
|
+
<input id="askInput" type="text" autocomplete="off" spellcheck="false" placeholder="what is a dog" aria-label="Ask the graph a question" disabled>
|
|
245
|
+
<button type="button" class="btn primary" id="askGo" disabled>ask</button>
|
|
246
|
+
</div>
|
|
247
|
+
<div id="answer" aria-live="polite">Ask the graph a question. The answer is drawn only from the sources you check on the right.</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="panel">
|
|
250
|
+
<div class="sourcesHead">
|
|
251
|
+
<h3 style="margin:0">scope by source</h3>
|
|
252
|
+
<button type="button" class="toggleAll" id="toggleAll">all / none</button>
|
|
253
|
+
</div>
|
|
254
|
+
<div id="sourcesList"><p class="empty">loading sources…</p></div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="toolsRow">
|
|
259
|
+
<button type="button" class="btn" id="exportFacts" disabled>export facts (JSONL)</button>
|
|
260
|
+
<button type="button" class="btn" id="resetBtn">reset the graph</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<script src="./research-browser.bundle.js"></script>
|
|
265
|
+
<script>
|
|
266
|
+
(function () {
|
|
267
|
+
"use strict";
|
|
268
|
+
const loadProgressLine = ${loadProgressLine.toString()};
|
|
269
|
+
const sourceLabelFor = ${sourceLabelFor.toString()};
|
|
270
|
+
const factTripleParts = ${factTripleParts.toString()};
|
|
271
|
+
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
272
|
+
const createTicker = ${createTicker.toString()};
|
|
273
|
+
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
274
|
+
const el = (id) => document.getElementById(id);
|
|
275
|
+
|
|
276
|
+
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
277
|
+
|
|
278
|
+
const statusEl = el("status");
|
|
279
|
+
let session = null;
|
|
280
|
+
let researchQueue = null; // the engine's latest research snapshot, null when no run stands
|
|
281
|
+
const checkedSources = new Set(); // source keys currently checked for the ask
|
|
282
|
+
|
|
283
|
+
// ---- boot --------------------------------------------------------------
|
|
284
|
+
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
285
|
+
const progressParts = {};
|
|
286
|
+
let progressActive = true;
|
|
287
|
+
function noteProgress(key, loaded, total) {
|
|
288
|
+
progressParts[key] = { loaded: loaded, total: total };
|
|
289
|
+
if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function tryLoadWink() {
|
|
293
|
+
let settled = false;
|
|
294
|
+
const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
|
|
295
|
+
try {
|
|
296
|
+
const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
|
|
297
|
+
settled = true;
|
|
298
|
+
window.tmctResearch.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
299
|
+
return "loaded";
|
|
300
|
+
} catch (err) {
|
|
301
|
+
settled = true;
|
|
302
|
+
console.warn("tmct research: the wink vendor asset failed to load", err);
|
|
303
|
+
return "unavailable";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let seedPayload = null;
|
|
308
|
+
let seedFacts = 0;
|
|
309
|
+
async function fetchSeed() {
|
|
310
|
+
try {
|
|
311
|
+
const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
|
|
312
|
+
seedPayload = JSON.parse(await blob.text());
|
|
313
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
seedPayload = null;
|
|
316
|
+
console.warn("tmct research: chat-seed.json unavailable — starting unseeded", err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const cloneSeed = () => {
|
|
320
|
+
if (!seedPayload) return null;
|
|
321
|
+
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
322
|
+
};
|
|
323
|
+
function newSession() {
|
|
324
|
+
return window.tmctResearch.createResearchSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload) });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The curated reference pack provider, same fetch seam chat.html registers,
|
|
328
|
+
// so a research/teach miss can still reach a shipped article where one exists.
|
|
329
|
+
let packIndexPromise = null;
|
|
330
|
+
function fetchPackIndex() {
|
|
331
|
+
if (!packIndexPromise) {
|
|
332
|
+
packIndexPromise = fetch("./reference-pack/index.json").then((res) => (res.ok ? res.json() : null)).catch(() => null);
|
|
333
|
+
}
|
|
334
|
+
return packIndexPromise;
|
|
335
|
+
}
|
|
336
|
+
const fetchPackProvider = {
|
|
337
|
+
async lookup(normTerm) {
|
|
338
|
+
const index = await fetchPackIndex();
|
|
339
|
+
const id = index && index.terms ? index.terms[String(normTerm || "")] : null;
|
|
340
|
+
if (!id) return null;
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch("./reference-pack/articles/" + id + ".json");
|
|
343
|
+
return res.ok ? await res.json() : null;
|
|
344
|
+
} catch { return null; }
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
async function boot() {
|
|
349
|
+
if (!window.tmctResearch) {
|
|
350
|
+
statusEl.textContent = "the research engine didn't load — this page needs its build step (npm run demo:build)";
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const [winkStatus] = await Promise.all([tryLoadWink(), fetchSeed()]);
|
|
354
|
+
progressActive = false;
|
|
355
|
+
window.tmctResearch.registerReferencePackProvider(fetchPackProvider);
|
|
356
|
+
session = newSession();
|
|
357
|
+
const winkPart = winkStatus === "loaded" ? "wink-nlp: loaded" : "wink-nlp unavailable — curated tiers only";
|
|
358
|
+
statusEl.textContent = (seedPayload ? seedFacts + " seed facts" : "no seed") + " · " + winkPart + " — ready.";
|
|
359
|
+
enableInputs();
|
|
360
|
+
await refresh();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function enableInputs() {
|
|
364
|
+
el("researchGo").disabled = false;
|
|
365
|
+
el("teachGo").disabled = false;
|
|
366
|
+
el("ingestGo").disabled = false;
|
|
367
|
+
el("askInput").disabled = false;
|
|
368
|
+
el("askGo").disabled = false;
|
|
369
|
+
el("exportFacts").disabled = false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- refresh the panels from one snapshot ------------------------------
|
|
373
|
+
async function refresh() {
|
|
374
|
+
if (!session) return;
|
|
375
|
+
let snap;
|
|
376
|
+
try { snap = await window.tmctResearch.researchSnapshot(session.memoryDir, session.sessionIds); }
|
|
377
|
+
catch (err) { console.warn("tmct research: snapshot failed", err); return; }
|
|
378
|
+
renderRecent(snap.recent);
|
|
379
|
+
renderHubs(snap.hubs);
|
|
380
|
+
renderSources(snap.sources, snap.history);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function factRow(fact) {
|
|
384
|
+
const parts = factTripleParts(fact);
|
|
385
|
+
const tone = sourceLabelFor({ key: parts.source, band: parts.source.indexOf("seed:") === 0 ? parts.source.slice(5) : "" }).tone;
|
|
386
|
+
const row = document.createElement("div");
|
|
387
|
+
row.className = "fact";
|
|
388
|
+
const dot = document.createElement("span");
|
|
389
|
+
dot.className = "dot tone-" + tone;
|
|
390
|
+
const subj = document.createElement("span"); subj.className = "subj"; subj.textContent = parts.subject;
|
|
391
|
+
const pred = document.createElement("span"); pred.className = "pred"; pred.textContent = parts.predicate;
|
|
392
|
+
const obj = document.createElement("span"); obj.className = "obj"; obj.textContent = parts.object;
|
|
393
|
+
row.appendChild(dot); row.appendChild(subj); row.appendChild(pred); row.appendChild(obj);
|
|
394
|
+
return row;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function renderRecent(recent) {
|
|
398
|
+
const box = el("recentList");
|
|
399
|
+
box.textContent = "";
|
|
400
|
+
if (!recent || !recent.length) {
|
|
401
|
+
const p = document.createElement("p"); p.className = "empty";
|
|
402
|
+
p.textContent = "Nothing learned yet this session. Research a term, teach a fact, or ingest some text above.";
|
|
403
|
+
box.appendChild(p);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const fact of recent) box.appendChild(factRow(fact));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function renderHubs(hubs) {
|
|
410
|
+
const box = el("hubsList");
|
|
411
|
+
box.textContent = "";
|
|
412
|
+
if (!hubs || !hubs.length) {
|
|
413
|
+
const p = document.createElement("p"); p.className = "empty";
|
|
414
|
+
p.textContent = "The graph's hubs appear here as it grows.";
|
|
415
|
+
box.appendChild(p);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const hub of hubs) {
|
|
419
|
+
const chip = document.createElement("span");
|
|
420
|
+
chip.className = "chip";
|
|
421
|
+
chip.appendChild(document.createTextNode(hub.term));
|
|
422
|
+
const deg = document.createElement("span");
|
|
423
|
+
deg.className = "deg";
|
|
424
|
+
deg.textContent = hub.degree + (hub.degree === 1 ? " fact" : " facts");
|
|
425
|
+
chip.appendChild(deg);
|
|
426
|
+
box.appendChild(chip);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderSources(sources, history) {
|
|
431
|
+
const box = el("sourcesList");
|
|
432
|
+
box.textContent = "";
|
|
433
|
+
// Keep the checked set current: default a brand-new source to checked, drop
|
|
434
|
+
// any source that has vanished.
|
|
435
|
+
const present = new Set();
|
|
436
|
+
for (const s of sources || []) {
|
|
437
|
+
present.add(s.key);
|
|
438
|
+
if (!checkedSources.has(s.key + ":seen")) { checkedSources.add(s.key); checkedSources.add(s.key + ":seen"); }
|
|
439
|
+
}
|
|
440
|
+
for (const k of [...checkedSources]) {
|
|
441
|
+
const base = k.replace(/:seen$/, "");
|
|
442
|
+
if (!present.has(base)) checkedSources.delete(k);
|
|
443
|
+
}
|
|
444
|
+
if (!sources || !sources.length) {
|
|
445
|
+
const p = document.createElement("p"); p.className = "empty";
|
|
446
|
+
p.textContent = "No facts yet — grow the graph above, then scope your question here.";
|
|
447
|
+
box.appendChild(p);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const s of sources) {
|
|
451
|
+
const info = sourceLabelFor(s);
|
|
452
|
+
const det = document.createElement("details");
|
|
453
|
+
det.className = "source";
|
|
454
|
+
const sum = document.createElement("summary");
|
|
455
|
+
const labelWrap = document.createElement("span");
|
|
456
|
+
labelWrap.className = "srcLabel";
|
|
457
|
+
const cb = document.createElement("input");
|
|
458
|
+
cb.type = "checkbox";
|
|
459
|
+
cb.checked = checkedSources.has(s.key);
|
|
460
|
+
cb.setAttribute("data-key", s.key);
|
|
461
|
+
cb.addEventListener("click", (e) => e.stopPropagation());
|
|
462
|
+
cb.addEventListener("change", () => {
|
|
463
|
+
if (cb.checked) checkedSources.add(s.key); else checkedSources.delete(s.key);
|
|
464
|
+
});
|
|
465
|
+
const dot = document.createElement("span");
|
|
466
|
+
dot.className = "tone tone-" + info.tone;
|
|
467
|
+
dot.style.cssText = "display:inline-block;width:8px;height:8px;border-radius:50%";
|
|
468
|
+
const name = document.createElement("span");
|
|
469
|
+
name.textContent = info.label;
|
|
470
|
+
labelWrap.appendChild(cb); labelWrap.appendChild(dot); labelWrap.appendChild(name);
|
|
471
|
+
const count = document.createElement("span");
|
|
472
|
+
count.className = "count";
|
|
473
|
+
count.textContent = s.count + (s.count === 1 ? " fact" : " facts");
|
|
474
|
+
const caret = document.createElement("span");
|
|
475
|
+
caret.className = "caret";
|
|
476
|
+
caret.textContent = "history";
|
|
477
|
+
sum.appendChild(labelWrap); sum.appendChild(count); sum.appendChild(caret);
|
|
478
|
+
det.appendChild(sum);
|
|
479
|
+
const hist = document.createElement("div");
|
|
480
|
+
hist.className = "srcHistory";
|
|
481
|
+
const rows = (history && history[s.key]) || [];
|
|
482
|
+
if (!rows.length) {
|
|
483
|
+
const p = document.createElement("p"); p.className = "empty";
|
|
484
|
+
p.textContent = "no facts recorded from this source yet.";
|
|
485
|
+
hist.appendChild(p);
|
|
486
|
+
} else {
|
|
487
|
+
for (const row of rows) hist.appendChild(factRow(row));
|
|
488
|
+
}
|
|
489
|
+
det.appendChild(hist);
|
|
490
|
+
box.appendChild(det);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
el("toggleAll").addEventListener("click", () => {
|
|
495
|
+
const boxes = [...el("sourcesList").querySelectorAll('input[type="checkbox"]')];
|
|
496
|
+
const anyUnchecked = boxes.some((b) => !b.checked);
|
|
497
|
+
for (const b of boxes) {
|
|
498
|
+
b.checked = anyUnchecked;
|
|
499
|
+
const key = b.getAttribute("data-key");
|
|
500
|
+
if (anyUnchecked) checkedSources.add(key); else checkedSources.delete(key);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ---- ask, scoped by source ---------------------------------------------
|
|
505
|
+
async function ask() {
|
|
506
|
+
const q = el("askInput").value.trim();
|
|
507
|
+
if (!q || !session) return;
|
|
508
|
+
const boxes = [...el("sourcesList").querySelectorAll('input[type="checkbox"]')];
|
|
509
|
+
const checked = boxes.filter((b) => b.checked).map((b) => b.getAttribute("data-key"));
|
|
510
|
+
// Every box checked (or none present) -> ask the whole store; a subset ->
|
|
511
|
+
// scope to those keys. No box checked -> honest miss, nothing to ask.
|
|
512
|
+
const allChecked = boxes.length > 0 && checked.length === boxes.length;
|
|
513
|
+
const answerEl = el("answer");
|
|
514
|
+
if (boxes.length && checked.length === 0) {
|
|
515
|
+
answerEl.className = "miss";
|
|
516
|
+
answerEl.textContent = "No source is checked — check at least one on the right to ask against it.";
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
answerEl.className = "";
|
|
520
|
+
answerEl.textContent = "thinking…";
|
|
521
|
+
let res;
|
|
522
|
+
try { res = await session.ask(q, { sources: allChecked ? null : checked }); }
|
|
523
|
+
catch (err) { res = { text: "", miss: true }; }
|
|
524
|
+
if (res.miss || !res.text) {
|
|
525
|
+
answerEl.className = "miss";
|
|
526
|
+
const scope = allChecked ? "any checked source" : "the " + checked.length + " checked source" + (checked.length === 1 ? "" : "s");
|
|
527
|
+
answerEl.textContent = "No grounded answer from " + scope + ". It abstains rather than guess.";
|
|
528
|
+
} else {
|
|
529
|
+
answerEl.className = "";
|
|
530
|
+
answerEl.textContent = res.text;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
el("askGo").addEventListener("click", ask);
|
|
534
|
+
el("askInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); ask(); } });
|
|
535
|
+
|
|
536
|
+
// ---- grow: teach by telling --------------------------------------------
|
|
537
|
+
async function teach() {
|
|
538
|
+
const q = el("teachInput").value.trim();
|
|
539
|
+
if (!q || !session) return;
|
|
540
|
+
const note = el("teachNote");
|
|
541
|
+
note.textContent = "…";
|
|
542
|
+
let res;
|
|
543
|
+
try { res = await session.turn(q); } catch { res = null; }
|
|
544
|
+
if (res && res.record && res.record.via === "assert" && !res.record.miss) {
|
|
545
|
+
note.textContent = "stored: " + (res.answer || "remembered.");
|
|
546
|
+
el("teachInput").value = "";
|
|
547
|
+
} else {
|
|
548
|
+
note.textContent = res && res.answer ? res.answer : "not a recognized fact shape — nothing stored.";
|
|
549
|
+
}
|
|
550
|
+
await refresh();
|
|
551
|
+
}
|
|
552
|
+
el("teachGo").addEventListener("click", teach);
|
|
553
|
+
el("teachInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); teach(); } });
|
|
554
|
+
|
|
555
|
+
// ---- grow: ingest documents --------------------------------------------
|
|
556
|
+
async function ingest() {
|
|
557
|
+
const text = el("ingestText").value.trim();
|
|
558
|
+
if (!text || !session) return;
|
|
559
|
+
const note = el("ingestNote");
|
|
560
|
+
note.textContent = "reading…";
|
|
561
|
+
let summary;
|
|
562
|
+
try { summary = await session.ingest(text, { optimistic: el("fuzzyToggle").checked }); }
|
|
563
|
+
catch (err) { note.textContent = "something went wrong reading that."; return; }
|
|
564
|
+
note.textContent = summary.sentences + " sentence" + (summary.sentences === 1 ? "" : "s")
|
|
565
|
+
+ " read, " + summary.recognized + " grounded, " + summary.skipped + " skipped.";
|
|
566
|
+
await refresh();
|
|
567
|
+
}
|
|
568
|
+
el("ingestGo").addEventListener("click", ingest);
|
|
569
|
+
el("ingestBrowse").addEventListener("click", () => el("ingestFile").click());
|
|
570
|
+
el("ingestFile").addEventListener("change", async () => {
|
|
571
|
+
const file = el("ingestFile").files && el("ingestFile").files[0];
|
|
572
|
+
el("ingestFile").value = "";
|
|
573
|
+
if (!file) return;
|
|
574
|
+
try { el("ingestText").value = await file.text(); }
|
|
575
|
+
catch { el("ingestNote").textContent = "couldn't read that file."; }
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ---- grow: research a term + its queue ----------------------------------
|
|
579
|
+
const RESEARCH_TICK_MS = 2400;
|
|
580
|
+
const researchTicker = createTicker({
|
|
581
|
+
onTick: async () => { await researchStep("research next"); },
|
|
582
|
+
hasNext: () => Boolean(researchQueue && !researchQueue.complete),
|
|
583
|
+
onRender: renderResearchControls,
|
|
584
|
+
waitMs: RESEARCH_TICK_MS,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
function renderResearchControls(tickState) {
|
|
588
|
+
const state = tickState || researchTicker.getState();
|
|
589
|
+
const active = Boolean(researchQueue && !researchQueue.complete);
|
|
590
|
+
el("researchNext").hidden = !active;
|
|
591
|
+
el("researchPlay").hidden = !active;
|
|
592
|
+
el("researchPlay").textContent = state.playing ? "pause" : "play";
|
|
593
|
+
el("researchPlay").setAttribute("aria-pressed", String(state.playing));
|
|
594
|
+
const note = el("researchNote");
|
|
595
|
+
if (!researchQueue) { /* leave whatever the last turn's note said */ }
|
|
596
|
+
else if (researchQueue.complete) {
|
|
597
|
+
note.textContent = 'research "' + researchQueue.topic + '" complete — '
|
|
598
|
+
+ researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s") + " grounded.";
|
|
599
|
+
} else {
|
|
600
|
+
note.textContent = 'research "' + researchQueue.topic + '": '
|
|
601
|
+
+ researchQueue.done.length + " done · " + researchQueue.pending.length + " queued.";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function researchStep(line) {
|
|
606
|
+
if (!session) return;
|
|
607
|
+
let res;
|
|
608
|
+
try { res = await session.turn(line); } catch { res = null; }
|
|
609
|
+
if (res && res.research !== undefined) {
|
|
610
|
+
researchQueue = res.research;
|
|
611
|
+
renderResearchControls();
|
|
612
|
+
} else if (res && res.answer) {
|
|
613
|
+
el("researchNote").textContent = res.answer.split("\\n")[0];
|
|
614
|
+
}
|
|
615
|
+
await refresh();
|
|
616
|
+
return res;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function startResearch() {
|
|
620
|
+
const topic = el("researchTopic").value.trim();
|
|
621
|
+
if (!topic || !session) return;
|
|
622
|
+
el("researchTopic").value = "";
|
|
623
|
+
el("researchNote").textContent = 'researching "' + topic + '"…';
|
|
624
|
+
const previous = researchQueue;
|
|
625
|
+
await researchStep("research " + topic);
|
|
626
|
+
const fresh = Boolean(researchQueue && !researchQueue.complete && (!previous || previous.complete || previous.topic !== researchQueue.topic));
|
|
627
|
+
if (fresh && !prefersReducedMotion() && !researchTicker.getState().playing) researchTicker.play();
|
|
628
|
+
}
|
|
629
|
+
el("researchGo").addEventListener("click", startResearch);
|
|
630
|
+
el("researchTopic").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); startResearch(); } });
|
|
631
|
+
el("researchNext").addEventListener("click", () => researchStep("research next"));
|
|
632
|
+
el("researchPlay").addEventListener("click", () => {
|
|
633
|
+
if (researchTicker.getState().playing) researchTicker.pause(); else researchTicker.play();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ---- tools --------------------------------------------------------------
|
|
637
|
+
el("exportFacts").addEventListener("click", async () => {
|
|
638
|
+
if (!session || !window.tmctResearch.exportFactsJsonl) return;
|
|
639
|
+
let jsonl;
|
|
640
|
+
try { jsonl = await window.tmctResearch.exportFactsJsonl(session.memoryDir); }
|
|
641
|
+
catch { return; }
|
|
642
|
+
const blob = new Blob([jsonl], { type: "application/x-ndjson" });
|
|
643
|
+
const url = URL.createObjectURL(blob);
|
|
644
|
+
const link = document.createElement("a");
|
|
645
|
+
link.href = url; link.download = "tmct-facts.jsonl";
|
|
646
|
+
document.body.appendChild(link); link.click(); link.remove();
|
|
647
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
648
|
+
});
|
|
649
|
+
el("resetBtn").addEventListener("click", async () => {
|
|
650
|
+
researchTicker.pause();
|
|
651
|
+
researchQueue = null;
|
|
652
|
+
checkedSources.clear();
|
|
653
|
+
session = newSession();
|
|
654
|
+
el("teachNote").textContent = "";
|
|
655
|
+
el("ingestNote").textContent = "";
|
|
656
|
+
el("researchNote").textContent = "";
|
|
657
|
+
el("answer").className = "";
|
|
658
|
+
el("answer").textContent = "Ask the graph a question. The answer is drawn only from the sources you check on the right.";
|
|
659
|
+
renderResearchControls();
|
|
660
|
+
await refresh();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
window.tmctResearchReady = boot().catch((err) => {
|
|
664
|
+
console.error("tmct research failed to boot", err);
|
|
665
|
+
statusEl.textContent = "the research page failed to start (" + (err && err.message ? err.message : err) + ")";
|
|
666
|
+
});
|
|
667
|
+
})();
|
|
668
|
+
</script>
|
|
669
|
+
</body>
|
|
670
|
+
</html>
|
|
671
|
+
`;
|
|
672
|
+
}
|