@andespindola/brainlink 0.1.0-beta.3 → 0.1.0-beta.30
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/AGENTS.md +5 -5
- package/CHANGELOG.md +37 -3
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +172 -20
- package/SECURITY.md +1 -1
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +95 -8
- package/dist/application/frontend/client-css.js +214 -100
- package/dist/application/frontend/client-html.js +60 -45
- package/dist/application/frontend/client-js.js +525 -88
- package/dist/application/get-graph-layout.js +22 -7
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +11 -4
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/migrate-vault.js +91 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +75 -5
- package/dist/application/server/routes.js +27 -1
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +412 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +205 -4
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/context.js +53 -11
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +94 -8
- package/dist/infrastructure/file-index.js +294 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +73 -0
- package/dist/infrastructure/search-packs.js +348 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +571 -19
- package/docs/AGENT_USAGE.md +112 -16
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +111 -0
- package/package.json +2 -3
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -120
- package/dist/infrastructure/sqlite/schema.js +0 -111
- package/dist/infrastructure/sqlite/search-reader.js +0 -156
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -25
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
export const createClientJs = () => `const canvas = document.getElementById('graph')
|
|
2
2
|
const ctx = canvas.getContext('2d')
|
|
3
|
+
const largeGraphNodeThreshold = 4000
|
|
4
|
+
const largeGraphEdgeRenderLimit = 16000
|
|
5
|
+
const renderNodeBudget = 1800
|
|
6
|
+
const minNodePixelRadius = 1.8
|
|
7
|
+
const viewportPaddingPx = 280
|
|
3
8
|
const state = {
|
|
4
9
|
graph: { nodes: [], edges: [] },
|
|
5
10
|
nodes: [],
|
|
6
11
|
edges: [],
|
|
12
|
+
visibleNodes: [],
|
|
13
|
+
visibleEdges: [],
|
|
14
|
+
renderNodes: [],
|
|
15
|
+
renderEdges: [],
|
|
16
|
+
nodeDegrees: new Map(),
|
|
7
17
|
selected: null,
|
|
8
18
|
hovered: null,
|
|
9
19
|
query: '',
|
|
20
|
+
contentFilter: { query: '', ids: null, token: 0, timer: null },
|
|
10
21
|
agentId: '',
|
|
11
22
|
agentsSignature: '',
|
|
23
|
+
nodeDetails: new Map(),
|
|
12
24
|
transform: { x: 0, y: 0, scale: 1 },
|
|
13
25
|
pointer: { x: 0, y: 0, down: false, dragNode: null, moved: false },
|
|
14
26
|
graphSignature: '',
|
|
27
|
+
graphStatus: '',
|
|
15
28
|
last: performance.now()
|
|
16
29
|
}
|
|
17
30
|
|
|
@@ -23,26 +36,40 @@ const escapeHtml = value => String(value)
|
|
|
23
36
|
.replaceAll('"', '"')
|
|
24
37
|
.replaceAll("'", ''')
|
|
25
38
|
const elements = {
|
|
26
|
-
stats: byId('stats'),
|
|
27
39
|
search: byId('search'),
|
|
28
40
|
agent: byId('agent'),
|
|
29
|
-
title: byId('title'),
|
|
30
|
-
path: byId('path'),
|
|
31
|
-
tags: byId('tags'),
|
|
32
|
-
notes: byId('notes'),
|
|
33
|
-
content: byId('content'),
|
|
34
|
-
outgoing: byId('outgoing'),
|
|
35
|
-
incoming: byId('incoming'),
|
|
36
41
|
nodeCount: byId('nodeCount'),
|
|
37
42
|
edgeCount: byId('edgeCount'),
|
|
38
43
|
tagCount: byId('tagCount'),
|
|
39
44
|
zoomIn: byId('zoomIn'),
|
|
40
45
|
zoomOut: byId('zoomOut'),
|
|
41
|
-
|
|
46
|
+
fit: byId('fit'),
|
|
47
|
+
reset: byId('reset'),
|
|
48
|
+
contentDialog: byId('contentDialog'),
|
|
49
|
+
contentTitle: byId('contentTitle'),
|
|
50
|
+
contentPath: byId('contentPath'),
|
|
51
|
+
contentTags: byId('contentTags'),
|
|
52
|
+
contentOutgoing: byId('contentOutgoing'),
|
|
53
|
+
contentIncoming: byId('contentIncoming'),
|
|
54
|
+
contentBody: byId('contentBody'),
|
|
55
|
+
contentClose: byId('contentClose')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const zoomRange = {
|
|
59
|
+
min: 0.05,
|
|
60
|
+
max: 4.5
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
const agentQuery = () => state.agentId ? '?agent=' + encodeURIComponent(state.agentId) : ''
|
|
45
64
|
|
|
65
|
+
const setGraphStatus = text => {
|
|
66
|
+
state.graphStatus = text
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleGraphRefreshError = error => {
|
|
70
|
+
console.error(error)
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
const graphTheme = {
|
|
47
74
|
node: '#aeb8c5',
|
|
48
75
|
nodeSelected: '#f3f7fb',
|
|
@@ -66,35 +93,138 @@ const resize = () => {
|
|
|
66
93
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
const normalizeQuery = value => value.trim().toLowerCase()
|
|
97
|
+
|
|
98
|
+
const localFilteredNodes = query =>
|
|
99
|
+
state.nodes.filter(node =>
|
|
73
100
|
node.title.toLowerCase().includes(query) ||
|
|
74
101
|
node.path.toLowerCase().includes(query) ||
|
|
75
102
|
node.tags.some(tag => tag.toLowerCase().includes(query))
|
|
76
103
|
)
|
|
77
|
-
}
|
|
78
104
|
|
|
79
|
-
const
|
|
105
|
+
const filteredNodes = () => {
|
|
106
|
+
const query = normalizeQuery(state.query)
|
|
107
|
+
if (!query) return state.nodes
|
|
108
|
+
if (state.contentFilter.query === query && state.contentFilter.ids instanceof Set) {
|
|
109
|
+
return state.nodes.filter(node => state.contentFilter.ids.has(node.id))
|
|
110
|
+
}
|
|
80
111
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
return localFilteredNodes(query)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const recomputeVisibility = () => {
|
|
116
|
+
const nodes = filteredNodes()
|
|
117
|
+
const ids = new Set(nodes.map(node => node.id))
|
|
118
|
+
const edges = state.edges.filter(edge => ids.has(edge.source) && edge.target && ids.has(edge.target))
|
|
119
|
+
const limitedEdges = state.nodes.length > largeGraphNodeThreshold
|
|
120
|
+
? [...edges]
|
|
121
|
+
.sort((left, right) => edgeWeight(right) - edgeWeight(left))
|
|
122
|
+
.slice(0, largeGraphEdgeRenderLimit)
|
|
123
|
+
: edges
|
|
124
|
+
|
|
125
|
+
state.visibleNodes = nodes
|
|
126
|
+
state.visibleEdges = limitedEdges
|
|
84
127
|
}
|
|
85
128
|
|
|
86
129
|
const edgeWeight = edge => Number.isFinite(edge.weight) ? Math.max(1, edge.weight) : 1
|
|
87
130
|
|
|
88
|
-
const
|
|
131
|
+
const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
|
|
132
|
+
|
|
133
|
+
const graphBounds = nodes => {
|
|
134
|
+
if (nodes.length === 0) return null
|
|
135
|
+
let minX = Number.POSITIVE_INFINITY
|
|
136
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
137
|
+
let minY = Number.POSITIVE_INFINITY
|
|
138
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
139
|
+
|
|
140
|
+
nodes.forEach(node => {
|
|
141
|
+
const radius = baseNodeRadius(node)
|
|
142
|
+
minX = Math.min(minX, node.x - radius)
|
|
143
|
+
maxX = Math.max(maxX, node.x + radius)
|
|
144
|
+
minY = Math.min(minY, node.y - radius)
|
|
145
|
+
maxY = Math.max(maxY, node.y + radius)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
minX,
|
|
150
|
+
maxX,
|
|
151
|
+
minY,
|
|
152
|
+
maxY,
|
|
153
|
+
width: Math.max(maxX - minX, 1),
|
|
154
|
+
height: Math.max(maxY - minY, 1)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fitScaleBiasByNodeCount = nodeCount => {
|
|
159
|
+
if (nodeCount <= 6) return 2.8
|
|
160
|
+
if (nodeCount <= 20) return 2.2
|
|
161
|
+
if (nodeCount <= 60) return 1.72
|
|
162
|
+
if (nodeCount <= 180) return 1.34
|
|
163
|
+
if (nodeCount <= 600) return 1.08
|
|
164
|
+
if (nodeCount <= 2000) return 0.9
|
|
165
|
+
if (nodeCount <= 6000) return 0.72
|
|
166
|
+
return 0.58
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fitView = (options = { useFiltered: true }) => {
|
|
89
170
|
const rect = canvas.getBoundingClientRect()
|
|
90
|
-
|
|
171
|
+
const width = Math.max(rect.width, 320)
|
|
172
|
+
const height = Math.max(rect.height, 320)
|
|
173
|
+
const nodes = options.useFiltered ? filteredNodes() : state.nodes
|
|
174
|
+
const bounds = graphBounds(nodes)
|
|
175
|
+
|
|
176
|
+
if (!bounds) {
|
|
177
|
+
state.transform = { x: width / 2, y: height / 2, scale: 1 }
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const paddingByNodeCount = nodeCount => {
|
|
182
|
+
if (nodeCount <= 6) return 28
|
|
183
|
+
if (nodeCount <= 20) return 44
|
|
184
|
+
if (nodeCount <= 60) return 68
|
|
185
|
+
if (nodeCount <= 180) return 86
|
|
186
|
+
if (nodeCount <= 600) return 110
|
|
187
|
+
if (nodeCount <= 2000) return 140
|
|
188
|
+
return 180
|
|
189
|
+
}
|
|
190
|
+
const minFitScaleByNodeCount = nodeCount => {
|
|
191
|
+
if (nodeCount <= 6) return 2.4
|
|
192
|
+
if (nodeCount <= 20) return 1.8
|
|
193
|
+
if (nodeCount <= 60) return 1.2
|
|
194
|
+
if (nodeCount <= 180) return 0.86
|
|
195
|
+
if (nodeCount <= 600) return 0.58
|
|
196
|
+
if (nodeCount <= 2000) return 0.34
|
|
197
|
+
if (nodeCount <= 6000) return 0.2
|
|
198
|
+
return 0.13
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const padding = paddingByNodeCount(nodes.length)
|
|
202
|
+
const scaleX = width / (bounds.width + padding * 2)
|
|
203
|
+
const scaleY = height / (bounds.height + padding * 2)
|
|
204
|
+
const fitScale = clampScale(Math.min(scaleX, scaleY))
|
|
205
|
+
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
|
|
206
|
+
const minimumScale = minFitScaleByNodeCount(nodes.length)
|
|
207
|
+
const minimumLargeGraphScale = nodes.length > largeGraphNodeThreshold ? 0.13 : zoomRange.min
|
|
208
|
+
const scale = Math.max(biasedScale, minimumScale, minimumLargeGraphScale)
|
|
209
|
+
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
210
|
+
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
211
|
+
|
|
212
|
+
state.transform = {
|
|
213
|
+
x: width / 2 - centerX * scale,
|
|
214
|
+
y: height / 2 - centerY * scale,
|
|
215
|
+
scale
|
|
216
|
+
}
|
|
91
217
|
}
|
|
92
218
|
|
|
219
|
+
const resetView = () => fitView({ useFiltered: false })
|
|
220
|
+
|
|
93
221
|
const createLayout = graph => {
|
|
94
222
|
const nodes = graph.nodes.map(node => ({
|
|
95
223
|
...node,
|
|
96
224
|
x: Number.isFinite(node.x) ? node.x : 0,
|
|
97
|
-
y: Number.isFinite(node.y) ? node.y : 0
|
|
225
|
+
y: Number.isFinite(node.y) ? node.y : 0,
|
|
226
|
+
vx: Number.isFinite(node.vx) ? node.vx : 0,
|
|
227
|
+
vy: Number.isFinite(node.vy) ? node.vy : 0
|
|
98
228
|
}))
|
|
99
229
|
const nodeMap = new Map(nodes.map(node => [node.id, node]))
|
|
100
230
|
const edges = graph.edges
|
|
@@ -111,23 +241,88 @@ const encodeEntityTag = (value) => {
|
|
|
111
241
|
binary += String.fromCharCode(utf8[index])
|
|
112
242
|
}
|
|
113
243
|
|
|
114
|
-
return btoa(binary).
|
|
244
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '')
|
|
115
245
|
}
|
|
116
246
|
|
|
117
247
|
const graphSignature = graph => JSON.stringify({
|
|
118
|
-
nodes: graph.nodes.map(node => [node.id, node.title, node.path, node.
|
|
248
|
+
nodes: graph.nodes.map(node => [node.id, node.title, node.path, node.tags]),
|
|
119
249
|
edges: graph.edges.map(edge => [edge.source, edge.target, edge.targetTitle, edge.weight, edge.priority])
|
|
120
250
|
})
|
|
121
251
|
|
|
252
|
+
const resetContentFilter = () => {
|
|
253
|
+
if (state.contentFilter.timer) {
|
|
254
|
+
clearTimeout(state.contentFilter.timer)
|
|
255
|
+
}
|
|
256
|
+
state.contentFilter = {
|
|
257
|
+
query: '',
|
|
258
|
+
ids: null,
|
|
259
|
+
token: state.contentFilter.token + 1,
|
|
260
|
+
timer: null
|
|
261
|
+
}
|
|
262
|
+
recomputeVisibility()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const syncContentFilter = async (query, token) => {
|
|
266
|
+
const response = await fetch(
|
|
267
|
+
'/api/graph-filter?q=' +
|
|
268
|
+
encodeURIComponent(query) +
|
|
269
|
+
'&limit=' +
|
|
270
|
+
encodeURIComponent(String(Math.max(state.nodes.length, 1))) +
|
|
271
|
+
agentQuery()
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (!response.ok || token !== state.contentFilter.token) {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const payload = await response.json()
|
|
279
|
+
const nodeIds = Array.isArray(payload?.nodeIds) ? payload.nodeIds.filter(id => typeof id === 'string') : []
|
|
280
|
+
if (token !== state.contentFilter.token) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
state.contentFilter.query = query
|
|
285
|
+
state.contentFilter.ids = new Set(nodeIds)
|
|
286
|
+
recomputeVisibility()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const scheduleContentFilterSync = () => {
|
|
290
|
+
const query = normalizeQuery(state.query)
|
|
291
|
+
if (!query) {
|
|
292
|
+
resetContentFilter()
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (state.contentFilter.timer) {
|
|
297
|
+
clearTimeout(state.contentFilter.timer)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const token = state.contentFilter.token + 1
|
|
301
|
+
state.contentFilter = {
|
|
302
|
+
query: state.contentFilter.query,
|
|
303
|
+
ids: state.contentFilter.ids,
|
|
304
|
+
token,
|
|
305
|
+
timer: setTimeout(() => {
|
|
306
|
+
syncContentFilter(query, token).catch(() => {})
|
|
307
|
+
}, 180)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
122
311
|
const tick = delta => {
|
|
123
|
-
const nodes =
|
|
124
|
-
const
|
|
125
|
-
|
|
312
|
+
const nodes = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
|
|
313
|
+
const edges = state.renderEdges.length > 0 ? state.renderEdges : state.visibleEdges
|
|
314
|
+
if (nodes.length > 1200) {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
126
317
|
const strength = Math.min(delta / 16, 2)
|
|
127
318
|
|
|
128
319
|
edges.forEach(edge => {
|
|
129
320
|
const source = edge.sourceNode
|
|
130
321
|
const target = edge.targetNode
|
|
322
|
+
source.vx = Number.isFinite(source.vx) ? source.vx : 0
|
|
323
|
+
source.vy = Number.isFinite(source.vy) ? source.vy : 0
|
|
324
|
+
target.vx = Number.isFinite(target.vx) ? target.vx : 0
|
|
325
|
+
target.vy = Number.isFinite(target.vy) ? target.vy : 0
|
|
131
326
|
const dx = target.x - source.x
|
|
132
327
|
const dy = target.y - source.y
|
|
133
328
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -144,6 +339,10 @@ const tick = delta => {
|
|
|
144
339
|
for (let j = i + 1; j < nodes.length; j += 1) {
|
|
145
340
|
const a = nodes[i]
|
|
146
341
|
const b = nodes[j]
|
|
342
|
+
a.vx = Number.isFinite(a.vx) ? a.vx : 0
|
|
343
|
+
a.vy = Number.isFinite(a.vy) ? a.vy : 0
|
|
344
|
+
b.vx = Number.isFinite(b.vx) ? b.vx : 0
|
|
345
|
+
b.vy = Number.isFinite(b.vy) ? b.vy : 0
|
|
147
346
|
const dx = b.x - a.x
|
|
148
347
|
const dy = b.y - a.y
|
|
149
348
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -158,6 +357,10 @@ const tick = delta => {
|
|
|
158
357
|
}
|
|
159
358
|
|
|
160
359
|
nodes.forEach(node => {
|
|
360
|
+
node.vx = Number.isFinite(node.vx) ? node.vx : 0
|
|
361
|
+
node.vy = Number.isFinite(node.vy) ? node.vy : 0
|
|
362
|
+
node.x = Number.isFinite(node.x) ? node.x : 0
|
|
363
|
+
node.y = Number.isFinite(node.y) ? node.y : 0
|
|
161
364
|
if (state.pointer.dragNode === node) {
|
|
162
365
|
node.vx = 0
|
|
163
366
|
node.vy = 0
|
|
@@ -181,7 +384,11 @@ const worldPoint = event => {
|
|
|
181
384
|
}
|
|
182
385
|
|
|
183
386
|
const hitNode = point => {
|
|
184
|
-
|
|
387
|
+
if (state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.55) {
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const nodes = state.renderNodes
|
|
185
392
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
186
393
|
const node = nodes[index]
|
|
187
394
|
const radius = nodeRadius(node)
|
|
@@ -190,14 +397,118 @@ const hitNode = point => {
|
|
|
190
397
|
return null
|
|
191
398
|
}
|
|
192
399
|
|
|
193
|
-
const
|
|
194
|
-
const degree = state.
|
|
400
|
+
const baseNodeRadius = node => {
|
|
401
|
+
const degree = state.nodeDegrees.get(node.id) ?? 0
|
|
195
402
|
return 9 + Math.min(degree, 8) * 1.6
|
|
196
403
|
}
|
|
197
404
|
|
|
405
|
+
const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
|
|
406
|
+
|
|
407
|
+
const worldViewportBounds = () => {
|
|
408
|
+
const rect = canvas.getBoundingClientRect()
|
|
409
|
+
const width = Math.max(rect.width, 320)
|
|
410
|
+
const height = Math.max(rect.height, 320)
|
|
411
|
+
const padding = viewportPaddingPx
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
minX: (-state.transform.x - padding) / state.transform.scale,
|
|
415
|
+
maxX: (width - state.transform.x + padding) / state.transform.scale,
|
|
416
|
+
minY: (-state.transform.y - padding) / state.transform.scale,
|
|
417
|
+
maxY: (height - state.transform.y + padding) / state.transform.scale
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const isNodeInViewport = (node, viewport) =>
|
|
422
|
+
node.x >= viewport.minX &&
|
|
423
|
+
node.x <= viewport.maxX &&
|
|
424
|
+
node.y >= viewport.minY &&
|
|
425
|
+
node.y <= viewport.maxY
|
|
426
|
+
|
|
427
|
+
const viewportNodeStride = () => {
|
|
428
|
+
if (state.nodes.length <= largeGraphNodeThreshold) {
|
|
429
|
+
return 1
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (state.transform.scale >= 0.95) {
|
|
433
|
+
return 1
|
|
434
|
+
}
|
|
435
|
+
if (state.transform.scale >= 0.7) {
|
|
436
|
+
return 2
|
|
437
|
+
}
|
|
438
|
+
if (state.transform.scale >= 0.48) {
|
|
439
|
+
return 3
|
|
440
|
+
}
|
|
441
|
+
if (state.transform.scale >= 0.28) {
|
|
442
|
+
return 5
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return 8
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const computeRenderVisibility = () => {
|
|
449
|
+
if (state.visibleNodes.length <= 2000) {
|
|
450
|
+
state.renderNodes = state.visibleNodes
|
|
451
|
+
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
452
|
+
state.renderEdges = state.visibleEdges.filter((edge) => ids.has(edge.source) && edge.target && ids.has(edge.target))
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const viewport = worldViewportBounds()
|
|
457
|
+
const stride = viewportNodeStride()
|
|
458
|
+
const picked = []
|
|
459
|
+
|
|
460
|
+
for (let index = 0; index < state.visibleNodes.length; index += 1) {
|
|
461
|
+
const node = state.visibleNodes[index]
|
|
462
|
+
if (!isNodeInViewport(node, viewport)) {
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const isPriority =
|
|
467
|
+
node.id === state.selected?.id ||
|
|
468
|
+
node.id === state.hovered?.id ||
|
|
469
|
+
node.id === state.pointer.dragNode?.id
|
|
470
|
+
if (isPriority || index % stride === 0) {
|
|
471
|
+
picked.push(node)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const nodes = picked.length > renderNodeBudget
|
|
476
|
+
? picked.slice(0, renderNodeBudget)
|
|
477
|
+
: picked
|
|
478
|
+
if (nodes.length === 0 && state.visibleNodes.length > 0) {
|
|
479
|
+
const centerX = (viewport.minX + viewport.maxX) / 2
|
|
480
|
+
const centerY = (viewport.minY + viewport.maxY) / 2
|
|
481
|
+
const closest = [...state.visibleNodes]
|
|
482
|
+
.sort((left, right) => {
|
|
483
|
+
const leftDistance = (left.x - centerX) ** 2 + (left.y - centerY) ** 2
|
|
484
|
+
const rightDistance = (right.x - centerX) ** 2 + (right.y - centerY) ** 2
|
|
485
|
+
return leftDistance - rightDistance
|
|
486
|
+
})
|
|
487
|
+
.slice(0, Math.min(renderNodeBudget, 180))
|
|
488
|
+
const closestIds = new Set(closest.map((node) => node.id))
|
|
489
|
+
|
|
490
|
+
state.renderNodes = closest
|
|
491
|
+
state.renderEdges = state.visibleEdges.filter(
|
|
492
|
+
(edge) => closestIds.has(edge.source) && edge.target && closestIds.has(edge.target)
|
|
493
|
+
)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const nodeIds = new Set(nodes.map((node) => node.id))
|
|
498
|
+
const edges = state.visibleEdges.filter((edge) => nodeIds.has(edge.source) && edge.target && nodeIds.has(edge.target))
|
|
499
|
+
|
|
500
|
+
state.renderNodes = nodes
|
|
501
|
+
state.renderEdges = edges
|
|
502
|
+
}
|
|
503
|
+
|
|
198
504
|
const render = now => {
|
|
199
505
|
const delta = now - state.last
|
|
200
506
|
state.last = now
|
|
507
|
+
const minFrameIntervalMs = state.nodes.length > largeGraphNodeThreshold ? 48 : 16
|
|
508
|
+
if (delta < minFrameIntervalMs) {
|
|
509
|
+
requestAnimationFrame(render)
|
|
510
|
+
return
|
|
511
|
+
}
|
|
201
512
|
const rect = canvas.getBoundingClientRect()
|
|
202
513
|
const width = Math.max(rect.width, 320)
|
|
203
514
|
const height = Math.max(rect.height, 320)
|
|
@@ -214,7 +525,11 @@ const render = now => {
|
|
|
214
525
|
ctx.translate(state.transform.x, state.transform.y)
|
|
215
526
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
216
527
|
|
|
217
|
-
|
|
528
|
+
computeRenderVisibility()
|
|
529
|
+
tick(delta)
|
|
530
|
+
const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
|
|
531
|
+
if (drawEdges) {
|
|
532
|
+
state.renderEdges.forEach(edge => {
|
|
218
533
|
const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
219
534
|
ctx.beginPath()
|
|
220
535
|
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
@@ -222,9 +537,10 @@ const render = now => {
|
|
|
222
537
|
ctx.strokeStyle = selectedEdge ? graphTheme.edgeActive : graphTheme.edge
|
|
223
538
|
ctx.lineWidth = (selectedEdge ? 1.8 : 1) + Math.min(edgeWeight(edge) - 1, 8) * 0.22
|
|
224
539
|
ctx.stroke()
|
|
225
|
-
|
|
540
|
+
})
|
|
541
|
+
}
|
|
226
542
|
|
|
227
|
-
|
|
543
|
+
state.renderNodes.forEach(node => {
|
|
228
544
|
const radius = nodeRadius(node)
|
|
229
545
|
const isSelected = state.selected?.id === node.id
|
|
230
546
|
const isHovered = state.hovered?.id === node.id
|
|
@@ -240,7 +556,11 @@ const render = now => {
|
|
|
240
556
|
ctx.strokeStyle = isSelected ? graphTheme.nodeStrokeActive : graphTheme.nodeStroke
|
|
241
557
|
ctx.stroke()
|
|
242
558
|
|
|
243
|
-
|
|
559
|
+
const shouldDrawLabels =
|
|
560
|
+
isSelected ||
|
|
561
|
+
isHovered ||
|
|
562
|
+
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
563
|
+
if (shouldDrawLabels) {
|
|
244
564
|
ctx.fillStyle = graphTheme.label
|
|
245
565
|
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
246
566
|
ctx.textAlign = 'center'
|
|
@@ -250,6 +570,12 @@ const render = now => {
|
|
|
250
570
|
})
|
|
251
571
|
|
|
252
572
|
ctx.restore()
|
|
573
|
+
if (state.renderNodes.length === 0) {
|
|
574
|
+
ctx.fillStyle = '#99a5b5'
|
|
575
|
+
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
576
|
+
ctx.textAlign = 'center'
|
|
577
|
+
ctx.fillText('Move or zoom to reveal nearby notes', width / 2, height / 2)
|
|
578
|
+
}
|
|
253
579
|
requestAnimationFrame(render)
|
|
254
580
|
}
|
|
255
581
|
|
|
@@ -257,22 +583,7 @@ const list = items => items.length
|
|
|
257
583
|
? items.map(item => '<li>' + (item.id ? '<button type="button" data-node-id="' + escapeHtml(item.id) + '">' + escapeHtml(item.title) + '</button>' : escapeHtml(item.title)) + '<small>' + escapeHtml(item.path) + (item.weight ? ' · weight ' + escapeHtml(item.weight) + ' · ' + escapeHtml(item.priority || 'normal') : '') + '</small></li>').join('')
|
|
258
584
|
: '<li><small>No links found.</small></li>'
|
|
259
585
|
|
|
260
|
-
const
|
|
261
|
-
? state.nodes.map(node => '<li><button type="button" data-node-id="' + escapeHtml(node.id) + '">' + escapeHtml(node.title) + '</button><small>' + escapeHtml(node.path) + '</small></li>').join('')
|
|
262
|
-
: '<li><small>No notes indexed.</small></li>'
|
|
263
|
-
|
|
264
|
-
const selectNode = node => {
|
|
265
|
-
state.selected = node
|
|
266
|
-
if (!node) {
|
|
267
|
-
elements.title.textContent = 'Graph Overview'
|
|
268
|
-
elements.path.textContent = state.nodes.length + ' notes and ' + state.graph.edges.length + ' links indexed.'
|
|
269
|
-
elements.tags.innerHTML = ''
|
|
270
|
-
elements.notes.innerHTML = allNotesList()
|
|
271
|
-
elements.content.textContent = 'Selecione uma nota no grafo ou na lista para ver o Markdown completo, backlinks e links de saida.'
|
|
272
|
-
elements.outgoing.innerHTML = '<li><small>Select a note to inspect outgoing links.</small></li>'
|
|
273
|
-
elements.incoming.innerHTML = '<li><small>Select a note to inspect backlinks.</small></li>'
|
|
274
|
-
return
|
|
275
|
-
}
|
|
586
|
+
const linkedNodes = node => {
|
|
276
587
|
const nodeById = new Map(state.nodes.map(item => [item.id, item]))
|
|
277
588
|
const withEdgeMeta = (linkedNode, edge) => linkedNode ? {
|
|
278
589
|
...linkedNode,
|
|
@@ -288,57 +599,151 @@ const selectNode = node => {
|
|
|
288
599
|
.map(edge => withEdgeMeta(nodeById.get(edge.source), edge))
|
|
289
600
|
.filter(Boolean)
|
|
290
601
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
602
|
+
return { outgoing, incoming }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const fetchNodeDetails = async node => {
|
|
606
|
+
const cached = state.nodeDetails.get(node.id)
|
|
607
|
+
if (cached) {
|
|
608
|
+
return cached
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const response = await fetch('/api/graph-node?id=' + encodeURIComponent(node.id) + agentQuery())
|
|
612
|
+
if (!response.ok) {
|
|
613
|
+
throw new Error('Failed to load graph node details')
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const payload = await response.json()
|
|
617
|
+
const detail = payload?.node
|
|
618
|
+
if (!detail || !detail.id) {
|
|
619
|
+
throw new Error('Invalid graph node payload')
|
|
620
|
+
}
|
|
621
|
+
state.nodeDetails.set(detail.id, detail)
|
|
622
|
+
return detail
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const openContentDialog = async node => {
|
|
626
|
+
if (!node) return
|
|
627
|
+
const { outgoing, incoming } = linkedNodes(node)
|
|
628
|
+
elements.contentTitle.textContent = node.title
|
|
629
|
+
elements.contentPath.textContent = node.path
|
|
630
|
+
elements.contentTags.innerHTML = node.tags.length
|
|
294
631
|
? node.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
|
|
295
632
|
: '<span>No tags</span>'
|
|
296
|
-
elements.
|
|
297
|
-
elements.
|
|
298
|
-
elements.
|
|
299
|
-
elements.
|
|
633
|
+
elements.contentOutgoing.innerHTML = list(outgoing)
|
|
634
|
+
elements.contentIncoming.innerHTML = list(incoming)
|
|
635
|
+
elements.contentBody.textContent = 'Loading note content...'
|
|
636
|
+
if (!elements.contentDialog.open) {
|
|
637
|
+
elements.contentDialog.showModal()
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
const detailedNode = await fetchNodeDetails(node)
|
|
642
|
+
if (state.selected?.id !== node.id) {
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
elements.contentBody.textContent = detailedNode.content
|
|
646
|
+
} catch {
|
|
647
|
+
elements.contentBody.textContent = 'Unable to load note content.'
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const selectNode = (node, options = { openContent: false }) => {
|
|
652
|
+
state.selected = node
|
|
653
|
+
if (node && options.openContent) {
|
|
654
|
+
openContentDialog(node).catch(() => {
|
|
655
|
+
elements.contentBody.textContent = 'Unable to load note content.'
|
|
656
|
+
})
|
|
657
|
+
}
|
|
300
658
|
}
|
|
301
659
|
|
|
302
660
|
const selectNodeById = id => {
|
|
303
661
|
const node = state.nodes.find(item => item.id === id)
|
|
304
|
-
if (node) selectNode(node)
|
|
662
|
+
if (node) selectNode(node, { openContent: true })
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const zoomAtPoint = (screenX, screenY, factor) => {
|
|
666
|
+
const nextScale = clampScale(state.transform.scale * factor)
|
|
667
|
+
if (nextScale === state.transform.scale) return
|
|
668
|
+
const worldX = (screenX - state.transform.x) / state.transform.scale
|
|
669
|
+
const worldY = (screenY - state.transform.y) / state.transform.scale
|
|
670
|
+
state.transform.scale = nextScale
|
|
671
|
+
state.transform.x = screenX - worldX * nextScale
|
|
672
|
+
state.transform.y = screenY - worldY * nextScale
|
|
305
673
|
}
|
|
306
674
|
|
|
307
|
-
const
|
|
308
|
-
|
|
675
|
+
const wheelZoomFactor = event => {
|
|
676
|
+
const isModifierZoom = event.metaKey || event.ctrlKey
|
|
677
|
+
const deltaModeFactor = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 120 : 1
|
|
678
|
+
const absoluteDelta = Math.min(Math.abs(event.deltaY * deltaModeFactor), 1600)
|
|
679
|
+
|
|
680
|
+
if (absoluteDelta <= 0.0001) {
|
|
681
|
+
return 1
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const baseStep = Math.max(0.06, Math.min(0.45, absoluteDelta / 480))
|
|
685
|
+
const adjustedStep = baseStep * (isModifierZoom ? 1.4 : 1)
|
|
686
|
+
|
|
687
|
+
return event.deltaY < 0 ? 1 + adjustedStep : 1 / (1 + adjustedStep)
|
|
309
688
|
}
|
|
310
689
|
|
|
311
690
|
const bindEvents = () => {
|
|
312
691
|
window.addEventListener('resize', resize)
|
|
313
692
|
elements.search.addEventListener('input', event => {
|
|
314
693
|
state.query = event.target.value
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
: state.nodes.length + ' notes · ' + state.edges.length + ' links'
|
|
694
|
+
recomputeVisibility()
|
|
695
|
+
scheduleContentFilterSync()
|
|
318
696
|
})
|
|
319
697
|
elements.agent.addEventListener('change', event => {
|
|
320
698
|
state.agentId = event.target.value
|
|
321
699
|
state.selected = null
|
|
700
|
+
state.nodeDetails = new Map()
|
|
701
|
+
resetContentFilter()
|
|
702
|
+
recomputeVisibility()
|
|
703
|
+
scheduleContentFilterSync()
|
|
322
704
|
loadGraph({ reset: true }).catch(error => {
|
|
323
|
-
elements.stats.textContent = 'Failed to load agent graph'
|
|
324
705
|
console.error(error)
|
|
325
706
|
})
|
|
326
707
|
})
|
|
327
|
-
elements.zoomIn.addEventListener('click', () =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
708
|
+
elements.zoomIn.addEventListener('click', () => {
|
|
709
|
+
const rect = canvas.getBoundingClientRect()
|
|
710
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.3)
|
|
711
|
+
})
|
|
712
|
+
elements.zoomOut.addEventListener('click', () => {
|
|
713
|
+
const rect = canvas.getBoundingClientRect()
|
|
714
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.77)
|
|
715
|
+
})
|
|
716
|
+
if (elements.fit) {
|
|
717
|
+
elements.fit.addEventListener('click', () => {
|
|
718
|
+
fitView({ useFiltered: true })
|
|
336
719
|
})
|
|
720
|
+
}
|
|
721
|
+
elements.reset.addEventListener('click', () => {
|
|
722
|
+
resetView()
|
|
723
|
+
})
|
|
724
|
+
elements.contentClose.addEventListener('click', () => elements.contentDialog.close())
|
|
725
|
+
elements.contentDialog.addEventListener('click', event => {
|
|
726
|
+
const target = event.target
|
|
727
|
+
if (target instanceof HTMLElement && target.dataset.nodeId) {
|
|
728
|
+
selectNodeById(target.dataset.nodeId)
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
if (event.target === elements.contentDialog) elements.contentDialog.close()
|
|
337
732
|
})
|
|
338
733
|
canvas.addEventListener('wheel', event => {
|
|
339
734
|
event.preventDefault()
|
|
340
|
-
|
|
735
|
+
const rect = canvas.getBoundingClientRect()
|
|
736
|
+
const cursorX = event.clientX - rect.left
|
|
737
|
+
const cursorY = event.clientY - rect.top
|
|
738
|
+
const factor = wheelZoomFactor(event)
|
|
739
|
+
zoomAtPoint(cursorX, cursorY, factor)
|
|
341
740
|
}, { passive: false })
|
|
741
|
+
canvas.addEventListener('dblclick', event => {
|
|
742
|
+
const rect = canvas.getBoundingClientRect()
|
|
743
|
+
const cursorX = event.clientX - rect.left
|
|
744
|
+
const cursorY = event.clientY - rect.top
|
|
745
|
+
zoomAtPoint(cursorX, cursorY, 1.25)
|
|
746
|
+
})
|
|
342
747
|
canvas.addEventListener('pointerdown', event => {
|
|
343
748
|
const point = worldPoint(event)
|
|
344
749
|
const node = hitNode(point)
|
|
@@ -367,11 +772,34 @@ const bindEvents = () => {
|
|
|
367
772
|
state.transform.y += dy
|
|
368
773
|
})
|
|
369
774
|
canvas.addEventListener('pointerup', event => {
|
|
370
|
-
if (state.pointer.dragNode && !state.pointer.moved) selectNode(state.pointer.dragNode)
|
|
371
|
-
if (!state.pointer.dragNode && !state.pointer.moved) selectNode(state.hovered)
|
|
775
|
+
if (state.pointer.dragNode && !state.pointer.moved) selectNode(state.pointer.dragNode, { openContent: true })
|
|
776
|
+
if (!state.pointer.dragNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
|
|
372
777
|
state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
|
|
373
778
|
canvas.releasePointerCapture(event.pointerId)
|
|
374
779
|
})
|
|
780
|
+
canvas.addEventListener('pointercancel', () => {
|
|
781
|
+
state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
|
|
782
|
+
})
|
|
783
|
+
window.addEventListener('keydown', event => {
|
|
784
|
+
if (event.key === '+' || event.key === '=') {
|
|
785
|
+
event.preventDefault()
|
|
786
|
+
const rect = canvas.getBoundingClientRect()
|
|
787
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.25)
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (event.key === '-' || event.key === '_') {
|
|
792
|
+
event.preventDefault()
|
|
793
|
+
const rect = canvas.getBoundingClientRect()
|
|
794
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.8)
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (event.key === '0') {
|
|
799
|
+
event.preventDefault()
|
|
800
|
+
resetView()
|
|
801
|
+
}
|
|
802
|
+
})
|
|
375
803
|
}
|
|
376
804
|
|
|
377
805
|
const loadAgents = async () => {
|
|
@@ -386,9 +814,10 @@ const loadAgents = async () => {
|
|
|
386
814
|
|
|
387
815
|
state.agentId = selected
|
|
388
816
|
if (signature !== state.agentsSignature) {
|
|
817
|
+
const formatAgentLabel = (agent) => agent.id
|
|
389
818
|
elements.agent.innerHTML = agents.length
|
|
390
|
-
? agents.map(agent => '<option value="' + escapeHtml(agent.id) + '">' + escapeHtml(agent
|
|
391
|
-
: '<option value="shared">shared
|
|
819
|
+
? agents.map(agent => '<option value="' + escapeHtml(agent.id) + '">' + escapeHtml(formatAgentLabel(agent)) + '</option>').join('')
|
|
820
|
+
: '<option value="shared">shared</option>'
|
|
392
821
|
state.agentsSignature = signature
|
|
393
822
|
}
|
|
394
823
|
elements.agent.value = selected
|
|
@@ -417,14 +846,29 @@ const loadGraph = async (options = { reset: false }) => {
|
|
|
417
846
|
state.graph = graph
|
|
418
847
|
state.nodes = layout.nodes
|
|
419
848
|
state.edges = layout.edges
|
|
849
|
+
state.nodeDegrees = state.edges.reduce((degrees, edge) => {
|
|
850
|
+
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edgeWeight(edge))
|
|
851
|
+
if (edge.target) {
|
|
852
|
+
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edgeWeight(edge))
|
|
853
|
+
}
|
|
854
|
+
return degrees
|
|
855
|
+
}, new Map())
|
|
856
|
+
state.nodeDetails = new Map()
|
|
857
|
+
resetContentFilter()
|
|
858
|
+
recomputeVisibility()
|
|
859
|
+
scheduleContentFilterSync()
|
|
420
860
|
const tags = new Set(graph.nodes.flatMap(node => node.tags))
|
|
421
|
-
|
|
861
|
+
setGraphStatus(state.agentId + ' · ' + graph.nodes.length + ' notes · ' + graph.edges.length + ' links · live')
|
|
422
862
|
elements.nodeCount.textContent = graph.nodes.length
|
|
423
863
|
elements.edgeCount.textContent = graph.edges.length
|
|
424
864
|
elements.tagCount.textContent = tags.size
|
|
425
865
|
resize()
|
|
426
866
|
if (options.reset) resetView()
|
|
427
|
-
|
|
867
|
+
const selectedNode = state.nodes.find(node => node.id === selectedId) ?? null
|
|
868
|
+
selectNode(selectedNode, { openContent: Boolean(selectedNode && elements.contentDialog.open) })
|
|
869
|
+
if (!selectedNode && elements.contentDialog.open) {
|
|
870
|
+
elements.contentDialog.close()
|
|
871
|
+
}
|
|
428
872
|
}
|
|
429
873
|
|
|
430
874
|
bindEvents()
|
|
@@ -441,10 +885,7 @@ const refreshGraphLoop = () => {
|
|
|
441
885
|
return
|
|
442
886
|
}
|
|
443
887
|
|
|
444
|
-
loadGraph().catch(
|
|
445
|
-
elements.stats.textContent = 'Failed to refresh graph'
|
|
446
|
-
console.error(error)
|
|
447
|
-
})
|
|
888
|
+
loadGraph().catch(handleGraphRefreshError)
|
|
448
889
|
|
|
449
890
|
tickCounter += 1
|
|
450
891
|
if (tickCounter % 3 === 0) {
|
|
@@ -461,7 +902,6 @@ loadAgents()
|
|
|
461
902
|
setInterval(refreshGraphLoop, pollIntervalMs)
|
|
462
903
|
})
|
|
463
904
|
.catch(error => {
|
|
464
|
-
elements.stats.textContent = 'Failed to load graph'
|
|
465
905
|
console.error(error)
|
|
466
906
|
})
|
|
467
907
|
|
|
@@ -470,9 +910,6 @@ document.addEventListener('visibilitychange', () => {
|
|
|
470
910
|
return
|
|
471
911
|
}
|
|
472
912
|
|
|
473
|
-
loadGraph({ reset: true }).catch(
|
|
474
|
-
elements.stats.textContent = 'Failed to refresh graph'
|
|
475
|
-
console.error(error)
|
|
476
|
-
})
|
|
913
|
+
loadGraph({ reset: true }).catch(handleGraphRefreshError)
|
|
477
914
|
})
|
|
478
915
|
`;
|