@polycode-projects/the-mechanical-code-talker 4.1.0 → 4.1.2
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 +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +1 -1
- package/src/services/adventure-viz.mjs +43 -39
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +403 -332
- package/src/services/chat.mjs +274 -156
- package/src/services/code-explorer-viz.mjs +142 -55
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +153 -28
- package/src/services/ledger-viz.mjs +47 -47
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +474 -234
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +72 -13
- package/src/services/research-viz.mjs +35 -24
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +24 -24
- package/src/services/sprite-catalog-viz.mjs +307 -82
- package/src/surfaces/web/adventure-browser-entry.mjs +46 -25
- package/src/surfaces/web/chat-browser-entry.mjs +55 -9
- package/src/surfaces/web/code-explorer-browser-entry.mjs +30 -16
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +81 -11
- package/src/surfaces/web/ledger-browser-entry.mjs +47 -14
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +77 -25
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +49 -11
- package/src/surfaces/web/research-browser-entry.mjs +33 -24
- package/src/surfaces/web/spider-fly-browser-entry.mjs +32 -13
- package/src/surfaces/web/sprites-browser-entry.mjs +51 -11
- package/src/surfaces/web/tmct-surface.mjs +159 -0
- package/src/surfaces/web/turn-session.mjs +16 -5
- package/src/tools/server.mjs +13 -6
|
@@ -107,10 +107,111 @@ export function computeCodeExplorerData(payload, opts = {}) {
|
|
|
107
107
|
return { payload, ledger, hints, focus: ledger.focus || focus, meta: { title: opts.title || "code graph" } };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// ---- client script pieces, spliced into CLIENT_JS below -------------------
|
|
111
|
+
// Named module functions so the page's own source reads as a list of calls
|
|
112
|
+
// into real, unit-tested code rather than an inlined block. Each is written
|
|
113
|
+
// to survive a `.toString()` round trip into a browser `<script>` tag: no
|
|
114
|
+
// closure over this module's own scope, only its parameters and whichever
|
|
115
|
+
// sibling below it is also spliced into the same page script.
|
|
116
|
+
|
|
117
|
+
/** One line naming the loaded graph's own size: individual/edge counts, then
|
|
118
|
+
* up to four of its most common classes. */
|
|
119
|
+
export function statsSummaryLine(ledgerStats) {
|
|
120
|
+
var parts = [ledgerStats.individuals + " individuals", ledgerStats.edges + " edges"];
|
|
121
|
+
var cls = ledgerStats.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
|
|
122
|
+
return parts.concat(cls).join(" · ");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Every fact the chat can draw on: the graph's own edges plus whatever the
|
|
126
|
+
* general-knowledge seed has counted so far. */
|
|
127
|
+
export function factTotalText(edges, seedFacts) {
|
|
128
|
+
return (edges + seedFacts).toLocaleString();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function rowKey(row) { return JSON.stringify([row.s, row.kind, row.o]); }
|
|
132
|
+
|
|
133
|
+
/** The sidebar's rows: the engine's own answer about the focus when it
|
|
134
|
+
* grounded one, the row list's plain filter otherwise, then whatever
|
|
135
|
+
* neither named — a fold over the rest of the graph. */
|
|
136
|
+
export function focusRowsHtml(data, related) {
|
|
137
|
+
var focus = data.focus;
|
|
138
|
+
var rows = data.ledger.rows;
|
|
139
|
+
var grounded = Boolean(related && related.grounded);
|
|
140
|
+
var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
|
|
141
|
+
var nearKeys = {};
|
|
142
|
+
near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
|
|
143
|
+
var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
|
|
144
|
+
var ordered = near.concat(rest);
|
|
145
|
+
return ordered.map(function (r) {
|
|
146
|
+
var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
|
|
147
|
+
return '<li class="row' + hot + '">'
|
|
148
|
+
+ '<button class="term" data-term="' + escapeHtml(r.s) + '">' + escapeHtml(r.s) + '</button> '
|
|
149
|
+
+ '<span class="verb">' + escapeHtml(r.phrase) + '</span> '
|
|
150
|
+
+ '<button class="term" data-term="' + escapeHtml(r.o) + '">' + escapeHtml(r.o) + '</button>'
|
|
151
|
+
+ '</li>';
|
|
152
|
+
}).join("") || (grounded
|
|
153
|
+
? '<li class="row muted">nothing in this graph relates to ' + escapeHtml(focus) + '.</li>'
|
|
154
|
+
: '<li class="row muted">no edges in this graph.</li>');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function hintsHtml(hints) {
|
|
158
|
+
return hints.map(function (h) {
|
|
159
|
+
return '<button class="hint" data-q="' + escapeHtml(h.text) + '" title="' + escapeHtml(h.rationale) + '">'
|
|
160
|
+
+ escapeHtml(h.text) + '</button>';
|
|
161
|
+
}).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function turnHtml(role, text) {
|
|
165
|
+
return '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + escapeHtml(text) + '</span>';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function chatEmptyStateHtml(hasEngine) {
|
|
169
|
+
return hasEngine
|
|
170
|
+
? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
|
|
171
|
+
+ '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
|
|
172
|
+
+ '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
|
|
173
|
+
: '<p class="chat-empty-head">Static view</p>'
|
|
174
|
+
+ '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function mbText(n) { return (n / 1048576).toFixed(1); }
|
|
178
|
+
|
|
179
|
+
/** How many of the seed's own individuals are facts — the general-knowledge
|
|
180
|
+
* half of the fact-total pill. */
|
|
181
|
+
export function seedFactsFromPayload(payload) {
|
|
182
|
+
return (payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function seedLoadingNote(loadedBytes, totalBytes) {
|
|
186
|
+
return "loading general knowledge… " + mbText(loadedBytes) + (totalBytes ? " of " + mbText(totalBytes) : "") + " MB";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** One retry with a cache-busting query param, fetch side only: a CDN edge
|
|
190
|
+
* can serve a corrupted or truncated precompressed response (a transient
|
|
191
|
+
* bad cache entry, not a code defect — real bytes decompress fine, and the
|
|
192
|
+
* same URL fetched moments later is clean), and JSON.parse throwing is the
|
|
193
|
+
* only signal of that. `fetcher` is `fetchWithProgress` (or a stand-in for
|
|
194
|
+
* a test); `baseUrl` already carries the build's own content-hash query
|
|
195
|
+
* string, if any, so the bust param joins with `&` or `?` accordingly. */
|
|
196
|
+
export async function fetchSeedPayloadWithRetry(fetcher, baseUrl, seedQuery, onProgress) {
|
|
197
|
+
var lastErr = null;
|
|
198
|
+
for (var attempt = 1; attempt <= 2; attempt++) {
|
|
199
|
+
try {
|
|
200
|
+
var bust = attempt === 1 ? "" : (seedQuery ? "&" : "?") + "retry=1";
|
|
201
|
+
var seedBlob = await fetcher(baseUrl + seedQuery + bust, onProgress);
|
|
202
|
+
var text = await seedBlob.text();
|
|
203
|
+
return JSON.parse(text); // throwing here is the corrupted/truncated-response retry signal
|
|
204
|
+
} catch (e) {
|
|
205
|
+
lastErr = e;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw lastErr;
|
|
209
|
+
}
|
|
210
|
+
|
|
110
211
|
const CLIENT_JS = String.raw`
|
|
111
212
|
(function () {
|
|
112
213
|
var DATA = window.__CODE_EXPLORER__;
|
|
113
|
-
var api = window.
|
|
214
|
+
var api = window.tmct ? window.tmct.page : null;
|
|
114
215
|
var els = {
|
|
115
216
|
focus: document.getElementById("focus-name"),
|
|
116
217
|
ledger: document.getElementById("ledger"),
|
|
@@ -126,13 +227,17 @@ const CLIENT_JS = String.raw`
|
|
|
126
227
|
};
|
|
127
228
|
var session = null;
|
|
128
229
|
|
|
129
|
-
var
|
|
230
|
+
var escapeHtml = ${escapeHtml.toString()};
|
|
231
|
+
var rowKey = ${rowKey.toString()};
|
|
232
|
+
var statsSummaryLine = ${statsSummaryLine.toString()};
|
|
233
|
+
var factTotalText = ${factTotalText.toString()};
|
|
234
|
+
var focusRowsHtml = ${focusRowsHtml.toString()};
|
|
235
|
+
var hintsHtml = ${hintsHtml.toString()};
|
|
236
|
+
var turnHtml = ${turnHtml.toString()};
|
|
237
|
+
var chatEmptyStateHtml = ${chatEmptyStateHtml.toString()};
|
|
130
238
|
|
|
131
239
|
function renderStats(data) {
|
|
132
|
-
|
|
133
|
-
var parts = [s.individuals + " individuals", s.edges + " edges"];
|
|
134
|
-
var cls = s.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
|
|
135
|
-
els.stats.textContent = parts.concat(cls).join(" · ");
|
|
240
|
+
els.stats.textContent = statsSummaryLine(data.ledger.stats);
|
|
136
241
|
renderFactTotal(data);
|
|
137
242
|
}
|
|
138
243
|
|
|
@@ -141,7 +246,7 @@ const CLIENT_JS = String.raw`
|
|
|
141
246
|
function renderFactTotal(data) {
|
|
142
247
|
if (!els.factTotal) return;
|
|
143
248
|
var edges = (data && data.ledger && data.ledger.stats && data.ledger.stats.edges) || 0;
|
|
144
|
-
els.factTotal.textContent = (edges
|
|
249
|
+
els.factTotal.textContent = factTotalText(edges, seedState.facts);
|
|
145
250
|
}
|
|
146
251
|
|
|
147
252
|
// "What relates to the focus", put to the engine. askRelatedFacts asks
|
|
@@ -159,41 +264,19 @@ const CLIENT_JS = String.raw`
|
|
|
159
264
|
}
|
|
160
265
|
}
|
|
161
266
|
|
|
162
|
-
|
|
163
|
-
|
|
267
|
+
// The engine's answer whenever it grounded one. The row list's own split is
|
|
268
|
+
// what is left when there is no engine (the static page) or when every
|
|
269
|
+
// question came back parsed as something else — an identifier that is
|
|
270
|
+
// itself a relation verb reads as a question about the verb. Whatever the
|
|
271
|
+
// neighbourhood did not already name follows, in degree order: a bulk view
|
|
272
|
+
// of the rest of the graph, which is a fold and not a question.
|
|
164
273
|
function renderFocusRows(data, related) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// The engine's answer whenever it grounded one. The row list's own split is
|
|
168
|
-
// what is left when there is no engine (the static page) or when every
|
|
169
|
-
// question came back parsed as something else — an identifier that is
|
|
170
|
-
// itself a relation verb reads as a question about the verb.
|
|
171
|
-
var grounded = Boolean(related && related.grounded);
|
|
172
|
-
var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
|
|
173
|
-
var nearKeys = {};
|
|
174
|
-
near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
|
|
175
|
-
// Whatever the neighbourhood did not already name, in degree order: a bulk
|
|
176
|
-
// view of the rest of the graph, which is a fold and not a question.
|
|
177
|
-
var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
|
|
178
|
-
var ordered = near.concat(rest);
|
|
179
|
-
els.ledger.innerHTML = ordered.map(function (r) {
|
|
180
|
-
var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
|
|
181
|
-
return '<li class="row' + hot + '">'
|
|
182
|
-
+ '<button class="term" data-term="' + esc(r.s) + '">' + esc(r.s) + '</button> '
|
|
183
|
-
+ '<span class="verb">' + esc(r.phrase) + '</span> '
|
|
184
|
-
+ '<button class="term" data-term="' + esc(r.o) + '">' + esc(r.o) + '</button>'
|
|
185
|
-
+ '</li>';
|
|
186
|
-
}).join("") || (grounded
|
|
187
|
-
? '<li class="row muted">nothing in this graph relates to ' + esc(focus) + '.</li>'
|
|
188
|
-
: '<li class="row muted">no edges in this graph.</li>');
|
|
189
|
-
els.focus.textContent = focus || "—";
|
|
274
|
+
els.ledger.innerHTML = focusRowsHtml(data, related);
|
|
275
|
+
els.focus.textContent = data.focus || "—";
|
|
190
276
|
}
|
|
191
277
|
|
|
192
278
|
function renderHints(data) {
|
|
193
|
-
els.hints.innerHTML = data.hints
|
|
194
|
-
return '<button class="hint" data-q="' + esc(h.text) + '" title="' + esc(h.rationale) + '">'
|
|
195
|
-
+ esc(h.text) + '</button>';
|
|
196
|
-
}).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
|
|
279
|
+
els.hints.innerHTML = hintsHtml(data.hints);
|
|
197
280
|
}
|
|
198
281
|
|
|
199
282
|
function focusOn(term) {
|
|
@@ -218,7 +301,7 @@ const CLIENT_JS = String.raw`
|
|
|
218
301
|
clearEmptyState();
|
|
219
302
|
var div = document.createElement("div");
|
|
220
303
|
div.className = "turn turn-" + role;
|
|
221
|
-
div.innerHTML =
|
|
304
|
+
div.innerHTML = turnHtml(role, text);
|
|
222
305
|
els.log.appendChild(div);
|
|
223
306
|
els.log.scrollTop = els.log.scrollHeight;
|
|
224
307
|
}
|
|
@@ -227,12 +310,7 @@ const CLIENT_JS = String.raw`
|
|
|
227
310
|
var div = document.createElement("div");
|
|
228
311
|
div.className = "chat-empty";
|
|
229
312
|
div.id = "chat-empty";
|
|
230
|
-
div.innerHTML = api
|
|
231
|
-
? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
|
|
232
|
-
+ '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
|
|
233
|
-
+ '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
|
|
234
|
-
: '<p class="chat-empty-head">Static view</p>'
|
|
235
|
-
+ '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
|
|
313
|
+
div.innerHTML = chatEmptyStateHtml(Boolean(api));
|
|
236
314
|
els.log.appendChild(div);
|
|
237
315
|
}
|
|
238
316
|
|
|
@@ -248,25 +326,34 @@ const CLIENT_JS = String.raw`
|
|
|
248
326
|
var SEED_QUERY = DATA.seedStamp ? "?b=" + DATA.seedStamp : "";
|
|
249
327
|
var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
|
|
250
328
|
function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
|
|
251
|
-
function mbText(n) { return (n / 1048576).toFixed(1); }
|
|
252
329
|
|
|
330
|
+
var mbText = ${mbText.toString()};
|
|
253
331
|
var fetchWithProgress = ${fetchWithProgress.toString()};
|
|
332
|
+
var seedFactsFromPayload = ${seedFactsFromPayload.toString()};
|
|
333
|
+
var seedLoadingNote = ${seedLoadingNote.toString()};
|
|
334
|
+
var fetchSeedPayloadWithRetry = ${fetchSeedPayloadWithRetry.toString()};
|
|
335
|
+
|
|
336
|
+
// The desktop shell's readSeed() reads off local disk, not a CDN, so it
|
|
337
|
+
// keeps its single attempt in loadSeed() below; only this fetch path retries.
|
|
338
|
+
function fetchSeedPayload() {
|
|
339
|
+
return fetchSeedPayloadWithRetry(fetchWithProgress, "./chat-seed.json", SEED_QUERY, function (loaded, total) {
|
|
340
|
+
seedNote(seedLoadingNote(loaded, total));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
254
343
|
|
|
255
344
|
async function loadSeed() {
|
|
256
345
|
try {
|
|
257
|
-
var
|
|
346
|
+
var payload = null;
|
|
258
347
|
if (window.tmctDesktop && typeof window.tmctDesktop.readSeed === "function") {
|
|
259
348
|
seedNote("loading general knowledge…");
|
|
260
|
-
text = await window.tmctDesktop.readSeed();
|
|
349
|
+
var text = await window.tmctDesktop.readSeed();
|
|
350
|
+
if (text) payload = JSON.parse(text);
|
|
261
351
|
} else {
|
|
262
|
-
|
|
263
|
-
seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
|
|
264
|
-
});
|
|
265
|
-
text = await seedBlob.text();
|
|
352
|
+
payload = await fetchSeedPayload();
|
|
266
353
|
}
|
|
267
|
-
if (
|
|
268
|
-
seedState.payload =
|
|
269
|
-
seedState.facts = (seedState.payload
|
|
354
|
+
if (payload) {
|
|
355
|
+
seedState.payload = payload;
|
|
356
|
+
seedState.facts = seedFactsFromPayload(seedState.payload);
|
|
270
357
|
seedState.status = "ready";
|
|
271
358
|
seedNote("general knowledge: " + seedState.facts + " facts");
|
|
272
359
|
renderFactTotal(DATA);
|
package/src/services/index.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export { relationKind, impactClosure } from "../domain/codegraph.mjs";
|
|
|
39
39
|
export { createGraphService } from "../adapters/providers/graph-service.mjs";
|
|
40
40
|
|
|
41
41
|
// Tool dispatch (slash-commands and CLI tool calls route through here).
|
|
42
|
-
export { dispatchTool } from "../tools/server.mjs";
|
|
42
|
+
export { dispatchTool, dispatchToolStructured } from "../tools/server.mjs";
|
|
43
43
|
|
|
44
44
|
// Conversational memory — tmct's OWN OWL-labelled graph under
|
|
45
45
|
// .tmct/memory/, distinct from any provider-supplied code graph.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// self-contained document shaped exactly like chat-page-viz.mjs's own
|
|
3
3
|
// page-builder — one inlined <style> importing viz-theme.mjs's shared tokens,
|
|
4
4
|
// behaviour as an inlined IIFE — running the ingest engine
|
|
5
|
-
// (ingest-browser.bundle.js's globalThis.
|
|
5
|
+
// (ingest-browser.bundle.js's globalThis.tmct) by same-origin relative
|
|
6
6
|
// paths.
|
|
7
7
|
//
|
|
8
8
|
// The page's own chrome is a two-pane translate-tool layout: mode pills
|
|
@@ -113,7 +113,8 @@ ${THEME_TOKENS_CSS}
|
|
|
113
113
|
.browse { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .16rem .55rem; background: var(--card); }
|
|
114
114
|
.browse:hover { color: var(--ink); }
|
|
115
115
|
|
|
116
|
-
/* right: the canonical facts, on the soft panel background
|
|
116
|
+
/* right: the canonical facts, on the soft panel background, with the ask
|
|
117
|
+
dock pinned under them — you read what landed, then question it in place. */
|
|
117
118
|
.outPane { background: var(--card); }
|
|
118
119
|
#facts { flex: 1 1 auto; overflow-y: auto; padding: .6rem .9rem 1rem; font-family: ${MONO_STACK}; font-size: .8rem; }
|
|
119
120
|
#facts .fact { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: baseline; padding: .3rem 0; border-bottom: 1px solid var(--line); }
|
|
@@ -123,6 +124,24 @@ ${THEME_TOKENS_CSS}
|
|
|
123
124
|
#facts .fact .prov { grid-column: 1 / -1; color: var(--muted); font-size: .66rem; }
|
|
124
125
|
#facts .empty { color: var(--muted); text-align: center; max-width: 24rem; line-height: 1.6; margin: 3rem auto 0; }
|
|
125
126
|
|
|
127
|
+
/* the ask dock: one line in, one answer out, against the graph this session
|
|
128
|
+
projects from what it has ingested. */
|
|
129
|
+
.askDock { flex: 0 0 auto; border-top: 1px solid var(--line); display: flex; flex-direction: column; }
|
|
130
|
+
#askLog { max-height: 11rem; overflow-y: auto; padding: .5rem .9rem 0; font-family: ${MONO_STACK}; font-size: .78rem; }
|
|
131
|
+
#askLog:empty { display: none; }
|
|
132
|
+
.askLine { margin: 0 0 .35rem; white-space: pre-wrap; word-break: break-word; }
|
|
133
|
+
.askLine.q { color: var(--muted); }
|
|
134
|
+
.askLine.q::before { content: "> "; }
|
|
135
|
+
.askLine.a { color: var(--ink); }
|
|
136
|
+
.askLine.detail { color: var(--muted); font-size: .68rem; }
|
|
137
|
+
.askLine.miss { color: var(--muted); font-style: italic; }
|
|
138
|
+
.askRow { display: flex; align-items: center; gap: .5rem; padding: .5rem .9rem .6rem; }
|
|
139
|
+
#askq { flex: 1 1 auto; min-width: 0; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); font-family: ${MONO_STACK}; font-size: .78rem; padding: .35rem .6rem; }
|
|
140
|
+
#askq::placeholder { color: var(--muted); }
|
|
141
|
+
#askq:disabled { opacity: .55; }
|
|
142
|
+
.askGo { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .85rem; background: var(--card); }
|
|
143
|
+
.askGo:disabled { opacity: .45; cursor: default; }
|
|
144
|
+
|
|
126
145
|
.actions { flex: 0 0 auto; display: flex; align-items: center; gap: .6rem; padding: .6rem 1.1rem; border-top: 1px solid var(--line); flex-wrap: wrap; }
|
|
127
146
|
.actions .btn { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .35rem .9rem; background: var(--card); }
|
|
128
147
|
.actions .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
@@ -133,7 +152,7 @@ ${THEME_TOKENS_CSS}
|
|
|
133
152
|
the right of the ingest column (a real layout column, not an overlay) —
|
|
134
153
|
the same class names and breakpoint chat-page-viz.mjs's own docked panel
|
|
135
154
|
uses, re-rendered after boot and after every ingest from
|
|
136
|
-
window.
|
|
155
|
+
window.tmct's own memoryStats(). */
|
|
137
156
|
.statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; display: flex; flex-direction: column; }
|
|
138
157
|
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
139
158
|
.statsPanel h2:first-child { margin-top: 0; }
|
|
@@ -203,6 +222,14 @@ ${THEME_TOKENS_CSS}
|
|
|
203
222
|
<span id="factCount" class="mono"></span>
|
|
204
223
|
</div>
|
|
205
224
|
<div id="facts"><p class="empty">Nothing ingested yet. The facts it grounds will appear here as it reads.</p></div>
|
|
225
|
+
<form class="askDock" id="askForm" autocomplete="off">
|
|
226
|
+
<div id="askLog" aria-live="polite"></div>
|
|
227
|
+
<div class="askRow">
|
|
228
|
+
<input type="text" id="askq" aria-label="Ask about what you have ingested"
|
|
229
|
+
placeholder="ask about what you’ve ingested…" disabled>
|
|
230
|
+
<button type="submit" class="askGo" id="askGo" disabled>ask</button>
|
|
231
|
+
</div>
|
|
232
|
+
</form>
|
|
206
233
|
</section>
|
|
207
234
|
</main>
|
|
208
235
|
<div class="actions">
|
|
@@ -248,6 +275,10 @@ ${THEME_TOKENS_CSS}
|
|
|
248
275
|
const fuzzyToggleEl = el("fuzzyToggle");
|
|
249
276
|
const statsPanelEl = el("statsPanel");
|
|
250
277
|
const factPillValueEl = el("factPillValue");
|
|
278
|
+
const askFormEl = el("askForm");
|
|
279
|
+
const askInputEl = el("askq");
|
|
280
|
+
const askGoBtn = el("askGo");
|
|
281
|
+
const askLogEl = el("askLog");
|
|
251
282
|
|
|
252
283
|
let session = null;
|
|
253
284
|
let grounded = 0; // facts on show in the right pane, from the CURRENT ingest only
|
|
@@ -356,11 +387,86 @@ ${THEME_TOKENS_CSS}
|
|
|
356
387
|
factsEl.scrollTop = factsEl.scrollHeight;
|
|
357
388
|
}
|
|
358
389
|
|
|
390
|
+
// ---- the ask dock: one question, put to what this session has ingested ---
|
|
391
|
+
// Two real routes, tried in order, and neither one guesses. window.tmct.ask
|
|
392
|
+
// puts the question to the graph this session projects from its OWN grounded
|
|
393
|
+
// rows (ingest-facts.mjs), which is what answers a listing — "list dogs"
|
|
394
|
+
// reads the beagles actually on record. On a miss, window.tmct.turn runs the
|
|
395
|
+
// full chat turn engine over the same store, which reads a term's own facts
|
|
396
|
+
// back ("what is a beagle") and takes a taught line ("remember: …") the
|
|
397
|
+
// graph shape has no lane for. Both missing is the honest wall, and the note
|
|
398
|
+
// there names the kinds this memory really holds instead of inventing an
|
|
399
|
+
// example question.
|
|
400
|
+
let asking = false;
|
|
401
|
+
|
|
402
|
+
function updateAskEnabled() {
|
|
403
|
+
const ready = Boolean(session) && !asking;
|
|
404
|
+
askInputEl.disabled = !ready;
|
|
405
|
+
askGoBtn.disabled = !ready || !askInputEl.value.trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function addAskLine(cls, text) {
|
|
409
|
+
const line = document.createElement("div");
|
|
410
|
+
line.className = "askLine " + cls;
|
|
411
|
+
line.textContent = text;
|
|
412
|
+
askLogEl.appendChild(line);
|
|
413
|
+
askLogEl.scrollTop = askLogEl.scrollHeight;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// The engine writes its answer first and its reasoning trailers ("Goal
|
|
417
|
+
// (inferred): …", "Canonical: …") after a blank line. The answer leads; the
|
|
418
|
+
// trailers stay, quieter, because they are how a reader audits it.
|
|
419
|
+
function addAskAnswer(answer) {
|
|
420
|
+
const blocks = String(answer).split(/\\n{2,}/).map(function (b) { return b.trim(); }).filter(Boolean);
|
|
421
|
+
if (!blocks.length) return;
|
|
422
|
+
addAskLine("a", blocks[0]);
|
|
423
|
+
for (const block of blocks.slice(1)) addAskLine("detail", block);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function askMissNote() {
|
|
427
|
+
const kinds = session && session.askableClasses ? session.askableClasses() : [];
|
|
428
|
+
if (!kinds.length) return "Nothing of your own is in this session's memory yet. Ingest some text first.";
|
|
429
|
+
return "I can't ground that in what you've ingested. The kinds it holds: " + kinds.slice(0, 8).join(", ") + ".";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
askInputEl.addEventListener("input", updateAskEnabled);
|
|
433
|
+
askFormEl.addEventListener("submit", async (e) => {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
const q = askInputEl.value.trim();
|
|
436
|
+
if (!q || asking || !session) return;
|
|
437
|
+
asking = true;
|
|
438
|
+
askInputEl.value = "";
|
|
439
|
+
updateAskEnabled();
|
|
440
|
+
addAskLine("q", q);
|
|
441
|
+
try {
|
|
442
|
+
let asked = null;
|
|
443
|
+
try { asked = await window.tmct.ask(q); } catch { asked = null; }
|
|
444
|
+
if (asked && asked.answer && !asked.miss) { addAskAnswer(asked.answer); return; }
|
|
445
|
+
let turned = null;
|
|
446
|
+
try { turned = await window.tmct.turn(q); } catch { turned = null; }
|
|
447
|
+
if (turned && turned.answer && !(turned.record && turned.record.miss)) {
|
|
448
|
+
addAskAnswer(turned.answer);
|
|
449
|
+
// A taught line writes to the same store an ingest does, so it earns
|
|
450
|
+
// the same debounced save and the same panel refresh.
|
|
451
|
+
if (turned.record && turned.record.via === "assert") {
|
|
452
|
+
scheduleSave();
|
|
453
|
+
await renderStatsPanel();
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
addAskLine("miss", askMissNote());
|
|
458
|
+
} finally {
|
|
459
|
+
asking = false;
|
|
460
|
+
updateAskEnabled();
|
|
461
|
+
askInputEl.focus();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
359
465
|
// ---- memory stats: the docked panel, same convention as chat.html --------
|
|
360
466
|
async function renderStatsPanel(stats) {
|
|
361
467
|
if (!stats) {
|
|
362
|
-
if (!session || !window.
|
|
363
|
-
try { stats = await window.
|
|
468
|
+
if (!session || !window.tmct.page.memoryStats) return;
|
|
469
|
+
try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
|
|
364
470
|
catch { return; }
|
|
365
471
|
}
|
|
366
472
|
factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
|
|
@@ -402,26 +508,38 @@ ${THEME_TOKENS_CSS}
|
|
|
402
508
|
// The one branch this page's seed choice makes: checked, fetch and parse
|
|
403
509
|
// the same chat-seed.json chat.html embeds; unchecked, skip the request
|
|
404
510
|
// outright and stay on the previous empty-store fast path.
|
|
511
|
+
//
|
|
512
|
+
// One retry with a cache-busting query param: a CDN edge can serve a
|
|
513
|
+
// corrupted or truncated precompressed response (a transient bad cache
|
|
514
|
+
// entry, not a code defect — real bytes decompress fine, and the same URL
|
|
515
|
+
// fetched moments later is clean), and JSON.parse throwing is the only
|
|
516
|
+
// signal of that. The bust param forces a fresh fetch past that one entry.
|
|
405
517
|
async function fetchSeedIfWanted() {
|
|
406
518
|
if (!seedToggleEl.checked) { seedPayload = null; seedFacts = 0; return; }
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
519
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
520
|
+
try {
|
|
521
|
+
const bust = attempt === 1 ? "" : (SEED_QUERY ? "&" : "?") + "retry=1";
|
|
522
|
+
const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY + bust, (loaded, total) => noteProgress("seed", loaded, total));
|
|
523
|
+
seedPayload = JSON.parse(await blob.text());
|
|
524
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
525
|
+
return;
|
|
526
|
+
} catch (err) {
|
|
527
|
+
if (attempt === 2) {
|
|
528
|
+
seedPayload = null;
|
|
529
|
+
seedFacts = 0;
|
|
530
|
+
console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
415
533
|
}
|
|
416
534
|
}
|
|
417
535
|
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
418
|
-
function newSession() {
|
|
419
|
-
return window.
|
|
536
|
+
async function newSession() {
|
|
537
|
+
return window.tmct.open({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload) });
|
|
420
538
|
}
|
|
421
539
|
|
|
422
540
|
// ---- engine boot ---------------------------------------------------------
|
|
423
541
|
const loadWinkVendor = ${loadWinkVendor.toString()};
|
|
424
|
-
const tryLoadWink = loadWinkVendor({ register: (factory) => window.
|
|
542
|
+
const tryLoadWink = loadWinkVendor({ register: (factory) => window.tmct.page.registerWinkModel(factory) });
|
|
425
543
|
|
|
426
544
|
// The deploy's own version, read off the service worker file the build
|
|
427
545
|
// already stamps — the only same-origin place the number exists at runtime
|
|
@@ -467,10 +585,12 @@ ${THEME_TOKENS_CSS}
|
|
|
467
585
|
clearTimeout(saveTimer);
|
|
468
586
|
saveTimer = null;
|
|
469
587
|
if (persist) await persist.clear();
|
|
470
|
-
session = newSession();
|
|
588
|
+
session = await newSession();
|
|
471
589
|
clearFactsPane();
|
|
590
|
+
askLogEl.textContent = "";
|
|
472
591
|
updateIngestEnabled();
|
|
473
|
-
|
|
592
|
+
updateAskEnabled();
|
|
593
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
474
594
|
statusEl.textContent = "forgot everything taught on this device. Back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
|
|
475
595
|
await renderStatsPanel(stats);
|
|
476
596
|
}
|
|
@@ -486,17 +606,19 @@ ${THEME_TOKENS_CSS}
|
|
|
486
606
|
await fetchSeedIfWanted();
|
|
487
607
|
clearTimeout(saveTimer);
|
|
488
608
|
saveTimer = null;
|
|
489
|
-
session = newSession();
|
|
609
|
+
session = await newSession();
|
|
490
610
|
clearFactsPane();
|
|
491
|
-
|
|
611
|
+
askLogEl.textContent = "";
|
|
612
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
492
613
|
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
|
|
493
614
|
await renderStatsPanel(stats);
|
|
494
615
|
updateIngestEnabled();
|
|
616
|
+
updateAskEnabled();
|
|
495
617
|
sourceEl.focus();
|
|
496
618
|
});
|
|
497
619
|
|
|
498
620
|
async function boot() {
|
|
499
|
-
if (!window.
|
|
621
|
+
if (!window.tmct) {
|
|
500
622
|
statusEl.textContent = "the ingest engine didn't load. This page needs its build step (npm run demo:build)";
|
|
501
623
|
return;
|
|
502
624
|
}
|
|
@@ -507,16 +629,17 @@ ${THEME_TOKENS_CSS}
|
|
|
507
629
|
fetchSiteVersion().then((v) => { siteVersion = v; }),
|
|
508
630
|
]);
|
|
509
631
|
progressActive = false;
|
|
510
|
-
if (window.
|
|
511
|
-
persist = window.
|
|
632
|
+
if (window.tmct.page.openPersistedStore) {
|
|
633
|
+
persist = window.tmct.page.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
|
|
512
634
|
}
|
|
513
635
|
const savedRecord = persist ? await persist.load() : null;
|
|
514
636
|
session = savedRecord && savedRecord.payload
|
|
515
|
-
? window.
|
|
516
|
-
: newSession();
|
|
637
|
+
? await window.tmct.open({ seedPayload: savedRecord.payload, vocabSeeded: true })
|
|
638
|
+
: await newSession();
|
|
517
639
|
setMode(false);
|
|
518
640
|
updateIngestEnabled();
|
|
519
|
-
|
|
641
|
+
updateAskEnabled();
|
|
642
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
520
643
|
const winkPart = winkStatus === "loaded"
|
|
521
644
|
? "wink-nlp: loaded"
|
|
522
645
|
: "wink-nlp unavailable. The recognizer can't split sentences without it";
|
|
@@ -569,10 +692,10 @@ ${THEME_TOKENS_CSS}
|
|
|
569
692
|
|
|
570
693
|
// ---- download the canonical facts as JSONL -------------------------------
|
|
571
694
|
downloadBtn.addEventListener("click", async () => {
|
|
572
|
-
if (!session || !window.
|
|
695
|
+
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
573
696
|
let jsonl;
|
|
574
697
|
try {
|
|
575
|
-
jsonl = await window.
|
|
698
|
+
jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
|
|
576
699
|
} catch (err) {
|
|
577
700
|
statusEl.textContent = "couldn't build the download (" + (err && err.message ? err.message : err) + ")";
|
|
578
701
|
return;
|
|
@@ -609,6 +732,7 @@ ${THEME_TOKENS_CSS}
|
|
|
609
732
|
sourceTag = "pasted text";
|
|
610
733
|
srcLabel.textContent = modeDocBtn.getAttribute("aria-pressed") === "true" ? "drop or browse for a file" : "pasted text";
|
|
611
734
|
clearFactsPane();
|
|
735
|
+
askLogEl.textContent = "";
|
|
612
736
|
statusEl.textContent = "cleared";
|
|
613
737
|
updateIngestEnabled();
|
|
614
738
|
sourceEl.focus();
|
|
@@ -618,6 +742,7 @@ ${THEME_TOKENS_CSS}
|
|
|
618
742
|
console.error("tmct ingest failed to boot", err);
|
|
619
743
|
statusEl.textContent = "the ingest page failed to start (" + (err && err.message ? err.message : err) + ")";
|
|
620
744
|
});
|
|
745
|
+
window.tmct.ready = window.tmctIngestReady;
|
|
621
746
|
})();
|
|
622
747
|
</script>
|
|
623
748
|
</body>
|