@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +5 -1
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +381 -310
- package/src/services/chat.mjs +273 -155
- package/src/services/code-explorer-viz.mjs +141 -54
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +134 -9
- package/src/services/ledger-viz.mjs +7 -4
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +441 -206
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +63 -4
- package/src/services/research-viz.mjs +18 -7
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +2 -2
- package/src/services/sprite-catalog-viz.mjs +303 -78
- package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
- package/src/surfaces/web/chat-browser-entry.mjs +37 -10
- package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
- package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
- package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +38 -7
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +33 -2
- package/src/surfaces/web/research-browser-entry.mjs +11 -19
- package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
- package/src/surfaces/web/tmct-surface.mjs +12 -0
- package/src/surfaces/web/turn-session.mjs +10 -3
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
// to public/chat.html, after chat-browser.bundle.js/chat-seed.json already
|
|
30
30
|
// exist (both built earlier in that same script, for the embedded widget).
|
|
31
31
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
32
|
+
import {
|
|
33
|
+
SHARE_OVERLAY_CSS, shareOverlayHtml, shareStepStates, activeWaves, offerBlobIn, peerTerm,
|
|
34
|
+
shareMessageFor, replyMessageFor, whatsAppShareUrl, isProbablyMobile, copyTextToClipboard, flashCopyTip,
|
|
35
|
+
} from "./share-overlay-viz.mjs";
|
|
32
36
|
import { provBucketFor } from "./ledger-viz.mjs";
|
|
33
37
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
34
38
|
import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
|
|
@@ -139,7 +143,7 @@ export function wireStateLabel(state) {
|
|
|
139
143
|
tone: "failed",
|
|
140
144
|
pill: "can't connect",
|
|
141
145
|
word: "couldn't connect",
|
|
142
|
-
note: "your two machines
|
|
146
|
+
note: "your two machines couldn't find a path to each other. a public STUN server helps with most networks, but some firewalls or strict NATs still block it.",
|
|
143
147
|
};
|
|
144
148
|
default:
|
|
145
149
|
return {
|
|
@@ -199,14 +203,17 @@ export function tapeClock() {
|
|
|
199
203
|
|
|
200
204
|
/**
|
|
201
205
|
* The node list: every peer this graph knows about, each with the node name it
|
|
202
|
-
* chose
|
|
206
|
+
* chose, the timestamp of the most recent fact it contributed, and that fact
|
|
207
|
+
* itself (`lastFact` — subject/predicate/object, null before any lands), most
|
|
203
208
|
* recently active first.
|
|
204
209
|
*
|
|
205
210
|
* Activity is read off the provenance the wire already carries. A fact a peer
|
|
206
|
-
* broadcast arrives tagged `teach:peer:<their node name>@<ts>`,
|
|
207
|
-
* names both who contributed it and when — no separate activity
|
|
208
|
-
* keep.
|
|
209
|
-
*
|
|
211
|
+
* broadcast arrives tagged `teach:peer:<their node name>#node:<their id>@<ts>`,
|
|
212
|
+
* so the tag names both who contributed it and when — no separate activity
|
|
213
|
+
* ledger to keep. The `#node:` segment is identity and never reaches the
|
|
214
|
+
* screen; a tag from a peer old enough not to carry one is just a name. This
|
|
215
|
+
* node's own row reads its local `teach:`/`ace:` tags instead, because a fact
|
|
216
|
+
* never leaves here relabelled in its own store.
|
|
210
217
|
*
|
|
211
218
|
* `nameFor` and `latestTimestampOf` are injected (the room's own
|
|
212
219
|
* `displayNameFor` and the P2P layer's `latestProvenanceTimestamp`) rather
|
|
@@ -215,20 +222,48 @@ export function tapeClock() {
|
|
|
215
222
|
*/
|
|
216
223
|
export function nodeRowsFor({ peers, factRows, myPeerId, myDisplayName, nameFor, latestTimestampOf }) {
|
|
217
224
|
const activeByName = new Map();
|
|
225
|
+
const factByName = new Map();
|
|
226
|
+
const factAtByName = new Map();
|
|
218
227
|
let mineLastActive = null;
|
|
228
|
+
let mineLastFact = null;
|
|
229
|
+
let mineLastFactAt = null;
|
|
230
|
+
// The P2P layer's own bookkeeping rows (names, claims, waves, who invited
|
|
231
|
+
// whom) key on raw node ids, and the roster's rule is that no id ever
|
|
232
|
+
// reaches the screen — they still count as activity, they just never show
|
|
233
|
+
// as "last shared". The fact keeps its own newest-timestamp race, apart
|
|
234
|
+
// from activity, so a fresh wave can never mask an older real fact.
|
|
235
|
+
const BOOKKEEPING = ["mgx:worldName", "mgx:nodeName", "mgx:playedBy", "mgx:waved", "mgx:invitedBy"];
|
|
236
|
+
const factOf = (row) => (BOOKKEEPING.indexOf(row.predicate) >= 0
|
|
237
|
+
? null
|
|
238
|
+
: { subject: row.subject, predicate: row.predicate, object: row.object });
|
|
219
239
|
for (const row of factRows || []) {
|
|
220
240
|
for (const segment of String(row.provenance || "").split(" | ")) {
|
|
221
241
|
if (!segment) continue;
|
|
222
242
|
const at = latestTimestampOf(segment);
|
|
223
243
|
if (at === null) continue;
|
|
244
|
+
const fact = factOf(row);
|
|
224
245
|
if (segment.indexOf("teach:peer:") === 0) {
|
|
225
246
|
const marker = segment.lastIndexOf("@");
|
|
226
247
|
if (marker < 0) continue;
|
|
227
|
-
const
|
|
248
|
+
const label = segment.slice("teach:peer:".length, marker);
|
|
249
|
+
// Everything from the `#` on is the origin's stable node id, which
|
|
250
|
+
// keys the fact but is never shown: the roster matches on the name a
|
|
251
|
+
// peer chose, and that is the part before it.
|
|
252
|
+
const node = label.indexOf("#node:");
|
|
253
|
+
const name = node < 0 ? label : label.slice(0, node);
|
|
228
254
|
const prior = activeByName.get(name);
|
|
229
255
|
if (prior === undefined || at > prior) activeByName.set(name, at);
|
|
256
|
+
const factPrior = factAtByName.get(name);
|
|
257
|
+
if (fact && (factPrior === undefined || at > factPrior)) {
|
|
258
|
+
factAtByName.set(name, at);
|
|
259
|
+
factByName.set(name, fact);
|
|
260
|
+
}
|
|
230
261
|
} else if (segment.indexOf("teach:") === 0 || segment.indexOf("ace:") === 0) {
|
|
231
262
|
if (mineLastActive === null || at > mineLastActive) mineLastActive = at;
|
|
263
|
+
if (fact && (mineLastFactAt === null || at > mineLastFactAt)) {
|
|
264
|
+
mineLastFactAt = at;
|
|
265
|
+
mineLastFact = fact;
|
|
266
|
+
}
|
|
232
267
|
}
|
|
233
268
|
}
|
|
234
269
|
}
|
|
@@ -238,6 +273,7 @@ export function nodeRowsFor({ peers, factRows, myPeerId, myDisplayName, nameFor,
|
|
|
238
273
|
connected: true,
|
|
239
274
|
isSelf: true,
|
|
240
275
|
lastActiveAt: mineLastActive,
|
|
276
|
+
lastFact: mineLastFact,
|
|
241
277
|
}];
|
|
242
278
|
for (const peer of peers || []) {
|
|
243
279
|
const name = nameFor(peer.peerId);
|
|
@@ -247,6 +283,7 @@ export function nodeRowsFor({ peers, factRows, myPeerId, myDisplayName, nameFor,
|
|
|
247
283
|
connected: Boolean(peer.connected),
|
|
248
284
|
isSelf: false,
|
|
249
285
|
lastActiveAt: activeByName.has(name) ? activeByName.get(name) : null,
|
|
286
|
+
lastFact: factByName.has(name) ? factByName.get(name) : null,
|
|
250
287
|
});
|
|
251
288
|
}
|
|
252
289
|
rows.sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0));
|
|
@@ -586,7 +623,7 @@ ${THEME_TOKENS_CSS}
|
|
|
586
623
|
.fact-pill { display: inline-flex; align-items: baseline; gap: .34rem; font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--corpus-t1); border-radius: 99px; padding: .2rem .8rem; background: var(--corpus-soft); white-space: nowrap; }
|
|
587
624
|
.fact-pill .fact-pill-value { font-size: .96rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
|
|
588
625
|
|
|
589
|
-
.chrome { display: flex; align-items: center; gap: .4rem; }
|
|
626
|
+
.chrome { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
|
|
590
627
|
.chrome-btn { font-family: ${SERIF_STACK}; font-size: .8rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .22rem .75rem; background: var(--card); text-decoration: none; display: inline-flex; align-items: center; gap: .32rem; white-space: nowrap; line-height: 1.35; }
|
|
591
628
|
.chrome-btn:hover { color: var(--ink); border-color: var(--ink); }
|
|
592
629
|
.chrome-btn.share { color: var(--ink); }
|
|
@@ -602,111 +639,9 @@ ${THEME_TOKENS_CSS}
|
|
|
602
639
|
.state-pill[data-tone="failed"] { color: var(--alert); border-color: var(--alert); }
|
|
603
640
|
.state-pill[data-tone="failed"] .pill-dot { background: var(--alert); }
|
|
604
641
|
|
|
605
|
-
/* ---- the network rail --------------------------------------------------
|
|
606
|
-
The docks encode the two halves of a shared graph: the room on the left
|
|
607
|
-
(who else holds this graph, and what is crossing the wire between you),
|
|
608
|
-
the mind on the right (what it knows), the conversation between them.
|
|
609
|
-
Mono throughout — the conversation is prose, the network is telemetry,
|
|
610
|
-
and the register shift is the point. */
|
|
611
|
-
.netPanel { flex: 0 0 288px; max-width: 288px; overflow-y: auto; border-right: 1px solid var(--line); padding: 1rem 1rem 1.6rem; font-family: ${MONO_STACK}; font-size: .72rem; line-height: 1.5; display: flex; flex-direction: column; gap: 1.15rem; }
|
|
612
|
-
.net-block { display: flex; flex-direction: column; gap: .45rem; }
|
|
613
|
-
.netPanel h2 { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin: 0; display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
|
|
614
|
-
.netPanel h2 .h2-count { letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums; }
|
|
615
|
-
.net-field { display: flex; flex-direction: column; gap: .2rem; }
|
|
616
|
-
.net-label { font-family: ${SERIF_STACK}; font-size: .74rem; color: var(--muted); }
|
|
617
|
-
.net-name-input { font-family: ${SERIF_STACK}; font-size: .86rem; color: var(--ink); background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .32rem .6rem; width: 100%; box-sizing: border-box; }
|
|
618
|
-
.net-name-input:read-only { background: none; border-color: transparent; padding-left: 0; color: var(--muted); }
|
|
619
|
-
.net-note { font-family: ${SERIF_STACK}; font-size: .74rem; color: var(--muted); margin: 0; }
|
|
620
|
-
.net-help { font-family: ${SERIF_STACK}; font-size: .74rem; color: var(--corpus); }
|
|
621
|
-
.net-btn { font-family: ${SERIF_STACK}; font-size: .82rem; line-height: 1.35; color: var(--ink); border: 1px solid var(--line); border-radius: 99px; padding: .32rem .85rem; background: var(--card); align-self: flex-start; }
|
|
622
|
-
.net-btn:hover { border-color: var(--ink); }
|
|
623
|
-
.net-btn:disabled { opacity: .5; cursor: default; }
|
|
624
|
-
.net-btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
625
|
-
.net-btn.ghost { border-color: transparent; color: var(--muted); padding-left: 0; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 3px; }
|
|
626
|
-
.net-btn.ghost:hover { color: var(--ink); text-decoration-color: var(--ink); }
|
|
627
|
-
/* the blobs stay mono: they are machine text a person only ever copies, and
|
|
628
|
-
a serif face on base64 is a lie about what it is. */
|
|
629
|
-
.net-blob { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .58rem; line-height: 1.35; color: var(--muted); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: .4rem .5rem; resize: vertical; word-break: break-all; }
|
|
630
|
-
.net-blob:focus { color: var(--ink); }
|
|
631
|
-
.net-problem { margin: 0; font-family: ${SERIF_STACK}; font-size: .78rem; color: var(--alert); border-left: 2px solid var(--alert); padding-left: .5rem; }
|
|
632
|
-
.net-invite { display: flex; flex-direction: column; gap: .4rem; padding-top: .2rem; }
|
|
633
|
-
|
|
634
|
-
/* connection state, as a real visual state at every point: a calm slow
|
|
635
|
-
breath while nothing is in flight, a quicker one while ICE runs, a solid
|
|
636
|
-
green edge when the channel is open, and a static dashed red when it
|
|
637
|
-
failed — a fault should not twitch. */
|
|
638
|
-
.wire-state { position: relative; overflow: hidden; border: 1px solid var(--line); border-left-width: 3px; border-radius: 8px; padding: .55rem .7rem .6rem .75rem; background: var(--card); font-family: ${SERIF_STACK}; }
|
|
639
|
-
.wire-state-word { display: block; font-size: .95rem; color: var(--ink); }
|
|
640
|
-
.wire-state-note { display: block; margin-top: .25rem; font-size: .74rem; line-height: 1.4; color: var(--muted); }
|
|
641
|
-
.wire-state[data-tone="waiting"], .wire-state[data-tone="working"] { border-left-color: var(--corpus); }
|
|
642
|
-
.wire-state[data-tone="waiting"]::before, .wire-state[data-tone="working"]::before { content: ""; position: absolute; left: -3px; top: 0; bottom: 0; width: 3px; background: var(--corpus); animation: wire-breathe 2.4s ease-in-out infinite; }
|
|
643
|
-
.wire-state[data-tone="working"]::before { animation-duration: .9s; }
|
|
644
|
-
.wire-state[data-tone="live"] { border-left-color: var(--taught); background: var(--taught-soft); }
|
|
645
|
-
.wire-state[data-tone="live"] .wire-state-word { color: var(--taught); }
|
|
646
|
-
.wire-state[data-tone="failed"] { border-style: dashed; border-left-style: solid; border-left-color: var(--alert); background: var(--alert-soft); }
|
|
647
|
-
.wire-state[data-tone="failed"] .wire-state-word { color: var(--alert); }
|
|
648
642
|
@keyframes wire-breathe { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }
|
|
649
|
-
|
|
650
|
-
/* a member list, the way every chat app already draws one: a monogram, a
|
|
651
|
-
presence badge on it, the name, and when they were last heard from. The
|
|
652
|
-
monogram is neutral on purpose — on this page colour means provenance,
|
|
653
|
-
and an avatar palette would spend that meaning on decoration. */
|
|
654
|
-
.node-list { list-style: none; margin: 0; padding: 0; }
|
|
655
|
-
.node-row { display: flex; align-items: center; gap: .55rem; padding: .3rem 0; }
|
|
656
|
-
.node-avatar { position: relative; flex: 0 0 auto; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--line); color: var(--muted); display: flex; align-items: center; justify-content: center; font-family: ${MONO_STACK}; font-size: .58rem; letter-spacing: .04em; text-transform: uppercase; }
|
|
657
|
-
.node-row[data-self="true"] .node-avatar { background: var(--ink); color: var(--bg); }
|
|
658
|
-
.node-dot { position: absolute; right: -1px; bottom: -1px; width: 7px; height: 7px; border-radius: 50%; background: var(--taught); box-shadow: 0 0 0 2px var(--bg); }
|
|
659
|
-
.node-row[data-away="true"] .node-dot { background: var(--muted); }
|
|
660
|
-
.node-name { font-family: ${SERIF_STACK}; color: var(--ink); font-size: .86rem; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
|
|
661
|
-
.node-row[data-self="true"] .node-name::after { content: " (you)"; color: var(--muted); font-size: .74rem; }
|
|
662
|
-
.node-row[data-away="true"] .node-name { color: var(--muted); }
|
|
663
|
-
.node-when { color: var(--muted); font-family: ${MONO_STACK}; font-size: .62rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
664
|
-
.node-hand { display: inline-block; font-size: .88rem; opacity: 0; transform-origin: 70% 80%; }
|
|
665
|
-
.node-row[data-waving="true"] .node-hand { opacity: 1; animation: hand-wave .8s ease-in-out infinite; }
|
|
666
|
-
.node-empty { font-family: ${SERIF_STACK}; color: var(--muted); font-size: .76rem; line-height: 1.4; margin: 0; }
|
|
667
643
|
@keyframes hand-wave { 0%, 100% { transform: rotate(-14deg); } 50% { transform: rotate(20deg); } }
|
|
668
644
|
|
|
669
|
-
/* the wire tape — this page's own instrument. Every message in and out, in
|
|
670
|
-
arrival order, newest first so the latest is readable without scrolling.
|
|
671
|
-
The 3px bar carries the message family in the page's own provenance
|
|
672
|
-
colours; the type is spelled out beside it, so colour is never the only
|
|
673
|
-
thing distinguishing one row from another. */
|
|
674
|
-
.net-tape-block { flex: 1 1 auto; min-height: 0; }
|
|
675
|
-
.tape-meter { display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin: 0 0 .3rem; }
|
|
676
|
-
.meter-item { display: inline-flex; align-items: center; gap: .3rem; font-size: .6rem; color: var(--muted); }
|
|
677
|
-
.meter-bar { width: 5px; height: 5px; border-radius: 1px; flex: 0 0 auto; }
|
|
678
|
-
.meter-n { color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
679
|
-
.tape { list-style: none; margin: 0; padding: 0; max-height: 20rem; overflow-y: auto; border-top: 1px solid var(--line); }
|
|
680
|
-
.tape-row { display: grid; grid-template-columns: 3px auto minmax(0, 1fr) auto; align-items: center; gap: .4rem; padding: .18rem 0 .18rem .2rem; border-bottom: 1px dotted var(--line); font-size: .6rem; font-variant-numeric: tabular-nums; }
|
|
681
|
-
.tape-row:first-child { animation: tape-arrive .6s ease-out; }
|
|
682
|
-
.tape-bar { align-self: stretch; border-radius: 1px; background: var(--muted); }
|
|
683
|
-
.tape-clock { color: var(--muted); }
|
|
684
|
-
.tape-type { color: var(--ink); overflow-wrap: anywhere; }
|
|
685
|
-
.tape-detail { color: var(--muted); text-align: right; white-space: nowrap; }
|
|
686
|
-
.tape-row[data-dir="out"] .tape-type::before { content: "\\2192 "; color: var(--muted); }
|
|
687
|
-
.tape-row[data-dir="in"] .tape-type::before { content: "\\2190 "; color: var(--muted); }
|
|
688
|
-
.tape-row[data-dir="note"] .tape-type::before { content: "\\00b7 "; color: var(--muted); }
|
|
689
|
-
.tape-row[data-family="facts"] .tape-bar { background: var(--taught); }
|
|
690
|
-
.tape-row[data-family="state"] .tape-bar { background: var(--corpus); }
|
|
691
|
-
.tape-row[data-family="greeting"] .tape-bar { background: var(--entail); }
|
|
692
|
-
.tape-row[data-family="signal"] .tape-bar { background: var(--entail-t1); }
|
|
693
|
-
.tape-row[data-family="fault"] .tape-bar { background: var(--alert); }
|
|
694
|
-
.tape-row[data-family="fault"] .tape-type { color: var(--alert); }
|
|
695
|
-
.tape-empty { font-family: ${SERIF_STACK}; color: var(--muted); font-size: .76rem; line-height: 1.4; padding: .45rem 0 0; margin: 0; }
|
|
696
|
-
@keyframes tape-arrive { from { background: var(--corpus-soft); } to { background: transparent; } }
|
|
697
|
-
|
|
698
|
-
.netPanel-close { display: none; align-self: flex-end; font-family: ${MONO_STACK}; font-size: .8rem; color: var(--muted); padding: 0 .2rem; }
|
|
699
|
-
|
|
700
|
-
/* the join card: the only thing a joiner sees until they act. The world's
|
|
701
|
-
generated two-word name is the one place it gets to be a headline. */
|
|
702
|
-
.joinCard { position: fixed; inset: 0; z-index: 40; background: rgba(0, 0, 0, .45); display: flex; align-items: center; justify-content: center; padding: 1.2rem; }
|
|
703
|
-
.joinCard-inner { background: var(--card); border: 1px solid var(--line); border-radius: 8px; width: 100%; max-width: 27rem; padding: 1.5rem 1.6rem 1.3rem; box-shadow: 0 18px 48px rgba(0, 0, 0, .3); display: flex; flex-direction: column; gap: .8rem; }
|
|
704
|
-
.joinCard-eyebrow { font-size: .82rem; color: var(--muted); margin: 0; }
|
|
705
|
-
.joinCard-world { font-size: 1.75rem; line-height: 1.1; margin: -.45rem 0 0; color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }
|
|
706
|
-
.joinCard-body { font-size: .9rem; color: var(--muted); margin: 0; max-width: 36ch; }
|
|
707
|
-
.joinCard .net-btn.primary { font-size: .95rem; padding: .5rem 1.15rem; }
|
|
708
|
-
.joinCard-reply { display: flex; flex-direction: column; gap: .45rem; }
|
|
709
|
-
|
|
710
645
|
/* a wave, on every page it reaches: the waver's node name, over the
|
|
711
646
|
conversation, for as long as the wave is recent. Anchored under the
|
|
712
647
|
topbar, where a chat app puts a presence toast — the foot of the column
|
|
@@ -714,17 +649,10 @@ ${THEME_TOKENS_CSS}
|
|
|
714
649
|
.waveBurst { position: absolute; left: 50%; top: 3.4rem; transform: translateX(-50%); z-index: 20; display: flex; flex-direction: column; align-items: center; gap: .3rem; pointer-events: none; }
|
|
715
650
|
.wave-pill { display: flex; align-items: center; gap: .45rem; background: var(--card); border: 1px solid var(--line); border-radius: 99px; padding: .3rem .9rem .3rem .7rem; font-family: ${SERIF_STACK}; font-size: .85rem; color: var(--ink); box-shadow: 0 4px 14px rgba(0, 0, 0, .16); animation: wave-rise .3s ease-out; }
|
|
716
651
|
.wave-pill .hand { display: inline-block; font-size: 1rem; transform-origin: 70% 80%; animation: hand-wave .8s ease-in-out infinite; }
|
|
652
|
+
/* a wave aimed at you, specifically: the same pill, insistently louder. */
|
|
653
|
+
.wave-pill--you { border-color: var(--taught); box-shadow: 0 4px 14px rgba(0, 0, 0, .16), 0 0 0 3px var(--taught-soft); font-weight: 600; }
|
|
717
654
|
@keyframes wave-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
718
655
|
|
|
719
|
-
.copyTip { position: fixed; z-index: 60; background: var(--ink); color: var(--bg); font-family: ${SERIF_STACK}; font-size: .78rem; padding: .3rem .7rem; border-radius: 99px; pointer-events: none; box-shadow: 0 3px 10px rgba(0, 0, 0, .22); animation: wave-rise .14s ease-out; }
|
|
720
|
-
|
|
721
|
-
@media (max-width: 1080px) {
|
|
722
|
-
/* the rail becomes a drawer rather than disappearing: the invite flow has
|
|
723
|
-
to stay reachable on a laptop and a phone alike. */
|
|
724
|
-
.netPanel { position: fixed; left: 0; top: 0; bottom: 0; width: 288px; max-width: 86vw; flex: none; z-index: 30; background: var(--bg); transform: translateX(-101%); transition: transform .18s ease-out; }
|
|
725
|
-
body.net-open .netPanel { transform: none; box-shadow: 0 0 40px rgba(0, 0, 0, .3); }
|
|
726
|
-
.netPanel-close { display: block; }
|
|
727
|
-
}
|
|
728
656
|
@media (max-width: 1360px) {
|
|
729
657
|
/* the legend is decorative and the controls are not, so the legend goes
|
|
730
658
|
first rather than folding the controls onto a second row. */
|
|
@@ -738,9 +666,8 @@ ${THEME_TOKENS_CSS}
|
|
|
738
666
|
}
|
|
739
667
|
@media (prefers-reduced-motion: reduce) {
|
|
740
668
|
* { scroll-behavior: auto !important; }
|
|
741
|
-
.
|
|
742
|
-
.
|
|
743
|
-
.wave-pill, .wave-pill .hand, .node-row[data-waving="true"] .node-hand, .copyTip { animation: none; }
|
|
669
|
+
.state-pill .pill-dot { animation: none; transition: none; }
|
|
670
|
+
.wave-pill, .wave-pill .hand { animation: none; }
|
|
744
671
|
}
|
|
745
672
|
|
|
746
673
|
/* print: the WHOLE transcript, not the scrolled-into-view slice — the
|
|
@@ -756,70 +683,13 @@ ${THEME_TOKENS_CSS}
|
|
|
756
683
|
main.chatMain { overflow: visible; height: auto; }
|
|
757
684
|
.messages { min-height: 0; }
|
|
758
685
|
form.composer, .statusline, .statsPanel, .legend { display: none; }
|
|
759
|
-
.
|
|
686
|
+
.shareOverlay, .waveBurst, .chrome, .copyTip { display: none; }
|
|
760
687
|
}
|
|
688
|
+
${SHARE_OVERLAY_CSS}
|
|
761
689
|
</style>
|
|
762
690
|
</head>
|
|
763
691
|
<body>
|
|
764
|
-
|
|
765
|
-
<button type="button" class="netPanel-close" id="netPanelClose" aria-label="close the network panel">×</button>
|
|
766
|
-
<section class="net-block">
|
|
767
|
-
<h2>this node</h2>
|
|
768
|
-
<div class="net-field">
|
|
769
|
-
<label class="net-label" for="nodeNameInput">your node name</label>
|
|
770
|
-
<input class="net-name-input" id="nodeNameInput" type="text" autocomplete="off" spellcheck="false"
|
|
771
|
-
placeholder="two words from the graph's own vocabulary">
|
|
772
|
-
</div>
|
|
773
|
-
<div class="net-field">
|
|
774
|
-
<label class="net-label" for="worldNameInput">the graph you are sharing</label>
|
|
775
|
-
<input class="net-name-input" id="worldNameInput" type="text" autocomplete="off" spellcheck="false"
|
|
776
|
-
placeholder="named when you first invite someone">
|
|
777
|
-
</div>
|
|
778
|
-
<p class="net-note">both names are facts in the graph, not settings. change yours whenever you like.</p>
|
|
779
|
-
</section>
|
|
780
|
-
|
|
781
|
-
<section class="net-block">
|
|
782
|
-
<h2>connection</h2>
|
|
783
|
-
<div class="wire-state" id="wireState" data-tone="idle" role="status">
|
|
784
|
-
<span class="wire-state-word" id="wireStateWord">not shared</span>
|
|
785
|
-
<span class="wire-state-note" id="wireStateNote">this browser holds the only copy of what you teach it.</span>
|
|
786
|
-
</div>
|
|
787
|
-
<div class="net-invite" id="sharePanel" hidden>
|
|
788
|
-
<div class="net-field">
|
|
789
|
-
<label class="net-label" for="shareLink">the link, in case the copy didn’t take</label>
|
|
790
|
-
<textarea class="net-blob" id="shareLink" rows="2" readonly></textarea>
|
|
791
|
-
</div>
|
|
792
|
-
<p class="net-note">each link invites one person. invite again for the next.</p>
|
|
793
|
-
<div class="net-field">
|
|
794
|
-
<label class="net-label" for="replyBox">paste their reply here</label>
|
|
795
|
-
<textarea class="net-blob" id="replyBox" rows="3" placeholder="the reply they send back"></textarea>
|
|
796
|
-
</div>
|
|
797
|
-
<button type="button" class="net-btn" id="replyBtn">connect</button>
|
|
798
|
-
<p class="net-problem" id="replyProblem" role="alert" hidden></p>
|
|
799
|
-
</div>
|
|
800
|
-
<div class="net-invite" id="answerPanel" hidden>
|
|
801
|
-
<div class="net-field">
|
|
802
|
-
<label class="net-label" for="replyOut">your reply — send it back the same way the invite reached you</label>
|
|
803
|
-
<textarea class="net-blob" id="replyOut" rows="3" readonly></textarea>
|
|
804
|
-
</div>
|
|
805
|
-
<button type="button" class="net-btn" id="copyReplyBtn">copy it again</button>
|
|
806
|
-
</div>
|
|
807
|
-
<a class="net-help" href="./help.html#sharing" target="_blank" rel="noopener">how sharing works ↗</a>
|
|
808
|
-
</section>
|
|
809
|
-
|
|
810
|
-
<section class="net-block">
|
|
811
|
-
<h2>nodes <span class="h2-count" id="nodeCount"></span></h2>
|
|
812
|
-
<ul class="node-list" id="nodeList"></ul>
|
|
813
|
-
<p class="node-empty" id="nodeEmpty">nobody else yet. invite someone and their node appears here.</p>
|
|
814
|
-
</section>
|
|
815
|
-
|
|
816
|
-
<section class="net-block net-tape-block">
|
|
817
|
-
<h2>wire <span class="h2-count" id="tapeTotal"></span></h2>
|
|
818
|
-
<div class="tape-meter" id="tapeMeter"></div>
|
|
819
|
-
<ol class="tape" id="tape"></ol>
|
|
820
|
-
<p class="tape-empty" id="tapeEmpty">every message this browser sends or receives lands here, as it happens.</p>
|
|
821
|
-
</section>
|
|
822
|
-
</aside>
|
|
692
|
+
${shareOverlayHtml({ withTape: true })}
|
|
823
693
|
<div class="chatCol">
|
|
824
694
|
<header class="topbar">
|
|
825
695
|
<div class="brand">
|
|
@@ -891,22 +761,6 @@ ${THEME_TOKENS_CSS}
|
|
|
891
761
|
<div id="statsPanelStats"><p class="empty">loading memory stats…</p></div>
|
|
892
762
|
<div id="researchedPanel"></div>
|
|
893
763
|
</aside>
|
|
894
|
-
<div class="joinCard" id="joinCard" role="dialog" aria-labelledby="joinWorld" hidden>
|
|
895
|
-
<div class="joinCard-inner">
|
|
896
|
-
<p class="joinCard-eyebrow" id="joinEyebrow">you’ve been invited to</p>
|
|
897
|
-
<h1 class="joinCard-world" id="joinWorld"></h1>
|
|
898
|
-
<p class="joinCard-body" id="joinBody">Nothing has run yet. The button below makes your reply and copies it — send it back the same way this invite reached you, and leave this tab open.</p>
|
|
899
|
-
<button type="button" class="net-btn primary" id="joinBtn">create my reply</button>
|
|
900
|
-
<p class="net-problem" id="joinProblem" role="alert" hidden></p>
|
|
901
|
-
<div class="joinCard-reply" id="joinReplyWrap" hidden>
|
|
902
|
-
<label class="net-label" for="joinReply">your reply, copied — send it back now</label>
|
|
903
|
-
<textarea class="net-blob" id="joinReply" rows="3" readonly></textarea>
|
|
904
|
-
<button type="button" class="net-btn" id="joinCopyBtn">copy it again</button>
|
|
905
|
-
</div>
|
|
906
|
-
<button type="button" class="net-btn ghost" id="joinDismiss">start talking on your own instead</button>
|
|
907
|
-
<a class="net-help" href="./help.html#sharing" target="_blank" rel="noopener">how sharing works ↗</a>
|
|
908
|
-
</div>
|
|
909
|
-
</div>
|
|
910
764
|
<script src="./chat-browser.bundle.js"></script>
|
|
911
765
|
<script>
|
|
912
766
|
(function () {
|
|
@@ -934,6 +788,16 @@ ${THEME_TOKENS_CSS}
|
|
|
934
788
|
const inviteParamsFrom = ${inviteParamsFrom.toString()};
|
|
935
789
|
const tapeClock = ${tapeClock.toString()};
|
|
936
790
|
const relativeWhen = ${relativeWhen.toString()};
|
|
791
|
+
const shareStepStates = ${shareStepStates.toString()};
|
|
792
|
+
const activeWaves = ${activeWaves.toString()};
|
|
793
|
+
const offerBlobIn = ${offerBlobIn.toString()};
|
|
794
|
+
const peerTerm = ${peerTerm.toString()};
|
|
795
|
+
const shareMessageFor = ${shareMessageFor.toString()};
|
|
796
|
+
const replyMessageFor = ${replyMessageFor.toString()};
|
|
797
|
+
const whatsAppShareUrl = ${whatsAppShareUrl.toString()};
|
|
798
|
+
const isProbablyMobile = ${isProbablyMobile.toString()};
|
|
799
|
+
const copyText = ${copyTextToClipboard.toString()};
|
|
800
|
+
const flashTip = ${flashCopyTip.toString()};
|
|
937
801
|
const DIGEST_STRUCTURES = ${digestStructuresJson};
|
|
938
802
|
const el = (id) => document.getElementById(id);
|
|
939
803
|
|
|
@@ -1156,14 +1020,25 @@ ${THEME_TOKENS_CSS}
|
|
|
1156
1020
|
|
|
1157
1021
|
let seedPayload = null;
|
|
1158
1022
|
let seedFacts = 0;
|
|
1023
|
+
// One retry with a cache-busting query param: a CDN edge can serve a
|
|
1024
|
+
// corrupted or truncated precompressed response (a transient bad cache
|
|
1025
|
+
// entry, not a code defect — real bytes decompress fine, and the same URL
|
|
1026
|
+
// fetched moments later is clean), and JSON.parse throwing is the only
|
|
1027
|
+
// signal of that. The bust param forces a fresh fetch past that one entry.
|
|
1159
1028
|
async function fetchSeed() {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1029
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1030
|
+
try {
|
|
1031
|
+
const bust = attempt === 1 ? "" : (SEED_QUERY ? "&" : "?") + "retry=1";
|
|
1032
|
+
const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY + bust, (loaded, total) => noteProgress("seed", loaded, total));
|
|
1033
|
+
seedPayload = JSON.parse(await blob.text());
|
|
1034
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
1035
|
+
return;
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
if (attempt === 2) {
|
|
1038
|
+
seedPayload = null;
|
|
1039
|
+
console.warn("tmct chat: chat-seed.json unavailable — starting unseeded", err);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1167
1042
|
}
|
|
1168
1043
|
}
|
|
1169
1044
|
const cloneSeed = () => {
|
|
@@ -1215,14 +1090,16 @@ ${THEME_TOKENS_CSS}
|
|
|
1215
1090
|
let saveTimer = null;
|
|
1216
1091
|
let restoredCount = 0;
|
|
1217
1092
|
let siteVersion = "dev";
|
|
1093
|
+
let lastStatsTotal = null;
|
|
1218
1094
|
window.tmctChatLastSave = null;
|
|
1095
|
+
window.tmct.lastSave = null;
|
|
1219
1096
|
|
|
1220
1097
|
function scheduleSave() {
|
|
1221
1098
|
if (!persist) return;
|
|
1222
1099
|
clearTimeout(saveTimer);
|
|
1223
1100
|
saveTimer = setTimeout(() => {
|
|
1224
1101
|
saveTimer = null;
|
|
1225
|
-
const session = window.
|
|
1102
|
+
const session = window.tmct.session;
|
|
1226
1103
|
if (!session) return;
|
|
1227
1104
|
const started = performance.now();
|
|
1228
1105
|
let snapshot;
|
|
@@ -1232,7 +1109,10 @@ ${THEME_TOKENS_CSS}
|
|
|
1232
1109
|
try { snapshot = JSON.parse(JSON.stringify(session.memoryDir.payload)); } catch { return; }
|
|
1233
1110
|
}
|
|
1234
1111
|
persist.save(snapshot).then((saved) => {
|
|
1235
|
-
if (saved)
|
|
1112
|
+
if (saved) {
|
|
1113
|
+
window.tmctChatLastSave = { at: Date.now(), ms: Math.round(performance.now() - started) };
|
|
1114
|
+
window.tmct.lastSave = window.tmctChatLastSave;
|
|
1115
|
+
}
|
|
1236
1116
|
});
|
|
1237
1117
|
}, 500);
|
|
1238
1118
|
}
|
|
@@ -1246,8 +1126,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1246
1126
|
// it would keep merging peers' facts into a store nothing reads any more.
|
|
1247
1127
|
// Rejoining is a fresh invite, which is what a dropped node needs anyway.
|
|
1248
1128
|
dropRoom();
|
|
1249
|
-
|
|
1250
|
-
const stats = await window.tmct.page.memoryStats(window.
|
|
1129
|
+
await newSession();
|
|
1130
|
+
const stats = await window.tmct.page.memoryStats(window.tmct.session.memoryDir);
|
|
1251
1131
|
addSystemLine("forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").");
|
|
1252
1132
|
await renderStatsPanel(stats);
|
|
1253
1133
|
}
|
|
@@ -1266,11 +1146,12 @@ ${THEME_TOKENS_CSS}
|
|
|
1266
1146
|
* rather than blanking it. */
|
|
1267
1147
|
async function renderStatsPanel(stats) {
|
|
1268
1148
|
if (!stats) {
|
|
1269
|
-
if (!window.
|
|
1270
|
-
try { stats = await window.tmct.page.memoryStats(window.
|
|
1149
|
+
if (!window.tmct.session || !window.tmct.page.memoryStats) return;
|
|
1150
|
+
try { stats = await window.tmct.page.memoryStats(window.tmct.session.memoryDir); }
|
|
1271
1151
|
catch { return; }
|
|
1272
1152
|
}
|
|
1273
|
-
|
|
1153
|
+
lastStatsTotal = Number(stats.total || 0);
|
|
1154
|
+
factPillValueEl.textContent = lastStatsTotal.toLocaleString();
|
|
1274
1155
|
renderStatsPanelInto(statsPanelEl, stats, {
|
|
1275
1156
|
bandLabel: bandLabelFor,
|
|
1276
1157
|
onForget: persist ? forgetEverything : null,
|
|
@@ -1348,9 +1229,9 @@ ${THEME_TOKENS_CSS}
|
|
|
1348
1229
|
* own entry — the passage was still read, even where nothing new stuck. */
|
|
1349
1230
|
async function noteResearchLearned(result) {
|
|
1350
1231
|
if (result.research === undefined || !result.record || result.record.miss) return;
|
|
1351
|
-
if (!window.tmct.page.researchedFactRows || !window.
|
|
1232
|
+
if (!window.tmct.page.researchedFactRows || !window.tmct.session) return;
|
|
1352
1233
|
let rows;
|
|
1353
|
-
try { rows = await window.tmct.page.researchedFactRows(window.
|
|
1234
|
+
try { rows = await window.tmct.page.researchedFactRows(window.tmct.session.memoryDir); }
|
|
1354
1235
|
catch { return; }
|
|
1355
1236
|
const newFacts = [];
|
|
1356
1237
|
for (const row of rows) {
|
|
@@ -1386,7 +1267,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1386
1267
|
: winkStatus === "unavailable"
|
|
1387
1268
|
? "wink-nlp unavailable — curated + fuzzy tiers only (still zero guesses, zero LLM)"
|
|
1388
1269
|
: "wink-nlp: loading\\u2026";
|
|
1389
|
-
const liveReference = window.
|
|
1270
|
+
const liveReference = window.tmct.session ? window.tmct.session.liveReference : liveReferenceForMode(checkedWikiMode());
|
|
1390
1271
|
const livePart = "live wikipedia: " + liveStatusWord(liveReference);
|
|
1391
1272
|
const netPart = room
|
|
1392
1273
|
? " \\u00b7 world \\u201c" + worldName + "\\u201d: " + wireStateLabel(room.state).word
|
|
@@ -1399,7 +1280,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1399
1280
|
// "supplement" clears every radio (it has none of its own) rather than
|
|
1400
1281
|
// leaving a stale mode checked.
|
|
1401
1282
|
function mirrorWikiModeFromSession() {
|
|
1402
|
-
const session = window.
|
|
1283
|
+
const session = window.tmct.session;
|
|
1403
1284
|
if (!session || typeof session.liveReference === "undefined") return;
|
|
1404
1285
|
const liveReference = session.liveReference;
|
|
1405
1286
|
const mode = liveReference === "always" ? "always" : liveReference === true ? "miss" : liveReference === false ? "off" : null;
|
|
@@ -1410,8 +1291,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1410
1291
|
wikiModeFieldset.addEventListener("change", function () {
|
|
1411
1292
|
const mode = checkedWikiMode();
|
|
1412
1293
|
writeWikiMode(mode);
|
|
1413
|
-
if (window.
|
|
1414
|
-
window.
|
|
1294
|
+
if (window.tmct.session && window.tmct.session.setLiveReference) {
|
|
1295
|
+
window.tmct.session.setLiveReference(liveReferenceForMode(mode));
|
|
1415
1296
|
}
|
|
1416
1297
|
renderStatus();
|
|
1417
1298
|
});
|
|
@@ -1420,15 +1301,15 @@ ${THEME_TOKENS_CSS}
|
|
|
1420
1301
|
const n = Number(synthSliderEl.value);
|
|
1421
1302
|
synthValueEl.textContent = String(n);
|
|
1422
1303
|
writeSynthBudget(n);
|
|
1423
|
-
if (window.
|
|
1424
|
-
window.
|
|
1304
|
+
if (window.tmct.session && window.tmct.session.setSynthesisBudget) {
|
|
1305
|
+
window.tmct.session.setSynthesisBudget(n);
|
|
1425
1306
|
}
|
|
1426
1307
|
});
|
|
1427
1308
|
|
|
1428
1309
|
let busy = true;
|
|
1429
1310
|
function setBusy(v) {
|
|
1430
1311
|
busy = v;
|
|
1431
|
-
const ready = Boolean(window.
|
|
1312
|
+
const ready = Boolean(window.tmct.session);
|
|
1432
1313
|
inputEl.disabled = v || !ready;
|
|
1433
1314
|
sendBtn.disabled = v || !ready;
|
|
1434
1315
|
}
|
|
@@ -1439,7 +1320,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1439
1320
|
// bubble, settle, persist, stats. Resolves once the turn has settled (the
|
|
1440
1321
|
// ticker awaits it before pacing the next step).
|
|
1441
1322
|
async function submitLine(q) {
|
|
1442
|
-
if (!q || busy || !window.
|
|
1323
|
+
if (!q || busy || !window.tmct.session) return null;
|
|
1443
1324
|
addUserBubble(q);
|
|
1444
1325
|
transcript.push({ role: "you", text: q, chipTier: null, ts: Date.now() });
|
|
1445
1326
|
const pendingRow = addPendingAssistantBubble();
|
|
@@ -1482,7 +1363,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1482
1363
|
composerForm.addEventListener("submit", (e) => {
|
|
1483
1364
|
e.preventDefault();
|
|
1484
1365
|
const q = inputEl.value.trim();
|
|
1485
|
-
if (!q || busy || !window.
|
|
1366
|
+
if (!q || busy || !window.tmct.session) return;
|
|
1486
1367
|
inputEl.value = "";
|
|
1487
1368
|
const lowered = q.toLowerCase();
|
|
1488
1369
|
if (lowered === "wave" || lowered === "/wave") {
|
|
@@ -1546,7 +1427,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1546
1427
|
|
|
1547
1428
|
researchGoBtn.addEventListener("click", () => {
|
|
1548
1429
|
const topic = researchTopicEl.value.trim();
|
|
1549
|
-
if (!topic || busy || !window.
|
|
1430
|
+
if (!topic || busy || !window.tmct.session) return;
|
|
1550
1431
|
researchTopicEl.value = "";
|
|
1551
1432
|
submitLine("research " + topic);
|
|
1552
1433
|
});
|
|
@@ -1567,7 +1448,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1567
1448
|
// @media print stylesheet above to un-pin the message column so every
|
|
1568
1449
|
// turn reaches paper.
|
|
1569
1450
|
el("exportMd").addEventListener("click", () => {
|
|
1570
|
-
const sessionId = (window.
|
|
1451
|
+
const sessionId = (window.tmct.session && window.tmct.session.sessionId) || "";
|
|
1571
1452
|
const md = transcriptMarkdown(transcript, { version: siteVersion, sessionId: sessionId }, sessionLogHeaderMarkdown, sessionLogTurnMarkdown);
|
|
1572
1453
|
const blob = new Blob([md], { type: "text/markdown" });
|
|
1573
1454
|
const url = URL.createObjectURL(blob);
|
|
@@ -1586,7 +1467,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1586
1467
|
// subject/predicate/object/provenance shape the extract and memory-export
|
|
1587
1468
|
// CLI paths emit), so what you taught leaves in the standard shape.
|
|
1588
1469
|
el("exportFacts").addEventListener("click", async () => {
|
|
1589
|
-
const session = window.
|
|
1470
|
+
const session = window.tmct.session;
|
|
1590
1471
|
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
1591
1472
|
let jsonl;
|
|
1592
1473
|
try {
|
|
@@ -1615,7 +1496,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1615
1496
|
el("ingestInput").addEventListener("change", async (e) => {
|
|
1616
1497
|
const file = e.target.files && e.target.files[0];
|
|
1617
1498
|
e.target.value = "";
|
|
1618
|
-
const session = window.
|
|
1499
|
+
const session = window.tmct.session;
|
|
1619
1500
|
if (!file || busy || !session || !window.tmct.page.splitSentences) return;
|
|
1620
1501
|
let text;
|
|
1621
1502
|
try {
|
|
@@ -1683,11 +1564,16 @@ ${THEME_TOKENS_CSS}
|
|
|
1683
1564
|
const wireStateNoteEl = el("wireStateNote");
|
|
1684
1565
|
const statePillEl = el("statePill");
|
|
1685
1566
|
const statePillWordEl = el("statePillWord");
|
|
1686
|
-
const sharePanelEl = el("sharePanel");
|
|
1687
1567
|
const shareLinkEl = el("shareLink");
|
|
1568
|
+
const inviteSummaryEl = el("inviteSummary");
|
|
1569
|
+
const webShareBtnEl = el("webShareBtn");
|
|
1570
|
+
const waShareBtnEl = el("waShareBtn");
|
|
1571
|
+
const replyShareBtnEl = el("replyShareBtn");
|
|
1572
|
+
const replyWaBtnEl = el("replyWaBtn");
|
|
1573
|
+
const joinShareBtnEl = el("joinShareBtn");
|
|
1574
|
+
const joinWaBtnEl = el("joinWaBtn");
|
|
1688
1575
|
const replyBoxEl = el("replyBox");
|
|
1689
1576
|
const replyProblemEl = el("replyProblem");
|
|
1690
|
-
const answerPanelEl = el("answerPanel");
|
|
1691
1577
|
const replyOutEl = el("replyOut");
|
|
1692
1578
|
const nodeListEl = el("nodeList");
|
|
1693
1579
|
const nodeEmptyEl = el("nodeEmpty");
|
|
@@ -1713,18 +1599,36 @@ ${THEME_TOKENS_CSS}
|
|
|
1713
1599
|
|
|
1714
1600
|
const P2P_ASSET = "./vendor/p2p.js";
|
|
1715
1601
|
const NODE_NAME_KEY = "tmct.chat.nodeName";
|
|
1602
|
+
// Not the IndexedDB store beside it: that record is stamped with the site
|
|
1603
|
+
// version and the seed, and is dropped whenever either moves. A node id has
|
|
1604
|
+
// to outlive a deploy, because peers have already keyed this node's facts
|
|
1605
|
+
// on it.
|
|
1606
|
+
const NODE_ID_KEY = "tmct.chat.nodeId";
|
|
1716
1607
|
const invite = inviteParamsFrom(window.location.search);
|
|
1717
1608
|
|
|
1718
1609
|
let p2p = null;
|
|
1719
1610
|
let p2pLoad = null;
|
|
1720
1611
|
let room = null;
|
|
1721
1612
|
let myPeerId = null;
|
|
1613
|
+
let myNodeId = "";
|
|
1722
1614
|
let myDisplayName = "";
|
|
1723
1615
|
let worldId = "";
|
|
1724
1616
|
let worldName = "";
|
|
1725
1617
|
let waveTimer = null;
|
|
1726
1618
|
let nodeClockTimer = null;
|
|
1727
1619
|
let channelCount = 0;
|
|
1620
|
+
let mintedBlob = "";
|
|
1621
|
+
|
|
1622
|
+
// Which seat this page occupies in the handshake — "idle" until something
|
|
1623
|
+
// happens, "sponsor" once an invite is minted here, "joiner" once an invite
|
|
1624
|
+
// is opened here. The overlay's CSS reads it to decide which calls to
|
|
1625
|
+
// action stand at each step; entry distinguishes an invite that arrived
|
|
1626
|
+
// as a link (the hero card owns the reply) from one pasted in as text
|
|
1627
|
+
// (step 4's own slot owns it).
|
|
1628
|
+
function setOverlayRole(role, entry) {
|
|
1629
|
+
netPanelEl.dataset.role = role;
|
|
1630
|
+
netPanelEl.dataset.entry = entry || "";
|
|
1631
|
+
}
|
|
1728
1632
|
|
|
1729
1633
|
function loadP2p() {
|
|
1730
1634
|
if (!p2pLoad) {
|
|
@@ -1747,6 +1651,13 @@ ${THEME_TOKENS_CSS}
|
|
|
1747
1651
|
async function ensureIdentity() {
|
|
1748
1652
|
const mod = await loadP2p();
|
|
1749
1653
|
if (!myPeerId) myPeerId = mod.generatePeerId();
|
|
1654
|
+
if (!myNodeId) {
|
|
1655
|
+
try { myNodeId = localStorage.getItem(NODE_ID_KEY) || ""; } catch { myNodeId = ""; }
|
|
1656
|
+
if (!myNodeId) {
|
|
1657
|
+
myNodeId = mod.generateNodeId();
|
|
1658
|
+
try { localStorage.setItem(NODE_ID_KEY, myNodeId); } catch { /* private mode — this visit still has an id, it just won't outlive the tab */ }
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1750
1661
|
if (!myDisplayName) {
|
|
1751
1662
|
myDisplayName = readStoredNodeName() || mod.generateDisplayName();
|
|
1752
1663
|
nodeNameInputEl.value = myDisplayName;
|
|
@@ -1762,7 +1673,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1762
1673
|
// message crossing one can be seen. The room asks for transports through
|
|
1763
1674
|
// this factory and never learns it is being watched.
|
|
1764
1675
|
function instrumentedTransport() {
|
|
1765
|
-
const transport = p2p.createTransport(
|
|
1676
|
+
const transport = p2p.createTransport();
|
|
1766
1677
|
const channel = "ch" + (++channelCount);
|
|
1767
1678
|
transport.onMessage(function (message) { noteWire("in", message, channel); });
|
|
1768
1679
|
transport.onOpen(function () { noteTape("note", "link", "channel open", channel); });
|
|
@@ -1791,12 +1702,13 @@ ${THEME_TOKENS_CSS}
|
|
|
1791
1702
|
if (room) return room;
|
|
1792
1703
|
const mod = await loadP2p();
|
|
1793
1704
|
await ensureIdentity();
|
|
1794
|
-
if (!window.
|
|
1795
|
-
if (!window.
|
|
1705
|
+
if (!window.tmct.session) await window.tmctChatReady;
|
|
1706
|
+
if (!window.tmct.session) throw new Error("the chat engine didn't finish booting");
|
|
1796
1707
|
room = mod.createP2pRoom({
|
|
1797
|
-
memoryDir: window.
|
|
1708
|
+
memoryDir: window.tmct.session.memoryDir,
|
|
1798
1709
|
myPeerId: myPeerId,
|
|
1799
1710
|
myDisplayName: myDisplayName,
|
|
1711
|
+
myNodeId: myNodeId,
|
|
1800
1712
|
worldId: worldId,
|
|
1801
1713
|
worldName: worldName,
|
|
1802
1714
|
transportFactory: instrumentedTransport,
|
|
@@ -1804,10 +1716,14 @@ ${THEME_TOKENS_CSS}
|
|
|
1804
1716
|
});
|
|
1805
1717
|
room.onStateChanged(function (state) {
|
|
1806
1718
|
noteTape("note", state === "failed" ? "fault" : "state", "state " + state, "");
|
|
1807
|
-
// The
|
|
1808
|
-
//
|
|
1809
|
-
//
|
|
1810
|
-
if (state === "connected")
|
|
1719
|
+
// The overlay exists to make one connection. The moment the channel is
|
|
1720
|
+
// open its work is done — the lights come back up, and the pill in the
|
|
1721
|
+
// chrome is the way back in.
|
|
1722
|
+
if (state === "connected") {
|
|
1723
|
+
joinCardEl.hidden = true;
|
|
1724
|
+
closeNetPanel();
|
|
1725
|
+
flashTip(statePillEl, "connected \\u2014 you're sharing live");
|
|
1726
|
+
}
|
|
1811
1727
|
renderWire();
|
|
1812
1728
|
renderStatus();
|
|
1813
1729
|
});
|
|
@@ -1841,6 +1757,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1841
1757
|
nodeClockTimer = null;
|
|
1842
1758
|
shareLinkEl.value = "";
|
|
1843
1759
|
replyOutEl.value = "";
|
|
1760
|
+
mintedBlob = "";
|
|
1761
|
+
setOverlayRole("idle", "");
|
|
1844
1762
|
noteTape("note", "link", "world closed", worldName);
|
|
1845
1763
|
renderWire();
|
|
1846
1764
|
renderNodes();
|
|
@@ -1924,7 +1842,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1924
1842
|
}
|
|
1925
1843
|
}
|
|
1926
1844
|
|
|
1927
|
-
// ---- what a person sees: state, nodes, waves
|
|
1845
|
+
// ---- what a person sees: state, the ladder, nodes, waves ----------------
|
|
1928
1846
|
function renderWire() {
|
|
1929
1847
|
const label = wireStateLabel(room ? room.state : "idle");
|
|
1930
1848
|
wireStateEl.dataset.tone = label.tone;
|
|
@@ -1933,10 +1851,29 @@ ${THEME_TOKENS_CSS}
|
|
|
1933
1851
|
statePillEl.dataset.tone = label.tone;
|
|
1934
1852
|
statePillWordEl.textContent = label.pill;
|
|
1935
1853
|
statePillEl.title = label.note;
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1854
|
+
netPanelEl.dataset.tone = label.tone;
|
|
1855
|
+
renderSteps();
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function renderSteps() {
|
|
1859
|
+
const states = shareStepStates({
|
|
1860
|
+
role: netPanelEl.dataset.role,
|
|
1861
|
+
state: room ? room.state : "idle",
|
|
1862
|
+
hasReply: Boolean(replyOutEl.value),
|
|
1863
|
+
});
|
|
1864
|
+
for (const step of states) {
|
|
1865
|
+
const item = el("step-" + step.key);
|
|
1866
|
+
if (item) item.dataset.status = step.status;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// The scene's two browser cards: this one wears the node's own name, the
|
|
1871
|
+
// far one wears the first peer's the moment there is one to name.
|
|
1872
|
+
function renderScene() {
|
|
1873
|
+
el("sceneYouName").textContent = myDisplayName || "you";
|
|
1874
|
+
const peers = room ? room.peers() : [];
|
|
1875
|
+
const first = peers.find(function (p) { return p.connected; }) || peers[0];
|
|
1876
|
+
el("sceneThemName").textContent = first ? room.displayNameFor(first.peerId) : "your friend";
|
|
1940
1877
|
}
|
|
1941
1878
|
|
|
1942
1879
|
function renderNodes() {
|
|
@@ -1944,6 +1881,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1944
1881
|
nodeCountEl.textContent = "";
|
|
1945
1882
|
nodeListEl.textContent = "";
|
|
1946
1883
|
nodeEmptyEl.hidden = false;
|
|
1884
|
+
renderScene();
|
|
1947
1885
|
return;
|
|
1948
1886
|
}
|
|
1949
1887
|
const rows = nodeRowsFor({
|
|
@@ -1955,6 +1893,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1955
1893
|
latestTimestampOf: p2p.latestProvenanceTimestamp,
|
|
1956
1894
|
});
|
|
1957
1895
|
const nowMs = Date.now();
|
|
1896
|
+
const wavers = new Set(activeWaves(room.factRows(), nowMs).map(function (w) { return w.waver; }));
|
|
1958
1897
|
nodeCountEl.textContent = rows.length + (rows.length === 1 ? " node" : " nodes");
|
|
1959
1898
|
nodeListEl.textContent = "";
|
|
1960
1899
|
for (const entry of rows) {
|
|
@@ -1963,7 +1902,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1963
1902
|
item.dataset.self = String(entry.isSelf);
|
|
1964
1903
|
item.dataset.away = String(!entry.connected);
|
|
1965
1904
|
item.dataset.peer = entry.peerId;
|
|
1966
|
-
item.dataset.waving = String(
|
|
1905
|
+
item.dataset.waving = String(wavers.has(peerTerm(entry.peerId)));
|
|
1967
1906
|
const avatar = document.createElement("span");
|
|
1968
1907
|
avatar.className = "node-avatar";
|
|
1969
1908
|
avatar.textContent = nodeInitials(entry.name);
|
|
@@ -1971,132 +1910,262 @@ ${THEME_TOKENS_CSS}
|
|
|
1971
1910
|
const dot = document.createElement("i");
|
|
1972
1911
|
dot.className = "node-dot";
|
|
1973
1912
|
avatar.appendChild(dot);
|
|
1974
|
-
const name = document.createElement("span");
|
|
1975
|
-
name.className = "node-name";
|
|
1976
|
-
name.textContent = entry.name;
|
|
1977
1913
|
const hand = document.createElement("span");
|
|
1978
1914
|
hand.className = "node-hand";
|
|
1979
1915
|
hand.textContent = "👋";
|
|
1916
|
+
avatar.appendChild(hand);
|
|
1917
|
+
const name = document.createElement("span");
|
|
1918
|
+
name.className = "node-name";
|
|
1919
|
+
name.textContent = entry.name;
|
|
1980
1920
|
const when = document.createElement("span");
|
|
1981
1921
|
when.className = "node-when";
|
|
1982
1922
|
when.textContent = relativeWhen(entry.lastActiveAt, nowMs);
|
|
1983
1923
|
when.title = entry.lastActiveAt
|
|
1984
1924
|
? "last contributed a fact at " + new Date(entry.lastActiveAt).toLocaleTimeString()
|
|
1985
1925
|
: "has contributed no fact yet";
|
|
1926
|
+
const fact = document.createElement("span");
|
|
1927
|
+
fact.className = "node-fact";
|
|
1928
|
+
if (entry.lastFact) {
|
|
1929
|
+
const line = entry.lastFact.subject + " " + String(entry.lastFact.predicate || "").replace(/^[a-z0-9_-]+:/i, "") + " " + entry.lastFact.object;
|
|
1930
|
+
fact.textContent = line;
|
|
1931
|
+
fact.title = "last shared: " + line;
|
|
1932
|
+
} else {
|
|
1933
|
+
fact.textContent = "nothing shared yet";
|
|
1934
|
+
}
|
|
1935
|
+
const waveOne = document.createElement("button");
|
|
1936
|
+
waveOne.type = "button";
|
|
1937
|
+
waveOne.className = "node-wave-btn";
|
|
1938
|
+
waveOne.textContent = "👋";
|
|
1939
|
+
waveOne.title = entry.isSelf ? "wave at everyone" : "wave at " + entry.name + " — they see it as a fact arriving";
|
|
1940
|
+
waveOne.setAttribute("aria-label", waveOne.title);
|
|
1941
|
+
waveOne.addEventListener("click", function () { waveAt(entry.isSelf ? null : entry.peerId, waveOne); });
|
|
1986
1942
|
item.appendChild(avatar);
|
|
1987
1943
|
item.appendChild(name);
|
|
1988
|
-
item.appendChild(hand);
|
|
1989
1944
|
item.appendChild(when);
|
|
1945
|
+
item.appendChild(waveOne);
|
|
1946
|
+
item.appendChild(fact);
|
|
1990
1947
|
nodeListEl.appendChild(item);
|
|
1991
1948
|
}
|
|
1992
1949
|
nodeEmptyEl.hidden = rows.length > 1;
|
|
1950
|
+
renderScene();
|
|
1993
1951
|
}
|
|
1994
1952
|
|
|
1995
1953
|
// A wave is a fact like any other, so it reaches every page through the
|
|
1996
1954
|
// same merge every other fact does; "currently waving" is a recency read
|
|
1997
1955
|
// over that fact, never stored state. Nothing here is a second live-update
|
|
1998
1956
|
// path — onFactsChanged is what wakes it, and the timer below only lets a
|
|
1999
|
-
// wave stop rendering once its window has passed.
|
|
1957
|
+
// wave stop rendering once its window has passed. The fact's object names
|
|
1958
|
+
// the audience: the presence scope is everyone, a peer term is one node —
|
|
1959
|
+
// and a wave aimed at this node says so to its face.
|
|
2000
1960
|
function renderWaves() {
|
|
2001
1961
|
if (!room) return;
|
|
2002
1962
|
const nowMs = Date.now();
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
const
|
|
1963
|
+
const myTerm = peerTerm(myPeerId);
|
|
1964
|
+
const nameByTerm = new Map([[myTerm, myDisplayName]]);
|
|
1965
|
+
for (const peer of room.peers()) nameByTerm.set(peerTerm(peer.peerId), room.displayNameFor(peer.peerId));
|
|
1966
|
+
const waves = activeWaves(room.factRows(), nowMs).filter(function (w) { return nameByTerm.has(w.waver); });
|
|
2006
1967
|
waveBurstEl.textContent = "";
|
|
2007
|
-
for (const
|
|
1968
|
+
for (const wave of waves) {
|
|
2008
1969
|
const pill = document.createElement("div");
|
|
2009
|
-
pill.className = "wave-pill";
|
|
1970
|
+
pill.className = "wave-pill" + (wave.target === myTerm ? " wave-pill--you" : "");
|
|
2010
1971
|
const hand = document.createElement("span");
|
|
2011
1972
|
hand.className = "hand";
|
|
2012
1973
|
hand.textContent = "👋";
|
|
2013
1974
|
const label = document.createElement("span");
|
|
2014
|
-
|
|
1975
|
+
const who = nameByTerm.get(wave.waver);
|
|
1976
|
+
label.textContent = !wave.target ? who + " waved at everyone"
|
|
1977
|
+
: wave.target === myTerm ? who + " is waving at you"
|
|
1978
|
+
: nameByTerm.has(wave.target) ? who + " is waving at " + nameByTerm.get(wave.target)
|
|
1979
|
+
: who + " waved";
|
|
2015
1980
|
pill.appendChild(hand);
|
|
2016
1981
|
pill.appendChild(label);
|
|
2017
1982
|
waveBurstEl.appendChild(pill);
|
|
2018
1983
|
}
|
|
1984
|
+
const wavers = new Set(waves.map(function (w) { return w.waver; }));
|
|
2019
1985
|
for (const item of nodeListEl.children) {
|
|
2020
|
-
item.dataset.waving = String(
|
|
1986
|
+
item.dataset.waving = String(wavers.has(peerTerm(item.dataset.peer)));
|
|
2021
1987
|
}
|
|
2022
1988
|
clearTimeout(waveTimer);
|
|
2023
|
-
if (
|
|
1989
|
+
if (waves.length) waveTimer = setTimeout(renderWaves, 1000);
|
|
2024
1990
|
}
|
|
2025
1991
|
|
|
2026
|
-
async function
|
|
1992
|
+
async function waveAt(targetPeerId, anchor) {
|
|
2027
1993
|
try {
|
|
2028
1994
|
const active = await ensureRoom();
|
|
2029
|
-
await active.wave("peer:" + myPeerId, null);
|
|
2030
|
-
|
|
1995
|
+
await active.wave("peer:" + myPeerId, targetPeerId ? "peer:" + targetPeerId : null);
|
|
1996
|
+
const audience = targetPeerId ? (active.displayNameFor(targetPeerId) || "one node") : "everyone";
|
|
1997
|
+
noteTape("note", "facts", "waved", audience);
|
|
2031
1998
|
renderNodes();
|
|
2032
1999
|
renderWaves();
|
|
2000
|
+
if (anchor) flashTip(anchor, "you waved at " + audience);
|
|
2033
2001
|
} catch (err) {
|
|
2034
2002
|
addSystemLine("couldn't wave (" + (err && err.message ? err.message : err) + ").");
|
|
2035
2003
|
}
|
|
2036
2004
|
}
|
|
2005
|
+
function waveNow() { return waveAt(null, null); }
|
|
2037
2006
|
|
|
2038
|
-
// ----
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
return true;
|
|
2044
|
-
}
|
|
2045
|
-
} catch { /* fall through — the box on the page still holds the text */ }
|
|
2046
|
-
try {
|
|
2047
|
-
const holder = document.createElement("textarea");
|
|
2048
|
-
holder.value = text;
|
|
2049
|
-
holder.setAttribute("readonly", "");
|
|
2050
|
-
holder.style.position = "fixed";
|
|
2051
|
-
holder.style.opacity = "0";
|
|
2052
|
-
document.body.appendChild(holder);
|
|
2053
|
-
holder.select();
|
|
2054
|
-
const copied = document.execCommand("copy");
|
|
2055
|
-
holder.remove();
|
|
2056
|
-
return copied;
|
|
2057
|
-
} catch {
|
|
2058
|
-
return false;
|
|
2059
|
-
}
|
|
2007
|
+
// ---- the overlay itself: the lights go down, the handshake takes the room
|
|
2008
|
+
function openNetPanel() {
|
|
2009
|
+
netPanelEl.hidden = false;
|
|
2010
|
+
const card = netPanelEl.querySelector(".so-card");
|
|
2011
|
+
if (card) card.focus({ preventScroll: true });
|
|
2060
2012
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
const tip = document.createElement("div");
|
|
2064
|
-
tip.className = "copyTip";
|
|
2065
|
-
tip.setAttribute("role", "status");
|
|
2066
|
-
tip.textContent = text;
|
|
2067
|
-
document.body.appendChild(tip);
|
|
2068
|
-
const box = anchor.getBoundingClientRect();
|
|
2069
|
-
const width = tip.offsetWidth;
|
|
2070
|
-
tip.style.top = (box.bottom + 6) + "px";
|
|
2071
|
-
tip.style.left = Math.max(6, Math.min(window.innerWidth - width - 6, box.left + box.width / 2 - width / 2)) + "px";
|
|
2072
|
-
setTimeout(function () { tip.remove(); }, 2600);
|
|
2013
|
+
function closeNetPanel() {
|
|
2014
|
+
netPanelEl.hidden = true;
|
|
2073
2015
|
}
|
|
2074
|
-
|
|
2075
|
-
const openNetPanel = () => document.body.classList.add("net-open");
|
|
2076
|
-
const closeNetPanel = () => document.body.classList.remove("net-open");
|
|
2077
2016
|
el("netPanelClose").addEventListener("click", closeNetPanel);
|
|
2017
|
+
netPanelEl.addEventListener("click", function (e) {
|
|
2018
|
+
if (e.target === netPanelEl) closeNetPanel();
|
|
2019
|
+
});
|
|
2020
|
+
document.addEventListener("keydown", function (e) {
|
|
2021
|
+
if (e.key === "Escape" && !netPanelEl.hidden) closeNetPanel();
|
|
2022
|
+
});
|
|
2078
2023
|
statePillEl.addEventListener("click", function () {
|
|
2079
|
-
if (
|
|
2080
|
-
else
|
|
2024
|
+
if (netPanelEl.hidden) openNetPanel();
|
|
2025
|
+
else closeNetPanel();
|
|
2081
2026
|
});
|
|
2027
|
+
if (isProbablyMobile()) netPanelEl.classList.add("so-mobile");
|
|
2028
|
+
// The blobs are made to be copied whole — a click selects everything, so a
|
|
2029
|
+
// manual copy can never take half a code.
|
|
2030
|
+
for (const boxId of ["shareLink", "replyOut", "joinReply"]) {
|
|
2031
|
+
el(boxId).addEventListener("focus", function (e) { e.target.select(); });
|
|
2032
|
+
}
|
|
2082
2033
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2034
|
+
function renderInviteShare() {
|
|
2035
|
+
const factsPart = lastStatsTotal === null ? "" : " \\u00b7 " + lastStatsTotal.toLocaleString() + " facts travel when they join";
|
|
2036
|
+
inviteSummaryEl.textContent = "invites one person into \\u201c" + worldName + "\\u201d" + factsPart;
|
|
2037
|
+
const message = shareMessageFor({ worldName: worldName, link: shareLinkEl.value, thing: "graph" });
|
|
2038
|
+
waShareBtnEl.href = whatsAppShareUrl(message);
|
|
2039
|
+
webShareBtnEl.hidden = !navigator.share;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function renderReplyShare() {
|
|
2043
|
+
const message = replyMessageFor({ worldName: worldName, blob: replyOutEl.value });
|
|
2044
|
+
replyWaBtnEl.href = whatsAppShareUrl(message);
|
|
2045
|
+
joinWaBtnEl.href = whatsAppShareUrl(message);
|
|
2046
|
+
replyShareBtnEl.hidden = !navigator.share;
|
|
2047
|
+
joinShareBtnEl.hidden = !navigator.share;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
async function mintInvite(anchor) {
|
|
2085
2051
|
try {
|
|
2086
2052
|
const active = await ensureRoom();
|
|
2087
2053
|
const minted = await active.startSharing();
|
|
2054
|
+
mintedBlob = minted.blob;
|
|
2088
2055
|
shareLinkEl.value = inviteLinkFor(window.location.href, { blob: minted.blob, world: worldId, worldName: worldName });
|
|
2056
|
+
setOverlayRole("sponsor", "");
|
|
2057
|
+
renderInviteShare();
|
|
2089
2058
|
replyProblemEl.hidden = true;
|
|
2090
2059
|
renderWire();
|
|
2091
2060
|
openNetPanel();
|
|
2092
2061
|
await copyText(shareLinkEl.value);
|
|
2093
|
-
flashTip(
|
|
2062
|
+
flashTip(anchor, "link copied — send it to one person");
|
|
2094
2063
|
} catch (err) {
|
|
2095
2064
|
addSystemLine("couldn't create an invite (" + (err && err.message ? err.message : err) + ").");
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
shareBtn.addEventListener("click", async function () {
|
|
2069
|
+
shareBtn.disabled = true;
|
|
2070
|
+
try {
|
|
2071
|
+
await mintInvite(shareBtn);
|
|
2096
2072
|
} finally {
|
|
2097
2073
|
shareBtn.disabled = false;
|
|
2098
2074
|
}
|
|
2099
2075
|
});
|
|
2076
|
+
for (const mintId of ["mintInviteBtn", "remintBtn"]) {
|
|
2077
|
+
el(mintId).addEventListener("click", async function () {
|
|
2078
|
+
const btn = el(mintId);
|
|
2079
|
+
btn.disabled = true;
|
|
2080
|
+
try {
|
|
2081
|
+
await mintInvite(btn);
|
|
2082
|
+
} finally {
|
|
2083
|
+
btn.disabled = false;
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
el("copyLinkBtn").addEventListener("click", async function () {
|
|
2089
|
+
await copyText(shareLinkEl.value);
|
|
2090
|
+
flashTip(el("copyLinkBtn"), "link copied — send it to one person");
|
|
2091
|
+
});
|
|
2092
|
+
el("copyCodeBtn").addEventListener("click", async function () {
|
|
2093
|
+
await copyText(mintedBlob);
|
|
2094
|
+
flashTip(el("copyCodeBtn"), "code copied — they paste it under step 3 on their page");
|
|
2095
|
+
});
|
|
2096
|
+
webShareBtnEl.addEventListener("click", function () {
|
|
2097
|
+
navigator.share({
|
|
2098
|
+
title: "join \\u201c" + worldName + "\\u201d",
|
|
2099
|
+
text: shareMessageFor({ worldName: worldName, link: shareLinkEl.value, thing: "graph" }),
|
|
2100
|
+
}).catch(function () { /* the sheet was closed — the copy buttons still stand */ });
|
|
2101
|
+
});
|
|
2102
|
+
function shareReplyViaSheet() {
|
|
2103
|
+
navigator.share({
|
|
2104
|
+
title: "my reply to \\u201c" + worldName + "\\u201d",
|
|
2105
|
+
text: replyMessageFor({ worldName: worldName, blob: replyOutEl.value }),
|
|
2106
|
+
}).catch(function () { /* the sheet was closed — the copy buttons still stand */ });
|
|
2107
|
+
}
|
|
2108
|
+
replyShareBtnEl.addEventListener("click", shareReplyViaSheet);
|
|
2109
|
+
joinShareBtnEl.addEventListener("click", shareReplyViaSheet);
|
|
2110
|
+
|
|
2111
|
+
// The other way in: an invite that arrived as text rather than as a link.
|
|
2112
|
+
// The same paste box reads a whole link or a bare code — telling them
|
|
2113
|
+
// apart by looking is cheaper than asking anyone to.
|
|
2114
|
+
el("inviteBtn").addEventListener("click", async function () {
|
|
2115
|
+
const problem = el("inviteProblem");
|
|
2116
|
+
const pasted = el("inviteBox").value.trim();
|
|
2117
|
+
if (!pasted) {
|
|
2118
|
+
problem.textContent = "paste the invite you were sent, then try again";
|
|
2119
|
+
problem.hidden = false;
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (room) {
|
|
2123
|
+
problem.textContent = "this graph is already shared. reload the page first, then open the invite.";
|
|
2124
|
+
problem.hidden = false;
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const blob = offerBlobIn(pasted);
|
|
2128
|
+
const mod = await loadP2p();
|
|
2129
|
+
const decoded = mod.decodeInviteBlob(blob);
|
|
2130
|
+
if (decoded.error || decoded.value.kind !== "offer") {
|
|
2131
|
+
problem.textContent = decoded.error
|
|
2132
|
+
? "this invite looks cut short — ask for it to be sent again"
|
|
2133
|
+
: "that's a reply, not an invite — it belongs under step 5, on the page that sent the invite";
|
|
2134
|
+
problem.hidden = false;
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
// The world id has to be the invite's before the room is opened: a room
|
|
2138
|
+
// that minted its own would refuse the invite as belonging elsewhere.
|
|
2139
|
+
await ensureIdentity();
|
|
2140
|
+
worldId = decoded.value.world || worldId;
|
|
2141
|
+
if (decoded.value.worldName) {
|
|
2142
|
+
worldName = decoded.value.worldName;
|
|
2143
|
+
worldNameInputEl.value = worldName;
|
|
2144
|
+
}
|
|
2145
|
+
try {
|
|
2146
|
+
const active = await ensureRoom();
|
|
2147
|
+
const outcome = await active.acceptInvite(blob);
|
|
2148
|
+
if (outcome && outcome.error) {
|
|
2149
|
+
problem.textContent = outcome.message;
|
|
2150
|
+
problem.hidden = false;
|
|
2151
|
+
noteTape("note", "fault", "invite rejected", outcome.error);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
problem.hidden = true;
|
|
2155
|
+
el("inviteBox").value = "";
|
|
2156
|
+
replyOutEl.value = outcome.blob;
|
|
2157
|
+
setOverlayRole("joiner", "paste");
|
|
2158
|
+
renderReplyShare();
|
|
2159
|
+
renderWire();
|
|
2160
|
+
await copyText(outcome.blob);
|
|
2161
|
+
flashTip(copyReplyBtn, "reply copied — send it back the same way");
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
problem.textContent = "couldn't make a reply (" + (err && err.message ? err.message : err) + ").";
|
|
2164
|
+
problem.hidden = false;
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
el("waveAllBtn").addEventListener("click", function () { waveAt(null, el("waveAllBtn")); });
|
|
2100
2169
|
|
|
2101
2170
|
// The inviter's page has exactly one paste target, so there is no wrong box
|
|
2102
2171
|
// to choose. A rejected paste keeps its text so the copy can be fixed.
|
|
@@ -2148,10 +2217,13 @@ ${THEME_TOKENS_CSS}
|
|
|
2148
2217
|
|
|
2149
2218
|
waveBtn.addEventListener("click", waveNow);
|
|
2150
2219
|
|
|
2151
|
-
// ---- joining:
|
|
2220
|
+
// ---- joining: the hero card, one button, nothing running until pressed --
|
|
2152
2221
|
async function prepareJoinCard() {
|
|
2222
|
+
setOverlayRole("joiner", "link");
|
|
2153
2223
|
joinCardEl.hidden = false;
|
|
2154
2224
|
joinWorldEl.textContent = invite.worldName || "a shared graph";
|
|
2225
|
+
renderSteps();
|
|
2226
|
+
openNetPanel();
|
|
2155
2227
|
joinBtn.focus();
|
|
2156
2228
|
const mod = await loadP2p();
|
|
2157
2229
|
const decoded = mod.decodeInviteBlob(invite.offer);
|
|
@@ -2191,6 +2263,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2191
2263
|
joinReplyWrapEl.hidden = false;
|
|
2192
2264
|
joinBtn.textContent = "reply created";
|
|
2193
2265
|
joinDismissBtn.textContent = "close this and start talking";
|
|
2266
|
+
renderReplyShare();
|
|
2194
2267
|
renderWire();
|
|
2195
2268
|
await copyText(outcome.blob);
|
|
2196
2269
|
flashTip(joinCopyBtn, "reply copied — send it back the same way");
|
|
@@ -2205,10 +2278,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2205
2278
|
await copyText(joinReplyEl.value);
|
|
2206
2279
|
flashTip(joinCopyBtn, "reply copied");
|
|
2207
2280
|
});
|
|
2208
|
-
joinDismissBtn.addEventListener("click",
|
|
2209
|
-
joinCardEl.hidden = true;
|
|
2210
|
-
if (replyOutEl.value) openNetPanel();
|
|
2211
|
-
});
|
|
2281
|
+
joinDismissBtn.addEventListener("click", closeNetPanel);
|
|
2212
2282
|
|
|
2213
2283
|
renderWire();
|
|
2214
2284
|
renderTapeMeter();
|
|
@@ -2239,7 +2309,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2239
2309
|
synthSliderEl.value = String(readSynthBudget());
|
|
2240
2310
|
synthValueEl.textContent = synthSliderEl.value;
|
|
2241
2311
|
if (savedRecord && savedRecord.payload) {
|
|
2242
|
-
|
|
2312
|
+
await window.tmct.open({
|
|
2243
2313
|
seedPayload: savedRecord.payload,
|
|
2244
2314
|
vocabSeeded: true,
|
|
2245
2315
|
liveReference: liveReferenceForMode(initialMode),
|
|
@@ -2248,9 +2318,9 @@ ${THEME_TOKENS_CSS}
|
|
|
2248
2318
|
digestStructures: DIGEST_STRUCTURES,
|
|
2249
2319
|
});
|
|
2250
2320
|
} else {
|
|
2251
|
-
|
|
2321
|
+
await newSession();
|
|
2252
2322
|
}
|
|
2253
|
-
const stats = await window.tmct.page.memoryStats(window.
|
|
2323
|
+
const stats = await window.tmct.page.memoryStats(window.tmct.session.memoryDir);
|
|
2254
2324
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
2255
2325
|
const restoredNote = savedRecord
|
|
2256
2326
|
? " Restored " + restoredCount + " taught fact" + (restoredCount === 1 ? "" : "s")
|
|
@@ -2266,7 +2336,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2266
2336
|
// was never open to read.
|
|
2267
2337
|
if (window.tmct.page.researchedFactRows) {
|
|
2268
2338
|
try {
|
|
2269
|
-
const existingResearch = await window.tmct.page.researchedFactRows(window.
|
|
2339
|
+
const existingResearch = await window.tmct.page.researchedFactRows(window.tmct.session.memoryDir);
|
|
2270
2340
|
for (const row of existingResearch) researchedFactKeysSeen.add(row.subject + "|" + row.predicate + "|" + row.object);
|
|
2271
2341
|
} catch { /* best-effort seeding only — a fresh session has none to seed */ }
|
|
2272
2342
|
}
|
|
@@ -2285,6 +2355,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2285
2355
|
console.error("tmct chat failed to boot", err);
|
|
2286
2356
|
statusEl.textContent = "the chat failed to start (" + (err && err.message ? err.message : err) + ")";
|
|
2287
2357
|
});
|
|
2358
|
+
window.tmct.ready = window.tmctChatReady;
|
|
2288
2359
|
})();
|
|
2289
2360
|
</script>
|
|
2290
2361
|
</body>
|