@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.150
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 +3 -0
- package/CHANGELOG.md +24 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +135 -7
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +111 -47
- package/dist/application/frontend/client-html.js +42 -26
- package/dist/application/frontend/client-js.js +788 -554
- package/dist/application/frontend/client-render-worker-js.js +569 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +38 -5
- package/dist/application/get-graph-stream-chunk.js +289 -0
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +249 -21
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/server/routes.js +187 -5
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +842 -8
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +275 -3
- package/dist/domain/markdown.js +29 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +117 -4
- package/dist/infrastructure/file-index.js +70 -3
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +28 -10
- package/dist/mcp/tools.js +110 -0
- package/docs/AGENT_USAGE.md +87 -3
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -1,35 +1,6 @@
|
|
|
1
1
|
export const createClientJs = () => `const canvas = document.getElementById('graph')
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const largeGraphEdgeRenderLimit = 16000
|
|
5
|
-
const state = {
|
|
6
|
-
graph: { nodes: [], edges: [] },
|
|
7
|
-
nodes: [],
|
|
8
|
-
edges: [],
|
|
9
|
-
visibleNodes: [],
|
|
10
|
-
visibleEdges: [],
|
|
11
|
-
nodeDegrees: new Map(),
|
|
12
|
-
selected: null,
|
|
13
|
-
hovered: null,
|
|
14
|
-
query: '',
|
|
15
|
-
contentFilter: { query: '', ids: null, token: 0, timer: null },
|
|
16
|
-
agentId: '',
|
|
17
|
-
agentsSignature: '',
|
|
18
|
-
nodeDetails: new Map(),
|
|
19
|
-
transform: { x: 0, y: 0, scale: 1 },
|
|
20
|
-
pointer: { x: 0, y: 0, down: false, dragNode: null, moved: false },
|
|
21
|
-
graphSignature: '',
|
|
22
|
-
graphStatus: '',
|
|
23
|
-
last: performance.now()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const byId = id => document.getElementById(id)
|
|
27
|
-
const escapeHtml = value => String(value)
|
|
28
|
-
.replaceAll('&', '&')
|
|
29
|
-
.replaceAll('<', '<')
|
|
30
|
-
.replaceAll('>', '>')
|
|
31
|
-
.replaceAll('"', '"')
|
|
32
|
-
.replaceAll("'", ''')
|
|
2
|
+
const ctx2dFallback = canvas.getContext('2d')
|
|
3
|
+
const byId = (id) => document.getElementById(id)
|
|
33
4
|
const elements = {
|
|
34
5
|
search: byId('search'),
|
|
35
6
|
agent: byId('agent'),
|
|
@@ -43,6 +14,8 @@ const elements = {
|
|
|
43
14
|
contentDialog: byId('contentDialog'),
|
|
44
15
|
contentTitle: byId('contentTitle'),
|
|
45
16
|
contentPath: byId('contentPath'),
|
|
17
|
+
contentFacts: byId('contentFacts'),
|
|
18
|
+
contentContextLinks: byId('contentContextLinks'),
|
|
46
19
|
contentTags: byId('contentTags'),
|
|
47
20
|
contentOutgoing: byId('contentOutgoing'),
|
|
48
21
|
contentIncoming: byId('contentIncoming'),
|
|
@@ -50,661 +23,922 @@ const elements = {
|
|
|
50
23
|
contentClose: byId('contentClose')
|
|
51
24
|
}
|
|
52
25
|
|
|
26
|
+
const state = {
|
|
27
|
+
camera: {
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
30
|
+
scale: 0.22
|
|
31
|
+
},
|
|
32
|
+
pointer: {
|
|
33
|
+
down: false,
|
|
34
|
+
moved: false,
|
|
35
|
+
dragging: false,
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
startX: 0,
|
|
39
|
+
startY: 0,
|
|
40
|
+
worldAnchorX: 0,
|
|
41
|
+
worldAnchorY: 0
|
|
42
|
+
},
|
|
43
|
+
viewport: {
|
|
44
|
+
width: 320,
|
|
45
|
+
height: 320,
|
|
46
|
+
ratio: window.devicePixelRatio || 1
|
|
47
|
+
},
|
|
48
|
+
workerReady: false,
|
|
49
|
+
rendererMode: 'worker',
|
|
50
|
+
renderWorker: null,
|
|
51
|
+
agentId: '',
|
|
52
|
+
graphSignature: '',
|
|
53
|
+
graphMode: 'near',
|
|
54
|
+
chunk: {
|
|
55
|
+
nodes: [],
|
|
56
|
+
edges: []
|
|
57
|
+
},
|
|
58
|
+
selectedNodeId: null,
|
|
59
|
+
searchToken: 0,
|
|
60
|
+
fetchToken: 0,
|
|
61
|
+
fetchTimer: null,
|
|
62
|
+
fetchAbortController: null,
|
|
63
|
+
cameraSyncScheduled: false,
|
|
64
|
+
lastDragFetchAt: 0,
|
|
65
|
+
lastWheelAt: 0,
|
|
66
|
+
lastVisibleNodes: 0,
|
|
67
|
+
lastVisibleEdges: 0,
|
|
68
|
+
totals: {
|
|
69
|
+
nodes: 0,
|
|
70
|
+
edges: 0
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
const zoomRange = {
|
|
54
|
-
min: 0.
|
|
75
|
+
min: 0.0002,
|
|
55
76
|
max: 4.5
|
|
56
77
|
}
|
|
57
78
|
|
|
58
|
-
const
|
|
79
|
+
const selectedAgentStorageKey = 'brainlink:selected-agent'
|
|
59
80
|
|
|
60
|
-
const
|
|
61
|
-
|
|
81
|
+
const escapeHtml = (value) => String(value)
|
|
82
|
+
.replaceAll('&', '&')
|
|
83
|
+
.replaceAll('<', '<')
|
|
84
|
+
.replaceAll('>', '>')
|
|
85
|
+
.replaceAll('"', '"')
|
|
86
|
+
.replaceAll("'", ''')
|
|
87
|
+
|
|
88
|
+
const readStoredAgent = () => {
|
|
89
|
+
try {
|
|
90
|
+
const value = window.localStorage.getItem(selectedAgentStorageKey)?.trim() ?? ''
|
|
91
|
+
return value.length > 0 ? value : ''
|
|
92
|
+
} catch {
|
|
93
|
+
return ''
|
|
94
|
+
}
|
|
62
95
|
}
|
|
63
96
|
|
|
64
|
-
const
|
|
65
|
-
|
|
97
|
+
const writeStoredAgent = (agentId) => {
|
|
98
|
+
try {
|
|
99
|
+
if (!agentId) {
|
|
100
|
+
window.localStorage.removeItem(selectedAgentStorageKey)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
window.localStorage.setItem(selectedAgentStorageKey, agentId)
|
|
104
|
+
} catch {}
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
label: '#edf2f7'
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const resize = () => {
|
|
82
|
-
const rect = canvas.getBoundingClientRect()
|
|
83
|
-
const width = Math.max(rect.width, 320)
|
|
84
|
-
const height = Math.max(rect.height, 320)
|
|
85
|
-
const ratio = window.devicePixelRatio || 1
|
|
86
|
-
canvas.width = Math.floor(width * ratio)
|
|
87
|
-
canvas.height = Math.floor(height * ratio)
|
|
88
|
-
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
107
|
+
const syncAgentInUrl = (agentId) => {
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(window.location.href)
|
|
110
|
+
if (agentId && agentId.trim().length > 0) {
|
|
111
|
+
url.searchParams.set('agent', agentId)
|
|
112
|
+
} else {
|
|
113
|
+
url.searchParams.delete('agent')
|
|
114
|
+
}
|
|
115
|
+
window.history.replaceState({}, '', url.toString())
|
|
116
|
+
} catch {}
|
|
89
117
|
}
|
|
90
118
|
|
|
91
|
-
const
|
|
119
|
+
const initialAgentFromUrl = (() => {
|
|
120
|
+
try {
|
|
121
|
+
const raw = new URL(window.location.href).searchParams.get('agent')
|
|
122
|
+
const value = raw?.trim() ?? ''
|
|
123
|
+
return value.length > 0 ? value : ''
|
|
124
|
+
} catch {
|
|
125
|
+
return ''
|
|
126
|
+
}
|
|
127
|
+
})()
|
|
128
|
+
|
|
129
|
+
const agentQuery = (separator = '?') => state.agentId ? separator + 'agent=' + encodeURIComponent(state.agentId) : ''
|
|
130
|
+
|
|
131
|
+
const parseColor = (hex) => {
|
|
132
|
+
const normalized = String(hex || '#ffffff').replace('#', '')
|
|
133
|
+
const expanded = normalized.length === 3
|
|
134
|
+
? normalized.split('').map((char) => char + char).join('')
|
|
135
|
+
: normalized.padEnd(6, 'f')
|
|
136
|
+
const value = Number.parseInt(expanded, 16)
|
|
137
|
+
return [
|
|
138
|
+
((value >> 16) & 255) / 255,
|
|
139
|
+
((value >> 8) & 255) / 255,
|
|
140
|
+
(value & 255) / 255,
|
|
141
|
+
1
|
|
142
|
+
]
|
|
143
|
+
}
|
|
92
144
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
const graphTheme = {
|
|
146
|
+
node: parseColor('#aeb8c5'),
|
|
147
|
+
nodeCluster: parseColor('#6bb7e8'),
|
|
148
|
+
nodeHighlight: parseColor('#f5c24a'),
|
|
149
|
+
nodeSelected: parseColor('#ffffff'),
|
|
150
|
+
edge: [0.58, 0.64, 0.74, 0.24],
|
|
151
|
+
edgeHeavy: [0.78, 0.84, 0.92, 0.44],
|
|
152
|
+
clear: parseColor('#0d0f12')
|
|
153
|
+
}
|
|
99
154
|
|
|
100
|
-
const
|
|
101
|
-
const query = normalizeQuery(state.query)
|
|
102
|
-
if (!query) return state.nodes
|
|
103
|
-
if (state.contentFilter.query === query && state.contentFilter.ids instanceof Set) {
|
|
104
|
-
return state.nodes.filter(node => state.contentFilter.ids.has(node.id))
|
|
105
|
-
}
|
|
155
|
+
const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
|
|
106
156
|
|
|
107
|
-
|
|
157
|
+
const getZoomNodeBudget = () => {
|
|
158
|
+
const scale = state.camera.scale
|
|
159
|
+
if (scale < 0.06) return 900
|
|
160
|
+
if (scale < 0.12) return 1600
|
|
161
|
+
if (scale < 0.24) return 2600
|
|
162
|
+
if (scale < 0.7) return 4000
|
|
163
|
+
return 6000
|
|
108
164
|
}
|
|
109
165
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.slice(0, largeGraphEdgeRenderLimit)
|
|
118
|
-
: edges
|
|
119
|
-
|
|
120
|
-
state.visibleNodes = nodes
|
|
121
|
-
state.visibleEdges = limitedEdges
|
|
166
|
+
const getZoomEdgeBudget = () => {
|
|
167
|
+
const scale = state.camera.scale
|
|
168
|
+
if (scale < 0.06) return 2000
|
|
169
|
+
if (scale < 0.12) return 4800
|
|
170
|
+
if (scale < 0.24) return 9000
|
|
171
|
+
if (scale < 0.7) return 15000
|
|
172
|
+
return 26000
|
|
122
173
|
}
|
|
123
174
|
|
|
124
|
-
const
|
|
175
|
+
const screenToWorld = (screenX, screenY) => ({
|
|
176
|
+
x: (screenX - state.camera.x) / state.camera.scale,
|
|
177
|
+
y: (screenY - state.camera.y) / state.camera.scale
|
|
178
|
+
})
|
|
125
179
|
|
|
126
|
-
const
|
|
180
|
+
const worldToScreen = (x, y) => ({
|
|
181
|
+
x: x * state.camera.scale + state.camera.x,
|
|
182
|
+
y: y * state.camera.scale + state.camera.y
|
|
183
|
+
})
|
|
127
184
|
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
185
|
+
const drawFallback = () => {
|
|
186
|
+
if (state.rendererMode !== 'fallback' || !ctx2dFallback) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
const width = state.viewport.width
|
|
190
|
+
const height = state.viewport.height
|
|
191
|
+
const ratio = state.viewport.ratio
|
|
192
|
+
canvas.width = Math.floor(width * ratio)
|
|
193
|
+
canvas.height = Math.floor(height * ratio)
|
|
194
|
+
ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
195
|
+
ctx2dFallback.fillStyle = '#0d0f12'
|
|
196
|
+
ctx2dFallback.fillRect(0, 0, width, height)
|
|
134
197
|
|
|
135
|
-
nodes.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
198
|
+
const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
|
|
199
|
+
const edges = Array.isArray(state.chunk.edges) ? state.chunk.edges : []
|
|
200
|
+
const nodeById = new Map()
|
|
201
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
202
|
+
nodeById.set(nodes[i][0], nodes[i])
|
|
203
|
+
}
|
|
142
204
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
ctx2dFallback.strokeStyle = 'rgba(150,165,190,0.2)'
|
|
206
|
+
ctx2dFallback.lineWidth = 1
|
|
207
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
208
|
+
const edge = edges[i]
|
|
209
|
+
const source = nodeById.get(edge[0])
|
|
210
|
+
const target = nodeById.get(edge[1])
|
|
211
|
+
if (!source || !target) continue
|
|
212
|
+
const from = worldToScreen(source[2], source[3])
|
|
213
|
+
const to = worldToScreen(target[2], target[3])
|
|
214
|
+
ctx2dFallback.beginPath()
|
|
215
|
+
ctx2dFallback.moveTo(from.x, from.y)
|
|
216
|
+
ctx2dFallback.lineTo(to.x, to.y)
|
|
217
|
+
ctx2dFallback.stroke()
|
|
150
218
|
}
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
221
|
+
const node = nodes[i]
|
|
222
|
+
const p = worldToScreen(node[2], node[3])
|
|
223
|
+
const selected = state.selectedNodeId === node[0]
|
|
224
|
+
const color = node[6] === 'cluster' ? '#6bb7e8' : '#aeb8c5'
|
|
225
|
+
const radius = Math.max(2.4, Math.min(14, 4 + node[7] * 0.55))
|
|
226
|
+
|
|
227
|
+
ctx2dFallback.beginPath()
|
|
228
|
+
ctx2dFallback.fillStyle = selected ? '#ffffff' : color
|
|
229
|
+
ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
|
230
|
+
ctx2dFallback.fill()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ctx2dFallback.fillStyle = '#edf2f7'
|
|
234
|
+
ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
|
|
235
|
+
ctx2dFallback.textAlign = 'center'
|
|
236
|
+
ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
|
|
151
237
|
}
|
|
152
238
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
const updateTotals = () => {
|
|
240
|
+
elements.nodeCount.textContent = String(state.totals.nodes)
|
|
241
|
+
elements.edgeCount.textContent = String(state.totals.edges)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const updateTagCount = () => {
|
|
245
|
+
elements.tagCount.textContent = state.graphMode === 'far' ? 'clusters' : state.graphMode
|
|
246
|
+
}
|
|
159
247
|
|
|
160
|
-
|
|
161
|
-
|
|
248
|
+
const updateWorkerCamera = () => {
|
|
249
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
162
250
|
return
|
|
163
251
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const scaleX = width / (bounds.width + padding * 2)
|
|
167
|
-
const scaleY = height / (bounds.height + padding * 2)
|
|
168
|
-
const scale = clampScale(Math.min(scaleX, scaleY))
|
|
169
|
-
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
170
|
-
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
171
|
-
|
|
172
|
-
state.transform = {
|
|
173
|
-
x: width / 2 - centerX * scale,
|
|
174
|
-
y: height / 2 - centerY * scale,
|
|
175
|
-
scale
|
|
252
|
+
if (state.cameraSyncScheduled) {
|
|
253
|
+
return
|
|
176
254
|
}
|
|
255
|
+
state.cameraSyncScheduled = true
|
|
256
|
+
requestAnimationFrame(() => {
|
|
257
|
+
state.cameraSyncScheduled = false
|
|
258
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
state.renderWorker.postMessage({
|
|
262
|
+
type: 'camera',
|
|
263
|
+
camera: state.camera
|
|
264
|
+
})
|
|
265
|
+
})
|
|
177
266
|
}
|
|
178
267
|
|
|
179
|
-
const
|
|
268
|
+
const updateWorkerSize = () => {
|
|
269
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
state.renderWorker.postMessage({
|
|
273
|
+
type: 'resize',
|
|
274
|
+
width: state.viewport.width,
|
|
275
|
+
height: state.viewport.height,
|
|
276
|
+
devicePixelRatio: state.viewport.ratio
|
|
277
|
+
})
|
|
278
|
+
}
|
|
180
279
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
280
|
+
const normalizeList = (items) => Array.isArray(items) ? items : []
|
|
281
|
+
|
|
282
|
+
const list = (items) => {
|
|
283
|
+
const rows = normalizeList(items)
|
|
284
|
+
if (rows.length === 0) {
|
|
285
|
+
return '<li><small>No links found.</small></li>'
|
|
286
|
+
}
|
|
287
|
+
return rows
|
|
288
|
+
.map((item) => {
|
|
289
|
+
const title = typeof item?.title === 'string' ? item.title : 'Untitled'
|
|
290
|
+
const id = typeof item?.id === 'string' ? item.id : ''
|
|
291
|
+
const path = typeof item?.path === 'string' ? item.path : ''
|
|
292
|
+
const meta = item?.weight ? ' · weight ' + escapeHtml(item.weight) + ' · ' + escapeHtml(item.priority || 'normal') : ''
|
|
293
|
+
return '<li>' +
|
|
294
|
+
(id ? '<button type="button" data-node-id="' + escapeHtml(id) + '">' + escapeHtml(title) + '</button>' : escapeHtml(title)) +
|
|
295
|
+
'<small>' + escapeHtml(path) + meta + '</small>' +
|
|
296
|
+
'</li>'
|
|
297
|
+
})
|
|
298
|
+
.join('')
|
|
192
299
|
}
|
|
193
300
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
301
|
+
const extractContextLinks = (content) => {
|
|
302
|
+
if (typeof content !== 'string' || content.length === 0) {
|
|
303
|
+
return []
|
|
304
|
+
}
|
|
305
|
+
const lines = content.split(/\\r?\\n/)
|
|
306
|
+
let start = -1
|
|
307
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
308
|
+
if (/^#{1,6}\\s+(?:context\\s+links?|links?\\s+de\\s+contexto)\\b/i.test(lines[index].trim())) {
|
|
309
|
+
start = index + 1
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (start < 0) {
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
197
316
|
|
|
198
|
-
|
|
199
|
-
|
|
317
|
+
const links = []
|
|
318
|
+
const seenTitles = new Set()
|
|
319
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
320
|
+
const line = lines[index].trim()
|
|
321
|
+
if (!line) {
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
if (/^#{1,6}\\s+/.test(line)) {
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
const matches = Array.from(line.matchAll(/\\[\\[([^\\]]+)\\]\\]/g))
|
|
328
|
+
if (matches.length === 0) {
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
const priorityMatch = line.match(/#(critical|important)\\b|priority:\\s*(high|critical)/i)
|
|
332
|
+
const priority = priorityMatch ? String(priorityMatch[1] || priorityMatch[2] || 'normal').toLowerCase() : 'normal'
|
|
333
|
+
|
|
334
|
+
for (let matchIndex = 0; matchIndex < matches.length; matchIndex += 1) {
|
|
335
|
+
const title = String(matches[matchIndex][1] || '').trim()
|
|
336
|
+
if (!title || seenTitles.has(title.toLowerCase())) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
seenTitles.add(title.toLowerCase())
|
|
340
|
+
links.push({ title, priority })
|
|
341
|
+
}
|
|
200
342
|
}
|
|
343
|
+
return links
|
|
344
|
+
}
|
|
201
345
|
|
|
202
|
-
|
|
346
|
+
const buildFacts = (node, outgoingCount, incomingCount) => {
|
|
347
|
+
const content = typeof node?.content === 'string' ? node.content : ''
|
|
348
|
+
const words = content.trim().length > 0 ? content.trim().split(/\\s+/).length : 0
|
|
349
|
+
return [
|
|
350
|
+
{ label: 'Agent', value: typeof node?.agentId === 'string' && node.agentId ? node.agentId : 'shared' },
|
|
351
|
+
{ label: 'Words', value: String(words) },
|
|
352
|
+
{ label: 'Chars', value: String(content.length) },
|
|
353
|
+
{ label: 'Outgoing', value: String(outgoingCount) },
|
|
354
|
+
{ label: 'Backlinks', value: String(incomingCount) }
|
|
355
|
+
]
|
|
203
356
|
}
|
|
204
357
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
})
|
|
358
|
+
const listFacts = (facts) => facts
|
|
359
|
+
.map((fact) => '<li><strong>' + escapeHtml(fact.label) + ':</strong> <small>' + escapeHtml(fact.value) + '</small></li>')
|
|
360
|
+
.join('')
|
|
209
361
|
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
362
|
+
const listContextLinks = (links) => {
|
|
363
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
364
|
+
return '<li><small>No context links found.</small></li>'
|
|
213
365
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
366
|
+
return links
|
|
367
|
+
.map((link) => '<li><span>' + escapeHtml(link.title) + '</span><small>' + escapeHtml(link.priority || 'normal') + '</small></li>')
|
|
368
|
+
.join('')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const linkedNodes = (node) => {
|
|
372
|
+
const nodeById = new Map((state.chunk.nodes || []).map((item) => [item[0], item]))
|
|
373
|
+
const edges = normalizeList(state.chunk.edges)
|
|
374
|
+
|
|
375
|
+
const outgoing = []
|
|
376
|
+
const incoming = []
|
|
377
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
378
|
+
const edge = edges[index]
|
|
379
|
+
if (edge[0] === node.id) {
|
|
380
|
+
const target = nodeById.get(edge[1])
|
|
381
|
+
if (target) {
|
|
382
|
+
outgoing.push({ id: target[0], title: target[1], path: target[4] || '', weight: edge[2], priority: edge[3] })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (edge[1] === node.id) {
|
|
386
|
+
const source = nodeById.get(edge[0])
|
|
387
|
+
if (source) {
|
|
388
|
+
incoming.push({ id: source[0], title: source[1], path: source[4] || '', weight: edge[2], priority: edge[3] })
|
|
389
|
+
}
|
|
390
|
+
}
|
|
219
391
|
}
|
|
220
|
-
|
|
392
|
+
|
|
393
|
+
return { outgoing, incoming }
|
|
221
394
|
}
|
|
222
395
|
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
agentQuery()
|
|
230
|
-
)
|
|
396
|
+
const openContentDialog = () => {
|
|
397
|
+
const dialog = elements.contentDialog
|
|
398
|
+
if (!dialog.open) {
|
|
399
|
+
dialog.show()
|
|
400
|
+
}
|
|
401
|
+
}
|
|
231
402
|
|
|
232
|
-
|
|
403
|
+
const loadNodeDetails = async (nodeId) => {
|
|
404
|
+
if (!nodeId) {
|
|
233
405
|
return
|
|
234
406
|
}
|
|
235
407
|
|
|
408
|
+
const response = await fetch('/api/graph-node?id=' + encodeURIComponent(nodeId) + agentQuery('&'))
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
throw new Error('Failed to load graph node details')
|
|
411
|
+
}
|
|
412
|
+
|
|
236
413
|
const payload = await response.json()
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
414
|
+
if (!payload || typeof payload !== 'object' || !payload.node) {
|
|
415
|
+
throw new Error('Invalid graph node payload')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const node = payload.node
|
|
419
|
+
state.selectedNodeId = node.id
|
|
420
|
+
|
|
421
|
+
if (state.renderWorker && state.workerReady) {
|
|
422
|
+
state.renderWorker.postMessage({ type: 'select', id: node.id })
|
|
240
423
|
}
|
|
241
424
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
425
|
+
elements.contentTitle.textContent = node.title || 'Untitled'
|
|
426
|
+
elements.contentPath.textContent = node.path || ''
|
|
427
|
+
|
|
428
|
+
const tags = Array.isArray(node.tags) ? node.tags : []
|
|
429
|
+
elements.contentTags.innerHTML = tags.length > 0
|
|
430
|
+
? tags.map((tag) => '<span>' + escapeHtml(tag) + '</span>').join('')
|
|
431
|
+
: '<span>No tags</span>'
|
|
432
|
+
|
|
433
|
+
const related = linkedNodes(node)
|
|
434
|
+
const contextLinks = extractContextLinks(node.content)
|
|
435
|
+
const facts = buildFacts(node, related.outgoing.length, related.incoming.length)
|
|
436
|
+
elements.contentFacts.innerHTML = listFacts(facts)
|
|
437
|
+
elements.contentContextLinks.innerHTML = listContextLinks(contextLinks)
|
|
438
|
+
elements.contentOutgoing.innerHTML = list(related.outgoing)
|
|
439
|
+
elements.contentIncoming.innerHTML = list(related.incoming)
|
|
440
|
+
elements.contentBody.textContent = typeof node.content === 'string' ? node.content : ''
|
|
441
|
+
|
|
442
|
+
openContentDialog()
|
|
245
443
|
}
|
|
246
444
|
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
resetContentFilter()
|
|
445
|
+
const fitFromChunk = () => {
|
|
446
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
447
|
+
if (nodes.length === 0) {
|
|
251
448
|
return
|
|
252
449
|
}
|
|
253
450
|
|
|
254
|
-
|
|
255
|
-
|
|
451
|
+
let minX = Infinity
|
|
452
|
+
let minY = Infinity
|
|
453
|
+
let maxX = -Infinity
|
|
454
|
+
let maxY = -Infinity
|
|
455
|
+
|
|
456
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
457
|
+
const node = nodes[index]
|
|
458
|
+
const x = Number(node[2])
|
|
459
|
+
const y = Number(node[3])
|
|
460
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
if (x < minX) minX = x
|
|
464
|
+
if (y < minY) minY = y
|
|
465
|
+
if (x > maxX) maxX = x
|
|
466
|
+
if (y > maxY) maxY = y
|
|
256
467
|
}
|
|
257
468
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
query: state.contentFilter.query,
|
|
261
|
-
ids: state.contentFilter.ids,
|
|
262
|
-
token,
|
|
263
|
-
timer: setTimeout(() => {
|
|
264
|
-
syncContentFilter(query, token).catch(() => {})
|
|
265
|
-
}, 180)
|
|
469
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
470
|
+
return
|
|
266
471
|
}
|
|
472
|
+
|
|
473
|
+
const width = Math.max(1, maxX - minX)
|
|
474
|
+
const height = Math.max(1, maxY - minY)
|
|
475
|
+
const scaleX = state.viewport.width / width
|
|
476
|
+
const scaleY = state.viewport.height / height
|
|
477
|
+
const scale = clampScale(Math.min(scaleX, scaleY) * 0.72)
|
|
478
|
+
|
|
479
|
+
state.camera.scale = scale
|
|
480
|
+
state.camera.x = state.viewport.width / 2 - (minX + width / 2) * scale
|
|
481
|
+
state.camera.y = state.viewport.height / 2 - (minY + height / 2) * scale
|
|
482
|
+
updateWorkerCamera()
|
|
267
483
|
}
|
|
268
484
|
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
485
|
+
const fetchChunk = async ({ fit } = { fit: false }) => {
|
|
486
|
+
const token = ++state.fetchToken
|
|
487
|
+
if (state.fetchAbortController) {
|
|
488
|
+
state.fetchAbortController.abort()
|
|
489
|
+
}
|
|
490
|
+
const controller = new AbortController()
|
|
491
|
+
state.fetchAbortController = controller
|
|
492
|
+
const worldTopLeft = screenToWorld(0, 0)
|
|
493
|
+
const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
|
|
494
|
+
const x = Math.min(worldTopLeft.x, worldBottomRight.x)
|
|
495
|
+
const y = Math.min(worldTopLeft.y, worldBottomRight.y)
|
|
496
|
+
const w = Math.abs(worldBottomRight.x - worldTopLeft.x)
|
|
497
|
+
const h = Math.abs(worldBottomRight.y - worldTopLeft.y)
|
|
498
|
+
|
|
499
|
+
const params = new URLSearchParams({
|
|
500
|
+
x: String(x),
|
|
501
|
+
y: String(y),
|
|
502
|
+
w: String(Math.max(1, w)),
|
|
503
|
+
h: String(Math.max(1, h)),
|
|
504
|
+
scale: String(state.camera.scale),
|
|
505
|
+
nodeBudget: String(getZoomNodeBudget()),
|
|
506
|
+
edgeBudget: String(getZoomEdgeBudget())
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
if (state.agentId) {
|
|
510
|
+
params.set('agent', state.agentId)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const response = await fetch('/api/graph-stream?' + params.toString(), { signal: controller.signal })
|
|
514
|
+
if (!response.ok) {
|
|
515
|
+
throw new Error('Failed to fetch graph stream chunk')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const chunk = await response.json()
|
|
519
|
+
if (controller.signal.aborted) {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
if (token !== state.fetchToken) {
|
|
273
523
|
return
|
|
274
524
|
}
|
|
275
|
-
const strength = Math.min(delta / 16, 2)
|
|
276
|
-
|
|
277
|
-
edges.forEach(edge => {
|
|
278
|
-
const source = edge.sourceNode
|
|
279
|
-
const target = edge.targetNode
|
|
280
|
-
const dx = target.x - source.x
|
|
281
|
-
const dy = target.y - source.y
|
|
282
|
-
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
283
|
-
const force = (distance - 150) * 0.002 * strength
|
|
284
|
-
const fx = dx * force
|
|
285
|
-
const fy = dy * force
|
|
286
|
-
source.vx += fx
|
|
287
|
-
source.vy += fy
|
|
288
|
-
target.vx -= fx
|
|
289
|
-
target.vy -= fy
|
|
290
|
-
})
|
|
291
525
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const fy = (dy / distance) * force
|
|
302
|
-
a.vx -= fx
|
|
303
|
-
a.vy -= fy
|
|
304
|
-
b.vx += fx
|
|
305
|
-
b.vy += fy
|
|
306
|
-
}
|
|
526
|
+
state.graphSignature = typeof chunk.signature === 'string' ? chunk.signature : ''
|
|
527
|
+
state.graphMode = typeof chunk.mode === 'string' ? chunk.mode : 'near'
|
|
528
|
+
state.chunk = {
|
|
529
|
+
nodes: normalizeList(chunk.nodes),
|
|
530
|
+
edges: normalizeList(chunk.edges)
|
|
531
|
+
}
|
|
532
|
+
state.totals = {
|
|
533
|
+
nodes: Number.isFinite(chunk?.totals?.nodes) ? Number(chunk.totals.nodes) : state.chunk.nodes.length,
|
|
534
|
+
edges: Number.isFinite(chunk?.totals?.edges) ? Number(chunk.totals.edges) : state.chunk.edges.length
|
|
307
535
|
}
|
|
308
536
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
537
|
+
updateTotals()
|
|
538
|
+
updateTagCount()
|
|
539
|
+
|
|
540
|
+
if (fit) {
|
|
541
|
+
fitFromChunk()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (state.renderWorker && state.workerReady) {
|
|
545
|
+
state.renderWorker.postMessage({ type: 'chunk', chunk })
|
|
546
|
+
state.renderWorker.postMessage({ type: 'select', id: state.selectedNodeId })
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
drawFallback()
|
|
322
550
|
}
|
|
323
551
|
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
x: (event.clientX - rect.left - state.transform.x) / state.transform.scale,
|
|
328
|
-
y: (event.clientY - rect.top - state.transform.y) / state.transform.scale
|
|
552
|
+
const scheduleChunkFetch = ({ fit } = { fit: false }) => {
|
|
553
|
+
if (state.fetchTimer) {
|
|
554
|
+
clearTimeout(state.fetchTimer)
|
|
329
555
|
}
|
|
556
|
+
|
|
557
|
+
const now = performance.now()
|
|
558
|
+
const recentlyWheeling = now - state.lastWheelAt < 180
|
|
559
|
+
const delay = fit ? 0 : (state.pointer.down ? 120 : (recentlyWheeling ? 140 : 48))
|
|
560
|
+
state.fetchTimer = setTimeout(() => {
|
|
561
|
+
state.fetchTimer = null
|
|
562
|
+
fetchChunk({ fit }).catch((error) => {
|
|
563
|
+
if (error && error.name === 'AbortError') {
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
console.error(error)
|
|
567
|
+
})
|
|
568
|
+
}, delay)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const setViewportFromCanvas = () => {
|
|
572
|
+
const rect = canvas.getBoundingClientRect()
|
|
573
|
+
state.viewport.width = Math.max(320, rect.width)
|
|
574
|
+
state.viewport.height = Math.max(320, rect.height)
|
|
575
|
+
state.viewport.ratio = window.devicePixelRatio || 1
|
|
576
|
+
updateWorkerSize()
|
|
577
|
+
drawFallback()
|
|
330
578
|
}
|
|
331
579
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
580
|
+
const pickFallbackNodeId = (screenX, screenY) => {
|
|
581
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
582
|
+
if (nodes.length === 0) {
|
|
583
|
+
return ''
|
|
335
584
|
}
|
|
336
585
|
|
|
337
|
-
|
|
338
|
-
|
|
586
|
+
let bestId = ''
|
|
587
|
+
let bestDistance = Infinity
|
|
588
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
339
589
|
const node = nodes[index]
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
590
|
+
const id = typeof node[0] === 'string' ? node[0] : ''
|
|
591
|
+
if (!id) continue
|
|
592
|
+
const x = Number(node[2])
|
|
593
|
+
const y = Number(node[3])
|
|
594
|
+
const weight = Number(node[7])
|
|
595
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
596
|
+
const point = worldToScreen(x, y)
|
|
597
|
+
const radius = Math.max(2.4, Math.min(14, 4 + (Number.isFinite(weight) ? weight : 0) * 0.55))
|
|
598
|
+
const distance = Math.hypot(screenX - point.x, screenY - point.y)
|
|
599
|
+
if (distance <= radius && distance < bestDistance) {
|
|
600
|
+
bestDistance = distance
|
|
601
|
+
bestId = id
|
|
602
|
+
}
|
|
342
603
|
}
|
|
343
|
-
return null
|
|
344
|
-
}
|
|
345
604
|
|
|
346
|
-
|
|
347
|
-
const degree = state.nodeDegrees.get(node.id) ?? 0
|
|
348
|
-
return 9 + Math.min(degree, 8) * 1.6
|
|
605
|
+
return bestId
|
|
349
606
|
}
|
|
350
607
|
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
608
|
+
const pickAt = (screenX, screenY) => {
|
|
609
|
+
if (state.rendererMode === 'fallback') {
|
|
610
|
+
const nodeId = pickFallbackNodeId(screenX, screenY)
|
|
611
|
+
if (nodeId) {
|
|
612
|
+
loadNodeDetails(nodeId).catch((error) => console.error(error))
|
|
613
|
+
}
|
|
357
614
|
return
|
|
358
615
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const height = Math.max(rect.height, 320)
|
|
362
|
-
ctx.clearRect(0, 0, width, height)
|
|
363
|
-
if (state.nodes.length === 0) {
|
|
364
|
-
ctx.fillStyle = '#99a5b5'
|
|
365
|
-
ctx.font = '14px Inter, system-ui, sans-serif'
|
|
366
|
-
ctx.textAlign = 'center'
|
|
367
|
-
ctx.fillText('No indexed notes found', width / 2, height / 2)
|
|
368
|
-
requestAnimationFrame(render)
|
|
616
|
+
|
|
617
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
369
618
|
return
|
|
370
619
|
}
|
|
371
|
-
ctx.save()
|
|
372
|
-
ctx.translate(state.transform.x, state.transform.y)
|
|
373
|
-
ctx.scale(state.transform.scale, state.transform.scale)
|
|
374
|
-
|
|
375
|
-
tick(delta)
|
|
376
|
-
const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
|
|
377
|
-
if (drawEdges) {
|
|
378
|
-
state.visibleEdges.forEach(edge => {
|
|
379
|
-
const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
380
|
-
ctx.beginPath()
|
|
381
|
-
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
382
|
-
ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
|
|
383
|
-
ctx.strokeStyle = selectedEdge ? graphTheme.edgeActive : graphTheme.edge
|
|
384
|
-
ctx.lineWidth = (selectedEdge ? 1.8 : 1) + Math.min(edgeWeight(edge) - 1, 8) * 0.22
|
|
385
|
-
ctx.stroke()
|
|
386
|
-
})
|
|
387
|
-
}
|
|
388
620
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
|
|
396
|
-
ctx.fill()
|
|
397
|
-
ctx.beginPath()
|
|
398
|
-
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
|
|
399
|
-
ctx.fillStyle = isSelected ? graphTheme.nodeSelected : isHovered ? graphTheme.nodeHover : graphTheme.node
|
|
400
|
-
ctx.fill()
|
|
401
|
-
ctx.lineWidth = isSelected ? 2.6 : 1.5
|
|
402
|
-
ctx.strokeStyle = isSelected ? graphTheme.nodeStrokeActive : graphTheme.nodeStroke
|
|
403
|
-
ctx.stroke()
|
|
404
|
-
|
|
405
|
-
const shouldDrawLabels =
|
|
406
|
-
isSelected ||
|
|
407
|
-
isHovered ||
|
|
408
|
-
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
409
|
-
if (shouldDrawLabels) {
|
|
410
|
-
ctx.fillStyle = graphTheme.label
|
|
411
|
-
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
412
|
-
ctx.textAlign = 'center'
|
|
413
|
-
ctx.textBaseline = 'top'
|
|
414
|
-
ctx.fillText(node.title.slice(0, 34), node.x, node.y + radius + 8)
|
|
415
|
-
}
|
|
621
|
+
const requestId = Math.random().toString(36).slice(2)
|
|
622
|
+
state.renderWorker.postMessage({
|
|
623
|
+
type: 'pick',
|
|
624
|
+
requestId,
|
|
625
|
+
x: screenX,
|
|
626
|
+
y: screenY
|
|
416
627
|
})
|
|
628
|
+
}
|
|
417
629
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const linkedNodes = node => {
|
|
427
|
-
const nodeById = new Map(state.nodes.map(item => [item.id, item]))
|
|
428
|
-
const withEdgeMeta = (linkedNode, edge) => linkedNode ? {
|
|
429
|
-
...linkedNode,
|
|
430
|
-
weight: edge.weight,
|
|
431
|
-
priority: edge.priority
|
|
432
|
-
} : null
|
|
433
|
-
const outgoing = state.graph.edges
|
|
434
|
-
.filter(edge => edge.source === node.id)
|
|
435
|
-
.map(edge => withEdgeMeta(edge.target ? nodeById.get(edge.target) : { title: edge.targetTitle + ' (unresolved)', path: 'Missing note' }, edge))
|
|
436
|
-
.filter(Boolean)
|
|
437
|
-
const incoming = state.graph.edges
|
|
438
|
-
.filter(edge => edge.target === node.id)
|
|
439
|
-
.map(edge => withEdgeMeta(nodeById.get(edge.source), edge))
|
|
440
|
-
.filter(Boolean)
|
|
441
|
-
|
|
442
|
-
return { outgoing, incoming }
|
|
630
|
+
const zoomAtPoint = (screenX, screenY, factor) => {
|
|
631
|
+
const clamped = Math.max(0.92, Math.min(1.09, factor))
|
|
632
|
+
const before = screenToWorld(screenX, screenY)
|
|
633
|
+
state.camera.scale = clampScale(state.camera.scale * clamped)
|
|
634
|
+
state.camera.x = screenX - before.x * state.camera.scale
|
|
635
|
+
state.camera.y = screenY - before.y * state.camera.scale
|
|
636
|
+
updateWorkerCamera()
|
|
637
|
+
scheduleChunkFetch()
|
|
443
638
|
}
|
|
444
639
|
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
640
|
+
const resolvePointer = (event) => {
|
|
641
|
+
const rect = canvas.getBoundingClientRect()
|
|
642
|
+
return {
|
|
643
|
+
x: event.clientX - rect.left,
|
|
644
|
+
y: event.clientY - rect.top
|
|
449
645
|
}
|
|
646
|
+
}
|
|
450
647
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
throw new Error('Failed to load graph node details')
|
|
454
|
-
}
|
|
648
|
+
const setupInput = () => {
|
|
649
|
+
const dragActivationDistance = 6
|
|
455
650
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
651
|
+
canvas.addEventListener('wheel', (event) => {
|
|
652
|
+
event.preventDefault()
|
|
653
|
+
state.lastWheelAt = performance.now()
|
|
654
|
+
const pointer = resolvePointer(event)
|
|
655
|
+
const exponent = Math.max(-0.05, Math.min(0.05, -event.deltaY * 0.001))
|
|
656
|
+
zoomAtPoint(pointer.x, pointer.y, Math.exp(exponent))
|
|
657
|
+
}, { passive: false })
|
|
464
658
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
659
|
+
canvas.addEventListener('pointerdown', (event) => {
|
|
660
|
+
const pointer = resolvePointer(event)
|
|
661
|
+
state.pointer.down = true
|
|
662
|
+
state.pointer.moved = false
|
|
663
|
+
state.pointer.dragging = false
|
|
664
|
+
state.pointer.x = pointer.x
|
|
665
|
+
state.pointer.y = pointer.y
|
|
666
|
+
state.pointer.startX = pointer.x
|
|
667
|
+
state.pointer.startY = pointer.y
|
|
668
|
+
const world = screenToWorld(pointer.x, pointer.y)
|
|
669
|
+
state.pointer.worldAnchorX = world.x
|
|
670
|
+
state.pointer.worldAnchorY = world.y
|
|
671
|
+
canvas.setPointerCapture(event.pointerId)
|
|
672
|
+
})
|
|
479
673
|
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
674
|
+
canvas.addEventListener('pointermove', (event) => {
|
|
675
|
+
const pointer = resolvePointer(event)
|
|
676
|
+
|
|
677
|
+
if (state.pointer.down) {
|
|
678
|
+
const dx = pointer.x - state.pointer.x
|
|
679
|
+
const dy = pointer.y - state.pointer.y
|
|
680
|
+
const distanceFromStart = Math.hypot(pointer.x - state.pointer.startX, pointer.y - state.pointer.startY)
|
|
681
|
+
if (distanceFromStart >= dragActivationDistance) {
|
|
682
|
+
state.pointer.moved = true
|
|
683
|
+
state.pointer.dragging = true
|
|
684
|
+
}
|
|
685
|
+
if (!state.pointer.dragging) {
|
|
686
|
+
state.pointer.x = pointer.x
|
|
687
|
+
state.pointer.y = pointer.y
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
state.camera.x += dx
|
|
691
|
+
state.camera.y += dy
|
|
692
|
+
state.pointer.x = pointer.x
|
|
693
|
+
state.pointer.y = pointer.y
|
|
694
|
+
updateWorkerCamera()
|
|
695
|
+
const now = performance.now()
|
|
696
|
+
if (now - state.lastDragFetchAt > 180) {
|
|
697
|
+
state.lastDragFetchAt = now
|
|
698
|
+
scheduleChunkFetch()
|
|
699
|
+
}
|
|
700
|
+
drawFallback()
|
|
483
701
|
return
|
|
484
702
|
}
|
|
485
|
-
elements.contentBody.textContent = detailedNode.content
|
|
486
|
-
} catch {
|
|
487
|
-
elements.contentBody.textContent = 'Unable to load note content.'
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
703
|
|
|
491
|
-
|
|
492
|
-
state.selected = node
|
|
493
|
-
if (node && options.openContent) {
|
|
494
|
-
openContentDialog(node).catch(() => {
|
|
495
|
-
elements.contentBody.textContent = 'Unable to load note content.'
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
}
|
|
704
|
+
})
|
|
499
705
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
706
|
+
canvas.addEventListener('pointerup', (event) => {
|
|
707
|
+
const pointer = resolvePointer(event)
|
|
708
|
+
const distanceFromStart = Math.hypot(pointer.x - state.pointer.startX, pointer.y - state.pointer.startY)
|
|
709
|
+
const shouldPick = !state.pointer.dragging && distanceFromStart < dragActivationDistance
|
|
710
|
+
const shouldRefreshAfterDrag = state.pointer.dragging
|
|
711
|
+
state.pointer.down = false
|
|
712
|
+
state.pointer.dragging = false
|
|
713
|
+
canvas.releasePointerCapture(event.pointerId)
|
|
504
714
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
state.transform.y = screenY - worldY * nextScale
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const bindEvents = () => {
|
|
516
|
-
window.addEventListener('resize', resize)
|
|
517
|
-
elements.search.addEventListener('input', event => {
|
|
518
|
-
state.query = event.target.value
|
|
519
|
-
recomputeVisibility()
|
|
520
|
-
scheduleContentFilterSync()
|
|
715
|
+
if (shouldPick) {
|
|
716
|
+
pickAt(pointer.x, pointer.y)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
if (shouldRefreshAfterDrag) {
|
|
720
|
+
scheduleChunkFetch()
|
|
721
|
+
}
|
|
521
722
|
})
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
resetContentFilter()
|
|
527
|
-
recomputeVisibility()
|
|
528
|
-
scheduleContentFilterSync()
|
|
529
|
-
loadGraph({ reset: true }).catch(error => {
|
|
530
|
-
console.error(error)
|
|
531
|
-
})
|
|
723
|
+
|
|
724
|
+
canvas.addEventListener('dblclick', (event) => {
|
|
725
|
+
const pointer = resolvePointer(event)
|
|
726
|
+
zoomAtPoint(pointer.x, pointer.y, 1.065)
|
|
532
727
|
})
|
|
728
|
+
|
|
729
|
+
window.addEventListener('keydown', (event) => {
|
|
730
|
+
if (event.key === '+') {
|
|
731
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 1.06)
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
if (event.key === '-') {
|
|
735
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 0.944)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
if (event.key === '0') {
|
|
739
|
+
scheduleChunkFetch({ fit: true })
|
|
740
|
+
}
|
|
741
|
+
})
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const setupControls = () => {
|
|
533
745
|
elements.zoomIn.addEventListener('click', () => {
|
|
534
|
-
|
|
535
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.18)
|
|
746
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 1.06)
|
|
536
747
|
})
|
|
748
|
+
|
|
537
749
|
elements.zoomOut.addEventListener('click', () => {
|
|
538
|
-
|
|
539
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.84)
|
|
750
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 0.944)
|
|
540
751
|
})
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
752
|
+
|
|
753
|
+
elements.fit.addEventListener('click', () => {
|
|
754
|
+
fitFromChunk()
|
|
755
|
+
scheduleChunkFetch()
|
|
756
|
+
})
|
|
757
|
+
|
|
546
758
|
elements.reset.addEventListener('click', () => {
|
|
547
|
-
|
|
759
|
+
state.camera = { x: 0, y: 0, scale: 0.22 }
|
|
760
|
+
updateWorkerCamera()
|
|
761
|
+
scheduleChunkFetch({ fit: true })
|
|
548
762
|
})
|
|
549
|
-
|
|
550
|
-
elements.
|
|
551
|
-
|
|
552
|
-
if (target instanceof HTMLElement && target.dataset.nodeId) {
|
|
553
|
-
selectNodeById(target.dataset.nodeId)
|
|
554
|
-
return
|
|
555
|
-
}
|
|
556
|
-
if (event.target === elements.contentDialog) elements.contentDialog.close()
|
|
763
|
+
|
|
764
|
+
elements.contentClose.addEventListener('click', () => {
|
|
765
|
+
elements.contentDialog.close()
|
|
557
766
|
})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const cursorY = event.clientY - rect.top
|
|
563
|
-
const factor = event.deltaY < 0 ? 1.08 : 0.92
|
|
564
|
-
zoomAtPoint(cursorX, cursorY, factor)
|
|
565
|
-
}, { passive: false })
|
|
566
|
-
canvas.addEventListener('pointerdown', event => {
|
|
567
|
-
const point = worldPoint(event)
|
|
568
|
-
const node = hitNode(point)
|
|
569
|
-
state.pointer = { x: event.clientX, y: event.clientY, down: true, dragNode: node, moved: false }
|
|
570
|
-
if (node) {
|
|
571
|
-
node.x = point.x
|
|
572
|
-
node.y = point.y
|
|
767
|
+
|
|
768
|
+
elements.contentDialog.addEventListener('click', (event) => {
|
|
769
|
+
if (event.target === elements.contentDialog) {
|
|
770
|
+
elements.contentDialog.close()
|
|
573
771
|
}
|
|
574
|
-
canvas.setPointerCapture(event.pointerId)
|
|
575
772
|
})
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
state.pointer.moved = state.pointer.moved || Math.abs(dx) + Math.abs(dy) > 3
|
|
585
|
-
if (state.pointer.dragNode) {
|
|
586
|
-
state.pointer.dragNode.x = point.x
|
|
587
|
-
state.pointer.dragNode.y = point.y
|
|
773
|
+
|
|
774
|
+
elements.search.addEventListener('input', () => {
|
|
775
|
+
const token = ++state.searchToken
|
|
776
|
+
const query = (elements.search.value || '').trim()
|
|
777
|
+
if (!query) {
|
|
778
|
+
if (state.renderWorker && state.workerReady) {
|
|
779
|
+
state.renderWorker.postMessage({ type: 'highlight', ids: [] })
|
|
780
|
+
}
|
|
588
781
|
return
|
|
589
782
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
783
|
+
|
|
784
|
+
fetch('/api/graph-filter?q=' + encodeURIComponent(query) + '&limit=1800' + agentQuery('&'))
|
|
785
|
+
.then((response) => response.json())
|
|
786
|
+
.then((payload) => {
|
|
787
|
+
if (token !== state.searchToken) {
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
const ids = Array.isArray(payload?.nodeIds) ? payload.nodeIds : []
|
|
791
|
+
if (state.renderWorker && state.workerReady) {
|
|
792
|
+
state.renderWorker.postMessage({ type: 'highlight', ids })
|
|
793
|
+
}
|
|
794
|
+
})
|
|
795
|
+
.catch((error) => {
|
|
796
|
+
console.error(error)
|
|
797
|
+
})
|
|
598
798
|
})
|
|
599
799
|
}
|
|
600
800
|
|
|
601
801
|
const loadAgents = async () => {
|
|
602
802
|
const response = await fetch('/api/agents')
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const currentExists = agents.some(agent => agent.id === state.agentId)
|
|
606
|
-
const selected = currentExists
|
|
607
|
-
? state.agentId
|
|
608
|
-
: (agents.find(agent => agent.id === 'shared')?.id ?? agents[0]?.id ?? 'shared')
|
|
609
|
-
const signature = JSON.stringify(agents.map(agent => [agent.id, agent.documentCount]))
|
|
610
|
-
|
|
611
|
-
state.agentId = selected
|
|
612
|
-
if (signature !== state.agentsSignature) {
|
|
613
|
-
elements.agent.innerHTML = agents.length
|
|
614
|
-
? agents.map(agent => '<option value="' + escapeHtml(agent.id) + '">' + escapeHtml(agent.id) + ' · ' + agent.documentCount + '</option>').join('')
|
|
615
|
-
: '<option value="shared">shared · 0</option>'
|
|
616
|
-
state.agentsSignature = signature
|
|
617
|
-
}
|
|
618
|
-
elements.agent.value = selected
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const loadGraph = async (options = { reset: false }) => {
|
|
622
|
-
const response = await fetch('/api/graph-layout' + agentQuery(), {
|
|
623
|
-
headers: state.graphSignature
|
|
624
|
-
? {
|
|
625
|
-
'if-none-match': encodeEntityTag(state.graphSignature)
|
|
626
|
-
}
|
|
627
|
-
: undefined
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
if (response.status === 304) {
|
|
631
|
-
return
|
|
803
|
+
if (!response.ok) {
|
|
804
|
+
throw new Error('Failed to load agents')
|
|
632
805
|
}
|
|
633
806
|
|
|
634
807
|
const payload = await response.json()
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
elements.nodeCount.textContent = graph.nodes.length
|
|
658
|
-
elements.edgeCount.textContent = graph.edges.length
|
|
659
|
-
elements.tagCount.textContent = tags.size
|
|
660
|
-
resize()
|
|
661
|
-
if (options.reset) resetView()
|
|
662
|
-
const selectedNode = state.nodes.find(node => node.id === selectedId) ?? null
|
|
663
|
-
selectNode(selectedNode, { openContent: Boolean(selectedNode && elements.contentDialog.open) })
|
|
664
|
-
if (!selectedNode && elements.contentDialog.open) {
|
|
665
|
-
elements.contentDialog.close()
|
|
666
|
-
}
|
|
667
|
-
}
|
|
808
|
+
const agents = Array.isArray(payload?.agents) ? payload.agents : []
|
|
809
|
+
|
|
810
|
+
elements.agent.innerHTML = agents
|
|
811
|
+
.map((agent) => {
|
|
812
|
+
const id = String(agent?.id || '')
|
|
813
|
+
const count = Number.isFinite(agent?.documentCount) ? agent.documentCount : 0
|
|
814
|
+
const label = id === 'shared' ? 'shared' : id
|
|
815
|
+
return '<option value="' + escapeHtml(id) + '">' + escapeHtml(label) + ' (' + count + ')</option>'
|
|
816
|
+
})
|
|
817
|
+
.join('')
|
|
818
|
+
|
|
819
|
+
const preferredAgent = initialAgentFromUrl || readStoredAgent()
|
|
820
|
+
const hasPreferred = preferredAgent && agents.some((agent) => agent?.id === preferredAgent)
|
|
821
|
+
state.agentId = hasPreferred ? preferredAgent : String(agents[0]?.id || '')
|
|
822
|
+
elements.agent.value = state.agentId
|
|
823
|
+
|
|
824
|
+
elements.agent.addEventListener('change', () => {
|
|
825
|
+
state.agentId = elements.agent.value || ''
|
|
826
|
+
writeStoredAgent(state.agentId)
|
|
827
|
+
syncAgentInUrl(state.agentId)
|
|
828
|
+
scheduleChunkFetch({ fit: true })
|
|
829
|
+
})
|
|
668
830
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
resize()
|
|
672
|
-
resetView()
|
|
673
|
-
})
|
|
831
|
+
syncAgentInUrl(state.agentId)
|
|
832
|
+
}
|
|
674
833
|
|
|
675
|
-
const
|
|
676
|
-
|
|
834
|
+
const setupRenderWorker = () => {
|
|
835
|
+
const hasWorker = typeof Worker !== 'undefined'
|
|
836
|
+
const canTransfer = typeof canvas.transferControlToOffscreen === 'function'
|
|
677
837
|
|
|
678
|
-
|
|
679
|
-
|
|
838
|
+
if (!hasWorker || !canTransfer) {
|
|
839
|
+
state.rendererMode = 'fallback'
|
|
840
|
+
drawFallback()
|
|
680
841
|
return
|
|
681
842
|
}
|
|
682
843
|
|
|
683
|
-
|
|
844
|
+
try {
|
|
845
|
+
const offscreen = canvas.transferControlToOffscreen()
|
|
846
|
+
const worker = new Worker('/render-worker.js')
|
|
847
|
+
state.renderWorker = worker
|
|
848
|
+
|
|
849
|
+
worker.onmessage = (event) => {
|
|
850
|
+
const payload = event.data
|
|
851
|
+
if (!payload || typeof payload !== 'object') {
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (payload.type === 'ready') {
|
|
856
|
+
state.workerReady = true
|
|
857
|
+
scheduleChunkFetch({ fit: true })
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (payload.type === 'pick-result') {
|
|
862
|
+
if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
|
|
863
|
+
loadNodeDetails(payload.node.id).catch((error) => console.error(error))
|
|
864
|
+
}
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (payload.type === 'frame-stats') {
|
|
869
|
+
state.lastVisibleNodes = Number.isFinite(payload.visibleNodes) ? payload.visibleNodes : state.lastVisibleNodes
|
|
870
|
+
state.lastVisibleEdges = Number.isFinite(payload.visibleEdges) ? payload.visibleEdges : state.lastVisibleEdges
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (payload.type === 'fatal') {
|
|
875
|
+
console.error(payload.message)
|
|
876
|
+
state.rendererMode = 'fallback'
|
|
877
|
+
state.workerReady = false
|
|
878
|
+
state.renderWorker.terminate()
|
|
879
|
+
state.renderWorker = null
|
|
880
|
+
drawFallback()
|
|
881
|
+
}
|
|
882
|
+
}
|
|
684
883
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
884
|
+
worker.postMessage({
|
|
885
|
+
type: 'init',
|
|
886
|
+
canvas: offscreen,
|
|
887
|
+
width: state.viewport.width,
|
|
888
|
+
height: state.viewport.height,
|
|
889
|
+
devicePixelRatio: state.viewport.ratio,
|
|
890
|
+
camera: state.camera,
|
|
891
|
+
theme: graphTheme
|
|
892
|
+
}, [offscreen])
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.error(error)
|
|
895
|
+
state.rendererMode = 'fallback'
|
|
896
|
+
drawFallback()
|
|
690
897
|
}
|
|
691
898
|
}
|
|
692
899
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
.
|
|
696
|
-
|
|
697
|
-
|
|
900
|
+
const wireNodeLinkClicks = () => {
|
|
901
|
+
const dialog = elements.contentDialog
|
|
902
|
+
dialog.addEventListener('click', (event) => {
|
|
903
|
+
const target = event.target
|
|
904
|
+
if (!(target instanceof HTMLElement)) {
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const button = target.closest('button[data-node-id]')
|
|
909
|
+
if (!button) {
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const id = button.getAttribute('data-node-id') || ''
|
|
914
|
+
if (id) {
|
|
915
|
+
loadNodeDetails(id).catch((error) => console.error(error))
|
|
916
|
+
}
|
|
698
917
|
})
|
|
699
|
-
|
|
700
|
-
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const bootstrap = async () => {
|
|
921
|
+
setViewportFromCanvas()
|
|
922
|
+
setupRenderWorker()
|
|
923
|
+
setupInput()
|
|
924
|
+
setupControls()
|
|
925
|
+
wireNodeLinkClicks()
|
|
926
|
+
|
|
927
|
+
window.addEventListener('resize', () => {
|
|
928
|
+
setViewportFromCanvas()
|
|
929
|
+
scheduleChunkFetch()
|
|
701
930
|
})
|
|
702
931
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
932
|
+
await loadAgents()
|
|
933
|
+
updateTotals()
|
|
934
|
+
updateTagCount()
|
|
935
|
+
|
|
936
|
+
if (state.rendererMode === 'fallback') {
|
|
937
|
+
scheduleChunkFetch({ fit: true })
|
|
706
938
|
}
|
|
939
|
+
}
|
|
707
940
|
|
|
708
|
-
|
|
941
|
+
bootstrap().catch((error) => {
|
|
942
|
+
console.error(error)
|
|
709
943
|
})
|
|
710
944
|
`;
|