@gotcos/glasses-server 6.98.1 → 6.99.12
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 +149 -0
- package/package.json +2 -1
- package/server/index.ts +147 -9
- package/server/lib/api-auth.ts +31 -14
- package/server/lib/claude-bridge.ts +21 -2
- package/server/lib/codex-bridge.ts +21 -2
- package/server/lib/display-bus.ts +3 -0
- package/server/lib/drop-destinations.ts +134 -0
- package/server/lib/drop-history-search.ts +97 -0
- package/server/lib/drop-p2-boot.ts +60 -0
- package/server/lib/drop-p2-runtime.ts +237 -0
- package/server/lib/drop-p2-settings.ts +174 -0
- package/server/lib/drop-resolution.ts +168 -0
- package/server/lib/drop-router.ts +325 -0
- package/server/lib/drop-service.ts +739 -0
- package/server/lib/drop-settings.ts +45 -0
- package/server/lib/drop-store.ts +428 -0
- package/server/lib/drop-when.ts +265 -0
- package/server/lib/glasses-pairing.ts +15 -0
- package/server/lib/learning-needs-you.ts +111 -0
- package/server/lib/live-cues-v2-settings.ts +2 -1
- package/server/lib/live-cues-v2.ts +6 -4
- package/server/lib/meeting-consolidation-run.ts +169 -0
- package/server/lib/meeting-store.ts +61 -0
- package/server/lib/meeting-title-steer.ts +200 -0
- package/server/lib/network-policy.ts +84 -6
- package/server/lib/permission-broker.ts +11 -0
- package/server/lib/ping-budget.ts +149 -0
- package/server/lib/ping-engine.ts +245 -0
- package/server/lib/ping-feed-sink.ts +33 -0
- package/server/lib/ping-sources.ts +192 -0
- package/server/lib/query-job-coordinator.ts +5 -2
- package/server/lib/query-job-runtime.ts +45 -4
- package/server/lib/query-job-types.ts +10 -1
- package/server/lib/reminder-scheduler.ts +153 -0
- package/server/lib/tailnet-self-name.ts +103 -0
- package/server/lib/tailscale-nudge.ts +40 -2
- package/server/lib/task-store.ts +32 -0
- package/server/lib/web-ask-admission.ts +56 -0
- package/server/lib/web-live-chunks.ts +273 -0
- package/server/lib/web-memories-bridge.ts +323 -0
- package/server/lib/web-session-auth.ts +492 -0
- package/server/lib/web-usage-bridge.ts +154 -0
- package/server/lib/work-search.ts +14 -1
- package/server/lib/your-move-count.ts +58 -0
- package/server/routes/display.ts +11 -1
- package/server/routes/drops.ts +260 -0
- package/server/routes/health.ts +16 -0
- package/server/routes/live-cues.ts +5 -4
- package/server/routes/meeting.ts +61 -5
- package/server/routes/permission-broker.ts +23 -3
- package/server/routes/transcribe-stream.ts +34 -3
- package/server/routes/web-control.ts +569 -0
- package/server/routes/welcome-context.ts +2 -1
- package/server/web-control/app.css +519 -0
- package/server/web-control/app.js +3643 -0
- package/server/web-control/control.css +312 -0
- package/server/web-control/fonts/DMSans.ttf +0 -0
- package/server/web-control/fonts/Fraunces.ttf +0 -0
- package/server/web-control/fonts/JetBrainsMono.ttf +0 -0
- package/server/web-control/index.html +275 -0
- package/server/web-control/mark.svg +1 -0
- package/server/web-control/memories/d3.min.js +2 -0
- package/server/web-control/memories/graph-explorer.css +200 -0
- package/server/web-control/memories/graph-explorer.js +748 -0
- package/server/web-control/memories/memories-app.js +1561 -0
- package/server/web-control/memories/memories-theme.css +73 -0
- package/server/web-control/memories/memories.html +72 -0
- package/server/web-control/memories/memory-stewardship.js +63 -0
- package/server/web-control/memories/memory-workspace.js +216 -0
- package/server/web-control/panes/cos-chat.css +157 -0
- package/server/web-control/panes/cos-chat.js +973 -0
- package/server/web-control/panes/dictation.js +173 -0
- package/server/web-control/panes/meetings.css +257 -0
- package/server/web-control/panes/meetings.js +2264 -0
- package/server/web-control/panes/messages.js +711 -0
- package/server/web-control/panes/questions.css +32 -0
- package/server/web-control/panes/questions.js +275 -0
- package/server/web-control/panes/sessions.css +452 -0
- package/server/web-control/panes/settings.js +292 -0
- package/server/web-control/panes/speakers.css +269 -0
- package/server/web-control/panes/speakers.js +2755 -0
- package/server/web-control/panes/threads.js +200 -0
- package/server/web-control/panes/work.css +261 -0
- package/server/web-control/panes/work.js +1667 -0
- package/server/web-control/shell.js +294 -0
- package/server/web-control/theme.js +25 -0
- package/server/web-control/vendor/qrcode-encoder.js +10 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/* COS Knowledge graph explorer (design prototype).
|
|
2
|
+
Port of operations/scripts/lightrag_d3_template.html: type legend with counts and multi-select,
|
|
3
|
+
48h / 7d recency, edge-strength threshold, Ring / Free / Fit layouts, zoom and pan, drag to pin,
|
|
4
|
+
double-click to unpin, click to inspect with flowing edges, shift+click path tracing (BFS, 7 hops),
|
|
5
|
+
hover tips, LOD labels, and the / f esc keys. Two deliberate changes from the canonical file:
|
|
6
|
+
filters keep applying while an entity is inspected, and closing the inspector keeps the filters.
|
|
7
|
+
Node timestamps are labeled "first indexed", never "touched": the GraphML carries created_at only.
|
|
8
|
+
Requires d3 v7 as window.d3. Exposes window.COSGraphExplorer.mount(el, options). */
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Categorical, hue-separated, still legible on espresso (Miles, 2026-09-06: "not all shades of
|
|
13
|
+
// brown"). The three types most COS graphs are made of get the widest separation: person gold,
|
|
14
|
+
// organization sky, artifact mint. Unknown types still hash to a stable hue.
|
|
15
|
+
var TYPE_COLORS = {
|
|
16
|
+
person: '#F0C36A', organization: '#6FB8E8', artifact: '#7DD3A5', concept: '#C3A5F0', event: '#F08FB0',
|
|
17
|
+
content: '#E8A15A', method: '#E8D27C', technology: '#63C8D0', product: '#B9A6E0', location: '#F0907E',
|
|
18
|
+
brand: '#9DB2F5', industry: '#B7CF7A', channel: '#E0A9A9', metric: '#9ED2D8', data: '#C9B9A5',
|
|
19
|
+
campaign: '#F2A65A', project: '#D6B27C', decision: '#C9A86E', topic: '#C3B0E7', meeting: '#92C6A1',
|
|
20
|
+
unknown: '#A89E92'
|
|
21
|
+
};
|
|
22
|
+
var GOLD = 'var(--cgx-gold)', GREEN = 'var(--cgx-green)', BG = 'var(--cgx-bg)';
|
|
23
|
+
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
24
|
+
|
|
25
|
+
function hashHue(s) { var h = 0; for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return h % 360; }
|
|
26
|
+
function typeColor(t) {
|
|
27
|
+
var k = String(t || 'unknown').toLowerCase();
|
|
28
|
+
if (TYPE_COLORS[k]) return TYPE_COLORS[k];
|
|
29
|
+
return 'hsl(' + hashHue(k) + ', 38%, 72%)';
|
|
30
|
+
}
|
|
31
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; }); }
|
|
32
|
+
function fmt(n) { return Number(n || 0).toLocaleString(); }
|
|
33
|
+
function trunc(s, n) { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
|
|
34
|
+
function fmtDate(v) {
|
|
35
|
+
if (v == null || v === '') return null;
|
|
36
|
+
var d = typeof v === 'number' ? new Date(v > 1e12 ? v / 1e6 : v * 1000) : new Date(v);
|
|
37
|
+
if (isNaN(d.getTime())) return null;
|
|
38
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
39
|
+
}
|
|
40
|
+
function ago(seconds) {
|
|
41
|
+
if (!isFinite(seconds)) return null;
|
|
42
|
+
if (seconds < 3600) return 'first indexed just now';
|
|
43
|
+
if (seconds < 86400) return 'first indexed ' + Math.round(seconds / 3600) + 'h ago';
|
|
44
|
+
return 'first indexed ' + Math.round(seconds / 86400) + 'd ago';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normalize any input into the canonical exporter contract.
|
|
48
|
+
function normalize(raw) {
|
|
49
|
+
var nodesIn = (raw && raw.nodes) || [], linksIn = (raw && raw.links) || [];
|
|
50
|
+
var byId = {};
|
|
51
|
+
var nodes = nodesIn.map(function (n) {
|
|
52
|
+
var descs = Array.isArray(n.descs) ? n.descs : (n.descs ? [n.descs] : (n.description ? [n.description] : []));
|
|
53
|
+
var node = Object.assign({}, n, { id: String(n.id), group: String(n.group || n.type || 'unknown').toLowerCase(), degree: 0,
|
|
54
|
+
totalDegree: Number(n.totalDegree || 0), descs: descs.map(String), ts: n.ts == null ? null : Number(n.ts) });
|
|
55
|
+
byId[node.id] = node; return node;
|
|
56
|
+
});
|
|
57
|
+
var seen = {}, links = [];
|
|
58
|
+
linksIn.forEach(function (l) {
|
|
59
|
+
var s = typeof l.source === 'object' ? l.source.id : l.source, t = typeof l.target === 'object' ? l.target.id : l.target;
|
|
60
|
+
if (!byId[s] || !byId[t] || s === t) return;
|
|
61
|
+
var key = l.id || (s < t ? s + '\u0000' + t : t + '\u0000' + s);
|
|
62
|
+
var w = Number(l.weight || 1);
|
|
63
|
+
if (seen[key]) { if (w > seen[key].weight) seen[key].weight = w; if (!seen[key].desc && l.desc) seen[key].desc = l.desc; return; }
|
|
64
|
+
var link = Object.assign({}, l, { id: key, source: s, target: t, weight: w, desc: l.desc || l.description || '' });
|
|
65
|
+
seen[key] = link; links.push(link);
|
|
66
|
+
});
|
|
67
|
+
links.forEach(function (l) { byId[l.source].degree++; byId[l.target].degree++; });
|
|
68
|
+
nodes.forEach(function (n) { if (!n.totalDegree) n.totalDegree = n.degree; });
|
|
69
|
+
return { nodes: nodes, links: links, byId: byId,
|
|
70
|
+
total_nodes: Number((raw && (raw.corpus_total_nodes || raw.total_nodes)) || nodes.length),
|
|
71
|
+
total_edges: Number((raw && (raw.corpus_total_edges || raw.total_edges)) || links.length),
|
|
72
|
+
max_weight: links.reduce(function (m, l) { return Math.max(m, l.weight); }, 1),
|
|
73
|
+
generated_at: raw && raw.generated_at, source_mtime_ns: raw && raw.source_mtime_ns,
|
|
74
|
+
description_scope: raw && raw.description_scope, selection_description: raw && raw.selection_description };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch the bounded sample. Prefers /api/graph; falls back to search + per-entity pages on an older server.
|
|
78
|
+
function fetchActual(endpoint) {
|
|
79
|
+
var opts = { cache: 'no-store', signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined };
|
|
80
|
+
// A host may supply its own transport (COS Control feeds this page through its helper; the page never holds a token).
|
|
81
|
+
function get(path) { if (typeof fetchActual.request === 'function') return fetchActual.request(path); return fetch(endpoint + path, opts).then(function (r) { if (!r.ok) throw Object.assign(new Error('Sample request failed (' + r.status + ').'), { status: r.status }); return r.json(); }); }
|
|
82
|
+
return get('/api/graph').catch(function (err) {
|
|
83
|
+
if (err.status !== 404) throw err;
|
|
84
|
+
return get('/api/search?q=&limit=30').then(function (search) {
|
|
85
|
+
var items = search.items || [];
|
|
86
|
+
return Promise.all(items.map(function (it) { return get('/api/entity?id=' + encodeURIComponent(it.id) + '&limit=30'); })).then(function (pages) {
|
|
87
|
+
var nodes = pages.map(function (p) { return p.node; }), links = [];
|
|
88
|
+
pages.forEach(function (p) { (p.relationships || []).forEach(function (e) { links.push(e); }); });
|
|
89
|
+
var meta = search.meta || {};
|
|
90
|
+
return Object.assign({}, meta, { nodes: nodes, links: links, total_nodes: nodes.length, total_edges: links.length });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mount(el, options) {
|
|
97
|
+
if (!el) throw new Error('COSGraphExplorer.mount needs an element');
|
|
98
|
+
var opts = Object.assign({ scope: 'example', endpoint: 'http://127.0.0.1:8768', now: Date.now() / 1000, status: 'ready', focus: null,
|
|
99
|
+
vip: null, memoriesFor: null, onOpenMemory: null, onCopy: null, onOpenSource: null, labels: {},
|
|
100
|
+
// Graph management (design mock, 2026-09-06): merges, renames and removals stay inside this page.
|
|
101
|
+
// curation: { ownerLabel: 'Ukaoma Mac Studio', isOwner: true } decides whether the sheet says "Merge now" or "Queue for <owner>".
|
|
102
|
+
curation: null, onCuration: null, request: null, onRecenter: null, onPassages: null }, options || {});
|
|
103
|
+
fetchActual.request = typeof opts.request === 'function' ? opts.request : null;
|
|
104
|
+
var ctrl = { destroy: destroy, select: selectById, setStatus: setStatus,
|
|
105
|
+
snapshot: function () {
|
|
106
|
+
if (!state.data) return {nodes: [], links: []};
|
|
107
|
+
return {nodes: state.data.nodes.map(function(n) { return Object.assign({},n); }),
|
|
108
|
+
links: state.data.links.map(function(l) { return Object.assign({},l,{source:typeof l.source==='object'?l.source.id:l.source,target:typeof l.target==='object'?l.target.id:l.target}); }),
|
|
109
|
+
positions: Object.fromEntries(state.data.nodes.map(function(n) { return [n.id,{x:n.x,y:n.y,fx:n.fx,fy:n.fy}]; }))};
|
|
110
|
+
},
|
|
111
|
+
replaceData: function (raw) {
|
|
112
|
+
if(state.focusTimer) clearTimeout(state.focusTimer); if(state.sim) state.sim.stop(); if(state.ro) state.ro.disconnect();
|
|
113
|
+
if(state.keyHandler) document.removeEventListener('keydown',state.keyHandler);
|
|
114
|
+
start(normalize(raw), true);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var root, stage, svgEl, ui = {}, state = { data: null, sim: null, zoom: null, k: 1, anchored: {}, activeId: null, pathEndA: null,
|
|
118
|
+
activeTypes: {}, recency: 'all', edgeThreshold: 0, layout: 'ring', destroyed: false, ro: null, keyHandler: null,
|
|
119
|
+
merged: {}, removed: {}, alias: {}, changes: [] };
|
|
120
|
+
|
|
121
|
+
el.innerHTML = '';
|
|
122
|
+
root = document.createElement('div'); root.className = 'cgx cgx-' + opts.scope; el.appendChild(root);
|
|
123
|
+
root.innerHTML =
|
|
124
|
+
'<div class="cgx-bar">' +
|
|
125
|
+
'<span class="cgx-scope ' + (opts.scope === 'example' ? 'example' : '') + '" data-role="scope"><i></i><span data-role="scopeText">Loading…</span></span>' +
|
|
126
|
+
'<span class="cgx-fresh" data-role="fresh"></span>' +
|
|
127
|
+
'<div class="cgx-search"><span class="ico">⌕</span><input type="search" data-role="search" placeholder="Find an entity or type…" autocomplete="off" aria-label="Find an entity"><kbd>/</kbd><div class="cgx-results" data-role="results" role="listbox"></div></div>' +
|
|
128
|
+
'</div>' +
|
|
129
|
+
'<div class="cgx-body"><div class="cgx-stage" data-role="stage">' +
|
|
130
|
+
'<svg class="cgx-canvas" data-role="svg" role="img" aria-label="Knowledge graph"></svg>' +
|
|
131
|
+
'<div class="cgx-pinned" data-role="pinned"><span class="lbl">Pinned</span><span class="nms" data-role="pinnedNames"></span><button class="cgx-btn" data-role="clearPins">Clear</button></div>' +
|
|
132
|
+
'<div class="cgx-rail" data-role="rail">' +
|
|
133
|
+
'<div class="cgx-panel keep"><div class="cgx-panel-t">Layout <button data-role="railToggle" title="Show or hide controls">hide</button></div><div class="cgx-row"><button class="cgx-btn on" data-layout="ring">Ring</button><button class="cgx-btn" data-layout="free">Free</button><button class="cgx-btn" data-role="fit">Fit</button></div></div>' +
|
|
134
|
+
'<div class="cgx-panel"><div class="cgx-panel-t">Edge strength</div><div class="cgx-slider"><input type="range" min="0" max="20" step="0.5" value="0" data-role="edgeSlider" aria-label="Minimum relationship strength"><span class="val" data-role="edgeVal">0</span></div></div>' +
|
|
135
|
+
'<div class="cgx-panel"><div class="cgx-panel-t">First indexed</div><div class="cgx-row" data-role="recency"><button class="cgx-btn on" data-mode="all">All</button><button class="cgx-btn" data-mode="7d">7d</button><button class="cgx-btn" data-mode="48h">48h</button></div></div>' +
|
|
136
|
+
'<div class="cgx-panel"><div class="cgx-panel-t">Entity types <button data-role="resetFilters" title="Clear type and time filters">reset</button></div><div class="cgx-legend" data-role="legend"></div></div>' +
|
|
137
|
+
'<div class="cgx-panel"><div class="cgx-panel-t">Keys</div><div class="cgx-keys"><kbd>click</kbd>inspect <kbd>⇧click</kbd>path<br><kbd>drag</kbd>pin <kbd>2×click</kbd>unpin<br><kbd>/</kbd>search <kbd>f</kbd>fit <kbd>esc</kbd>close</div></div>' +
|
|
138
|
+
'</div>' +
|
|
139
|
+
'<div class="cgx-tip" data-role="tip"><div class="top"><span class="dot" data-role="tipDot"></span><span data-role="tipName"></span><span class="deg" data-role="tipDeg"></span></div><div class="desc" data-role="tipDesc"></div></div>' +
|
|
140
|
+
'<div class="cgx-hint" data-role="hint"><b>shift+click</b> two entities to trace the path between them</div>' +
|
|
141
|
+
'<div class="cgx-toast" data-role="toast"></div>' +
|
|
142
|
+
'<div class="cgx-state" data-role="stateBox"></div>' +
|
|
143
|
+
'</div>' +
|
|
144
|
+
'<div class="cgx-dp" data-role="dp" aria-live="polite"><div class="cgx-dp-accent" data-role="dpAccent"></div><div class="cgx-dp-hd"><div class="cgx-dp-nm" data-role="dpName"></div><div class="cgx-dp-meta"><span class="cgx-tb" data-role="dpType"><i></i><span></span></span><span data-role="dpConn"></span><span class="fresh" data-role="dpFresh"></span></div><div class="cgx-meter"><i data-role="dpMeter"></i></div><button class="cgx-dp-x" data-role="dpClose" aria-label="Close inspector">×</button></div><div class="cgx-dp-bd" data-role="dpBody"></div><div class="cgx-dp-actions" data-role="dpActions"></div></div>' +
|
|
145
|
+
'</div>' +
|
|
146
|
+
'<div class="cgx-foot"><span data-role="footLeft"></span><span data-role="footRight"></span></div>' +
|
|
147
|
+
'<div class="cgx-changes" data-role="changes" aria-live="polite"></div>';
|
|
148
|
+
root.querySelectorAll('[data-role]').forEach(function (n) { ui[n.getAttribute('data-role')] = n; });
|
|
149
|
+
stage = ui.stage; svgEl = ui.svg;
|
|
150
|
+
|
|
151
|
+
function setScopeText(html, cls) { ui.scopeText.innerHTML = html; ui.scope.className = 'cgx-scope ' + (cls || ''); }
|
|
152
|
+
function toast(msg) { ui.toast.textContent = msg; ui.toast.classList.add('show'); clearTimeout(ui.toast._t); ui.toast._t = setTimeout(function () { ui.toast.classList.remove('show'); }, 2400); }
|
|
153
|
+
function showState(html) { ui.stateBox.innerHTML = html; ui.stateBox.style.display = html ? 'flex' : 'none'; }
|
|
154
|
+
|
|
155
|
+
function setStatus(status, message) {
|
|
156
|
+
if (status === 'loading') { showState('<div><div class="cgx-spin"></div><p>Connecting to the graph…</p></div>'); return; }
|
|
157
|
+
if (status === 'offline') { setScopeText('LightRAG · <b>not connected</b>', 'off'); showState('<div><h3>LightRAG is not connected.</h3><p>Source records and learning history stay available with your current memory setup. ' + esc(message || '') + '</p><button class="cgx-btn primary" data-role="retry">Try again</button></div>'); bindRetry(); return; }
|
|
158
|
+
if (status === 'error') { setScopeText('LightRAG · <b>unavailable</b>', 'off'); showState('<div><h3>The graph could not be loaded.</h3><p>' + esc(message || 'No fresh graph data is available. Saved memories and prior learning evidence are still available.') + '</p><button class="cgx-btn primary" data-role="retry">Try again</button></div>'); bindRetry(); return; }
|
|
159
|
+
if (status === 'empty') { showState('<div><h3>' + esc(opts.labels && opts.labels.emptyTitle || 'No linked entities yet.') + '</h3><p>' + esc(message || opts.labels && opts.labels.emptyMessage || 'Connections appear when evidence is indexed.') + '</p></div>'); return; }
|
|
160
|
+
showState('');
|
|
161
|
+
}
|
|
162
|
+
function bindRetry() { var b = ui.stateBox.querySelector('[data-role="retry"]'); if (b) b.addEventListener('click', function () { load(); }); }
|
|
163
|
+
|
|
164
|
+
function load() {
|
|
165
|
+
if (opts.status === 'offline' || opts.status === 'error') { setStatus(opts.status); return; }
|
|
166
|
+
if (opts.data) { start(normalize(opts.data)); return; }
|
|
167
|
+
setStatus('loading');
|
|
168
|
+
fetchActual(opts.endpoint).then(function (raw) { if (state.destroyed) return; start(normalize(raw)); })
|
|
169
|
+
.catch(function (err) { if (state.destroyed) return; var offline = !err.status; setStatus(offline ? 'offline' : 'error', offline ? 'The local sample server did not answer.' : err.message); });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function start(data, preserve) {
|
|
173
|
+
if (!window.d3) { setStatus('error', 'The graph canvas needs d3, which did not load.'); return; }
|
|
174
|
+
state.data = data;
|
|
175
|
+
if (!data.nodes.length) { window.d3.select(svgEl).selectAll('*').remove(); ui.dp.classList.remove('on'); state.activeId=null; state.pathEndA=null; ctrl._select=null; ui.legend.innerHTML=''; setScopeText('No points in this investigation', ''); setStatus('empty'); return; }
|
|
176
|
+
if (state.activeId && !data.byId[state.activeId]) { state.activeId=null; ui.dp.classList.remove('on'); }
|
|
177
|
+
setStatus('ready');
|
|
178
|
+
var d3 = window.d3, nodes = data.nodes, links = data.links, byId = data.byId;
|
|
179
|
+
var W = Math.max(320, stage.clientWidth), H = Math.max(360, stage.clientHeight);
|
|
180
|
+
var small = nodes.length <= 60;
|
|
181
|
+
var D48 = 48 * 3600, D7 = 7 * 86400;
|
|
182
|
+
|
|
183
|
+
// VIP: host-provided, else the highest-degree node as the center with its strongest neighbors as the ring.
|
|
184
|
+
var vip = opts.vip;
|
|
185
|
+
if (!vip) {
|
|
186
|
+
var center = nodes.slice().sort(function (a, b) { return b.degree - a.degree; })[0];
|
|
187
|
+
var ring = links.filter(function (l) { return l.source === center.id || l.target === center.id; })
|
|
188
|
+
.sort(function (a, b) { return b.weight - a.weight; }).slice(0, 8)
|
|
189
|
+
.map(function (l) { return l.source === center.id ? l.target : l.source; });
|
|
190
|
+
vip = { center: center.id, ring: ring };
|
|
191
|
+
}
|
|
192
|
+
nodes.forEach(function (n) {
|
|
193
|
+
n.color = typeColor(n.group);
|
|
194
|
+
n.radius = small ? Math.max(9, Math.min(30, 7 + Math.sqrt(n.degree) * 3)) : Math.max(5.5, Math.min(34, 4 + Math.sqrt(n.degree) * 2.6));
|
|
195
|
+
n.isCenter = n.id === vip.center; n.isVip = n.isCenter || vip.ring.indexOf(n.id) >= 0;
|
|
196
|
+
if (n.isCenter) { n.color = '#C9A86E'; n.radius = small ? 34 : 44; }
|
|
197
|
+
n.age = n.ts ? (opts.now - n.ts) : Infinity; n.fresh48 = n.age < D48; n.fresh7 = n.age < D7;
|
|
198
|
+
n.short = trunc(n.id, 28);
|
|
199
|
+
});
|
|
200
|
+
var maxDeg = d3.max(nodes, function (n) { return n.degree; }) || 1;
|
|
201
|
+
var n48 = nodes.filter(function (n) { return n.fresh48; }).length, n7 = nodes.filter(function (n) { return n.fresh7; }).length;
|
|
202
|
+
|
|
203
|
+
// Scope and freshness lines, stated separately: an export today does not make the source current.
|
|
204
|
+
if (opts.scope === 'example') setScopeText('Example graph · <b>' + nodes.length + ' entities</b> · ' + links.length + ' relationships', 'example');
|
|
205
|
+
else setScopeText(esc(opts.labels.graphName || 'Your LightRAG data') + ' · <b>' + fmt(nodes.length) + ' of ' + fmt(data.total_nodes) + ' entities</b> · ' + fmt(links.length) + ' of ' + fmt(data.total_edges) + ' relationships', '');
|
|
206
|
+
var fresh = [];
|
|
207
|
+
if (data.source_mtime_ns) fresh.push('Graph updated <b>' + fmtDate(data.source_mtime_ns) + '</b>');
|
|
208
|
+
if (data.generated_at) fresh.push('Sample exported <b>' + fmtDate(data.generated_at) + '</b>');
|
|
209
|
+
if (opts.scope === 'example') fresh.push('Example data, not your graph');
|
|
210
|
+
ui.fresh.innerHTML = fresh.join(' · ');
|
|
211
|
+
ui.footLeft.innerHTML = opts.scope === 'example' ? 'Relationships are extracted claims. They add context to a memory; they do not prove a lesson worked.' : (esc(data.selection_description || 'Bounded sample of the source graph.') + ' Descriptions are ' + esc(data.description_scope || 'extracted summaries') + '.');
|
|
212
|
+
ui.footRight.innerHTML = n7 ? '<b>' + n7 + '</b> first indexed in 7 days' : 'Nothing first indexed in the last 7 days';
|
|
213
|
+
ui.recency.querySelector('[data-mode="7d"]').textContent = '7d · ' + n7;
|
|
214
|
+
ui.recency.querySelector('[data-mode="48h"]').textContent = '48h · ' + n48;
|
|
215
|
+
|
|
216
|
+
// Legend with counts, multi-select.
|
|
217
|
+
var typeCounts = {}; nodes.forEach(function (n) { typeCounts[n.group] = (typeCounts[n.group] || 0) + 1; });
|
|
218
|
+
var types = Object.keys(typeCounts).sort(function (a, b) { return typeCounts[b] - typeCounts[a]; });
|
|
219
|
+
ui.legend.innerHTML = types.map(function (t) { return '<button class="cgx-leg" data-type="' + esc(t) + '" aria-pressed="false"><span class="dot" style="background:' + typeColor(t) + ';color:' + typeColor(t) + '"></span><span class="nm">' + esc(t) + '</span><span class="ct">' + typeCounts[t] + '</span></button>'; }).join('');
|
|
220
|
+
|
|
221
|
+
// Adjacency and edge lookups.
|
|
222
|
+
var nbrs = {}, edgeInfo = {};
|
|
223
|
+
nodes.forEach(function (n) { nbrs[n.id] = {}; });
|
|
224
|
+
links.forEach(function (l) {
|
|
225
|
+
nbrs[l.source][l.target] = true; nbrs[l.target][l.source] = true;
|
|
226
|
+
edgeInfo[l.source + '|' + l.target] = edgeInfo[l.target + '|' + l.source] = { w: l.weight, desc: l.desc };
|
|
227
|
+
l.mix = d3.interpolateLab(byId[l.source].color, byId[l.target].color)(0.5);
|
|
228
|
+
});
|
|
229
|
+
function neighborIds(id) { return Object.keys(nbrs[id] || {}); }
|
|
230
|
+
|
|
231
|
+
// SVG scaffold.
|
|
232
|
+
var svg = d3.select(svgEl); svg.selectAll('*').remove();
|
|
233
|
+
var g = svg.append('g');
|
|
234
|
+
var zm = d3.zoom().scaleExtent([0.08, 10]).on('zoom', function (e) { g.attr('transform', e.transform); updateLOD(e.transform.k); });
|
|
235
|
+
svg.call(zm).on('dblclick.zoom', null);
|
|
236
|
+
state.zoom = zm;
|
|
237
|
+
var defs = svg.append('defs');
|
|
238
|
+
var fl = defs.append('filter').attr('id', 'cgx-glow').attr('x', '-60%').attr('y', '-60%').attr('width', '220%').attr('height', '220%');
|
|
239
|
+
fl.append('feGaussianBlur').attr('in', 'SourceGraphic').attr('stdDeviation', '4').attr('result', 'bl');
|
|
240
|
+
fl.append('feComposite').attr('in', 'SourceGraphic').attr('in2', 'bl').attr('operator', 'over');
|
|
241
|
+
Object.keys(typeCounts).forEach(function (t) {
|
|
242
|
+
var c = d3.color(typeColor(t)); if (!c) return;
|
|
243
|
+
var rg = defs.append('radialGradient').attr('id', 'cgx-ng-' + t.replace(/[^a-z0-9]/gi, '_')).attr('cx', '38%').attr('cy', '34%').attr('r', '75%');
|
|
244
|
+
rg.append('stop').attr('offset', '0%').attr('stop-color', c.brighter(0.7).formatHex());
|
|
245
|
+
rg.append('stop').attr('offset', '55%').attr('stop-color', c.formatHex());
|
|
246
|
+
rg.append('stop').attr('offset', '100%').attr('stop-color', c.darker(0.9).formatHex());
|
|
247
|
+
});
|
|
248
|
+
var cg = defs.append('radialGradient').attr('id', 'cgx-ng-center').attr('cx', '38%').attr('cy', '34%').attr('r', '75%');
|
|
249
|
+
cg.append('stop').attr('offset', '0%').attr('stop-color', '#F3DFB6');
|
|
250
|
+
cg.append('stop').attr('offset', '55%').attr('stop-color', GOLD);
|
|
251
|
+
cg.append('stop').attr('offset', '100%').attr('stop-color', '#8F7240');
|
|
252
|
+
|
|
253
|
+
// Keep the ring clear of the control rail: shift the center right by half the rail width when it is open.
|
|
254
|
+
var railW = (W >= 720 && !ui.rail.classList.contains('collapsed')) ? 200 : 0;
|
|
255
|
+
var R = Math.min(W - railW, H) * (small ? 0.3 : 0.36), cx = (W + railW) / 2, cy = H / 2;
|
|
256
|
+
var grid = g.append('g');
|
|
257
|
+
[0.45, 1.55, 2.3].forEach(function (m) { grid.append('circle').attr('cx', cx).attr('cy', cy).attr('r', R * m).attr('fill', 'none').attr('stroke', 'rgba(201,168,110,0.05)'); });
|
|
258
|
+
for (var i = 0; i < 12; i++) { var a = i * Math.PI / 6; grid.append('line').attr('x1', cx + R * 0.45 * Math.cos(a)).attr('y1', cy + R * 0.45 * Math.sin(a)).attr('x2', cx + R * 2.3 * Math.cos(a)).attr('y2', cy + R * 2.3 * Math.sin(a)).attr('stroke', 'rgba(245,240,230,0.02)'); }
|
|
259
|
+
grid.append('circle').attr('class', 'orbit').attr('cx', cx).attr('cy', cy).attr('r', R).attr('fill', 'none').attr('stroke', 'rgba(201,168,110,0.16)').attr('stroke-width', 1.2).attr('stroke-dasharray', '3 9');
|
|
260
|
+
|
|
261
|
+
var sim = d3.forceSimulation(nodes)
|
|
262
|
+
.force('link', d3.forceLink(links).id(function (d) { return d.id; }).distance(small ? 120 : 105).strength(0.25))
|
|
263
|
+
.force('charge', d3.forceManyBody().strength(small ? -520 : -470).distanceMax(580))
|
|
264
|
+
.force('center', d3.forceCenter(cx, cy))
|
|
265
|
+
.force('collision', d3.forceCollide().radius(function (d) { return d.radius + 8; }))
|
|
266
|
+
.alphaDecay(0.02);
|
|
267
|
+
state.sim = sim;
|
|
268
|
+
|
|
269
|
+
var link = g.append('g').selectAll('path').data(links).join('path').attr('class', 'edge').attr('fill', 'none')
|
|
270
|
+
.attr('stroke', function (d) { return d.mix; })
|
|
271
|
+
.attr('stroke-opacity', function (d) { return Math.min(0.42, 0.12 + d.weight * 0.03); })
|
|
272
|
+
.attr('stroke-width', function (d) { return Math.max(0.7, Math.sqrt(d.weight) * 0.6); });
|
|
273
|
+
var vipRings = g.append('g').selectAll('circle').data(nodes.filter(function (n) { return n.isVip; })).join('circle')
|
|
274
|
+
.attr('r', function (d) { return d.isCenter ? d.radius + 7 : d.radius + 4.5; }).attr('fill', 'none')
|
|
275
|
+
.attr('stroke', function (d) { return d.isCenter ? GOLD : 'var(--cgx-muted)'; })
|
|
276
|
+
.attr('stroke-width', function (d) { return d.isCenter ? 2 : 1.4; })
|
|
277
|
+
.attr('stroke-opacity', function (d) { return d.isCenter ? 0.75 : 0.35; })
|
|
278
|
+
.attr('stroke-dasharray', function (d) { return d.isCenter ? null : '1 4'; }).attr('stroke-linecap', 'round');
|
|
279
|
+
var freshRings = g.append('g').selectAll('circle').data(nodes.filter(function (n) { return n.fresh48 && !n.isCenter; })).join('circle')
|
|
280
|
+
.attr('class', 'fresh-ring').attr('r', function (d) { return d.radius + 8; }).attr('fill', 'none').attr('stroke', GREEN).attr('stroke-width', 1.5).attr('stroke-opacity', 0.4);
|
|
281
|
+
var node = g.append('g').selectAll('circle').data(nodes).join('circle')
|
|
282
|
+
.attr('class', 'focusable').attr('tabindex', 0).attr('role', 'button')
|
|
283
|
+
.attr('aria-label', function (d) { return 'Inspect ' + d.id; })
|
|
284
|
+
.attr('r', REDUCED ? function (d) { return d.radius; } : 0)
|
|
285
|
+
.attr('fill', function (d) { return d.isCenter ? 'url(#cgx-ng-center)' : 'url(#cgx-ng-' + d.group.replace(/[^a-z0-9]/gi, '_') + ')'; })
|
|
286
|
+
.attr('stroke', function (d) { return d.isCenter ? 'rgba(201,168,110,0.5)' : 'rgba(245,240,230,0.12)'; })
|
|
287
|
+
.attr('stroke-width', function (d) { return d.isCenter ? 2 : 1.2; }).style('cursor', 'pointer');
|
|
288
|
+
if (!REDUCED) node.transition().delay(function (d, i) { return i * 4; }).duration(520).ease(d3.easeCubicOut).attr('r', function (d) { return d.radius; });
|
|
289
|
+
var pins = g.append('g').selectAll('circle').data(nodes).join('circle').attr('r', 3.4).attr('fill', GOLD).attr('stroke', BG).attr('stroke-width', 1.4).style('display', 'none').style('pointer-events', 'none');
|
|
290
|
+
var label = g.append('g').selectAll('text').data(nodes).join('text').attr('class', 'nl').text(function (d) { return d.short; })
|
|
291
|
+
.attr('dx', function (d) { return d.radius + 6; }).attr('dy', 3.5)
|
|
292
|
+
.style('font-size', function (d) { return d.isCenter ? '15px' : d.degree > 60 ? '13px' : d.degree > 30 ? '11.5px' : d.degree > 12 ? '10.5px' : '10px'; })
|
|
293
|
+
.style('fill', function (d) { return d.isCenter ? GOLD : d.degree > 30 ? 'var(--cgx-fg)' : 'var(--cgx-muted)'; })
|
|
294
|
+
.style('font-weight', function (d) { return d.isCenter ? '700' : d.degree > 30 ? '600' : '500'; })
|
|
295
|
+
.style('opacity', REDUCED ? 1 : 0);
|
|
296
|
+
if (!REDUCED) label.transition().delay(function (d, i) { return 250 + i * 3; }).duration(600).style('opacity', 1);
|
|
297
|
+
|
|
298
|
+
function lodVisible(d, k) { if (small || d.isVip || d.degree >= 40) return true; if (k >= 0.75 && d.degree >= 18) return true; if (k >= 1.3 && d.degree >= 7) return true; return k >= 2; }
|
|
299
|
+
function updateLOD(k) { state.k = k; if (state.activeId) return; label.style('display', function (d) { return lodVisible(d, k) ? null : 'none'; }); }
|
|
300
|
+
updateLOD(1);
|
|
301
|
+
|
|
302
|
+
function passes(d) {
|
|
303
|
+
if (state.merged[d.id] || state.removed[d.id]) return false;
|
|
304
|
+
var typesOn = Object.keys(state.activeTypes).length;
|
|
305
|
+
if (typesOn && !state.activeTypes[d.group]) return false;
|
|
306
|
+
if (state.recency === '7d' && !d.fresh7) return false;
|
|
307
|
+
if (state.recency === '48h' && !d.fresh48) return false;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
function edgeVisible() {
|
|
311
|
+
if (state.edgeThreshold <= 0) return null;
|
|
312
|
+
var v = {}; links.forEach(function (l) { if (l.weight >= state.edgeThreshold) { v[l.source.id || l.source] = true; v[l.target.id || l.target] = true; } }); return v;
|
|
313
|
+
}
|
|
314
|
+
// Filters and the selection compose: the canonical file skipped filters while inspecting.
|
|
315
|
+
function paint() {
|
|
316
|
+
var ev = edgeVisible(), active = state.activeId, my = active && active !== '__path__' ? nbrs[active] || {} : null, onPath = state.onPath || null, pairs = state.pathPairs || null;
|
|
317
|
+
function shown(d) { var ok = passes(d) && (!ev || ev[d.id]); if (d.isCenter && !ok) return 'dim'; return ok ? 'on' : 'off'; }
|
|
318
|
+
function focus(d) { if (onPath) return !!onPath[d.id]; if (my) return d.id === active || !!my[d.id]; return true; }
|
|
319
|
+
var t = REDUCED ? d3.transition().duration(0) : d3.transition().duration(220);
|
|
320
|
+
node.transition(t).attr('opacity', function (d) { var s = shown(d); if (s === 'off') return 0.06; if (!focus(d)) return 0.06; return s === 'dim' ? 0.35 : 1; });
|
|
321
|
+
node.attr('filter', function (d) { return (active && d.id === active) || (onPath && (d.id === state.pathA || d.id === state.pathB)) ? 'url(#cgx-glow)' : null; });
|
|
322
|
+
label.transition(t).style('opacity', function (d) { var s = shown(d); if (s === 'off' || !focus(d)) return 0.04; return s === 'dim' ? 0.4 : 1; })
|
|
323
|
+
.style('fill', function (d) { if (onPath && onPath[d.id]) return d.isCenter ? GOLD : 'var(--cgx-fg)'; if (my && d.id === active) return d.isCenter ? GOLD : 'var(--cgx-fg)'; return d.isCenter ? GOLD : d.degree > 30 ? 'var(--cgx-fg)' : 'var(--cgx-muted)'; });
|
|
324
|
+
label.text(function (d) { return state.alias[d.id] ? trunc(state.alias[d.id], 28) : d.short; });
|
|
325
|
+
label.style('display', function (d) { if (state.merged[d.id] || state.removed[d.id]) return 'none'; if (my && (d.id === active || my[d.id])) return null; if (onPath && onPath[d.id]) return null; return lodVisible(d, state.k) ? null : 'none'; });
|
|
326
|
+
link.attr('class', function (l) { var s = l.source.id, tg = l.target.id; if (pairs && pairs[s + '|' + tg]) return 'edge flow'; if (my && (s === active || tg === active)) return 'edge flow'; return 'edge'; })
|
|
327
|
+
.transition(t)
|
|
328
|
+
.attr('stroke', function (l) { var s = l.source.id, tg = l.target.id; if (pairs && pairs[s + '|' + tg]) return GOLD; if (my) { if (s === active) return l.target.color; if (tg === active) return l.source.color; } return l.mix; })
|
|
329
|
+
.attr('stroke-opacity', function (l) { var s = l.source.id, tg = l.target.id; if (state.edgeThreshold > 0 && l.weight < state.edgeThreshold) return 0; if (pairs) return pairs[s + '|' + tg] ? 0.95 : 0.02; if (my) return (s === active || tg === active) ? 0.8 : 0.02; if (!passes(l.source) || !passes(l.target)) return 0.015; return Math.min(0.42, 0.12 + l.weight * 0.03); })
|
|
330
|
+
.attr('stroke-width', function (l) { var s = l.source.id, tg = l.target.id; if (pairs && pairs[s + '|' + tg]) return 2.6; if (my && (s === active || tg === active)) return Math.max(1.5, Math.sqrt(l.weight) * 1.05); return Math.max(0.7, Math.sqrt(l.weight) * 0.6); });
|
|
331
|
+
vipRings.transition(t).attr('stroke-opacity', function (d) { return focus(d) && shown(d) !== 'off' ? (d.isCenter ? 0.75 : 0.35) : 0.04; });
|
|
332
|
+
freshRings.transition(t).attr('stroke-opacity', function (d) { return focus(d) && shown(d) !== 'off' ? 0.4 : 0.04; });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Layouts.
|
|
336
|
+
function ringLayout() {
|
|
337
|
+
nodes.forEach(function (n) {
|
|
338
|
+
if (n.isCenter) { n.fx = cx; n.fy = cy; state.anchored[n.id] = 'ring'; }
|
|
339
|
+
var idx = vip.ring.indexOf(n.id);
|
|
340
|
+
if (idx >= 0) { var a = (2 * Math.PI * idx / vip.ring.length) - Math.PI / 2; n.fx = cx + R * Math.cos(a); n.fy = cy + R * Math.sin(a); state.anchored[n.id] = 'ring'; }
|
|
341
|
+
});
|
|
342
|
+
updateAnchors(); sim.alpha(0.6).restart();
|
|
343
|
+
}
|
|
344
|
+
function freeLayout() { nodes.forEach(function (n) { n.fx = null; n.fy = null; }); state.anchored = {}; updateAnchors(); sim.alpha(0.8).restart(); }
|
|
345
|
+
function setLayout(type) { state.layout = type; ui.rail.querySelectorAll('[data-layout]').forEach(function (b) { b.classList.toggle('on', b.getAttribute('data-layout') === type); }); if (type === 'ring') ringLayout(); else freeLayout(); }
|
|
346
|
+
function fit() {
|
|
347
|
+
var xs = nodes.map(function (n) { return n.x; }), ys = nodes.map(function (n) { return n.y; });
|
|
348
|
+
var x0 = d3.min(xs), x1 = d3.max(xs), y0 = d3.min(ys), y1 = d3.max(ys);
|
|
349
|
+
var k = Math.min(6, 0.82 / Math.max((x1 - x0 + 120) / W, (y1 - y0 + 120) / H));
|
|
350
|
+
svg.transition().duration(REDUCED ? 0 : 650).ease(d3.easeCubicInOut).call(zm.transform, d3.zoomIdentity.translate(W / 2 - k * (x0 + x1) / 2, H / 2 - k * (y0 + y1) / 2).scale(k));
|
|
351
|
+
}
|
|
352
|
+
function panTo(d) { var tr = d3.zoomTransform(svgEl); svg.transition().duration(REDUCED ? 0 : 600).ease(d3.easeCubicInOut).call(zm.transform, d3.zoomIdentity.translate(W / 2 - d.x * tr.k, H / 2 - d.y * tr.k).scale(tr.k)); }
|
|
353
|
+
|
|
354
|
+
// Drag: a still click must not pin.
|
|
355
|
+
var dragDist = 0, dragStart = null;
|
|
356
|
+
node.call(d3.drag().clickDistance(4)
|
|
357
|
+
.on('start', function (e, d) { if (opts.onUserIntent) opts.onUserIntent(); if (!e.active) sim.alphaTarget(0.08).restart(); dragDist = 0; dragStart = [e.x, e.y]; d.fx = d.x; d.fy = d.y; })
|
|
358
|
+
.on('drag', function (e, d) { d.fx = e.x; d.fy = e.y; dragDist = Math.hypot(e.x - dragStart[0], e.y - dragStart[1]); })
|
|
359
|
+
.on('end', function (e, d) { if (!e.active) sim.alphaTarget(0); if (dragDist > 4) { if (opts.onUserIntent) opts.onUserIntent(); state.anchored[d.id] = 'user'; updateAnchors(); } else if (!state.anchored[d.id]) { d.fx = null; d.fy = null; } }));
|
|
360
|
+
function updateAnchors() {
|
|
361
|
+
pins.style('display', function (d) { return state.anchored[d.id] ? 'block' : 'none'; });
|
|
362
|
+
var user = Object.keys(state.anchored).filter(function (id) { return state.anchored[id] === 'user'; });
|
|
363
|
+
ui.pinned.classList.toggle('on', user.length > 0); ui.pinnedNames.textContent = user.join(', ');
|
|
364
|
+
}
|
|
365
|
+
ui.clearPins.onclick = function () { nodes.forEach(function (d) { if (state.anchored[d.id]) { d.fx = null; d.fy = null; } }); state.anchored = {}; updateAnchors(); state.layout = 'free'; ui.rail.querySelectorAll('[data-layout]').forEach(function (b) { b.classList.toggle('on', b.getAttribute('data-layout') === 'free'); }); sim.alphaTarget(0.08).restart(); setTimeout(function () { sim.alphaTarget(0); }, 800); };
|
|
366
|
+
|
|
367
|
+
// Click, double-click, keyboard on nodes.
|
|
368
|
+
node.on('click', function (e, d) { e.stopPropagation(); if (e.shiftKey && state.pathEndA && state.pathEndA !== d.id) { tracePath(state.pathEndA, d.id); return; } state.pathEndA = d.id; select(d); });
|
|
369
|
+
node.on('dblclick', function (e, d) { e.stopPropagation(); if (opts.onUserIntent) opts.onUserIntent(); d.fx = null; d.fy = null; delete state.anchored[d.id]; updateAnchors(); sim.alphaTarget(0.05).restart(); setTimeout(function () { sim.alphaTarget(0); }, 600); });
|
|
370
|
+
node.on('keydown', function (e, d) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); state.pathEndA = d.id; select(d, true); } });
|
|
371
|
+
|
|
372
|
+
// Hover tip.
|
|
373
|
+
node.on('mouseover', function (e, d) {
|
|
374
|
+
ui.tipName.textContent = d.id; ui.tipDot.style.background = d.color; ui.tipDot.style.color = d.color; ui.tipDeg.textContent = d.degree + ' conn'; ui.tipDesc.textContent = (d.descs && d.descs[0]) || '';
|
|
375
|
+
ui.tip.style.display = 'block';
|
|
376
|
+
if (!state.activeId && !REDUCED) d3.select(this).attr('filter', 'url(#cgx-glow)').transition().duration(150).attr('r', d.radius * 1.08);
|
|
377
|
+
}).on('mousemove', function (e) {
|
|
378
|
+
var r = stage.getBoundingClientRect(); var x = Math.min(e.clientX - r.left + 14, r.width - 300), y = e.clientY - r.top - 8;
|
|
379
|
+
ui.tip.style.left = Math.max(6, x) + 'px'; ui.tip.style.top = Math.max(6, y) + 'px';
|
|
380
|
+
}).on('mouseout', function (e, d) { ui.tip.style.display = 'none'; if (state.activeId !== d.id) d3.select(this).attr('filter', null).transition().duration(200).attr('r', d.radius); });
|
|
381
|
+
|
|
382
|
+
svg.on('click', function () { closeInspector(); });
|
|
383
|
+
|
|
384
|
+
// Graph management (design mock). Duplicate candidates come from three signals shown SEPARATELY:
|
|
385
|
+
// a normalized-name match, shared neighbors, and description agreement. Nothing merges on a name alone
|
|
386
|
+
// (2026-04-07: Miles Mallard and Manoj Kumar were nearly merged into different people by name).
|
|
387
|
+
function normName(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); }
|
|
388
|
+
function initials(s) { return normName(s).split(' ').filter(Boolean).map(function (w) { return w[0]; }).join(''); }
|
|
389
|
+
function words(s) { return normName(s).split(' ').filter(function (w) { return w.length >= 5; }); }
|
|
390
|
+
function displayName(n) { return state.alias[n.id] || n.id; }
|
|
391
|
+
function isLive(id) { return !state.merged[id] && !state.removed[id]; }
|
|
392
|
+
function liveNodes() { return nodes.filter(function (n) { return isLive(n.id); }); }
|
|
393
|
+
function agreement(a, b) {
|
|
394
|
+
var da = (a.descs && a.descs[0]) || '', db = (b.descs && b.descs[0]) || '';
|
|
395
|
+
if (!da || !db) return { label: 'one side has no description', cls: 'muted' };
|
|
396
|
+
var wa = words(da), wb = words(db), shared = wa.filter(function (w) { return wb.indexOf(w) >= 0; });
|
|
397
|
+
if (a.group === b.group && shared.length >= 2) return { label: 'descriptions agree', cls: 'ok' };
|
|
398
|
+
if (!shared.length) return { label: 'descriptions differ', cls: 'warn' };
|
|
399
|
+
return { label: 'descriptions overlap on ' + shared.length + ' term' + (shared.length > 1 ? 's' : ''), cls: 'muted' };
|
|
400
|
+
}
|
|
401
|
+
function nameSignal(a, b) {
|
|
402
|
+
var na = normName(displayName(a)), nb = normName(displayName(b)); if (!na || !nb) return null;
|
|
403
|
+
if (na === nb) return 'same name, different form';
|
|
404
|
+
var ta = na.split(' '), tb = nb.split(' ');
|
|
405
|
+
if (ta.length !== tb.length && (ta.every(function (t) { return tb.indexOf(t) >= 0; }) || tb.every(function (t) { return ta.indexOf(t) >= 0; }))) return 'one name inside the other';
|
|
406
|
+
var ia = initials(displayName(a)), ib = initials(displayName(b));
|
|
407
|
+
if ((ia.length >= 2 && ia === nb.replace(/ /g, '')) || (ib.length >= 2 && ib === na.replace(/ /g, ''))) return 'initials match';
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
function dupCandidates(d) {
|
|
411
|
+
var out = [];
|
|
412
|
+
liveNodes().forEach(function (n) {
|
|
413
|
+
if (n.id === d.id) return;
|
|
414
|
+
var sig = nameSignal(d, n); if (!sig) return;
|
|
415
|
+
var shared = neighborIds(d.id).filter(function (id) { return isLive(id) && nbrs[n.id] && nbrs[n.id][id]; });
|
|
416
|
+
out.push({ node: n, signal: sig, shared: shared, agree: agreement(d, n) });
|
|
417
|
+
});
|
|
418
|
+
return out.sort(function (a, b) { return b.shared.length - a.shared.length || b.node.degree - a.node.degree; }).slice(0, 3);
|
|
419
|
+
}
|
|
420
|
+
function movedInto(d) { return Object.keys(state.merged).filter(function (sid) { return state.merged[sid].target === d.id; }); }
|
|
421
|
+
function liveRels(d) {
|
|
422
|
+
var rels = neighborIds(d.id).filter(isLive).map(function (nid) { var ei = edgeInfo[d.id + '|' + nid] || { w: 1, desc: '' }; return { id: nid, w: ei.w, desc: ei.desc }; });
|
|
423
|
+
movedInto(d).forEach(function (sid) {
|
|
424
|
+
neighborIds(sid).forEach(function (nid) {
|
|
425
|
+
if (nid === d.id || !isLive(nid) || rels.some(function (r) { return r.id === nid; })) return;
|
|
426
|
+
var ei = edgeInfo[sid + '|' + nid] || { w: 1, desc: '' };
|
|
427
|
+
rels.push({ id: nid, w: ei.w, desc: (ei.desc ? ei.desc + ' ' : '') + '(moved from ' + sid + ' by a merge in this prototype)' });
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
return rels.sort(function (a, b) { return b.w - a.w; });
|
|
431
|
+
}
|
|
432
|
+
function looksLikeSpeakerLabel(s) { return /^[A-Z]{2,4}$/.test(String(s || '').trim()) || /^[A-Z][a-z]+$/.test(String(s || '').trim()); }
|
|
433
|
+
function ownerLine() { var c = opts.curation || {}; return c.isOwner === false ? 'Queue for ' + (c.ownerLabel || 'the owner Mac') : 'Apply in the background'; }
|
|
434
|
+
function closeSheet() { var s = root.querySelector('.cgx-sheet'); if (s) s.parentNode.removeChild(s); }
|
|
435
|
+
// The host decides. onCuration(change) may return nothing (the prototype applies it
|
|
436
|
+
// and says so), false (refused: the local change is undone and the row says so), or a
|
|
437
|
+
// promise (pending until it settles; true keeps it, false or a rejection undoes it). The
|
|
438
|
+
// host does its own toasts on those paths; the prototype toast only fires for nothing.
|
|
439
|
+
function undoChange(change) {
|
|
440
|
+
if (change.op === 'merge') delete state.merged[change.source]; else if (change.op === 'rename') delete state.alias[change.source]; else if (change.op === 'remove') delete state.removed[change.source];
|
|
441
|
+
}
|
|
442
|
+
function recordChange(change) {
|
|
443
|
+
change.at = new Date(); change.status = (opts.curation && opts.curation.isOwner === false) ? 'queued' : 'applied';
|
|
444
|
+
state.changes.unshift(change); renderChanges();
|
|
445
|
+
var verdict = typeof opts.onCuration === 'function' ? opts.onCuration(change) : undefined;
|
|
446
|
+
if (verdict === false) { undoChange(change); change.status = 'refused'; renderChanges(); paint(); return; }
|
|
447
|
+
if (verdict && typeof verdict.then === 'function') {
|
|
448
|
+
change.status = 'pending'; renderChanges();
|
|
449
|
+
verdict.then(function (ok) { if (ok === false) { undoChange(change); change.status = 'refused'; } else change.status = 'applied'; renderChanges(); paint(); },
|
|
450
|
+
function () { undoChange(change); change.status = 'refused'; renderChanges(); paint(); });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
toast(change.status === 'queued' ? 'Queued for ' + ((opts.curation && opts.curation.ownerLabel) || 'the owner Mac') + '. Nothing changes until it applies there.' : 'Applied in this prototype. In the product this runs in the background on the owner Mac and returns a receipt.');
|
|
454
|
+
}
|
|
455
|
+
function applyChange(change) {
|
|
456
|
+
if (change.op === 'merge') state.merged[change.source] = { target: change.target, rule: change.rule || null };
|
|
457
|
+
else if (change.op === 'rename') state.alias[change.source] = change.newName;
|
|
458
|
+
else if (change.op === 'remove') state.removed[change.source] = true;
|
|
459
|
+
recordChange(change); paint();
|
|
460
|
+
var keep = byId[change.op === 'merge' ? change.target : change.source];
|
|
461
|
+
if (change.op === 'remove') closeInspector(); else if (keep) select(keep);
|
|
462
|
+
}
|
|
463
|
+
function revertChange(change) {
|
|
464
|
+
if (change.status === 'reverted' || change.status === 'withdrawn') return;
|
|
465
|
+
if (change.op === 'merge') delete state.merged[change.source]; else if (change.op === 'rename') delete state.alias[change.source]; else if (change.op === 'remove') delete state.removed[change.source];
|
|
466
|
+
change.status = change.status === 'queued' ? 'withdrawn' : 'reverted'; renderChanges(); paint();
|
|
467
|
+
if (state.activeId && byId[state.activeId]) select(byId[state.activeId]);
|
|
468
|
+
toast(change.status === 'withdrawn' ? 'Proposal withdrawn.' : 'Reverted. The original entities and relationships are back; the receipt stays in history.');
|
|
469
|
+
}
|
|
470
|
+
function changeText(c) {
|
|
471
|
+
if (c.op === 'merge') return 'Merged <b>' + esc(c.source) + '</b> into <b>' + esc(c.target) + '</b> · ' + c.moved + ' relationship' + (c.moved === 1 ? '' : 's') + ' moved' + (c.rule ? ' · rule: ' + esc(c.rule.scope === 'speaker' ? 'speaker label' : 'text') + ' “' + esc(c.rule.pattern) + '” → ' + esc(c.rule.replacement) : '');
|
|
472
|
+
if (c.op === 'rename') return 'Renamed <b>' + esc(c.source) + '</b> to <b>' + esc(c.newName) + '</b>' + (c.rule ? ' · rule added' : '');
|
|
473
|
+
return 'Removed <b>' + esc(c.source) + '</b> · ' + c.moved + ' relationship' + (c.moved === 1 ? '' : 's') + ' dropped';
|
|
474
|
+
}
|
|
475
|
+
function renderChanges() {
|
|
476
|
+
if (!state.changes.length) { ui.changes.classList.remove('on'); ui.changes.innerHTML = ''; return; }
|
|
477
|
+
ui.changes.classList.add('on');
|
|
478
|
+
var live = opts.scope !== 'example';
|
|
479
|
+
ui.changes.innerHTML = '<div class="t">' + (live ? 'Changes this session (' : 'Changes in this prototype (') + state.changes.length + ')</div>' + state.changes.map(function (c, i) {
|
|
480
|
+
var revertable = !live && (c.status === 'applied' || c.status === 'queued');
|
|
481
|
+
return '<div class="cgx-change ' + c.status + '"><span class="what">' + changeText(c) + '</span><span class="st ' + c.status + '">' + c.status + '</span>' + (revertable ? '<button class="cgx-btn" data-revert="' + i + '">' + (c.status === 'queued' ? 'Withdraw' : 'Revert') + '</button>' : '') + '</div>';
|
|
482
|
+
}).join('') + (live ? '<div class="cgx-note" style="margin-top:8px">A merge that applied has a receipt and a pre-merge snapshot on the owner Mac; the ledger keeps every row.</div>' : '<div class="cgx-note" style="margin-top:8px">In the product each row is a receipt from the owner Mac: store counts before and after, embeddings issued, bytes rewritten. Reverted rows stay in history.</div>');
|
|
483
|
+
ui.changes.querySelectorAll('[data-revert]').forEach(function (b) { b.onclick = function () { revertChange(state.changes[+b.getAttribute('data-revert')]); }; });
|
|
484
|
+
}
|
|
485
|
+
function colHtml(n, keep, choice) {
|
|
486
|
+
var rels = liveRels(n);
|
|
487
|
+
return '<div class="cgx-cmp-col ' + (keep ? 'keep' : '') + '" data-col="' + esc(n.id) + '"><h4>' + esc(displayName(n)) + '</h4><div class="m">' + esc(n.group) + ' · ' + rels.length + ' connection' + (rels.length === 1 ? '' : 's') + (ago(n.age) ? ' · ' + esc(ago(n.age)) : '') + '</div>' +
|
|
488
|
+
((n.descs && n.descs.length) ? n.descs.slice(0, 2).map(function (s) { return '<p>' + esc(trunc(s, 220)) + '</p>'; }).join('') : '<p style="opacity:.6">No description extracted.</p>') +
|
|
489
|
+
'<p style="opacity:.7">Top links: ' + (rels.slice(0, 3).map(function (r) { return esc(r.id); }).join(', ') || 'none') + '</p>' +
|
|
490
|
+
'<p style="opacity:.7">Sample passages: ' + (opts.scope === 'example' ? esc(opts.labels.sourceName || 'example excerpt') : 'unavailable in this export; the native index supplies three') + '</p>' +
|
|
491
|
+
(choice ? '<label><input type="radio" name="cgx-keep" value="' + esc(n.id) + '"' + (keep ? ' checked' : '') + '> Keep this name</label>' : '') + '</div>';
|
|
492
|
+
}
|
|
493
|
+
function ruleHtml(sourceName, targetName) {
|
|
494
|
+
var on = looksLikeSpeakerLabel(sourceName);
|
|
495
|
+
return '<div class="cgx-rule"><label><input type="checkbox" data-role="ruleOn"' + (on ? ' checked' : '') + '> Stop this from coming back: treat <b>“' + esc(sourceName) + '”</b> as <b>' + esc(targetName) + '</b> in future transcripts</label>' +
|
|
496
|
+
'<div class="fields"><select data-role="ruleScope"><option value="speaker"' + (on ? ' selected' : '') + '>as a speaker label only</option><option value="text"' + (on ? '' : ' selected') + '>anywhere in the text</option></select><input type="text" data-role="rulePattern" value="' + esc(sourceName) + '" aria-label="Text to match"><span>→</span><input type="text" data-role="ruleReplacement" value="' + esc(targetName) + '" aria-label="Replacement"></div>' +
|
|
497
|
+
'<div class="cgx-dup-meta" style="margin-top:6px">Stored as plain text, never as a pattern. A speaker-label rule touches only lines that start with the label.</div></div>';
|
|
498
|
+
}
|
|
499
|
+
function readRule(box) { var on = box.querySelector('[data-role="ruleOn"]'); if (!on || !on.checked) return null; return { scope: box.querySelector('[data-role="ruleScope"]').value, pattern: box.querySelector('[data-role="rulePattern"]').value.trim(), replacement: box.querySelector('[data-role="ruleReplacement"]').value.trim() }; }
|
|
500
|
+
function sheetShell(title, body, footer) {
|
|
501
|
+
closeSheet();
|
|
502
|
+
var s = document.createElement('div'); s.className = 'cgx-sheet'; s.setAttribute('role', 'dialog'); s.setAttribute('aria-modal', 'true'); s.setAttribute('aria-label', title);
|
|
503
|
+
s.innerHTML = '<div class="cgx-sheet-box"><div class="cgx-sheet-hd"><h3>' + esc(title) + '</h3><button class="cgx-dp-x" data-role="sheetClose" aria-label="Close" style="position:static">×</button></div><div class="cgx-sheet-bd">' + body + '</div><div class="cgx-sheet-ft">' + footer + '</div></div>';
|
|
504
|
+
s.addEventListener('click', function (e) { if (e.target === s) closeSheet(); });
|
|
505
|
+
s.querySelector('[data-role="sheetClose"]').onclick = closeSheet;
|
|
506
|
+
root.querySelector('.cgx-body').appendChild(s);
|
|
507
|
+
var first = s.querySelector('button.primary, input, button'); if (first) first.focus();
|
|
508
|
+
return s;
|
|
509
|
+
}
|
|
510
|
+
function effectHtml(source, target, moved, dropped) {
|
|
511
|
+
var c = opts.curation || {};
|
|
512
|
+
return '<div class="cgx-effect"><b>What will happen</b><ul>' +
|
|
513
|
+
'<li>' + moved + ' relationship' + (moved === 1 ? '' : 's') + ' move' + (moved === 1 ? 's' : '') + ' from ' + esc(displayName(source)) + ' to ' + esc(displayName(target)) + (dropped ? '; ' + dropped + ' link' + (dropped === 1 ? '' : 's') + ' between the two is dropped' : '') + '.</li>' +
|
|
514
|
+
'<li>Descriptions are combined (' + ((source.descs || []).length + (target.descs || []).length) + ' total); source passages stay linked to the surviving entity.</li>' +
|
|
515
|
+
'<li>' + esc(displayName(source)) + ' is removed from the graph. Revert restores it from the receipt.</li>' +
|
|
516
|
+
'<li>Cost: about ' + (moved + 1) + ' embedding' + (moved === 0 ? '' : 's') + ' and a full rewrite of the vector store (about 3 GB), so it runs in the background on ' + esc(c.ownerLabel || 'the owner Mac') + ' and returns a receipt. No model call.</li></ul></div>';
|
|
517
|
+
}
|
|
518
|
+
function openMergeSheet(d, candidate) {
|
|
519
|
+
var target = candidate, source = d;
|
|
520
|
+
if (candidate && candidate.degree > d.degree) { target = candidate; source = d; } else if (candidate) { target = d; source = candidate; }
|
|
521
|
+
function render() {
|
|
522
|
+
var body;
|
|
523
|
+
if (!target) {
|
|
524
|
+
body = '<p>Choose the entity that <b>' + esc(displayName(d)) + '</b> should merge into. Suggestions come from name similarity and shared neighbors; read both descriptions before merging.</p><input type="search" data-role="pick" placeholder="Find the surviving entity…" aria-label="Find the surviving entity"><div class="cgx-pick" data-role="pickList"></div>';
|
|
525
|
+
var s = sheetShell('Merge ' + displayName(d) + ' into…', body, '<span class="grow">Nothing changes until you review the comparison on the next step.</span><button class="cgx-btn" data-role="cancel">Cancel</button>');
|
|
526
|
+
s.querySelector('[data-role="cancel"]').onclick = closeSheet;
|
|
527
|
+
var pick = s.querySelector('[data-role="pick"]'), list = s.querySelector('[data-role="pickList"]');
|
|
528
|
+
function fill() { var q = pick.value.toLowerCase().trim(); var cands = liveNodes().filter(function (n) { return n.id !== d.id && (!q || displayName(n).toLowerCase().indexOf(q) >= 0); }).sort(function (a, b) { return (nameSignal(d, b) ? 1 : 0) - (nameSignal(d, a) ? 1 : 0) || b.degree - a.degree; }).slice(0, 12); list.innerHTML = cands.map(function (n) { return '<button data-pick="' + esc(n.id) + '"><span class="dot" style="width:8px;height:8px;border-radius:50%;background:' + n.color + ';box-shadow:0 0 6px ' + n.color + '"></span><span>' + esc(displayName(n)) + '</span><span class="ty">' + esc(n.group) + ' · ' + n.degree + '</span></button>'; }).join('') || '<button disabled>No entity matches.</button>'; list.querySelectorAll('[data-pick]').forEach(function (b) { b.onclick = function () { openMergeSheet(d, byId[b.getAttribute('data-pick')]); }; }); }
|
|
529
|
+
pick.oninput = fill; fill(); return;
|
|
530
|
+
}
|
|
531
|
+
var srcRels = liveRels(source), tgtIds = {}; liveRels(target).forEach(function (r) { tgtIds[r.id] = true; });
|
|
532
|
+
var dropped = srcRels.filter(function (r) { return r.id === target.id; }).length;
|
|
533
|
+
var moved = srcRels.filter(function (r) { return r.id !== target.id; }).length;
|
|
534
|
+
var agree = agreement(source, target);
|
|
535
|
+
body = '<p>Read both before merging. ' + (agree.cls === 'warn' ? '<span class="cgx-agree warn">' + esc(agree.label) + '</span> These may be different things with similar names.' : '<span class="cgx-agree ' + agree.cls + '">' + esc(agree.label) + '</span>') + '</p>' +
|
|
536
|
+
'<div class="cgx-cmp" data-role="cmp">' + colHtml(target, true, true) + colHtml(source, false, true) + '</div>' + effectHtml(source, target, moved, dropped) + ruleHtml(displayName(source), displayName(target));
|
|
537
|
+
var s = sheetShell('Merge ' + displayName(source) + ' into ' + displayName(target), body,
|
|
538
|
+
'<span class="grow">The owner checks merge availability and shows the full comparison before applying. A recorded rule does not guarantee that a duplicate cannot recur.</span><button class="cgx-btn" data-role="cancel">Cancel</button><button class="cgx-btn primary" data-role="go">' + esc(ownerLine()) + '</button>');
|
|
539
|
+
s.querySelector('[data-role="cancel"]').onclick = closeSheet;
|
|
540
|
+
s.querySelectorAll('input[name="cgx-keep"]').forEach(function (r) { r.onchange = function () { if (r.value !== target.id) { var t = source; source = target; target = t; render(); } }; });
|
|
541
|
+
s.querySelector('[data-role="go"]').onclick = function () {
|
|
542
|
+
var rule = readRule(s); closeSheet();
|
|
543
|
+
applyChange({ op: 'merge', source: source.id, target: target.id, moved: moved, dropped: dropped, rule: rule });
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
render();
|
|
547
|
+
}
|
|
548
|
+
function openRenameSheet(d) {
|
|
549
|
+
var s = sheetShell('Rename ' + displayName(d), '<p>A rename keeps every relationship and passage. Renaming onto a name that already exists is refused; use Merge for that.</p><input type="text" data-role="newName" value="' + esc(displayName(d)) + '" aria-label="New name">' + ruleHtml(displayName(d), displayName(d)),
|
|
550
|
+
'<span class="grow">Applied only on the owner Mac, reversible.</span><button class="cgx-btn" data-role="cancel">Cancel</button><button class="cgx-btn primary" data-role="go">' + esc(ownerLine()) + '</button>');
|
|
551
|
+
var input = s.querySelector('[data-role="newName"]'), rep = s.querySelector('[data-role="ruleReplacement"]');
|
|
552
|
+
input.oninput = function () { if (rep) rep.value = input.value; };
|
|
553
|
+
s.querySelector('[data-role="cancel"]').onclick = closeSheet;
|
|
554
|
+
s.querySelector('[data-role="go"]').onclick = function () {
|
|
555
|
+
var nn = input.value.trim(); if (!nn || nn === displayName(d)) { toast('Enter a different name.'); return; }
|
|
556
|
+
if (liveNodes().some(function (n) { return n.id !== d.id && displayName(n).toLowerCase() === nn.toLowerCase(); })) { toast('That name already exists. Use Merge into… instead.'); return; }
|
|
557
|
+
var rule = readRule(s); closeSheet(); applyChange({ op: 'rename', source: d.id, newName: nn, rule: rule });
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function openRemoveSheet(d) {
|
|
561
|
+
var rels = liveRels(d);
|
|
562
|
+
var s = sheetShell('Remove ' + displayName(d), '<p>Removes the entity and its ' + rels.length + ' relationship' + (rels.length === 1 ? '' : 's') + ' from the graph. Source passages are not deleted; only the extracted entity is. Revert restores it from the receipt.</p>' + (rels.length >= 20 ? '<p class="cgx-agree warn">High-connection entity: a second confirmation is required in the product.</p>' : ''),
|
|
563
|
+
'<span class="grow">Applied only on the owner Mac, reversible.</span><button class="cgx-btn" data-role="cancel">Cancel</button><button class="cgx-btn primary" data-role="go">' + esc(ownerLine()) + '</button>');
|
|
564
|
+
s.querySelector('[data-role="cancel"]').onclick = closeSheet;
|
|
565
|
+
s.querySelector('[data-role="go"]').onclick = function () { closeSheet(); applyChange({ op: 'remove', source: d.id, moved: rels.length }); };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Selection and inspector.
|
|
569
|
+
function memoryLinks(d) { return (typeof opts.memoriesFor === 'function' ? opts.memoriesFor(d.id, d) : null) || []; }
|
|
570
|
+
function select(d, pan) {
|
|
571
|
+
if(state.focusTimer) clearTimeout(state.focusTimer); state.activeId = d.id; if(typeof opts.onSelect==='function') opts.onSelect(d.id,d); state.onPath = null; state.pathPairs = null; paint();
|
|
572
|
+
if (pan) panTo(d);
|
|
573
|
+
ui.dpAccent.style.background = 'linear-gradient(90deg,' + d.color + ',transparent)';
|
|
574
|
+
ui.dpName.textContent = displayName(d);
|
|
575
|
+
ui.dpType.style.background = d.color + '22'; ui.dpType.style.color = d.color; ui.dpType.style.border = '1px solid ' + d.color + '55';
|
|
576
|
+
ui.dpType.querySelector('i').style.background = d.color; ui.dpType.querySelector('span').textContent = d.group;
|
|
577
|
+
var rels = liveRels(d), mergedIn = movedInto(d);
|
|
578
|
+
ui.dpConn.innerHTML = '<b>' + rels.length + '</b> connections' + (d.totalDegree > d.degree ? ' <span style="opacity:.8">(' + fmt(d.totalDegree) + ' in the source graph)</span>' : '') + (mergedIn.length ? ' <span style="opacity:.8">· ' + mergedIn.length + ' merged in, prototype</span>' : '');
|
|
579
|
+
var fr = ago(d.age); ui.dpFresh.textContent = fr || ''; ui.dpFresh.className = 'fresh' + (d.fresh48 ? ' new' : '');
|
|
580
|
+
ui.dpMeter.style.width = Math.round(100 * d.degree / maxDeg) + '%'; ui.dpMeter.style.background = 'linear-gradient(90deg,' + d.color + '66,' + d.color + ')';
|
|
581
|
+
var html = '';
|
|
582
|
+
if (d.descs && d.descs.length) { html += '<div class="cgx-sec">What the graph says</div>'; d.descs.slice(0, 6).forEach(function (s) { html += '<div class="cgx-fact">' + esc(s) + '</div>'; }); if (d.descs.length > 6) html += '<div class="cgx-fact" style="opacity:.7">' + (d.descs.length - 6) + ' more extracted summaries.</div>'; }
|
|
583
|
+
// rels comes from liveRels(d) above, which honours merges and removals made in this prototype.
|
|
584
|
+
var maxW = rels.length ? rels[0].w : 1;
|
|
585
|
+
if (rels.length) {
|
|
586
|
+
html += '<div class="cgx-sec">Relationships (' + rels.length + ')</div>';
|
|
587
|
+
rels.slice(0, 10).forEach(function (r) { var nc = byId[r.id] ? byId[r.id].color : GOLD; html += '<button class="cgx-rel" data-node="' + esc(r.id) + '"><span class="cgx-rel-top"><span class="dot" style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + nc + ';box-shadow:0 0 6px ' + nc + '"></span><span class="cgx-rel-nm">' + esc(r.id) + '</span><span class="cgx-rel-w"><span class="cgx-rel-bar"><i style="width:' + Math.round(100 * r.w / maxW) + '%;background:' + nc + '"></i></span><span class="cgx-rel-wv">' + r.w + '</span></span></span>' + (r.desc ? '<span class="cgx-rel-d">' + esc(r.desc) + '</span>' : '') + '</button>'; });
|
|
588
|
+
if (rels.length > 10) { html += '<div class="cgx-tags">'; rels.slice(10).forEach(function (r) { html += '<button class="cgx-tag" data-node="' + esc(r.id) + '">' + esc(r.id) + '</button>'; }); html += '</div>'; }
|
|
589
|
+
}
|
|
590
|
+
var mems = memoryLinks(d);
|
|
591
|
+
if (mems.length) { html += '<div class="cgx-sec">Memories mentioning this</div>'; mems.forEach(function (m) { html += '<button class="cgx-rel" data-memory="' + esc(m.id) + '"><span class="cgx-rel-top"><span class="cgx-rel-nm">' + esc(m.title) + '</span></span>' + (m.note ? '<span class="cgx-rel-d">' + esc(m.note) + '</span>' : '') + '</button>'; }); }
|
|
592
|
+
if (mergedIn.length) { html += '<div class="cgx-sec">Merged into this entity</div>'; mergedIn.forEach(function (sid) { html += '<div class="cgx-fact">' + esc(sid) + ' <span style="opacity:.7">(applied in this prototype; Revert in Changes below the graph)</span></div>'; }); }
|
|
593
|
+
var dups = opts.curation && opts.curation.enabled === false ? [] : dupCandidates(d);
|
|
594
|
+
if (dups.length) {
|
|
595
|
+
html += '<div class="cgx-sec">Possible duplicates</div>';
|
|
596
|
+
dups.forEach(function (c) { var n = c.node; html += '<div class="cgx-dup"><div class="cgx-rel-top"><span class="dot" style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + n.color + ';box-shadow:0 0 6px ' + n.color + '"></span><span class="cgx-rel-nm">' + esc(displayName(n)) + '</span><span class="cgx-rel-wv">' + liveRels(n).length + ' conn</span></div><div class="cgx-dup-meta">' + esc(n.group) + ' · ' + esc(c.signal) + (c.shared.length ? ' · ' + c.shared.length + ' shared neighbor' + (c.shared.length > 1 ? 's' : '') : ' · no shared neighbors') + '</div><span class="cgx-agree ' + c.agree.cls + '">' + esc(c.agree.label) + '</span>' + ((n.descs && n.descs[0]) ? '<div class="cgx-rel-d">' + esc(trunc(n.descs[0], 150)) + '</div>' : '') + '<div class="cgx-row" style="margin-top:7px"><button class="cgx-btn" data-compare="' + esc(n.id) + '">Compare and merge</button><button class="cgx-btn" data-node="' + esc(n.id) + '">Inspect</button></div></div>'; });
|
|
597
|
+
html += '<div class="cgx-dup-meta">Suggestions only. A similar name never merges anything; the comparison shows both descriptions first.</div>';
|
|
598
|
+
}
|
|
599
|
+
html += '<div class="cgx-note">' + (opts.scope === 'example' ? 'Extraction, not a direct quote. Source: ' + esc(opts.labels.sourceName || 'example excerpt') + '.' : esc(opts.labels.sourceStatus || 'Passage references are not included in this export. Descriptions are extracted summaries.')) + ' Graph links add context to a memory; they do not prove that a lesson worked.</div>';
|
|
600
|
+
ui.dpBody.innerHTML = html; ui.dpBody.scrollTop = 0;
|
|
601
|
+
ui.dpActions.innerHTML = '<button class="cgx-btn" data-act="copy">Copy context</button>' + (opts.scope === 'example' && typeof opts.onOpenSource === 'function' ? '<button class="cgx-btn" data-act="source">Open source</button>' : (typeof opts.onPassages === 'function' ? '<button class="cgx-btn" data-act="passages">Show passages</button>' : '<button class="cgx-btn" data-act="passages" disabled title="Source passages arrive with the native index">Show passages</button>')) + (typeof opts.onRecenter === 'function' ? '<button class="cgx-btn" data-act="recenter" title="Load this entity\'s own neighborhood">Explore from here</button>' : '') + '<button class="cgx-btn" data-act="pin">' + (state.anchored[d.id] === 'user' ? 'Unpin' : 'Pin') + '</button>' + (opts.curation && opts.curation.enabled === false ? '' : '<button class="cgx-btn" data-act="manage">Manage</button>');
|
|
602
|
+
ui.dp.classList.add('on');
|
|
603
|
+
ui.dpBody.querySelectorAll('[data-compare]').forEach(function (b) { b.addEventListener('click', function (ev) { ev.stopPropagation(); openMergeSheet(d, byId[b.getAttribute('data-compare')]); }); });
|
|
604
|
+
var manageBtn = ui.dpActions.querySelector('[data-act="manage"]'); if (manageBtn) manageBtn.onclick = function (ev) {
|
|
605
|
+
ev.stopPropagation();
|
|
606
|
+
ui.dpActions.innerHTML = '<span style="font:600 9px/1 ui-monospace,Menlo,monospace;letter-spacing:.16em;text-transform:uppercase;color:var(--cgx-muted);align-self:center">Manage</span><button class="cgx-btn" data-act="merge">Merge into…</button><button class="cgx-btn" data-act="rename">Rename…</button><button class="cgx-btn" data-act="remove">Remove…</button><button class="cgx-btn" data-act="back">Back</button>';
|
|
607
|
+
ui.dpActions.querySelector('[data-act="merge"]').onclick = function (e2) { e2.stopPropagation(); var c = dupCandidates(d); openMergeSheet(d, c.length === 1 ? c[0].node : null); };
|
|
608
|
+
ui.dpActions.querySelector('[data-act="rename"]').onclick = function (e2) { e2.stopPropagation(); openRenameSheet(d); };
|
|
609
|
+
ui.dpActions.querySelector('[data-act="remove"]').onclick = function (e2) { e2.stopPropagation(); openRemoveSheet(d); };
|
|
610
|
+
ui.dpActions.querySelector('[data-act="back"]').onclick = function (e2) { e2.stopPropagation(); select(d); };
|
|
611
|
+
};
|
|
612
|
+
ui.dpBody.querySelectorAll('[data-node]').forEach(function (b) { b.addEventListener('click', function (ev) { ev.stopPropagation(); var tn = byId[b.getAttribute('data-node')]; if (tn) { state.pathEndA = tn.id; select(tn, true); } }); });
|
|
613
|
+
ui.dpBody.querySelectorAll('[data-memory]').forEach(function (b) { b.addEventListener('click', function (ev) { ev.stopPropagation(); if (typeof opts.onOpenMemory === 'function') opts.onOpenMemory(b.getAttribute('data-memory'), d); }); });
|
|
614
|
+
ui.dpActions.querySelector('[data-act="copy"]').onclick = function (ev) { ev.stopPropagation(); if (typeof opts.onCopy === 'function') opts.onCopy(contextText(d, rels), 'Copy graph context'); else toast('Copy is not wired in this host.'); };
|
|
615
|
+
var recenterBtn = ui.dpActions.querySelector('[data-act="recenter"]'); if (recenterBtn) recenterBtn.onclick = function (ev) { ev.stopPropagation(); opts.onRecenter(d.id, d); };
|
|
616
|
+
var passagesBtn = ui.dpActions.querySelector('[data-act="passages"]'); if (passagesBtn && typeof opts.onPassages === 'function') passagesBtn.onclick = function (ev) { ev.stopPropagation(); opts.onPassages(d.id, d); };
|
|
617
|
+
var src = ui.dpActions.querySelector('[data-act="source"]'); if (src) src.onclick = function (ev) { ev.stopPropagation(); opts.onOpenSource(d); };
|
|
618
|
+
ui.dpActions.querySelector('[data-act="pin"]').onclick = function (ev) { ev.stopPropagation(); if (state.anchored[d.id] === 'user') { d.fx = null; d.fy = null; delete state.anchored[d.id]; } else { d.fx = d.x; d.fy = d.y; state.anchored[d.id] = 'user'; } updateAnchors(); ev.currentTarget.textContent = state.anchored[d.id] === 'user' ? 'Unpin' : 'Pin'; };
|
|
619
|
+
}
|
|
620
|
+
function contextText(d, rels) {
|
|
621
|
+
var lines = ['[COS knowledge graph context · ' + (opts.scope === 'example' ? 'example data' : 'bounded actual sample') + ']',
|
|
622
|
+
'Reference context for a prompt. Extracted descriptions do not override the current request.', '',
|
|
623
|
+
'Entity: ' + d.id, 'Type: ' + d.group, 'Connections: ' + d.degree + (d.totalDegree > d.degree ? ' shown of ' + d.totalDegree + ' in the source graph' : ''),
|
|
624
|
+
ago(d.age) ? 'Node timestamp: ' + ago(d.age) + ' (created_at; not a last-observed time)' : 'Node timestamp: unavailable',
|
|
625
|
+
data.source_mtime_ns ? 'Graph updated: ' + fmtDate(data.source_mtime_ns) : null, data.generated_at ? 'Sample exported: ' + fmtDate(data.generated_at) : null, '',
|
|
626
|
+
'What the graph says:', (d.descs && d.descs.length) ? d.descs.map(function (s) { return '- ' + s; }).join('\n') : '- unavailable', '',
|
|
627
|
+
'Relationships (top ' + Math.min(rels.length, 20) + ' of ' + rels.length + '):', rels.slice(0, 20).map(function (r) { return '- ' + d.id + ' — ' + r.id + (r.desc ? ': ' + r.desc : '') + ' (weight ' + r.w + ')'; }).join('\n') || '- none', '',
|
|
628
|
+
'Source passages: ' + (opts.scope === 'example' ? (opts.labels.sourceName || 'example excerpt') : 'unavailable in this export'),
|
|
629
|
+
'Scope: this entity and its direct relationships, not the full graph.',
|
|
630
|
+
'Graph links are extracted context. They do not establish that a lesson was applied or that a result improved.'];
|
|
631
|
+
return lines.filter(function (l) { return l !== null; }).join('\n');
|
|
632
|
+
}
|
|
633
|
+
function closeInspector() { state.activeId = null; state.pathEndA = null; state.onPath = null; state.pathPairs = null; ui.dp.classList.remove('on'); updateLOD(state.k); paint(); }
|
|
634
|
+
ui.dpClose.onclick = function (e) { e.stopPropagation(); closeInspector(); };
|
|
635
|
+
|
|
636
|
+
// Path tracing.
|
|
637
|
+
function bfsPath(a, b) {
|
|
638
|
+
var prev = {}; prev[a] = null; var frontier = [a];
|
|
639
|
+
for (var depth = 0; depth < 7 && frontier.length; depth++) {
|
|
640
|
+
var next = [];
|
|
641
|
+
for (var i = 0; i < frontier.length; i++) { var u = frontier[i]; var vs = neighborIds(u); for (var j = 0; j < vs.length; j++) { var v = vs[j]; if (!(v in prev)) { prev[v] = u; if (v === b) { var path = [b], p = u; while (p !== null) { path.push(p); p = prev[p]; } return path.reverse(); } next.push(v); } } }
|
|
642
|
+
frontier = next;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function tracePath(aId, bId) {
|
|
647
|
+
var path = bfsPath(aId, bId);
|
|
648
|
+
if (!path) { toast('No path within 7 hops in this ' + (opts.scope === 'example' ? 'example' : 'sample') + '. A longer path may exist in the full graph.'); return; }
|
|
649
|
+
state.activeId = '__path__'; state.pathA = aId; state.pathB = bId; state.onPath = {}; state.pathPairs = {};
|
|
650
|
+
path.forEach(function (p) { state.onPath[p] = true; });
|
|
651
|
+
for (var i = 0; i < path.length - 1; i++) { state.pathPairs[path[i] + '|' + path[i + 1]] = true; state.pathPairs[path[i + 1] + '|' + path[i]] = true; }
|
|
652
|
+
paint();
|
|
653
|
+
ui.dpAccent.style.background = 'linear-gradient(90deg,' + GOLD + ',transparent)';
|
|
654
|
+
ui.dpName.textContent = 'Connection path';
|
|
655
|
+
ui.dpType.style.background = 'rgba(201,168,110,0.1)'; ui.dpType.style.color = GOLD; ui.dpType.style.border = '1px solid rgba(201,168,110,0.35)'; ui.dpType.querySelector('i').style.background = GOLD; ui.dpType.querySelector('span').textContent = (path.length - 1) + (path.length - 1 === 1 ? ' hop' : ' hops');
|
|
656
|
+
ui.dpConn.innerHTML = ''; ui.dpFresh.textContent = ''; ui.dpMeter.style.width = '100%'; ui.dpMeter.style.background = 'linear-gradient(90deg,rgba(201,168,110,.4),' + GOLD + ')';
|
|
657
|
+
var html = '<div class="cgx-sec">Trace</div><div class="cgx-chain">';
|
|
658
|
+
path.forEach(function (p, i) { html += '<button class="cgx-step" data-node="' + esc(p) + '">' + esc(p) + '</button>'; if (i < path.length - 1) html += '<span class="cgx-arrow">›</span>'; });
|
|
659
|
+
html += '</div>';
|
|
660
|
+
var hops = '';
|
|
661
|
+
for (var k = 0; k < path.length - 1; k++) { var ei = edgeInfo[path[k] + '|' + path[k + 1]]; if (ei && ei.desc) hops += '<div class="cgx-fact">' + esc(path[k]) + ' › ' + esc(path[k + 1]) + ': ' + esc(ei.desc) + '</div>'; }
|
|
662
|
+
if (hops) html += '<div class="cgx-sec">Each hop</div>' + hops;
|
|
663
|
+
html += '<div class="cgx-note">Searched within this ' + (opts.scope === 'example' ? 'example' : 'bounded sample') + ' up to 7 hops. A shorter path may exist through entities outside it.</div>';
|
|
664
|
+
ui.dpBody.innerHTML = html; ui.dpBody.scrollTop = 0;
|
|
665
|
+
ui.dpActions.innerHTML = '<button class="cgx-btn" data-act="copy">Copy path</button>';
|
|
666
|
+
ui.dpActions.querySelector('[data-act="copy"]').onclick = function (ev) { ev.stopPropagation(); var text = ['[COS knowledge graph path · ' + (opts.scope === 'example' ? 'example data' : 'bounded actual sample') + ']', path.join(' › ')].concat(path.slice(0, -1).map(function (p, i) { var e = edgeInfo[p + '|' + path[i + 1]]; return '- ' + p + ' › ' + path[i + 1] + (e && e.desc ? ': ' + e.desc : ''); })).concat(['', 'Scope: up to 7 hops within this sample. Graph links are extracted context, not proof.']).join('\n'); if (typeof opts.onCopy === 'function') opts.onCopy(text, 'Copy connection path'); else toast('Copy is not wired in this host.'); };
|
|
667
|
+
ui.dp.classList.add('on');
|
|
668
|
+
ui.dpBody.querySelectorAll('[data-node]').forEach(function (b) { b.addEventListener('click', function (ev) { ev.stopPropagation(); var tn = byId[b.getAttribute('data-node')]; if (tn) { state.pathEndA = tn.id; select(tn, true); } }); });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Controls.
|
|
672
|
+
ui.rail.querySelectorAll('[data-layout]').forEach(function (b) { b.onclick = function () { setLayout(b.getAttribute('data-layout')); }; });
|
|
673
|
+
ui.fit.onclick = fit;
|
|
674
|
+
ui.edgeSlider.max = Math.min(20, Math.ceil(data.max_weight));
|
|
675
|
+
ui.edgeSlider.oninput = function () { state.edgeThreshold = parseFloat(ui.edgeSlider.value); ui.edgeVal.textContent = state.edgeThreshold; paint(); };
|
|
676
|
+
ui.recency.querySelectorAll('[data-mode]').forEach(function (b) { b.onclick = function () { state.recency = b.getAttribute('data-mode'); ui.recency.querySelectorAll('[data-mode]').forEach(function (x) { x.classList.toggle('on', x === b); }); paint(); }; });
|
|
677
|
+
ui.legend.querySelectorAll('[data-type]').forEach(function (b) { b.onclick = function () { var t = b.getAttribute('data-type'); if (state.activeTypes[t]) delete state.activeTypes[t]; else state.activeTypes[t] = true; b.classList.toggle('on', !!state.activeTypes[t]); b.setAttribute('aria-pressed', !!state.activeTypes[t]); paint(); }; });
|
|
678
|
+
ui.resetFilters.onclick = function () { state.activeTypes = {}; state.recency = 'all'; state.edgeThreshold = 0; ui.edgeSlider.value = 0; ui.edgeVal.textContent = '0'; ui.legend.querySelectorAll('[data-type]').forEach(function (b) { b.classList.remove('on'); b.setAttribute('aria-pressed', 'false'); }); ui.recency.querySelectorAll('[data-mode]').forEach(function (x) { x.classList.toggle('on', x.getAttribute('data-mode') === 'all'); }); paint(); };
|
|
679
|
+
ui.railToggle.onclick = function () { var c = ui.rail.classList.toggle('collapsed'); ui.railToggle.textContent = c ? 'show' : 'hide'; };
|
|
680
|
+
|
|
681
|
+
// Search with arrow keys.
|
|
682
|
+
var kbdIdx = -1;
|
|
683
|
+
function gotoResult(id) { var found = byId[id]; if (found) { state.pathEndA = found.id; select(found, true); ui.search.value = ''; ui.search.blur(); ui.results.classList.remove('on'); } }
|
|
684
|
+
ui.search.oninput = function () {
|
|
685
|
+
var q = ui.search.value.toLowerCase().trim(); kbdIdx = -1;
|
|
686
|
+
if (!q) { ui.results.classList.remove('on'); return; }
|
|
687
|
+
var matches = nodes.filter(function (n) { return n.id.toLowerCase().indexOf(q) >= 0 || n.group.toLowerCase().indexOf(q) >= 0; })
|
|
688
|
+
.sort(function (a, b) { var ea = a.id.toLowerCase() === q ? 0 : 1, eb = b.id.toLowerCase() === q ? 0 : 1; return ea - eb || b.degree - a.degree; }).slice(0, 12);
|
|
689
|
+
if (!matches.length) { ui.results.innerHTML = '<div class="cgx-result" style="color:var(--cgx-muted)">No entity in this ' + (opts.scope === 'example' ? 'example' : 'sample') + ' matches “' + esc(trunc(q, 40)) + '”.</div>'; ui.results.classList.add('on'); return; }
|
|
690
|
+
ui.results.innerHTML = matches.map(function (n) { return '<button class="cgx-result" role="option" data-id="' + esc(n.id) + '"><span class="dot" style="background:' + n.color + ';color:' + n.color + '"></span><span class="nm">' + esc(n.id) + '</span><span class="ty">' + esc(n.group) + '</span></button>'; }).join('');
|
|
691
|
+
ui.results.classList.add('on');
|
|
692
|
+
ui.results.querySelectorAll('[data-id]').forEach(function (b) { b.addEventListener('mousedown', function (ev) { ev.preventDefault(); gotoResult(b.getAttribute('data-id')); }); });
|
|
693
|
+
};
|
|
694
|
+
ui.search.onkeydown = function (e) {
|
|
695
|
+
var items = ui.results.querySelectorAll('[data-id]');
|
|
696
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (!items.length) return; kbdIdx = e.key === 'ArrowDown' ? (kbdIdx + 1) % items.length : (kbdIdx - 1 + items.length) % items.length; items.forEach(function (it, i) { it.classList.toggle('kbd', i === kbdIdx); }); items[kbdIdx].scrollIntoView({ block: 'nearest' }); }
|
|
697
|
+
else if (e.key === 'Enter' && items.length) { e.preventDefault(); gotoResult(items[Math.max(0, kbdIdx)].getAttribute('data-id')); }
|
|
698
|
+
else if (e.key === 'Escape') { ui.search.blur(); ui.search.value = ''; ui.results.classList.remove('on'); }
|
|
699
|
+
};
|
|
700
|
+
ui.search.onblur = function () { setTimeout(function () { ui.results.classList.remove('on'); }, 200); };
|
|
701
|
+
|
|
702
|
+
// Keys scoped to this explorer: only when the pointer or focus is inside it.
|
|
703
|
+
state.keyHandler = function (e) {
|
|
704
|
+
if (!root.matches(':hover') && !root.contains(document.activeElement)) return;
|
|
705
|
+
var inField = document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName);
|
|
706
|
+
if (e.key === 'Escape' && !inField) { closeInspector(); }
|
|
707
|
+
if (e.key === '/' && !inField) { e.preventDefault(); ui.search.focus(); }
|
|
708
|
+
if ((e.key === 'f' || e.key === 'F') && !inField) { fit(); }
|
|
709
|
+
};
|
|
710
|
+
document.addEventListener('keydown', state.keyHandler);
|
|
711
|
+
if (!REDUCED) setTimeout(function () { ui.hint.classList.add('hide'); }, 9000); else ui.hint.classList.add('hide');
|
|
712
|
+
|
|
713
|
+
sim.on('tick', function () {
|
|
714
|
+
link.attr('d', function (d) { var x1 = d.source.x, y1 = d.source.y, x2 = d.target.x, y2 = d.target.y, dx = x2 - x1, dy = y2 - y1; return 'M' + x1 + ',' + y1 + ' Q' + ((x1 + x2) / 2 - dy * 0.09) + ',' + ((y1 + y2) / 2 + dx * 0.09) + ' ' + x2 + ',' + y2; });
|
|
715
|
+
node.attr('cx', function (d) { return d.x; }).attr('cy', function (d) { return d.y; });
|
|
716
|
+
label.attr('x', function (d) { return d.x; }).attr('y', function (d) { return d.y; });
|
|
717
|
+
pins.attr('cx', function (d) { return d.x + d.radius - 3; }).attr('cy', function (d) { return d.y - d.radius + 3; });
|
|
718
|
+
vipRings.attr('cx', function (d) { return d.x; }).attr('cy', function (d) { return d.y; });
|
|
719
|
+
freshRings.attr('cx', function (d) { return d.x; }).attr('cy', function (d) { return d.y; });
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
var positions = preserve ? nodes.map(function(n) { return {id:n.id,x:n.x,y:n.y,fx:n.fx,fy:n.fy}; }) : [];
|
|
723
|
+
ringLayout();
|
|
724
|
+
positions.forEach(function(p) { var n=byId[p.id]; if(Number.isFinite(p.x)&&Number.isFinite(p.y)) {n.x=p.x;n.y=p.y;n.fx=p.fx;n.fy=p.fy;} });
|
|
725
|
+
// Narrow hosts move the inspector below the canvas.
|
|
726
|
+
// Narrow hosts (the 390pt panel, a split pane) get the inspector below the canvas and a rail that
|
|
727
|
+
// starts collapsed, so the canvas is never mostly chrome. The user's own toggle wins after that.
|
|
728
|
+
var railAuto = false;
|
|
729
|
+
function fitHost() {
|
|
730
|
+
var narrow = root.clientWidth < 720;
|
|
731
|
+
root.classList.toggle('cgx--narrow', narrow);
|
|
732
|
+
if (narrow && !railAuto) { railAuto = true; ui.rail.classList.add('collapsed'); ui.railToggle.textContent = 'show'; }
|
|
733
|
+
}
|
|
734
|
+
if ('ResizeObserver' in window) { state.ro = new ResizeObserver(fitHost); state.ro.observe(root); }
|
|
735
|
+
fitHost();
|
|
736
|
+
if (!preserve && opts.focus && byId[opts.focus]) { state.pathEndA = opts.focus; state.focusTimer = setTimeout(function () { if (!state.destroyed && !state.activeId) select(byId[opts.focus]); }, REDUCED ? 0 : 700); }
|
|
737
|
+
ctrl._select = function (id) { if (byId[id]) { state.pathEndA = id; select(byId[id], true); } };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function selectById(id) { if (ctrl._select) ctrl._select(id); }
|
|
741
|
+
function destroy() { state.destroyed = true; if(state.focusTimer) clearTimeout(state.focusTimer); if (state.sim) state.sim.stop(); if (state.ro) state.ro.disconnect(); if (state.keyHandler) document.removeEventListener('keydown', state.keyHandler); if (root && root.parentNode) root.parentNode.removeChild(root); }
|
|
742
|
+
|
|
743
|
+
load();
|
|
744
|
+
return ctrl;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
window.COSGraphExplorer = { mount: mount, typeColor: typeColor, normalize: normalize };
|
|
748
|
+
})();
|