@polycode-projects/the-mechanical-code-talker 2.11.6 → 2.11.10
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 +52 -51
- package/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +207 -47
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -119
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
|
@@ -139,9 +139,16 @@ const ISA_GENERIC_HEADS = new Set([
|
|
|
139
139
|
"type", "kind", "sort", "form", "class", "variety", "group", "part",
|
|
140
140
|
"piece", "member", "way", "term", "name", "word", "thing", "example",
|
|
141
141
|
"family", "genus", "species", "unit", "series", "set", "list", "number",
|
|
142
|
-
"amount",
|
|
142
|
+
"amount", "body", "mass",
|
|
143
143
|
]);
|
|
144
144
|
const ISA_ARTICLES = new Set(["a", "an", "the"]);
|
|
145
|
+
// The classifier heads an of-chain reads THROUGH to the real class ("a kind
|
|
146
|
+
// of dog" → dog). Any other head before "of" keeps the outer phrase: "a body
|
|
147
|
+
// of ice" states composition, and "a game of skill" is a game — neither
|
|
148
|
+
// makes the of-object the class.
|
|
149
|
+
const ISA_OF_READ_THROUGH = new Set([
|
|
150
|
+
"type", "kind", "sort", "form", "class", "variety", "species", "breed", "genus",
|
|
151
|
+
]);
|
|
145
152
|
|
|
146
153
|
/** The isa lemma of a lead's first sentence, or null. The copula must sit in
|
|
147
154
|
* the first sentence; an of-chain resolves to its final noun ("a kind of
|
|
@@ -158,8 +165,13 @@ export function isaOf(plain, lexicon) {
|
|
|
158
165
|
window.push(word);
|
|
159
166
|
if (window.length >= 6) break;
|
|
160
167
|
}
|
|
161
|
-
|
|
162
|
-
let
|
|
168
|
+
let headWords = window;
|
|
169
|
+
for (let ofIdx = headWords.indexOf("of"); ofIdx > 0; ofIdx = headWords.indexOf("of")) {
|
|
170
|
+
const outer = headWords[ofIdx - 1]?.split("-").pop();
|
|
171
|
+
if (outer && ISA_OF_READ_THROUGH.has(outer)) { headWords = headWords.slice(ofIdx + 1); continue; }
|
|
172
|
+
headWords = headWords.slice(0, ofIdx);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
163
175
|
while (headWords.length && ISA_ARTICLES.has(headWords[0])) headWords = headWords.slice(1);
|
|
164
176
|
if (!headWords.length) return null;
|
|
165
177
|
const headToken = headWords[headWords.length - 1].split("-").pop();
|
|
@@ -570,7 +570,6 @@ ${THEME_TOKENS_CSS}
|
|
|
570
570
|
main { max-width: 920px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
571
571
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--gilt); }
|
|
572
572
|
.titlebar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 1rem; }
|
|
573
|
-
h1 { font-size: 1.4rem; margin: 0; text-wrap: balance; }
|
|
574
573
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
575
574
|
button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
576
575
|
.mode-toggle { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; padding: .4rem .8rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); white-space: nowrap; }
|
|
@@ -646,8 +645,11 @@ ${THEME_TOKENS_CSS}
|
|
|
646
645
|
/* the room-kind icon — filled through the exact same property-aware
|
|
647
646
|
sprite resolver the room's own object cards use, keyed on the room's own
|
|
648
647
|
name (so library/kitchen/garden reach their large TOMLs, everything else
|
|
649
|
-
falls back through room's own ancestor chain to the generic room icon).
|
|
650
|
-
|
|
648
|
+
falls back through room's own ancestor chain to the generic room icon).
|
|
649
|
+
Rendered at 120px — twice a room-object sprite's own 60px frame
|
|
650
|
+
(.sprite-frame below) — so the room itself reads as the biggest thing
|
|
651
|
+
drawn in its own scene. */
|
|
652
|
+
.room-kind-icon { position: absolute; top: .5rem; right: .6rem; width: 120px; height: 120px; opacity: .92; }
|
|
651
653
|
.room-kind-icon svg { width: 100%; height: 100%; display: block; }
|
|
652
654
|
.room-kind-icon:empty { display: none; }
|
|
653
655
|
.sprite-row { display: flex; align-items: flex-end; gap: .9rem .7rem; min-height: 2.5rem; }
|
|
@@ -735,6 +737,19 @@ ${THEME_TOKENS_CSS}
|
|
|
735
737
|
.map-viewport > div { width: 100%; height: 100%; }
|
|
736
738
|
.map-viewport svg { width: 100%; height: 100%; display: block; }
|
|
737
739
|
.map-viewport .empty-note { color: var(--parchment); opacity: .8; }
|
|
740
|
+
/* the play-mode map only: a fixed SQUARE viewport (width pinned to the
|
|
741
|
+
same 190px height above, centered) rather than stretching to the side
|
|
742
|
+
column's own width — the svg still scales to fit inside it either way,
|
|
743
|
+
this just keeps the board's own footprint stable and click-to-enlarge
|
|
744
|
+
honest about what it's enlarging. */
|
|
745
|
+
.map-viewport-fixed { width: 190px; margin: 0 auto; cursor: zoom-in; }
|
|
746
|
+
/* the lights-down map lightbox — the same board, the same roomMapSvg
|
|
747
|
+
output, just drawn bigger over a dimmed backdrop. Closes on a click
|
|
748
|
+
anywhere outside the enlarged board, or Escape. */
|
|
749
|
+
.map-lightbox { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 2.4rem; background: rgba(10, 8, 4, .74); }
|
|
750
|
+
.map-lightbox[hidden] { display: none; }
|
|
751
|
+
.map-lightbox-inner { width: min(70vmin, 640px); height: min(70vmin, 640px); background: var(--baize); border: 3px solid var(--gilt); box-shadow: inset 0 0 0 1px var(--baize-line), 0 12px 48px rgba(0, 0, 0, .5); padding: 16px; box-sizing: border-box; }
|
|
752
|
+
.map-lightbox-inner svg { width: 100%; height: 100%; display: block; }
|
|
738
753
|
.roommap .room-edge { stroke: var(--board-path); stroke-width: 7; }
|
|
739
754
|
.roommap .room-hint { fill: var(--board-path); opacity: .8; }
|
|
740
755
|
.roommap .room-node rect { fill: var(--parchment); stroke: var(--baize-line); stroke-width: 1.5; }
|
|
@@ -764,6 +779,19 @@ ${THEME_TOKENS_CSS}
|
|
|
764
779
|
a quoted manuscript line reporting what "look" would say right now. */
|
|
765
780
|
.caption { background: var(--parchment); border-left: 3px solid var(--gilt); padding: .55rem .7rem; font-size: .86rem; font-style: italic; margin: 0 0 .5rem; }
|
|
766
781
|
.caption:empty { display: none; margin: 0; }
|
|
782
|
+
/* "what would you like to do" — the room's own contextual pills sit
|
|
783
|
+
top-right of the heading, on the header's own row; more pills than fit
|
|
784
|
+
wrap onto further lines still pinned to the right (justify-content:
|
|
785
|
+
flex-end on the inner .pills flex box), so the block grows down and
|
|
786
|
+
toward the left rather than pushing the heading around. */
|
|
787
|
+
.command-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .3rem .6rem; margin: 0 0 .5rem; padding-bottom: .3rem; border-bottom: 1px solid var(--line); }
|
|
788
|
+
.command-head h2 { flex: 0 0 auto; margin: 0; padding: 0; border-bottom: none; }
|
|
789
|
+
/* nowrap on .command-head itself keeps the pills box on the SAME row as
|
|
790
|
+
the heading (top-right); .pills' own min-width: 0 lets IT shrink below
|
|
791
|
+
its natural width so ITS children (not the whole box) are what wraps
|
|
792
|
+
onto further lines, staying right-anchored rather than dropping the
|
|
793
|
+
whole pill row under the heading. */
|
|
794
|
+
.command-head .pills { flex: 1 1 auto; min-width: 0; justify-content: flex-end; margin-bottom: 0; }
|
|
767
795
|
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
|
|
768
796
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
769
797
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
@@ -799,23 +827,34 @@ ${THEME_TOKENS_CSS}
|
|
|
799
827
|
body.preview .side, body.preview .stage-left > .panel, body.preview .controls-row, body.preview .status { display: none; }
|
|
800
828
|
body.preview main { padding: 0; max-width: none; }
|
|
801
829
|
body.preview .stage { display: block; }
|
|
802
|
-
body.preview .eyebrow, body.preview
|
|
830
|
+
body.preview .eyebrow, body.preview .mode-toggle, body.preview #editStage, body.preview .page-note { display: none; }
|
|
803
831
|
</style>
|
|
804
832
|
</head>
|
|
805
833
|
<body>
|
|
806
834
|
<main>
|
|
807
|
-
<div class="eyebrow">tmct · the adventure</div>
|
|
808
835
|
<div class="titlebar">
|
|
809
|
-
<
|
|
836
|
+
<div class="eyebrow">tmct · the adventure</div>
|
|
810
837
|
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
811
838
|
</div>
|
|
812
839
|
<p class="page-note">${escapeHtml(worldPayload.opening)}</p>
|
|
813
840
|
<div class="stage" id="playStage">
|
|
814
841
|
<div class="stage-left">
|
|
842
|
+
<div class="controls-row" id="playControls">
|
|
843
|
+
<button id="resetBtn" type="button" disabled>reset</button>
|
|
844
|
+
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
845
|
+
<button id="stepBtn" type="button" disabled>step</button>
|
|
846
|
+
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="goal-line" id="goalLine"></div>
|
|
849
|
+
<div class="status" id="status">loading the engine…</div>
|
|
815
850
|
<div class="panel goals">
|
|
816
851
|
<h2>quest</h2>
|
|
817
852
|
<div id="goalList"></div>
|
|
818
853
|
</div>
|
|
854
|
+
<div class="panel carrying">
|
|
855
|
+
<h2>satchel</h2>
|
|
856
|
+
<div class="chips" id="carryList"></div>
|
|
857
|
+
</div>
|
|
819
858
|
<div class="room-frame" id="roomFrame">
|
|
820
859
|
<div class="room-plaque mono" id="roomName"></div>
|
|
821
860
|
<div class="room-kind-icon" id="roomKindIcon"></div>
|
|
@@ -825,39 +864,32 @@ ${THEME_TOKENS_CSS}
|
|
|
825
864
|
<div class="you-slot" id="youSlot"></div>
|
|
826
865
|
</div>
|
|
827
866
|
</div>
|
|
828
|
-
<div class="controls-row" id="playControls">
|
|
829
|
-
<button id="resetBtn" type="button" disabled>reset</button>
|
|
830
|
-
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
831
|
-
<button id="stepBtn" type="button" disabled>step</button>
|
|
832
|
-
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
833
|
-
</div>
|
|
834
|
-
<div class="goal-line" id="goalLine"></div>
|
|
835
|
-
<div class="status" id="status">loading the engine…</div>
|
|
836
|
-
<div class="panel carrying">
|
|
837
|
-
<h2>satchel</h2>
|
|
838
|
-
<div class="chips" id="carryList"></div>
|
|
839
|
-
</div>
|
|
840
867
|
<div class="panel command">
|
|
841
|
-
<
|
|
868
|
+
<div class="command-head">
|
|
869
|
+
<h2>What would you like to do</h2>
|
|
870
|
+
<div class="pills" id="pills"></div>
|
|
871
|
+
</div>
|
|
842
872
|
<form class="chatask" id="chatform">
|
|
843
873
|
<span class="prompt mono">tmct></span>
|
|
844
874
|
<input id="chatq" type="text" placeholder="go north" aria-label="Type a command, or ask a question" disabled>
|
|
845
875
|
</form>
|
|
846
876
|
</div>
|
|
877
|
+
</div>
|
|
878
|
+
<aside class="side" aria-label="The adventure's log and chat">
|
|
847
879
|
<div class="panel roommap">
|
|
848
880
|
<h2>the manor, so far</h2>
|
|
849
|
-
<div class="map-viewport"><div id="mapWrap"></div></div>
|
|
881
|
+
<div class="map-viewport map-viewport-fixed" id="mapViewport"><div id="mapWrap"></div></div>
|
|
850
882
|
</div>
|
|
851
|
-
</div>
|
|
852
|
-
<aside class="side" aria-label="The adventure's log and chat">
|
|
853
883
|
<div class="chat">
|
|
854
884
|
<h2>the manor's own account</h2>
|
|
855
885
|
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
856
|
-
<div class="pills" id="pills"></div>
|
|
857
886
|
<div class="caption" id="caption"></div>
|
|
858
887
|
</div>
|
|
859
888
|
</aside>
|
|
860
889
|
</div>
|
|
890
|
+
<div class="map-lightbox" id="mapLightbox" role="dialog" aria-modal="true" aria-label="The manor map, enlarged" hidden>
|
|
891
|
+
<div class="map-lightbox-inner roommap" id="mapLightboxInner"></div>
|
|
892
|
+
</div>
|
|
861
893
|
|
|
862
894
|
<div class="stage editor-stage" id="editStage" aria-label="The world editor">
|
|
863
895
|
<div class="panel edittext">
|
|
@@ -925,6 +957,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
925
957
|
const chatqEl = el("chatq");
|
|
926
958
|
const carryListEl = el("carryList");
|
|
927
959
|
const mapWrapEl = el("mapWrap");
|
|
960
|
+
const mapViewportEl = el("mapViewport");
|
|
961
|
+
const mapLightboxEl = el("mapLightbox");
|
|
962
|
+
const mapLightboxInnerEl = el("mapLightboxInner");
|
|
928
963
|
const goalListEl = el("goalList");
|
|
929
964
|
const editModeBtn = el("editModeBtn");
|
|
930
965
|
const editorTextEl = el("editorText");
|
|
@@ -1120,6 +1155,25 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1120
1155
|
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
1121
1156
|
mapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
1122
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
// ---- the map lightbox — clicking the fixed-square play-mode map redraws
|
|
1160
|
+
// the SAME roomMapSvg output larger, over a dimmed backdrop; clicking the
|
|
1161
|
+
// backdrop (not the board itself) or pressing Escape closes it.
|
|
1162
|
+
function openMapLightbox() {
|
|
1163
|
+
if (!lastSnapshot) return;
|
|
1164
|
+
const svg = roomMapSvg(visitedRoomGraph(lastSnapshot.state, lastSnapshot.visitedRoomIds), false);
|
|
1165
|
+
if (!svg) return;
|
|
1166
|
+
mapLightboxInnerEl.innerHTML = svg;
|
|
1167
|
+
mapLightboxEl.hidden = false;
|
|
1168
|
+
}
|
|
1169
|
+
function closeMapLightbox() {
|
|
1170
|
+
mapLightboxEl.hidden = true;
|
|
1171
|
+
mapLightboxInnerEl.innerHTML = "";
|
|
1172
|
+
}
|
|
1173
|
+
mapViewportEl.addEventListener("click", openMapLightbox);
|
|
1174
|
+
mapLightboxEl.addEventListener("click", (e) => { if (e.target === mapLightboxEl) closeMapLightbox(); });
|
|
1175
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !mapLightboxEl.hidden) closeMapLightbox(); });
|
|
1176
|
+
|
|
1123
1177
|
function renderEditMap(rows, state) {
|
|
1124
1178
|
editMapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, allRoomIds(rows)), true) || '<span class="empty-note">this world defines no rooms</span>';
|
|
1125
1179
|
}
|
|
@@ -117,6 +117,26 @@ export function loadProgressLine(parts) {
|
|
|
117
117
|
: "loading the engine… " + mb(loaded) + " MB";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The "researched this session" panel's own reading of a settled research
|
|
122
|
+
* turn's answer — the passage it read and where it read it, straight off
|
|
123
|
+
* research.mjs's own `renderResearchAnswer` shape (`term — summary (source:
|
|
124
|
+
* research article "title", Simple English Wikipedia, CC BY-SA 4.0 — url)`),
|
|
125
|
+
* never a second fetch: the citation text is already the retrieved passage,
|
|
126
|
+
* this just pulls its pieces apart for display. Returns null on anything
|
|
127
|
+
* else (a miss, a status/stop reply, or text that doesn't open this way) —
|
|
128
|
+
* an honest "nothing to show" rather than a guessed passage.
|
|
129
|
+
*
|
|
130
|
+
* Self-contained, `.toString()`-splice safe — the same discipline every
|
|
131
|
+
* other pure export in this module holds.
|
|
132
|
+
*/
|
|
133
|
+
export function parseResearchAnswer(answer) {
|
|
134
|
+
const text = String(answer || "");
|
|
135
|
+
const m = /^(.*?) — ([\s\S]*?) \(source: research article "([^"]+)", Simple English Wikipedia, CC BY-SA 4\.0 — (\S+)\)/.exec(text);
|
|
136
|
+
if (!m) return null;
|
|
137
|
+
return { term: m[1], passage: m[2], title: m[3], url: m[4] };
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
/**
|
|
121
141
|
* The exported transcript as ONE Markdown document, in the SAME shape the
|
|
122
142
|
* Node CLI/TUI's own .tmct/session-<id>.md writes (session-log-format.mjs,
|
|
@@ -286,6 +306,24 @@ ${THEME_TOKENS_CSS}
|
|
|
286
306
|
.statsPanel .forget-btn:hover { color: var(--ink); }
|
|
287
307
|
.statsPanel .persist-note { color: var(--muted); font-size: .64rem; margin: .4rem 0 0; }
|
|
288
308
|
|
|
309
|
+
/* the "researched this session" panel: its own section under the memory
|
|
310
|
+
stats, filled from window.tmctChat.researchedFactRows() plus each
|
|
311
|
+
settled research turn's own answer text — the passage tmct actually
|
|
312
|
+
read, the article it read it from, and the facts that passage grounded.
|
|
313
|
+
A sibling section, not folded into #statsPanelStats — that div's own
|
|
314
|
+
re-render (renderStatsPanelInto) clears and rebuilds its children on
|
|
315
|
+
every turn, which would wipe this section's own history too if it lived
|
|
316
|
+
inside it. */
|
|
317
|
+
#researchedPanel:not(:empty) { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
|
|
318
|
+
.statsPanel .researched-item { margin: 0 0 .9rem; padding-bottom: .8rem; border-bottom: 1px dashed var(--line); }
|
|
319
|
+
.statsPanel .researched-item:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
|
|
320
|
+
.statsPanel .researched-title { display: block; color: var(--ink); font-weight: 600; }
|
|
321
|
+
.statsPanel .researched-passage { display: block; color: var(--muted); margin: .3rem 0; }
|
|
322
|
+
.statsPanel .researched-link { display: inline-block; margin: 0 0 .3rem; }
|
|
323
|
+
.statsPanel .researched-facts { margin: .3rem 0 0; padding-left: 1.1rem; }
|
|
324
|
+
.statsPanel .researched-facts li { margin: .12rem 0; }
|
|
325
|
+
.statsPanel .researched-none { color: var(--muted); font-style: italic; margin: .3rem 0 0; }
|
|
326
|
+
|
|
289
327
|
@media (max-width: 860px) {
|
|
290
328
|
.statsPanel { display: none; }
|
|
291
329
|
}
|
|
@@ -361,8 +399,9 @@ ${THEME_TOKENS_CSS}
|
|
|
361
399
|
</form>
|
|
362
400
|
<div class="statusline" id="status">loading the engine…</div>
|
|
363
401
|
</div>
|
|
364
|
-
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
|
|
365
|
-
<p class="empty">loading memory stats…</p>
|
|
402
|
+
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory and research">
|
|
403
|
+
<div id="statsPanelStats"><p class="empty">loading memory stats…</p></div>
|
|
404
|
+
<div id="researchedPanel"></div>
|
|
366
405
|
</aside>
|
|
367
406
|
<script src="./chat-browser.bundle.js"></script>
|
|
368
407
|
<script>
|
|
@@ -371,6 +410,7 @@ ${THEME_TOKENS_CSS}
|
|
|
371
410
|
const provBucketFor = ${provBucketFor.toString()};
|
|
372
411
|
const provenanceChipFor = ${provenanceChipFor.toString()};
|
|
373
412
|
const loadProgressLine = ${loadProgressLine.toString()};
|
|
413
|
+
const parseResearchAnswer = ${parseResearchAnswer.toString()};
|
|
374
414
|
const sessionLogTimeOfDay = ${sessionLogTimeOfDay.toString()};
|
|
375
415
|
const sessionLogHeaderMarkdown = ${sessionLogHeaderMarkdown.toString()};
|
|
376
416
|
const sessionLogTurnMarkdown = ${sessionLogTurnMarkdown.toString()};
|
|
@@ -390,7 +430,8 @@ ${THEME_TOKENS_CSS}
|
|
|
390
430
|
const inputEl = el("composerInput");
|
|
391
431
|
const sendBtn = el("composerSend");
|
|
392
432
|
const statusEl = el("status");
|
|
393
|
-
const statsPanelEl = el("
|
|
433
|
+
const statsPanelEl = el("statsPanelStats");
|
|
434
|
+
const researchedPanelEl = el("researchedPanel");
|
|
394
435
|
const wikiModeFieldset = el("wikiMode");
|
|
395
436
|
const wikiModeRadios = Array.prototype.slice.call(wikiModeFieldset.querySelectorAll('input[type="radio"]'));
|
|
396
437
|
const synthSliderEl = el("synthSlider");
|
|
@@ -708,6 +749,98 @@ ${THEME_TOKENS_CSS}
|
|
|
708
749
|
});
|
|
709
750
|
}
|
|
710
751
|
|
|
752
|
+
// ---- researched this session: what "research <topic>" has actually read
|
|
753
|
+
// and grounded so far — each entry pairs the passage a settled research
|
|
754
|
+
// turn's own answer cites (parseResearchAnswer, off the SAME "(source:
|
|
755
|
+
// research article ...)" text the chat bubble already shows) with the real
|
|
756
|
+
// facts that turn stored, read back through window.tmctChat.
|
|
757
|
+
// researchedFactRows(memoryDir) rather than re-deriving them from the
|
|
758
|
+
// answer text — the citation names WHERE tmct read, the fact rows name
|
|
759
|
+
// WHAT it kept, and this panel never invents either from the other.
|
|
760
|
+
const researchedEntries = [];
|
|
761
|
+
const researchedFactKeysSeen = new Set();
|
|
762
|
+
|
|
763
|
+
function renderResearchedPanel() {
|
|
764
|
+
researchedPanelEl.textContent = "";
|
|
765
|
+
researchedPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "researched this session" }));
|
|
766
|
+
if (!researchedEntries.length) {
|
|
767
|
+
const empty = document.createElement("p");
|
|
768
|
+
empty.className = "empty";
|
|
769
|
+
empty.textContent = 'nothing yet — ask it to "research <a topic>" and what it reads, with the passage and the facts it grounded, lands here.';
|
|
770
|
+
researchedPanelEl.appendChild(empty);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
for (const entry of researchedEntries.slice(-8).reverse()) {
|
|
774
|
+
const item = document.createElement("div");
|
|
775
|
+
item.className = "researched-item";
|
|
776
|
+
const title = document.createElement("span");
|
|
777
|
+
title.className = "researched-title";
|
|
778
|
+
title.textContent = entry.title || "(untitled)";
|
|
779
|
+
item.appendChild(title);
|
|
780
|
+
if (entry.passage) {
|
|
781
|
+
const passage = document.createElement("span");
|
|
782
|
+
passage.className = "researched-passage";
|
|
783
|
+
passage.textContent = entry.passage;
|
|
784
|
+
item.appendChild(passage);
|
|
785
|
+
}
|
|
786
|
+
if (entry.url) {
|
|
787
|
+
const link = document.createElement("a");
|
|
788
|
+
link.className = "researched-link";
|
|
789
|
+
link.href = entry.url;
|
|
790
|
+
link.target = "_blank";
|
|
791
|
+
link.rel = "noopener noreferrer";
|
|
792
|
+
link.textContent = "source \\u2197";
|
|
793
|
+
item.appendChild(link);
|
|
794
|
+
}
|
|
795
|
+
if (entry.facts.length) {
|
|
796
|
+
const list = document.createElement("ul");
|
|
797
|
+
list.className = "researched-facts";
|
|
798
|
+
for (const fact of entry.facts) {
|
|
799
|
+
const li = document.createElement("li");
|
|
800
|
+
li.textContent = fact.subject + " " + fact.predicate + " " + fact.object;
|
|
801
|
+
list.appendChild(li);
|
|
802
|
+
}
|
|
803
|
+
item.appendChild(list);
|
|
804
|
+
} else {
|
|
805
|
+
const none = document.createElement("p");
|
|
806
|
+
none.className = "researched-none";
|
|
807
|
+
none.textContent = "no new fact grounded from this passage.";
|
|
808
|
+
item.appendChild(none);
|
|
809
|
+
}
|
|
810
|
+
researchedPanelEl.appendChild(item);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** After a settled, non-miss research turn: read back the facts that turn
|
|
815
|
+
* actually stored (a set-diff against every research fact seen so far, so
|
|
816
|
+
* a later step never re-lists an earlier one's facts) and pair them with
|
|
817
|
+
* this turn's own cited passage. A turn that grounded nothing new (an
|
|
818
|
+
* empty article, or a re-fetch of an already-known one) still gets its
|
|
819
|
+
* own entry — the passage was still read, even where nothing new stuck. */
|
|
820
|
+
async function noteResearchLearned(result) {
|
|
821
|
+
if (result.research === undefined || !result.record || result.record.miss) return;
|
|
822
|
+
if (!window.tmctChat.researchedFactRows || !window.tmctChatSession) return;
|
|
823
|
+
let rows;
|
|
824
|
+
try { rows = await window.tmctChat.researchedFactRows(window.tmctChatSession.memoryDir); }
|
|
825
|
+
catch { return; }
|
|
826
|
+
const newFacts = [];
|
|
827
|
+
for (const row of rows) {
|
|
828
|
+
const key = row.subject + "|" + row.predicate + "|" + row.object;
|
|
829
|
+
if (researchedFactKeysSeen.has(key)) continue;
|
|
830
|
+
researchedFactKeysSeen.add(key);
|
|
831
|
+
newFacts.push(row);
|
|
832
|
+
}
|
|
833
|
+
const parsed = parseResearchAnswer(result.answer);
|
|
834
|
+
if (!parsed && !newFacts.length) return;
|
|
835
|
+
researchedEntries.push({
|
|
836
|
+
title: parsed ? parsed.title : "",
|
|
837
|
+
passage: parsed ? parsed.passage : "",
|
|
838
|
+
url: parsed ? parsed.url : "",
|
|
839
|
+
facts: newFacts,
|
|
840
|
+
});
|
|
841
|
+
renderResearchedPanel();
|
|
842
|
+
}
|
|
843
|
+
|
|
711
844
|
// "supplement" (typed /wiki supplement only) has no radio; the statusline
|
|
712
845
|
// still names it, read straight off the session's own liveReference getter
|
|
713
846
|
// rather than the last radio the page itself set.
|
|
@@ -791,6 +924,7 @@ ${THEME_TOKENS_CSS}
|
|
|
791
924
|
// that changed nothing costs at most one coalesced write.
|
|
792
925
|
if (result.record && result.record.via !== "command") scheduleSave();
|
|
793
926
|
await renderStatsPanel(); // a teach or learned-load turn grew this session's memory; a plain ask leaves it unchanged either way
|
|
927
|
+
await noteResearchLearned(result);
|
|
794
928
|
} catch (err) {
|
|
795
929
|
settleAssistantBubble(pendingRow,
|
|
796
930
|
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
@@ -1018,6 +1152,18 @@ ${THEME_TOKENS_CSS}
|
|
|
1018
1152
|
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats, bandLabelFor)
|
|
1019
1153
|
+ "." + restoredNote + " Ask it something, or teach it a fact of your own.");
|
|
1020
1154
|
await renderStatsPanel(stats);
|
|
1155
|
+
// A restored session may already carry earlier research facts (they
|
|
1156
|
+
// persist with everything else this session taught) — seed the
|
|
1157
|
+
// seen-set from them so a later research turn only reports what's
|
|
1158
|
+
// actually new, without fabricating passages for a visit this page
|
|
1159
|
+
// was never open to read.
|
|
1160
|
+
if (window.tmctChat.researchedFactRows) {
|
|
1161
|
+
try {
|
|
1162
|
+
const existingResearch = await window.tmctChat.researchedFactRows(window.tmctChatSession.memoryDir);
|
|
1163
|
+
for (const row of existingResearch) researchedFactKeysSeen.add(row.subject + "|" + row.predicate + "|" + row.object);
|
|
1164
|
+
} catch { /* best-effort seeding only — a fresh session has none to seed */ }
|
|
1165
|
+
}
|
|
1166
|
+
renderResearchedPanel();
|
|
1021
1167
|
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmctChat.vocabExampleHint(false);
|
|
1022
1168
|
renderStatus();
|
|
1023
1169
|
setBusy(false);
|