@gotcos/glasses-server 6.99.20 → 6.99.29
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/CHANGELOG.md +257 -0
- package/docs/work-orchestrator.md +20 -0
- package/package.json +1 -1
- package/server/index.ts +48 -5
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/lib/web-control-static.ts +226 -0
- package/server/lib/web-memories-bridge.ts +9 -8
- package/server/lib/web-pet-choice.ts +225 -0
- package/server/lib/web-pet.ts +226 -0
- package/server/lib/work-lens-rounds.ts +29 -4
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/routes/web-control.ts +110 -7
- package/server/routes/work-lens-rounds.ts +19 -5
- package/server/web-control/app.css +26 -5
- package/server/web-control/app.js +1351 -177
- package/server/web-control/control.css +1 -1
- package/server/web-control/index.html +31 -13
- package/server/web-control/memories/memory-workspace.js +1 -1
- package/server/web-control/panes/cos-chat.css +22 -0
- package/server/web-control/panes/cos-chat.js +264 -48
- package/server/web-control/panes/meetings.css +15 -2
- package/server/web-control/panes/meetings.js +20 -9
- package/server/web-control/panes/memories.css +291 -0
- package/server/web-control/panes/memories.js +2208 -0
- package/server/web-control/panes/questions.css +39 -1
- package/server/web-control/panes/questions.js +419 -64
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +2 -0
- package/server/web-control/panes/settings.js +136 -6
- package/server/web-control/panes/today-actions.css +167 -0
- package/server/web-control/panes/today-actions.js +1303 -0
- package/server/web-control/panes/work.css +54 -0
- package/server/web-control/panes/work.js +1037 -18
- package/server/web-control/shell.js +6 -2
- package/server/web-control/theme.js +0 -2
- package/server/web-control/memories/memories-app.js +0 -1561
- package/server/web-control/memories/memories-theme.css +0 -73
- package/server/web-control/memories/memories.html +0 -72
- package/server/web-control/memories/memory-stewardship.js +0 -63
|
@@ -0,0 +1,2208 @@
|
|
|
1
|
+
/* COS Control Web: Memories, built to Control's Memories page (the bundle ActivityWindow.swift MemoriesWebView hosts,
|
|
2
|
+
server/web-control/memories/memories-app.js 0.5.191 to 0.5.362): Recent learning, Applied this week, All memories,
|
|
3
|
+
To review and Knowledge, the lesson reader with Open sources, Explore in graph and Copy context, the counts line,
|
|
4
|
+
the activity log and Memory settings. Native here, so it fills the phone and follows the app's theme; the old
|
|
5
|
+
hosted page (memories/memories.html) is no longer on this path.
|
|
6
|
+
Data: POST /api/web-bridge/memories {id, op, args} answers {ok, message, details} (lib/web-memories-bridge.ts).
|
|
7
|
+
Reads: status, workspace.request (learning_page, review_page, memory_page, get_memory, policy_get, rule_page,
|
|
8
|
+
source_status, trace_summary, identity_status, status, graph_overview, graph_expand), learning.list, learning.review,
|
|
9
|
+
learning.event, memories.list, memories.search, memory.detail, cues.summary, graph.status, graph.search,
|
|
10
|
+
graph.entity, graph.passages, graph.setup, graph.progress, graph.ask, graph.duplicates, graph.merge.preview,
|
|
11
|
+
graph.merge.status, profile.owner, memory.guardrails.
|
|
12
|
+
Writes (each only from a tap, never on load): memory.review, learning.decide, workspace.request (review_memory,
|
|
13
|
+
policy_set, propose_rule, review_rule, activate_rule, rollback_rule, refresh_source, identity_keep_apart and the
|
|
14
|
+
graph workspace's own saves), profile.owner.set, memory.guardrails.set, memory.guardrails.run, graph.ingest,
|
|
15
|
+
graph.build, graph.merge, graph.setup.sources, graph.setup.owner, graph.setup.extraction, graph.setup.embedding,
|
|
16
|
+
graph.sample, graph.schedule.
|
|
17
|
+
Routes: #/memories, #/memories/e/<eventId>, #/memories/m/<memoryId>, #/memories/k/<graph|sources|setup>,
|
|
18
|
+
#/memories/view/<recent|applied|memories|review|knowledge>. */
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
var W = window.COSWeb;
|
|
22
|
+
var esc = W.esc;
|
|
23
|
+
var pane = W.$('pane-memories');
|
|
24
|
+
if (!pane) return;
|
|
25
|
+
|
|
26
|
+
// ── vocabulary (plain words for the store and event names Control's page shows) ──
|
|
27
|
+
var KIND = { captured: 'Captured', proposed: 'Proposed', promotable: 'Suggested rule', saved: 'Saved', retrieved: 'Recalled',
|
|
28
|
+
used: 'Used in an answer', included: 'Included in context', applied: 'Use recorded', checked: 'Checked', dismissed: 'Dismissed',
|
|
29
|
+
reverted: 'Reverted', reopened: 'Reopened', consolidated: 'Merged with another', previewed: 'Previewed', accepted: 'Accepted', pruned: 'Quarantined' };
|
|
30
|
+
var STORE = { self_improvement_queue: 'Improvement suggestions', correction_journal: 'Your corrections', bot_memory: 'Saved memory',
|
|
31
|
+
review_ledger: 'Your review decisions', git_versions: 'Skill versions', eval_scores: 'Run checks', reflect_log: 'Daily reflection',
|
|
32
|
+
capture_ledger: 'Capture record', memory_trace: 'Recall record', retrieval_log: 'Recall record', used: 'Use record',
|
|
33
|
+
memory: 'Saved memory', occurrence: 'When it happened', assistant_turn: 'An answer' };
|
|
34
|
+
var GATE = { attribution: 'Who said it is unclear', low_confidence: 'Low confidence', prune_ready: 'Ready to quarantine', contradiction: 'Contradicts another memory' };
|
|
35
|
+
var VIEWS = [['recent', 'Recent learning'], ['applied', 'Applied this week'], ['memories', 'All memories'], ['review', 'To review'], ['knowledge', 'Knowledge']];
|
|
36
|
+
// Control's cue review (MeetingAsk.swift CueReviewPane): shown beside the views once the server's cue history answers.
|
|
37
|
+
var CUE_VIEWS = [['cues', 'Cues to rate'], ['rated', 'Rated cues']];
|
|
38
|
+
function knownView(v) { return VIEWS.concat(CUE_VIEWS).some(function (x) { return x[0] === v; }); }
|
|
39
|
+
var KTABS = ['graph', 'sources', 'setup'];
|
|
40
|
+
var PRUNE_READY = 'Prune ready';
|
|
41
|
+
// The bridge's read-only ops (READ_ONLY_MEMORY_WEB_OPS and WORKSPACE_READ_ACTIONS in lib/web-memories-bridge.ts).
|
|
42
|
+
var READ_OPS = {}; ('status learning.list learning.review learning.event learning.status memories.list memories.search memory.detail graph.status ' +
|
|
43
|
+
'profile.owner graph.search graph.entity graph.passages graph.setup graph.ask graph.progress memory.guardrails graph.merge.preview ' +
|
|
44
|
+
'graph.merge.status graph.duplicates cues.summary').split(' ').forEach(function (k) { READ_OPS[k] = 1; });
|
|
45
|
+
var WS_READ = {}; ('status graph_overview graph_expand graph_paths graph_resolve list_explorations get_exploration policy_get memory_page ' +
|
|
46
|
+
'learning_page review_page get_memory trace_summary source_status current_sources identity_split_preview identity_status rule_page ' +
|
|
47
|
+
'rule_evaluation').split(' ').forEach(function (k) { WS_READ[k] = 1; });
|
|
48
|
+
// Reads give up a little after the bridge's own limit, so a sleeping Mac never leaves a view loading forever.
|
|
49
|
+
// Writes wait for the answer (W.api's rule): a slow save is never reported as failed while it may still land.
|
|
50
|
+
var READ_LIMIT = { 'graph.ask': 175000, 'graph.duplicates': 50000, 'graph.merge.preview': 40000 };
|
|
51
|
+
// Server codes in plain words. The code itself stays on err.code for the retry rules below.
|
|
52
|
+
var PLAIN = {
|
|
53
|
+
learning_unavailable: 'This record could not be read on your Mac right now. Try again in a moment.',
|
|
54
|
+
revision_conflict: 'This changed since you opened it. It has been reloaded; check it and choose again.',
|
|
55
|
+
idempotency_conflict: 'That request was already used. Reload and choose again.',
|
|
56
|
+
memory_not_found: 'That memory is no longer on your Mac.',
|
|
57
|
+
not_memory_authority: 'Only the Mac that owns your memory can change this.',
|
|
58
|
+
owner_review_required: 'This needs your review first.',
|
|
59
|
+
index_missing: 'There is no knowledge index on this Mac yet.'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── small helpers ────────────────────────────────────────
|
|
63
|
+
function fmt(n) { return Number(n || 0).toLocaleString(); }
|
|
64
|
+
function plural(n, one, many) { return Number(n) === 1 ? one : many; }
|
|
65
|
+
function str(v) { return typeof v === 'string' ? v : ''; }
|
|
66
|
+
function clip(text, n) { var s = String(text || ''); return s.length > n ? s.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : s; }
|
|
67
|
+
function toDate(v) { if (v == null || v === '') return null; var d = new Date(v); return isNaN(d.getTime()) ? null : d; }
|
|
68
|
+
function when(v) {
|
|
69
|
+
var d = toDate(v); if (!d) return v ? String(v) : '';
|
|
70
|
+
var o = { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' };
|
|
71
|
+
if (d.getFullYear() !== new Date().getFullYear()) o.year = 'numeric';
|
|
72
|
+
return d.toLocaleString(undefined, o);
|
|
73
|
+
}
|
|
74
|
+
function day(v) {
|
|
75
|
+
var d = toDate(v); if (!d) return v ? String(v).slice(0, 10) : '';
|
|
76
|
+
var o = { month: 'short', day: 'numeric' }; if (d.getFullYear() !== new Date().getFullYear()) o.year = 'numeric';
|
|
77
|
+
return d.toLocaleDateString(undefined, o);
|
|
78
|
+
}
|
|
79
|
+
// Server titles end with the trace source ("Included in context 39 times · context_emitted"); people read the first part.
|
|
80
|
+
function cleanTitle(t) { return String(t || '').replace(/\s+·\s+[a-z]+(?:_[a-z]+)+\s*$/, '').trim(); }
|
|
81
|
+
function known(v) { return v && v !== 'unknown' ? v : ''; }
|
|
82
|
+
function storeName(k) { return STORE[k] || (k ? String(k).replace(/_/g, ' ') : ''); }
|
|
83
|
+
// A UUID v4 from getRandomValues: crypto.randomUUID needs a secure context, and a tailnet page over http is not one.
|
|
84
|
+
function uuid() {
|
|
85
|
+
var b = new Uint8Array(16); crypto.getRandomValues(b);
|
|
86
|
+
b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80;
|
|
87
|
+
var h = Array.prototype.map.call(b, function (x) { return (x + 256).toString(16).slice(1); }).join('');
|
|
88
|
+
return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20);
|
|
89
|
+
}
|
|
90
|
+
function icon(name) { return '<svg class="icon"><use href="#i-' + name + '"/></svg>'; }
|
|
91
|
+
// The tab is in the address: #/memories/t/<view> (app.js isTabRoute), so a reload, a new browser or Safari clearing its
|
|
92
|
+
// storage all land on the same tab; the stored view is only the fallback for a bare #/memories.
|
|
93
|
+
function tabRoute(args) { return args.length === 2 && args[0] === 't'; }
|
|
94
|
+
function readerArgs(r) { return r.pane === 'memories' && !tabRoute(r.args) ? r.args : []; }
|
|
95
|
+
function showTab(v) {
|
|
96
|
+
var h = '#/memories/t/' + encodeURIComponent(v);
|
|
97
|
+
if (location.hash === h) paint(); else location.replace(h);
|
|
98
|
+
}
|
|
99
|
+
function isWide() { return !!(window.matchMedia && window.matchMedia('(min-width: 760px)').matches); }
|
|
100
|
+
function el(id) { return document.getElementById(id); }
|
|
101
|
+
function btn(act, label, extra) { return '<button type="button" class="cx-btn' + (extra && extra.cls ? ' ' + extra.cls : ' quiet') + '" data-mm="' + act + '"' + (extra && extra.id != null ? ' data-id="' + esc(extra.id) + '"' : '') + (extra && extra.arg != null ? ' data-arg="' + esc(extra.arg) + '"' : '') + (extra && extra.disabled ? ' disabled' : '') + '>' + label + '</button>'; }
|
|
102
|
+
function textBtn(act, label, extra) { return '<button type="button" class="mm-link" data-mm="' + act + '"' + (extra && extra.id != null ? ' data-id="' + esc(extra.id) + '"' : '') + (extra && extra.arg != null ? ' data-arg="' + esc(extra.arg) + '"' : '') + (extra && extra.disabled ? ' disabled' : '') + '>' + label + '</button>'; }
|
|
103
|
+
|
|
104
|
+
// ── the bridge ───────────────────────────────────────────
|
|
105
|
+
var seq = 0;
|
|
106
|
+
function isRead(op, args) { return !!READ_OPS[op] || (op === 'workspace.request' && !!WS_READ[str(args && args.action)]); }
|
|
107
|
+
function call(op, args) {
|
|
108
|
+
var a = args || {};
|
|
109
|
+
// Copy stays in this browser: the same copy the rest of Control Web uses (plain-http fallback included).
|
|
110
|
+
if (op === 'copy') return W.copyText(String(a.text || '')).then(function () { return {}; });
|
|
111
|
+
var limit = READ_LIMIT[op] != null ? READ_LIMIT[op] : (isRead(op, a) ? 45000 : 0);
|
|
112
|
+
return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(null, plainNetError).then(function (r) {
|
|
113
|
+
if (r && r.ok) return r.details || {};
|
|
114
|
+
var raw = (r && r.message) || 'Your Mac did not answer that.';
|
|
115
|
+
var err = new Error(PLAIN[raw] || raw); err.code = raw; throw err;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// No HTTP status means the request never reached COS: the browser's own words ("Failed to fetch", "Load failed") say
|
|
119
|
+
// nothing to a person, so say what happened and what to do. A timeout already says it in plain words (W.api).
|
|
120
|
+
var UNREACHABLE = 'Could not reach COS on your Mac. Check that the Mac is awake and on Tailscale, then try again.';
|
|
121
|
+
function plainNetError(err) {
|
|
122
|
+
if (err && !err.status && err.code !== 'timeout') { var e = new Error(UNREACHABLE); e.code = 'unreachable'; throw e; }
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
function codeOf(e) { return String((e && (e.code || e.message)) || ''); }
|
|
126
|
+
|
|
127
|
+
// ── state ────────────────────────────────────────────────
|
|
128
|
+
var STORE_KEY = 'cos.web.memories';
|
|
129
|
+
var saved = W.readStore(STORE_KEY);
|
|
130
|
+
function list() { return { rows: [], total: null, cursor: null, loaded: false, loading: false, error: '', paged: undefined, coverage: {}, info: null }; }
|
|
131
|
+
var S = {
|
|
132
|
+
view: knownView(saved.view) ? saved.view : 'recent',
|
|
133
|
+
ktab: KTABS.indexOf(saved.ktab) >= 0 ? saved.ktab : 'graph', ktabChosen: false,
|
|
134
|
+
started: false, loadedAt: 0,
|
|
135
|
+
status: null, statusError: '', statusLoading: false,
|
|
136
|
+
recent: list(), review: list(), applied: Object.assign(list(), { days: 7 }), mem: Object.assign(list(), { q: '', hits: null, hitCursor: null }),
|
|
137
|
+
graph: null, graphError: '', setup: null, setupError: '', setupLoading: false, setupBusy: '', progress: null,
|
|
138
|
+
ingest: null, ingestLimit: 10, building: '', folderDraft: '',
|
|
139
|
+
detail: {}, memDetail: {}, passages: {}, loading: {},
|
|
140
|
+
decisions: {}, decisionTs: {}, decided: {},
|
|
141
|
+
armed: '', reviewBusy: '', reviewIntent: null, bulk: null,
|
|
142
|
+
ask: { q: '', busy: false, answer: null, error: '', cards: [], cardsBusy: false, startedAt: 0, message: '', pending: null },
|
|
143
|
+
focus: null, focusChosen: false, focusResolved: false, knowledgeFocus: null, neighbors: [], graphVisible: null,
|
|
144
|
+
graphFull: false, kit: null, kitError: '', ws: null, graphMemories: {},
|
|
145
|
+
dups: { busy: false, data: null, error: '' }, merge: null,
|
|
146
|
+
modal: null, copyId: null, copyOpts: { sources: true, graph: false }, drafts: {},
|
|
147
|
+
owner: null, ownerError: '', ownerBusy: false,
|
|
148
|
+
policy: null, policyError: '', policyBusy: false, policyIntent: null, policyLoading: false,
|
|
149
|
+
guard: null, guardError: '', guardBusy: '', guardRun: null, guardLines: 0, guardLoading: false,
|
|
150
|
+
steward: { tab: 'rules', rows: [], cursor: null, busy: false, error: '', intent: null, outcomes: null, serial: 0 }
|
|
151
|
+
};
|
|
152
|
+
function persist() { W.writeStore(STORE_KEY, { view: S.view, ktab: S.ktab }); }
|
|
153
|
+
var serials = {};
|
|
154
|
+
function nextSerial(k) { serials[k] = (serials[k] || 0) + 1; return serials[k]; }
|
|
155
|
+
|
|
156
|
+
// ── loading ──────────────────────────────────────────────
|
|
157
|
+
function loadAll() {
|
|
158
|
+
S.started = true; S.loadedAt = Date.now();
|
|
159
|
+
loadStatus(); loadRecent(false); loadReview(false); loadCues(); loadGraph();
|
|
160
|
+
if (S.view === 'memories') loadMem(false);
|
|
161
|
+
if (S.view === 'applied') loadApplied(false);
|
|
162
|
+
paint();
|
|
163
|
+
}
|
|
164
|
+
function loadStatus() {
|
|
165
|
+
if (S.statusLoading) return;
|
|
166
|
+
S.statusLoading = true;
|
|
167
|
+
// A decision made here stays out of the needs-you count until Refresh: the status read right after a write can still
|
|
168
|
+
// list it while the server's needs-you reader catches up.
|
|
169
|
+
call('status').then(function (d) { S.status = d; S.statusError = ''; chooseDefaultFocus(); },
|
|
170
|
+
function (e) { S.statusError = e.message; })
|
|
171
|
+
.then(function () { S.statusLoading = false; paint(); });
|
|
172
|
+
}
|
|
173
|
+
// Cues to rate and Rated cues read the server's cue history themselves, as Control's store does: the read is the
|
|
174
|
+
// capability check (a 404 is a server from before the routes, and both entries hide).
|
|
175
|
+
function loadCues() { loadCueWeek(); }
|
|
176
|
+
// Newest first, then by event id: many recall rows share one timestamp, and the server's order among them can differ from
|
|
177
|
+
// one read to the next, which reshuffled Recent learning on every visit.
|
|
178
|
+
function stableOrder(rows) {
|
|
179
|
+
return rows.map(function (r, i) { return { r: r, t: toDate(r && r.ts), i: i }; }).sort(function (a, b) {
|
|
180
|
+
var ta = a.t ? a.t.getTime() : -Infinity, tb = b.t ? b.t.getTime() : -Infinity;
|
|
181
|
+
if (ta !== tb) return tb - ta;
|
|
182
|
+
var ia = str(a.r && a.r.event_id), ib = str(b.r && b.r.event_id);
|
|
183
|
+
return ia < ib ? -1 : ia > ib ? 1 : a.i - b.i;
|
|
184
|
+
}).map(function (x) { return x.r; });
|
|
185
|
+
}
|
|
186
|
+
// Recent learning: the snapshot pages (protocol 1), else the older 90-day list.
|
|
187
|
+
function loadRecent(more) {
|
|
188
|
+
var L = S.recent;
|
|
189
|
+
if (more && (L.loading || !L.cursor)) return;
|
|
190
|
+
var serial = nextSerial('recent'); L.loading = true; if (!more) L.error = '';
|
|
191
|
+
paint();
|
|
192
|
+
var paged = L.paged === false ? Promise.reject(new Error('older server')) :
|
|
193
|
+
call('workspace.request', { action: 'learning_page', days: 90, limit: 50, cursor: more ? L.cursor : null }).then(function (d) {
|
|
194
|
+
if (d.protocol !== 1 || !Array.isArray(d.items)) throw new Error('older server');
|
|
195
|
+
L.paged = true;
|
|
196
|
+
return { rows: d.items, total: d.matched_total, cursor: d.next_cursor || null, coverage: d.coverage || {}, info: d };
|
|
197
|
+
});
|
|
198
|
+
paged.catch(function (err) {
|
|
199
|
+
if (more && L.paged) throw new Error(err.message + ' Refresh to start a new snapshot.');
|
|
200
|
+
L.paged = false;
|
|
201
|
+
var args = { days: 90, limit: 50 };
|
|
202
|
+
if (more && L.cursor) { args.sinceTs = L.cursor.since_ts; args.sinceEventId = L.cursor.since_event_id; }
|
|
203
|
+
return call('learning.list', args).then(function (d) {
|
|
204
|
+
return { rows: d.events || [], total: d.total, cursor: d.nextCursor || null, coverage: d.coverage || {},
|
|
205
|
+
info: { counts_by_type: {}, stores: {}, complete: false, notice: 'Activity totals need a newer server. The list covers 90 days.' } };
|
|
206
|
+
});
|
|
207
|
+
}).then(function (r) {
|
|
208
|
+
if (serial !== serials.recent) return;
|
|
209
|
+
L.rows = stableOrder(more ? L.rows.concat(r.rows) : r.rows); L.total = r.total != null ? r.total : L.total; L.cursor = r.cursor; L.coverage = r.coverage; L.info = r.info;
|
|
210
|
+
L.loaded = true; L.error = '';
|
|
211
|
+
seedDecisions(r.rows); prefetchLessons(r.rows);
|
|
212
|
+
}, function (e) {
|
|
213
|
+
if (serial !== serials.recent) return;
|
|
214
|
+
if (more) W.toast(e.message); else { L.error = e.message; L.loaded = true; }
|
|
215
|
+
}).then(function () { if (serial === serials.recent) L.loading = false; paint(); });
|
|
216
|
+
}
|
|
217
|
+
function loadReview(more) {
|
|
218
|
+
var L = S.review;
|
|
219
|
+
if (more && (L.loading || !L.cursor)) return;
|
|
220
|
+
var serial = nextSerial('review'); L.loading = true; if (!more) L.error = '';
|
|
221
|
+
paint();
|
|
222
|
+
call('workspace.request', { action: 'review_page', limit: 25, cursor: more ? L.cursor : null }).then(function (d) {
|
|
223
|
+
if (d.protocol !== 1 || !Array.isArray(d.items)) throw new Error('older server');
|
|
224
|
+
return { rows: d.items, total: d.matched_total, cursor: d.next_cursor || null };
|
|
225
|
+
}).catch(function (err) {
|
|
226
|
+
if (more) throw err;
|
|
227
|
+
return call('learning.review', { limit: 200 }).then(function (d) {
|
|
228
|
+
return { rows: d.events || [], total: d.reviewCount != null ? d.reviewCount : d.total, cursor: null };
|
|
229
|
+
});
|
|
230
|
+
}).then(function (r) {
|
|
231
|
+
if (serial !== serials.review) return;
|
|
232
|
+
L.rows = more ? L.rows.concat(r.rows) : r.rows; L.total = r.total; L.cursor = r.cursor; L.loaded = true; L.error = '';
|
|
233
|
+
seedDecisions(r.rows);
|
|
234
|
+
}, function (e) {
|
|
235
|
+
if (serial !== serials.review) return;
|
|
236
|
+
if (more) W.toast(e.message + ' Refresh To review to start again.'); else { L.error = e.message; L.loaded = true; }
|
|
237
|
+
}).then(function () { if (serial === serials.review) L.loading = false; paint(); });
|
|
238
|
+
}
|
|
239
|
+
var memTimer = null;
|
|
240
|
+
function loadMem(more) {
|
|
241
|
+
var L = S.mem, q = L.q.trim();
|
|
242
|
+
var cursor = more ? (q ? L.hitCursor : L.cursor) : null;
|
|
243
|
+
if (more && (L.loading || !cursor)) return;
|
|
244
|
+
var serial = nextSerial('mem'); L.loading = true; if (!more) L.error = '';
|
|
245
|
+
paint();
|
|
246
|
+
var paged = L.paged === false ? Promise.reject(new Error('older server')) :
|
|
247
|
+
call('workspace.request', { action: 'memory_page', q: q, limit: 25, cursor: cursor }).then(function (d) {
|
|
248
|
+
if (d.protocol !== 1 || !Array.isArray(d.items)) throw new Error('older server');
|
|
249
|
+
L.paged = true; return { rows: d.items, total: d.matched_total, cursor: d.next_cursor || null };
|
|
250
|
+
});
|
|
251
|
+
paged.catch(function (err) {
|
|
252
|
+
if (more) throw new Error(err.message + ' Search again to refresh.');
|
|
253
|
+
L.paged = false;
|
|
254
|
+
return call(q ? 'memories.search' : 'memories.list', q ? { q: q, limit: 20 } : { limit: 50 }).then(function (d) {
|
|
255
|
+
return q ? { rows: d.hits || [], total: null, cursor: null } : { rows: d.memories || [], total: d.total, cursor: null };
|
|
256
|
+
});
|
|
257
|
+
}).then(function (r) {
|
|
258
|
+
if (serial !== serials.mem || L.q.trim() !== q) return;
|
|
259
|
+
if (q) { L.hits = more ? (L.hits || []).concat(r.rows) : r.rows; L.hitCursor = r.cursor; }
|
|
260
|
+
else { L.hits = null; L.rows = more ? L.rows.concat(r.rows) : r.rows; L.total = r.total; L.cursor = r.cursor; }
|
|
261
|
+
L.loaded = true; L.error = '';
|
|
262
|
+
}, function (e) {
|
|
263
|
+
if (serial !== serials.mem) return;
|
|
264
|
+
if (more) W.toast(e.message); else { L.error = e.message; L.loaded = true; }
|
|
265
|
+
}).then(function () { if (serial === serials.mem) L.loading = false; paint(); });
|
|
266
|
+
}
|
|
267
|
+
function memQuery(q) {
|
|
268
|
+
S.mem.q = q; clearTimeout(memTimer);
|
|
269
|
+
if (q.trim().length === 1) return;
|
|
270
|
+
memTimer = setTimeout(function () { loadMem(false); }, q ? 350 : 0);
|
|
271
|
+
}
|
|
272
|
+
// Applied: the helper's `--kind used` window, never Recent filtered here. Fails closed: a server that ignored
|
|
273
|
+
// `kind` returns the whole window, and Applied stays empty rather than call those rows uses.
|
|
274
|
+
function loadApplied(more) {
|
|
275
|
+
var L = S.applied;
|
|
276
|
+
if (more && (L.loading || !L.cursor)) return;
|
|
277
|
+
var serial = nextSerial('applied'); L.loading = true; if (!more) L.error = '';
|
|
278
|
+
paint();
|
|
279
|
+
var args = { kind: 'used', days: L.days, limit: 50 };
|
|
280
|
+
if (more) { args.sinceTs = L.cursor.since_ts; args.sinceEventId = L.cursor.since_event_id; }
|
|
281
|
+
call('learning.list', args).then(function (d) {
|
|
282
|
+
if (serial !== serials.applied) return;
|
|
283
|
+
var events = d.events || [];
|
|
284
|
+
if (events.some(function (e) { return e.event_type !== 'used'; })) {
|
|
285
|
+
L.rows = []; L.total = null; L.cursor = null; L.loaded = true;
|
|
286
|
+
L.error = 'This server sent learning rows that are not uses. Applied stays empty rather than mislabel them.';
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
var seen = {}; (more ? L.rows : []).forEach(function (e) { seen[e.event_id] = 1; });
|
|
290
|
+
var rows = events.filter(function (e) { if (seen[e.event_id]) return false; seen[e.event_id] = 1; return true; });
|
|
291
|
+
L.rows = stableOrder(more ? L.rows.concat(rows) : rows); L.total = d.total; L.cursor = d.nextCursor || null; L.coverage = d.coverage || {};
|
|
292
|
+
L.loaded = true; L.error = '';
|
|
293
|
+
L.rows.forEach(function (e) { if (e.lesson_id) ensureMem(e.lesson_id); });
|
|
294
|
+
}, function (e) { if (serial !== serials.applied) return; L.error = e.message; L.loaded = true; })
|
|
295
|
+
.then(function () { if (serial === serials.applied) L.loading = false; paint(); });
|
|
296
|
+
}
|
|
297
|
+
function setAppliedDays(days) {
|
|
298
|
+
S.applied = Object.assign(list(), { days: days === 90 ? 90 : 7 });
|
|
299
|
+
loadApplied(false);
|
|
300
|
+
}
|
|
301
|
+
function loadGraph() {
|
|
302
|
+
return call('graph.status').then(function (d) {
|
|
303
|
+
S.graph = d; S.graphError = '';
|
|
304
|
+
var lk = d.lock || {};
|
|
305
|
+
if (lk.state === 'exclusive' || lk.state === 'shared') watchIngest();
|
|
306
|
+
if (d.entities == null && !S.ktabChosen) S.ktab = 'setup';
|
|
307
|
+
}, function (e) { S.graphError = e.message; }).then(paint);
|
|
308
|
+
}
|
|
309
|
+
// A bot-memory row is read from the memory store (memory.detail): the learning index cannot look those events up
|
|
310
|
+
// by id ("learning_unavailable" after 8 s on 11/06). Governed rows read the workspace record; the rest, learning.event.
|
|
311
|
+
function ensureDetail(row) {
|
|
312
|
+
var id = row && row.event_id;
|
|
313
|
+
if (!id || S.detail[id] || S.loading[id]) return;
|
|
314
|
+
if (row.store === 'bot_memory' && row.lesson_id) { ensureMem(row.lesson_id); return; }
|
|
315
|
+
S.loading[id] = true;
|
|
316
|
+
var p = row.governance ? call('workspace.request', { action: 'get_memory', id: row.lesson_id }).then(function (d) {
|
|
317
|
+
var item = d.item || {};
|
|
318
|
+
return { detail: { content: item.payload && item.payload.content }, governance: item.state, journal_revision: item.revision };
|
|
319
|
+
}) : call('learning.event', { id: id });
|
|
320
|
+
p.then(function (d) { S.detail[id] = d; }, function (e) { S.detail[id] = { _error: e.message }; })
|
|
321
|
+
.then(function () { delete S.loading[id]; paint(); });
|
|
322
|
+
}
|
|
323
|
+
function ensureMem(id) {
|
|
324
|
+
if (!id || S.memDetail[id] || S.loading['m:' + id]) return;
|
|
325
|
+
S.loading['m:' + id] = true;
|
|
326
|
+
call('memory.detail', { id: id }).then(function (d) { S.memDetail[id] = d; }, function (e) { S.memDetail[id] = { _error: e.message }; })
|
|
327
|
+
.then(function () { delete S.loading['m:' + id]; paint(); });
|
|
328
|
+
}
|
|
329
|
+
// Recall rows are titled by what happened ("Included in context 39 times"). The row says which memory it was once its
|
|
330
|
+
// record is read: the first page only, three at a time, each record once.
|
|
331
|
+
var prefetchQueue = [], prefetchRunning = 0;
|
|
332
|
+
function prefetchLessons(rows) {
|
|
333
|
+
(rows || []).slice(0, 25).forEach(function (e) {
|
|
334
|
+
if (e && /^mem_/.test(str(e.lesson_id)) && isUseType(e) && !S.memDetail[e.lesson_id] && prefetchQueue.indexOf(e.lesson_id) < 0) prefetchQueue.push(e.lesson_id);
|
|
335
|
+
});
|
|
336
|
+
pump();
|
|
337
|
+
}
|
|
338
|
+
function pump() {
|
|
339
|
+
while (prefetchRunning < 3 && prefetchQueue.length) {
|
|
340
|
+
var id = prefetchQueue.shift();
|
|
341
|
+
if (S.memDetail[id] || S.loading['m:' + id]) continue;
|
|
342
|
+
prefetchRunning++; S.loading['m:' + id] = true;
|
|
343
|
+
(function (mid) {
|
|
344
|
+
call('memory.detail', { id: mid }).then(function (d) { S.memDetail[mid] = d; }, function (e) { S.memDetail[mid] = { _error: e.message }; })
|
|
345
|
+
.then(function () { delete S.loading['m:' + mid]; prefetchRunning--; paintSoon(); pump(); });
|
|
346
|
+
})(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// A review decision survives a reload: the ledger's pruned/accepted rows seed the map the actions read, newest wins,
|
|
350
|
+
// and one made in this session is stamped now so a stale seed never undoes it.
|
|
351
|
+
function seedDecisions(rows) {
|
|
352
|
+
(rows || []).forEach(function (e) {
|
|
353
|
+
if (!e || e.store !== 'review_ledger' || !e.lesson_id || (e.event_type !== 'pruned' && e.event_type !== 'accepted')) return;
|
|
354
|
+
var ts = String(e.ts || '');
|
|
355
|
+
if (S.decisionTs[e.lesson_id] && S.decisionTs[e.lesson_id] >= ts) return;
|
|
356
|
+
S.decisionTs[e.lesson_id] = ts; S.decisions[e.lesson_id] = e.event_type;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// The graph opens on the person this COS is about (the owner name, resolved through a search so the entity's exact
|
|
360
|
+
// spelling wins). A focus the person picks is never overridden.
|
|
361
|
+
function chooseDefaultFocus() {
|
|
362
|
+
var name = S.status && S.status.ownerName;
|
|
363
|
+
if (!name || S.focusChosen || S.focusResolved) return;
|
|
364
|
+
S.focusResolved = true;
|
|
365
|
+
call('graph.search', { q: name, limit: 5 }).then(function (d) {
|
|
366
|
+
var items = d.items || [];
|
|
367
|
+
var pick = items.filter(function (it) { return String(it.id).toLowerCase() === String(name).toLowerCase(); })[0] ||
|
|
368
|
+
items.filter(function (it) { return String(it.type || '').toLowerCase() === 'person'; })[0] || items[0];
|
|
369
|
+
if (pick && !S.focusChosen) { S.focus = pick.id; if (S.ws && el('mm-graph-host')) S.ws.attach(el('mm-graph-host'), S.focus); }
|
|
370
|
+
}, function () {});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── what a row is ────────────────────────────────────────
|
|
374
|
+
function isProposal(e) { return !!(e && e.target && e.target.kind === 'task-proposal'); }
|
|
375
|
+
function isPattern(e) { return !!(e && (e.event_type === 'promotable' || (e.target && e.target.kind === 'pattern'))); }
|
|
376
|
+
function isPruneReady(e) { return !!e && e.store === 'bot_memory' && typeof e.category === 'string' && e.category.indexOf(PRUNE_READY) === 0; }
|
|
377
|
+
function pruneReason(e) { var c = String(e.category || ''); return c.indexOf(' · ') > 0 ? c.slice(c.indexOf(' · ') + 3) : ''; }
|
|
378
|
+
function decisionFor(e) { return e && S.decisions[e.lesson_id]; }
|
|
379
|
+
function isUseType(e) { return !!e && ['used', 'checked', 'retrieved', 'included', 'applied'].indexOf(e.event_type) >= 0; }
|
|
380
|
+
function isMemoryLesson(e) { return !!e && !!e.lesson_id && (/^mem_/.test(e.lesson_id) || (e.target && e.target.kind === 'memory')); }
|
|
381
|
+
function kindLabel(e) {
|
|
382
|
+
if (e && e.store === 'bot_memory' && e.event_type === 'captured' && decisionFor(e) === 'pruned') return 'Quarantined';
|
|
383
|
+
if (isPruneReady(e) && !decisionFor(e)) return 'Ready to quarantine';
|
|
384
|
+
if (isProposal(e)) return 'Suggested change';
|
|
385
|
+
if (isPattern(e)) return 'Suggested rule';
|
|
386
|
+
return KIND[e && e.event_type] || 'Event';
|
|
387
|
+
}
|
|
388
|
+
function outcomeText(e) { var o = e && e.outcome; if (!o) return null; if (typeof o === 'string') return o; return o.result ? o.result + (o.name ? ' · ' + o.name : '') : null; }
|
|
389
|
+
function rowStatus(e) {
|
|
390
|
+
var o = outcomeText(e);
|
|
391
|
+
if (o) { var r = typeof e.outcome === 'object' ? e.outcome.result : e.outcome; return { text: 'Checked: ' + o, tone: /^(pass|passed|success)$/.test(r) ? 'ok' : /^(fail|failed)$/.test(r) ? 'bad' : '' }; }
|
|
392
|
+
if (e.needs) return { text: (GATE[e.needs.gate] || 'Needs you') + (e.needs.reason ? '. ' + e.needs.reason : ''), tone: 'warn' };
|
|
393
|
+
if (isProposal(e) || isPattern(e)) return { text: decisionFor(e) === 'dismissed' ? 'Dismissed' : 'Needs your review', tone: decisionFor(e) === 'dismissed' ? '' : 'warn' };
|
|
394
|
+
if (e.event_type === 'dismissed') return { text: 'Dismissed', tone: '' };
|
|
395
|
+
if (e.event_type === 'retrieved') return { text: 'Recalled. Not yet shown to be used.', tone: '' };
|
|
396
|
+
if (e.event_type === 'included') return { text: 'Included in context. Not yet shown to be used.', tone: '' };
|
|
397
|
+
if (e.event_type === 'applied' || e.event_type === 'used') return { text: 'Use recorded', tone: 'ok' };
|
|
398
|
+
if (isPruneReady(e) && !decisionFor(e)) return { text: pruneReason(e) || 'Ready to quarantine', tone: 'warn' };
|
|
399
|
+
return { text: 'Saved. No later use yet.', tone: '' };
|
|
400
|
+
}
|
|
401
|
+
function memText(id) { var m = id && S.memDetail[id]; return m && !m._error ? str(m.summary) || str(m.content) : ''; }
|
|
402
|
+
function memError(id) { var m = id && S.memDetail[id]; return m && m._error ? String(m._error) : ''; }
|
|
403
|
+
// What a row is titled: a recall row names its memory once that record is read.
|
|
404
|
+
function rowTitle(e) {
|
|
405
|
+
var t = cleanTitle(e.title);
|
|
406
|
+
if (isUseType(e) && isMemoryLesson(e)) { var m = memText(e.lesson_id); if (m) return m; }
|
|
407
|
+
return t || '(untitled)';
|
|
408
|
+
}
|
|
409
|
+
function rowWhat(e) { return isUseType(e) && isMemoryLesson(e) && memText(e.lesson_id) ? cleanTitle(e.title) : ''; }
|
|
410
|
+
|
|
411
|
+
// ── To review: what needs you, then every proposed change ──
|
|
412
|
+
// The count is Control's gated needs-you list (status.needs_you, capped), never the review page's total. The page
|
|
413
|
+
// lists those items first so the number on the tab is a list you can see, then every proposed change after them.
|
|
414
|
+
// The gate's reason in plain words where its shape is known; otherwise as the server wrote it.
|
|
415
|
+
function plainReason(r) {
|
|
416
|
+
var t = String(r || ''), m;
|
|
417
|
+
if ((m = /^promotable at confidence ([\d.]+) \(< ([\d.]+)\)$/.exec(t))) return 'Confidence ' + m[1] + ', under the ' + m[2] + ' bar for a rule';
|
|
418
|
+
if ((m = /^names (.+?) near '(.+?)' with no attribution field$/.exec(t))) return 'Mentions ' + m[1].replace(/\b\w/g, function (c) { return c.toUpperCase(); }) + ' near "' + m[2] + '" without saying who said it';
|
|
419
|
+
return t;
|
|
420
|
+
}
|
|
421
|
+
function needsItems() {
|
|
422
|
+
var ny = (S.recent.info && S.recent.info.needs_you) || (S.status && S.status.needs_you) || null;
|
|
423
|
+
return ny && Array.isArray(ny.items) ? ny.items : [];
|
|
424
|
+
}
|
|
425
|
+
function needsRows() {
|
|
426
|
+
// A row decided here stays listed (its reader offers Restore); only the count leaves it out.
|
|
427
|
+
return needsItems().map(function (it) {
|
|
428
|
+
var fromList = S.review.rows.filter(function (r) { return r.event_id === it.event_id; })[0] ||
|
|
429
|
+
S.recent.rows.filter(function (r) { return r.event_id === it.event_id; })[0];
|
|
430
|
+
var mem = /^mem_/.test(str(it.lesson_id));
|
|
431
|
+
var base = fromList || { event_id: it.event_id, lesson_id: it.lesson_id, ts: it.ts, title: it.title, category: it.category,
|
|
432
|
+
store: it.store || (mem ? 'bot_memory' : null), event_type: mem ? 'captured' : 'promotable', scope: 'unknown', engine: 'unknown',
|
|
433
|
+
target: { kind: mem ? 'memory' : 'pattern', id: it.lesson_id }, source_refs: [], applies_to: [] };
|
|
434
|
+
return Object.assign({}, base, { needs: { gate: it.gate, reason: plainReason(it.reason) } });
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function needsCount() {
|
|
438
|
+
var s = S.status || {}, ny = (S.recent.info && S.recent.info.needs_you) || s.needs_you;
|
|
439
|
+
if (ny && Array.isArray(ny.items)) return ny.items.filter(function (it) { return !S.decided[it.lesson_id]; }).length;
|
|
440
|
+
if (ny && ny.count != null) return ny.limit != null ? Math.min(Number(ny.count), Number(ny.limit)) : Number(ny.count);
|
|
441
|
+
if (typeof s.learningToReview === 'number') return s.learningToReview;
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
function findRow(id) {
|
|
445
|
+
var all = needsRows().concat(S.review.rows, S.recent.rows, S.applied.rows);
|
|
446
|
+
for (var i = 0; i < all.length; i++) if (all[i].event_id === id) return all[i];
|
|
447
|
+
var d = S.detail[id];
|
|
448
|
+
return d && !d._error && d.event_id ? d : null;
|
|
449
|
+
}
|
|
450
|
+
function fullRow(e) { var d = S.detail[e.event_id]; return Object.assign({}, e, d && !d._error ? d : {}); }
|
|
451
|
+
function rewardOn() { return !!(S.status && S.status.learningRewardEnabled === true); }
|
|
452
|
+
function rankingSentence() { return rewardOn() ? '<p>This lesson now ranks ahead of unused memories like it in later recall.</p>' : ''; }
|
|
453
|
+
function appliedCount(e) { var m = /Applied\s+([\d,]+)\s+time/.exec(String((e && e.title) || '')); return m ? Number(m[1].replace(/,/g, '')) : null; }
|
|
454
|
+
function bridgeOn() { return !!(S.status && S.status.contextScriptsDirectory); }
|
|
455
|
+
function fileGraph() { return !!(S.graph && S.graph.engine === 'explicit_document_links'); }
|
|
456
|
+
function memoryPath() { if (bridgeOn()) return 'bridge'; if (S.graph && (S.graph.entities != null || S.graph.engine)) return 'knowledge'; return 'files'; }
|
|
457
|
+
function hostLabel(h) { return String(h || '').replace(/\.local$/, '').replace(/-/g, ' '); }
|
|
458
|
+
function lockHeld(lk) { return !!lk && (lk.state === 'exclusive' || lk.state === 'shared'); }
|
|
459
|
+
|
|
460
|
+
// ── Cues to rate and Rated cues (MeetingAsk.swift CueReviewPane, CueRateCard, CueRatedRow; MeetingAskCore.swift
|
|
461
|
+
// CueVerdict, CueVerdicts, CueVerdictsStore) ─────────────────────────────────────────────────────────────────────
|
|
462
|
+
// The server keeps the cue log and the verdicts: GET /api/live-cues/history (days, unrated, limit) and
|
|
463
|
+
// POST /api/live-cues/feedback {sessionId, cueId, verdict, note?}. A verdict here never touches the glasses' up or down
|
|
464
|
+
// vote, which the server keeps beside it. Rating is the only write, and only from a tap or a key on a focused card.
|
|
465
|
+
var VERDICTS = ['useful', 'late', 'repeat', 'wrong', 'obvious'];
|
|
466
|
+
var CUE_MIN_SERVER = '6.79.0', CUE_DAYS = 7, CUE_NOTE_MAX = 200;
|
|
467
|
+
var CUE_KIND = { challenge: 'Challenge', reinforce: 'Reinforce', decision: 'Decision', promise: 'Promise', followUp: 'Follow-up', insight: 'Insight' };
|
|
468
|
+
var CUE_READ_LINE = {
|
|
469
|
+
checking: 'Loading cues…', olderServer: 'Rating cues needs server ' + CUE_MIN_SERVER + '. Update the server in Settings.',
|
|
470
|
+
unreachable: 'COS server unreachable. Cues show here once it answers.', reconnect: 'Sign this browser in again to see cues.',
|
|
471
|
+
unreadable: 'The server\'s answer about cues was not readable. Update the server in Settings.',
|
|
472
|
+
storageUnreadable: 'The server\'s ratings file cannot be read, so cues cannot be listed. Check again after the server restarts.' };
|
|
473
|
+
S.cue = { available: null, weekState: 'checking', queue: [], counts: null, ratedState: 'checking', rated: [], ratedLoaded: false,
|
|
474
|
+
saving: {}, errors: {}, skipped: [], notes: {}, weekBusy: false, ratedBusy: false, failedStatus: 0, ratedStatus: 0 };
|
|
475
|
+
function verdictLabel(v) { return v ? v.charAt(0).toUpperCase() + v.slice(1) : ''; }
|
|
476
|
+
function cueNum(v) { var n = Number(v); return isFinite(n) ? n : 0; }
|
|
477
|
+
// CueVerdicts.read: the answer to a state, and from a 200 in the contract's shape, the cues and counts.
|
|
478
|
+
function cueRead(d) {
|
|
479
|
+
var c = d && d.counts;
|
|
480
|
+
if (!d || d.v !== 1 || !Array.isArray(d.cues) || !c || typeof c.total !== 'number' || typeof c.rated !== 'number' || typeof c.unrated !== 'number') return { state: 'unreadable' };
|
|
481
|
+
var by = {}; VERDICTS.forEach(function (v) { by[v] = c.byVerdict && typeof c.byVerdict[v] === 'number' ? c.byVerdict[v] : 0; });
|
|
482
|
+
var cues = d.cues.filter(function (r) { return r && typeof r.cueId === 'string' && r.cueId && typeof r.sessionId === 'string' && r.sessionId && cueNum(r.createdAt) > 0 && typeof r.detail === 'string' && r.detail; });
|
|
483
|
+
return { state: 'loaded', cues: cues, counts: { total: c.total, rated: c.rated, unrated: c.unrated, byVerdict: by } };
|
|
484
|
+
}
|
|
485
|
+
function cueReadError(err) {
|
|
486
|
+
var st = err && err.status;
|
|
487
|
+
if (!st) return { state: 'unreachable' };
|
|
488
|
+
if (st === 404) return { state: 'olderServer' };
|
|
489
|
+
if (st === 401 || st === 403) return { state: 'reconnect' };
|
|
490
|
+
if (st === 503 && err.code === 'verdicts_unreadable') return { state: 'storageUnreadable' };
|
|
491
|
+
return { state: 'failed', status: st };
|
|
492
|
+
}
|
|
493
|
+
function cueLearn(state) {
|
|
494
|
+
var next = state === 'loaded' ? true : state === 'olderServer' ? false : S.cue.available;
|
|
495
|
+
S.cue.available = next;
|
|
496
|
+
if (next === false && (S.view === 'cues' || S.view === 'rated')) { S.view = 'recent'; persist(); if (tabRoute(W.parseRoute().args)) showTab('recent'); }
|
|
497
|
+
}
|
|
498
|
+
function cueVerdict(c) { return c && c.rating && c.rating.verdict ? c.rating.verdict : null; }
|
|
499
|
+
// CueVerdicts.queueOrder: oldest first, then the skipped cues in the order they were skipped.
|
|
500
|
+
function queueOrder(cues) {
|
|
501
|
+
var skip = S.cue.skipped;
|
|
502
|
+
var fresh = cues.filter(function (c) { return skip.indexOf(c.cueId) < 0; }).sort(function (a, b) { return cueNum(a.createdAt) - cueNum(b.createdAt); });
|
|
503
|
+
return fresh.concat(skip.map(function (id) { return cues.filter(function (c) { return c.cueId === id; })[0]; }).filter(Boolean));
|
|
504
|
+
}
|
|
505
|
+
function loadCueWeek() {
|
|
506
|
+
var C = S.cue; if (C.weekBusy) return;
|
|
507
|
+
C.weekBusy = true; if (C.weekState !== 'loaded') C.weekState = 'checking'; paint();
|
|
508
|
+
W.api('/api/live-cues/history?days=' + CUE_DAYS + '&unrated=1&limit=200').then(cueRead, cueReadError).then(function (r) {
|
|
509
|
+
C.weekBusy = false; cueLearn(r.state); C.weekState = r.state; C.failedStatus = r.status || 0;
|
|
510
|
+
if (r.state === 'loaded') { C.queue = queueOrder(r.cues.filter(function (c) { return !cueVerdict(c); })); C.counts = r.counts; }
|
|
511
|
+
paint();
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// The rated list reads the whole week (up to the server's 1,000 rows), so every rated cue shows, not only the newest 200's.
|
|
515
|
+
function loadCueRated() {
|
|
516
|
+
var C = S.cue; if (C.ratedBusy) return;
|
|
517
|
+
C.ratedBusy = true; if (C.ratedState !== 'loaded') C.ratedState = 'checking'; paint();
|
|
518
|
+
W.api('/api/live-cues/history?days=' + CUE_DAYS + '&limit=1000').then(cueRead, cueReadError).then(function (r) {
|
|
519
|
+
C.ratedBusy = false; cueLearn(r.state); C.ratedState = r.state; C.ratedStatus = r.status || 0;
|
|
520
|
+
if (r.state === 'loaded') { C.rated = r.cues.filter(cueVerdict).sort(function (a, b) { return cueNum(b.createdAt) - cueNum(a.createdAt); }); C.counts = r.counts; C.ratedLoaded = true; }
|
|
521
|
+
paint();
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
// CueVerdicts.note: whitespace collapsed, at most 200 UTF-16 units, cut on a character, as the server stores it.
|
|
525
|
+
function cueNote(raw) {
|
|
526
|
+
var collapsed = String(raw || '').split(/\s+/).filter(Boolean).join(' '), out = '';
|
|
527
|
+
for (var i = 0, chars = Array.from(collapsed); i < chars.length; i++) { if (out.length + chars[i].length > CUE_NOTE_MAX) break; out += chars[i]; }
|
|
528
|
+
return out.trim();
|
|
529
|
+
}
|
|
530
|
+
// CueVerdicts.rateOutcome and rateLine: why a rating did not save, in Control's words.
|
|
531
|
+
function cueRateLine(err) {
|
|
532
|
+
var st = err && err.status, code = err && err.code;
|
|
533
|
+
if (code === 'unknown_cue') return 'This cue is older than 30 days and was removed.';
|
|
534
|
+
if (code === 'cue_session_mismatch' || st === 409) return 'The server files this cue under another meeting. The rating was not saved.';
|
|
535
|
+
if (code === 'verdicts_unreadable') return 'The server\'s ratings file cannot be read. The rating was not saved.';
|
|
536
|
+
if (!st) return 'Your Mac did not answer. The rating was not saved; try again.';
|
|
537
|
+
if (st === 404) return 'Rating cues needs server ' + CUE_MIN_SERVER + '. Update the server in Settings.';
|
|
538
|
+
if (st === 503) return 'Server is updating. Rating not saved, try again.';
|
|
539
|
+
if (st === 400) return 'The server did not accept this rating.';
|
|
540
|
+
if (st === 401 || st === 403) return 'Sign this browser in again to rate cues.';
|
|
541
|
+
return 'The rating was not saved (HTTP ' + st + '). Try again.';
|
|
542
|
+
}
|
|
543
|
+
function findCue(id) {
|
|
544
|
+
var C = S.cue; return C.queue.filter(function (c) { return c.cueId === id; })[0] || C.rated.filter(function (c) { return c.cueId === id; })[0] || null;
|
|
545
|
+
}
|
|
546
|
+
// CueVerdictsStore.rate: one call per cue at a time; on success every list that holds the cue shows the new verdict.
|
|
547
|
+
function rateCue(id, verdict, note) {
|
|
548
|
+
var C = S.cue, cue = findCue(id);
|
|
549
|
+
if (!cue || C.saving[id] || (verdict && VERDICTS.indexOf(verdict) < 0)) return;
|
|
550
|
+
var text = verdict ? cueNote(note) : '';
|
|
551
|
+
var body = { sessionId: cue.sessionId, cueId: cue.cueId, verdict: verdict || 'clear' };
|
|
552
|
+
if (text) body.note = text;
|
|
553
|
+
C.saving[id] = true; delete C.errors[id]; paint();
|
|
554
|
+
W.api('/api/live-cues/feedback', { method: 'POST', body: body }).then(function (d) {
|
|
555
|
+
if (!d || d.ok !== true) throw Object.assign(new Error('not saved'), { status: 200 });
|
|
556
|
+
var stored = VERDICTS.indexOf(d.verdict) >= 0 ? d.verdict : null, vote = d.vote === 'up' || d.vote === 'down' ? d.vote : null;
|
|
557
|
+
var old = cueVerdict(cue), updated = Object.assign({}, cue, { rating: stored || vote ? { verdict: stored, note: stored ? text || null : null, vote: vote, at: Date.now() } : null });
|
|
558
|
+
applyCue(updated, old);
|
|
559
|
+
delete C.notes[id];
|
|
560
|
+
}, function (err) {
|
|
561
|
+
if (err && err.status === 404 && err.code !== 'unknown_cue') C.available = false;
|
|
562
|
+
C.errors[id] = cueRateLine(err);
|
|
563
|
+
}).then(function () { delete C.saving[id]; paint(); focusCueCard(); });
|
|
564
|
+
}
|
|
565
|
+
function moveCounts(old, next) {
|
|
566
|
+
var c = S.cue.counts; if (!c || old === next) return;
|
|
567
|
+
c = { total: c.total, rated: c.rated, unrated: c.unrated, byVerdict: Object.assign({}, c.byVerdict) };
|
|
568
|
+
if (old) c.byVerdict[old] = Math.max(0, (c.byVerdict[old] || 0) - 1);
|
|
569
|
+
if (next) c.byVerdict[next] = (c.byVerdict[next] || 0) + 1;
|
|
570
|
+
if (!old) { c.rated += 1; c.unrated = Math.max(0, c.unrated - 1); }
|
|
571
|
+
if (!next) { c.rated = Math.max(0, c.rated - 1); c.unrated += 1; }
|
|
572
|
+
S.cue.counts = c;
|
|
573
|
+
}
|
|
574
|
+
function applyCue(cue, old) {
|
|
575
|
+
var C = S.cue, now = cueVerdict(cue);
|
|
576
|
+
moveCounts(old, now);
|
|
577
|
+
C.skipped = C.skipped.filter(function (x) { return x !== cue.cueId; });
|
|
578
|
+
var waiting = C.queue.filter(function (c) { return c.cueId !== cue.cueId; });
|
|
579
|
+
if (!now && C.weekState === 'loaded' && cueNum(cue.createdAt) > Date.now() - CUE_DAYS * 86400000) waiting.push(cue);
|
|
580
|
+
C.queue = queueOrder(waiting);
|
|
581
|
+
var done = C.rated.filter(function (c) { return c.cueId !== cue.cueId; });
|
|
582
|
+
if (now && C.ratedState === 'loaded') done.push(cue);
|
|
583
|
+
C.rated = done.sort(function (a, b) { return cueNum(b.createdAt) - cueNum(a.createdAt); });
|
|
584
|
+
}
|
|
585
|
+
function skipCue(id) {
|
|
586
|
+
var C = S.cue; C.skipped = C.skipped.filter(function (x) { return x !== id; }); C.skipped.push(id);
|
|
587
|
+
C.queue = queueOrder(C.queue); paint(); focusCueCard();
|
|
588
|
+
}
|
|
589
|
+
// The card takes focus after a rating or a skip only when the person was already working from the keyboard there.
|
|
590
|
+
var cueKeyboard = false;
|
|
591
|
+
function focusCueCard() { if (!cueKeyboard) return; var card = el('mm-cue-card'); if (card && card.focus) card.focus({ preventScroll: true }); }
|
|
592
|
+
// CueVerdicts.key: 1 to 5 rate, N goes to the note, S skips; only while the card itself has focus, without modifiers.
|
|
593
|
+
function cueKey(e) {
|
|
594
|
+
var card = e.target;
|
|
595
|
+
if (!card || card.id !== 'mm-cue-card' || e.metaKey || e.ctrlKey || e.altKey) return false;
|
|
596
|
+
var id = card.getAttribute('data-cue'), k = String(e.key || '');
|
|
597
|
+
if (/^[1-5]$/.test(k)) { cueKeyboard = true; rateCue(id, VERDICTS[Number(k) - 1], S.cue.notes[id]); return true; }
|
|
598
|
+
if (k === 'n' || k === 'N') { cueKeyboard = true; var n = el('mm-cue-note'); if (n && n.focus) n.focus(); return true; }
|
|
599
|
+
if (k === 's' || k === 'S') { cueKeyboard = true; skipCue(id); return true; }
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
function cueClock(ms) { return new Date(ms).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); }
|
|
603
|
+
// "Today 7:48 AM, 7 min in"; "Yesterday", a weekday within the week, else "Oct 12".
|
|
604
|
+
function cueWhen(c) {
|
|
605
|
+
var d = new Date(cueNum(c.createdAt)), now = new Date();
|
|
606
|
+
var days = Math.round((new Date(now.getFullYear(), now.getMonth(), now.getDate()) - new Date(d.getFullYear(), d.getMonth(), d.getDate())) / 86400000);
|
|
607
|
+
var day = days === 0 ? 'Today' : days === 1 ? 'Yesterday' : days > 1 && days < 7 ? d.toLocaleDateString(undefined, { weekday: 'short' }) : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
608
|
+
var start = c.armedAt ? cueNum(c.armedAt) : (/^meeting_(\d{13})_/.exec(c.sessionId || '') || [])[1] ? Number(/^meeting_(\d{13})_/.exec(c.sessionId)[1]) : null;
|
|
609
|
+
var sec = start ? (cueNum(c.createdAt) - start) / 1000 : -1;
|
|
610
|
+
return day + ' ' + cueClock(cueNum(c.createdAt)) + (sec >= 0 && sec < 43200 ? ', ' + Math.floor(sec / 60) + ' min in' : '');
|
|
611
|
+
}
|
|
612
|
+
function cueTitle(c) {
|
|
613
|
+
var saved = (c.titleSource === 'saved' || c.titleSource === 'library') ? str(c.title) : '';
|
|
614
|
+
return [saved, str(c.meetingTitle)].filter(function (t) { return t.trim(); })[0] || 'Glasses meeting';
|
|
615
|
+
}
|
|
616
|
+
function cueScore(g) { return typeof g === 'number' && isFinite(g) ? g.toFixed(2) : ''; }
|
|
617
|
+
// A host a stranger could own, as MeetingAskText.publicHost decides; anything else stays plain text.
|
|
618
|
+
function publicHost(raw) {
|
|
619
|
+
var h = String(raw || '').toLowerCase(); if (h.slice(-1) === '.') h = h.slice(0, -1);
|
|
620
|
+
if (!h || h.charAt(0) === '.' || h.slice(-1) === '.' || h.indexOf('..') >= 0 || /[:[%]/.test(h)) return false;
|
|
621
|
+
if (h === 'localhost' || /\.(localhost|local|internal)$/.test(h)) return false;
|
|
622
|
+
var labels = h.split('.'); return labels.length >= 2 && /^[a-z]/.test(labels[labels.length - 1]);
|
|
623
|
+
}
|
|
624
|
+
function cueHttps(u) {
|
|
625
|
+
try { var x = new URL(String(u || '')); return x.protocol === 'https:' && publicHost(x.hostname) && !x.username && !x.password && (!x.port || x.port === '443') ? x : null; } catch (_e) { return null; }
|
|
626
|
+
}
|
|
627
|
+
function cueSourceText(s) {
|
|
628
|
+
var x = s.kind === 'web' ? cueHttps(s.url) : null;
|
|
629
|
+
if (x) { var host = x.hostname.toLowerCase().replace(/\.$/, '').replace(/^www\./, ''); return (s.title || host) + ' (' + host + ')'; }
|
|
630
|
+
return s.date ? (s.title || '') + ' (' + s.date + ')' : (s.title || '');
|
|
631
|
+
}
|
|
632
|
+
// CueSourcesList: a web source opens its https page and a memory opens in All memories; a meeting or a file has no place
|
|
633
|
+
// to open from the browser (the Mac opens its library row or shows the file in Finder), so those stay plain text.
|
|
634
|
+
function cueSourcesHtml(c) {
|
|
635
|
+
var src = (c.sources || []).filter(function (s) { return s && s.title; });
|
|
636
|
+
if (!src.length) return '<p class="mm-muted">Sources: none, from the room only</p>';
|
|
637
|
+
function opens(s) { return (s.kind === 'web' && cueHttps(s.url)) || (s.kind === 'memory' && s.memoryId); }
|
|
638
|
+
if (!src.some(opens)) return '<p class="mm-muted">Sources: ' + esc(src.map(cueSourceText).join('; ')) + '</p>';
|
|
639
|
+
return '<div class="mm-cue-sources"><span class="mm-muted">Sources:</span><ul>' + src.map(function (s) {
|
|
640
|
+
var t = esc(cueSourceText(s)), x = s.kind === 'web' ? cueHttps(s.url) : null;
|
|
641
|
+
if (x) return '<li><a href="' + esc(x.href) + '" target="_blank" rel="noopener noreferrer">' + t + '</a></li>';
|
|
642
|
+
if (s.kind === 'memory' && s.memoryId) return '<li>' + textBtn('cue-memory', t, { id: s.memoryId }) + '</li>';
|
|
643
|
+
return '<li class="mm-muted">' + t + '</li>';
|
|
644
|
+
}).join('') + '</ul></div>';
|
|
645
|
+
}
|
|
646
|
+
function cueMetaHtml(c, extra) {
|
|
647
|
+
return '<p class="mm-cue-meta"><b>' + esc(cueTitle(c)) + '</b><span>' + esc(cueWhen(c)) + '</span><span>' + esc(CUE_KIND[c.kind] || c.kind || '') + '</span>' +
|
|
648
|
+
(cueScore(c.gate) ? '<span class="mm-mono">score ' + esc(cueScore(c.gate)) + '</span>' : '') + (extra || '') + '</p>';
|
|
649
|
+
}
|
|
650
|
+
function verdictButtons(id, selected, showKeys, disabled) {
|
|
651
|
+
return '<div class="mm-verdicts" role="group" aria-label="Rate this cue">' + VERDICTS.map(function (v, i) {
|
|
652
|
+
var on = v === selected;
|
|
653
|
+
return '<button type="button" class="cx-btn' + (on ? ' primary' : ' quiet') + '" data-mm="cue-rate" data-id="' + esc(id) + '" data-arg="' + v + '" aria-pressed="' + on + '"' + (disabled ? ' disabled' : '') + '>' +
|
|
654
|
+
(showKeys ? '<span class="mm-key" aria-hidden="true">' + (i + 1) + '</span>' : '') + esc(verdictLabel(v)) + '</button>';
|
|
655
|
+
}).join('') + '</div>';
|
|
656
|
+
}
|
|
657
|
+
function cueStateHtml(state, status) {
|
|
658
|
+
var line = state === 'failed' ? 'The server could not list cues (HTTP ' + status + '). Check again.' : CUE_READ_LINE[state];
|
|
659
|
+
if (!line) return '';
|
|
660
|
+
return '<div class="cx-list"><div class="mm-state"><strong' + (state === 'checking' ? '' : ' class="mm-warn"') + '>' + esc(line) + '</strong>' +
|
|
661
|
+
(state !== 'checking' && state !== 'olderServer' ? '<span class="mm-state-act">' + btn('cue-check', 'Check again') + '</span>' : '') + '</div></div>';
|
|
662
|
+
}
|
|
663
|
+
function cueHeading(title, detail) {
|
|
664
|
+
var c = S.cue.counts;
|
|
665
|
+
return '<div class="mm-cue-head"><div><h2 class="mm-cue-title">' + esc(title) + '</h2><p class="mm-muted">' + detail + '</p>' +
|
|
666
|
+
(c ? '<p class="mm-muted">Ratings in the last 7 days: ' + fmt(c.rated) + ' of ' + fmt(c.total) + '.</p>' : '') + '</div>' +
|
|
667
|
+
'<button type="button" class="cx-btn quiet mm-icon-btn" data-mm="cue-check" aria-label="Check again" title="Check again">' + icon('refresh') + '</button></div>';
|
|
668
|
+
}
|
|
669
|
+
function cueQueueHtml() {
|
|
670
|
+
var C = S.cue, head = cueHeading('Cues to rate', 'Oldest first.<span class="mm-keys-hint"> Keys 1 to 5 rate, N adds a note, S skips.</span>');
|
|
671
|
+
if (C.weekState !== 'loaded') return head + cueStateHtml(C.weekState, C.failedStatus);
|
|
672
|
+
var cue = C.queue[0];
|
|
673
|
+
if (!cue) return head + '<div class="cx-list"><div class="mm-state"><strong>' + esc(C.counts && C.counts.total ? 'Every cue from the last 7 days is rated.' : 'No cues in the last 7 days.') + '</strong></div></div>';
|
|
674
|
+
var id = cue.cueId, saving = !!C.saving[id], left = Math.max(C.queue.length, C.counts ? C.counts.unrated : 0), next = C.queue[1];
|
|
675
|
+
var vote = cue.rating && cue.rating.vote === 'up' ? 'Voted up on the glasses' : cue.rating && cue.rating.vote === 'down' ? 'Voted down on the glasses' : '';
|
|
676
|
+
return head + '<section class="cx-card mm-cue" id="mm-cue-card" tabindex="0" data-cue="' + esc(id) + '" aria-label="Cue to rate. Keys 1 to 5 rate, N adds a note, S skips.">' +
|
|
677
|
+
cueMetaHtml(cue, '<span class="mm-cue-left">' + (left === 1 ? '1 left' : fmt(left) + ' left') + '</span>') +
|
|
678
|
+
'<p class="mm-cue-text">' + esc(cue.detail) + '</p>' + cueSourcesHtml(cue) +
|
|
679
|
+
(cue.general && cue.general.query ? '<p class="mm-muted">Searched the web for: ' + esc(cue.general.query) + '</p>' : '') +
|
|
680
|
+
(cue.before ? '<div class="mm-cue-before"><span class="mm-muted">What was said just before</span><p>' + esc(cue.before) + '</p></div>' : '') +
|
|
681
|
+
(vote ? '<p class="mm-muted">' + vote + '</p>' : '') + verdictButtons(id, null, true, saving) +
|
|
682
|
+
'<label class="mm-pick mm-block"><span>Note (optional)</span><input class="field" id="mm-cue-note" data-cue="' + esc(id) + '" maxlength="' + CUE_NOTE_MAX + '" autocomplete="off" placeholder="What would have made it useful?" value="' + esc(C.notes[id] || '') + '"></label>' +
|
|
683
|
+
'<div class="mm-actions">' + btn('cue-skip', 'Skip', { id: id, disabled: saving }) + (saving ? '<span class="mm-muted">Saving…</span>' : '') +
|
|
684
|
+
(C.errors[id] ? '<span class="mm-warn">' + esc(C.errors[id]) + '</span>' : '') + '</div></section>' +
|
|
685
|
+
(next ? '<p class="mm-muted mm-cue-next">Next up: ' + esc(cueClock(cueNum(next.createdAt))) + ' · ' + esc(clip(next.headline || next.detail, 80)) + '</p>' : '');
|
|
686
|
+
}
|
|
687
|
+
function cueRatedHtml() {
|
|
688
|
+
var C = S.cue, head = cueHeading('Rated cues', 'Newest first. Pick another verdict to change one.');
|
|
689
|
+
if (C.ratedState !== 'loaded') return head + cueStateHtml(C.ratedState, C.ratedStatus);
|
|
690
|
+
if (!C.rated.length) return head + '<div class="cx-list"><div class="mm-state"><strong>No rated cues in the last 7 days.</strong></div></div>';
|
|
691
|
+
return head + C.rated.map(function (c) {
|
|
692
|
+
var id = c.cueId, saving = !!C.saving[id], r = c.rating || {};
|
|
693
|
+
return '<article class="cx-card mm-cue-row">' + cueMetaHtml(c) + '<p class="mm-cue-text small">' + esc(c.detail) + '</p>' +
|
|
694
|
+
(r.verdict ? '<p class="mm-cue-rated">Rated ' + esc(verdictLabel(r.verdict)) + (r.note ? ' · “' + esc(r.note) + '”' : '') + '</p>' : '') +
|
|
695
|
+
'<div class="mm-actions">' + verdictButtons(id, r.verdict, false, saving) +
|
|
696
|
+
'<button type="button" class="mm-link" data-mm="cue-clear" data-id="' + esc(id) + '" title="Back to Cues to rate"' + (saving ? ' disabled' : '') + '>Clear</button></div>' +
|
|
697
|
+
(C.errors[id] ? '<p class="mm-warn">' + esc(C.errors[id]) + '</p>' : '') + '</article>';
|
|
698
|
+
}).join('');
|
|
699
|
+
}
|
|
700
|
+
function cueAct(name, id, arg) {
|
|
701
|
+
var C = S.cue, cue = findCue(id);
|
|
702
|
+
if (name === 'cue-check') return S.view === 'rated' ? loadCueRated() : loadCueWeek();
|
|
703
|
+
if (name === 'cue-skip') return skipCue(id);
|
|
704
|
+
if (name === 'cue-clear') return rateCue(id, null);
|
|
705
|
+
if (name === 'cue-memory') { S.view = 'memories'; persist(); if (!S.mem.loaded && !S.mem.loading) loadMem(false); return W.go('memories', ['m', id]); }
|
|
706
|
+
if (name === 'cue-rate') {
|
|
707
|
+
if (!cue) return;
|
|
708
|
+
// A rated row keeps its note when its verdict changes; picking the verdict it already has changes nothing.
|
|
709
|
+
if (cueVerdict(cue)) { if (arg === cueVerdict(cue)) return; return rateCue(id, arg, cue.rating && cue.rating.note); }
|
|
710
|
+
return rateCue(id, arg, C.notes[id]);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ── chrome: hero, views, counts, footer ──────────────────
|
|
715
|
+
function heroHtml() {
|
|
716
|
+
return '<header class="cx-hero mm-hero" data-tint="memories"><span class="cx-glyph">' + icon('memories') + '</span>' +
|
|
717
|
+
'<div class="cx-hero-text"><h1 class="cx-title">Memories</h1><p class="cx-detail-line">Follow what COS learns, how skills change, and whether it helps.</p></div>' +
|
|
718
|
+
'<div class="cx-actions">' + btn('settings', 'Memory settings') +
|
|
719
|
+
'<button type="button" class="cx-btn quiet mm-icon-btn" data-mm="refresh" aria-label="Refresh memories" title="Refresh memories">' + icon('refresh') + '</button></div></header>';
|
|
720
|
+
}
|
|
721
|
+
function viewsHtml() {
|
|
722
|
+
var n = needsCount();
|
|
723
|
+
// The two cue views show once the server's cue history has answered (a 404, a server without it, hides them).
|
|
724
|
+
var cueOn = S.cue.available === true || (S.cue.available === null && (S.view === 'cues' || S.view === 'rated'));
|
|
725
|
+
var unrated = S.cue.counts ? S.cue.counts.unrated : null;
|
|
726
|
+
return '<div class="cx-switch mm-tabs" role="group" aria-label="Memories view">' + VIEWS.concat(cueOn ? CUE_VIEWS : []).map(function (v) {
|
|
727
|
+
var label = v[0] === 'review' && n ? 'To review (' + fmt(n) + ')' : v[0] === 'cues' && unrated ? 'Cues to rate (' + fmt(unrated) + ')' : v[1];
|
|
728
|
+
return '<button type="button" data-mm="view" data-arg="' + v[0] + '" aria-pressed="' + (S.view === v[0]) + '">' + esc(label) + '</button>';
|
|
729
|
+
}).join('') + '</div>';
|
|
730
|
+
}
|
|
731
|
+
function countsHtml() {
|
|
732
|
+
var n = needsCount(), parts = [];
|
|
733
|
+
var checked = S.recent.rows.filter(function (e) { return outcomeText(e); }).length;
|
|
734
|
+
if (S.statusError && !S.status) parts.push('<span class="mm-bad">' + esc('Could not read this Mac\'s status. ' + S.statusError) + '</span>');
|
|
735
|
+
if (n) parts.push('<span><b>' + fmt(n) + '</b> ' + plural(n, 'change', 'changes') + ' to review</span>');
|
|
736
|
+
if (S.recent.total != null) parts.push('<span><b>' + fmt(S.recent.total) + '</b> ' + plural(S.recent.total, 'event', 'events') + ' in 90 days</span>');
|
|
737
|
+
else if (S.recent.loading) parts.push('<span>Counting events…</span>');
|
|
738
|
+
else if (S.recent.error) parts.push('<span class="mm-warn">Events in 90 days could not be counted</span>');
|
|
739
|
+
if (S.recent.loaded) parts.push('<span><b>' + fmt(checked) + '</b> ' + plural(checked, 'result', 'results') + ' checked on this page</span>');
|
|
740
|
+
parts.push('<span>Capture status in ' + textBtn('settings', 'Memory settings') + '</span>');
|
|
741
|
+
parts.push('<span>' + textBtn('log', 'View activity log') + '<span class="mm-phone-only"><span class="mm-dot" aria-hidden="true"> · </span>' + textBtn('refresh', 'Refresh') + '</span></span>');
|
|
742
|
+
return parts.join('<span class="mm-dot" aria-hidden="true">·</span>');
|
|
743
|
+
}
|
|
744
|
+
function footHtml() {
|
|
745
|
+
var s = S.status;
|
|
746
|
+
if (!s) return 'Stored on this Mac';
|
|
747
|
+
return 'Stored on this Mac · ' + (bridgeOn() ? 'Advanced search connected' : 'Plain files') + (s.installedVersion ? ' · Server ' + esc(s.installedVersion) : '');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── lists ────────────────────────────────────────────────
|
|
751
|
+
function rowHtml(e, selected, act) {
|
|
752
|
+
var st = rowStatus(e), what = rowWhat(e);
|
|
753
|
+
var facts = [kindLabel(e), storeName(e.store), known(e.scope), when(e.ts)].filter(Boolean);
|
|
754
|
+
return '<button type="button" class="mm-row" data-mm="' + (act || 'open-e') + '" data-id="' + esc(e.event_id) + '" aria-selected="' + (selected ? 'true' : 'false') + '">' +
|
|
755
|
+
'<span class="mm-row-body"><span class="mm-row-title">' + esc(clip(rowTitle(e), 220)) + '</span>' +
|
|
756
|
+
(what ? '<span class="mm-row-what">' + esc(what) + '</span>' : '') +
|
|
757
|
+
'<span class="mm-facts"><span class="mm-kind">' + esc(facts[0]) + '</span>' + (facts.length > 1 ? ' · ' + esc(facts.slice(1).join(' · ')) : '') + '</span>' +
|
|
758
|
+
'<span class="mm-status' + (st.tone ? ' ' + st.tone : '') + '">' + esc(clip(st.text, 200)) + '</span></span><span class="chev"></span></button>';
|
|
759
|
+
}
|
|
760
|
+
function memRowHtml(m, selected) {
|
|
761
|
+
var facts = [m.type ? String(m.type).replace(/_/g, ' ') : 'Memory', day(m.created_at)].filter(Boolean);
|
|
762
|
+
return '<button type="button" class="mm-row" data-mm="open-m" data-id="' + esc(m.id) + '" aria-selected="' + (selected ? 'true' : 'false') + '">' +
|
|
763
|
+
'<span class="mm-row-body"><span class="mm-row-title">' + esc(clip(m.summary || m.content || '(untitled)', 220)) + '</span>' +
|
|
764
|
+
'<span class="mm-facts"><span class="mm-kind">' + esc(facts[0]) + '</span>' + (facts[1] ? ' · ' + esc(facts[1]) : '') + '</span></span><span class="chev"></span></button>';
|
|
765
|
+
}
|
|
766
|
+
function appliedRowHtml(e, selected) {
|
|
767
|
+
var lesson = memText(e.lesson_id) || e.lesson_id || 'Lesson';
|
|
768
|
+
var facts = [known(e.engine), when(e.ts)].filter(Boolean);
|
|
769
|
+
var ex = appliedExcerpt(e);
|
|
770
|
+
return '<button type="button" class="mm-row" data-mm="open-e" data-id="' + esc(e.event_id) + '" aria-selected="' + (selected ? 'true' : 'false') + '">' +
|
|
771
|
+
'<span class="mm-row-body"><span class="mm-row-title">' + esc(clip(lesson, 220)) + '</span>' +
|
|
772
|
+
'<span class="mm-facts"><span class="mm-kind">Used in an answer</span>' + (facts.length ? ' · ' + esc(facts.join(' · ')) : '') + '</span>' +
|
|
773
|
+
'<span class="mm-status ok mm-clamp">' + esc(clip(ex, 200) || 'A later answer used this lesson') + '</span></span><span class="chev"></span></button>';
|
|
774
|
+
}
|
|
775
|
+
function appliedExcerpt(e) { return ((e.source_refs || [])[0] || {}).excerpt || (e.detail && e.detail.excerpts && e.detail.excerpts[0]) || ''; }
|
|
776
|
+
function groupHead(text, side) { return '<h2 class="group-title mm-group"><span>' + text + '</span>' + (side ? '<span>' + side + '</span>' : '') + '</h2>'; }
|
|
777
|
+
function stateBox(title, body, retry) {
|
|
778
|
+
return '<div class="cx-list"><div class="mm-state"><strong>' + esc(title) + '</strong>' + (body ? '<span>' + esc(body) + '</span>' : '') +
|
|
779
|
+
(retry ? '<span class="mm-state-act">' + btn(retry, 'Try again') + '</span>' : '') + '</div></div>';
|
|
780
|
+
}
|
|
781
|
+
function loadingBox(what) { return '<div class="cx-list" aria-busy="true"><div class="mm-state"><strong>' + esc(what) + '</strong><span>Your Mac reads this from its memory store. It can take a few seconds.</span></div>' + W.skeletonRows(3).replace(/^<div class="list">|<\/div>$/g, '') + '</div>'; }
|
|
782
|
+
function moreBtn(act, label, busy) { return '<div class="mm-more">' + btn(act, busy ? 'Loading…' : label, { disabled: busy }) + '</div>'; }
|
|
783
|
+
function coverageNote(cov) {
|
|
784
|
+
var c = cov || {}, gaps = Object.keys(c).filter(function (k) { return c[k] && c[k].state && c[k].state !== 'ok' && c[k].state !== 'legacy'; });
|
|
785
|
+
if (!gaps.length) return '';
|
|
786
|
+
return '<p class="cx-note">Not recorded on this Mac yet: ' + esc(gaps.map(function (k) { return storeName(k); }).join(', ')) + '.</p>';
|
|
787
|
+
}
|
|
788
|
+
function selectedId() { var a = readerArgs(W.parseRoute()); return a.length === 2 ? a[1] : (autoPick() || ''); }
|
|
789
|
+
|
|
790
|
+
function listHtml() {
|
|
791
|
+
if (S.view === 'cues') return cueQueueHtml();
|
|
792
|
+
if (S.view === 'rated') return cueRatedHtml();
|
|
793
|
+
if (S.view === 'knowledge') return knowledgeListHtml();
|
|
794
|
+
if (S.view === 'memories') return memListHtml();
|
|
795
|
+
if (S.view === 'applied') return appliedListHtml();
|
|
796
|
+
if (S.view === 'review') return reviewListHtml();
|
|
797
|
+
return recentListHtml();
|
|
798
|
+
}
|
|
799
|
+
function recentListHtml() {
|
|
800
|
+
var L = S.recent, sel = selectedId();
|
|
801
|
+
if (!L.loaded && L.loading) return groupHead('Recent learning', '90 days') + loadingBox('Reading what COS learned in the last 90 days');
|
|
802
|
+
if (L.error && !L.rows.length) return groupHead('Recent learning', '90 days') + stateBox('Recent learning did not load.', L.error, 'refresh');
|
|
803
|
+
if (!L.rows.length) return groupHead('Recent learning', '90 days') + stateBox('Nothing learned in the last 90 days.', 'Lessons, corrections and recalls show here as COS records them.');
|
|
804
|
+
return groupHead('Recent learning', L.total != null ? fmt(L.total) + ' in 90 days' : '90 days') +
|
|
805
|
+
'<div class="cx-list">' + L.rows.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>' +
|
|
806
|
+
(L.cursor ? moreBtn('more-recent', 'Load more', L.loading) : '') + coverageNote(L.coverage);
|
|
807
|
+
}
|
|
808
|
+
function reviewListHtml() {
|
|
809
|
+
var L = S.review, sel = selectedId(), needs = needsRows(), seen = {};
|
|
810
|
+
needs.forEach(function (e) { seen[e.event_id] = 1; });
|
|
811
|
+
var rest = L.rows.filter(function (e) { return !seen[e.event_id]; });
|
|
812
|
+
var other = rest.filter(function (e) { return !isPruneReady(e); }), ready = rest.filter(isPruneReady);
|
|
813
|
+
var out = '';
|
|
814
|
+
var openNeeds = needsCount();
|
|
815
|
+
if (needs.length) out += groupHead('Needs you now', openNeeds ? fmt(openNeeds) + ' open' : 'All decided') + '<div class="cx-list">' + needs.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
816
|
+
if (!L.loaded && L.loading) return out + groupHead('Proposed changes') + loadingBox('Reading the changes waiting for you');
|
|
817
|
+
if (L.error && !L.rows.length) return out + groupHead('Proposed changes') + stateBox('The review list did not load.', L.error, 'refresh');
|
|
818
|
+
if (!needs.length && !rest.length) return groupHead('To review') + stateBox('You are up to date.', 'No proposed changes are waiting for review.');
|
|
819
|
+
if (other.length) out += groupHead('Proposed changes', L.total != null ? fmt(L.total) + ' in all' : '') + '<div class="cx-list">' + other.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
820
|
+
if (ready.length) {
|
|
821
|
+
var open = ready.filter(function (e) { return !decisionFor(e); }).length, b = S.bulk;
|
|
822
|
+
out += groupHead('Ready to quarantine', open ? fmt(open) + ' open' : 'All decided') +
|
|
823
|
+
'<p class="mm-group-note">Saved memories that are old, unused and rarely recalled. Quarantine takes them out of recall and keeps them; Restore brings one back.</p>' +
|
|
824
|
+
(b ? '<p class="mm-group-note" aria-live="polite">Quarantining ' + fmt(b.done) + ' of ' + fmt(b.total) + '…</p>' :
|
|
825
|
+
open ? '<div class="mm-more mm-more-top">' + btn('prune-all', S.armed === 'prune-all' ? 'Tap again to quarantine ' + fmt(open) : 'Quarantine all ' + fmt(open)) + '</div>' : '') +
|
|
826
|
+
'<div class="cx-list">' + ready.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
827
|
+
}
|
|
828
|
+
if (L.cursor) out += moreBtn('more-review', 'Load more to review', L.loading);
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
function memSearchHtml() {
|
|
832
|
+
return '<form class="cx-search mm-search" data-mm-form="mem-search" role="search"><input class="field" type="search" id="mm-mem-q" placeholder="Search topics, ideas…" value="' + esc(S.mem.q) + '" autocomplete="off" enterkeyhint="search" aria-label="Search memories">' +
|
|
833
|
+
(S.mem.q ? '<button type="button" class="cx-clear" data-mm="mem-clear">Clear</button>' : '') + '</form>';
|
|
834
|
+
}
|
|
835
|
+
function memListHtml() {
|
|
836
|
+
var L = S.mem, q = L.q.trim(), rows = L.hits != null ? L.hits : L.rows, sel = selectedId();
|
|
837
|
+
var head = groupHead(q ? 'Memories matching “' + esc(q) + '”' : 'Saved memories', !q && L.total != null ? fmt(L.total) + ' stored' : '');
|
|
838
|
+
if (!L.loaded && L.loading) return head + loadingBox(q ? 'Searching your memories' : 'Reading your saved memories');
|
|
839
|
+
if (L.error && !rows.length) return head + stateBox(q ? 'That search did not finish.' : 'Your memories did not load.', L.error, 'mem-retry');
|
|
840
|
+
if (!rows.length) return head + stateBox(q ? 'No memories match that search.' : 'No memories yet.', q ? 'Try fewer or different words.' : 'COS saves memories as you work. Plain notes in the memory folder show here too.');
|
|
841
|
+
var cursor = q ? L.hitCursor : L.cursor;
|
|
842
|
+
return head + '<div class="cx-list">' + rows.map(function (m) { return memRowHtml(m, m.id === sel); }).join('') + '</div>' +
|
|
843
|
+
(cursor ? moreBtn('more-mem', 'Load more memories', L.loading) : '') + (L.paged === false ? '<p class="cx-note">This server shows the newest memories only.</p>' : '');
|
|
844
|
+
}
|
|
845
|
+
function appliedListHtml() {
|
|
846
|
+
var L = S.applied, sel = selectedId();
|
|
847
|
+
var toggle = textBtn('applied-days', L.days === 90 ? 'Show this week' : 'Show 90 days', { arg: L.days === 90 ? 7 : 90 });
|
|
848
|
+
var head = groupHead((L.days === 90 ? 'Applied in 90 days' : 'Applied this week') + (L.total != null ? ' · ' + fmt(L.total) : ''), toggle);
|
|
849
|
+
if (!L.loaded) return head + loadingBox('Reading which lessons later answers used');
|
|
850
|
+
if (!L.rows.length) { var c = appliedEmpty(); return head + stateBox(c.h, c.p, L.error ? 'applied-retry' : ''); }
|
|
851
|
+
return head + '<div class="cx-list">' + L.rows.map(function (e) { return appliedRowHtml(e, e.event_id === sel); }).join('') + '</div>' +
|
|
852
|
+
(L.cursor ? moreBtn('more-applied', 'Load more', L.loading) : '') + coverageNote(L.coverage);
|
|
853
|
+
}
|
|
854
|
+
// Empty copy by memory path: the bridge can detect use and saw none; the other paths cannot detect it at all.
|
|
855
|
+
function appliedEmpty() {
|
|
856
|
+
var L = S.applied, cov = (L.coverage && L.coverage.used) || (S.recent.coverage && S.recent.coverage.used) || {};
|
|
857
|
+
if (L.error) return { h: 'Applied is not available right now.', p: L.error };
|
|
858
|
+
if (!S.status) return S.statusError ? { h: 'Could not read this Mac\'s status.', p: 'Applied says nothing about the memory path until your Mac answers. ' + S.statusError }
|
|
859
|
+
: { h: 'Checking how this Mac stores memory…', p: 'Applied says nothing about use until your Mac has answered.' };
|
|
860
|
+
var path = memoryPath();
|
|
861
|
+
if (path === 'files') return { h: 'This memory setup cannot detect use.', p: 'Applied stays empty until advanced search is connected. Plain-file memories keep working.' };
|
|
862
|
+
if (path === 'knowledge') return { h: 'Knowledge citations are not lesson use.', p: 'Applied is for saved lessons. Connect advanced search to see when a later answer uses one.' };
|
|
863
|
+
if (cov.state === 'unavailable') return { h: 'The use record could not be read on this Mac.', p: 'Nothing is made up here. Check the record on your Mac and refresh.' };
|
|
864
|
+
if (cov.state && cov.state !== 'ok' && cov.state !== 'legacy') return { h: 'Use has not been recorded on this Mac yet.', p: 'Nothing here is a measured zero.' };
|
|
865
|
+
return { h: 'No later answer has used a lesson ' + (L.days === 90 ? 'in 90 days.' : 'this week.'), p: 'Being recalled is not the same as being used. A lesson shows here when a later answer visibly applied it.' };
|
|
866
|
+
}
|
|
867
|
+
// On a wide screen with nothing chosen, the reader shows the first row (Control's page does the same).
|
|
868
|
+
function autoPick() {
|
|
869
|
+
if (!isWide() || S.view === 'cues' || S.view === 'rated') return '';
|
|
870
|
+
if (S.view === 'knowledge') return S.ktab;
|
|
871
|
+
if (S.view === 'memories') { var m = S.mem.hits != null ? S.mem.hits : S.mem.rows; return m[0] ? m[0].id : ''; }
|
|
872
|
+
var rows = S.view === 'applied' ? S.applied.rows : S.view === 'review' ? needsRows().concat(S.review.rows) : S.recent.rows;
|
|
873
|
+
return rows[0] ? rows[0].event_id : '';
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ── the reader: a lesson's timeline ──────────────────────
|
|
877
|
+
function step(title, meta, body, last) {
|
|
878
|
+
return '<section class="mm-step' + (last ? ' last' : '') + '"><div class="mm-step-head"><h3>' + title + '</h3>' + (meta ? '<span class="mm-step-meta">' + meta + '</span>' : '') + '</div><div class="mm-step-body">' + body + '</div></section>';
|
|
879
|
+
}
|
|
880
|
+
function quote(text) { return '<blockquote class="mm-quote">' + esc(text) + '</blockquote>'; }
|
|
881
|
+
function readerHead(title, facts, actions) {
|
|
882
|
+
return '<div class="mm-reader-head"><h2 class="mm-title">' + esc(title) + '</h2>' + (facts ? '<p class="mm-sub">' + facts + '</p>' : '') +
|
|
883
|
+
(actions ? '<div class="mm-actions">' + actions + '</div>' : '') + '</div>';
|
|
884
|
+
}
|
|
885
|
+
function eventReader(id) {
|
|
886
|
+
var base = findRow(id);
|
|
887
|
+
if (!base) {
|
|
888
|
+
var loading = !S.recent.loaded || !S.review.loaded || S.loading[id];
|
|
889
|
+
if (loading) return readerState('Opening this item…', 'Your Mac is reading it.');
|
|
890
|
+
if (!S.detail[id]) { S.detail[id] = null; ensureDetail({ event_id: id }); return readerState('Opening this item…', 'Your Mac is reading it.'); }
|
|
891
|
+
return readerState('This item is not in the list any more.', 'It may have been reviewed already.', true);
|
|
892
|
+
}
|
|
893
|
+
if (S.view === 'applied' && base.event_type === 'used' && S.applied.rows.indexOf(base) >= 0) return appliedReader(base);
|
|
894
|
+
ensureDetail(base);
|
|
895
|
+
if (isMemoryLesson(base)) ensureMem(base.lesson_id);
|
|
896
|
+
var full = S.detail[base.event_id], e = fullRow(base);
|
|
897
|
+
crumb(clip(rowTitle(e), 60));
|
|
898
|
+
var o = outcomeText(e);
|
|
899
|
+
var facts = ['<span class="mm-kind">' + esc(o ? 'Checked: ' + o : kindLabel(e)) + '</span>', esc(storeName(e.store)), esc(known(e.scope)), esc(known(e.engine) !== known(e.scope) ? known(e.engine) : ''), esc(when(e.ts))].filter(function (x) { return x && x !== '<span class="mm-kind"></span>'; }).join(' · ');
|
|
900
|
+
var actions = btn('open-sources', 'Open sources', { id: e.event_id }) + btn('explore', 'Explore in graph', { id: e.event_id }) + btn('copy-open', 'Copy context', { id: e.event_id, cls: 'primary' });
|
|
901
|
+
var note = '';
|
|
902
|
+
if (e.needs) note += '<div class="cx-notice warn">' + esc((GATE[e.needs.gate] || 'Needs you') + (e.needs.reason ? ': ' + e.needs.reason : '') + '.') + '</div>';
|
|
903
|
+
if (full && full._error && !(e.store === 'bot_memory')) note += '<div class="cx-notice">' + esc(full._error) + '</div>';
|
|
904
|
+
else if (!full && !(e.store === 'bot_memory' && e.lesson_id) && S.loading[e.event_id]) note += '<p class="cx-note">Reading the full record…</p>';
|
|
905
|
+
if (e.detail && e.detail.truncated) note += '<div class="cx-notice">The record was shortened to fit. Your Mac holds more than shown here.</div>';
|
|
906
|
+
return readerHead(clip(rowTitle(e), 140), facts, actions) + note +
|
|
907
|
+
'<div class="mm-timeline">' + sourceStep(e) + changeStep(e) + useSteps(e) + evidenceStep(e) + '</div>';
|
|
908
|
+
}
|
|
909
|
+
function sourceStep(e) {
|
|
910
|
+
var ref = (e.source_refs || [])[0] || {}, d = e.detail || {};
|
|
911
|
+
var lessonText = isMemoryLesson(e) ? (S.memDetail[e.lesson_id] && !S.memDetail[e.lesson_id]._error ? str(S.memDetail[e.lesson_id].content) || str(S.memDetail[e.lesson_id].summary) : '') : '';
|
|
912
|
+
var q = ref.excerpt || d.content || (d.bodies || [])[0] || lessonText || '';
|
|
913
|
+
var title = e.store === 'correction_journal' ? 'You corrected COS' : e.store === 'self_improvement_queue' ? 'A task suggested a change' :
|
|
914
|
+
e.store === 'bot_memory' ? 'COS saved this' : e.store === 'eval_scores' || e.store === 'reflect_log' ? 'A run was checked' : isMemoryLesson(e) ? 'The memory' : 'Captured';
|
|
915
|
+
var body = q ? quote(q) : (isMemoryLesson(e) && S.loading['m:' + e.lesson_id] ? '<p class="cx-note">Reading the memory…</p>' :
|
|
916
|
+
isMemoryLesson(e) && memError(e.lesson_id) ? '<p class="mm-muted">The memory could not be read: ' + esc(memError(e.lesson_id)) + '</p>' : '<p class="mm-muted">No excerpt was stored with this record.</p>');
|
|
917
|
+
if (isMemoryLesson(e)) body += '<p>' + textBtn('open-m', 'Open the memory', { id: e.lesson_id }) + '</p>';
|
|
918
|
+
else if (ref.id) body += '<p class="mm-muted">' + esc(storeName(ref.kind)) + ' · ' + esc(ref.id) + '</p>';
|
|
919
|
+
return step(esc(title), esc(when(e.ts)), body + memoryActions(e));
|
|
920
|
+
}
|
|
921
|
+
// Accept keeps a memory in recall; Quarantine (prune on the wire) takes it out and keeps the content; Restore brings it
|
|
922
|
+
// back. Forget is separate and explicit. Each needs a second tap where it removes something.
|
|
923
|
+
function memoryActions(e) {
|
|
924
|
+
var busy = S.reviewBusy === e.lesson_id;
|
|
925
|
+
if (e.governance) {
|
|
926
|
+
var acts = [['accept', 'Accept for recall'], ['quarantine', 'Quarantine'], ['restore', 'Restore to recall'], ['forget', 'Forget permanently']].filter(function (p) {
|
|
927
|
+
return !(p[0] === 'accept' && e.governance !== 'candidate' || p[0] === 'restore' && e.governance !== 'quarantined' || p[0] === 'quarantine' && e.governance === 'quarantined');
|
|
928
|
+
}).map(function (p) { var armed = p[0] === 'forget' && S.armed === 'forget:' + e.lesson_id; return btn('governed', armed ? 'Tap again to forget permanently' : p[1], { id: e.lesson_id, arg: p[0], disabled: busy }); }).join('');
|
|
929
|
+
return '<p class="mm-muted">State: ' + esc(e.governance) + '</p><div class="mm-actions">' + acts + '</div>' +
|
|
930
|
+
(S.armed === 'forget:' + e.lesson_id ? '<p class="mm-warn">Forgetting removes the searchable content. Outside sources and protected backups are reported separately.</p>' : '');
|
|
931
|
+
}
|
|
932
|
+
if (e.store !== 'bot_memory' || !e.lesson_id || e.event_type !== 'captured') return '';
|
|
933
|
+
var decision = decisionFor(e);
|
|
934
|
+
if (decision === 'pruned') return '<p class="mm-muted">Quarantined from recall. The content is kept for review.</p><div class="mm-actions">' + btn('memory-review', busy ? 'Working…' : 'Restore to recall', { id: e.lesson_id, arg: 'restore', disabled: busy }) + '</div>';
|
|
935
|
+
var armed = S.armed === 'prune:' + e.lesson_id;
|
|
936
|
+
return (isPruneReady(e) && !decision ? '<p class="mm-muted">Ready to quarantine' + (pruneReason(e) ? ': ' + esc(pruneReason(e)) : '') + '. Accept keeps it in recall and stops it being suggested again; Quarantine takes it out of recall; Restore brings it back.</p>' : '') +
|
|
937
|
+
'<div class="mm-actions">' + (decision === 'accepted' ? '<span class="cx-cap ok">Accepted</span>' : btn('memory-review', busy ? 'Working…' : 'Accept', { id: e.lesson_id, arg: 'accept', disabled: busy, cls: 'primary' })) +
|
|
938
|
+
btn('memory-review', armed ? 'Tap again to quarantine' : 'Quarantine', { id: e.lesson_id, arg: 'prune', disabled: busy }) + '</div>';
|
|
939
|
+
}
|
|
940
|
+
function changeStep(e) {
|
|
941
|
+
var d = e.detail || {}, proposal = isProposal(e), pattern = isPattern(e), body = '';
|
|
942
|
+
var applies = (e.applies_to || []).map(function (a) { return typeof a === 'string' ? a : [a.name || a.path || a.id, a.cadence].filter(Boolean).join(' · '); }).filter(Boolean);
|
|
943
|
+
if (proposal) {
|
|
944
|
+
body += '<p>Suggested by <b>' + esc(d.task || e.category || 'a scheduled task') + '</b>' + (d.layer ? ' · ' + esc(d.layer) : '') + (d.date ? ' · ' + esc(d.date) : '') + '.</p>';
|
|
945
|
+
body += (d.entry || d.content ? quote(d.entry || d.content) : '') + (d.bodies || []).map(function (b) { return '<p>' + esc(b) + '</p>'; }).join('');
|
|
946
|
+
if (d.logged_times) body += '<p>Logged ' + fmt(d.logged_times) + ' ' + plural(d.logged_times, 'time', 'times') + '.</p>';
|
|
947
|
+
} else if (d.before || d.after || d.rule) {
|
|
948
|
+
body += '<div class="mm-change">' + (d.before ? '<div><span class="mm-label">Before</span>' + esc(d.before) + '</div>' : '') + '<div class="after"><span class="mm-label">' + (d.after ? 'After' : 'Rule') + '</span>' + esc(d.after || d.rule || '') + '</div></div>';
|
|
949
|
+
if (d.rule && d.after) body += '<p><b>Rule</b> ' + esc(d.rule) + '</p>';
|
|
950
|
+
} else if (d.content && !(e.store === 'bot_memory')) body += quote(d.content);
|
|
951
|
+
else if (!isMemoryLesson(e)) body += '<p>' + esc(cleanTitle(e.title)) + '</p>';
|
|
952
|
+
else return '';
|
|
953
|
+
if (e.target && e.target.version) body += '<p>Version <b>' + esc(e.target.version) + '</b></p>';
|
|
954
|
+
body += effectBlock(e, applies);
|
|
955
|
+
var decision = decisionFor(e);
|
|
956
|
+
if (proposal || pattern) body += '<div class="mm-actions">' + btn('steward', 'Review rule versions', { cls: 'primary' }) +
|
|
957
|
+
(decision === 'dismissed' ? btn('decide', 'Restore suggestion', { id: e.event_id, arg: 'reopened' }) : btn('decide', 'Dismiss', { id: e.event_id, arg: 'dismissed' })) + '</div>';
|
|
958
|
+
var title = decision === 'dismissed' ? 'Change dismissed' : proposal ? 'Suggested change' : e.event_type === 'promotable' ? 'Suggested rule' : (KIND[e.event_type] || 'Change');
|
|
959
|
+
return step(esc(title), esc(proposal ? 'Instruction' : known(e.engine) || known(e.scope)), body);
|
|
960
|
+
}
|
|
961
|
+
function effectBlock(e, applies) {
|
|
962
|
+
var d = e.detail || {}, o = outcomeText(e), rows = [];
|
|
963
|
+
var repeat = d.occurrences != null ? fmt(d.occurrences) + ' ' + plural(d.occurrences, 'occurrence', 'occurrences') + (d.threshold ? ' (threshold ' + esc(d.threshold) + ')' : '')
|
|
964
|
+
: d.logged_times ? 'logged ' + fmt(d.logged_times) + ' ' + plural(d.logged_times, 'time', 'times') : null;
|
|
965
|
+
if (applies.length) rows.push('<li><b>Where it applies</b>' + esc(applies.join(', ')) + '</li>');
|
|
966
|
+
if (o || d.status) rows.push('<li><b>Checks</b>' + esc(o || d.status) + '</li>');
|
|
967
|
+
if (repeat) rows.push('<li><b>Repeat rate</b>' + esc(repeat) + '</li>');
|
|
968
|
+
return rows.length ? '<div class="mm-effect"><span class="mm-label">Expected effect</span><ul>' + rows.join('') + '</ul></div>' : '';
|
|
969
|
+
}
|
|
970
|
+
function useSteps(e) {
|
|
971
|
+
var id = e.lesson_id, rows = [], seen = {};
|
|
972
|
+
S.recent.rows.concat(S.review.rows).forEach(function (r) { if (r && !seen[r.event_id] && isUseType(r) && id && r.lesson_id === id) { seen[r.event_id] = 1; rows.push(r); } });
|
|
973
|
+
if (isUseType(e) && !seen[e.event_id]) rows.push(e);
|
|
974
|
+
rows.sort(function (a, b) { return String(a.ts || '').localeCompare(String(b.ts || '')); });
|
|
975
|
+
return rows.map(useStep).join('');
|
|
976
|
+
}
|
|
977
|
+
function useStep(r) {
|
|
978
|
+
var o = outcomeText(r), stage = (r.detail && r.detail.stage) || r.event_type, eng = known(r.engine) || (r.outcome && r.outcome.evaluator) || '';
|
|
979
|
+
var times = r.detail && r.detail.count ? ' · ' + fmt(r.detail.count) + ' ' + plural(r.detail.count, 'time', 'times') : '';
|
|
980
|
+
if (o || stage === 'checked') return step('A later result was checked', esc((r.outcome && r.outcome.evaluator) || when(r.ts)), '<p class="mm-check">' + esc(o || cleanTitle(r.title) || 'Checked') + '</p><p class="mm-muted">One recorded check. Evidence for that run, not a promise about every later answer.</p>');
|
|
981
|
+
if (r.event_type === 'used' || stage === 'applied') {
|
|
982
|
+
var ex = appliedExcerpt(r);
|
|
983
|
+
return step('Used in a later session', esc(when(r.ts)), (ex ? quote(ex) : '') + '<p>' + esc(cleanTitle(r.title)) + (eng ? ' by ' + esc(eng) : '') + times + '.</p>' + rankingSentence());
|
|
984
|
+
}
|
|
985
|
+
return step(stage === 'included' ? 'Included in a later session' : 'Recalled in a later session', esc(when(r.ts)), '<p>' + esc(cleanTitle(r.title)) + (eng ? ' by ' + esc(eng) : '') + times + '.</p><p class="mm-muted">Being recalled is not the same as being used.</p>');
|
|
986
|
+
}
|
|
987
|
+
function evidenceStep(e) {
|
|
988
|
+
var n = (e.source_refs || []).length;
|
|
989
|
+
return step('Evidence for this lesson', 'Knowledge', '<p>' + fmt(n) + ' source ' + plural(n, 'record', 'records') + '. Sources are what was actually said. Graph links are extracted context, not proof that the lesson worked.</p>' +
|
|
990
|
+
(e.provenance && e.provenance !== 'complete' ? '<p class="mm-muted">Provenance: ' + esc(e.provenance) + '.</p>' : ''), true);
|
|
991
|
+
}
|
|
992
|
+
function appliedReader(e) {
|
|
993
|
+
ensureMem(e.lesson_id);
|
|
994
|
+
var lesson = memText(e.lesson_id) ? (S.memDetail[e.lesson_id].content || S.memDetail[e.lesson_id].summary) : '', err = memError(e.lesson_id), ex = appliedExcerpt(e), d = e.detail || {};
|
|
995
|
+
var n = appliedCount(e);
|
|
996
|
+
crumb(clip(lesson || 'Lesson', 60));
|
|
997
|
+
var facts = ['<span class="mm-kind">Used in an answer</span>', esc(known(e.engine)), d.sessions ? fmt(d.sessions) + ' ' + plural(d.sessions, 'session', 'sessions') : '', esc(when(e.ts))].filter(Boolean).join(' · ');
|
|
998
|
+
var lessonBody = lesson ? quote(lesson) : err ? '<div class="cx-notice">This lesson\'s record could not be read (' + esc(err) + '). The use below is still recorded.</div>' : '<p class="cx-note">Reading the record…</p>';
|
|
999
|
+
return readerHead(lesson ? clip(lesson, 200) : 'Lesson', facts, e.lesson_id ? btn('open-m', 'Open the memory', { id: e.lesson_id }) : '') +
|
|
1000
|
+
'<div class="mm-timeline">' + step('The lesson', 'Saved memory', lessonBody) +
|
|
1001
|
+
step('A later answer used it', esc(when(e.ts)), (ex ? quote(ex) : '') + '<p>' + esc(n != null ? 'Recorded ' + fmt(n) + ' ' + plural(n, 'time', 'times') + ' since first use' : 'Recorded') + ', from the assistant\'s own words. Evidence for that answer, not a promise about every later one.</p>', !rewardOn()) +
|
|
1002
|
+
(rewardOn() ? step('Ranking', 'Reward on', rankingSentence(), true) : '') + '</div>';
|
|
1003
|
+
}
|
|
1004
|
+
function memoryReader(id) {
|
|
1005
|
+
var rows = S.mem.hits != null ? S.mem.hits.concat(S.mem.rows) : S.mem.rows;
|
|
1006
|
+
var m = rows.filter(function (x) { return x.id === id; })[0] || { id: id };
|
|
1007
|
+
ensureMem(id);
|
|
1008
|
+
var full = S.memDetail[id] || null, r = Object.assign({}, m, full && !full._error ? full : {});
|
|
1009
|
+
var text = r.content || r.summary || '';
|
|
1010
|
+
if (!text && (!full || S.loading['m:' + id])) return readerState('Opening this memory…', 'Your Mac is reading it.');
|
|
1011
|
+
crumb(clip(r.summary || 'Memory', 60));
|
|
1012
|
+
var facts = ['<span class="mm-kind">Saved memory</span>', esc(r.type ? String(r.type).replace(/_/g, ' ') : ''), esc(day(r.created_at)), esc(r.source || '')].filter(Boolean).join(' · ');
|
|
1013
|
+
return readerHead(clip(r.summary || text || 'Memory', 200), facts, btn('copy-memory', 'Copy context', { id: id, cls: 'primary' })) +
|
|
1014
|
+
(full && full._error ? '<div class="cx-notice">' + esc(full._error) + '</div>' : '') +
|
|
1015
|
+
'<div class="mm-timeline">' + step('Captured memory', esc(r.type ? String(r.type).replace(/_/g, ' ') : 'Memory'), text ? quote(text) : '<p class="mm-muted">This memory has no text.</p>') +
|
|
1016
|
+
step('Available for recall', r.filePath ? 'Plain files' : 'Memory store', '<p>Active memories are available to signed-in sessions. Later recall, use or results show under Recent learning when they are recorded.</p>', true) + '</div>';
|
|
1017
|
+
}
|
|
1018
|
+
function readerState(title, body, back) {
|
|
1019
|
+
return '<div class="mm-reader-state"><strong>' + esc(title) + '</strong>' + (body ? '<span>' + esc(body) + '</span>' : '') + (back ? '<span class="mm-state-act">' + btn('back', 'Back to the list') + '</span>' : '') + '</div>';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ── Knowledge ────────────────────────────────────────────
|
|
1023
|
+
function setupDone() {
|
|
1024
|
+
var s = S.setup; if (!s || fileGraph()) return null;
|
|
1025
|
+
var checks = s.checks || [], ok = checks.length > 0 && checks.every(function (c) { return c.ok; });
|
|
1026
|
+
var on = (s.sources || []).some(function (r) { return r.enabled && r.exists && (r.files || 0) > 0; });
|
|
1027
|
+
var b = s.budget || {}, budgetOk = b.used != null && b.cap != null && b.used < b.cap, graphN = (s.graph || {}).entities;
|
|
1028
|
+
var sample = (s.sample || {}).indexed >= 1 || (graphN != null && graphN > 0);
|
|
1029
|
+
return [ok, on, (s.owner || {}).is_owner === true, ok && budgetOk, sample, !!S.ask.answer, (s.schedule || {}).installed === true].filter(Boolean).length;
|
|
1030
|
+
}
|
|
1031
|
+
function knowledgeListHtml() {
|
|
1032
|
+
var sel = selectedId(), focus = S.knowledgeFocus && findRow(S.knowledgeFocus), done = setupDone();
|
|
1033
|
+
function route(tab, title, sub) {
|
|
1034
|
+
return '<button type="button" class="mm-row" data-mm="ktab" data-arg="' + tab + '" aria-selected="' + (sel === tab) + '"><span class="mm-row-body"><span class="mm-row-title">' + esc(title) + '</span><span class="mm-status">' + esc(sub) + '</span></span><span class="chev"></span></button>';
|
|
1035
|
+
}
|
|
1036
|
+
return groupHead('Explore knowledge') + '<div class="cx-list">' +
|
|
1037
|
+
route('graph', 'Ask the graph', 'Explore connections · ' + (fileGraph() ? 'your document links' : 'your knowledge graph')) +
|
|
1038
|
+
route('sources', 'Source records', 'What was actually said') +
|
|
1039
|
+
route('setup', 'Set up Knowledge', done != null ? done + ' of 7 steps done' : 'Sources, owner, first index') + '</div>' +
|
|
1040
|
+
'<div class="mm-aside-note"><h3>' + (focus ? 'Related to: ' + esc(clip(rowTitle(focus), 80)) : 'Better context. Traceable learning.') + '</h3><p>Sources explain a memory. Relationships add context. Run evidence shows whether a change helped.</p>' +
|
|
1041
|
+
textBtn('back-learning', 'Back to recent learning') + '</div>';
|
|
1042
|
+
}
|
|
1043
|
+
function knowledgeReader(tab) {
|
|
1044
|
+
crumb(tab === 'graph' ? 'Ask the graph' : tab === 'sources' ? 'Source records' : 'Set up Knowledge');
|
|
1045
|
+
if (tab === 'sources') return syncCard() + sourcesHtml();
|
|
1046
|
+
if (tab === 'setup') { if (!S.setup && !S.setupLoading && !S.setupError) loadSetup(); return syncCard() + setupHtml(); }
|
|
1047
|
+
return '<div id="mm-ask">' + askHtml() + '</div>' + graphSectionHtml() +
|
|
1048
|
+
'<details class="mm-health"><summary>Knowledge status and maintenance</summary>' + syncCard() + dupsHtml() + '</details>';
|
|
1049
|
+
}
|
|
1050
|
+
// Ask the graph in plain words; the hybrid query the setup step uses. About a minute, two model calls under today's budget.
|
|
1051
|
+
function askHtml() {
|
|
1052
|
+
if (fileGraph()) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Ask the graph</h3><p>Model answers need the advanced graph. Explore your document links below.</p></section>';
|
|
1053
|
+
var a = S.ask, focus = S.focus, nb = S.neighbors || [];
|
|
1054
|
+
var hint = !focus ? 'Ask about people, ideas, or how they connect' : nb.length ? 'Show me the relation between ' + focus + ' and ' + nb[0] : 'What does the graph know about ' + focus + '?';
|
|
1055
|
+
var tries = !a.busy && focus ? nb.slice(0, 3).map(function (n) { return '<li>' + textBtn('ask-pair', esc(focus) + ' and ' + esc(n), { arg: n }) + '</li>'; }).join('') : '';
|
|
1056
|
+
var parsed = a.answer ? parseAnswer(a.answer.answer) : null;
|
|
1057
|
+
return '<section class="cx-card mm-card mm-ask"' + (a.busy ? ' aria-busy="true"' : '') + '><div class="mm-card-head"><h3 class="mm-card-title">Ask the graph</h3><span class="mm-step-meta" data-role="ask-meta">' + (a.busy ? 'Working · ' + askSecs() + ' s' : 'About a minute') + '</span></div>' +
|
|
1058
|
+
'<p class="mm-muted">Ask about a person, an idea, or how things connect. COS picks the points and traces their connections.</p>' +
|
|
1059
|
+
'<form class="mm-inline" data-mm-form="ask"><input class="field" id="mm-ask-q" aria-label="Ask the graph" placeholder="' + esc(hint) + '" value="' + esc(a.q) + '"' + (a.busy ? ' disabled' : '') + ' maxlength="400" enterkeyhint="go">' +
|
|
1060
|
+
'<button type="submit" class="cx-btn primary"' + (a.busy ? ' disabled' : '') + '>' + (a.busy ? 'Asking…' : 'Ask') + '</button></form>' +
|
|
1061
|
+
(tries ? '<p class="mm-muted mm-try">Try the relation between</p><ul class="mm-tries">' + tries + '</ul>' : '') +
|
|
1062
|
+
(a.busy ? '<p class="mm-progress" role="status" aria-live="polite" data-role="ask-progress">' + esc(askBusyCopy(askSecs())) + '</p>' : '') +
|
|
1063
|
+
(a.message ? '<p class="mm-muted" role="status">' + esc(a.message) + '</p>' : '') +
|
|
1064
|
+
(a.error ? '<p class="mm-bad">' + esc(a.error) + '</p>' : '') +
|
|
1065
|
+
(parsed ? '<div class="mm-answer">' + renderMarkdown(parsed.prose) + '</div>' + refsHtml(parsed.refs) +
|
|
1066
|
+
'<p class="mm-muted">' + (a.answer.elapsed_s != null ? Math.round(a.answer.elapsed_s) + ' s · ' : '') + esc(a.answer.mode || 'hybrid') + ' mode · put together from the graph, not a quote</p>' +
|
|
1067
|
+
'<div class="mm-actions">' + btn('copy-ask', 'Copy answer') + btn('copy-ask', 'Copy with context', { arg: 'ctx' }) + '</div>' +
|
|
1068
|
+
(a.cardsBusy ? '<p class="cx-note">Looking up the people and ideas it names…</p>' : a.cards.length ? '<h4 class="mm-sub-title">In the graph</h4>' + a.cards.map(entityCard).join('') : '') : '') +
|
|
1069
|
+
'</section>';
|
|
1070
|
+
}
|
|
1071
|
+
function askSecs() { return S.ask.startedAt ? Math.max(0, Math.floor((Date.now() - S.ask.startedAt) / 1000)) : 0; }
|
|
1072
|
+
function askBusyCopy(s) { return (s < 8 ? 'Searching the graph' : s < 25 ? 'Choosing the relevant points' : 'Still working') + ' · ' + s + ' s · usually about a minute'; }
|
|
1073
|
+
// A safe Markdown subset for a model answer: escape first, then headings, bold, code, lists, pipe tables, paragraphs.
|
|
1074
|
+
function renderMarkdown(md) {
|
|
1075
|
+
var lines = esc(md || '').split('\n'), out = [], ul = null, table = null;
|
|
1076
|
+
function inline(s) { return s.replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); }
|
|
1077
|
+
function flush() { if (ul) { out.push('<ul>' + ul.join('') + '</ul>'); ul = null; } if (table) { out.push('<div class="mm-table"><table>' + table.join('') + '</table></div>'); table = null; } }
|
|
1078
|
+
lines.forEach(function (raw) {
|
|
1079
|
+
var line = raw.replace(/\s+$/, '');
|
|
1080
|
+
if (!line.trim()) { flush(); return; }
|
|
1081
|
+
var h = line.match(/^(#{1,4})\s+(.*)$/);
|
|
1082
|
+
if (h) { flush(); out.push('<h4>' + inline(h[2]) + '</h4>'); return; }
|
|
1083
|
+
var li = line.match(/^\s*[-*•]\s+(.*)$/);
|
|
1084
|
+
if (li) { if (table) flush(); ul = ul || []; ul.push('<li>' + inline(li[1]) + '</li>'); return; }
|
|
1085
|
+
if (/^\s*\|.*\|\s*$/.test(line)) {
|
|
1086
|
+
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) return;
|
|
1087
|
+
if (ul) flush();
|
|
1088
|
+
var cells = line.trim().replace(/^\||\|$/g, '').split('|').map(function (c) { return inline(c.trim()); });
|
|
1089
|
+
var tag = table ? 'td' : 'th'; table = table || [];
|
|
1090
|
+
table.push('<tr>' + cells.map(function (c) { return '<' + tag + '>' + c + '</' + tag + '>'; }).join('') + '</tr>'); return;
|
|
1091
|
+
}
|
|
1092
|
+
flush(); out.push('<p>' + inline(line) + '</p>');
|
|
1093
|
+
});
|
|
1094
|
+
flush();
|
|
1095
|
+
return out.join('');
|
|
1096
|
+
}
|
|
1097
|
+
function parseAnswer(answer) {
|
|
1098
|
+
var text = String(answer || ''), marker = '\n\nEvidence references:\n', i = text.indexOf(marker), refs = [];
|
|
1099
|
+
if (i >= 0) text.slice(i + marker.length).split('\n').forEach(function (line) {
|
|
1100
|
+
var m = line.match(/^\[(S\d+)\]\s+(.*)$/); if (!m) return;
|
|
1101
|
+
var p = m[2].split(' · ');
|
|
1102
|
+
refs.push({ id: m[1], source: (p[0] || '').trim() || 'Original passage', date: (p[1] || '').trim() === 'date unknown' ? '' : (p[1] || '').trim(), status: p[3] || '' });
|
|
1103
|
+
});
|
|
1104
|
+
return { prose: i < 0 ? text : text.slice(0, i), refs: refs };
|
|
1105
|
+
}
|
|
1106
|
+
function refsHtml(refs) {
|
|
1107
|
+
if (!refs.length) return '';
|
|
1108
|
+
return '<div class="mm-refs"><h4 class="mm-sub-title">Sources</h4><ol>' + refs.map(function (r) {
|
|
1109
|
+
var meta = [r.date, r.status === 'source_unresolved' ? 'not linked to a meeting file' : ''].filter(Boolean).join(' · ');
|
|
1110
|
+
return '<li><span class="mm-ref">[' + esc(r.id) + ']</span> ' + esc(r.source) + (meta ? '<span class="mm-muted"> · ' + esc(meta) + '</span>' : '') + '</li>';
|
|
1111
|
+
}).join('') + '</ol></div>';
|
|
1112
|
+
}
|
|
1113
|
+
function entityCard(en) {
|
|
1114
|
+
var desc = (en.descriptions && en.descriptions.length ? en.descriptions[0] : en.description) || '';
|
|
1115
|
+
var rels = (en.edges || []).slice(0, 3).map(function (e) { var other = e.source === en.id ? e.target : e.source; return '<li><b>' + esc(other) + '</b> ' + esc(clip(e.description || '', 140)) + '</li>'; }).join('');
|
|
1116
|
+
return '<section class="mm-entity"><div class="mm-card-head"><h4>' + esc(en.id) + '</h4><span class="mm-step-meta">' + esc(en.type || 'entity') + ' · ' + fmt(en.degree || 0) + ' ' + plural(en.degree, 'connection', 'connections') + '</span></div>' +
|
|
1117
|
+
(desc ? '<p>' + esc(clip(String(desc).split('<SEP>')[0], 320)) + '</p>' : '<p class="mm-muted">No description stored.</p>') + (rels ? '<ul class="mm-rels">' + rels + '</ul>' : '') +
|
|
1118
|
+
'<div class="mm-actions">' + btn('copy-entity', 'Copy context', { id: en.id }) + btn('passages', 'Show passages', { id: en.id }) + btn('focus', 'Explore from here', { id: en.id }) + '</div></section>';
|
|
1119
|
+
}
|
|
1120
|
+
function entityText(en) {
|
|
1121
|
+
var desc = (en.descriptions && en.descriptions.length ? en.descriptions : (en.description ? [en.description] : [])).join('\n');
|
|
1122
|
+
var rels = (en.edges || []).slice(0, 6).map(function (e) { var other = e.source === en.id ? e.target : e.source; return '- ' + other + (e.description ? ': ' + e.description : ''); }).join('\n');
|
|
1123
|
+
return en.id + ' (' + (en.type || 'entity') + ', ' + fmt(en.degree || 0) + ' connections)\n' + desc + (rels ? '\nRelationships:\n' + rels : '');
|
|
1124
|
+
}
|
|
1125
|
+
function graphSectionHtml() {
|
|
1126
|
+
var full = S.graphFull;
|
|
1127
|
+
return '<section class="mm-graph' + (full ? ' full' : '') + '" id="mm-graph"' + (full ? ' role="dialog" aria-modal="true" aria-label="Your knowledge graph"' : '') + '>' +
|
|
1128
|
+
'<div class="mm-graph-head"><div class="mm-graph-text"><h3 class="mm-card-title">Your knowledge</h3><p class="mm-muted">' + (fileGraph() ? 'Your document links.' : 'Connected people and ideas from your meetings and notes.') +
|
|
1129
|
+
(S.graphVisible != null ? ' ' + fmt(S.graphVisible) + ' points in this view.' : '') + '</p></div>' +
|
|
1130
|
+
btn('graph-full', full ? 'Done' : 'Full screen', { cls: full ? 'primary' : 'quiet' }) + '</div>' +
|
|
1131
|
+
'<div id="mm-graph-host" class="mm-graph-host">' + (S.ws ? '' : S.kitError ? '<div class="mm-state"><strong>The graph explorer did not load.</strong><span>' + esc(S.kitError) + '</span><span class="mm-state-act">' + btn('graph-retry', 'Try again') + '</span></div>' : '<p class="cx-note mm-graph-wait">Loading the graph explorer…</p>') + '</div></section>';
|
|
1132
|
+
}
|
|
1133
|
+
function syncCard() {
|
|
1134
|
+
var g = S.graph;
|
|
1135
|
+
if (S.graphError) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Sync</h3><p class="mm-bad">' + esc(S.graphError) + '</p><div class="mm-actions">' + btn('graph-status', 'Try again') + '</div></section>';
|
|
1136
|
+
if (!g) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Sync</h3><p class="cx-note">Reading the graph\'s state…</p></section>';
|
|
1137
|
+
if (fileGraph()) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Document links</h3><p>Documents you grant and your own links build this local graph. Links describe associations; they do not establish causes.</p><dl class="mm-grid"><dt>Indexed</dt><dd>' + fmt(g.entities || 0) + ' points · ' + fmt(g.relationships || 0) + ' links</dd><dt>Index</dt><dd>' + esc(indexWord(g.index_state)) + '</dd></dl><div class="mm-actions">' + btn('steward', 'Current sources and rules') + '</div></section>';
|
|
1138
|
+
var src = g.source || {}, q = g.queue || {}, b = g.budget || {}, lk = g.lock || {}, proc = g.processor || {};
|
|
1139
|
+
var ownerName = hostLabel(src.owner_host) || 'the owner Mac';
|
|
1140
|
+
var owner = src.owner_state === 'owner' ? 'This Mac builds the graph (' + esc(ownerName) + ').' : src.owner_state === 'replica' ? '<b>' + esc(ownerName) + '</b> builds the graph; this Mac reads a copy through iCloud.' : 'No Mac is set to build the graph yet. Set one in Set up Knowledge.';
|
|
1141
|
+
var pending = q.pending != null ? Number(q.pending) : 0;
|
|
1142
|
+
var indexing = proc.state === 'none' || !proc.state ? 'No scheduled indexing on this Mac' : proc.state === 'installed-idle' ? 'Scheduled, no run recorded yet' : 'Scheduled: ' + esc(proc.state);
|
|
1143
|
+
if (S.ingest && S.ingest.note) indexing += '<br><span class="mm-muted">' + esc(S.ingest.note) + '</span>';
|
|
1144
|
+
else if (lockHeld(lk)) indexing += '<br><span class="mm-muted">Indexing now</span>';
|
|
1145
|
+
var control = '';
|
|
1146
|
+
if (!S.ingest && !lockHeld(lk) && src.owner_state === 'owner' && pending > 0) {
|
|
1147
|
+
var sizes = [5, 10, 25, 50].filter(function (n) { return n < pending; });
|
|
1148
|
+
var opts = sizes.map(function (n) { return '<option value="' + n + '"' + (n === S.ingestLimit ? ' selected' : '') + '>' + n + '</option>'; }).join('');
|
|
1149
|
+
if (pending <= 50) opts += '<option value="' + pending + '"' + (sizes.indexOf(S.ingestLimit) < 0 ? ' selected' : '') + '>All ' + fmt(pending) + '</option>';
|
|
1150
|
+
control = '<div class="mm-inline mm-ingest"><label class="mm-pick"><span>Batch</span><select class="field" id="mm-ingest-limit" data-mm-change="ingest-limit" aria-label="How many to index">' + opts + '</select></label>' + btn('ingest', 'Index now', { cls: 'primary' }) + '</div>';
|
|
1151
|
+
}
|
|
1152
|
+
var queued = (q.pending != null ? fmt(q.pending) + ' waiting' : 'unknown') + (q.oldest_pending_at ? ' · oldest ' + esc(day(q.oldest_pending_at)) : '') + (q.missing_sources ? ' · ' + fmt(q.missing_sources) + ' sources missing' : '') + (q.conflict_copies ? ' · ' + fmt(q.conflict_copies) + ' iCloud conflict copies' : '');
|
|
1153
|
+
var indexed = (g.entities != null ? fmt(g.entities) + ' people and ideas' : 'no graph yet') + (g.relationships != null ? ' · ' + fmt(g.relationships) + ' connections' : '') + (g.source_updated_at ? ' · updated ' + esc(when(g.source_updated_at)) : '');
|
|
1154
|
+
var invite = g.index_state === 'missing' || g.index_state === 'stale';
|
|
1155
|
+
var build = S.building ? ' <span class="mm-muted">' + esc(S.building) + '</span>' : invite ? '<div class="mm-actions">' + btn('build-index', 'Build index (usually a few seconds)') + '</div>' : '';
|
|
1156
|
+
var captured = S.status && S.status.meetingLibraryCount != null ? fmt(S.status.meetingLibraryCount) + ' meetings in the library' : '';
|
|
1157
|
+
return '<section class="cx-card mm-card"><h3 class="mm-card-title">Sync</h3><p>' + owner + '</p><dl class="mm-grid">' +
|
|
1158
|
+
(captured ? '<dt>Captured</dt><dd>' + esc(captured) + '</dd>' : '') + '<dt>Queued</dt><dd>' + queued + '</dd><dt>Indexing</dt><dd>' + indexing + control + '</dd>' +
|
|
1159
|
+
'<dt>Indexed</dt><dd>' + indexed + '</dd><dt>Index</dt><dd>' + esc(indexWord(g.index_state)) + (g.index_built_at ? ' · built ' + esc(when(g.index_built_at)) : '') + (g.index_degraded ? ' · partly built' : '') + build + '</dd>' +
|
|
1160
|
+
'<dt>Budget</dt><dd>' + (b.used != null && b.cap != null ? fmt(b.used) + ' of ' + fmt(b.cap) + ' model calls today' : 'unknown') + '</dd></dl>' + progressHtml() +
|
|
1161
|
+
'<p class="cx-note">Values from this Mac.</p></section>';
|
|
1162
|
+
}
|
|
1163
|
+
function indexWord(s) { return s === 'fresh' ? 'Up to date' : s === 'stale' ? 'Out of date' : s === 'missing' ? 'Not built yet' : s === 'building' ? 'Building' : s || 'Unknown'; }
|
|
1164
|
+
function progressHtml() {
|
|
1165
|
+
var pr = S.progress, g = S.graph || {}, lk = (pr && pr.lock) || g.lock || {};
|
|
1166
|
+
if (!((pr && pr.running === true) || lockHeld(lk))) return '';
|
|
1167
|
+
if (!pr) return '<p class="mm-progress">Indexing now · reading progress…</p>';
|
|
1168
|
+
var head = pr.external ? 'Indexing now from another session · ' + fmt(pr.pending || 0) + ' waiting' :
|
|
1169
|
+
'Indexing ' + (pr.total != null ? fmt(Math.min(pr.done + pr.failed + (pr.current ? 1 : 0), pr.total)) + ' of ' + fmt(pr.total) : 'now') + ' · ' + fmt(pr.pending || 0) + ' waiting';
|
|
1170
|
+
var items = (pr.items || []).slice(-4).map(function (it) { return '<li class="' + (it.outcome === 'failed' ? 'mm-bad' : '') + '">' + esc((it.outcome === 'indexed' ? 'Indexed ' : it.outcome === 'failed' ? 'Failed ' : '') + String(it.id).split('/').pop()) + (it.seconds != null ? ' · ' + Math.round(it.seconds) + ' s' : '') + '</li>'; }).join('');
|
|
1171
|
+
return '<div class="mm-progress" aria-live="polite"><b>' + esc(head) + '</b>' + (items ? '<ul>' + items + '</ul>' : '') +
|
|
1172
|
+
(pr.budget ? '<span class="mm-muted">Budget: ' + fmt(pr.budget.used) + ' of ' + fmt(pr.budget.cap) + ' model calls today</span>' : '') +
|
|
1173
|
+
(pr.log_tail && pr.log_tail.length ? '<details><summary>Show log</summary><pre>' + esc(pr.log_tail.join('\n')) + '</pre></details>' : '') + '</div>';
|
|
1174
|
+
}
|
|
1175
|
+
function dupsHtml() {
|
|
1176
|
+
if (fileGraph()) return '';
|
|
1177
|
+
var d = S.dups, head = '<h3 class="mm-card-title">Possible duplicates</h3><p class="mm-muted">People whose names look like one person. Suggestions only.</p>';
|
|
1178
|
+
var again = '<div class="mm-actions">' + btn('dups', 'Scan again') + '</div>';
|
|
1179
|
+
if (d.error) return '<section class="cx-card mm-card">' + head + '<p class="mm-bad">' + esc(d.error) + '</p>' + again + '</section>';
|
|
1180
|
+
if (d.busy) return '<section class="cx-card mm-card">' + head + '<p class="cx-note">Scanning this Mac\'s index…</p></section>';
|
|
1181
|
+
if (!d.data) return '<section class="cx-card mm-card">' + head + '<p>Groups people whose names are variants of one another: a first name that matches one full name, a surname within two letters, one name spelling out the other. People the graph knows are different never share a group. Nothing merges on its own; each row opens the same two-step preview.</p><div class="mm-actions">' + btn('dups', 'Find possible duplicates') + '</div></section>';
|
|
1182
|
+
var groups = d.data.groups || [];
|
|
1183
|
+
if (!groups.length) return '<section class="cx-card mm-card">' + head + '<p>No name variants among ' + fmt(d.data.scanned || 0) + ' people' + (d.data.available === false ? ' (no index on this Mac yet)' : '') + '.</p>' + again + '</section>';
|
|
1184
|
+
return '<section class="cx-card mm-card">' + head + '<p>' + fmt(d.data.total_groups || groups.length) + ' ' + plural(d.data.total_groups || groups.length, 'group', 'groups') + ' among ' + fmt(d.data.scanned || 0) + ' people. Read both descriptions before merging.</p>' +
|
|
1185
|
+
groups.map(function (g) {
|
|
1186
|
+
var t = g.members[0] || {};
|
|
1187
|
+
return '<div class="mm-dup"><div class="mm-card-head"><b>' + esc(g.target) + '</b><span class="mm-step-meta">' + fmt(t.degree || 0) + ' connections · ' + esc(g.confidence) + ' confidence</span></div>' +
|
|
1188
|
+
g.members.slice(1).map(function (m) { return '<div class="mm-dup-member"><span><b>' + esc(m.id) + '</b> <span class="mm-muted">' + fmt(m.degree) + ' ' + plural(m.degree, 'connection', 'connections') + ' · ' + fmt(m.shared_neighbors) + ' shared' + (m.why ? ' · ' + esc(m.why) : '') + '</span></span>' + btn('merge-from', 'Preview merge', { id: m.id, arg: g.target }) + '</div>'; }).join('') + '</div>';
|
|
1189
|
+
}).join('') + again + '</section>';
|
|
1190
|
+
}
|
|
1191
|
+
function exploreTerm(e) { if (known(e.category) && e.category !== e.scope && e.category.indexOf(PRUNE_READY) !== 0) return e.category; return String(cleanTitle(e.title) || '').split(' ').slice(0, 4).join(' ') || 'COS'; }
|
|
1192
|
+
function sourcesHtml() {
|
|
1193
|
+
var focus = S.knowledgeFocus && findRow(S.knowledgeFocus), cards = [];
|
|
1194
|
+
if (focus) {
|
|
1195
|
+
ensureDetail(focus);
|
|
1196
|
+
var e = fullRow(focus);
|
|
1197
|
+
(e.source_refs || []).forEach(function (r, i) { cards.push({ title: storeName(r.kind) || 'Source', kind: '', text: r.excerpt || '', note: i === 0 ? 'The record this lesson came from.' : '' }); });
|
|
1198
|
+
var term = exploreTerm(e);
|
|
1199
|
+
if (!S.passages[term] && !S.loading['p:' + term]) {
|
|
1200
|
+
S.loading['p:' + term] = true;
|
|
1201
|
+
call('graph.passages', { entity: term, limit: 5 }).then(function (d) { S.passages[term] = d; }, function (er) { S.passages[term] = { _error: er.message }; }).then(function () { delete S.loading['p:' + term]; paint(); });
|
|
1202
|
+
}
|
|
1203
|
+
var p = S.passages[term];
|
|
1204
|
+
if (p && !p._error) (p.items || []).forEach(function (it) { var s = it.source || {}; cards.push({ title: s.title || 'Passage', kind: s.status === 'resolved' ? 'Meeting excerpt' : 'Passage, not linked to a file', text: it.excerpt, note: s.date || '' }); });
|
|
1205
|
+
else if (p && p._error) cards.push({ title: 'Passages', kind: 'Not available', text: p._error, note: '' });
|
|
1206
|
+
else cards.push({ title: 'Passages', kind: '', text: '', note: 'Reading passages about ' + term + '…' });
|
|
1207
|
+
} else {
|
|
1208
|
+
cards = S.recent.rows.slice(0, 8).filter(function (e) { return (e.source_refs || []).some(function (r) { return r.excerpt; }); }).map(function (e) {
|
|
1209
|
+
var r = e.source_refs.filter(function (x) { return x.excerpt; })[0];
|
|
1210
|
+
return { title: rowTitle(e), kind: storeName(r.kind), text: r.excerpt, note: when(e.ts), event: e.event_id };
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
return readerHead('The context behind the lesson.', 'Open the original evidence before reusing an interpretation.' + (focus ? ' For: ' + esc(clip(rowTitle(focus), 80)) : '')) +
|
|
1214
|
+
(cards.length ? cards.map(function (x) {
|
|
1215
|
+
return '<article class="cx-card mm-card"><div class="mm-card-head"><h3 class="mm-card-title">' + esc(clip(x.title, 140)) + '</h3>' + (x.kind ? '<span class="mm-step-meta">' + esc(x.kind) + '</span>' : '') + '</div>' +
|
|
1216
|
+
(x.text ? quote(x.text) : '') + (x.note ? '<p class="mm-muted">' + esc(x.note) + '</p>' : '') + (x.event ? '<div class="mm-actions">' + btn('open-e', 'Inspect learning', { id: x.event }) + '</div>' : '') + '</article>';
|
|
1217
|
+
}).join('') : '<div class="mm-reader-state"><strong>No source records yet.</strong><span>Your first saved lesson brings its source here.</span></div>');
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// ── Set up Knowledge: seven steps, each a live check with its own control ──
|
|
1221
|
+
function loadSetup() {
|
|
1222
|
+
if (fileGraph()) { S.setup = { engine: 'explicit_document_links' }; paint(); return Promise.resolve(); }
|
|
1223
|
+
S.setupLoading = true; S.setupError = ''; paint();
|
|
1224
|
+
return call('graph.setup').then(function (d) { S.setup = d; }, function (e) { S.setupError = e.message; })
|
|
1225
|
+
.then(function () { S.setupLoading = false; paint(); });
|
|
1226
|
+
}
|
|
1227
|
+
function setupAction(name, op, args, onDone) {
|
|
1228
|
+
if (S.setupBusy) return Promise.resolve();
|
|
1229
|
+
S.setupBusy = name; S.armed = ''; paint();
|
|
1230
|
+
return call(op, args).then(function (d) { S.setupBusy = ''; if (onDone) onDone(d); return loadSetup(); },
|
|
1231
|
+
function (e) { S.setupBusy = ''; W.toast(e.message); paint(); });
|
|
1232
|
+
}
|
|
1233
|
+
function setupStep(n, done, title, body, blocked) {
|
|
1234
|
+
return '<li class="mm-setup-step' + (done ? ' done' : blocked ? ' blocked' : '') + '"><span class="mm-setup-mark" aria-hidden="true">' + (done ? '✓' : n) + '</span>' +
|
|
1235
|
+
'<div class="mm-setup-body"><h4>' + title + (done ? '<span class="sr-only"> (done)</span>' : '') + '</h4>' + body + '</div></li>';
|
|
1236
|
+
}
|
|
1237
|
+
function shortPath(p) { return String(p || '').replace(/^\/Users\/[^/]+/, '~'); }
|
|
1238
|
+
function setupHtml() {
|
|
1239
|
+
if (fileGraph()) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Set up document links</h3><p>This setup indexes Markdown and text documents the owner grants on the Mac. Write links as [[Document title]] to connect ideas. Model extraction and embeddings are not set up here; keyword search and document links work.</p><div class="mm-actions">' + btn('steward', 'Current sources and rules') + '</div></section>';
|
|
1240
|
+
var s = S.setup;
|
|
1241
|
+
if (S.setupError) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Set up Knowledge</h3><p class="mm-bad">' + esc(S.setupError) + '</p><div class="mm-actions">' + btn('setup-refresh', 'Try again') + '</div></section>';
|
|
1242
|
+
if (!s) return '<section class="cx-card mm-card"><h3 class="mm-card-title">Set up Knowledge</h3><p class="cx-note">Checking this Mac…</p></section>';
|
|
1243
|
+
var checks = s.checks || [], checksOk = checks.length > 0 && checks.every(function (c) { return c.ok; });
|
|
1244
|
+
var sources = s.sources || [], withFiles = sources.filter(function (r) { return r.enabled && r.exists && (r.files || 0) > 0; });
|
|
1245
|
+
var owner = s.owner || {}, isOwner = owner.is_owner === true, budget = s.budget || {}, sample = s.sample || {}, cands = sample.candidates || [];
|
|
1246
|
+
var graphN = (s.graph || {}).entities, hasGraph = graphN != null && graphN > 0, sched = s.schedule || {}, held = lockHeld(s.lock);
|
|
1247
|
+
var busy = !!S.setupBusy, emb = s.embedding || {}, ext = s.extraction || {};
|
|
1248
|
+
var budgetOk = budget.used != null && budget.cap != null && budget.used < budget.cap, embNotReady = emb.ready === false;
|
|
1249
|
+
var steps = [];
|
|
1250
|
+
steps.push(setupStep(1, checksOk, 'Turn on Knowledge', '<p>What indexing needs on this Mac.</p><ul class="mm-checks">' + checks.map(function (c) { return '<li class="' + (c.ok ? 'ok' : 'mm-bad') + '"><span aria-hidden="true">' + (c.ok ? '✓' : '!') + '</span> ' + esc(c.detail) + '</li>'; }).join('') + '</ul>', checks.length && !checksOk));
|
|
1251
|
+
steps.push(setupStep(2, withFiles.length > 0, 'Choose sources', '<p>Folders of notes or documents (Markdown and text). Only files in these folders are read.</p>' +
|
|
1252
|
+
(sources.length ? '<ul class="mm-sources">' + sources.map(function (r) {
|
|
1253
|
+
return '<li><code>' + esc(shortPath(r.path)) + '</code><span class="mm-muted">' + (r.exists ? (r.files != null ? fmt(r.files) + ' ' + plural(r.files, 'document', 'documents') : '') : 'missing') + '</span>' +
|
|
1254
|
+
btn('source-toggle', r.enabled ? 'On' : 'Off', { arg: r.path, disabled: busy }) + btn('source-remove', S.armed === 'remove:' + r.path ? 'Tap again to remove' : 'Remove', { arg: r.path, disabled: busy }) + '</li>';
|
|
1255
|
+
}).join('') + '</ul>' : '') +
|
|
1256
|
+
'<form class="mm-inline" data-mm-form="folder"><input class="field" id="mm-folder" aria-label="Folder path on your Mac" placeholder="Folder path on your Mac, like ~/Documents/Notes" value="' + esc(S.folderDraft) + '" autocomplete="off" autocapitalize="off" spellcheck="false">' +
|
|
1257
|
+
'<button type="submit" class="cx-btn"' + (busy ? ' disabled' : '') + '>' + (S.setupBusy === 'add' ? 'Adding…' : 'Add folder') + '</button></form>' +
|
|
1258
|
+
(sources.length && !withFiles.length ? '<p class="mm-muted">No documents found in the folders that are on.</p>' : '')));
|
|
1259
|
+
var ownerBody = isOwner ? '<p>This Mac (' + esc(hostLabel(owner.this_host)) + ') indexes the queue. Its graph, queue and sources stay here.</p>' :
|
|
1260
|
+
owner.owner_host ? '<p><b>' + esc(hostLabel(owner.owner_host)) + '</b> indexes the queue; this Mac reads a copy. Make this Mac the owner only if that Mac stops indexing.</p>' : '<p>No Mac is set to index yet.</p>';
|
|
1261
|
+
if (!isOwner) ownerBody += '<div class="mm-actions">' + btn('claim-owner', S.armed === 'owner' ? 'Tap again to make this Mac the owner' : S.setupBusy === 'owner' ? 'Setting…' : 'Make this Mac the owner', { disabled: busy }) + '</div>';
|
|
1262
|
+
steps.push(setupStep(3, isOwner, 'Owner Mac', ownerBody));
|
|
1263
|
+
var exe = ext.execution || {};
|
|
1264
|
+
steps.push(setupStep(4, checksOk && budgetOk && !emb.mismatch && !embNotReady, 'Choose how it indexes',
|
|
1265
|
+
'<p>Extraction quality for the next run. Runs with: ' + esc([exe.provider, exe.requested_model, exe.reasoning_effort].filter(Boolean).join(' · ') || 'not reported by this server') + '.</p>' + extractionCards(ext, busy) +
|
|
1266
|
+
'<p>Embeddings for search: one choice for the knowledge graph and every meeting index. A graph keeps the embedding it was built with.</p>' + localOnlyStrip(emb, busy) + embeddingCards(emb, busy) +
|
|
1267
|
+
'<p class="mm-muted">Budget today: ' + (budget.used != null ? fmt(budget.used) + ' of ' + fmt(budget.cap) + ' model calls' : 'unknown') + (budgetOk ? '' : ' (used up until tomorrow)') + '.</p>' +
|
|
1268
|
+
'<p class="mm-muted">What leaves this Mac: each document\'s text goes to the model for extraction' + (emb.kind === 'local' ? '; embeddings are made on this Mac' : ' and to OpenAI for embeddings') + '. The graph, the queue and this folder list never leave it.</p>', emb.mismatch || embNotReady));
|
|
1269
|
+
var sampleDone = (sample.indexed || 0) >= 1 || hasGraph;
|
|
1270
|
+
var sb = sampleDone ? '<p>' + ((sample.indexed || 0) >= 1 ? fmt(sample.indexed) + ' sample ' + plural(sample.indexed, 'document', 'documents') + ' indexed.' : 'Your graph already holds ' + fmt(graphN) + ' people and ideas.') + '</p>' : '<p>The three newest documents from the folders that are on, through the same checks a meeting gets, in one bounded run.</p>';
|
|
1271
|
+
if (cands.length) sb += '<ul class="mm-sources">' + cands.map(function (c) { return '<li><code>' + esc(String(c.path).split('/').pop()) + '</code><span class="mm-muted">' + (c.bytes != null ? fmt(Math.max(1, Math.round(c.bytes / 1024))) + ' KB' : '') + '</span></li>'; }).join('') + '</ul>';
|
|
1272
|
+
if (S.ingest && S.ingest.note) sb += '<p class="mm-muted">' + esc(S.ingest.note) + '</p>'; else if (held) sb += progressHtml();
|
|
1273
|
+
if ((sample.queued || 0) > 0 && !held) sb += '<p class="mm-muted">' + fmt(sample.queued) + ' queued and waiting for a run.</p>';
|
|
1274
|
+
if (cands.length && isOwner && !held) sb += '<div class="mm-actions">' + btn('index-sample', S.setupBusy === 'sample' ? 'Queueing…' : 'Index ' + (cands.length === 1 ? 'this document' : 'these ' + cands.length), { disabled: busy || !checksOk || embNotReady }) + '</div>' + (embNotReady ? '<p class="mm-muted">Blocked: ' + esc(emb.label || emb.provider) + ' is not ready. ' + esc(emb.fix || '') + '</p>' : '');
|
|
1275
|
+
else if (cands.length && !isOwner) sb += '<p class="mm-muted">Indexing runs on the owner Mac.</p>';
|
|
1276
|
+
steps.push(setupStep(5, sampleDone, 'Index three sample documents', sb, !checksOk));
|
|
1277
|
+
steps.push(setupStep(6, !!S.ask.answer, 'Ask one question', '<p>One question, answered from the graph. About a minute; two model calls under today\'s budget.</p>' +
|
|
1278
|
+
(s.ask_ready ? '<p>' + btn('ktab', 'Ask the graph', { arg: 'graph' }) + '</p>' : '<p class="mm-muted">Index something first.</p>') +
|
|
1279
|
+
(S.ask.answer ? '<div class="mm-answer">' + renderMarkdown(parseAnswer(S.ask.answer.answer).prose) + '</div>' : ''), !s.ask_ready));
|
|
1280
|
+
var schedBody = sched.installed ? '<p>On: up to 4 queued documents every ' + (sched.interval_s ? fmt(Math.round(sched.interval_s / 60)) + ' minutes' : 'interval') + '.</p>' : '<p>Off. Turn it on and this Mac indexes what is queued on a schedule, 4 at a time, under the daily budget.</p>';
|
|
1281
|
+
if (isOwner) schedBody += sched.installed ? '<div class="mm-actions">' + btn('schedule', S.setupBusy === 'schedule' ? 'Working…' : 'Turn off', { arg: 'off', disabled: busy }) + '</div>' :
|
|
1282
|
+
'<div class="mm-inline"><label class="mm-pick"><span>How often</span><select class="field" id="mm-schedule"><option value="900">Every 15 minutes</option><option value="3600" selected>Every hour</option><option value="14400">Every 4 hours</option><option value="86400">Once a day</option></select></label>' + btn('schedule', S.setupBusy === 'schedule' ? 'Working…' : 'Turn on', { arg: 'on', disabled: busy }) + '</div>';
|
|
1283
|
+
else schedBody += '<p class="mm-muted">Schedules live on the owner Mac.</p>';
|
|
1284
|
+
steps.push(setupStep(7, sched.installed === true, 'Scheduled batches', schedBody));
|
|
1285
|
+
var done = setupDone();
|
|
1286
|
+
return '<section class="cx-card mm-card"><div class="mm-card-head"><h3 class="mm-card-title">Set up Knowledge</h3><span class="mm-step-meta">' + done + ' of 7 done' + (S.setupLoading ? ' · refreshing' : '') + '</span></div>' +
|
|
1287
|
+
'<p class="mm-muted">From nothing to a first index on this Mac. Each step is a live check; do them in order.</p><ol class="mm-setup">' + steps.join('') + '</ol>' +
|
|
1288
|
+
'<div class="mm-actions">' + btn('setup-refresh', 'Refresh checks') + btn('ktab', 'Source records', { arg: 'sources' }) + '</div></section>';
|
|
1289
|
+
}
|
|
1290
|
+
function extractionCards(ext, busy) {
|
|
1291
|
+
var tiers = ext.tiers || [];
|
|
1292
|
+
if (!tiers.length) return '<p class="mm-muted">Indexing tiers need a newer server.</p>';
|
|
1293
|
+
return '<div class="mm-picks" role="radiogroup" aria-label="Extraction quality">' + tiers.map(function (t) {
|
|
1294
|
+
return '<label class="mm-pick-card' + (t.selected ? ' on' : '') + '"><input type="radio" name="mm-tier" data-mm-change="tier" value="' + esc(t.id) + '"' + (t.selected ? ' checked' : '') + (busy ? ' disabled' : '') + '><span><b>' + esc(t.label) + '</b><span class="mm-muted">' + esc(t.detail) + '</span></span></label>';
|
|
1295
|
+
}).join('') + '</div>';
|
|
1296
|
+
}
|
|
1297
|
+
// One question the person answers (may text leave this Mac), one fact detected (an OpenAI key), and Ollama running
|
|
1298
|
+
// picks premium over light. The answer moves the Recommended mark, never the choice.
|
|
1299
|
+
function localOnlyStrip(emb, busy) {
|
|
1300
|
+
if (!emb.preference) return '<p class="mm-muted">The recommendation needs a newer server.</p>';
|
|
1301
|
+
var v = emb.preference.local_only, rec = emb.recommended || null;
|
|
1302
|
+
return '<div class="mm-inline mm-ask-strip"><span>May your text leave this Mac for embeddings?</span><span class="cx-switch">' +
|
|
1303
|
+
'<button type="button" data-mm="local-only" data-arg="0" aria-pressed="' + (v === false) + '"' + (busy ? ' disabled' : '') + '>Cloud is fine</button>' +
|
|
1304
|
+
'<button type="button" data-mm="local-only" data-arg="1" aria-pressed="' + (v === true) + '"' + (busy ? ' disabled' : '') + '>Stays on this Mac</button></span></div>' +
|
|
1305
|
+
(rec ? '<p class="mm-muted"><b>Recommended: ' + esc(rec.label) + '.</b> ' + esc(rec.reason) + '</p>' : '');
|
|
1306
|
+
}
|
|
1307
|
+
function embeddingCards(emb, busy) {
|
|
1308
|
+
var rows = emb.providers || [];
|
|
1309
|
+
if (!rows.length) return '<p class="mm-muted">Embedding choices need a newer server.</p>';
|
|
1310
|
+
var fetch = emb.fetch || null, recId = emb.recommended ? emb.recommended.id : null;
|
|
1311
|
+
var cards = rows.map(function (r) {
|
|
1312
|
+
var canPick = !emb.locked || r.selected, fetching = fetch && fetch.state === 'running' && fetch.provider === r.id, failed = fetch && fetch.state === 'failed' && fetch.provider === r.id;
|
|
1313
|
+
var offline = r.id === 'ollama' && /not running/.test(r.detail || '');
|
|
1314
|
+
return '<label class="mm-pick-card' + (r.selected ? ' on' : '') + (canPick ? '' : ' off') + '"><input type="radio" name="mm-emb" data-mm-change="embedding" value="' + esc(r.id) + '"' + (r.selected ? ' checked' : '') + (canPick && !busy ? '' : ' disabled') + '>' +
|
|
1315
|
+
'<span><b>' + esc(r.label) + '</b>' + (r.id === recId ? ' <span class="cx-cap gold">Recommended</span>' : '') + '<span class="mm-muted">' + esc(r.model) + (r.dimensions != null ? ' · ' + fmt(r.dimensions) + ' dims' : '') + ' · ' + esc(r.cost) + '</span>' +
|
|
1316
|
+
'<span class="' + (r.ready ? 'mm-ok' : 'mm-muted') + '">' + (r.ready ? '✓ ' : '') + esc(r.detail) + '</span>' + (r.selected && !r.ready && r.fix ? '<span class="mm-bad">' + esc(r.fix) + '</span>' : '') +
|
|
1317
|
+
(fetching ? '<span class="mm-muted">Fetching ' + esc(fetch.model) + '… this refreshes as it runs.</span>' : failed ? '<span class="mm-bad">Fetch failed: ' + esc(fetch.error || 'unknown') + '</span>' : '') +
|
|
1318
|
+
(r.kind === 'local' && !r.ready && !offline ? btn('fetch-model', fetching ? 'Fetching…' : 'Fetch model', { arg: r.id, disabled: busy || fetching }) : '') + '</span></label>';
|
|
1319
|
+
}).join('');
|
|
1320
|
+
var note = '';
|
|
1321
|
+
if (emb.mismatch && emb.manifest) note = '<p class="mm-bad">This graph was built with ' + esc(emb.manifest.provider) + ' (' + esc(emb.manifest.model) + ') but ' + esc(emb.provider) + ' is chosen. Choose the one it was built with; Index now is blocked until then.</p>';
|
|
1322
|
+
else if (emb.locked) note = '<p class="mm-muted">This graph was built with ' + esc(emb.label || emb.provider) + '. Changing it means rebuilding the graph, which is not available yet.</p>';
|
|
1323
|
+
else if (emb.ready === false) note = '<p class="mm-bad">' + esc(emb.label || emb.provider) + ' is chosen but not ready on this Mac. ' + esc(emb.fix || '') + ' Indexing waits until then.</p>';
|
|
1324
|
+
return '<div class="mm-picks" role="radiogroup" aria-label="Embeddings">' + cards + '</div>' + note;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// ── sheets: Copy context, activity log, Memory settings, stewardship, passages, merge ──
|
|
1328
|
+
function openModal(kind, extra) { S.modal = Object.assign({ kind: kind }, extra || {}); paintModal(true); }
|
|
1329
|
+
function closeModal() {
|
|
1330
|
+
var m = S.modal; if (!m) return;
|
|
1331
|
+
if (m.kind === 'merge' && S.merge && (S.merge.phase === 'preview' || S.merge.phase === 'loading')) { var pend = S.merge; S.merge = null; if (pend.resolve) pend.resolve(false); W.toast('Nothing was changed.'); }
|
|
1332
|
+
if (m.kind === 'steward') S.steward.serial++;
|
|
1333
|
+
S.modal = null; paintModal(); if (m.opener && m.opener.focus) { try { m.opener.focus({ preventScroll: true }); } catch (_e) {} }
|
|
1334
|
+
}
|
|
1335
|
+
function modalHtml() {
|
|
1336
|
+
var m = S.modal; if (!m) return '';
|
|
1337
|
+
var body = m.kind === 'copy' ? copyBody() : m.kind === 'log' ? logBody() : m.kind === 'settings' ? settingsBody() : m.kind === 'steward' ? stewardBody() :
|
|
1338
|
+
m.kind === 'passages' ? passagesBody(m) : m.kind === 'merge' ? mergeBody() : '';
|
|
1339
|
+
var title = m.kind === 'copy' ? 'Copy context' : m.kind === 'log' ? 'Recent learning activity' : m.kind === 'settings' ? 'Memory settings' : m.kind === 'steward' ? 'Rules, sources and outcome evidence' :
|
|
1340
|
+
m.kind === 'passages' ? 'Passages about ' + m.id : m.kind === 'merge' && S.merge ? 'Merge ' + S.merge.source + ' into ' + S.merge.target : '';
|
|
1341
|
+
return '<div class="mm-backdrop' + (m.entered ? '' : ' mm-enter') + '" data-mm="modal-backdrop"><section class="mm-modal" role="dialog" aria-modal="true" aria-labelledby="mm-modal-title">' +
|
|
1342
|
+
'<div class="mm-modal-head"><h2 id="mm-modal-title">' + esc(title) + '</h2><button type="button" class="cx-btn quiet" data-mm="modal-close">Close</button></div>' +
|
|
1343
|
+
'<div class="mm-modal-body">' + body + '</div></section></div>';
|
|
1344
|
+
}
|
|
1345
|
+
// Copy context: the exact text, quoted and labelled with its id (the data-not-instructions contract the glasses use).
|
|
1346
|
+
function contextText(e, o) {
|
|
1347
|
+
var x = fullRow(e), d = x.detail || {};
|
|
1348
|
+
var lines = ['Learning ' + x.event_id + ' (' + kindLabel(x) + ' · ' + (x.scope || '') + ' · ' + (x.ts || '') + ')', '', '"""', x.title || ''];
|
|
1349
|
+
if (d.before) lines.push('Before: ' + d.before); if (d.after) lines.push('After: ' + d.after); if (d.rule) lines.push('Rule: ' + d.rule);
|
|
1350
|
+
if (d.content) lines.push(d.content); (d.bodies || []).forEach(function (b) { lines.push(b); });
|
|
1351
|
+
if (isMemoryLesson(x) && memText(x.lesson_id) && !d.content) lines.push(S.memDetail[x.lesson_id].content || S.memDetail[x.lesson_id].summary);
|
|
1352
|
+
lines.push('"""');
|
|
1353
|
+
if (o.sources && (x.source_refs || []).length) { lines.push('', 'Sources:'); x.source_refs.forEach(function (r) { lines.push('- ' + (r.kind || '') + ' ' + (r.id || '') + (r.excerpt ? ': ' + r.excerpt : '')); }); }
|
|
1354
|
+
var oc = outcomeText(x); if (oc) lines.push('', 'Checked: ' + oc);
|
|
1355
|
+
if (o.graph) lines.push('', 'Related knowledge: open the entity in Knowledge and use its Copy context for descriptions and relationships.');
|
|
1356
|
+
return lines.join('\n');
|
|
1357
|
+
}
|
|
1358
|
+
function copyBody() {
|
|
1359
|
+
var e = findRow(S.copyId); if (!e) return '<p>This item is no longer in the list.</p>';
|
|
1360
|
+
return '<p>Check the exact text, then copy it. Sources are included unless you turn them off.</p>' +
|
|
1361
|
+
'<div class="mm-checkrow"><label class="cx-check"><input type="checkbox" data-mm-change="copy-opt" data-arg="sources"' + (S.copyOpts.sources ? ' checked' : '') + '> Include sources</label>' +
|
|
1362
|
+
'<label class="cx-check"><input type="checkbox" data-mm-change="copy-opt" data-arg="graph"' + (S.copyOpts.graph ? ' checked' : '') + '> Include a related-knowledge note</label></div>' +
|
|
1363
|
+
'<textarea class="field mm-preview" id="mm-copy-preview" readonly aria-label="Text to copy">' + esc(contextText(e, S.copyOpts)) + '</textarea>' +
|
|
1364
|
+
'<div class="mm-actions">' + btn('copy-now', 'Copy', { cls: 'primary' }) + btn('copy-select', 'Select text') + '</div>' +
|
|
1365
|
+
'<p class="cx-note">Quoted and labelled with its id, so whatever reads it treats it as data, not instructions.</p>';
|
|
1366
|
+
}
|
|
1367
|
+
function logBody() {
|
|
1368
|
+
var ls = S.recent.info || {}, counts = ls.counts_by_type || {}, cap = 10, A = S.applied;
|
|
1369
|
+
if (!A.loaded && !A.loading) loadApplied(false);
|
|
1370
|
+
var used = A.rows, total = Math.max(A.total != null ? Number(A.total) : 0, used.length), label = A.days === 90 ? 'in 90 days' : 'this week';
|
|
1371
|
+
var usedRows = used.slice(0, cap).map(function (e) { return '<li><b>' + esc(clip(memText(e.lesson_id) || (S.loading['m:' + e.lesson_id] ? 'Reading the lesson…' : 'Lesson'), 90)) + '</b><span class="mm-muted">' + esc(clip(appliedExcerpt(e), 110) || 'used by a later answer') + '</span></li>'; }).join('');
|
|
1372
|
+
var stores = Object.keys(ls.stores || {}).map(function (k) { var st = ls.stores[k]; return '<li><b>' + esc(storeName(k)) + '</b><span class="mm-muted">' + (st.readable || st.state === 'legacy' ? 'Readable · ' + fmt(st.count || 0) + (st.last_ts ? ' · last ' + esc(when(st.last_ts)) : '') : 'Not readable right now') + '</span></li>'; }).join('');
|
|
1373
|
+
return '<p>' + esc(ls.notice || ('Snapshot from ' + when(ls.generated_at) + '. Window: ' + when(ls.window_start) + ' to ' + when(ls.window_end) + '. ' + (ls.complete ? 'Every store answered.' : 'Some stores answered only in part.'))) + '</p>' +
|
|
1374
|
+
'<h3 class="mm-sub-title">Applied ' + label + '</h3>' + (!A.loaded ? '<p class="cx-note">Reading which lessons were used…</p>' : used.length ? '<ul class="mm-log">' + usedRows + '</ul>' + (total > cap ? '<p class="mm-muted">and ' + fmt(total - cap) + ' more in Applied ' + label + '.</p>' : '') : '<p class="mm-muted">' + esc(appliedEmpty().h) + '</p>') +
|
|
1375
|
+
'<h3 class="mm-sub-title">Event counts</h3>' + (Object.keys(counts).length ? '<ul class="mm-log">' + Object.keys(counts).sort().map(function (k) { return '<li><b>' + esc(KIND[k] || k) + '</b><span class="mm-muted">' + fmt(counts[k]) + ' in the window</span></li>'; }).join('') + '</ul>' : '<p class="mm-muted">No events counted.</p>') +
|
|
1376
|
+
(stores ? '<h3 class="mm-sub-title">Where they come from</h3><ul class="mm-log">' + stores + '</ul>' : '');
|
|
1377
|
+
}
|
|
1378
|
+
function loadSettings() { loadPolicy(); loadGuard(); loadOwner(); }
|
|
1379
|
+
function loadOwner() {
|
|
1380
|
+
if (S.ownerBusy) return; var serial = nextSerial('owner'); S.ownerError = '';
|
|
1381
|
+
call('profile.owner', {}).then(function (d) { if (serial !== serials.owner) return; S.owner = d; if (S.drafts.owner == null) S.drafts.owner = d.owner_name || ''; },
|
|
1382
|
+
function (e) { if (serial !== serials.owner) return; S.ownerError = e.message; }).then(function () { paintModal(); });
|
|
1383
|
+
}
|
|
1384
|
+
function loadPolicy() {
|
|
1385
|
+
if (S.policyLoading || S.policyBusy) return; S.policyLoading = true; var serial = nextSerial('policy');
|
|
1386
|
+
call('workspace.request', { action: 'policy_get' }).then(function (d) { if (serial !== serials.policy) return; S.policy = d; S.policyError = ''; },
|
|
1387
|
+
function (e) { if (serial !== serials.policy) return; S.policyError = e.message; }).then(function () { S.policyLoading = false; paintModal(); });
|
|
1388
|
+
}
|
|
1389
|
+
function loadGuard() {
|
|
1390
|
+
if (S.guardLoading) return; S.guardLoading = true;
|
|
1391
|
+
call('memory.guardrails').then(function (d) { S.guard = d.guardrails; S.guardLines = d.philosophy_rubric_lines; S.guardError = ''; },
|
|
1392
|
+
function (e) { S.guardError = e.message; }).then(function () { S.guardLoading = false; paintModal(); });
|
|
1393
|
+
}
|
|
1394
|
+
function readOnlyWords(st) { return st === 'adoption_required' || st === 'not_initialized' ? 'the memory workspace on your Mac is not set up yet.' : 'your Mac did not allow changes right now.'; }
|
|
1395
|
+
function settingsBody() {
|
|
1396
|
+
var s = S.status || {}, d = S.policy, p = d && d.policy;
|
|
1397
|
+
var name = S.drafts.owner != null ? S.drafts.owner : (S.owner && S.owner.owner_name) || '';
|
|
1398
|
+
var owner = '<section class="mm-set"><h3 class="mm-sub-title">Who is COS for?</h3><p class="mm-muted">Your name personalizes COS and where the graph starts. It does not change which Mac indexes or who owns the memory.</p>' +
|
|
1399
|
+
'<form class="mm-inline" data-mm-form="owner"><input class="field" id="mm-owner" data-draft="owner" aria-label="Your name" placeholder="Your full name" maxlength="120" value="' + esc(name) + '">' +
|
|
1400
|
+
'<button type="submit" class="cx-btn"' + (!S.owner || S.ownerBusy ? ' disabled' : '') + '>' + (S.ownerBusy ? 'Saving…' : 'Save name') + '</button></form>' +
|
|
1401
|
+
(S.ownerError ? '<p class="mm-bad">' + esc(S.ownerError) + '</p>' + btn('owner-retry', 'Try again') : '') + '</section>';
|
|
1402
|
+
function row(key, title, sub) {
|
|
1403
|
+
var on = !!(p && p[key]), dis = !d || !d.writable || S.policyBusy || !!S.policyIntent;
|
|
1404
|
+
return '<div class="cx-set-row"><span class="cx-set-text"><span class="cx-set-name">' + esc(title) + '</span><span class="cx-set-desc">' + esc(sub) + '</span></span>' +
|
|
1405
|
+
'<button type="button" class="switch" role="switch" aria-checked="' + on + '" aria-label="' + esc(title) + '" data-mm="policy" data-arg="' + key + '"' + (dis ? ' disabled' : '') + '></button></div>';
|
|
1406
|
+
}
|
|
1407
|
+
var policy = p ? '<div class="cx-list">' + row('capture_enabled', 'Capture completed sessions', 'Pausing affects new automatic captures. What COS already knows stays readable.') +
|
|
1408
|
+
row('review_required', 'Review new memories before recall', 'New memories wait for your acceptance. Accepted memories stay active.') + '</div>' +
|
|
1409
|
+
(d && !d.writable ? '<p class="mm-muted">Read-only: ' + esc(readOnlyWords(d.state)) + '</p>' : '<p class="mm-muted">Only the owner can change these. Skill changes need a reviewed, checked version.</p>') :
|
|
1410
|
+
S.policyError ? '' : '<p class="cx-note">Reading your memory settings…</p>';
|
|
1411
|
+
if (S.policyError) policy += '<p class="mm-bad">' + esc(S.policyError) + '</p>' + btn(S.policyIntent ? 'policy-retry' : 'policy-load', 'Try again');
|
|
1412
|
+
var tier = s.contextScriptsDirectory ? 'Advanced search (memory store and files)' : s.contextFilesDirectory ? 'Plain files' : 'Not set up';
|
|
1413
|
+
return owner + '<section class="mm-set"><h3 class="mm-sub-title">Capture and recall</h3><p class="mm-muted">Recall treats source text as evidence, never as instructions.</p>' + policy + '</section>' +
|
|
1414
|
+
'<section class="mm-set"><div class="mm-actions">' + btn('steward', 'Rules, current sources and outcome evidence') + '</div>' +
|
|
1415
|
+
'<p class="mm-muted">Where memory is stored: ' + esc(tier) + '.</p></section>' + guardHtml();
|
|
1416
|
+
}
|
|
1417
|
+
function guardHtml() {
|
|
1418
|
+
var g = S.guard, busy = S.guardBusy, dr = S.drafts;
|
|
1419
|
+
if (S.guardError) return '<section class="mm-set"><h3 class="mm-sub-title">Guardrails</h3><p class="mm-bad">' + esc(S.guardError) + '</p>' + btn('guard-load', 'Try again') + '</section>';
|
|
1420
|
+
if (!g) return '<section class="mm-set"><h3 class="mm-sub-title">Guardrails</h3><p class="cx-note">Reading your guardrails…</p></section>';
|
|
1421
|
+
var llm = g.llm_review || {}, run = S.guardRun, v = run ? run.verdicts || [] : [];
|
|
1422
|
+
var flagged = v.filter(function (x) { return x.verdict === 'prune'; }), review = v.filter(function (x) { return x.verdict === 'review'; });
|
|
1423
|
+
function val(k, fallback) { return dr['g.' + k] != null ? dr['g.' + k] : fallback; }
|
|
1424
|
+
function num(k, label, min, max, stepv, value) { return '<label class="mm-pick"><span>' + label + '</span><input class="field mm-num" type="number" id="mm-g-' + k + '" data-draft="g.' + k + '" min="' + min + '" max="' + max + '"' + (stepv ? ' step="' + stepv + '"' : '') + ' value="' + esc(val(k, value)) + '" inputmode="decimal"></label>'; }
|
|
1425
|
+
return '<section class="mm-set"><h3 class="mm-sub-title">Guardrails</h3><p class="mm-muted">What a captured memory must be to stay. The same rules refuse nonsense when it is captured; a run applies them to what already landed.</p>' +
|
|
1426
|
+
'<div class="mm-inline mm-wrap">' + num('min_words', 'Fewest words', 0, 200, '', g.min_words) + num('min_distinct_chars', 'Fewest different letters', 0, 100, '', g.min_distinct_chars) + num('max_repeat_ratio', 'Most repetition', 0.1, 1, 0.05, g.max_repeat_ratio) + '</div>' +
|
|
1427
|
+
'<label class="mm-pick mm-block"><span>Banned patterns, one per line (regular expressions)</span><textarea class="field" id="mm-g-banned" data-draft="g.banned" rows="3">' + esc(val('banned', (g.banned_patterns || []).join('\n'))) + '</textarea></label>' +
|
|
1428
|
+
(llm.supported === false ? '<p class="mm-muted">This setup applies fixed admission rules. A model review is not set up.</p>' :
|
|
1429
|
+
'<label class="cx-check mm-block"><input type="checkbox" id="mm-g-llm" data-draft="g.llm"' + ((dr['g.llm'] != null ? dr['g.llm'] : llm.enabled) ? ' checked' : '') + '> Also check against the COS philosophy with a model (' + fmt(S.guardLines || 0) + ' principles)</label>' +
|
|
1430
|
+
'<div class="mm-inline mm-wrap"><label class="mm-pick"><span>Model</span><select class="field" id="mm-g-model" data-draft="g.model">' + ['haiku', 'sonnet', 'opus'].map(function (m) { return '<option value="' + m + '"' + (val('model', llm.model) === m ? ' selected' : '') + '>' + ({ haiku: 'Fast (Haiku)', sonnet: 'Balanced (Sonnet)', opus: 'Deep (Opus)' })[m] + '</option>'; }).join('') + '</select></label>' +
|
|
1431
|
+
num('max', 'At most per run', 1, 100, '', llm.max_per_run || 20) + '</div><p class="mm-muted">The model check makes one bounded request per memory it reviews and stops at the cap. Off by default.</p>') +
|
|
1432
|
+
'<div class="mm-actions">' + btn('guard-save', busy === 'save' ? 'Saving…' : 'Save guardrails', { disabled: !!busy, cls: 'primary' }) + btn('guard-run', busy === 'run' ? 'Reviewing…' : 'Review captured memories now', { disabled: !!busy }) +
|
|
1433
|
+
(flagged.length && run && !run.run.applied ? btn('guard-apply', busy === 'apply' ? 'Quarantining…' : S.armed === 'guard-apply' ? 'Tap again to quarantine matches' : 'Review the next batch and quarantine matches', { disabled: !!busy }) : '') + '</div>' +
|
|
1434
|
+
(run ? '<p class="mm-muted">Last run: ' + fmt(run.run.scanned) + ' checked in ' + fmt(run.run.days) + ' days, ' + fmt(run.run.flagged) + ' flagged, ' + fmt(run.run.kept) + ' kept' + (run.run.review ? ', ' + fmt(run.run.review) + ' for you' : '') + (run.run.llm_reviewed ? ', ' + fmt(run.run.llm_reviewed) + ' judged by the model' : '') + (run.run.applied ? ', ' + fmt(run.run.pruned) + ' quarantined' : '') + (run.run.coverage ? ', ' + fmt(run.run.coverage.remaining_after_page) + ' left in this pass' : '') + '.</p>' +
|
|
1435
|
+
'<ul class="mm-log">' + flagged.concat(review).slice(0, 60).map(function (x) { return '<li><b class="' + (x.verdict === 'prune' ? 'mm-bad' : '') + '">' + (x.verdict === 'prune' ? (x.applied ? 'Quarantined' : 'Would quarantine') : 'For you') + '</b> <span>' + esc(clip(x.excerpt || x.id, 160)) + '</span><span class="mm-muted">' + esc((x.reasons || []).join('; ')) + (x.by === 'model' ? ' (model)' : '') + '</span></li>'; }).join('') + '</ul>' : '') + '</section>';
|
|
1436
|
+
}
|
|
1437
|
+
// Stewardship (memories/memory-stewardship.js, ported): standing rules, current sources and outcome evidence.
|
|
1438
|
+
function stewardLoad(more) {
|
|
1439
|
+
var T = S.steward; if (T.busy) return;
|
|
1440
|
+
T.busy = true; T.error = ''; var serial = ++T.serial;
|
|
1441
|
+
var action = T.tab === 'rules' ? 'rule_page' : T.tab === 'sources' ? 'source_status' : 'trace_summary', body = { action: action };
|
|
1442
|
+
if (action === 'rule_page') { body.limit = 25; body.cursor = more ? T.cursor : null; }
|
|
1443
|
+
paintModal();
|
|
1444
|
+
call('workspace.request', body).then(function (d) {
|
|
1445
|
+
if (serial !== T.serial) return;
|
|
1446
|
+
if (action === 'trace_summary') T.outcomes = d; else { T.rows = more ? T.rows.concat(d.items || []) : d.items || []; T.cursor = d.next_cursor || null; }
|
|
1447
|
+
}, function (e) { if (serial !== T.serial) return; T.error = e.message; }).then(function () { if (serial === T.serial) T.busy = false; paintModal(); });
|
|
1448
|
+
}
|
|
1449
|
+
function stewardMutate(body) {
|
|
1450
|
+
var T = S.steward; if (T.busy) return;
|
|
1451
|
+
if (T.intent && body !== T.intent) { T.error = 'Retry the pending action before starting another.'; paintModal(); return; }
|
|
1452
|
+
if (!T.intent) T.intent = JSON.parse(JSON.stringify(body));
|
|
1453
|
+
T.busy = true; T.error = ''; paintModal();
|
|
1454
|
+
call('workspace.request', T.intent).then(function (d) { T.busy = false; T.intent = null; W.toast(d.status ? 'Saved: ' + d.status : 'Saved'); stewardLoad(false); },
|
|
1455
|
+
function (e) {
|
|
1456
|
+
T.busy = false; var c = codeOf(e);
|
|
1457
|
+
T.error = /evaluation_not_found/.test(c) ? 'No passing check was found for this id. The rule stays in shadow. Check the result and enter a valid id.' :
|
|
1458
|
+
/evaluation_unreadable/.test(c) ? 'This check could not be verified. The rule stays in shadow. Run the check again before retrying.' : e.message;
|
|
1459
|
+
if (/revision_conflict|idempotency_conflict|invalid_|rule_not_|evaluation_|owner_review_required|not_memory_authority/.test(c)) T.intent = null;
|
|
1460
|
+
paintModal();
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
function stewardBody() {
|
|
1464
|
+
var T = S.steward, dis = T.busy || !!T.intent, dr = S.drafts;
|
|
1465
|
+
function sbtn(act, label, id) { return btn(act, label, { id: id, disabled: dis && act !== 'steward-retry' }); }
|
|
1466
|
+
var tabs = '<div class="cx-switch mm-tabs" role="group" aria-label="Stewardship section">' + [['rules', 'Standing rules'], ['sources', 'Current sources'], ['outcomes', 'Outcome evidence']].map(function (t) {
|
|
1467
|
+
return '<button type="button" data-mm="steward-tab" data-arg="' + t[0] + '" aria-pressed="' + (T.tab === t[0]) + '"' + (T.intent ? ' disabled' : '') + '>' + t[1] + '</button>'; }).join('') + '</div>';
|
|
1468
|
+
var body = '<p class="mm-muted">Evidence and your decisions govern changes. A saved rule stays in shadow until you review it and an independent check passes.</p>' + tabs;
|
|
1469
|
+
if (T.error) body += '<p class="mm-bad">' + esc(T.error) + '</p>' + (T.intent ? btn('steward-retry', 'Retry the same action') : btn('steward-reload', 'Reload'));
|
|
1470
|
+
if (T.busy) body += '<p class="cx-note">Working…</p>';
|
|
1471
|
+
if (T.tab === 'rules') {
|
|
1472
|
+
body += T.rows.length ? T.rows.map(function (r) {
|
|
1473
|
+
var p = r.payload || {};
|
|
1474
|
+
return '<section class="cx-card mm-card"><h4>' + esc(p.text) + '</h4><p class="mm-muted">' + esc(r.state) + ' · revision ' + esc(r.revision) + ' · ' + esc(p.review) + '</p><p>Applies when: ' + esc(p.trigger || 'not given') + '</p><p>Exclusions: ' + esc((p.exclusions || []).join('; ') || 'none recorded') + '</p><p class="mm-muted">Evidence: ' + esc((p.evidence_refs || []).join(', ')) + '</p><div class="mm-actions">' +
|
|
1475
|
+
(r.state === 'candidate' && p.review !== 'approved_by_owner' ? sbtn('steward-review', 'Review this version', r.id) : '') + (r.state === 'candidate' && p.review === 'approved_by_owner' ? sbtn('steward-activate', 'Check the evaluation and turn it on', r.id) : '') + (r.state === 'active' ? sbtn('steward-rollback', 'Roll back this rule', r.id) : '') + '</div></section>';
|
|
1476
|
+
}).join('') : (!T.busy ? '<p>No standing-rule versions recorded.</p>' : '');
|
|
1477
|
+
if (T.cursor) body += sbtn('steward-more', 'Load more');
|
|
1478
|
+
body += '<section class="mm-set"><h4>Propose a rule</h4><label class="mm-pick mm-block"><span>The behavior you want</span><textarea class="field" id="mm-st-text" data-draft="st.text" rows="3">' + esc(dr['st.text'] || '') + '</textarea></label>' +
|
|
1479
|
+
'<label class="mm-pick mm-block"><span>When it applies</span><input class="field" id="mm-st-trigger" data-draft="st.trigger" value="' + esc(dr['st.trigger'] || '') + '"></label>' +
|
|
1480
|
+
'<label class="mm-pick mm-block"><span>Exclusions, one per line</span><textarea class="field" id="mm-st-excl" data-draft="st.excl" rows="2">' + esc(dr['st.excl'] || '') + '</textarea></label>' +
|
|
1481
|
+
'<label class="mm-pick mm-block"><span>Source or memory ids, separated by commas</span><input class="field" id="mm-st-evidence" data-draft="st.evidence" value="' + esc(dr['st.evidence'] || '') + '"></label>' + sbtn('steward-propose', 'Save as a shadow proposal') + '</section>' +
|
|
1482
|
+
'<section class="mm-set"><label class="mm-pick mm-block"><span>Independent check id</span><input class="field" id="mm-st-eval" data-draft="st.eval" value="' + esc(dr['st.eval'] || '') + '"></label><p class="mm-muted">The check must cover this exact revision, when it applies, its exclusions, counterexamples and regressions. A rule cannot turn on without a verified check.</p></section>';
|
|
1483
|
+
} else if (T.tab === 'sources') {
|
|
1484
|
+
body += T.rows.length ? T.rows.map(function (r) { return '<section class="cx-card mm-card"><h4>' + esc(r.title || r.id) + '</h4><p class="mm-muted">Revision ' + esc(r.revision) + ' · ' + esc(when(r.captured_at)) + ' · ' + esc(r.projection) + '</p>' + sbtn('steward-refresh', 'Refresh this document', r.id) + '</section>'; }).join('') :
|
|
1485
|
+
(!T.busy ? '<p>No granted documents right now. The owner grants documents on the Mac.</p>' : '');
|
|
1486
|
+
} else {
|
|
1487
|
+
var d = T.outcomes;
|
|
1488
|
+
body += d ? '<dl class="mm-grid"><dt>Observed runs</dt><dd>' + fmt(d.unique_runs || 0) + '</dd><dt>Checked runs that count</dt><dd>' + fmt(d.checked_eligible_runs || 0) + '</dd><dt>Not conclusive</dt><dd>' + fmt(d.inconclusive_runs || 0) + '</dd><dt>Days observed</dt><dd>' + fmt(d.observation_days || 0) + '</dd><dt>Evidence bar</dt><dd>' + (d.evidence_gate_met ? 'Met' : 'Not met yet') + (d.rate_limit_reason ? '. ' + esc(d.rate_limit_reason) : '') + '</dd></dl>' +
|
|
1489
|
+
'<details><summary>Show the raw numbers</summary><pre class="mm-pre">' + esc(JSON.stringify(d, null, 2)) + '</pre></details>' : '';
|
|
1490
|
+
body += '<p class="mm-muted">Being recalled, being included in context, a model saying it applied something and an independently checked result are four different things. Too little evidence does not mean things improved.</p>';
|
|
1491
|
+
}
|
|
1492
|
+
return body;
|
|
1493
|
+
}
|
|
1494
|
+
function stewardAct(act, id) {
|
|
1495
|
+
var T = S.steward, dr = S.drafts;
|
|
1496
|
+
if (act === 'steward-retry') return stewardMutate(T.intent);
|
|
1497
|
+
if (act === 'steward-tab') { if (T.intent) return; T.tab = id; T.rows = []; T.cursor = null; T.outcomes = null; return stewardLoad(false); }
|
|
1498
|
+
if (act === 'steward-reload') return stewardLoad(false);
|
|
1499
|
+
if (act === 'steward-more') return stewardLoad(true);
|
|
1500
|
+
if (act === 'steward-propose') {
|
|
1501
|
+
var text = str(dr['st.text']).trim(); if (!text) { T.error = 'Write the behavior you want first.'; paintModal(); return; }
|
|
1502
|
+
return stewardMutate({ action: 'propose_rule', payload: { lesson_id: 'owner_proposal', text: text, trigger: str(dr['st.trigger']).trim(),
|
|
1503
|
+
exclusions: str(dr['st.excl']).split('\n').map(function (x) { return x.trim(); }).filter(Boolean), evidence_refs: str(dr['st.evidence']).split(',').map(function (x) { return x.trim(); }).filter(Boolean) } });
|
|
1504
|
+
}
|
|
1505
|
+
var row = T.rows.filter(function (r) { return r.id === id; })[0]; if (!row) return;
|
|
1506
|
+
var body = { action: act === 'steward-refresh' ? 'refresh_source' : act === 'steward-review' ? 'review_rule' : act === 'steward-rollback' ? 'rollback_rule' : 'activate_rule', id: id, expected_revision: row.revision, idempotency_key: 'steward_' + uuid() };
|
|
1507
|
+
if (act === 'steward-activate') { body.evaluation_id = str(dr['st.eval']).trim(); if (!body.evaluation_id) { T.error = 'Enter the independent check id for this reviewed revision.'; paintModal(); return; } }
|
|
1508
|
+
stewardMutate(body);
|
|
1509
|
+
}
|
|
1510
|
+
function openPassages(id, d) { openModal('passages', { id: id, data: d }); }
|
|
1511
|
+
function passagesBody(m) {
|
|
1512
|
+
var d = m.data || {}, items = d.items || [];
|
|
1513
|
+
return (d.note ? '<p>' + esc(d.note) + '</p>' : '') + (items.length ? '<ul class="mm-log">' + items.map(function (it) { var s = it.source || {}; return '<li><b>' + esc(s.title || 'Passage') + (s.date ? ' · ' + esc(s.date) : '') + (s.status && s.status !== 'resolved' ? ' · not linked to a file' : '') + '</b><span>' + esc(it.excerpt) + '</span></li>'; }).join('') + '</ul>' : '<p>No passages behind this in the index.</p>') +
|
|
1514
|
+
'<p class="mm-muted">' + fmt(d.total || 0) + ' ' + plural(d.total, 'passage', 'passages') + ' in the index.</p>';
|
|
1515
|
+
}
|
|
1516
|
+
// Merge (0.5.205): the explorer's comparison is step one, the owner Mac's preview step two, and the receipt is polled
|
|
1517
|
+
// until it settles. The promise keeps the explorer's row pending and undoes it on a refusal or failure.
|
|
1518
|
+
function mergeFlow(source, target, rule) {
|
|
1519
|
+
return new Promise(function (resolve) {
|
|
1520
|
+
if (S.merge && (S.merge.phase === 'running' || S.merge.phase === 'starting')) { W.toast('A merge is already running. Wait for its receipt.'); resolve(false); return; }
|
|
1521
|
+
var m = S.merge = { source: source, target: target, rule: rule || null, preview: null, phase: 'loading', receipt: null, logTail: [], armed: false, resolve: resolve, startedAt: 0 };
|
|
1522
|
+
openModal('merge');
|
|
1523
|
+
call('graph.merge.preview', { source: source, target: target }).then(function (pv) {
|
|
1524
|
+
if (S.merge !== m) return;
|
|
1525
|
+
m.preview = pv;
|
|
1526
|
+
if (pv.blocked) return finishMerge(false, pv.block_reason || 'Your Mac refused this merge.');
|
|
1527
|
+
if (!pv.source || !pv.source.found || !pv.target || !pv.target.found) return finishMerge(false, 'No entity named ' + (pv.source && pv.source.found ? target : source) + ' in this Mac\'s index. Rebuild the index and try again.');
|
|
1528
|
+
m.phase = 'preview'; paintModal();
|
|
1529
|
+
call('workspace.request', { action: 'identity_status', source: source, target: target }).then(function (d) { if (S.merge === m) { m.identity = d; paintModal(); } },
|
|
1530
|
+
function (e) { if (S.merge === m) { m.identityError = e.message; paintModal(); } });
|
|
1531
|
+
}, function (e) { if (S.merge === m) finishMerge(false, e.message); });
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function finishMerge(ok, message) {
|
|
1535
|
+
var m = S.merge; if (!m) return;
|
|
1536
|
+
S.merge = null;
|
|
1537
|
+
if (S.modal && S.modal.kind === 'merge') { S.modal = null; paintModal(); }
|
|
1538
|
+
if (message) W.toast(message);
|
|
1539
|
+
if (m.resolve) m.resolve(ok);
|
|
1540
|
+
}
|
|
1541
|
+
function mergeCol(en, keep) {
|
|
1542
|
+
var descs = (en.descriptions && en.descriptions.length) ? en.descriptions.map(function (d) { return '<p>' + esc(d) + '</p>'; }).join('') : '<p class="mm-muted">No description extracted.</p>';
|
|
1543
|
+
return '<div class="mm-merge-col' + (keep ? ' keep' : '') + '"><h4>' + esc(en.id) + '</h4><p class="mm-muted">' + (keep ? 'Stays · ' : 'Folds in · ') + esc(en.type || 'entity') + ' · ' + fmt(en.degree || 0) + ' ' + plural(en.degree, 'connection', 'connections') + '</p>' + descs + '</div>';
|
|
1544
|
+
}
|
|
1545
|
+
var MERGE_STEPS = [['starting', 'Starting the worker'], ['locking', 'Waiting for other indexing to finish'], ['staging', 'Preparing a separate copy of the graph'], ['snapshot', 'Setting a copy of the graph aside'],
|
|
1546
|
+
['merging', 'Merging (re-embeds the merged text)'], ['indexing', 'Rebuilding this Mac\'s index'], ['exporting', 'Refreshing the graph export']];
|
|
1547
|
+
function mergeSteps(stepName) {
|
|
1548
|
+
var at = Math.max(0, MERGE_STEPS.map(function (s) { return s[0]; }).indexOf(stepName || 'starting'));
|
|
1549
|
+
return '<ol class="mm-merge-steps">' + MERGE_STEPS.map(function (s, i) { return '<li class="' + (i < at ? 'done' : i === at ? 'now' : '') + '">' + esc(s[1]) + '</li>'; }).join('') + '</ol>';
|
|
1550
|
+
}
|
|
1551
|
+
function mergeBody() {
|
|
1552
|
+
var m = S.merge; if (!m) return '';
|
|
1553
|
+
if (m.phase === 'loading') return '<p class="cx-note">Asking this Mac what the merge would do…</p>';
|
|
1554
|
+
var pv = m.preview || {}, eff = pv.effect || {}, r = m.receipt || {};
|
|
1555
|
+
if (m.phase === 'preview' || m.phase === 'starting') {
|
|
1556
|
+
var big = (pv.warnings || []).some(function (w) { return w.code === 'large_merge'; }), off = (pv.warnings || []).some(function (w) { return w.code === 'physical_merge_unavailable'; });
|
|
1557
|
+
var mins = eff.estimated_seconds ? Math.max(1, Math.round(eff.estimated_seconds / 60)) : null, starting = m.phase === 'starting';
|
|
1558
|
+
return '<p>Read both before merging. ' + (pv.name_signal ? '<span class="cx-cap ok">' + esc(pv.name_signal) + '</span>' : '<span class="cx-cap">No name similarity</span>') + '</p>' +
|
|
1559
|
+
'<div class="mm-merge">' + mergeCol(pv.target, true) + mergeCol(pv.source, false) + '</div>' +
|
|
1560
|
+
'<p class="mm-muted">' + (pv.shared_count ? 'Shared neighbors: ' + esc((pv.shared_neighbors || []).join(', ')) + (pv.shared_count > (pv.shared_neighbors || []).length ? ' and ' + fmt(pv.shared_count - pv.shared_neighbors.length) + ' more' : '') : 'No shared neighbors.') + (pv.adjacent ? ' They are directly connected; that connection folds away.' : '') + '</p>' +
|
|
1561
|
+
'<p><b>What would change:</b> ' + fmt(eff.moved || 0) + ' ' + plural(eff.moved, 'connection moves', 'connections move') + ' to ' + esc(m.target) + ', ' + fmt(eff.collapsed || 0) + ' fold into ones it already has, ' + fmt(eff.embeddings || 0) + ' ' + plural(eff.embeddings, 'text', 'texts') + ' re-embedded' + (mins ? ', about ' + mins + ' ' + plural(mins, 'minute', 'minutes') : '') + '. Your original documents stay as they are. A graph-only copy does not restore every affected store.</p>' +
|
|
1562
|
+
(pv.warnings || []).map(function (w) { return '<p class="' + (w.code === 'large_merge' || w.code === 'nothing_links_them' ? 'mm-bad' : 'mm-muted') + '">' + esc(w.text) + '</p>'; }).join('') +
|
|
1563
|
+
(m.rule ? '<p class="mm-muted">Rule saved with the receipt: ' + esc(m.rule.scope || '') + ' "' + esc(m.rule.pattern || '') + '" becomes "' + esc(m.rule.replacement || '') + '".</p>' : '') +
|
|
1564
|
+
(m.identity && m.identity.writable && !m.identity.same_identity ? '<p class="mm-muted">If these are different people or ideas, keep them separate so they are never merged later.</p><div class="mm-actions">' + btn('keep-apart', 'Keep these separate', { disabled: starting }) + '</div>' : '') +
|
|
1565
|
+
(m.identityError ? '<p class="mm-muted">Identity rules: ' + esc(m.identityError) + '</p>' : '') +
|
|
1566
|
+
'<div class="mm-actions">' + btn('merge-cancel', 'Cancel', { disabled: starting }) + btn('merge-go', off ? 'Merge unavailable' : starting ? 'Starting…' : big && !m.armed ? 'Merge ' + fmt(pv.source.degree) + ' connections…' : m.armed ? 'Yes, merge ' + fmt(pv.source.degree) + ' connections' : 'Merge on this Mac', { disabled: starting || off, cls: 'primary' }) + '</div>' +
|
|
1567
|
+
(big && m.armed ? '<p class="mm-bad">Tap again to confirm. This re-embeds ' + fmt(eff.embeddings || 0) + ' texts and rewrites the search stores.</p>' : '');
|
|
1568
|
+
}
|
|
1569
|
+
if (m.phase === 'running') {
|
|
1570
|
+
var secs = m.startedAt ? Math.round((Date.now() - m.startedAt) / 1000) : 0;
|
|
1571
|
+
return '<p>Running on this Mac · ' + secs + ' s' + (eff.estimated_seconds ? ' of about ' + eff.estimated_seconds : '') + '</p>' + mergeSteps(r.step) +
|
|
1572
|
+
(m.logTail.length ? '<pre class="mm-pre">' + esc(m.logTail.slice(-4).join('\n')) + '</pre>' : '') +
|
|
1573
|
+
'<p class="mm-muted">It runs in the background; you can close this and come back. The graph reloads around ' + esc(m.target) + ' when it is done.</p><div class="mm-actions">' + btn('modal-close', 'Close') + '</div>';
|
|
1574
|
+
}
|
|
1575
|
+
if (m.phase === 'failed') return '<p class="mm-bad">' + esc(r.error || 'The merge did not finish.') + '</p>' + mergeSteps(r.step) + (r.snapshot ? '<p class="mm-muted">The copy of the graph from before the merge is intact.</p>' : '') + '<div class="mm-actions">' + btn('merge-cancel', 'Close') + '</div>';
|
|
1576
|
+
return '';
|
|
1577
|
+
}
|
|
1578
|
+
function pollMerge() {
|
|
1579
|
+
var m = S.merge; if (!m || m.phase !== 'running') return;
|
|
1580
|
+
setTimeout(function () {
|
|
1581
|
+
if (S.merge !== m || m.phase !== 'running') return;
|
|
1582
|
+
call('graph.merge.status').then(function (d) {
|
|
1583
|
+
if (S.merge !== m) return;
|
|
1584
|
+
m.receipt = d.receipt || m.receipt; m.logTail = d.log_tail || [];
|
|
1585
|
+
var st = m.receipt && m.receipt.state;
|
|
1586
|
+
if (st === 'done') return mergeDone();
|
|
1587
|
+
if (st === 'failed' || (!d.running && st !== 'done')) { m.phase = 'failed'; if (!S.modal || S.modal.kind !== 'merge') W.toast('Merging ' + m.source + ' did not finish: ' + ((m.receipt && m.receipt.error) || 'no detail')); paintModal(); return; }
|
|
1588
|
+
paintModal(); pollMerge();
|
|
1589
|
+
}, function () { paintModal(); pollMerge(); });
|
|
1590
|
+
}, 3000);
|
|
1591
|
+
}
|
|
1592
|
+
function mergeDone() {
|
|
1593
|
+
var m = S.merge, r = m.receipt || {}, after = r.after || {};
|
|
1594
|
+
finishMerge(true, 'Merged ' + m.source + ' into ' + m.target + (after.target != null ? ' · ' + fmt(after.target) + ' connections now' : '') + '.');
|
|
1595
|
+
S.focus = m.target; S.focusChosen = true; S.graphMemories = {};
|
|
1596
|
+
loadGraph(); if (S.dups.data) scanDups();
|
|
1597
|
+
}
|
|
1598
|
+
function scanDups() {
|
|
1599
|
+
if (S.dups.busy) return;
|
|
1600
|
+
S.dups = { busy: true, data: null, error: '' }; paint();
|
|
1601
|
+
call('graph.duplicates', { limit: 25 }).then(function (d) { S.dups = { busy: false, data: d, error: '' }; }, function (e) { S.dups = { busy: false, data: null, error: e.message }; }).then(paint);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ── the graph explorer (memories/graph-explorer.js and memory-workspace.js, loaded as they are) ──
|
|
1605
|
+
function graphAssets() { try { return JSON.parse((el('memories-graph-assets') || {}).textContent || '{}'); } catch (_e) { return {}; } }
|
|
1606
|
+
function loadScript(src) {
|
|
1607
|
+
return new Promise(function (resolve, reject) {
|
|
1608
|
+
var s = document.createElement('script'); s.src = src; s.async = false;
|
|
1609
|
+
s.onload = function () { resolve(); };
|
|
1610
|
+
s.onerror = function () { if (s.remove) s.remove(); reject(new Error('Could not load the graph explorer. Check the connection to your Mac.')); };
|
|
1611
|
+
document.body.appendChild(s);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function loadKit() {
|
|
1615
|
+
if (window.COSGraphExplorer && window.COSMemoryWorkspace) return Promise.resolve();
|
|
1616
|
+
if (S.kit) return S.kit;
|
|
1617
|
+
var a = graphAssets();
|
|
1618
|
+
if (!a.d3 || !a.explorer || !a.workspace) return Promise.reject(new Error('This page does not list the graph explorer files.'));
|
|
1619
|
+
if (a.css && !document.querySelector('link[data-mm-graph]')) { var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = a.css; l.setAttribute('data-mm-graph', '1'); document.head.appendChild(l); }
|
|
1620
|
+
// The workspace names its drafts with crypto.randomUUID, which a plain-http tailnet page does not have.
|
|
1621
|
+
if (window.crypto && typeof window.crypto.randomUUID !== 'function') { try { Object.defineProperty(window.crypto, 'randomUUID', { value: uuid, configurable: true }); } catch (_e) {} }
|
|
1622
|
+
S.kit = (window.d3 ? Promise.resolve() : loadScript(a.d3)).then(function () { return window.COSGraphExplorer ? null : loadScript(a.explorer); })
|
|
1623
|
+
.then(function () { return window.COSMemoryWorkspace ? null : loadScript(a.workspace); })
|
|
1624
|
+
.then(function () { if (!window.COSGraphExplorer || !window.COSMemoryWorkspace) throw new Error('The graph explorer did not start.'); }, function (e) { S.kit = null; throw e; });
|
|
1625
|
+
return S.kit;
|
|
1626
|
+
}
|
|
1627
|
+
function memoriesFor(id) {
|
|
1628
|
+
var hits = S.graphMemories[id];
|
|
1629
|
+
if (hits === undefined) {
|
|
1630
|
+
S.graphMemories[id] = null;
|
|
1631
|
+
call('memories.search', { q: id, limit: 5 }).then(function (d) {
|
|
1632
|
+
S.graphMemories[id] = (d.hits || []).map(function (h) { return { id: h.id, title: h.summary || h.content || h.id }; });
|
|
1633
|
+
var c = S.wsController; if (c && c.select) c.select(id);
|
|
1634
|
+
}, function () { S.graphMemories[id] = []; });
|
|
1635
|
+
}
|
|
1636
|
+
return hits || [];
|
|
1637
|
+
}
|
|
1638
|
+
function mountGraph() {
|
|
1639
|
+
var host = el('mm-graph-host'); if (!host) return;
|
|
1640
|
+
if (S.ws) { if (host.firstChild && !host.querySelector('.memory-investigation')) host.innerHTML = ''; S.ws.attach(host, S.focus); return; }
|
|
1641
|
+
if (S.kitError) return;
|
|
1642
|
+
loadKit().then(function () {
|
|
1643
|
+
var h = el('mm-graph-host'); if (!h) return;
|
|
1644
|
+
if (!S.ws) {
|
|
1645
|
+
var src = (S.graph && S.graph.source) || {};
|
|
1646
|
+
S.ws = window.COSMemoryWorkspace.create({ call: call, toast: W.toast, openPassages: openPassages,
|
|
1647
|
+
onUserIntent: function () { S.focusChosen = true; },
|
|
1648
|
+
onController: function (c) {
|
|
1649
|
+
S.wsController = c;
|
|
1650
|
+
try {
|
|
1651
|
+
var snap = c.snapshot(); S.graphVisible = snap.nodes.length;
|
|
1652
|
+
var f = S.focus;
|
|
1653
|
+
S.neighbors = (snap.links || []).filter(function (l) { var a = typeof l.source === 'object' ? l.source.id : l.source, b = typeof l.target === 'object' ? l.target.id : l.target; return f && (a === f || b === f); })
|
|
1654
|
+
.sort(function (x, y) { return (y.weight || 0) - (x.weight || 0); }).map(function (l) { var a = typeof l.source === 'object' ? l.source.id : l.source, b = typeof l.target === 'object' ? l.target.id : l.target; return a === f ? b : a; })
|
|
1655
|
+
.filter(function (v, i, arr) { return v !== f && arr.indexOf(v) === i; }).slice(0, 6);
|
|
1656
|
+
} catch (_e) {}
|
|
1657
|
+
paintSoon();
|
|
1658
|
+
},
|
|
1659
|
+
graphOptions: {
|
|
1660
|
+
labels: { graphName: fileGraph() ? 'Your document links' : 'Your knowledge graph' },
|
|
1661
|
+
onCopy: function (text, label) { call('copy', { text: text, label: label }).then(function () { W.toast('Copied as grounded context'); }, function (e) { W.toast(e.message); }); },
|
|
1662
|
+
memoriesFor: memoriesFor,
|
|
1663
|
+
onOpenMemory: function (memoryId) { W.go('memories', ['m', memoryId]); },
|
|
1664
|
+
curation: { enabled: !fileGraph(), ownerLabel: hostLabel(src.owner_host) || 'the owner Mac', isOwner: !!src.is_owner },
|
|
1665
|
+
onCuration: function (change) { if (change.op !== 'merge') { W.toast('Rename and remove are not available. Nothing was changed.'); return false; } return mergeFlow(change.source, change.target, change.rule && change.rule.pattern ? change.rule : null); }
|
|
1666
|
+
} });
|
|
1667
|
+
}
|
|
1668
|
+
h.innerHTML = ''; S.ws.attach(h, S.focus); applyPending();
|
|
1669
|
+
}, function (e) { S.kitError = e.message; paint(); });
|
|
1670
|
+
}
|
|
1671
|
+
// A question answered while the graph was not on screen is shown in it once the explorer is free.
|
|
1672
|
+
function applyPending() {
|
|
1673
|
+
var inv = S.ask.pending; if (!inv || !S.ws) return;
|
|
1674
|
+
var tries = 0;
|
|
1675
|
+
(function again() {
|
|
1676
|
+
if (S.ask.pending !== inv || !S.ws) return;
|
|
1677
|
+
if (S.ws.applyQuestion(inv, S.ws.questionToken())) { S.ask.pending = null; S.ask.message = 'The graph now shows the points and connections chosen for your question.'; paint(); return; }
|
|
1678
|
+
if (++tries < 30) setTimeout(again, 500);
|
|
1679
|
+
})();
|
|
1680
|
+
}
|
|
1681
|
+
// The workspace's Find points button and Enter in its search box run this (the pane's click and keydown handlers).
|
|
1682
|
+
function graphSearch() {
|
|
1683
|
+
var input = el('graphQuery'), status = el('graphSearchStatus'), q = input ? String(input.value || '').trim() : '';
|
|
1684
|
+
if (q.length < 2) { if (status) status.textContent = 'Type at least two characters.'; return; }
|
|
1685
|
+
S.focusChosen = true; if (status) status.textContent = 'Looking up…';
|
|
1686
|
+
call('graph.search', { q: q, limit: 5 }).then(function (d) {
|
|
1687
|
+
var items = d.items || [];
|
|
1688
|
+
if (!items.length) { if (status) status.textContent = d.indexState === 'missing' ? 'No knowledge index on this Mac yet. Build it under Knowledge status.' : 'Nothing in the graph matches that.'; return; }
|
|
1689
|
+
if (S.ws) { S.ws.showMatches(items); if (status) status.textContent = items.length + ' ' + plural(items.length, 'match', 'matches') + '. Pick one below.'; }
|
|
1690
|
+
}, function (e) { if (status) status.textContent = e.message; });
|
|
1691
|
+
}
|
|
1692
|
+
function explore(eventId) {
|
|
1693
|
+
var e = findRow(eventId); if (!e) return;
|
|
1694
|
+
S.knowledgeFocus = eventId; S.view = 'knowledge'; S.ktab = 'graph'; S.ktabChosen = true; persist();
|
|
1695
|
+
W.go('memories', ['k', 'graph']);
|
|
1696
|
+
var term = exploreTerm(e);
|
|
1697
|
+
call('graph.search', { q: term, limit: 3 }).then(function (d) {
|
|
1698
|
+
var items = d.items || [];
|
|
1699
|
+
if (items.length) { S.focus = items[0].id; S.focusChosen = true; if (S.ws && el('mm-graph-host')) S.ws.attach(el('mm-graph-host'), S.focus); paint(); }
|
|
1700
|
+
else W.toast('Nothing in the graph matches "' + term + '".' + (S.focus ? ' Showing ' + S.focus + '.' : ''));
|
|
1701
|
+
}, function (er) { W.toast(er.message); });
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ── asking the graph ─────────────────────────────────────
|
|
1705
|
+
var askTimer = null, askSerial = 0;
|
|
1706
|
+
function ask(question) {
|
|
1707
|
+
var a = S.ask; if (a.busy) return;
|
|
1708
|
+
var input = el('mm-ask-q'), q = String(typeof question === 'string' ? question : input ? input.value : a.q).trim();
|
|
1709
|
+
if (q.length < 3) { W.toast('Ask a fuller question.'); return; }
|
|
1710
|
+
if (q.length > 400) { W.toast('Keep the question under 400 characters.'); return; }
|
|
1711
|
+
var serial = ++askSerial, ws = S.ws, token = ws && ws.questionToken();
|
|
1712
|
+
S.focusChosen = true;
|
|
1713
|
+
S.ask = { q: q, busy: true, answer: null, error: '', cards: [], cardsBusy: false, startedAt: Date.now(), message: '', pending: null };
|
|
1714
|
+
clearInterval(askTimer); askTimer = setInterval(tickAsk, 1000); paint();
|
|
1715
|
+
call('graph.ask', { q: q }).then(function (d) {
|
|
1716
|
+
if (serial !== askSerial) return;
|
|
1717
|
+
var inv = d.investigation, message = inv && inv.message, pending = null;
|
|
1718
|
+
if (inv && inv.status === 'ready') {
|
|
1719
|
+
if (ws && S.ws === ws && el('mm-graph-host')) message = ws.applyQuestion(inv, token) ? 'The graph now shows the points and connections chosen for your question.' : 'Your graph edits were kept. Ask again when you want the view updated.';
|
|
1720
|
+
else { pending = inv; message = 'Open Ask the graph to see these points in the graph.'; }
|
|
1721
|
+
} else if (!message) message = 'This answer has no checked graph plan. Use Advanced investigation under the graph to choose points.';
|
|
1722
|
+
S.ask = { q: q, busy: false, answer: d, error: '', cards: [], cardsBusy: false, startedAt: 0, message: message, pending: pending };
|
|
1723
|
+
clearInterval(askTimer); paint(); loadCards(d.answer || '', q, serial);
|
|
1724
|
+
}, function (e) {
|
|
1725
|
+
if (serial !== askSerial) return;
|
|
1726
|
+
S.ask = { q: q, busy: false, answer: null, error: e.message, cards: [], cardsBusy: false, startedAt: 0, message: '', pending: null };
|
|
1727
|
+
clearInterval(askTimer); paint();
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
function tickAsk() {
|
|
1731
|
+
if (!S.ask.busy) { clearInterval(askTimer); return; }
|
|
1732
|
+
var s = askSecs(), p = document.querySelector('[data-role="ask-progress"]'), m = document.querySelector('[data-role="ask-meta"]');
|
|
1733
|
+
if (p) p.textContent = askBusyCopy(s); if (m) m.textContent = 'Working · ' + s + ' s';
|
|
1734
|
+
}
|
|
1735
|
+
// The people and ideas an answer names, as cards: headings and bold names are looked up; an exact id wins, then a person.
|
|
1736
|
+
function askNames(answer, q) {
|
|
1737
|
+
var names = [];
|
|
1738
|
+
function add(n) { n = String(n || '').replace(/\s*\(.*?\)\s*$/, '').replace(/[*_#`]/g, '').trim(); if (n.length >= 3 && n.length <= 60 && /[A-Za-z]/.test(n) && names.indexOf(n) < 0) names.push(n); }
|
|
1739
|
+
(answer.match(/^#{1,4}\s+(.+)$/gm) || []).forEach(function (h) { add(h.replace(/^#+\s+/, '').replace(/^(Relationship|Relationships|References|Key Context|Summary|Overview)\s*:?\s*/i, '')); });
|
|
1740
|
+
(answer.match(/\*\*([^*]{3,60})\*\*/g) || []).forEach(function (b) { var n = b.replace(/\*\*/g, ''); if (/^[A-Z][\w'.-]*(\s+[A-Z][\w'.-]*){0,3}$/.test(n)) add(n); });
|
|
1741
|
+
var m = q.match(/between\s+(.+?)\s+and\s+(.+?)\??$/i); if (m) { add(m[1]); add(m[2]); }
|
|
1742
|
+
return names.filter(function (n) { return !/^(Primary Relationship|Role|Current Focus|Completion|Birthday|Children|Attribute|Detail|Regular syncs|Family management|Shared parenthood)$/i.test(n); }).slice(0, 8);
|
|
1743
|
+
}
|
|
1744
|
+
function loadCards(answer, q, serial) {
|
|
1745
|
+
var names = askNames(answer, q), seen = {};
|
|
1746
|
+
S.ask.cardsBusy = names.length > 0; paint();
|
|
1747
|
+
Promise.all(names.map(function (n) {
|
|
1748
|
+
return call('graph.search', { q: n, limit: 3 }).then(function (d) {
|
|
1749
|
+
var items = d.items || [];
|
|
1750
|
+
var hit = items.filter(function (it) { return String(it.id).toLowerCase() === n.toLowerCase(); })[0] || items.filter(function (it) { return String(it.type || '').toLowerCase() === 'person' && String(it.id).toLowerCase().indexOf(n.toLowerCase()) === 0; })[0];
|
|
1751
|
+
if (!hit || seen[hit.id]) return null; seen[hit.id] = 1;
|
|
1752
|
+
return call('graph.entity', { id: hit.id, limit: 6 }).then(function (en) { return en.found === false ? null : en; }, function () { return null; });
|
|
1753
|
+
}, function () { return null; });
|
|
1754
|
+
})).then(function (ents) { if (serial !== askSerial) return; S.ask.cards = ents.filter(Boolean); S.ask.cardsBusy = false; paint(); });
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// ── long runs: indexing, model fetch, index build ────────
|
|
1758
|
+
var watch = { ingest: null, fetch: null, build: null };
|
|
1759
|
+
function watchIngest() {
|
|
1760
|
+
if (watch.ingest) return;
|
|
1761
|
+
var before = S.graph && S.graph.queue ? Number(S.graph.queue.pending) : null, ticks = 0;
|
|
1762
|
+
function finish() {
|
|
1763
|
+
clearInterval(watch.ingest); watch.ingest = null;
|
|
1764
|
+
var pr = S.progress || {};
|
|
1765
|
+
loadGraph().then(function () {
|
|
1766
|
+
var g = S.graph || {}, after = g.queue ? Number(g.queue.pending) : null;
|
|
1767
|
+
var done = pr.done ? fmt(pr.done) + ' indexed' + (pr.failed ? ', ' + fmt(pr.failed) + ' failed' : '') : before != null && after != null && before > after ? fmt(before - after) + ' indexed' : 'Indexing finished';
|
|
1768
|
+
S.ingest = { note: done + (after != null ? ' · ' + fmt(after) + ' still waiting' : '') };
|
|
1769
|
+
if (S.setup) loadSetup();
|
|
1770
|
+
setTimeout(function () { S.ingest = null; paint(); }, 60000);
|
|
1771
|
+
paint();
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
function tick() {
|
|
1775
|
+
ticks++;
|
|
1776
|
+
var reads = [call('graph.progress').then(function (d) { S.progress = d; }, function () {})];
|
|
1777
|
+
if (ticks % 3 === 0) reads.push(loadGraph());
|
|
1778
|
+
Promise.all(reads).then(function () {
|
|
1779
|
+
var pr = S.progress || {}, lk = pr.lock || (S.graph || {}).lock || {};
|
|
1780
|
+
if (!(pr.running === true || lockHeld(lk)) || ticks > 2880) return finish();
|
|
1781
|
+
paint();
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
watch.ingest = setInterval(tick, 5000); tick();
|
|
1785
|
+
}
|
|
1786
|
+
function watchFetch() {
|
|
1787
|
+
if (watch.fetch) return;
|
|
1788
|
+
var ticks = 0;
|
|
1789
|
+
watch.fetch = setInterval(function () {
|
|
1790
|
+
ticks++;
|
|
1791
|
+
loadSetup().then(function () {
|
|
1792
|
+
var f = S.setup && S.setup.embedding && S.setup.embedding.fetch;
|
|
1793
|
+
if (!f || f.state !== 'running' || ticks > 360) { clearInterval(watch.fetch); watch.fetch = null; if (f && f.state === 'done') W.toast('Model fetched.'); else if (f && f.state === 'failed') W.toast('Fetch failed: ' + (f.error || 'unknown')); }
|
|
1794
|
+
});
|
|
1795
|
+
}, 10000);
|
|
1796
|
+
}
|
|
1797
|
+
function stopWatches() { clearInterval(watch.ingest); watch.ingest = null; clearInterval(watch.fetch); watch.fetch = null; clearInterval(watch.build); watch.build = null; clearInterval(askTimer); }
|
|
1798
|
+
|
|
1799
|
+
// ── decisions (writes, each from a tap) ──────────────────
|
|
1800
|
+
function arm(key) { S.armed = key; paint(); setTimeout(function () { if (S.armed === key) { S.armed = ''; paint(); } }, 6000); }
|
|
1801
|
+
function afterWrite() { loadStatus(); }
|
|
1802
|
+
function reviewMemory(id, decision) {
|
|
1803
|
+
if (S.reviewBusy) return;
|
|
1804
|
+
if (decision === 'prune' && S.armed !== 'prune:' + id) return arm('prune:' + id);
|
|
1805
|
+
S.armed = ''; S.reviewBusy = id; paint();
|
|
1806
|
+
// Restore is Accept on the wire: the bridge returns a quarantined memory to recall before marking it accepted.
|
|
1807
|
+
call('memory.review', { id: id, decision: decision === 'restore' ? 'accept' : decision }).then(function (d) {
|
|
1808
|
+
var row = d.decision || {};
|
|
1809
|
+
S.decisions[id] = row.decision || (decision === 'prune' ? 'pruned' : 'accepted'); S.decisionTs[id] = new Date().toISOString(); S.decided[id] = 1;
|
|
1810
|
+
if (decision === 'prune') { S.mem.rows = S.mem.rows.filter(function (m) { return m.id !== id; }); }
|
|
1811
|
+
W.toast(decision === 'prune' ? 'Memory quarantined.' : decision === 'restore' ? 'Memory restored to recall.' : 'Memory accepted.');
|
|
1812
|
+
afterWrite();
|
|
1813
|
+
}, function (e) { W.toast(e.message); }).then(function () { S.reviewBusy = ''; paint(); });
|
|
1814
|
+
}
|
|
1815
|
+
function reviewGoverned(id, decision) {
|
|
1816
|
+
if (S.reviewBusy) return;
|
|
1817
|
+
var row = S.review.rows.concat(S.recent.rows).filter(function (e) { return e.lesson_id === id && e.governance; })[0];
|
|
1818
|
+
if (row && S.detail[row.event_id] && S.detail[row.event_id].journal_revision) row = Object.assign({}, row, S.detail[row.event_id]);
|
|
1819
|
+
if (!row) { W.toast('Refresh To review before changing this memory.'); return; }
|
|
1820
|
+
if (decision === 'forget' && S.armed !== 'forget:' + id) return arm('forget:' + id);
|
|
1821
|
+
var intent = S.reviewIntent;
|
|
1822
|
+
if (!intent) intent = S.reviewIntent = { action: 'review_memory', id: id, decision: decision, expected_revision: row.journal_revision, idempotency_key: 'review_' + uuid() };
|
|
1823
|
+
if (intent.id !== id || intent.decision !== decision) { W.toast('Retry the pending review action before starting another.'); return; }
|
|
1824
|
+
S.reviewBusy = id; paint();
|
|
1825
|
+
call('workspace.request', intent).then(function (d) {
|
|
1826
|
+
S.reviewIntent = null; S.armed = ''; S.detail = {}; S.memDetail = {}; S.decided[id] = 1;
|
|
1827
|
+
W.toast(d.status === 'deletion_pending_projection' ? 'Forgotten content is hidden; cleanup is still running.' : decision === 'forget' ? 'Searchable memory removed. Outside sources and protected backups remain.' : 'Memory ' + (d.state || d.status || 'updated') + '.');
|
|
1828
|
+
loadReview(false); loadRecent(false); if (S.mem.loaded) loadMem(false); afterWrite();
|
|
1829
|
+
}, function (e) {
|
|
1830
|
+
if (/revision_conflict|idempotency_conflict|memory_not_found/.test(codeOf(e))) { S.reviewIntent = null; S.detail = {}; loadReview(false); loadRecent(false); }
|
|
1831
|
+
W.toast(e.message);
|
|
1832
|
+
}).then(function () { S.reviewBusy = ''; paint(); });
|
|
1833
|
+
}
|
|
1834
|
+
function decide(eventId, decision) {
|
|
1835
|
+
var e = findRow(eventId); if (!e) return;
|
|
1836
|
+
call('learning.decide', { id: e.lesson_id, decision: decision }).then(function () {
|
|
1837
|
+
S.decisions[e.lesson_id] = decision; if (decision === 'dismissed') S.decided[e.lesson_id] = 1; else delete S.decided[e.lesson_id];
|
|
1838
|
+
W.toast(decision === 'dismissed' ? 'Suggestion dismissed.' : 'Suggestion restored.');
|
|
1839
|
+
loadReview(false); afterWrite();
|
|
1840
|
+
}, function (er) { W.toast(er.message); });
|
|
1841
|
+
}
|
|
1842
|
+
function pruneAll() {
|
|
1843
|
+
if (S.bulk) return;
|
|
1844
|
+
var ids = S.review.rows.filter(function (e) { return isPruneReady(e) && !decisionFor(e); }).map(function (e) { return e.lesson_id; });
|
|
1845
|
+
if (!ids.length) return;
|
|
1846
|
+
if (S.armed !== 'prune-all') return arm('prune-all');
|
|
1847
|
+
S.armed = ''; S.bulk = { done: 0, total: ids.length }; paint();
|
|
1848
|
+
// One memory at a time through the same review call as the card's Quarantine; the first failure stops the run.
|
|
1849
|
+
ids.reduce(function (chain, id) {
|
|
1850
|
+
return chain.then(function () {
|
|
1851
|
+
return call('memory.review', { id: id, decision: 'prune', note: 'prune ready' }).then(function (d) {
|
|
1852
|
+
var row = d.decision || {}; S.decisions[id] = row.decision || 'pruned'; S.decisionTs[id] = new Date().toISOString(); S.decided[id] = 1; S.bulk.done += 1; paint();
|
|
1853
|
+
});
|
|
1854
|
+
});
|
|
1855
|
+
}, Promise.resolve()).then(function () { var n = S.bulk.done; S.bulk = null; W.toast('Quarantined ' + fmt(n) + '. Restore any of them from its card.'); afterWrite(); paint(); },
|
|
1856
|
+
function (e) { var b = S.bulk; S.bulk = null; W.toast('Stopped after ' + fmt(b.done) + ' of ' + fmt(b.total) + ': ' + e.message); paint(); });
|
|
1857
|
+
}
|
|
1858
|
+
function policySet(key) {
|
|
1859
|
+
if (S.policyBusy || !S.policy || !S.policy.policy) return;
|
|
1860
|
+
if (!S.policyIntent) { var changes = {}; changes[key] = !S.policy.policy[key]; S.policyIntent = { action: 'policy_set', changes: changes, expected_revision: S.policy.policy.revision, idempotency_key: 'policy_' + uuid() }; }
|
|
1861
|
+
serials.policy = (serials.policy || 0) + 1; S.policyLoading = false; S.policyBusy = true; S.policyError = ''; paintModal();
|
|
1862
|
+
call('workspace.request', S.policyIntent).then(function (d) { S.policy = { policy: d.policy, writable: true, state: 'available' }; S.policyIntent = null; W.toast('Memory settings saved.'); },
|
|
1863
|
+
function (e) {
|
|
1864
|
+
S.policyError = e.message;
|
|
1865
|
+
if (/revision_conflict|idempotency_conflict|unknown_policy|invalid_policy/.test(codeOf(e))) { S.policyIntent = null; S.policyError = 'These settings changed or the change was refused. Reload, check the current value, then choose again.'; }
|
|
1866
|
+
}).then(function () { S.policyBusy = false; paintModal(); });
|
|
1867
|
+
}
|
|
1868
|
+
function saveOwner() {
|
|
1869
|
+
if (S.ownerBusy || !S.owner) return;
|
|
1870
|
+
var name = str(S.drafts.owner).trim(); if (!name) { S.ownerError = 'Enter your name.'; paintModal(); return; }
|
|
1871
|
+
serials.owner = (serials.owner || 0) + 1; S.ownerBusy = true; S.ownerError = ''; paintModal();
|
|
1872
|
+
call('profile.owner.set', { name: name, expected: S.owner.owner_name || '' }).then(function (d) {
|
|
1873
|
+
S.owner = d; S.drafts.owner = d.owner_name; if (S.status) S.status.ownerName = d.owner_name; S.focusResolved = false; chooseDefaultFocus(); W.toast('Name saved.');
|
|
1874
|
+
}, function (e) { S.ownerError = e.message; }).then(function () { S.ownerBusy = false; paintModal(); });
|
|
1875
|
+
}
|
|
1876
|
+
function guardPatch() {
|
|
1877
|
+
var g = S.guard || {}, llm = g.llm_review || {}, dr = S.drafts;
|
|
1878
|
+
function pick(k, v) { return dr['g.' + k] != null ? dr['g.' + k] : v; }
|
|
1879
|
+
var patch = { min_words: Number(pick('min_words', g.min_words)), min_distinct_chars: Number(pick('min_distinct_chars', g.min_distinct_chars)), max_repeat_ratio: Number(pick('max_repeat_ratio', g.max_repeat_ratio)),
|
|
1880
|
+
banned_patterns: String(pick('banned', (g.banned_patterns || []).join('\n'))).split('\n').map(function (s) { return s.trim(); }).filter(Boolean),
|
|
1881
|
+
llm_review: { enabled: !!pick('llm', llm.enabled), model: pick('model', llm.model) || 'haiku', max_per_run: Number(pick('max', llm.max_per_run || 20)) || 20 } };
|
|
1882
|
+
if (llm.supported === false) delete patch.llm_review;
|
|
1883
|
+
return patch;
|
|
1884
|
+
}
|
|
1885
|
+
function guardSave() {
|
|
1886
|
+
if (S.guardBusy) return;
|
|
1887
|
+
S.guardBusy = 'save'; paintModal();
|
|
1888
|
+
call('memory.guardrails.set', { patch: JSON.stringify(guardPatch()) }).then(function (d) {
|
|
1889
|
+
S.guard = d.guardrails; Object.keys(S.drafts).forEach(function (k) { if (k.indexOf('g.') === 0) delete S.drafts[k]; }); W.toast('Guardrails saved.');
|
|
1890
|
+
}, function (e) { W.toast(e.message); }).then(function () { S.guardBusy = ''; paintModal(); });
|
|
1891
|
+
}
|
|
1892
|
+
function guardRun(apply) {
|
|
1893
|
+
if (S.guardBusy) return;
|
|
1894
|
+
if (apply && S.armed !== 'guard-apply') { S.armed = 'guard-apply'; paintModal(); return; }
|
|
1895
|
+
S.armed = ''; S.guardBusy = apply ? 'apply' : 'run'; paintModal();
|
|
1896
|
+
call('memory.guardrails.run', { days: 30, apply: apply === true }).then(function (d) {
|
|
1897
|
+
S.guardRun = d;
|
|
1898
|
+
if (apply) {
|
|
1899
|
+
var ids = (d.verdicts || []).filter(function (v) { return v.applied; }).map(function (v) { return v.id; });
|
|
1900
|
+
S.mem.rows = S.mem.rows.filter(function (m) { return ids.indexOf(m.id) < 0; }); ids.forEach(function (id) { S.decisions[id] = 'pruned'; S.decided[id] = 1; });
|
|
1901
|
+
W.toast('Quarantined ' + fmt(d.run.pruned) + '.'); afterWrite();
|
|
1902
|
+
} else W.toast(fmt(d.run.flagged) + ' flagged, ' + fmt(d.run.kept) + ' kept' + (d.run.review ? ', ' + fmt(d.run.review) + ' for you' : '') + '.');
|
|
1903
|
+
}, function (e) { W.toast(e.message); }).then(function () { S.guardBusy = ''; paintModal(); paint(); });
|
|
1904
|
+
}
|
|
1905
|
+
function copyMemory(id) {
|
|
1906
|
+
var rows = (S.mem.hits || []).concat(S.mem.rows), m = rows.filter(function (x) { return x.id === id; })[0] || {}, full = S.memDetail[id] || {}, r = Object.assign({}, m, full._error ? {} : full);
|
|
1907
|
+
var text = 'Memory ' + id + (r.type || r.created_at ? ' (' + [r.type, String(r.created_at || '').slice(0, 10)].filter(Boolean).join(' · ') + ')' : '') + '\n\n"""\n' + (r.content || r.summary || '') + '\n"""';
|
|
1908
|
+
call('copy', { text: text }).then(function () { W.toast('Copied as grounded context'); }, function (e) { W.toast(e.message); });
|
|
1909
|
+
}
|
|
1910
|
+
function copyAsk(withContext) {
|
|
1911
|
+
var a = S.ask; if (!a.answer) return;
|
|
1912
|
+
var text = 'Question: ' + a.q + '\n\nAnswer (put together from the COS knowledge graph, ' + (a.answer.mode || 'hybrid') + ' mode):\n' + a.answer.answer;
|
|
1913
|
+
if (withContext && a.cards.length) text += '\n\nIn the graph:\n\n' + a.cards.map(entityText).join('\n\n');
|
|
1914
|
+
call('copy', { text: text }).then(function () { W.toast(withContext ? 'Copied the answer with its context.' : 'Copied the answer.'); }, function (e) { W.toast(e.message); });
|
|
1915
|
+
}
|
|
1916
|
+
function startIngest() {
|
|
1917
|
+
var sel = el('mm-ingest-limit'), limit = sel ? Number(sel.value) || 10 : S.ingestLimit; S.ingestLimit = limit;
|
|
1918
|
+
S.ingest = { note: 'Starting…' }; paint();
|
|
1919
|
+
call('graph.ingest', { limit: limit }).then(function (d) {
|
|
1920
|
+
if (d.started) { var batch = Math.min(d.limit || 0, d.pending || 0); S.ingest = { note: d.pending <= batch ? 'Indexing all ' + fmt(d.pending) + ' waiting' : 'Indexing the next ' + fmt(batch) + ' of ' + fmt(d.pending) + ' waiting' }; watchIngest(); }
|
|
1921
|
+
else if (d.already_running) { S.ingest = null; W.toast('Indexing is already running. This refreshes as it runs.'); watchIngest(); }
|
|
1922
|
+
else if (d.nothing_pending) { S.ingest = null; W.toast('Nothing is waiting.'); }
|
|
1923
|
+
else if (d.budget_exhausted) { S.ingest = null; W.toast('Today\'s model budget for the graph is used up' + (d.budget ? ' (' + fmt(d.budget.used) + ' of ' + fmt(d.budget.cap) + ' calls)' : '') + '. It resets tomorrow.'); }
|
|
1924
|
+
else { S.ingest = null; W.toast('Nothing started.'); }
|
|
1925
|
+
}, function (e) { S.ingest = null; W.toast(e.message); }).then(paint);
|
|
1926
|
+
}
|
|
1927
|
+
function buildIndex() {
|
|
1928
|
+
S.building = 'Starting…'; paint();
|
|
1929
|
+
call('graph.build').then(function (d) {
|
|
1930
|
+
S.building = d.already_running ? 'A build is already running' : 'Building…'; paint();
|
|
1931
|
+
var tries = 0; clearInterval(watch.build);
|
|
1932
|
+
watch.build = setInterval(function () {
|
|
1933
|
+
tries++;
|
|
1934
|
+
loadGraph().then(function () {
|
|
1935
|
+
var b = S.graph && S.graph.build;
|
|
1936
|
+
if (b && (b.state === 'done' || b.state === 'failed')) { S.building = b.state === 'done' ? 'Index rebuilt' : 'Build failed: ' + (b.error || ''); clearInterval(watch.build); watch.build = null; paint(); }
|
|
1937
|
+
else if (tries > 150) { S.building = 'Still building after 5 minutes. Refresh later.'; clearInterval(watch.build); watch.build = null; paint(); }
|
|
1938
|
+
});
|
|
1939
|
+
}, 2000);
|
|
1940
|
+
}, function (e) { S.building = ''; W.toast(e.message); paint(); });
|
|
1941
|
+
}
|
|
1942
|
+
function indexSample() {
|
|
1943
|
+
setupAction('sample', 'graph.sample', { limit: 3 }, function (d) {
|
|
1944
|
+
var q = (d.queued || []).length;
|
|
1945
|
+
if (d.started) { S.ingest = { note: 'Indexing ' + fmt(q) + ' sample ' + plural(q, 'document', 'documents') }; watchIngest(); }
|
|
1946
|
+
else if (d.already_running) { W.toast((q ? fmt(q) + ' queued. ' : '') + 'Indexing is already running; this refreshes as it runs.'); watchIngest(); }
|
|
1947
|
+
else if (d.nothing_pending) W.toast('Nothing new to index in the folders that are on.');
|
|
1948
|
+
else if (d.budget_exhausted) W.toast('Today\'s model budget for the graph is used up. It resets tomorrow.');
|
|
1949
|
+
if ((d.skipped || []).length) W.toast(fmt(d.skipped.length) + ' skipped: ' + d.skipped.map(function (x) { return x.reason; }).join(', '));
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
function addFolder() {
|
|
1953
|
+
var input = el('mm-folder'), path = String(input ? input.value : S.folderDraft).trim();
|
|
1954
|
+
if (!path) { W.toast('Type the folder\'s path on your Mac first.'); return; }
|
|
1955
|
+
setupAction('add', 'graph.setup.sources', { action: 'add', path: path }, function () { S.folderDraft = ''; W.toast('Folder added.'); });
|
|
1956
|
+
}
|
|
1957
|
+
function mergeGo() {
|
|
1958
|
+
var m = S.merge; if (!m || m.phase !== 'preview') return;
|
|
1959
|
+
var w = (m.preview || {}).warnings || [], off = w.filter(function (x) { return x.code === 'physical_merge_unavailable'; })[0];
|
|
1960
|
+
if (off) { W.toast(off.text); return; }
|
|
1961
|
+
if (w.some(function (x) { return x.code === 'large_merge'; }) && !m.armed) { m.armed = true; paintModal(); return; }
|
|
1962
|
+
m.phase = 'starting'; paintModal();
|
|
1963
|
+
call('graph.merge', { source: m.source, target: m.target, confirm: true, rule: m.rule || undefined }).then(function (d) {
|
|
1964
|
+
if (S.merge !== m) return;
|
|
1965
|
+
m.phase = 'running'; m.receipt = d.receipt || null; m.startedAt = Date.now(); paintModal(); pollMerge();
|
|
1966
|
+
}, function (e) { if (S.merge !== m) return; m.phase = 'preview'; m.armed = false; paintModal(); W.toast(e.message); });
|
|
1967
|
+
}
|
|
1968
|
+
function keepApart() {
|
|
1969
|
+
var m = S.merge; if (!m || m.phase !== 'preview' || !m.identity || !m.identity.writable) return;
|
|
1970
|
+
if (!m.constraintIntent) m.constraintIntent = { action: 'identity_keep_apart', source: m.source, target: m.target, expected_revision: m.identity.revision, idempotency_key: 'keep-apart:' + uuid() };
|
|
1971
|
+
m.phase = 'starting'; paintModal();
|
|
1972
|
+
call('workspace.request', m.constraintIntent).then(function () { if (S.merge === m) { finishMerge(false, 'Saved. These two will be kept separate.'); loadGraph(); } },
|
|
1973
|
+
function (e) {
|
|
1974
|
+
if (S.merge !== m) return; m.phase = 'preview'; paintModal(); W.toast(e.message);
|
|
1975
|
+
if (/revision_conflict|identity_split_required|idempotency_conflict/.test(codeOf(e))) { m.constraintIntent = null; call('workspace.request', { action: 'identity_status', source: m.source, target: m.target }).then(function (d) { if (S.merge === m) { m.identity = d; paintModal(); } }, function () {}); }
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// ── painting ─────────────────────────────────────────────
|
|
1980
|
+
var SKELETON = '<div class="cx mm" data-tint="memories" id="mm-root"><div id="mm-hero"></div><div class="mm-controls" id="mm-views"></div><p class="mm-counts" id="mm-counts"></p>' +
|
|
1981
|
+
'<div class="split mm-split" id="mm-split"><div class="split-list mm-list" id="mm-list-col"><div id="mm-tools"></div><div id="mm-list"></div></div><div class="reader mm-reader" id="mm-reader"></div></div>' +
|
|
1982
|
+
'<p class="mm-foot" id="mm-foot"></p></div><div id="mm-modal-host"></div>';
|
|
1983
|
+
function write(node, html) { if (!node || node.__mm === html) return false; node.innerHTML = html; node.__mm = html; return true; }
|
|
1984
|
+
var paintTimer = null;
|
|
1985
|
+
function paintSoon() { if (paintTimer) return; paintTimer = setTimeout(function () { paintTimer = null; paint(); }, 60); }
|
|
1986
|
+
function paint() {
|
|
1987
|
+
var r = W.parseRoute(); if (r.pane !== 'memories') return;
|
|
1988
|
+
if (!el('mm-root')) pane.innerHTML = SKELETON;
|
|
1989
|
+
var reading = readerArgs(r).length > 0, split = el('mm-split');
|
|
1990
|
+
pane.classList.toggle('reading', reading); if (split) split.classList.toggle('reading', reading);
|
|
1991
|
+
// The cue views are one column at every width, like Control's cue review (a card, not a list beside a reader).
|
|
1992
|
+
if (split) split.classList.toggle('solo', !reading && (S.view === 'cues' || S.view === 'rated'));
|
|
1993
|
+
write(el('mm-hero'), heroHtml()); write(el('mm-views'), viewsHtml()); write(el('mm-counts'), countsHtml()); write(el('mm-foot'), footHtml());
|
|
1994
|
+
var tools = el('mm-tools');
|
|
1995
|
+
if (tools && tools.__view !== S.view) { tools.innerHTML = S.view === 'memories' ? memSearchHtml() : ''; tools.__view = S.view; }
|
|
1996
|
+
write(el('mm-list'), listHtml());
|
|
1997
|
+
paintReader(r);
|
|
1998
|
+
paintModal();
|
|
1999
|
+
}
|
|
2000
|
+
function readerHtml(r) {
|
|
2001
|
+
var a = r.args; // a tab route (t/<view>) opens no reader: the auto-pick below decides, as on a bare list
|
|
2002
|
+
S.crumbText = '';
|
|
2003
|
+
if (a[0] === 'e' && a[1]) return eventReader(a[1]);
|
|
2004
|
+
if (a[0] === 'm' && a[1]) return memoryReader(a[1]);
|
|
2005
|
+
if (a[0] === 'k') return knowledgeReader(KTABS.indexOf(a[1]) >= 0 ? a[1] : 'graph');
|
|
2006
|
+
var pick = autoPick();
|
|
2007
|
+
if (pick) return S.view === 'knowledge' ? knowledgeReader(pick) : S.view === 'memories' ? memoryReader(pick) : eventReader(pick);
|
|
2008
|
+
return isWide() ? '<p class="reader-placeholder">Pick an item to read it.</p>' : '';
|
|
2009
|
+
}
|
|
2010
|
+
function paintReader(r) {
|
|
2011
|
+
var node = el('mm-reader'); if (!node) return;
|
|
2012
|
+
var html = readerHtml(r);
|
|
2013
|
+
if (readerArgs(r).length && W.crumb && W.crumb() !== S.crumbText) W.setCrumb(S.crumbText);
|
|
2014
|
+
if (write(node, html) && el('mm-graph-host')) mountGraph();
|
|
2015
|
+
document.body.classList.toggle('mm-graph-open', S.graphFull && !!el('mm-graph-host'));
|
|
2016
|
+
}
|
|
2017
|
+
function crumb(t) { S.crumbText = String(t || ''); }
|
|
2018
|
+
function paintModal(focusFirst) {
|
|
2019
|
+
var host = el('mm-modal-host'); if (!host) return;
|
|
2020
|
+
var a = document.activeElement, keepId = a && a.id && host.contains && host.contains(a) ? a.id : '', caret = null;
|
|
2021
|
+
if (keepId) { try { caret = [a.selectionStart, a.selectionEnd]; } catch (_e) {} }
|
|
2022
|
+
var body = host.querySelector ? host.querySelector('.mm-modal-body') : null, top = body ? body.scrollTop : 0;
|
|
2023
|
+
var changed = write(host, modalHtml());
|
|
2024
|
+
// The sheet fades in once, when it opens; a later repaint (a list that finished loading) must not fade it again.
|
|
2025
|
+
if (S.modal && !S.modal.entered) S.modal.entered = true;
|
|
2026
|
+
if (changed) {
|
|
2027
|
+
var b2 = host.querySelector ? host.querySelector('.mm-modal-body') : null; if (b2 && top) b2.scrollTop = top;
|
|
2028
|
+
if (keepId) { var n = el(keepId); if (n && n.focus) { n.focus({ preventScroll: true }); if (caret && caret[0] != null) { try { n.setSelectionRange(caret[0], caret[1]); } catch (_e) {} } } }
|
|
2029
|
+
else if (focusFirst && S.modal) { var f = host.querySelector && host.querySelector('.mm-modal-head button'); if (f && f.focus) f.focus(); }
|
|
2030
|
+
}
|
|
2031
|
+
document.body.classList.toggle('mm-modal-open', !!S.modal);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// ── taps, typing, keys ───────────────────────────────────
|
|
2035
|
+
function setView(v) {
|
|
2036
|
+
if (!knownView(v)) return;
|
|
2037
|
+
S.view = v; persist();
|
|
2038
|
+
if (v === 'memories' && !S.mem.loaded && !S.mem.loading) loadMem(false);
|
|
2039
|
+
if (v === 'applied' && !S.applied.loaded && !S.applied.loading) loadApplied(false);
|
|
2040
|
+
if (v === 'cues' && S.cue.weekState !== 'loaded') loadCueWeek();
|
|
2041
|
+
if (v === 'rated' && !S.cue.ratedLoaded) loadCueRated();
|
|
2042
|
+
showTab(v);
|
|
2043
|
+
}
|
|
2044
|
+
function refresh() {
|
|
2045
|
+
S.loadedAt = Date.now(); S.detail = {}; S.passages = {}; S.decided = {};
|
|
2046
|
+
loadStatus(); loadRecent(false); loadReview(false); loadCues(); loadGraph();
|
|
2047
|
+
if (S.cue.ratedLoaded || S.view === 'rated') loadCueRated();
|
|
2048
|
+
if (S.mem.loaded || S.view === 'memories') loadMem(false);
|
|
2049
|
+
if (S.applied.loaded || S.view === 'applied') loadApplied(false);
|
|
2050
|
+
if (S.setup) loadSetup();
|
|
2051
|
+
}
|
|
2052
|
+
function openKnowledge(tab, focusId) {
|
|
2053
|
+
if (focusId) S.knowledgeFocus = focusId;
|
|
2054
|
+
S.view = 'knowledge'; S.ktab = tab; S.ktabChosen = true; persist();
|
|
2055
|
+
W.go('memories', ['k', tab]);
|
|
2056
|
+
}
|
|
2057
|
+
function act(name, t, ev) {
|
|
2058
|
+
var id = t.getAttribute('data-id'), arg = t.getAttribute('data-arg');
|
|
2059
|
+
if (name.indexOf('steward-') === 0) return stewardAct(name, id || arg);
|
|
2060
|
+
if (name.indexOf('cue-') === 0) return cueAct(name, id, arg);
|
|
2061
|
+
switch (name) {
|
|
2062
|
+
case 'view': return setView(arg);
|
|
2063
|
+
case 'refresh': return refresh();
|
|
2064
|
+
case 'settings': openModal('settings', { opener: t }); return loadSettings();
|
|
2065
|
+
case 'log': return openModal('log', { opener: t });
|
|
2066
|
+
case 'open-e': return W.go('memories', ['e', id]);
|
|
2067
|
+
case 'open-m': return W.go('memories', ['m', id]);
|
|
2068
|
+
case 'ktab': return openKnowledge(KTABS.indexOf(arg) >= 0 ? arg : 'graph');
|
|
2069
|
+
case 'back': return W.goBack();
|
|
2070
|
+
case 'back-learning': S.view = 'recent'; persist(); return S.knowledgeFocus ? W.go('memories', ['e', S.knowledgeFocus]) : showTab('recent');
|
|
2071
|
+
case 'more-recent': return loadRecent(true);
|
|
2072
|
+
case 'more-review': return loadReview(true);
|
|
2073
|
+
case 'more-mem': return loadMem(true);
|
|
2074
|
+
case 'more-applied': return loadApplied(true);
|
|
2075
|
+
case 'mem-retry': return loadMem(false);
|
|
2076
|
+
case 'mem-clear': { var qi = el('mm-mem-q'); if (qi) qi.value = ''; S.mem.hits = null; S.mem.hitCursor = null; var tl = el('mm-tools'); if (tl) tl.__view = ''; memQuery(''); return paint(); }
|
|
2077
|
+
case 'applied-days': return setAppliedDays(Number(arg));
|
|
2078
|
+
case 'applied-retry': S.applied.error = ''; S.applied.loaded = false; return loadApplied(false);
|
|
2079
|
+
case 'open-sources': return openKnowledge('sources', id);
|
|
2080
|
+
case 'explore': return explore(id);
|
|
2081
|
+
case 'copy-open': S.copyId = id; return openModal('copy', { opener: t });
|
|
2082
|
+
case 'copy-now': { var ce = findRow(S.copyId); if (!ce) return; call('copy', { text: contextText(ce, S.copyOpts) }).then(function () { W.toast('Copied as grounded context'); closeModal(); }, function (er) { W.toast(er.message); }); return; }
|
|
2083
|
+
case 'copy-select': { var ta = el('mm-copy-preview'); if (ta) { ta.focus(); ta.select(); } return; }
|
|
2084
|
+
case 'copy-memory': return copyMemory(id);
|
|
2085
|
+
case 'memory-review': return reviewMemory(id, arg);
|
|
2086
|
+
case 'governed': return reviewGoverned(id, arg);
|
|
2087
|
+
case 'decide': return decide(id, arg);
|
|
2088
|
+
case 'prune-all': return pruneAll();
|
|
2089
|
+
case 'steward': openModal('steward', { opener: S.modal && S.modal.opener || t }); return stewardLoad(false);
|
|
2090
|
+
case 'modal-close': return closeModal();
|
|
2091
|
+
case 'modal-backdrop': if (!ev || ev.target === t) closeModal(); return;
|
|
2092
|
+
case 'policy': case 'policy-retry': return policySet(arg);
|
|
2093
|
+
case 'policy-load': return loadPolicy();
|
|
2094
|
+
case 'owner-retry': return loadOwner();
|
|
2095
|
+
case 'guard-load': return loadGuard();
|
|
2096
|
+
case 'guard-save': return guardSave();
|
|
2097
|
+
case 'guard-run': return guardRun(false);
|
|
2098
|
+
case 'guard-apply': return guardRun(true);
|
|
2099
|
+
case 'graph-full': S.graphFull = !S.graphFull; return paint();
|
|
2100
|
+
case 'graph-retry': S.kitError = ''; S.kit = null; var gh = el('mm-reader'); if (gh) gh.__mm = ''; return paint();
|
|
2101
|
+
case 'graph-status': return loadGraph();
|
|
2102
|
+
case 'ingest': return startIngest();
|
|
2103
|
+
case 'build-index': return buildIndex();
|
|
2104
|
+
case 'dups': return scanDups();
|
|
2105
|
+
case 'merge-from': return mergeFlow(id, arg, null);
|
|
2106
|
+
case 'merge-go': return mergeGo();
|
|
2107
|
+
case 'merge-cancel': return finishMerge(false, 'Nothing was changed.');
|
|
2108
|
+
case 'keep-apart': return keepApart();
|
|
2109
|
+
case 'ask-pair': return ask('Show me the relation between ' + S.focus + ' and ' + arg);
|
|
2110
|
+
case 'copy-ask': return copyAsk(arg === 'ctx');
|
|
2111
|
+
case 'copy-entity': { var en = S.ask.cards.filter(function (c) { return c.id === id; })[0]; if (en) call('copy', { text: entityText(en) }).then(function () { W.toast('Copied ' + id + ' as context.'); }, function (er) { W.toast(er.message); }); return; }
|
|
2112
|
+
case 'passages': return call('graph.passages', { entity: id, limit: 5 }).then(function (d) { openPassages(id, d); }, function (er) { W.toast(er.message); });
|
|
2113
|
+
case 'focus': S.focus = id; S.focusChosen = true; if (S.ws && el('mm-graph-host')) S.ws.attach(el('mm-graph-host'), id); return paint();
|
|
2114
|
+
case 'setup-refresh': return loadSetup();
|
|
2115
|
+
case 'source-toggle': { var src = ((S.setup && S.setup.sources) || []).filter(function (r) { return r.path === arg; })[0]; return setupAction('toggle', 'graph.setup.sources', { action: src && src.enabled ? 'disable' : 'enable', path: arg }); }
|
|
2116
|
+
case 'source-remove': if (S.armed !== 'remove:' + arg) return arm('remove:' + arg); return setupAction('remove', 'graph.setup.sources', { action: 'remove', path: arg }, function () { W.toast('Folder removed.'); });
|
|
2117
|
+
case 'claim-owner': if (S.armed !== 'owner') return arm('owner'); return setupAction('owner', 'graph.setup.owner', {}, function () { W.toast('This Mac now builds the graph.'); });
|
|
2118
|
+
case 'local-only': return setupAction('preference', 'graph.setup.embedding', { local_only: arg === '1' }, function (d) { var rec = (d.embedding || {}).recommended; W.toast(rec ? 'Recommended: ' + rec.label + '.' : 'Saved.'); });
|
|
2119
|
+
case 'fetch-model': return setupAction('fetch', 'graph.setup.embedding', { provider: arg, fetch: true }, function (d) { var f = d.fetch || {}; W.toast(f.started ? 'Fetching the model in the background.' : f.already_running ? 'A fetch is already running.' : 'Nothing to fetch.'); watchFetch(); });
|
|
2120
|
+
case 'index-sample': return indexSample();
|
|
2121
|
+
case 'schedule': { var sel = el('mm-schedule'), every = sel ? Number(sel.value) || 3600 : 3600; return setupAction('schedule', 'graph.schedule', { enabled: arg === 'on', intervalS: every }, function (d) { W.toast(d.installed ? 'Scheduled batches on.' : 'Scheduled batches off.'); }); }
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
pane.addEventListener('click', function (e) {
|
|
2125
|
+
var target = e.target;
|
|
2126
|
+
var t = target && target.closest ? target.closest('[data-mm]') : null;
|
|
2127
|
+
// The workspace's Find points button (memories/memory-workspace.js, data-role find-points) has no handler of its own.
|
|
2128
|
+
if (!t) { if (target && target.closest && target.closest('.memory-investigation [data-role="find-points"]')) graphSearch(); return; }
|
|
2129
|
+
if (t.disabled) return;
|
|
2130
|
+
act(t.getAttribute('data-mm'), t, e);
|
|
2131
|
+
});
|
|
2132
|
+
pane.addEventListener('change', function (e) {
|
|
2133
|
+
var t = e.target; if (!t || !t.getAttribute) return;
|
|
2134
|
+
var c = t.getAttribute('data-mm-change');
|
|
2135
|
+
if (c === 'tier') return setupAction('extraction', 'graph.setup.extraction', { tier: t.value }, function (d) { W.toast('Indexing set to ' + ((d.extraction || {}).label || t.value) + '.'); });
|
|
2136
|
+
if (c === 'embedding') return setupAction('embedding', 'graph.setup.embedding', { provider: t.value }, function (d) { W.toast('Embeddings set to ' + ((d.embedding || {}).label || t.value) + '.'); });
|
|
2137
|
+
if (c === 'ingest-limit') { S.ingestLimit = Number(t.value) || 10; return; }
|
|
2138
|
+
if (c === 'copy-opt') { S.copyOpts[t.getAttribute('data-arg')] = !!t.checked; var ce = findRow(S.copyId), ta = el('mm-copy-preview'); if (ce && ta) ta.value = contextText(ce, S.copyOpts); return; }
|
|
2139
|
+
var dk = t.getAttribute('data-draft'); if (dk) S.drafts[dk] = t.type === 'checkbox' ? !!t.checked : t.value;
|
|
2140
|
+
});
|
|
2141
|
+
pane.addEventListener('input', function (e) {
|
|
2142
|
+
var t = e.target; if (!t || !t.getAttribute) return;
|
|
2143
|
+
if (t.id === 'mm-mem-q') { memQuery(t.value); var clr = pane.querySelector('[data-mm="mem-clear"]'); if (!t.value && clr) { var tl = el('mm-tools'); if (tl) tl.__view = ''; } return; }
|
|
2144
|
+
if (t.id === 'mm-ask-q') { S.ask.q = t.value; return; }
|
|
2145
|
+
if (t.id === 'mm-cue-note') { S.cue.notes[t.getAttribute('data-cue')] = t.value; return; }
|
|
2146
|
+
if (t.id === 'mm-folder') { S.folderDraft = t.value; return; }
|
|
2147
|
+
var dk = t.getAttribute('data-draft'); if (dk) S.drafts[dk] = t.type === 'checkbox' ? !!t.checked : t.value;
|
|
2148
|
+
});
|
|
2149
|
+
pane.addEventListener('submit', function (e) {
|
|
2150
|
+
var f = e.target, k = f && f.getAttribute && f.getAttribute('data-mm-form'); if (!k) return;
|
|
2151
|
+
e.preventDefault();
|
|
2152
|
+
if (k === 'mem-search') { clearTimeout(memTimer); var qi = el('mm-mem-q'); if (qi) { S.mem.q = qi.value; if (qi.blur) qi.blur(); } var tl = el('mm-tools'); if (tl) tl.__view = ''; loadMem(false); }
|
|
2153
|
+
else if (k === 'ask') ask();
|
|
2154
|
+
else if (k === 'folder') addFolder();
|
|
2155
|
+
else if (k === 'owner') saveOwner();
|
|
2156
|
+
});
|
|
2157
|
+
// Enter in a multi-line field writes a new line here: the shell's desktop rule (Enter submits a textarea's form) is
|
|
2158
|
+
// for chat boxes, and these lists are one pattern or one exclusion per line.
|
|
2159
|
+
pane.addEventListener('keydown', function (e) {
|
|
2160
|
+
if (cueKey(e)) { e.preventDefault(); return; }
|
|
2161
|
+
if (e.target && e.target.id === 'mm-cue-note' && (e.key === 'Enter' || e.key === 'Escape')) { e.preventDefault(); e.stopPropagation(); cueKeyboard = true; var cc = el('mm-cue-card'); if (cc) cc.focus({ preventScroll: true }); return; }
|
|
2162
|
+
if (e.key === 'Enter' && e.target && e.target.tagName === 'TEXTAREA') e.stopPropagation();
|
|
2163
|
+
if (e.key === 'Enter' && e.target && e.target.id === 'graphQuery') { e.preventDefault(); graphSearch(); }
|
|
2164
|
+
});
|
|
2165
|
+
document.addEventListener('keydown', function (e) {
|
|
2166
|
+
if (e.key !== 'Escape' || W.parseRoute().pane !== 'memories') return;
|
|
2167
|
+
if (S.modal) { e.preventDefault(); closeModal(); } else if (S.graphFull) { S.graphFull = false; paint(); }
|
|
2168
|
+
});
|
|
2169
|
+
// Folding or unfolding the Duo crosses 760px: the list and reader trade places by CSS, the reader fills in for the
|
|
2170
|
+
// wide layout, and the row being read is scrolled into view. Nothing reloads and the route stays as it was.
|
|
2171
|
+
var wideMq = window.matchMedia ? window.matchMedia('(min-width: 760px)') : null;
|
|
2172
|
+
function onWide() {
|
|
2173
|
+
if (W.parseRoute().pane !== 'memories' || !el('mm-root')) return;
|
|
2174
|
+
paint();
|
|
2175
|
+
var sel = pane.querySelector && pane.querySelector('.mm-row[aria-selected="true"]');
|
|
2176
|
+
if (sel && sel.scrollIntoView && isWide()) sel.scrollIntoView({ block: 'nearest' });
|
|
2177
|
+
}
|
|
2178
|
+
if (wideMq) { if (wideMq.addEventListener) wideMq.addEventListener('change', onWide); else if (wideMq.addListener) wideMq.addListener(onWide); }
|
|
2179
|
+
|
|
2180
|
+
W.registerPane('memories', {
|
|
2181
|
+
load: function (args) {
|
|
2182
|
+
// #/memories/view/<view> (older links) and a bare #/memories become the tab's own address; an unknown tab, the stored one.
|
|
2183
|
+
if (args[0] === 'view' || !args.length || (args[0] === 't' && !knownView(args[1]))) {
|
|
2184
|
+
if (args[0] === 'view' && knownView(args[1])) { S.view = args[1]; persist(); }
|
|
2185
|
+
location.replace('#/memories/t/' + encodeURIComponent(S.view)); return;
|
|
2186
|
+
}
|
|
2187
|
+
if (tabRoute(args) && S.view !== args[1]) { S.view = args[1]; persist(); }
|
|
2188
|
+
if (args[0] === 'k') { S.view = 'knowledge'; if (KTABS.indexOf(args[1]) >= 0) { S.ktab = args[1]; S.ktabChosen = true; } persist(); }
|
|
2189
|
+
if (!S.started) loadAll();
|
|
2190
|
+
else if (S.graph && lockHeld(S.graph.lock)) watchIngest();
|
|
2191
|
+
if (S.view === 'memories' && !S.mem.loaded && !S.mem.loading) loadMem(false);
|
|
2192
|
+
if (S.view === 'applied' && !S.applied.loaded && !S.applied.loading) loadApplied(false);
|
|
2193
|
+
if (S.view === 'rated' && !S.cue.ratedLoaded && !S.cue.ratedBusy) loadCueRated();
|
|
2194
|
+
paint();
|
|
2195
|
+
},
|
|
2196
|
+
leave: function () {
|
|
2197
|
+
stopWatches(); S.graphFull = false;
|
|
2198
|
+
if (S.modal && S.modal.kind !== 'merge') closeModal();
|
|
2199
|
+
document.body.classList.remove('mm-modal-open'); document.body.classList.remove('mm-graph-open');
|
|
2200
|
+
},
|
|
2201
|
+
// Back from a reader lands on the tab it was opened from.
|
|
2202
|
+
up: function (args) { return tabRoute(args) ? null : ['t', S.view]; },
|
|
2203
|
+
// For the vm tests (web-control/memories-web.test.ts): the same functions taps and loads run.
|
|
2204
|
+
_t: { S: S, act: act, paint: paint, listHtml: listHtml, readerHtml: readerHtml, countsHtml: countsHtml, viewsHtml: viewsHtml, modalHtml: modalHtml,
|
|
2205
|
+
needsCount: needsCount, needsRows: needsRows, contextText: contextText, findRow: findRow, isRead: isRead, cleanTitle: cleanTitle, mergeFlow: mergeFlow,
|
|
2206
|
+
loadApplied: loadApplied, ask: ask, renderMarkdown: renderMarkdown, cueKey: cueKey, cueNote: cueNote, cueHttps: cueHttps }
|
|
2207
|
+
});
|
|
2208
|
+
})();
|