@polycode-projects/the-mechanical-code-talker 2.11.5 → 2.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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&hellip;</div>
363
401
  </div>
364
- <aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
365
- <p class="empty">loading memory stats&hellip;</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&hellip;</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("statsPanel");
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);