@polycode-projects/the-mechanical-code-talker 1.11.0 → 1.11.5
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 +13 -20
- package/ROADMAP.md +9 -12
- package/bin/tmct.mjs +82 -98
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +962 -51
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +85 -6
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +31 -9
- package/src/memory/core.mjs +20 -1
- package/src/memory/shacl.mjs +4 -1
- package/src/memory-ask-browser-entry.mjs +5 -5
- package/src/memory-ask-browser.bundle.js +526 -47
- package/src/plan-viz.mjs +5 -6
- package/src/router/drive.mjs +122 -13
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +19 -8
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +19 -3
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5142
- package/src/viz.mjs +0 -959
package/src/viz.mjs
DELETED
|
@@ -1,959 +0,0 @@
|
|
|
1
|
-
// viz.mjs — `tmct viz`: a real, navigable, self-contained HTML graph view over
|
|
2
|
-
// the memory graph, with a real "Ask the graph" chat panel running tmct's OWN
|
|
3
|
-
// engine client-side (adapted to bundle tmct's ask.mjs directly rather than
|
|
4
|
-
// an external package import).
|
|
5
|
-
//
|
|
6
|
-
// Three pure/impure-separated pieces, mirroring src/syllogise.mjs's shape:
|
|
7
|
-
// - computeVizGraph(repoDir, {focus}) — I/O (loadMemory) + graph traversal,
|
|
8
|
-
// reusing spiralExpand/mostRecentIndividual/MEMORY_SPIRAL_EXPAND_KINDS/
|
|
9
|
-
// buildVizNodesAndEdges. No new traversal logic here.
|
|
10
|
-
// - renderVizHtml({nodes, edges, focus, payload, askBundle}) — a pure
|
|
11
|
-
// string-builder: one complete <!doctype html> document, graph data
|
|
12
|
-
// JSON-embedded inline, the real ask-engine bundle inlined verbatim, no
|
|
13
|
-
// external <script src>, no CDN, no fonts — opens and works offline.
|
|
14
|
-
// - readAskBundle() — the one bit of I/O renderVizHtml itself doesn't do:
|
|
15
|
-
// reads the checked-in build artifact (scripts/build-ask-bundle.mjs's
|
|
16
|
-
// output). bin/tmct.mjs's `viz` mode wires all three together.
|
|
17
|
-
|
|
18
|
-
import { loadMemory, CREATED_AT_PROP, normFactTerm } from "./memory/core.mjs";
|
|
19
|
-
import {
|
|
20
|
-
parseEntities, spiralExpand, mostRecentIndividual, MEMORY_SPIRAL_EXPAND_KINDS, MEMORY_FACT_LINK_KINDS,
|
|
21
|
-
buildVizNodesAndEdges, deriveFactTermGraph, pickLegendDimension, edgeKindsFor,
|
|
22
|
-
} from "./codegraph.mjs";
|
|
23
|
-
import { readFile } from "node:fs/promises";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { dirname, join } from "node:path";
|
|
26
|
-
|
|
27
|
-
// The page-size strategy: a three-cap default (200 base, tuned up to 300 here),
|
|
28
|
-
// raised from the code-graph-only SPIRAL_NODE_LIMIT_DEFAULT (12 — a MID-tier
|
|
29
|
-
// digest breadth, not a graph-viewer breadth) now that real concept-relation
|
|
30
|
-
// edges are walkable.
|
|
31
|
-
export const VIZ_NODE_LIMIT_DEFAULT = 300;
|
|
32
|
-
export const VIZ_HUB_DEGREE_DEFAULT = 40;
|
|
33
|
-
export const VIZ_DEPTH_DEFAULT = 3; // mirrors spiralExpand's own SPIRAL_DEPTH_DEFAULT
|
|
34
|
-
|
|
35
|
-
// edgeKindsFor lives in codegraph.mjs; re-exported here so the browser bundle's
|
|
36
|
-
// client-side re-walk (which can't import this fs-touching module) can share it.
|
|
37
|
-
export { edgeKindsFor };
|
|
38
|
-
|
|
39
|
-
/** Load the memory graph under `repoDir`, walk it from a seed, and enrich each walked node
|
|
40
|
-
* with the real label/class/timestamp data a renderer needs.
|
|
41
|
-
* Seed precedence: `--focus <id>`, then `--term <word>` (via deriveFactTermGraph's
|
|
42
|
-
* synthetic `term:<word>` node, reaching the term's whole concept neighbourhood), then
|
|
43
|
-
* `mostRecentIndividual`.
|
|
44
|
-
* `edgeKindMode`: "both" (default) walks meta/provenance kinds AND real concept-relation
|
|
45
|
-
* kinds together; "meta" is provenance-only; "relation" isolates the concept view.
|
|
46
|
-
* `hubDegree`: stop expanding THROUGH a node with more connections than this (still shows
|
|
47
|
-
* the hub itself), so a common hypernym can't swallow the whole node budget in one hop.
|
|
48
|
-
* Returns `{nodes, edges, focus, payload, legend}` — `payload` is the FULL raw graph (the
|
|
49
|
-
* "Ask the graph" panel can re-walk from a new focus over it). Never throws on a
|
|
50
|
-
* missing/empty memory dir. */
|
|
51
|
-
export async function computeVizGraph(repoDir, { focus, term, depth, nodeLimit, hubDegree, edgeKindMode = "both" } = {}) {
|
|
52
|
-
const payload = await loadMemory(repoDir);
|
|
53
|
-
const graph = parseEntities(payload);
|
|
54
|
-
// Always resolved (defaults filled in), so renderVizHtml can embed it for a
|
|
55
|
-
// client-side re-walk to stay consistent with how this page was generated.
|
|
56
|
-
const walkOpts = {
|
|
57
|
-
depth: depth != null ? depth : VIZ_DEPTH_DEFAULT,
|
|
58
|
-
nodeLimit: nodeLimit != null ? nodeLimit : VIZ_NODE_LIMIT_DEFAULT,
|
|
59
|
-
hubDegree: hubDegree != null ? hubDegree : VIZ_HUB_DEGREE_DEFAULT,
|
|
60
|
-
edgeKindMode,
|
|
61
|
-
};
|
|
62
|
-
if (!graph.individuals.length) return { nodes: [], edges: [], focus: null, payload, legend: null, walkOpts };
|
|
63
|
-
|
|
64
|
-
const { graph: augmented, factRelationKinds } = deriveFactTermGraph(graph);
|
|
65
|
-
|
|
66
|
-
let seedId = focus || null;
|
|
67
|
-
if (!seedId && term) {
|
|
68
|
-
const termId = `term:${normFactTerm(term)}`;
|
|
69
|
-
if (augmented.byId.has(termId)) seedId = termId;
|
|
70
|
-
// no match: falls through to the default seed below, exactly as if
|
|
71
|
-
// --term had never been given — an honest miss never seeds a phantom node.
|
|
72
|
-
}
|
|
73
|
-
if (!seedId) seedId = mostRecentIndividual(graph, CREATED_AT_PROP)?.id || null;
|
|
74
|
-
if (!seedId) return { nodes: [], edges: [], focus: null, payload, legend: null, walkOpts };
|
|
75
|
-
|
|
76
|
-
const walked = spiralExpand(augmented, [], {
|
|
77
|
-
kinds: edgeKindsFor(edgeKindMode, factRelationKinds),
|
|
78
|
-
classPredicate: () => true,
|
|
79
|
-
idNormalizer: (id) => id,
|
|
80
|
-
seeds: [seedId],
|
|
81
|
-
depth: walkOpts.depth,
|
|
82
|
-
nodeLimit: walkOpts.nodeLimit,
|
|
83
|
-
hubDegree: walkOpts.hubDegree,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const { nodes, edges } = buildVizNodesAndEdges(augmented, walked);
|
|
87
|
-
const legend = pickLegendDimension(augmented, nodes);
|
|
88
|
-
return { nodes, edges, focus: seedId, payload, legend, walkOpts };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Read the checked-in browser ask-engine bundle (`src/ask-browser.bundle.js`). Returns
|
|
92
|
-
* `""`, never throws, if the bundle hasn't been built — renderVizHtml then renders a
|
|
93
|
-
* graph-only page with an honest "chat unavailable" note. */
|
|
94
|
-
export async function readAskBundle() {
|
|
95
|
-
try {
|
|
96
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
97
|
-
return await readFile(join(here, "ask-browser.bundle.js"), "utf8");
|
|
98
|
-
} catch {
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Read the checked-in browser memory-ask-engine bundle (`src/memory-ask-browser.bundle.js`)
|
|
104
|
-
* — the real memory-graph answer engine (chat.mjs's factAnswer). Same never-throws
|
|
105
|
-
* contract as readAskBundle(). */
|
|
106
|
-
export async function readMemoryAskBundle() {
|
|
107
|
-
try {
|
|
108
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
109
|
-
return await readFile(join(here, "memory-ask-browser.bundle.js"), "utf8");
|
|
110
|
-
} catch {
|
|
111
|
-
return "";
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Escape untrusted text for safe placement inside HTML content/attributes. */
|
|
116
|
-
export function escapeHtml(s) {
|
|
117
|
-
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** JSON-embed graph data into a `<script>` tag safely — escape `</` so a
|
|
121
|
-
* label/id containing "</script>" can't break out of the tag, and escape
|
|
122
|
-
* U+2028/U+2029 (valid in JSON strings, invalid unescaped in JS source). */
|
|
123
|
-
export function embedJson(value) {
|
|
124
|
-
return JSON.stringify(value)
|
|
125
|
-
.replace(/</g, "\\u003c")
|
|
126
|
-
.replace(/\u2028/g, "\\u2028")
|
|
127
|
-
.replace(/\u2029/g, "\\u2029");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Render one complete, self-contained `<!doctype html>` document for
|
|
131
|
-
* `{nodes, edges, focus, payload, askBundle}` (computeVizGraph's return shape, plus the
|
|
132
|
-
* ask-engine bundle text from readAskBundle()): graph data JSON-embedded inline, the real
|
|
133
|
-
* ask.mjs engine inlined verbatim, inline <style>, inline vanilla-JS implementing a
|
|
134
|
-
* concentric ring layout keyed on hop, pan/zoom, click-a-node details, visibility filters,
|
|
135
|
-
* and a real "Ask the graph" chat panel (focus-follows-answer). Pure string building — no
|
|
136
|
-
* fs/network, no external <script src>, no CDN, no fonts. */
|
|
137
|
-
export function renderVizHtml({ nodes, edges, focus, payload, askBundle, memoryAskBundle, legend, walkOpts }) {
|
|
138
|
-
const graphJson = embedJson({ nodes, edges, focus });
|
|
139
|
-
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
140
|
-
const legendJson = embedJson(legend || null);
|
|
141
|
-
const walkOptsJson = embedJson(walkOpts || { depth: VIZ_DEPTH_DEFAULT, nodeLimit: VIZ_NODE_LIMIT_DEFAULT, hubDegree: VIZ_HUB_DEGREE_DEFAULT, edgeKindMode: "both" });
|
|
142
|
-
const title = `tmct viz — ${nodes.length} node${nodes.length === 1 ? "" : "s"}${focus ? ` (seed: ${escapeHtml(focus)})` : ""}`;
|
|
143
|
-
const hasChat = Boolean(askBundle);
|
|
144
|
-
const hasMemChat = Boolean(memoryAskBundle);
|
|
145
|
-
const hasAnyChat = hasChat || hasMemChat;
|
|
146
|
-
|
|
147
|
-
return `<!doctype html>
|
|
148
|
-
<html lang="en">
|
|
149
|
-
<head>
|
|
150
|
-
<meta charset="utf-8">
|
|
151
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
152
|
-
<title>${escapeHtml(title)}</title>
|
|
153
|
-
<style>
|
|
154
|
-
:root { color-scheme: light dark; }
|
|
155
|
-
html, body { margin: 0; padding: 0; height: 100%; background: #0b0d12; color: #e7e9ee; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
|
|
156
|
-
#wrap { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
|
|
157
|
-
canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
|
|
158
|
-
canvas.grabbing { cursor: grabbing; }
|
|
159
|
-
#hud { position: absolute; top: 12px; left: 12px; max-width: 34ch; background: rgba(20,22,30,0.82); border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; padding: 10px 12px; font-size: 12.5px; line-height: 1.45; pointer-events: none; }
|
|
160
|
-
#hud b { color: #fff; }
|
|
161
|
-
#hud .muted { color: #9aa1b0; display: block; margin: 4px 0 8px; }
|
|
162
|
-
#hud button { pointer-events: auto; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2); color: #e7e9ee; border-radius: 5px; padding: 3px 9px; font-size: 12px; cursor: pointer; }
|
|
163
|
-
#hud button:hover { background: rgba(255,255,255,0.18); }
|
|
164
|
-
#controls { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; align-items: center; background: rgba(20,22,30,0.88); border: 1px solid rgba(255,255,255,0.14); border-radius: 8px; padding: 7px 12px; font-size: 12.5px; flex-wrap: wrap; max-width: min(86vw, 900px); }
|
|
165
|
-
#controls .grp { display: flex; align-items: center; gap: 5px; }
|
|
166
|
-
#controls button { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2); color: #e7e9ee; border-radius: 5px; width: 22px; height: 22px; line-height: 1; cursor: pointer; font-size: 13px; }
|
|
167
|
-
#controls button:hover:not(:disabled) { background: rgba(255,255,255,0.18); }
|
|
168
|
-
#controls button:disabled { opacity: 0.35; cursor: default; }
|
|
169
|
-
#controls .depthval { min-width: 1.4em; text-align: center; display: inline-block; }
|
|
170
|
-
#controls label.typechk { display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
|
|
171
|
-
#controls label.typechk:hover { background: rgba(255,255,255,0.08); }
|
|
172
|
-
#controls .swatch { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
173
|
-
#controls select, #controls input[type="number"], #controls input[type="text"].search { background: #14161e; color: #e7e9ee; border: 1px solid #2a2e42; border-radius: 5px; padding: 2px 5px; font: inherit; font-size: 12px; }
|
|
174
|
-
#controls input[type="number"] { width: 3.6em; }
|
|
175
|
-
#controls input[type="text"].search { width: 9em; }
|
|
176
|
-
#controls .sep { width: 1px; align-self: stretch; background: rgba(255,255,255,0.14); margin: 0 2px; }
|
|
177
|
-
#legend { position: absolute; bottom: 12px; left: 12px; display: none; flex-wrap: wrap; gap: 6px; align-items: center; background: rgba(20,22,30,0.88); border: 1px solid rgba(255,255,255,0.14); border-radius: 8px; padding: 6px 10px; font-size: 11.5px; max-width: min(60vw, 640px); }
|
|
178
|
-
#legend.show { display: flex; }
|
|
179
|
-
#legend select { background: #14161e; color: #e7e9ee; border: 1px solid #2a2e42; border-radius: 5px; padding: 1px 4px; font: inherit; font-size: 11px; }
|
|
180
|
-
#legend .chip { display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 2px 6px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); }
|
|
181
|
-
#legend .chip.off { opacity: 0.4; }
|
|
182
|
-
#legend .chip .swatch { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
|
183
|
-
#legend .chip .n { color: #9aa1b0; }
|
|
184
|
-
#panel { position: absolute; top: 12px; right: 404px; width: 280px; max-width: calc(100vw - 428px); background: rgba(20,22,30,0.92); border: 1px solid rgba(255,255,255,0.14); border-radius: 8px; padding: 12px 14px; font-size: 13px; line-height: 1.5; display: none; }
|
|
185
|
-
#panel.show { display: block; }
|
|
186
|
-
#panel h2 { margin: 0 0 6px; font-size: 14px; word-break: break-word; }
|
|
187
|
-
#panel dl { margin: 8px 0 0; }
|
|
188
|
-
#panel dt { color: #9aa1b0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 6px; }
|
|
189
|
-
#panel dd { margin: 0; word-break: break-word; }
|
|
190
|
-
#panel dd a, #panel dd button.lnk { color: #7aa2f7; text-decoration: none; cursor: pointer; background: none; border: none; padding: 0; font: inherit; }
|
|
191
|
-
#panel dd a:hover, #panel dd button.lnk:hover { text-decoration: underline; }
|
|
192
|
-
#panel .row { display: flex; gap: 8px; margin-top: 10px; }
|
|
193
|
-
#panel button.act { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.18); color: #e7e9ee; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
194
|
-
#panel button.act:hover { background: rgba(255,255,255,0.16); }
|
|
195
|
-
#empty { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; text-align: center; padding: 24px; }
|
|
196
|
-
#empty.show { display: flex; }
|
|
197
|
-
#empty div { max-width: 46ch; color: #9aa1b0; }
|
|
198
|
-
#empty b { color: #e7e9ee; }
|
|
199
|
-
#ask { position: absolute; top: 12px; bottom: 12px; right: 12px; width: 380px; max-width: calc(100vw - 24px); background: rgba(20,22,30,0.92); border: 1px solid rgba(255,255,255,0.14); border-radius: 8px; padding: 10px 12px; font-size: 12.5px; display: flex; flex-direction: column; }
|
|
200
|
-
#ask h3 { margin: 0 0 6px; font-size: 12.5px; color: #9aa1b0; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; flex: 0 0 auto; }
|
|
201
|
-
#ask .row { display: flex; gap: 6px; flex: 0 0 auto; }
|
|
202
|
-
#askq { flex: 1; min-width: 0; background: #14161e; color: #e7e9ee; border: 1px solid #2a2e42; border-radius: 5px; padding: 6px 9px; font: inherit; font-size: 12.5px; }
|
|
203
|
-
#askq:focus { outline: none; border-color: #7aa2f7; }
|
|
204
|
-
#askq:disabled { opacity: 0.6; }
|
|
205
|
-
#asksubmit { background: rgba(122,162,247,0.18); border: 1px solid #7aa2f7; color: #cfe0ff; border-radius: 5px; padding: 6px 12px; font-size: 12.5px; cursor: pointer; }
|
|
206
|
-
#asksubmit:hover { background: rgba(122,162,247,0.3); }
|
|
207
|
-
#askresult { margin-top: 8px; flex: 1 1 auto; min-height: 0; overflow: auto; line-height: 1.55; color: #c0caf5; white-space: pre-wrap; }
|
|
208
|
-
#askresult .q { color: #565f89; font-style: normal; margin-bottom: 3px; }
|
|
209
|
-
#askresult.miss { color: #a9b1d6; font-style: italic; }
|
|
210
|
-
#askresult .canon { margin-top: 6px; color: #6b7189; font-size: 11px; font-style: normal; border-top: 1px dashed rgba(255,255,255,0.1); padding-top: 5px; }
|
|
211
|
-
#askresult .src { margin-top: 4px; color: #565f89; font-size: 10.5px; }
|
|
212
|
-
#ask .hint { color: #6b7189; font-size: 11px; flex: 0 0 auto; }
|
|
213
|
-
</style>
|
|
214
|
-
</head>
|
|
215
|
-
<body>
|
|
216
|
-
<div id="wrap">
|
|
217
|
-
<canvas id="c"></canvas>
|
|
218
|
-
<div id="hud"><b>tmct viz</b><span class="muted">drag to pan · scroll to zoom · click a node for details · double-click to re-centre</span><button id="resetview" title="reset pan/zoom to fit the current view">reset view</button></div>
|
|
219
|
-
<div id="controls">
|
|
220
|
-
<span class="grp"><span class="muted">depth</span><button id="depthdown" title="shallower">−</button><b class="depthval" id="depthval"></b><button id="depthup" title="deeper">+</button></span>
|
|
221
|
-
<span class="grp" id="typefilters"></span>
|
|
222
|
-
<span class="sep"></span>
|
|
223
|
-
<span class="grp"><span class="muted">edges</span><select id="edgekind" title="which kinds of edge the walk follows">
|
|
224
|
-
<option value="both">both (default)</option>
|
|
225
|
-
<option value="relation">concept relations</option>
|
|
226
|
-
<option value="meta">provenance only</option>
|
|
227
|
-
</select></span>
|
|
228
|
-
<span class="grp"><label title="hide nodes above N connections outright"><input type="checkbox" id="hubhideon"> hub-hide ></label><input type="number" id="hubhideval" min="1"></span>
|
|
229
|
-
<span class="grp"><label title="keep only the top-N neighbours by degree per hop"><input type="checkbox" id="beamon"> beam-prune</label><input type="number" id="beamval" min="1"></span>
|
|
230
|
-
<span class="grp"><span class="muted">labels</span><select id="labelmode">
|
|
231
|
-
<option value="smart">smart</option>
|
|
232
|
-
<option value="all">all names</option>
|
|
233
|
-
<option value="name-source">name + source</option>
|
|
234
|
-
<option value="none">none</option>
|
|
235
|
-
</select></span>
|
|
236
|
-
<span class="sep"></span>
|
|
237
|
-
<span class="grp"><input type="text" class="search" id="search" placeholder="search labels…" autocomplete="off"><b id="searchcount" class="muted"></b></span>
|
|
238
|
-
</div>
|
|
239
|
-
<div id="legend"><span class="muted">legend</span><select id="legenddim"></select><span id="legendchips"></span></div>
|
|
240
|
-
<div id="panel"></div>
|
|
241
|
-
<div id="empty"><div><b>No graph data.</b><br>This repo has no <code>.tmct/memory/graph.json</code> yet (or the requested <code>--focus</code>/<code>--term</code> wasn't found). Run <code>tmct chat</code> in that repo first, then re-run <code>tmct viz</code>.</div></div>
|
|
242
|
-
<div id="ask">
|
|
243
|
-
<h3>Ask the graph</h3>
|
|
244
|
-
<div class="row"><input id="askq" type="text" autocomplete="off" placeholder='ask e.g. "what is a dog"'${hasAnyChat ? "" : " disabled"}><button id="asksubmit"${hasAnyChat ? "" : " disabled"}>ask</button></div>
|
|
245
|
-
<div id="askresult">${hasAnyChat
|
|
246
|
-
? '<span class="hint">running the real tmct engine(s), client-side, right here — try "what is X" or "where is X mentioned". Answers re-centre the graph on what they resolve.</span>'
|
|
247
|
-
: '<span class="hint">chat unavailable — run <code>npm run build:ask-bundle</code> and re-generate this page.</span>'}</div>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
<script>
|
|
251
|
-
const GRAPH = ${graphJson};
|
|
252
|
-
const PAYLOAD = ${payloadJson};
|
|
253
|
-
const LEGEND = ${legendJson};
|
|
254
|
-
const WALK_OPTS = ${walkOptsJson};
|
|
255
|
-
</script>
|
|
256
|
-
${hasChat ? `<script>\n${askBundle}\n</script>` : ""}
|
|
257
|
-
${hasMemChat ? `<script>\n${memoryAskBundle}\n</script>` : ""}
|
|
258
|
-
<script>
|
|
259
|
-
(function () {
|
|
260
|
-
"use strict";
|
|
261
|
-
|
|
262
|
-
var emptyEl = document.getElementById("empty");
|
|
263
|
-
if (!GRAPH.nodes.length) { emptyEl.classList.add("show"); return; }
|
|
264
|
-
|
|
265
|
-
var hasEngine = typeof tmctViz !== "undefined";
|
|
266
|
-
var hasMemEngine = typeof tmctMemoryAsk !== "undefined";
|
|
267
|
-
var FULL_GRAPH = hasEngine ? tmctViz.parseEntities(PAYLOAD) : null;
|
|
268
|
-
// The term-relation view over the FULL graph, computed once —
|
|
269
|
-
// recentre()/edge-kind-toggle re-walks reuse it rather than re-deriving it
|
|
270
|
-
// per click. hasEngine-gated: the walk/legend exports only exist in the
|
|
271
|
-
// ask-browser bundle, not the memory-ask one.
|
|
272
|
-
var TERM_GRAPH = hasEngine ? tmctViz.deriveFactTermGraph(FULL_GRAPH) : null;
|
|
273
|
-
// kindsForMode reuses the bundled tmctViz.edgeKindsFor — the SAME function
|
|
274
|
-
// the CLI's own computeVizGraph calls server-side — rather than a second
|
|
275
|
-
// hand-rolled copy of the meta/relation/both combination logic.
|
|
276
|
-
function kindsForMode(mode) {
|
|
277
|
-
return tmctViz.edgeKindsFor(mode, TERM_GRAPH ? TERM_GRAPH.factRelationKinds : []);
|
|
278
|
-
}
|
|
279
|
-
var edgeKindMode = WALK_OPTS.edgeKindMode || "both";
|
|
280
|
-
document.getElementById("edgekind").value = edgeKindMode;
|
|
281
|
-
|
|
282
|
-
// ---- palette: one hue per class, stable across recentres --------------
|
|
283
|
-
var PALETTE = ["#7aa2f7", "#bb9af7", "#7dcfff", "#9ece6a", "#e0af68", "#f7768e", "#73daca", "#c0caf5"];
|
|
284
|
-
var classColor = new Map();
|
|
285
|
-
function colorFor(cls) {
|
|
286
|
-
if (!classColor.has(cls)) classColor.set(cls, PALETTE[classColor.size % PALETTE.length]);
|
|
287
|
-
return classColor.get(cls);
|
|
288
|
-
}
|
|
289
|
-
// seed the palette + type-filter checkboxes from every class in the FULL
|
|
290
|
-
// graph (not just the currently-walked subset), so filters stay stable
|
|
291
|
-
// across a recentre that reveals a class not in the initial view.
|
|
292
|
-
var allClasses = [];
|
|
293
|
-
(function () {
|
|
294
|
-
var seen = new Set();
|
|
295
|
-
(FULL_GRAPH ? FULL_GRAPH.individuals : GRAPH.nodes).forEach(function (n) {
|
|
296
|
-
var cls = n.class || (n.cls || "");
|
|
297
|
-
if (cls && !seen.has(cls)) { seen.add(cls); allClasses.push(cls); }
|
|
298
|
-
});
|
|
299
|
-
allClasses.sort();
|
|
300
|
-
allClasses.forEach(colorFor);
|
|
301
|
-
})();
|
|
302
|
-
|
|
303
|
-
var enabledTypes = new Set(allClasses);
|
|
304
|
-
var typeFiltersEl = document.getElementById("typefilters");
|
|
305
|
-
function renderTypeFilters() {
|
|
306
|
-
typeFiltersEl.innerHTML = allClasses.map(function (cls) {
|
|
307
|
-
return '<label class="typechk" data-cls="' + cls + '" title="show/hide ' + cls + ' nodes">'
|
|
308
|
-
+ '<input type="checkbox" checked><span class="swatch" style="background:' + colorFor(cls) + '"></span>' + cls
|
|
309
|
-
+ '</label>';
|
|
310
|
-
}).join("");
|
|
311
|
-
typeFiltersEl.querySelectorAll("label.typechk").forEach(function (lbl) {
|
|
312
|
-
lbl.querySelector("input").addEventListener("change", function (ev) {
|
|
313
|
-
var cls = lbl.dataset.cls;
|
|
314
|
-
if (ev.target.checked) enabledTypes.add(cls); else enabledTypes.delete(cls);
|
|
315
|
-
applyFilters();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
renderTypeFilters();
|
|
320
|
-
|
|
321
|
-
// ---- legend-as-filter: LEGEND.primary names the server's auto-picked dimension
|
|
322
|
-
// (class/predicate/provenance, scored by normalized Shannon entropy over
|
|
323
|
-
// the INITIAL walk); the dropdown lets a user switch dimension without
|
|
324
|
-
// regenerating the page. Bucket COUNTS are always recomputed live over the
|
|
325
|
-
// CURRENTLY visible walk (never the stale initial-generation counts) via
|
|
326
|
-
// the same legendValueFor derivation the CLI's own pickLegendDimension
|
|
327
|
-
// uses — one shared source of truth, never a second hand-rolled copy. ----
|
|
328
|
-
var legendEl = document.getElementById("legend");
|
|
329
|
-
var legendDimEl = document.getElementById("legenddim");
|
|
330
|
-
var legendChipsEl = document.getElementById("legendchips");
|
|
331
|
-
var legendDim = (LEGEND && LEGEND.primary) || "class";
|
|
332
|
-
var legendEnabled = null; // null = no legend filter active (every value passes)
|
|
333
|
-
// collapseBuckets reuses the bundled tmctViz.collapseToTopN (same top-15 +
|
|
334
|
-
// "Other" rule pickLegendDimension uses server-side) rather than a second
|
|
335
|
-
// hand-rolled copy; a tiny inline fallback covers the (untested-in-practice)
|
|
336
|
-
// no-engine case so the legend never hard-crashes if the bundle is absent.
|
|
337
|
-
function collapseBuckets(buckets) {
|
|
338
|
-
if (hasEngine) return tmctViz.collapseToTopN(buckets);
|
|
339
|
-
if (buckets.length <= 20) return buckets;
|
|
340
|
-
var sorted = buckets.slice().sort(function (a, b) { return b.count - a.count; });
|
|
341
|
-
var kept = sorted.slice(0, 15);
|
|
342
|
-
var restCount = sorted.slice(15).reduce(function (s, b) { return s + b.count; }, 0);
|
|
343
|
-
return restCount ? kept.concat([{ value: "Other", count: restCount }]) : kept;
|
|
344
|
-
}
|
|
345
|
-
function legendValueOf(node, dim) {
|
|
346
|
-
if (!hasEngine) return dim === "class" ? (node.class || "(none)") : null;
|
|
347
|
-
return tmctViz.legendValueFor(FULL_GRAPH, node, dim);
|
|
348
|
-
}
|
|
349
|
-
function computeLegendBuckets(dim) {
|
|
350
|
-
var counts = new Map();
|
|
351
|
-
GRAPH.nodes.forEach(function (n) {
|
|
352
|
-
var v = legendValueOf(n, dim);
|
|
353
|
-
if (v == null || v === "") return;
|
|
354
|
-
counts.set(v, (counts.get(v) || 0) + 1);
|
|
355
|
-
});
|
|
356
|
-
var buckets = Array.from(counts.entries()).map(function (e) { return { value: e[0], count: e[1] }; });
|
|
357
|
-
buckets.sort(function (a, b) { return b.count - a.count; });
|
|
358
|
-
return collapseBuckets(buckets);
|
|
359
|
-
}
|
|
360
|
-
function renderLegend() {
|
|
361
|
-
if (!LEGEND) { legendEl.classList.remove("show"); return; }
|
|
362
|
-
var dims = Object.keys(LEGEND.dimensions || { class: 1 });
|
|
363
|
-
legendDimEl.innerHTML = dims.map(function (d) {
|
|
364
|
-
var info = LEGEND.dimensions[d];
|
|
365
|
-
var tag = info && info.qualifies ? "" : " (low signal)";
|
|
366
|
-
return '<option value="' + d + '"' + (d === legendDim ? " selected" : "") + '>' + d + tag + '</option>';
|
|
367
|
-
}).join("");
|
|
368
|
-
var buckets = computeLegendBuckets(legendDim);
|
|
369
|
-
if (!legendEnabled) legendEnabled = new Set(buckets.map(function (b) { return b.value; }));
|
|
370
|
-
legendEl.classList.toggle("show", buckets.length > 0);
|
|
371
|
-
legendChipsEl.innerHTML = buckets.map(function (b) {
|
|
372
|
-
var on = legendEnabled.has(b.value);
|
|
373
|
-
return '<span class="chip' + (on ? "" : " off") + '" data-v="' + esc(b.value) + '" title="click to toggle">'
|
|
374
|
-
+ '<span class="swatch" style="background:' + colorFor(legendDim === "class" ? b.value : "__" + legendDim) + '"></span>'
|
|
375
|
-
+ esc(b.value) + '<span class="n">' + b.count + '</span></span>';
|
|
376
|
-
}).join("");
|
|
377
|
-
legendChipsEl.querySelectorAll(".chip").forEach(function (chip) {
|
|
378
|
-
chip.addEventListener("click", function () {
|
|
379
|
-
var v = chip.dataset.v;
|
|
380
|
-
if (legendEnabled.has(v)) legendEnabled.delete(v); else legendEnabled.add(v);
|
|
381
|
-
renderLegend();
|
|
382
|
-
applyFilters();
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
legendDimEl.addEventListener("change", function () {
|
|
387
|
-
legendDim = legendDimEl.value;
|
|
388
|
-
legendEnabled = null; // fresh "all on" set for the newly selected dimension
|
|
389
|
-
renderLegend();
|
|
390
|
-
applyFilters();
|
|
391
|
-
});
|
|
392
|
-
renderLegend();
|
|
393
|
-
|
|
394
|
-
// ---- hub-hide / beam-prune / search state --------------------------------
|
|
395
|
-
var hubHideOn = false, hubHideVal = WALK_OPTS.hubDegree || 40;
|
|
396
|
-
var beamOn = false, beamVal = 8;
|
|
397
|
-
var labelMode = "smart";
|
|
398
|
-
var searchTerm = "";
|
|
399
|
-
var hubHideOnEl = document.getElementById("hubhideon");
|
|
400
|
-
var hubHideValEl = document.getElementById("hubhideval");
|
|
401
|
-
var beamOnEl = document.getElementById("beamon");
|
|
402
|
-
var beamValEl = document.getElementById("beamval");
|
|
403
|
-
var labelModeEl = document.getElementById("labelmode");
|
|
404
|
-
var searchEl = document.getElementById("search");
|
|
405
|
-
var searchCountEl = document.getElementById("searchcount");
|
|
406
|
-
hubHideValEl.value = hubHideVal;
|
|
407
|
-
beamValEl.value = beamVal;
|
|
408
|
-
hubHideOnEl.addEventListener("change", function () { hubHideOn = hubHideOnEl.checked; applyFilters(); });
|
|
409
|
-
hubHideValEl.addEventListener("change", function () { hubHideVal = Number(hubHideValEl.value) || hubHideVal; applyFilters(); });
|
|
410
|
-
beamOnEl.addEventListener("change", function () { beamOn = beamOnEl.checked; applyFilters(); });
|
|
411
|
-
beamValEl.addEventListener("change", function () { beamVal = Number(beamValEl.value) || beamVal; applyFilters(); });
|
|
412
|
-
labelModeEl.addEventListener("change", function () { labelMode = labelModeEl.value; draw(); });
|
|
413
|
-
searchEl.addEventListener("input", function () { searchTerm = searchEl.value.trim().toLowerCase(); applyFilters(); });
|
|
414
|
-
|
|
415
|
-
// degree over the CURRENTLY displayed edge set (hub-hide/beam-prune are
|
|
416
|
-
// display-time filters, distinct from the generation-time hubDegree cap
|
|
417
|
-
// which only stops the WALK expanding through a hub — both useful).
|
|
418
|
-
// Memoized: draw() runs on every
|
|
419
|
-
// pan/zoom/hover mousemove, and both draw() and visibleNodeIds() (which
|
|
420
|
-
// draw() itself calls) each need it — recomputing an O(edges) map twice per
|
|
421
|
-
// frame during a drag is real, avoidable per-frame cost. Invalidated by
|
|
422
|
-
// relayout() (the ONLY place GRAPH.nodes/edges are mutated, on init and on
|
|
423
|
-
// every recentre()), so a stale cache can never survive a graph change.
|
|
424
|
-
var degCache = null;
|
|
425
|
-
function currentDegrees() {
|
|
426
|
-
if (!degCache) {
|
|
427
|
-
degCache = new Map();
|
|
428
|
-
GRAPH.edges.forEach(function (e) {
|
|
429
|
-
degCache.set(e.source, (degCache.get(e.source) || 0) + 1);
|
|
430
|
-
degCache.set(e.target, (degCache.get(e.target) || 0) + 1);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
return degCache;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ---- layout: concentric rings keyed on hop, seed at the centre, RE-runnable on recentre ----
|
|
437
|
-
var RING_GAP = 110;
|
|
438
|
-
var pos = new Map();
|
|
439
|
-
var byHopMax = 0;
|
|
440
|
-
function relayout() {
|
|
441
|
-
degCache = null; // GRAPH.nodes/edges just changed (initial load or recentre()) — invalidate
|
|
442
|
-
pos = new Map();
|
|
443
|
-
var byHop = new Map();
|
|
444
|
-
GRAPH.nodes.forEach(function (n) {
|
|
445
|
-
if (!byHop.has(n.hop)) byHop.set(n.hop, []);
|
|
446
|
-
byHop.get(n.hop).push(n);
|
|
447
|
-
});
|
|
448
|
-
byHop.forEach(function (list) { list.sort(function (a, b) { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; }); });
|
|
449
|
-
byHop.forEach(function (list, hop) {
|
|
450
|
-
var r = hop * RING_GAP, n = list.length;
|
|
451
|
-
list.forEach(function (node, i) {
|
|
452
|
-
if (r === 0) { pos.set(node.id, { x: 0, y: 0 }); return; }
|
|
453
|
-
var angle = (2 * Math.PI * i) / n + hop * 0.35;
|
|
454
|
-
pos.set(node.id, { x: r * Math.cos(angle), y: r * Math.sin(angle) });
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
byHopMax = 0;
|
|
458
|
-
GRAPH.nodes.forEach(function (n) { if (n.hop > byHopMax) byHopMax = n.hop; });
|
|
459
|
-
}
|
|
460
|
-
relayout();
|
|
461
|
-
|
|
462
|
-
// ---- depth stepper: hides nodes with hop > depthVal (client-side visibility only,
|
|
463
|
-
// no re-walk needed — spiralExpand already computed every hop up to the walk's own depth) ----
|
|
464
|
-
var depthVal = byHopMax;
|
|
465
|
-
function syncDepthUi() {
|
|
466
|
-
document.getElementById("depthval").textContent = depthVal;
|
|
467
|
-
document.getElementById("depthdown").disabled = depthVal <= 0;
|
|
468
|
-
document.getElementById("depthup").disabled = depthVal >= byHopMax;
|
|
469
|
-
}
|
|
470
|
-
function visibleNodeIds() {
|
|
471
|
-
var deg = (hubHideOn || beamOn) ? currentDegrees() : null;
|
|
472
|
-
var vis = new Set();
|
|
473
|
-
GRAPH.nodes.forEach(function (n) {
|
|
474
|
-
if (n.hop > depthVal || !enabledTypes.has(n.class)) return;
|
|
475
|
-
if (legendEnabled) {
|
|
476
|
-
var v = legendValueOf(n, legendDim);
|
|
477
|
-
if (v != null && v !== "" && !legendEnabled.has(v)) return;
|
|
478
|
-
}
|
|
479
|
-
if (hubHideOn && deg && (deg.get(n.id) || 0) > hubHideVal) return;
|
|
480
|
-
vis.add(n.id);
|
|
481
|
-
});
|
|
482
|
-
// beam-prune: BFS-order pruning — per hop (> 0), keep only the top-N
|
|
483
|
-
// (by CURRENT degree) neighbours; hop 0 (the seed(s)) is always kept.
|
|
484
|
-
if (beamOn && deg) {
|
|
485
|
-
var byHop = new Map();
|
|
486
|
-
vis.forEach(function (id) {
|
|
487
|
-
var n = GRAPH.nodes.find(function (x) { return x.id === id; });
|
|
488
|
-
if (!n || n.hop === 0) return;
|
|
489
|
-
if (!byHop.has(n.hop)) byHop.set(n.hop, []);
|
|
490
|
-
byHop.get(n.hop).push(n);
|
|
491
|
-
});
|
|
492
|
-
byHop.forEach(function (list) {
|
|
493
|
-
list.sort(function (a, b) { return (deg.get(b.id) || 0) - (deg.get(a.id) || 0); });
|
|
494
|
-
list.slice(beamVal).forEach(function (n) { vis.delete(n.id); });
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
if (searchTerm) {
|
|
498
|
-
var hits = new Set();
|
|
499
|
-
GRAPH.nodes.forEach(function (n) { if (vis.has(n.id) && String(n.label).toLowerCase().indexOf(searchTerm) !== -1) hits.add(n.id); });
|
|
500
|
-
searchCountEl.textContent = hits.size ? (hits.size + " match" + (hits.size === 1 ? "" : "es")) : "no match";
|
|
501
|
-
// search NARROWS visibility to matches + their direct neighbours, so the
|
|
502
|
-
// hit's own context stays legible instead of collapsing to lone dots.
|
|
503
|
-
var withNeighbours = new Set(hits);
|
|
504
|
-
GRAPH.edges.forEach(function (e) {
|
|
505
|
-
if (hits.has(e.source) && vis.has(e.target)) withNeighbours.add(e.target);
|
|
506
|
-
if (hits.has(e.target) && vis.has(e.source)) withNeighbours.add(e.source);
|
|
507
|
-
});
|
|
508
|
-
vis = new Set(Array.from(vis).filter(function (id) { return withNeighbours.has(id); }));
|
|
509
|
-
} else {
|
|
510
|
-
searchCountEl.textContent = "";
|
|
511
|
-
}
|
|
512
|
-
return vis;
|
|
513
|
-
}
|
|
514
|
-
function applyFilters() { syncDepthUi(); draw(); }
|
|
515
|
-
document.getElementById("depthdown").addEventListener("click", function () { if (depthVal > 0) { depthVal--; applyFilters(); } });
|
|
516
|
-
document.getElementById("depthup").addEventListener("click", function () { if (depthVal < byHopMax) { depthVal++; applyFilters(); } });
|
|
517
|
-
|
|
518
|
-
// ---- depth encoding: paint-order-by-hop + lightness/opacity falloff -----
|
|
519
|
-
function styleForHop(hop, cls) {
|
|
520
|
-
var t = byHopMax > 0 ? hop / byHopMax : 0;
|
|
521
|
-
var alpha = 1 - t * 0.55;
|
|
522
|
-
var radius = Math.max(4, 9 - t * 5);
|
|
523
|
-
return { fill: colorFor(cls), alpha: alpha, radius: radius };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ---- canvas + view transform (pan/zoom) ----------------------------------
|
|
527
|
-
var canvas = document.getElementById("c");
|
|
528
|
-
var ctx = canvas.getContext("2d");
|
|
529
|
-
var dpr = window.devicePixelRatio || 1;
|
|
530
|
-
var view = { scale: 1, x: 0, y: 0 };
|
|
531
|
-
var selectedId = null;
|
|
532
|
-
// The full set of node ids a query answer actually resolved to (not just
|
|
533
|
-
// the single "primary" selectedId) — draw() rings every one of them so a
|
|
534
|
-
// multi-fact answer shows ALL the nodes it came from, not just one.
|
|
535
|
-
var highlightIds = new Set();
|
|
536
|
-
|
|
537
|
-
function resize() {
|
|
538
|
-
canvas.width = Math.floor(canvas.clientWidth * dpr);
|
|
539
|
-
canvas.height = Math.floor(canvas.clientHeight * dpr);
|
|
540
|
-
draw();
|
|
541
|
-
}
|
|
542
|
-
window.addEventListener("resize", resize);
|
|
543
|
-
|
|
544
|
-
function worldToScreen(p) {
|
|
545
|
-
var cx = canvas.width / 2 + view.x * dpr, cy = canvas.height / 2 + view.y * dpr;
|
|
546
|
-
return { x: cx + p.x * view.scale * dpr, y: cy + p.y * view.scale * dpr };
|
|
547
|
-
}
|
|
548
|
-
function screenToWorld(sx, sy) {
|
|
549
|
-
var cx = canvas.width / 2 + view.x * dpr, cy = canvas.height / 2 + view.y * dpr;
|
|
550
|
-
return { x: (sx * dpr - cx) / (view.scale * dpr), y: (sy * dpr - cy) / (view.scale * dpr) };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Label-density modes: "smart" (the default) draws a label only for the
|
|
554
|
-
// focus/selection/direct-neighbours/top-20-by-degree; everything else
|
|
555
|
-
// labels on hover only. "all"/"name-source" always draw (name-source
|
|
556
|
-
// appends the Fact's own provenance prefix — trust tier is a first-class
|
|
557
|
-
// concept here). "none" draws no labels at all.
|
|
558
|
-
var hoverId = null;
|
|
559
|
-
canvas.addEventListener("mousemove", function (ev) {
|
|
560
|
-
if (labelMode !== "smart" || dragging) return;
|
|
561
|
-
var rect = canvas.getBoundingClientRect();
|
|
562
|
-
var w = screenToWorld(ev.clientX - rect.left, ev.clientY - rect.top);
|
|
563
|
-
var best = null, bestDist = Infinity;
|
|
564
|
-
GRAPH.nodes.forEach(function (n) {
|
|
565
|
-
var p = pos.get(n.id);
|
|
566
|
-
if (!p) return;
|
|
567
|
-
var dx = p.x - w.x, dy = p.y - w.y, d = Math.sqrt(dx * dx + dy * dy);
|
|
568
|
-
if (d < bestDist) { best = n.id; bestDist = d; }
|
|
569
|
-
});
|
|
570
|
-
var next = bestDist < 24 / view.scale ? best : null;
|
|
571
|
-
if (next !== hoverId) { hoverId = next; draw(); }
|
|
572
|
-
});
|
|
573
|
-
function topDegreeIds(vis, deg, n) {
|
|
574
|
-
var ranked = Array.from(vis).sort(function (a, b) { return (deg.get(b) || 0) - (deg.get(a) || 0); });
|
|
575
|
-
return new Set(ranked.slice(0, n));
|
|
576
|
-
}
|
|
577
|
-
function labelFor(n) {
|
|
578
|
-
var text = String(n.label).slice(0, 40);
|
|
579
|
-
if (labelMode !== "name-source") return text;
|
|
580
|
-
var prov = hasEngine ? tmctViz.legendValueFor(FULL_GRAPH, n, "provenance") : null;
|
|
581
|
-
return prov ? text + " [" + prov + "]" : text;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function draw() {
|
|
585
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
586
|
-
ctx.lineWidth = Math.max(1, 1 * dpr);
|
|
587
|
-
var vis = visibleNodeIds();
|
|
588
|
-
var deg = currentDegrees();
|
|
589
|
-
var smartLabelIds = labelMode === "smart" ? topDegreeIds(vis, deg, 20) : null;
|
|
590
|
-
var searchHits = searchTerm
|
|
591
|
-
? new Set(GRAPH.nodes.filter(function (n) { return vis.has(n.id) && String(n.label).toLowerCase().indexOf(searchTerm) !== -1; }).map(function (n) { return n.id; }))
|
|
592
|
-
: null;
|
|
593
|
-
ctx.strokeStyle = "rgba(255,255,255,0.14)";
|
|
594
|
-
GRAPH.edges.forEach(function (e) {
|
|
595
|
-
if (!vis.has(e.source) || !vis.has(e.target)) return;
|
|
596
|
-
var a = pos.get(e.source), b = pos.get(e.target);
|
|
597
|
-
if (!a || !b) return;
|
|
598
|
-
var sa = worldToScreen(a), sb = worldToScreen(b);
|
|
599
|
-
ctx.beginPath(); ctx.moveTo(sa.x, sa.y); ctx.lineTo(sb.x, sb.y); ctx.stroke();
|
|
600
|
-
});
|
|
601
|
-
var order = GRAPH.nodes.filter(function (n) { return vis.has(n.id); }).sort(function (a, b) { return a.hop - b.hop; });
|
|
602
|
-
order.forEach(function (n) {
|
|
603
|
-
var p = pos.get(n.id);
|
|
604
|
-
if (!p) return;
|
|
605
|
-
var sp = worldToScreen(p);
|
|
606
|
-
var st = styleForHop(n.hop, n.class);
|
|
607
|
-
var r = st.radius * view.scale * dpr;
|
|
608
|
-
ctx.globalAlpha = st.alpha;
|
|
609
|
-
ctx.beginPath(); ctx.arc(sp.x, sp.y, Math.max(1.5, r), 0, Math.PI * 2);
|
|
610
|
-
ctx.fillStyle = st.fill; ctx.fill();
|
|
611
|
-
ctx.globalAlpha = 1;
|
|
612
|
-
if (n.id === selectedId) {
|
|
613
|
-
ctx.lineWidth = Math.max(1.5, 2 * dpr); ctx.strokeStyle = "#fff"; ctx.stroke();
|
|
614
|
-
}
|
|
615
|
-
if (n.id === GRAPH.focus) {
|
|
616
|
-
ctx.lineWidth = Math.max(1.5, 2.5 * dpr); ctx.strokeStyle = "rgba(255,255,255,0.6)";
|
|
617
|
-
ctx.beginPath(); ctx.arc(sp.x, sp.y, Math.max(1.5, r) + 3 * dpr, 0, Math.PI * 2); ctx.stroke();
|
|
618
|
-
}
|
|
619
|
-
if (searchHits && searchHits.has(n.id)) {
|
|
620
|
-
ctx.lineWidth = Math.max(1.5, 2 * dpr); ctx.strokeStyle = "#e0af68";
|
|
621
|
-
ctx.beginPath(); ctx.arc(sp.x, sp.y, Math.max(1.5, r) + 5 * dpr, 0, Math.PI * 2); ctx.stroke();
|
|
622
|
-
}
|
|
623
|
-
// Every node the last "ask the graph" answer actually resolved to
|
|
624
|
-
// (frameQueryResult below) — a distinct green ring so a multi-fact
|
|
625
|
-
// answer's whole result set reads as one highlighted group, not just
|
|
626
|
-
// the single primary node selectedId/focus already mark.
|
|
627
|
-
if (highlightIds.has(n.id) && n.id !== selectedId) {
|
|
628
|
-
ctx.lineWidth = Math.max(1.5, 2 * dpr); ctx.strokeStyle = "#9ece6a";
|
|
629
|
-
ctx.beginPath(); ctx.arc(sp.x, sp.y, Math.max(1.5, r) + 4 * dpr, 0, Math.PI * 2); ctx.stroke();
|
|
630
|
-
}
|
|
631
|
-
var showLabel = view.scale > 0.55 && labelMode !== "none" && (
|
|
632
|
-
labelMode !== "smart"
|
|
633
|
-
|| n.id === selectedId || n.id === GRAPH.focus || n.id === hoverId
|
|
634
|
-
|| (smartLabelIds && smartLabelIds.has(n.id))
|
|
635
|
-
);
|
|
636
|
-
if (showLabel) {
|
|
637
|
-
ctx.font = (11 * dpr) + "px -apple-system, sans-serif";
|
|
638
|
-
ctx.fillStyle = "rgba(231,233,238," + Math.min(1, 0.55 + view.scale * 0.3) + ")";
|
|
639
|
-
ctx.textBaseline = "middle";
|
|
640
|
-
ctx.fillText(labelFor(n), sp.x + Math.max(6, r + 4), sp.y);
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// ---- pan (drag) -----------------------------------------------------------
|
|
646
|
-
var dragging = false, dragMoved = false, dragStart = null, viewStart = null;
|
|
647
|
-
canvas.addEventListener("mousedown", function (ev) {
|
|
648
|
-
dragging = true; dragMoved = false;
|
|
649
|
-
dragStart = { x: ev.clientX, y: ev.clientY }; viewStart = { x: view.x, y: view.y };
|
|
650
|
-
canvas.classList.add("grabbing");
|
|
651
|
-
});
|
|
652
|
-
window.addEventListener("mousemove", function (ev) {
|
|
653
|
-
if (!dragging) return;
|
|
654
|
-
var dx = ev.clientX - dragStart.x, dy = ev.clientY - dragStart.y;
|
|
655
|
-
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragMoved = true;
|
|
656
|
-
view.x = viewStart.x + dx; view.y = viewStart.y + dy;
|
|
657
|
-
draw();
|
|
658
|
-
});
|
|
659
|
-
window.addEventListener("mouseup", function () { dragging = false; canvas.classList.remove("grabbing"); });
|
|
660
|
-
|
|
661
|
-
// ---- zoom (wheel), anchored at the pointer --------------------------------
|
|
662
|
-
canvas.addEventListener("wheel", function (ev) {
|
|
663
|
-
ev.preventDefault();
|
|
664
|
-
var rect = canvas.getBoundingClientRect();
|
|
665
|
-
var mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
|
|
666
|
-
var before = screenToWorld(mx, my);
|
|
667
|
-
var factor = Math.exp(-ev.deltaY * 0.001);
|
|
668
|
-
view.scale = Math.min(8, Math.max(0.08, view.scale * factor));
|
|
669
|
-
var after = screenToWorld(mx, my);
|
|
670
|
-
view.x += (after.x - before.x) * view.scale; view.y += (after.y - before.y) * view.scale;
|
|
671
|
-
draw();
|
|
672
|
-
}, { passive: false });
|
|
673
|
-
|
|
674
|
-
// Shared bounding-box fit — pan/zoom so every position in "points" is framed
|
|
675
|
-
// with padding. fitToVisible/fitToIds are both thin wrappers naming WHICH
|
|
676
|
-
// positions to fit; the math itself lives here once.
|
|
677
|
-
function fitToPositions(points) {
|
|
678
|
-
if (!points.length) return;
|
|
679
|
-
var minX = Math.min.apply(null, points.map(function (p) { return p.x; })), maxX = Math.max.apply(null, points.map(function (p) { return p.x; }));
|
|
680
|
-
var minY = Math.min.apply(null, points.map(function (p) { return p.y; })), maxY = Math.max.apply(null, points.map(function (p) { return p.y; }));
|
|
681
|
-
var w = Math.max(1, maxX - minX), h = Math.max(1, maxY - minY);
|
|
682
|
-
view.scale = Math.min(4, Math.max(0.1, Math.min(canvas.width / dpr / (w + 160), canvas.height / dpr / (h + 160))));
|
|
683
|
-
view.x = -(minX + maxX) / 2 * view.scale; view.y = -(minY + maxY) / 2 * view.scale;
|
|
684
|
-
}
|
|
685
|
-
function fitToVisible() {
|
|
686
|
-
fitToPositions(Array.from(visibleNodeIds()).map(function (id) { return pos.get(id); }).filter(Boolean));
|
|
687
|
-
}
|
|
688
|
-
// Fit specifically to a query answer's own result set (not just "whatever
|
|
689
|
-
// recentre's re-walk happened to make visible") — a multi-fact answer's
|
|
690
|
-
// nodes can be spread wider than the default depth/nodeLimit view, so this
|
|
691
|
-
// is the precision framing step frameQueryResult calls after recentre.
|
|
692
|
-
function fitToIds(ids) {
|
|
693
|
-
fitToPositions(ids.map(function (id) { return pos.get(id); }).filter(Boolean));
|
|
694
|
-
}
|
|
695
|
-
document.getElementById("resetview").addEventListener("click", function () { fitToVisible(); draw(); });
|
|
696
|
-
|
|
697
|
-
// ---- recentre: RE-WALK the FULL graph (via TERM_GRAPH — the augmented
|
|
698
|
-
// view, so a recentre reaches real concept-relation edges the same way
|
|
699
|
-
// generation-time computeVizGraph does) from a new seed via the real,
|
|
700
|
-
// bundled spiralExpand (byte-identical to the CLI's own walk — never a
|
|
701
|
-
// hand-rolled client-side BFS) and rebuild via the real buildVizNodesAndEdges.
|
|
702
|
-
// Used for double-click-to-recentre, focus-follows-chat-answer, AND the
|
|
703
|
-
// edge-kind toggle (re-walks the SAME seed under a new kind set). Reuses
|
|
704
|
-
// WALK_OPTS (this page's own generation-time depth/nodeLimit/hubDegree) so a
|
|
705
|
-
// client-side re-walk never silently falls back to spiralExpand's smaller
|
|
706
|
-
// code-graph defaults. -------------------------------------------------
|
|
707
|
-
function walkGraph() { return TERM_GRAPH ? TERM_GRAPH.graph : FULL_GRAPH; }
|
|
708
|
-
function recentre(id) {
|
|
709
|
-
if (!hasEngine || !FULL_GRAPH || !walkGraph().byId.has(id)) return false;
|
|
710
|
-
var walked = tmctViz.spiralExpand(walkGraph(), [], {
|
|
711
|
-
kinds: kindsForMode(edgeKindMode),
|
|
712
|
-
classPredicate: function () { return true; }, idNormalizer: function (i) { return i; }, seeds: [id],
|
|
713
|
-
depth: WALK_OPTS.depth, nodeLimit: WALK_OPTS.nodeLimit, hubDegree: WALK_OPTS.hubDegree,
|
|
714
|
-
});
|
|
715
|
-
var built = tmctViz.buildVizNodesAndEdges(walkGraph(), walked);
|
|
716
|
-
GRAPH.nodes = built.nodes; GRAPH.edges = built.edges; GRAPH.focus = id;
|
|
717
|
-
// classes newly reached that weren't in the initial palette still resolve
|
|
718
|
-
// via colorFor()'s own on-demand assignment; the checkbox row itself was
|
|
719
|
-
// already seeded from the FULL graph's classes above, so no rebuild needed.
|
|
720
|
-
relayout();
|
|
721
|
-
depthVal = byHopMax;
|
|
722
|
-
legendEnabled = null; // re-derive "all on" for the new node set's own buckets
|
|
723
|
-
renderLegend();
|
|
724
|
-
fitToVisible();
|
|
725
|
-
return true;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Focus the graph on a QUERY ANSWER's own result set — every node it
|
|
729
|
-
// actually resolved to, not just one. Re-walks from the first valid id
|
|
730
|
-
// (recentre's existing mechanism, which already reaches most/all closely
|
|
731
|
-
// related result nodes), then fitToIds() precisely frames the full
|
|
732
|
-
// requested set — any id recentre's walk didn't reach simply has no
|
|
733
|
-
// position and drops out of the fit, an honest degrade, never a guess.
|
|
734
|
-
// Sets highlightIds so draw() rings every result node, not just the
|
|
735
|
-
// primary one selectedId/GRAPH.focus already mark.
|
|
736
|
-
function frameQueryResult(ids) {
|
|
737
|
-
var real = (ids || []).filter(function (id) { return walkGraph().byId.has(id); });
|
|
738
|
-
if (!real.length) return false;
|
|
739
|
-
if (!recentre(real[0])) return false;
|
|
740
|
-
fitToIds(real);
|
|
741
|
-
highlightIds = new Set(real);
|
|
742
|
-
selectedId = real[0];
|
|
743
|
-
return true;
|
|
744
|
-
}
|
|
745
|
-
document.getElementById("edgekind").addEventListener("change", function (ev) {
|
|
746
|
-
edgeKindMode = ev.target.value;
|
|
747
|
-
var seed = GRAPH.focus;
|
|
748
|
-
if (seed) recentre(seed);
|
|
749
|
-
draw();
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// ---- click-to-inspect ------------------------------------------------------
|
|
753
|
-
var panel = document.getElementById("panel");
|
|
754
|
-
function fmtTs(v) { return v ? String(v) : "(none)"; }
|
|
755
|
-
function esc(s) {
|
|
756
|
-
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
757
|
-
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function showPanel(n) {
|
|
762
|
-
selectedId = n.id;
|
|
763
|
-
panel.innerHTML =
|
|
764
|
-
"<h2>" + esc(n.label) + "</h2>" +
|
|
765
|
-
"<dl>" +
|
|
766
|
-
"<dt>id</dt><dd>" + esc(n.id) + "</dd>" +
|
|
767
|
-
"<dt>class</dt><dd>" + (n.class
|
|
768
|
-
? '<button class="lnk" id="classLink" title="isolate this class in the view, and ask the graph where the term appears">' + esc(n.class) + "</button>"
|
|
769
|
-
: "(none)") + "</dd>" +
|
|
770
|
-
"<dt>hop</dt><dd>" + n.hop + "</dd>" +
|
|
771
|
-
"<dt>created</dt><dd>" + esc(fmtTs(n.createdAt)) + "</dd>" +
|
|
772
|
-
"<dt>updated</dt><dd>" + esc(fmtTs(n.updatedAt)) + "</dd>" +
|
|
773
|
-
"</dl>" +
|
|
774
|
-
'<div class="row">' +
|
|
775
|
-
'<button class="act" id="labelLink" title="ask the graph where this specific label is mentioned">search this</button>' +
|
|
776
|
-
'<button class="act" id="panelClose">close</button>' +
|
|
777
|
-
"</div>";
|
|
778
|
-
panel.classList.add("show");
|
|
779
|
-
document.getElementById("panelClose").addEventListener("click", function () {
|
|
780
|
-
panel.classList.remove("show"); selectedId = null; draw();
|
|
781
|
-
});
|
|
782
|
-
// Class badge: a click-to-query affordance — isolate this class in the
|
|
783
|
-
// type-filter row (a REAL "show all of that kind currently in view"
|
|
784
|
-
// action; ask.mjs has no generic "list all X of class Y" shape for
|
|
785
|
-
// memory-graph classes — that richer machinery lives in chat.mjs's
|
|
786
|
-
// factAnswer cascade, out of this bundle's ask.mjs-only scope) AND fire a
|
|
787
|
-
// real "where is X mentioned" query on the class name — an honest
|
|
788
|
-
// attempt, may miss, never faked.
|
|
789
|
-
var classLink = document.getElementById("classLink");
|
|
790
|
-
if (classLink) classLink.addEventListener("click", function () {
|
|
791
|
-
typeFiltersEl.querySelectorAll("label.typechk").forEach(function (lbl) {
|
|
792
|
-
var on = lbl.dataset.cls === n.class;
|
|
793
|
-
lbl.querySelector("input").checked = on;
|
|
794
|
-
if (on) enabledTypes.add(lbl.dataset.cls); else enabledTypes.delete(lbl.dataset.cls);
|
|
795
|
-
});
|
|
796
|
-
applyFilters();
|
|
797
|
-
askAndPopulate('where is "' + n.class + '" mentioned');
|
|
798
|
-
});
|
|
799
|
-
document.getElementById("labelLink").addEventListener("click", function () {
|
|
800
|
-
askAndPopulate('where is "' + n.label + '" mentioned');
|
|
801
|
-
});
|
|
802
|
-
draw();
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
canvas.addEventListener("click", function (ev) {
|
|
806
|
-
if (dragMoved) return;
|
|
807
|
-
var rect = canvas.getBoundingClientRect();
|
|
808
|
-
var w = screenToWorld(ev.clientX - rect.left, ev.clientY - rect.top);
|
|
809
|
-
var vis = visibleNodeIds();
|
|
810
|
-
var best = null, bestDist = Infinity;
|
|
811
|
-
GRAPH.nodes.forEach(function (n) {
|
|
812
|
-
if (!vis.has(n.id)) return;
|
|
813
|
-
var p = pos.get(n.id);
|
|
814
|
-
if (!p) return;
|
|
815
|
-
var dx = p.x - w.x, dy = p.y - w.y, d = Math.sqrt(dx * dx + dy * dy);
|
|
816
|
-
var st = styleForHop(n.hop, n.class);
|
|
817
|
-
var hitR = Math.max(st.radius, 10) / view.scale;
|
|
818
|
-
if (d <= hitR && d < bestDist) { best = n; bestDist = d; }
|
|
819
|
-
});
|
|
820
|
-
if (best) showPanel(best);
|
|
821
|
-
});
|
|
822
|
-
canvas.addEventListener("dblclick", function (ev) {
|
|
823
|
-
var rect = canvas.getBoundingClientRect();
|
|
824
|
-
var w = screenToWorld(ev.clientX - rect.left, ev.clientY - rect.top);
|
|
825
|
-
var vis = visibleNodeIds();
|
|
826
|
-
var best = null, bestDist = Infinity;
|
|
827
|
-
GRAPH.nodes.forEach(function (n) {
|
|
828
|
-
if (!vis.has(n.id)) return;
|
|
829
|
-
var p = pos.get(n.id);
|
|
830
|
-
if (!p) return;
|
|
831
|
-
var dx = p.x - w.x, dy = p.y - w.y, d = Math.sqrt(dx * dx + dy * dy);
|
|
832
|
-
if (d < bestDist) { best = n; bestDist = d; }
|
|
833
|
-
});
|
|
834
|
-
if (best && recentre(best.id)) { selectedId = best.id; showPanel(GRAPH.nodes.filter(function (n) { return n.id === best.id; })[0]); }
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
// ---- Ask the graph: TWO real engines over the FULL graph, never just the
|
|
838
|
-
// currently-displayed subgraph — tried in order per query:
|
|
839
|
-
// 1. tmctMemoryAsk.factAnswer (src/chat.mjs's REAL memory-graph answer
|
|
840
|
-
// engine, the same one 'npm run chat' uses) — a Fact/definition-shaped
|
|
841
|
-
// question ("what is a dog", "what is a horse used for") answers HERE,
|
|
842
|
-
// which the code-graph engine below cannot. Given an in-memory
|
|
843
|
-
// Backend-B handle carrying the page's own embedded PAYLOAD — ZERO fs
|
|
844
|
-
// I/O (see memory-ask-browser-entry.mjs's own doc comment) — and
|
|
845
|
-
// envelope:null/miss:true, the exact documented "no parse pipeline
|
|
846
|
-
// available" bootstrap path that arms factAnswer's own bare-question
|
|
847
|
-
// regex fallbacks.
|
|
848
|
-
// 2. tmctViz.ask (tmct's code-graph query engine) — generic "where is X
|
|
849
|
-
// mentioned" navigation and code-graph queries. Only reached when (1)
|
|
850
|
-
// is unavailable or didn't hit.
|
|
851
|
-
// A resolved answer's own target re-centres the view — focus follows the
|
|
852
|
-
// answer — for EITHER engine. --------------------------------------------
|
|
853
|
-
var askInput = document.getElementById("askq");
|
|
854
|
-
var askBtn = document.getElementById("asksubmit");
|
|
855
|
-
var askOut = document.getElementById("askresult");
|
|
856
|
-
var memHandle = hasMemEngine ? tmctMemoryAsk.createInMemoryStore() : null;
|
|
857
|
-
if (memHandle) memHandle.payload = PAYLOAD;
|
|
858
|
-
|
|
859
|
-
// Placeholder is a real term from THIS graph, picked once per page load, so
|
|
860
|
-
// the hint stays honest ("what is X" where X actually resolves here) instead
|
|
861
|
-
// of a static example that may not exist in a given repo's graph. Native
|
|
862
|
-
// <input placeholder> behaviour (disappears on focus/typing, reappears when
|
|
863
|
-
// blank) is untouched — this only changes what text it starts with.
|
|
864
|
-
if (hasEngine && FULL_GRAPH) {
|
|
865
|
-
var termLabels = [];
|
|
866
|
-
walkGraph().byId.forEach(function (ind, id) {
|
|
867
|
-
if (id.indexOf("term:") === 0 && ind.label) termLabels.push(ind.label);
|
|
868
|
-
});
|
|
869
|
-
if (termLabels.length) {
|
|
870
|
-
askInput.placeholder = 'what is ' + termLabels[Math.floor(Math.random() * termLabels.length)];
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Best-effort focus-follow for a memory-engine hit: factAnswer returns
|
|
875
|
-
// rendered TEXT, not a list of resolved entity ids (unlike ask.mjs's
|
|
876
|
-
// envelope/matches). Two passes, both real-graph-checked, never a guessed
|
|
877
|
-
// id that doesn't exist:
|
|
878
|
-
// 1. every term node whose label appears in the ANSWER text — this is
|
|
879
|
-
// "the nodes that come back," e.g. "dog is a kind of animal" surfaces
|
|
880
|
-
// BOTH term:dog and term:animal, not just the one the question asked
|
|
881
|
-
// about, so a multi-fact answer highlights its whole result set.
|
|
882
|
-
// 2. if that finds nothing (e.g. a phrasing that doesn't echo a bare term
|
|
883
|
-
// label), fall back to stripping the QUESTION's own crust and trying
|
|
884
|
-
// the remainder as a single term id — the previous behaviour, kept as
|
|
885
|
-
// a fallback rather than replaced.
|
|
886
|
-
function findAnsweredTermIds(query, answerText) {
|
|
887
|
-
if (!hasEngine || !hasMemEngine) return [];
|
|
888
|
-
var hay = " " + String(answerText).toLowerCase() + " ";
|
|
889
|
-
var found = [];
|
|
890
|
-
walkGraph().byId.forEach(function (ind, id) {
|
|
891
|
-
if (id.indexOf("term:") !== 0) return;
|
|
892
|
-
var label = String(ind.label || "").toLowerCase();
|
|
893
|
-
if (label.length < 3) return; // skip too-short/noisy labels (dedupe/precision, not a real cap)
|
|
894
|
-
if (hay.indexOf(" " + label) !== -1 || hay.indexOf(label + " ") !== -1) found.push(id);
|
|
895
|
-
});
|
|
896
|
-
if (found.length) return found;
|
|
897
|
-
var stripped = String(query).toLowerCase()
|
|
898
|
-
.replace(/^(what|where|who|which|does|do|is|are)\b/, "")
|
|
899
|
-
.replace(/\b(is|are|used for|do|does|mean|means|a|an|the)\b/g, " ")
|
|
900
|
-
.replace(/[?.!]+$/, "")
|
|
901
|
-
.replace(/\s+/g, " ")
|
|
902
|
-
.trim();
|
|
903
|
-
if (!stripped) return [];
|
|
904
|
-
var id = "term:" + tmctMemoryAsk.normFactTerm(stripped);
|
|
905
|
-
return walkGraph().byId.has(id) ? [id] : [];
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function runAsk(query) {
|
|
909
|
-
askOut.classList.remove("miss");
|
|
910
|
-
(async function () {
|
|
911
|
-
if (memHandle) {
|
|
912
|
-
var fact = null;
|
|
913
|
-
try { fact = await tmctMemoryAsk.factAnswer(memHandle, query, null, true, {}); } catch { fact = null; }
|
|
914
|
-
if (fact && fact.text) {
|
|
915
|
-
askOut.innerHTML = '<div class="q">"' + esc(query) + '"</div>' + esc(fact.text)
|
|
916
|
-
+ '<div class="src">answered from the full embedded memory graph (not just what\\'s currently drawn)</div>';
|
|
917
|
-
frameQueryResult(findAnsweredTermIds(query, fact.text));
|
|
918
|
-
draw();
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (!hasEngine) {
|
|
923
|
-
askOut.classList.add("miss");
|
|
924
|
-
askOut.innerHTML = '<div class="q">"' + esc(query) + '"</div>no answer engine available.';
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
var t = tmctViz.ask(FULL_GRAPH, query, { contextId: selectedId });
|
|
928
|
-
var envelope = t.tmct_ask || {};
|
|
929
|
-
askOut.classList.toggle("miss", !!envelope.miss);
|
|
930
|
-
var canon = envelope.canonical
|
|
931
|
-
? '<div class="canon">read as: ' + esc(envelope.canonical.english) + "</div>"
|
|
932
|
-
: "";
|
|
933
|
-
askOut.innerHTML = '<div class="q">"' + esc(query) + '"</div>' + esc(t.content) + canon;
|
|
934
|
-
// Focus-follows-answer: frame EVERY real match this answer resolved to
|
|
935
|
-
// (envelope.matches is already the full candidate list ask.mjs itself
|
|
936
|
-
// ranked), never a guess beyond what the engine itself actually returned.
|
|
937
|
-
var targetIds = (envelope.matches || []).map(function (m) { return m.id; }).filter(Boolean);
|
|
938
|
-
frameQueryResult(targetIds);
|
|
939
|
-
draw();
|
|
940
|
-
})();
|
|
941
|
-
}
|
|
942
|
-
function askAndPopulate(query) {
|
|
943
|
-
askInput.value = query;
|
|
944
|
-
runAsk(query);
|
|
945
|
-
}
|
|
946
|
-
if (hasEngine || hasMemEngine) {
|
|
947
|
-
askBtn.addEventListener("click", function () { var q = askInput.value.trim(); if (q) runAsk(q); });
|
|
948
|
-
askInput.addEventListener("keydown", function (ev) { if (ev.key === "Enter") { var q = askInput.value.trim(); if (q) runAsk(q); } });
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
syncDepthUi();
|
|
952
|
-
resize();
|
|
953
|
-
draw();
|
|
954
|
-
})();
|
|
955
|
-
</script>
|
|
956
|
-
</body>
|
|
957
|
-
</html>
|
|
958
|
-
`;
|
|
959
|
-
}
|