@gamaze/hicortex 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/THIRD_PARTY_NOTICES.md +108 -0
- package/assets/vendor/3d-force-graph.min.js +5 -0
- package/assets/vendor/force-graph.min.js +5 -0
- package/assets/vendor/three.core.min.js +6 -0
- package/assets/vendor/three.module.min.js +6 -0
- package/assets/viz.html +1126 -0
- package/dist/classify-domains.d.ts +98 -0
- package/dist/classify-domains.js +340 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +63 -0
- package/dist/consolidate.d.ts +139 -2
- package/dist/consolidate.js +302 -87
- package/dist/db.js +70 -0
- package/dist/domain-classify.d.ts +164 -0
- package/dist/domain-classify.js +300 -0
- package/dist/extensions.d.ts +12 -0
- package/dist/graph.d.ts +56 -0
- package/dist/graph.js +145 -0
- package/dist/index.js +1 -1
- package/dist/init.d.ts +25 -0
- package/dist/init.js +54 -0
- package/dist/lesson-selection.js +12 -5
- package/dist/lessons-context.js +2 -1
- package/dist/llm.d.ts +67 -0
- package/dist/llm.js +122 -0
- package/dist/mcp-server.js +82 -27
- package/dist/nightly-status.js +9 -28
- package/dist/nightly.js +42 -32
- package/dist/nofit.d.ts +111 -0
- package/dist/nofit.js +176 -0
- package/dist/prompts.d.ts +0 -5
- package/dist/prompts.js +5 -29
- package/dist/relink.d.ts +100 -0
- package/dist/relink.js +277 -0
- package/dist/retrieval.d.ts +16 -1
- package/dist/retrieval.js +34 -2
- package/dist/schema-prototypes.d.ts +149 -0
- package/dist/schema-prototypes.js +329 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.js +29 -0
- package/dist/status.js +12 -19
- package/dist/storage.d.ts +44 -1
- package/dist/storage.js +70 -1
- package/dist/types.d.ts +90 -0
- package/dist/viz.d.ts +69 -0
- package/dist/viz.js +180 -0
- package/domains.example.json +36 -0
- package/package.json +6 -3
package/assets/viz.html
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hicortex — knowledge graph</title>
|
|
7
|
+
<!--
|
|
8
|
+
Hicortex knowledge-graph visualization (#124, redesigned in #139).
|
|
9
|
+
3D force-directed view by default (3d-force-graph + three.js), 2D toggle
|
|
10
|
+
(force-graph). All renderer bundles are vendored and served same-origin by
|
|
11
|
+
this daemon from /viz/vendor/ — zero external requests, works offline.
|
|
12
|
+
Versions + licenses: THIRD_PARTY_NOTICES.md in the package.
|
|
13
|
+
Data source: GET /graph?op=export on this server (same origin, bearer-only).
|
|
14
|
+
|
|
15
|
+
Script loading order (guaranteed by the HTML spec — module scripts and
|
|
16
|
+
deferred classic scripts execute in document order after parsing, before
|
|
17
|
+
DOMContentLoaded):
|
|
18
|
+
1. inline module: imports vendored three, publishes window.THREE
|
|
19
|
+
2. 3d-force-graph.min.js (UMD, deferred) — picks up window.THREE so the
|
|
20
|
+
page's sprite code and the renderer share ONE three instance
|
|
21
|
+
3. force-graph.min.js (UMD, deferred)
|
|
22
|
+
4. DOMContentLoaded → boot() from the main inline script below
|
|
23
|
+
-->
|
|
24
|
+
<script type="module">
|
|
25
|
+
import * as THREE from "/viz/vendor/three.module.min.js";
|
|
26
|
+
window.THREE = THREE;
|
|
27
|
+
</script>
|
|
28
|
+
<script src="/viz/vendor/3d-force-graph.min.js" defer></script>
|
|
29
|
+
<script src="/viz/vendor/force-graph.min.js" defer></script>
|
|
30
|
+
<style>
|
|
31
|
+
:root {
|
|
32
|
+
--bg: #0f1117;
|
|
33
|
+
--panel: #161a23;
|
|
34
|
+
--panel-border: #262c3a;
|
|
35
|
+
--text: #d5dae4;
|
|
36
|
+
--text-dim: #8b93a5;
|
|
37
|
+
--accent: #5b9dd9;
|
|
38
|
+
--danger: #e05555;
|
|
39
|
+
}
|
|
40
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
41
|
+
html, body { height: 100%; overflow: hidden; }
|
|
42
|
+
body {
|
|
43
|
+
background: var(--bg);
|
|
44
|
+
color: var(--text);
|
|
45
|
+
font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ---- graph containers (full-viewport, panels float above) ---- */
|
|
49
|
+
#graph-3d, #graph-2d { position: fixed; inset: 0; }
|
|
50
|
+
#graph-2d { display: none; }
|
|
51
|
+
body.mode-2d #graph-3d { display: none; }
|
|
52
|
+
body.mode-2d #graph-2d { display: block; }
|
|
53
|
+
|
|
54
|
+
/* ---- top bar ---- */
|
|
55
|
+
#topbar {
|
|
56
|
+
position: fixed; top: 0; left: 0; right: 0;
|
|
57
|
+
display: flex; align-items: center; gap: 10px;
|
|
58
|
+
padding: 8px 12px;
|
|
59
|
+
background: rgba(22, 26, 35, 0.94);
|
|
60
|
+
border-bottom: 1px solid var(--panel-border);
|
|
61
|
+
z-index: 10;
|
|
62
|
+
}
|
|
63
|
+
#topbar h1 { font-size: 14px; font-weight: 600; white-space: nowrap; }
|
|
64
|
+
#meta { color: var(--text-dim); margin-left: auto; white-space: nowrap; }
|
|
65
|
+
|
|
66
|
+
/* ---- left control panel ---- */
|
|
67
|
+
#panel {
|
|
68
|
+
position: fixed; top: 46px; left: 12px; width: 236px;
|
|
69
|
+
background: rgba(22, 26, 35, 0.94);
|
|
70
|
+
border: 1px solid var(--panel-border); border-radius: 8px;
|
|
71
|
+
padding: 12px; z-index: 10;
|
|
72
|
+
max-height: calc(100vh - 70px); overflow-y: auto;
|
|
73
|
+
}
|
|
74
|
+
#panel h2 {
|
|
75
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
76
|
+
color: var(--text-dim); margin: 14px 0 6px;
|
|
77
|
+
}
|
|
78
|
+
#panel h2:first-child { margin-top: 0; }
|
|
79
|
+
#panel .row { display: flex; align-items: center; gap: 6px; margin: 5px 0; color: var(--text-dim); }
|
|
80
|
+
#panel .row > label:first-child { flex: none; width: 78px; }
|
|
81
|
+
#panel select, #panel input[type="number"], #panel input[type="text"] {
|
|
82
|
+
flex: 1; min-width: 0;
|
|
83
|
+
background: var(--bg); color: var(--text);
|
|
84
|
+
border: 1px solid var(--panel-border); border-radius: 4px;
|
|
85
|
+
padding: 3px 6px; font: inherit;
|
|
86
|
+
}
|
|
87
|
+
#panel input[type="range"] { flex: 1; min-width: 0; accent-color: var(--accent); }
|
|
88
|
+
#panel .val { flex: none; width: 34px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
89
|
+
#panel button {
|
|
90
|
+
background: var(--accent); color: #0b0d12; font: inherit; font-weight: 600;
|
|
91
|
+
border: none; border-radius: 4px; padding: 5px 12px; cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
#panel button:hover { filter: brightness(1.1); }
|
|
94
|
+
#panel button.secondary { background: var(--panel-border); color: var(--text); }
|
|
95
|
+
#apply { width: 100%; margin-top: 4px; }
|
|
96
|
+
|
|
97
|
+
/* ---- iOS-style 2D/3D switch ---- */
|
|
98
|
+
.switch { position: relative; display: inline-block; width: 40px; height: 22px; flex: none; }
|
|
99
|
+
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
|
100
|
+
.switch .knob {
|
|
101
|
+
position: absolute; inset: 0; cursor: pointer;
|
|
102
|
+
background: var(--panel-border); border-radius: 22px; transition: background 0.15s;
|
|
103
|
+
}
|
|
104
|
+
.switch .knob::before {
|
|
105
|
+
content: ""; position: absolute; width: 18px; height: 18px; left: 2px; top: 2px;
|
|
106
|
+
background: var(--text); border-radius: 50%; transition: transform 0.15s;
|
|
107
|
+
}
|
|
108
|
+
.switch input:checked + .knob { background: var(--accent); }
|
|
109
|
+
.switch input:checked + .knob::before { transform: translateX(18px); }
|
|
110
|
+
.switch input:focus-visible + .knob { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
111
|
+
.mode-label { color: var(--text-dim); }
|
|
112
|
+
.mode-label.active { color: var(--text); font-weight: 600; }
|
|
113
|
+
|
|
114
|
+
/* ---- per-control tooltips (data-tip, CSS-only, 0.4s hover delay) ---- */
|
|
115
|
+
[data-tip] { position: relative; }
|
|
116
|
+
[data-tip]::after {
|
|
117
|
+
content: attr(data-tip);
|
|
118
|
+
position: absolute; left: 0; top: calc(100% + 6px); z-index: 30; width: 220px;
|
|
119
|
+
background: rgba(15, 17, 23, 0.97); border: 1px solid var(--panel-border);
|
|
120
|
+
border-radius: 5px; padding: 6px 8px;
|
|
121
|
+
color: var(--text); font-size: 11.5px; line-height: 1.4; font-weight: 400;
|
|
122
|
+
white-space: normal; pointer-events: none;
|
|
123
|
+
opacity: 0; visibility: hidden;
|
|
124
|
+
transition: opacity 0.12s ease 0s, visibility 0s linear 0.5s;
|
|
125
|
+
}
|
|
126
|
+
[data-tip]:hover::after {
|
|
127
|
+
opacity: 1; visibility: visible;
|
|
128
|
+
transition-delay: 0.4s, 0.4s;
|
|
129
|
+
}
|
|
130
|
+
/* Rows near the panel bottom open their tooltip upward — #panel scrolls
|
|
131
|
+
(overflow-y: auto), which would clip a downward tooltip at the edge. */
|
|
132
|
+
.tip-up[data-tip]::after { top: auto; bottom: calc(100% + 6px); }
|
|
133
|
+
|
|
134
|
+
#panel .hint {
|
|
135
|
+
margin-top: 12px; border-top: 1px solid var(--panel-border); padding-top: 8px;
|
|
136
|
+
color: var(--text-dim); font-size: 11.5px; line-height: 1.4;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ---- zoom controls (bottom-right) ---- */
|
|
140
|
+
#zoomctl {
|
|
141
|
+
position: fixed; bottom: 12px; right: 12px; z-index: 9;
|
|
142
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
143
|
+
}
|
|
144
|
+
#zoomctl button {
|
|
145
|
+
width: 34px; height: 34px; font-size: 17px; line-height: 1;
|
|
146
|
+
background: rgba(22, 26, 35, 0.94); color: var(--text);
|
|
147
|
+
border: 1px solid var(--panel-border); border-radius: 6px; cursor: pointer;
|
|
148
|
+
}
|
|
149
|
+
#zoomctl button:hover { border-color: var(--accent); color: var(--accent); }
|
|
150
|
+
|
|
151
|
+
/* ---- legend (bottom-right, above detail toggle area) ---- */
|
|
152
|
+
#legend {
|
|
153
|
+
position: fixed; bottom: 12px; left: 12px;
|
|
154
|
+
background: rgba(22, 26, 35, 0.94);
|
|
155
|
+
border: 1px solid var(--panel-border); border-radius: 6px;
|
|
156
|
+
padding: 10px 12px; max-width: 236px; z-index: 9;
|
|
157
|
+
max-height: 34vh; overflow-y: auto;
|
|
158
|
+
}
|
|
159
|
+
#legend h2 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin: 6px 0 4px; }
|
|
160
|
+
#legend h2:first-child { margin-top: 0; }
|
|
161
|
+
.legend-row { display: flex; align-items: center; gap: 7px; padding: 1.5px 0; color: var(--text-dim); }
|
|
162
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
|
|
163
|
+
.legend-line { width: 22px; height: 0; flex: none; border-top-width: 2px; }
|
|
164
|
+
.legend-halo { box-shadow: 0 0 0 2.5px rgba(255,255,255,0.55); }
|
|
165
|
+
|
|
166
|
+
/* ---- detail panel ---- */
|
|
167
|
+
#detail {
|
|
168
|
+
position: fixed; top: 0; right: 0; bottom: 0; width: 380px; max-width: 90vw;
|
|
169
|
+
background: var(--panel); border-left: 1px solid var(--panel-border);
|
|
170
|
+
z-index: 15; display: none; flex-direction: column;
|
|
171
|
+
}
|
|
172
|
+
#detail.open { display: flex; }
|
|
173
|
+
#detail header {
|
|
174
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
175
|
+
padding: 14px 14px 10px; border-bottom: 1px solid var(--panel-border);
|
|
176
|
+
}
|
|
177
|
+
#detail header h2 { font-size: 14px; flex: 1; word-break: break-word; }
|
|
178
|
+
#detail-close {
|
|
179
|
+
background: none; border: none; color: var(--text-dim);
|
|
180
|
+
font-size: 18px; cursor: pointer; line-height: 1; padding: 0 2px;
|
|
181
|
+
}
|
|
182
|
+
#detail-close:hover { color: var(--text); }
|
|
183
|
+
#detail-meta { padding: 10px 14px; border-bottom: 1px solid var(--panel-border); }
|
|
184
|
+
#detail-meta div { display: flex; gap: 8px; padding: 1.5px 0; }
|
|
185
|
+
#detail-meta dt { color: var(--text-dim); width: 90px; flex: none; }
|
|
186
|
+
#detail-meta dd { word-break: break-word; }
|
|
187
|
+
#detail-content {
|
|
188
|
+
padding: 12px 14px; overflow-y: auto; flex: 1;
|
|
189
|
+
white-space: pre-wrap; word-break: break-word; font-size: 12.5px; color: var(--text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ---- overlays: token prompt / status ---- */
|
|
193
|
+
#overlay {
|
|
194
|
+
position: fixed; inset: 0; display: none; z-index: 30;
|
|
195
|
+
align-items: center; justify-content: center;
|
|
196
|
+
background: rgba(15, 17, 23, 0.85);
|
|
197
|
+
}
|
|
198
|
+
#overlay.open { display: flex; }
|
|
199
|
+
#overlay .box {
|
|
200
|
+
background: var(--panel); border: 1px solid var(--panel-border);
|
|
201
|
+
border-radius: 8px; padding: 22px; max-width: 420px; text-align: center;
|
|
202
|
+
}
|
|
203
|
+
#overlay .box h2 { font-size: 15px; margin-bottom: 8px; }
|
|
204
|
+
#overlay .box p { color: var(--text-dim); margin-bottom: 12px; }
|
|
205
|
+
#overlay .box code { background: var(--bg); padding: 1px 5px; border-radius: 3px; }
|
|
206
|
+
#overlay input {
|
|
207
|
+
width: 100%; background: var(--bg); color: var(--text);
|
|
208
|
+
border: 1px solid var(--panel-border); border-radius: 4px;
|
|
209
|
+
padding: 6px 8px; font: inherit; margin-bottom: 10px;
|
|
210
|
+
}
|
|
211
|
+
#overlay button {
|
|
212
|
+
background: var(--accent); color: #0b0d12; font: inherit; font-weight: 600;
|
|
213
|
+
border: none; border-radius: 4px; padding: 6px 16px; cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
#overlay .err { color: var(--danger); margin-top: 8px; display: none; }
|
|
216
|
+
|
|
217
|
+
#status {
|
|
218
|
+
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
219
|
+
color: var(--text-dim); z-index: 5; display: none; text-align: center;
|
|
220
|
+
max-width: 420px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* graph tooltips (both renderers use .graph-tooltip / .scene-tooltip) */
|
|
224
|
+
.graph-tooltip, .scene-tooltip {
|
|
225
|
+
font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
|
226
|
+
background: rgba(15, 17, 23, 0.96) !important;
|
|
227
|
+
color: #d5dae4 !important;
|
|
228
|
+
border: 1px solid #262c3a; border-radius: 5px;
|
|
229
|
+
padding: 6px 8px !important; max-width: 340px;
|
|
230
|
+
}
|
|
231
|
+
</style>
|
|
232
|
+
</head>
|
|
233
|
+
<body>
|
|
234
|
+
<div id="graph-3d"></div>
|
|
235
|
+
<div id="graph-2d"></div>
|
|
236
|
+
|
|
237
|
+
<div id="topbar">
|
|
238
|
+
<h1>Hicortex — knowledge graph</h1>
|
|
239
|
+
<span id="meta"></span>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div id="panel">
|
|
243
|
+
<h2>Filters</h2>
|
|
244
|
+
<div class="row" data-tip="Show only memories from one knowledge domain. Node colors match domains (see legend)."><label for="f-domain">Domain</label>
|
|
245
|
+
<select id="f-domain"><option value="">all</option></select>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="row" data-tip="Show only one memory type (episode, lesson, fact…)."><label for="f-type">Type</label>
|
|
248
|
+
<select id="f-type"><option value="">all</option></select>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="row" data-tip="Hide memories weaker than this. Strength decays over time and grows with use and links — node size shows it."><label for="f-strength">Min strength</label>
|
|
251
|
+
<input type="range" id="f-strength" min="0" max="1" step="0.05" value="0">
|
|
252
|
+
<span class="val" id="f-strength-val">0.00</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="row" data-tip="How many memories to load, strongest first (server cap: 10000)."><label for="f-limit">Node limit</label>
|
|
255
|
+
<input type="number" id="f-limit" min="1" max="10000" value="5000">
|
|
256
|
+
</div>
|
|
257
|
+
<button id="apply" data-tip="Reload the graph from the server with these filters.">Apply</button>
|
|
258
|
+
|
|
259
|
+
<h2>View</h2>
|
|
260
|
+
<div class="row" data-tip="Switch between 3D and 2D rendering of the same graph. Drag to rotate (3D) or pan (2D), scroll to zoom.">
|
|
261
|
+
<label for="mode-switch">View</label>
|
|
262
|
+
<span class="mode-label" id="mode-label-2d">2D</span>
|
|
263
|
+
<label class="switch"><input type="checkbox" id="mode-switch" checked><span class="knob"></span></label>
|
|
264
|
+
<span class="mode-label active" id="mode-label-3d">3D</span>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="row" data-tip="Generic links (similarity, extends, relates to) weaker than this are hidden to reduce clutter. Specific relationships (updates, supersedes, contradicts…) are always shown."><label for="v-simcut">Weak links</label>
|
|
267
|
+
<input type="range" id="v-simcut" min="0" max="1" step="0.05" value="0.55">
|
|
268
|
+
<span class="val" id="v-simcut-val">0.55</span>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="row tip-up" data-tip="Share of the strongest memories that keep permanent labels. Hubs are always labeled; the rest appear as you zoom in."><label for="v-labels">Label density</label>
|
|
271
|
+
<input type="range" id="v-labels" min="0" max="1" step="0.05" value="0.15">
|
|
272
|
+
<span class="val" id="v-labels-val">15%</span>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<h2>Search</h2>
|
|
276
|
+
<div class="row tip-up" data-tip="Highlight memories matching this text; everything else dims. Clear to reset.">
|
|
277
|
+
<input type="text" id="search" placeholder="highlight matching nodes…">
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div class="hint">Click a node to read the memory. White halo = hub.</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div id="zoomctl">
|
|
284
|
+
<button id="zoom-in" title="Zoom in (or scroll / pinch on the graph)">+</button>
|
|
285
|
+
<button id="zoom-out" title="Zoom out">−</button>
|
|
286
|
+
<button id="zoom-fit" title="Fit the whole graph in view">⛶</button>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div id="legend">
|
|
290
|
+
<h2>Domains</h2>
|
|
291
|
+
<div id="legend-domains"></div>
|
|
292
|
+
<h2>Node</h2>
|
|
293
|
+
<div class="legend-row"><span class="legend-dot" style="background:#8b93a5; width:6px; height:6px;"></span> weak memory</div>
|
|
294
|
+
<div class="legend-row"><span class="legend-dot" style="background:#8b93a5; width:13px; height:13px;"></span> strong memory</div>
|
|
295
|
+
<div class="legend-row"><span class="legend-dot legend-halo" style="background:#8b93a5;"></span> hub (highly linked)</div>
|
|
296
|
+
<h2>Links</h2>
|
|
297
|
+
<div id="legend-edges"></div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<aside id="detail">
|
|
301
|
+
<header>
|
|
302
|
+
<h2 id="detail-title"></h2>
|
|
303
|
+
<button id="detail-close" title="Close (Esc)">×</button>
|
|
304
|
+
</header>
|
|
305
|
+
<dl id="detail-meta"></dl>
|
|
306
|
+
<div id="detail-content"></div>
|
|
307
|
+
</aside>
|
|
308
|
+
|
|
309
|
+
<div id="overlay">
|
|
310
|
+
<div class="box">
|
|
311
|
+
<h2>Authentication required</h2>
|
|
312
|
+
<p>This server requires a token for remote access.
|
|
313
|
+
Run <code>hicortex status</code> on the server to get the token.</p>
|
|
314
|
+
<input type="password" id="token-input" placeholder="hctx-…" autocomplete="off">
|
|
315
|
+
<button id="token-submit">Connect</button>
|
|
316
|
+
<div class="err" id="token-err">Still unauthorized — check the token.</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div id="status"></div>
|
|
321
|
+
|
|
322
|
+
<script>
|
|
323
|
+
"use strict";
|
|
324
|
+
(function () {
|
|
325
|
+
// =========================================================================
|
|
326
|
+
// Auth token handling — unchanged from #124: URL ?token= handoff (stripped
|
|
327
|
+
// from history immediately), localStorage persistence, in-page 401 prompt.
|
|
328
|
+
// The token is only ever sent as an Authorization header on /graph fetches.
|
|
329
|
+
// =========================================================================
|
|
330
|
+
var token = null;
|
|
331
|
+
(function initToken() {
|
|
332
|
+
var params = new URLSearchParams(window.location.search);
|
|
333
|
+
var fromUrl = params.get("token");
|
|
334
|
+
if (fromUrl) {
|
|
335
|
+
token = fromUrl;
|
|
336
|
+
try { localStorage.setItem("hicortexToken", fromUrl); } catch (e) { /* private mode */ }
|
|
337
|
+
// Strip the token from the URL immediately — no token in history/bookmarks.
|
|
338
|
+
params.delete("token");
|
|
339
|
+
var qs = params.toString();
|
|
340
|
+
history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : ""));
|
|
341
|
+
} else {
|
|
342
|
+
try { token = localStorage.getItem("hicortexToken"); } catch (e) { token = null; }
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
|
|
346
|
+
// =========================================================================
|
|
347
|
+
// DOM references
|
|
348
|
+
// =========================================================================
|
|
349
|
+
var el3d = document.getElementById("graph-3d");
|
|
350
|
+
var el2d = document.getElementById("graph-2d");
|
|
351
|
+
var detail = document.getElementById("detail");
|
|
352
|
+
var detailTitle = document.getElementById("detail-title");
|
|
353
|
+
var detailMeta = document.getElementById("detail-meta");
|
|
354
|
+
var detailContent = document.getElementById("detail-content");
|
|
355
|
+
var overlay = document.getElementById("overlay");
|
|
356
|
+
var statusEl = document.getElementById("status");
|
|
357
|
+
var metaEl = document.getElementById("meta");
|
|
358
|
+
var selDomain = document.getElementById("f-domain");
|
|
359
|
+
var selType = document.getElementById("f-type");
|
|
360
|
+
var rngStrength = document.getElementById("f-strength");
|
|
361
|
+
var rngStrengthVal = document.getElementById("f-strength-val");
|
|
362
|
+
var inpLimit = document.getElementById("f-limit");
|
|
363
|
+
var rngSimCut = document.getElementById("v-simcut");
|
|
364
|
+
var rngSimCutVal = document.getElementById("v-simcut-val");
|
|
365
|
+
var rngLabels = document.getElementById("v-labels");
|
|
366
|
+
var rngLabelsVal = document.getElementById("v-labels-val");
|
|
367
|
+
var inpSearch = document.getElementById("search");
|
|
368
|
+
var modeSwitch = document.getElementById("mode-switch");
|
|
369
|
+
var modeLabel2d = document.getElementById("mode-label-2d");
|
|
370
|
+
var modeLabel3d = document.getElementById("mode-label-3d");
|
|
371
|
+
|
|
372
|
+
// Reflect the current mode in the switch + side labels (checked = 3D).
|
|
373
|
+
function syncModeControl() {
|
|
374
|
+
modeSwitch.checked = mode === "3d";
|
|
375
|
+
modeLabel3d.classList.toggle("active", mode === "3d");
|
|
376
|
+
modeLabel2d.classList.toggle("active", mode === "2d");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function showStatus(msg) { statusEl.textContent = msg; statusEl.style.display = "block"; }
|
|
380
|
+
function hideStatus() { statusEl.style.display = "none"; }
|
|
381
|
+
|
|
382
|
+
// The built-in hover tooltips (nodeLabel/linkLabel) render HTML — escape
|
|
383
|
+
// every piece of memory-derived text that goes into them. Everything else
|
|
384
|
+
// in the page uses textContent only (pinned security property).
|
|
385
|
+
function esc(s) {
|
|
386
|
+
return String(s == null ? "" : s)
|
|
387
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
388
|
+
.replace(/"/g, """).replace(/'/g, "'");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// =========================================================================
|
|
392
|
+
// Visual encodings — same in both views
|
|
393
|
+
// =========================================================================
|
|
394
|
+
var PALETTE = [
|
|
395
|
+
"#5b9dd9", "#e0a545", "#5fbf77", "#d96b6b", "#a07ee0", "#4fc3c3",
|
|
396
|
+
"#e07ab8", "#b3c04a", "#e08a52", "#7a8ee0", "#5fae9a", "#c9c9c9"
|
|
397
|
+
];
|
|
398
|
+
function hashStr(s) {
|
|
399
|
+
var h = 5381;
|
|
400
|
+
for (var i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
401
|
+
return h;
|
|
402
|
+
}
|
|
403
|
+
function domainColor(domain) {
|
|
404
|
+
if (!domain) return "#6b7385"; // no-domain gray
|
|
405
|
+
return PALETTE[hashStr(domain) % PALETTE.length];
|
|
406
|
+
}
|
|
407
|
+
// Edge styling: dash only where the renderer supports it (2D); the 3D view
|
|
408
|
+
// is color-only for typed edges (accepted in #139).
|
|
409
|
+
// The full relationship taxonomy the server can emit (types.ts
|
|
410
|
+
// VALID_RELATIONSHIP_TYPES): lowercase heuristic types from the linking
|
|
411
|
+
// stage's classifier + uppercase LLM-classified types. In practice the
|
|
412
|
+
// lowercase generics (extends, relates_to) dominate the live data.
|
|
413
|
+
var EDGE_STYLES = {
|
|
414
|
+
similarity: { color: "rgba(139,147,165,0.30)", color3d: "#3a4150", width: 1, dash: [] },
|
|
415
|
+
extends: { color: "rgba(139,147,165,0.35)", color3d: "#4a5468", width: 1, dash: [] },
|
|
416
|
+
relates_to: { color: "rgba(139,165,150,0.35)", color3d: "#4a6058", width: 1, dash: [] },
|
|
417
|
+
updates: { color: "#3fb8b0", color3d: "#3fb8b0", width: 1.4, dash: [] },
|
|
418
|
+
derives: { color: "#8fbf5f", color3d: "#8fbf5f", width: 1.4, dash: [] },
|
|
419
|
+
CONTRADICTS: { color: "#e05555", color3d: "#e05555", width: 1.6, dash: [5, 4] },
|
|
420
|
+
SUPERSEDES: { color: "#e0913f", color3d: "#e0913f", width: 1.6, dash: [] },
|
|
421
|
+
DEPENDS_ON: { color: "#5b9dd9", color3d: "#5b9dd9", width: 1.6, dash: [] },
|
|
422
|
+
CAUSED_BY: { color: "#a07ee0", color3d: "#a07ee0", width: 1.6, dash: [] },
|
|
423
|
+
VALIDATES: { color: "#5fbf77", color3d: "#5fbf77", width: 1.6, dash: [] }
|
|
424
|
+
};
|
|
425
|
+
var EDGE_FALLBACK = { color: "rgba(139,147,165,0.4)", color3d: "#565e70", width: 1.2, dash: [] };
|
|
426
|
+
function edgeStyle(rel) { return EDGE_STYLES[rel] || EDGE_FALLBACK; }
|
|
427
|
+
|
|
428
|
+
// Generic "these two are broadly related" links — high-volume, low-signal.
|
|
429
|
+
// The weak-links slider thresholds these; specific types always show.
|
|
430
|
+
var GENERIC_RELS = { similarity: true, extends: true, relates_to: true };
|
|
431
|
+
|
|
432
|
+
var REL_LABELS = {
|
|
433
|
+
similarity: "similarity", extends: "extends", relates_to: "relates to",
|
|
434
|
+
updates: "updates", derives: "derives",
|
|
435
|
+
CONTRADICTS: "contradicts", SUPERSEDES: "supersedes",
|
|
436
|
+
DEPENDS_ON: "depends on", CAUSED_BY: "caused by", VALIDATES: "validates"
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
var DIM_NODE = "#262c3a";
|
|
440
|
+
var DIM_EDGE_2D = "rgba(38,44,58,0.35)";
|
|
441
|
+
var DIM_EDGE_3D = "#1c202a";
|
|
442
|
+
|
|
443
|
+
function nodeVal(n) {
|
|
444
|
+
var s = Math.max(0, Math.min(1, n.strength || 0));
|
|
445
|
+
var v = 1 + s * 7; // volume/area units — renderers turn this into radius
|
|
446
|
+
return n.isHub ? v * 1.6 : v;
|
|
447
|
+
}
|
|
448
|
+
function labelText(n) { return (n.label || "").slice(0, 40); }
|
|
449
|
+
|
|
450
|
+
// Edge legend — rebuilt per fetch from the relationship types actually
|
|
451
|
+
// present in the data, so it never lists types the graph doesn't show.
|
|
452
|
+
function buildEdgeLegend(edges) {
|
|
453
|
+
var host = document.getElementById("legend-edges");
|
|
454
|
+
host.innerHTML = "";
|
|
455
|
+
var present = {};
|
|
456
|
+
(edges || []).forEach(function (e) { present[e.relationship] = true; });
|
|
457
|
+
// Stable order: EDGE_STYLES declaration order, unknown types appended
|
|
458
|
+
var types = Object.keys(EDGE_STYLES).filter(function (t) { return present[t]; });
|
|
459
|
+
Object.keys(present).forEach(function (t) {
|
|
460
|
+
if (!EDGE_STYLES[t]) types.push(t);
|
|
461
|
+
});
|
|
462
|
+
if (types.length === 0) {
|
|
463
|
+
var none = document.createElement("div");
|
|
464
|
+
none.className = "legend-row";
|
|
465
|
+
none.appendChild(document.createTextNode("no links loaded"));
|
|
466
|
+
host.appendChild(none);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
types.forEach(function (t) {
|
|
470
|
+
var st = edgeStyle(t);
|
|
471
|
+
var row = document.createElement("div");
|
|
472
|
+
row.className = "legend-row";
|
|
473
|
+
var line = document.createElement("span");
|
|
474
|
+
line.className = "legend-line";
|
|
475
|
+
line.style.borderTopStyle = st.dash.length ? "dashed" : "solid";
|
|
476
|
+
line.style.borderTopColor = st.color;
|
|
477
|
+
row.appendChild(line);
|
|
478
|
+
row.appendChild(document.createTextNode(REL_LABELS[t] || t));
|
|
479
|
+
host.appendChild(row);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// =========================================================================
|
|
484
|
+
// State
|
|
485
|
+
// =========================================================================
|
|
486
|
+
var raw = { nodes: [], edges: [], domains: [], types: [], meta: { total: 0, shown: 0, edgeCount: 0 } };
|
|
487
|
+
var mode = "3d";
|
|
488
|
+
try { if (localStorage.getItem("hicortexVizMode") === "2d") mode = "2d"; } catch (e) { /* ignore */ }
|
|
489
|
+
var graph3d = null; // ForceGraph3D instance (lazy)
|
|
490
|
+
var graph2d = null; // ForceGraph instance (lazy)
|
|
491
|
+
var dataVersion = 0; // bumped on every server refetch
|
|
492
|
+
var appliedVersion = { "3d": -1, "2d": -1 }; // which dataVersion each renderer holds
|
|
493
|
+
var labeledIds = {}; // id -> true: always-visible label (hubs + top strength)
|
|
494
|
+
var searchTerm = "";
|
|
495
|
+
var matchIds = null; // null = no active search; {} = active match set
|
|
496
|
+
var selectedNode = null;
|
|
497
|
+
var graphRadius = 300; // rough world-space radius, used for 3D label fade
|
|
498
|
+
|
|
499
|
+
function simCut() { return parseFloat(rngSimCut.value); }
|
|
500
|
+
|
|
501
|
+
function isMatch(id) { return matchIds === null || matchIds[id] === true; }
|
|
502
|
+
|
|
503
|
+
// =========================================================================
|
|
504
|
+
// Data fetch (server refetch on Apply — same /graph?op=export surface)
|
|
505
|
+
// =========================================================================
|
|
506
|
+
function fetchGraph() {
|
|
507
|
+
var qs = new URLSearchParams({ op: "export" });
|
|
508
|
+
if (selDomain.value) qs.set("domain", selDomain.value);
|
|
509
|
+
if (selType.value) qs.set("type", selType.value);
|
|
510
|
+
if (parseFloat(rngStrength.value) > 0) qs.set("minStrength", rngStrength.value);
|
|
511
|
+
var lim = parseInt(inpLimit.value, 10);
|
|
512
|
+
if (lim > 0) qs.set("limit", String(Math.min(lim, 2000)));
|
|
513
|
+
|
|
514
|
+
var headers = {};
|
|
515
|
+
if (token) headers["Authorization"] = "Bearer " + token;
|
|
516
|
+
|
|
517
|
+
showStatus("Loading graph…");
|
|
518
|
+
fetch("/graph?" + qs.toString(), { headers: headers })
|
|
519
|
+
.then(function (resp) {
|
|
520
|
+
if (resp.status === 401) { showTokenPrompt(); throw new Error("unauthorized"); }
|
|
521
|
+
if (!resp.ok) {
|
|
522
|
+
return resp.json().catch(function () { return {}; }).then(function (body) {
|
|
523
|
+
throw new Error(body.error || ("Server error " + resp.status));
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return resp.json();
|
|
527
|
+
})
|
|
528
|
+
.then(function (data) {
|
|
529
|
+
overlay.classList.remove("open");
|
|
530
|
+
applyData(data);
|
|
531
|
+
})
|
|
532
|
+
.catch(function (err) {
|
|
533
|
+
if (err.message === "unauthorized") return; // prompt already shown
|
|
534
|
+
showStatus("Failed to load graph: " + err.message);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function showTokenPrompt() {
|
|
539
|
+
hideStatus();
|
|
540
|
+
overlay.classList.add("open");
|
|
541
|
+
document.getElementById("token-input").focus();
|
|
542
|
+
}
|
|
543
|
+
document.getElementById("token-submit").addEventListener("click", submitToken);
|
|
544
|
+
document.getElementById("token-input").addEventListener("keydown", function (e) {
|
|
545
|
+
if (e.key === "Enter") submitToken();
|
|
546
|
+
});
|
|
547
|
+
function submitToken() {
|
|
548
|
+
var v = document.getElementById("token-input").value.trim();
|
|
549
|
+
if (!v) return;
|
|
550
|
+
token = v;
|
|
551
|
+
try { localStorage.setItem("hicortexToken", v); } catch (e) { /* ignore */ }
|
|
552
|
+
document.getElementById("token-err").style.display = "none";
|
|
553
|
+
var headers = { "Authorization": "Bearer " + token };
|
|
554
|
+
fetch("/graph?op=export&limit=1", { headers: headers }).then(function (resp) {
|
|
555
|
+
if (resp.status === 401) {
|
|
556
|
+
document.getElementById("token-err").style.display = "block";
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
overlay.classList.remove("open");
|
|
560
|
+
fetchGraph();
|
|
561
|
+
}).catch(function () {
|
|
562
|
+
document.getElementById("token-err").style.display = "block";
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// =========================================================================
|
|
567
|
+
// Data → renderer state
|
|
568
|
+
// =========================================================================
|
|
569
|
+
function applyData(data) {
|
|
570
|
+
raw = data;
|
|
571
|
+
dataVersion++;
|
|
572
|
+
|
|
573
|
+
fillSelect(selDomain, data.domains || []);
|
|
574
|
+
fillSelect(selType, data.types || []);
|
|
575
|
+
buildDomainLegend(data.domains || []);
|
|
576
|
+
buildEdgeLegend(data.edges || []);
|
|
577
|
+
computeLabeled();
|
|
578
|
+
|
|
579
|
+
selectedNode = null;
|
|
580
|
+
detail.classList.remove("open");
|
|
581
|
+
|
|
582
|
+
if (!data.nodes || data.nodes.length === 0) {
|
|
583
|
+
updateMeta(0);
|
|
584
|
+
showStatus("No memories yet — the graph will appear once the nightly capture has run.");
|
|
585
|
+
// Still push the empty data so a previous graph is cleared.
|
|
586
|
+
} else {
|
|
587
|
+
hideStatus();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
refreshActive();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Always-visible labels: every hub, plus the top-strength slice picked by
|
|
594
|
+
// the label-density slider. The rest fade in by camera proximity (3D) or
|
|
595
|
+
// zoom level (2D).
|
|
596
|
+
function computeLabeled() {
|
|
597
|
+
labeledIds = {};
|
|
598
|
+
var frac = parseFloat(rngLabels.value);
|
|
599
|
+
var byStrength = raw.nodes.slice().sort(function (a, b) { return b.strength - a.strength; });
|
|
600
|
+
var quota = Math.max(raw.nodes.length ? 3 : 0, Math.round(raw.nodes.length * frac));
|
|
601
|
+
for (var i = 0; i < byStrength.length; i++) {
|
|
602
|
+
var n = byStrength[i];
|
|
603
|
+
if (n.isHub || i < quota) labeledIds[n.id] = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Generic links (similarity/extends/relates_to) below the threshold are
|
|
608
|
+
// hidden client-side; specific relationship types always show.
|
|
609
|
+
function visibleEdges() {
|
|
610
|
+
var cut = simCut();
|
|
611
|
+
return (raw.edges || []).filter(function (e) {
|
|
612
|
+
return !GENERIC_RELS[e.relationship] || (e.strength || 0) >= cut;
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function updateMeta(shownEdges) {
|
|
617
|
+
var hidden = (raw.meta.edgeCount || 0) - shownEdges;
|
|
618
|
+
metaEl.textContent = "showing " + raw.meta.shown + " of " + raw.meta.total +
|
|
619
|
+
" · " + shownEdges + " edges" + (hidden > 0 ? " (" + hidden + " hidden)" : "");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Renderers mutate node objects (positions) and rewrite link endpoints to
|
|
623
|
+
// object refs — each renderer gets its own copies, built from `raw`.
|
|
624
|
+
function cloneNodes() {
|
|
625
|
+
return raw.nodes.map(function (n) {
|
|
626
|
+
return {
|
|
627
|
+
id: n.id, label: n.label, content: n.content,
|
|
628
|
+
memory_type: n.memory_type, domain: n.domain, project: n.project,
|
|
629
|
+
strength: n.strength, linkCount: n.linkCount, isHub: n.isHub,
|
|
630
|
+
created_at: n.created_at
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function cloneEdges(edges) {
|
|
635
|
+
return edges.map(function (e) {
|
|
636
|
+
return { source: e.source, target: e.target, relationship: e.relationship, strength: e.strength };
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Push current data + encodings into the ACTIVE renderer. The hidden one is
|
|
641
|
+
// refreshed lazily on toggle.
|
|
642
|
+
function refreshActive() {
|
|
643
|
+
var edges = visibleEdges();
|
|
644
|
+
updateMeta(edges.length);
|
|
645
|
+
if (mode === "3d") {
|
|
646
|
+
ensure3d();
|
|
647
|
+
if (appliedVersion["3d"] !== dataVersion) {
|
|
648
|
+
appliedVersion["3d"] = dataVersion;
|
|
649
|
+
graph3d.graphData({ nodes: cloneNodes(), links: cloneEdges(edges) });
|
|
650
|
+
frame3dOnce = true;
|
|
651
|
+
} else {
|
|
652
|
+
// Same nodes (positions preserved), new link set (threshold change).
|
|
653
|
+
graph3d.graphData({ nodes: graph3d.graphData().nodes, links: cloneEdges(edges) });
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
ensure2d();
|
|
657
|
+
if (appliedVersion["2d"] !== dataVersion) {
|
|
658
|
+
appliedVersion["2d"] = dataVersion;
|
|
659
|
+
graph2d.graphData({ nodes: cloneNodes(), links: cloneEdges(edges) });
|
|
660
|
+
frame2dOnce = true;
|
|
661
|
+
} else {
|
|
662
|
+
graph2d.graphData({ nodes: graph2d.graphData().nodes, links: cloneEdges(edges) });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function fillSelect(sel, values) {
|
|
668
|
+
var current = sel.value;
|
|
669
|
+
while (sel.options.length > 1) sel.remove(1);
|
|
670
|
+
values.forEach(function (v) {
|
|
671
|
+
var opt = document.createElement("option");
|
|
672
|
+
opt.value = v; opt.textContent = v;
|
|
673
|
+
sel.appendChild(opt);
|
|
674
|
+
});
|
|
675
|
+
sel.value = values.indexOf(current) >= 0 ? current : "";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildDomainLegend(domains) {
|
|
679
|
+
var host = document.getElementById("legend-domains");
|
|
680
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
681
|
+
domains.slice(0, 14).forEach(function (d) {
|
|
682
|
+
var row = document.createElement("div");
|
|
683
|
+
row.className = "legend-row";
|
|
684
|
+
var dot = document.createElement("span");
|
|
685
|
+
dot.className = "legend-dot";
|
|
686
|
+
dot.style.background = domainColor(d);
|
|
687
|
+
row.appendChild(dot);
|
|
688
|
+
row.appendChild(document.createTextNode(d));
|
|
689
|
+
host.appendChild(row);
|
|
690
|
+
});
|
|
691
|
+
var noneRow = document.createElement("div");
|
|
692
|
+
noneRow.className = "legend-row";
|
|
693
|
+
var noneDot = document.createElement("span");
|
|
694
|
+
noneDot.className = "legend-dot";
|
|
695
|
+
noneDot.style.background = domainColor(null);
|
|
696
|
+
noneRow.appendChild(noneDot);
|
|
697
|
+
noneRow.appendChild(document.createTextNode("no domain"));
|
|
698
|
+
host.appendChild(noneRow);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function tooltipHtml(n) {
|
|
702
|
+
return "<b>" + esc(labelText(n)) + "</b><br>" +
|
|
703
|
+
esc((n.domain || "no domain") + " · " + (n.memory_type || "?") +
|
|
704
|
+
" · strength " + (typeof n.strength === "number" ? n.strength.toFixed(2) : "?"));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// =========================================================================
|
|
708
|
+
// 3D renderer (default view)
|
|
709
|
+
// =========================================================================
|
|
710
|
+
var frame3dOnce = false;
|
|
711
|
+
var SPRITE_CAP = 1500; // above this only always-on labels get sprites
|
|
712
|
+
|
|
713
|
+
function makeTextSprite(text, color, worldHeight) {
|
|
714
|
+
var THREE = window.THREE;
|
|
715
|
+
var fontPx = 28;
|
|
716
|
+
var pad = 10;
|
|
717
|
+
var canvas = document.createElement("canvas");
|
|
718
|
+
var g = canvas.getContext("2d");
|
|
719
|
+
var font = "600 " + fontPx + "px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
720
|
+
g.font = font;
|
|
721
|
+
var w = Math.max(2, Math.ceil(g.measureText(text).width));
|
|
722
|
+
canvas.width = w + pad * 2;
|
|
723
|
+
canvas.height = fontPx + pad * 2;
|
|
724
|
+
g = canvas.getContext("2d");
|
|
725
|
+
g.font = font;
|
|
726
|
+
g.textBaseline = "middle";
|
|
727
|
+
// soft dark backing for readability against edges
|
|
728
|
+
g.fillStyle = "rgba(15,17,23,0.55)";
|
|
729
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
730
|
+
g.fillStyle = color;
|
|
731
|
+
g.fillText(text, pad, canvas.height / 2);
|
|
732
|
+
var tex = new THREE.CanvasTexture(canvas);
|
|
733
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
734
|
+
var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
|
|
735
|
+
var sprite = new THREE.Sprite(mat);
|
|
736
|
+
sprite.scale.set(worldHeight * canvas.width / canvas.height, worldHeight, 1);
|
|
737
|
+
return sprite;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function makeHaloSprite(worldSize) {
|
|
741
|
+
var THREE = window.THREE;
|
|
742
|
+
var canvas = document.createElement("canvas");
|
|
743
|
+
canvas.width = 64; canvas.height = 64;
|
|
744
|
+
var g = canvas.getContext("2d");
|
|
745
|
+
var grad = g.createRadialGradient(32, 32, 8, 32, 32, 32);
|
|
746
|
+
grad.addColorStop(0, "rgba(255,255,255,0.5)");
|
|
747
|
+
grad.addColorStop(0.6, "rgba(255,255,255,0.15)");
|
|
748
|
+
grad.addColorStop(1, "rgba(255,255,255,0)");
|
|
749
|
+
g.fillStyle = grad;
|
|
750
|
+
g.fillRect(0, 0, 64, 64);
|
|
751
|
+
var tex = new THREE.CanvasTexture(canvas);
|
|
752
|
+
var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
|
|
753
|
+
var sprite = new THREE.Sprite(mat);
|
|
754
|
+
sprite.scale.set(worldSize, worldSize, 1);
|
|
755
|
+
return sprite;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function nodeRadius3d(n) {
|
|
759
|
+
// three-forcegraph: sphere radius = cbrt(val) * nodeRelSize (default 4)
|
|
760
|
+
return Math.cbrt(nodeVal(n)) * 4;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function ensure3d() {
|
|
764
|
+
if (graph3d) { graph3d.resumeAnimation(); return; }
|
|
765
|
+
graph3d = new window.ForceGraph3D(el3d, { controlType: "orbit" })
|
|
766
|
+
.width(window.innerWidth).height(window.innerHeight)
|
|
767
|
+
.backgroundColor("#0f1117")
|
|
768
|
+
.showNavInfo(false)
|
|
769
|
+
.nodeVal(nodeVal)
|
|
770
|
+
.nodeColor(function (n) { return isMatch(n.id) ? domainColor(n.domain) : DIM_NODE; })
|
|
771
|
+
.nodeLabel(function (n) { return tooltipHtml(n); })
|
|
772
|
+
.nodeThreeObjectExtend(true)
|
|
773
|
+
.nodeThreeObject(function (n) { return buildNodeExtras(n); })
|
|
774
|
+
.linkColor(function (l) {
|
|
775
|
+
if (matchIds !== null && !(isMatch(idOf(l.source)) && isMatch(idOf(l.target)))) return DIM_EDGE_3D;
|
|
776
|
+
return edgeStyle(l.relationship).color3d;
|
|
777
|
+
})
|
|
778
|
+
.linkWidth(function (l) { return GENERIC_RELS[l.relationship] ? 0 : 1.2; })
|
|
779
|
+
.linkOpacity(0.55)
|
|
780
|
+
.linkLabel(function (l) { return esc(l.relationship); })
|
|
781
|
+
.onNodeClick(function (n) { openDetail(n); })
|
|
782
|
+
.onBackgroundClick(function () { closeDetail(); })
|
|
783
|
+
.onEngineStop(function () {
|
|
784
|
+
computeGraphRadius();
|
|
785
|
+
updateSpriteFade();
|
|
786
|
+
if (frame3dOnce) { frame3dOnce = false; graph3d.zoomToFit(600, 40); }
|
|
787
|
+
});
|
|
788
|
+
// Fade secondary labels by camera proximity — orbit controls emit
|
|
789
|
+
// "change" on every camera move.
|
|
790
|
+
var controls = graph3d.controls();
|
|
791
|
+
if (controls && typeof controls.addEventListener === "function") {
|
|
792
|
+
var pending = false;
|
|
793
|
+
controls.addEventListener("change", function () {
|
|
794
|
+
if (pending) return;
|
|
795
|
+
pending = true;
|
|
796
|
+
requestAnimationFrame(function () { pending = false; updateSpriteFade(); });
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function idOf(endpoint) {
|
|
802
|
+
// after force-graph binds data, link endpoints become node objects
|
|
803
|
+
return typeof endpoint === "object" && endpoint !== null ? endpoint.id : endpoint;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function buildNodeExtras(n) {
|
|
807
|
+
var THREE = window.THREE;
|
|
808
|
+
var always = labeledIds[n.id] === true;
|
|
809
|
+
var makeSecondary = raw.nodes.length <= SPRITE_CAP;
|
|
810
|
+
if (!always && !makeSecondary && !n.isHub) return false;
|
|
811
|
+
|
|
812
|
+
var group = new THREE.Group();
|
|
813
|
+
var r = nodeRadius3d(n);
|
|
814
|
+
if (n.isHub) {
|
|
815
|
+
var halo = makeHaloSprite(r * 3.2);
|
|
816
|
+
group.add(halo);
|
|
817
|
+
}
|
|
818
|
+
if (always || makeSecondary) {
|
|
819
|
+
var color = isMatch(n.id) ? "#d5dae4" : "#4a5163";
|
|
820
|
+
var sprite = makeTextSprite(labelText(n), color, 6);
|
|
821
|
+
sprite.position.set(0, r + 5, 0);
|
|
822
|
+
sprite.material.opacity = always ? 1 : 0;
|
|
823
|
+
n.__labelSprite = sprite;
|
|
824
|
+
n.__labelAlways = always;
|
|
825
|
+
group.add(sprite);
|
|
826
|
+
}
|
|
827
|
+
return group;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function computeGraphRadius() {
|
|
831
|
+
if (!graph3d) return;
|
|
832
|
+
var nodes = graph3d.graphData().nodes;
|
|
833
|
+
var max = 0;
|
|
834
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
835
|
+
var n = nodes[i];
|
|
836
|
+
var d = (n.x || 0) * (n.x || 0) + (n.y || 0) * (n.y || 0) + (n.z || 0) * (n.z || 0);
|
|
837
|
+
if (d > max) max = d;
|
|
838
|
+
}
|
|
839
|
+
graphRadius = Math.max(120, Math.sqrt(max));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function updateSpriteFade() {
|
|
843
|
+
if (!graph3d) return;
|
|
844
|
+
var cam = graph3d.camera();
|
|
845
|
+
if (!cam) return;
|
|
846
|
+
var near = graphRadius * 0.45;
|
|
847
|
+
var far = graphRadius * 1.1;
|
|
848
|
+
var nodes = graph3d.graphData().nodes;
|
|
849
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
850
|
+
var n = nodes[i];
|
|
851
|
+
var s = n.__labelSprite;
|
|
852
|
+
if (!s) continue;
|
|
853
|
+
var matched = isMatch(n.id);
|
|
854
|
+
if (n.__labelAlways) {
|
|
855
|
+
s.material.opacity = matched ? 1 : 0.15;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (matchIds !== null) {
|
|
859
|
+
// active search: reveal matching secondary labels, hide the rest
|
|
860
|
+
s.material.opacity = matched ? 0.95 : 0;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
var dx = cam.position.x - (n.x || 0);
|
|
864
|
+
var dy = cam.position.y - (n.y || 0);
|
|
865
|
+
var dz = cam.position.z - (n.z || 0);
|
|
866
|
+
var dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
867
|
+
var t = (far - dist) / (far - near);
|
|
868
|
+
s.material.opacity = Math.max(0, Math.min(0.9, t));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// =========================================================================
|
|
873
|
+
// 2D renderer (toggle)
|
|
874
|
+
// =========================================================================
|
|
875
|
+
var frame2dOnce = false;
|
|
876
|
+
|
|
877
|
+
function ensure2d() {
|
|
878
|
+
if (graph2d) { graph2d.resumeAnimation(); return; }
|
|
879
|
+
graph2d = new window.ForceGraph(el2d)
|
|
880
|
+
.width(window.innerWidth).height(window.innerHeight)
|
|
881
|
+
.backgroundColor("#0f1117")
|
|
882
|
+
.nodeVal(nodeVal)
|
|
883
|
+
.nodeColor(function (n) { return isMatch(n.id) ? domainColor(n.domain) : DIM_NODE; })
|
|
884
|
+
.nodeLabel(function (n) { return tooltipHtml(n); })
|
|
885
|
+
.nodeCanvasObjectMode(function () { return "after"; })
|
|
886
|
+
.nodeCanvasObject(function (n, ctx, scale) { drawNodeExtras2d(n, ctx, scale); })
|
|
887
|
+
.linkColor(function (l) {
|
|
888
|
+
if (matchIds !== null && !(isMatch(idOf(l.source)) && isMatch(idOf(l.target)))) return DIM_EDGE_2D;
|
|
889
|
+
return edgeStyle(l.relationship).color;
|
|
890
|
+
})
|
|
891
|
+
.linkWidth(function (l) { return edgeStyle(l.relationship).width; })
|
|
892
|
+
.linkLineDash(function (l) { return edgeStyle(l.relationship).dash; })
|
|
893
|
+
.linkLabel(function (l) { return esc(l.relationship); })
|
|
894
|
+
.onNodeClick(function (n) { openDetail(n); })
|
|
895
|
+
.onBackgroundClick(function () { closeDetail(); })
|
|
896
|
+
.onEngineStop(function () {
|
|
897
|
+
if (frame2dOnce) { frame2dOnce = false; graph2d.zoomToFit(400, 60); }
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function nodeRadius2d(n) {
|
|
902
|
+
// force-graph: circle area ∝ val; radius = sqrt(val) * nodeRelSize (4)
|
|
903
|
+
return Math.sqrt(nodeVal(n)) * 4;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function drawNodeExtras2d(n, ctx, scale) {
|
|
907
|
+
var r = nodeRadius2d(n);
|
|
908
|
+
var matched = isMatch(n.id);
|
|
909
|
+
|
|
910
|
+
if (n.isHub) {
|
|
911
|
+
ctx.beginPath();
|
|
912
|
+
ctx.arc(n.x, n.y, r + 2.5 / scale, 0, Math.PI * 2);
|
|
913
|
+
ctx.strokeStyle = matched ? "rgba(255,255,255,0.55)" : "rgba(255,255,255,0.12)";
|
|
914
|
+
ctx.lineWidth = 1.6 / scale;
|
|
915
|
+
ctx.stroke();
|
|
916
|
+
}
|
|
917
|
+
if (n === selectedNode) {
|
|
918
|
+
ctx.beginPath();
|
|
919
|
+
ctx.arc(n.x, n.y, r + 1 / scale, 0, Math.PI * 2);
|
|
920
|
+
ctx.strokeStyle = "#ffffff";
|
|
921
|
+
ctx.lineWidth = 1.5 / scale;
|
|
922
|
+
ctx.stroke();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
var always = labeledIds[n.id] === true;
|
|
926
|
+
var alpha;
|
|
927
|
+
if (matchIds !== null) {
|
|
928
|
+
alpha = matched ? 1 : (always ? 0.12 : 0);
|
|
929
|
+
} else if (always) {
|
|
930
|
+
alpha = 1;
|
|
931
|
+
} else {
|
|
932
|
+
// fade the rest in by zoom
|
|
933
|
+
alpha = Math.max(0, Math.min(0.9, (scale - 1.4) / 1.6));
|
|
934
|
+
}
|
|
935
|
+
if (alpha <= 0.01) return;
|
|
936
|
+
|
|
937
|
+
var fontSize = Math.max(4, 11 / scale);
|
|
938
|
+
ctx.font = "600 " + fontSize + "px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
939
|
+
ctx.globalAlpha = alpha;
|
|
940
|
+
ctx.fillStyle = matched ? "#d5dae4" : "#4a5163";
|
|
941
|
+
ctx.textBaseline = "middle";
|
|
942
|
+
ctx.fillText(labelText(n), n.x + r + 3 / scale, n.y);
|
|
943
|
+
ctx.globalAlpha = 1;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// =========================================================================
|
|
947
|
+
// View toggle 3D ⇄ 2D
|
|
948
|
+
// =========================================================================
|
|
949
|
+
function setMode(next) {
|
|
950
|
+
mode = next;
|
|
951
|
+
try { localStorage.setItem("hicortexVizMode", mode); } catch (e) { /* ignore */ }
|
|
952
|
+
document.body.classList.toggle("mode-2d", mode === "2d");
|
|
953
|
+
syncModeControl();
|
|
954
|
+
if (mode === "3d" && graph2d) graph2d.pauseAnimation();
|
|
955
|
+
if (mode === "2d" && graph3d) graph3d.pauseAnimation();
|
|
956
|
+
refreshActive();
|
|
957
|
+
}
|
|
958
|
+
modeSwitch.addEventListener("change", function () { setMode(modeSwitch.checked ? "3d" : "2d"); });
|
|
959
|
+
|
|
960
|
+
// =========================================================================
|
|
961
|
+
// Zoom controls — explicit buttons alongside the built-in wheel/pinch zoom
|
|
962
|
+
// =========================================================================
|
|
963
|
+
function zoomBy(factor) {
|
|
964
|
+
if (mode === "3d") {
|
|
965
|
+
if (!graph3d) return;
|
|
966
|
+
// Move the camera toward/away from the orbit target (factor < 1 = in).
|
|
967
|
+
var p = graph3d.cameraPosition();
|
|
968
|
+
var t = graph3d.controls().target;
|
|
969
|
+
graph3d.cameraPosition({
|
|
970
|
+
x: t.x + (p.x - t.x) * factor,
|
|
971
|
+
y: t.y + (p.y - t.y) * factor,
|
|
972
|
+
z: t.z + (p.z - t.z) * factor
|
|
973
|
+
}, t, 200);
|
|
974
|
+
} else {
|
|
975
|
+
if (!graph2d) return;
|
|
976
|
+
graph2d.zoom(graph2d.zoom() / factor, 200);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function zoomFit() {
|
|
980
|
+
if (mode === "3d" && graph3d) graph3d.zoomToFit(600, 40);
|
|
981
|
+
if (mode === "2d" && graph2d) graph2d.zoomToFit(400, 60);
|
|
982
|
+
}
|
|
983
|
+
document.getElementById("zoom-in").addEventListener("click", function () { zoomBy(0.7); });
|
|
984
|
+
document.getElementById("zoom-out").addEventListener("click", function () { zoomBy(1.43); });
|
|
985
|
+
document.getElementById("zoom-fit").addEventListener("click", zoomFit);
|
|
986
|
+
|
|
987
|
+
// =========================================================================
|
|
988
|
+
// Search — client-side highlight (matching bright, others dimmed)
|
|
989
|
+
// =========================================================================
|
|
990
|
+
function matchesSearch(n) {
|
|
991
|
+
var q = searchTerm;
|
|
992
|
+
return (n.label && n.label.toLowerCase().indexOf(q) >= 0) ||
|
|
993
|
+
(n.content && n.content.toLowerCase().indexOf(q) >= 0) ||
|
|
994
|
+
(n.domain && n.domain.toLowerCase().indexOf(q) >= 0) ||
|
|
995
|
+
(n.project && n.project.toLowerCase().indexOf(q) >= 0);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function applySearch() {
|
|
999
|
+
searchTerm = inpSearch.value.trim().toLowerCase();
|
|
1000
|
+
if (!searchTerm) {
|
|
1001
|
+
matchIds = null;
|
|
1002
|
+
} else {
|
|
1003
|
+
matchIds = {};
|
|
1004
|
+
raw.nodes.forEach(function (n) { if (matchesSearch(n)) matchIds[n.id] = true; });
|
|
1005
|
+
}
|
|
1006
|
+
// Setting the color accessors to FRESH closures (new identity) forces
|
|
1007
|
+
// both renderers to re-evaluate colors for every node/link.
|
|
1008
|
+
if (graph3d) {
|
|
1009
|
+
graph3d
|
|
1010
|
+
.nodeColor(function (n) { return isMatch(n.id) ? domainColor(n.domain) : DIM_NODE; })
|
|
1011
|
+
.linkColor(function (l) {
|
|
1012
|
+
if (matchIds !== null && !(isMatch(idOf(l.source)) && isMatch(idOf(l.target)))) return DIM_EDGE_3D;
|
|
1013
|
+
return edgeStyle(l.relationship).color3d;
|
|
1014
|
+
});
|
|
1015
|
+
updateSpriteFade();
|
|
1016
|
+
}
|
|
1017
|
+
if (graph2d) {
|
|
1018
|
+
graph2d
|
|
1019
|
+
.nodeColor(function (n) { return isMatch(n.id) ? domainColor(n.domain) : DIM_NODE; })
|
|
1020
|
+
.linkColor(function (l) {
|
|
1021
|
+
if (matchIds !== null && !(isMatch(idOf(l.source)) && isMatch(idOf(l.target)))) return DIM_EDGE_2D;
|
|
1022
|
+
return edgeStyle(l.relationship).color;
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
inpSearch.addEventListener("input", applySearch);
|
|
1027
|
+
|
|
1028
|
+
// =========================================================================
|
|
1029
|
+
// Detail panel — memory content is ALWAYS set via textContent, never
|
|
1030
|
+
// innerHTML (pinned security property).
|
|
1031
|
+
// =========================================================================
|
|
1032
|
+
function openDetail(n) {
|
|
1033
|
+
selectedNode = n;
|
|
1034
|
+
detailTitle.textContent = n.label;
|
|
1035
|
+
while (detailMeta.firstChild) detailMeta.removeChild(detailMeta.firstChild);
|
|
1036
|
+
var fields = [
|
|
1037
|
+
["ID", n.id],
|
|
1038
|
+
["Domain", n.domain || "—"],
|
|
1039
|
+
["Project", n.project || "—"],
|
|
1040
|
+
["Type", n.memory_type || "—"],
|
|
1041
|
+
["Strength", typeof n.strength === "number" ? n.strength.toFixed(3) : "—"],
|
|
1042
|
+
["Links", String(n.linkCount) + (n.isHub ? " (hub)" : "")],
|
|
1043
|
+
["Created", n.created_at || "—"]
|
|
1044
|
+
];
|
|
1045
|
+
fields.forEach(function (f) {
|
|
1046
|
+
var row = document.createElement("div");
|
|
1047
|
+
var dt = document.createElement("dt"); dt.textContent = f[0];
|
|
1048
|
+
var dd = document.createElement("dd"); dd.textContent = f[1];
|
|
1049
|
+
row.appendChild(dt); row.appendChild(dd);
|
|
1050
|
+
detailMeta.appendChild(row);
|
|
1051
|
+
});
|
|
1052
|
+
detailContent.textContent = n.content;
|
|
1053
|
+
detail.classList.add("open");
|
|
1054
|
+
}
|
|
1055
|
+
function closeDetail() {
|
|
1056
|
+
selectedNode = null;
|
|
1057
|
+
detail.classList.remove("open");
|
|
1058
|
+
}
|
|
1059
|
+
document.getElementById("detail-close").addEventListener("click", closeDetail);
|
|
1060
|
+
window.addEventListener("keydown", function (e) {
|
|
1061
|
+
if (e.key === "Escape") closeDetail();
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// =========================================================================
|
|
1065
|
+
// Controls wiring
|
|
1066
|
+
// =========================================================================
|
|
1067
|
+
rngStrength.addEventListener("input", function () {
|
|
1068
|
+
rngStrengthVal.textContent = parseFloat(rngStrength.value).toFixed(2);
|
|
1069
|
+
});
|
|
1070
|
+
document.getElementById("apply").addEventListener("click", fetchGraph);
|
|
1071
|
+
|
|
1072
|
+
rngSimCut.addEventListener("input", function () {
|
|
1073
|
+
rngSimCutVal.textContent = parseFloat(rngSimCut.value).toFixed(2);
|
|
1074
|
+
});
|
|
1075
|
+
rngSimCut.addEventListener("change", function () { refreshActive(); });
|
|
1076
|
+
|
|
1077
|
+
rngLabels.addEventListener("input", function () {
|
|
1078
|
+
rngLabelsVal.textContent = Math.round(parseFloat(rngLabels.value) * 100) + "%";
|
|
1079
|
+
});
|
|
1080
|
+
rngLabels.addEventListener("change", function () {
|
|
1081
|
+
computeLabeled();
|
|
1082
|
+
// Rebuild node extras (3D) / trigger a redraw (2D) with the new label
|
|
1083
|
+
// set — fresh closures so the renderers can't skip the update.
|
|
1084
|
+
if (graph3d) {
|
|
1085
|
+
graph3d.nodeThreeObject(function (n) { return buildNodeExtras(n); });
|
|
1086
|
+
updateSpriteFade();
|
|
1087
|
+
}
|
|
1088
|
+
if (graph2d) {
|
|
1089
|
+
graph2d.nodeCanvasObject(function (n, ctx, scale) { drawNodeExtras2d(n, ctx, scale); });
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
window.addEventListener("resize", function () {
|
|
1094
|
+
if (graph3d) graph3d.width(window.innerWidth).height(window.innerHeight);
|
|
1095
|
+
if (graph2d) graph2d.width(window.innerWidth).height(window.innerHeight);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// =========================================================================
|
|
1099
|
+
// Boot — after DOMContentLoaded all deferred vendor scripts have executed.
|
|
1100
|
+
// Fail explicitly when a vendored bundle is missing (broken install).
|
|
1101
|
+
// =========================================================================
|
|
1102
|
+
function boot() {
|
|
1103
|
+
if (!window.THREE || !window.ForceGraph3D || !window.ForceGraph) {
|
|
1104
|
+
showStatus(
|
|
1105
|
+
"Failed to load the graph renderer bundles (/viz/vendor/…). " +
|
|
1106
|
+
"The package install looks incomplete — reinstall @gamaze/hicortex " +
|
|
1107
|
+
"and restart the server."
|
|
1108
|
+
);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
document.body.classList.toggle("mode-2d", mode === "2d");
|
|
1112
|
+
syncModeControl();
|
|
1113
|
+
fetchGraph();
|
|
1114
|
+
}
|
|
1115
|
+
if (document.readyState === "complete" || document.readyState === "interactive") {
|
|
1116
|
+
// DOM already parsed — but deferred scripts may still be pending; queue
|
|
1117
|
+
// behind them via DOMContentLoaded when possible, else run now.
|
|
1118
|
+
if (document.readyState === "complete") boot();
|
|
1119
|
+
else window.addEventListener("DOMContentLoaded", boot);
|
|
1120
|
+
} else {
|
|
1121
|
+
window.addEventListener("DOMContentLoaded", boot);
|
|
1122
|
+
}
|
|
1123
|
+
})();
|
|
1124
|
+
</script>
|
|
1125
|
+
</body>
|
|
1126
|
+
</html>
|