@andespindola/brainlink 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +46 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/find-similar-notes.js +75 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +190 -0
- package/dist/application/index-vault.js +46 -167
- package/dist/application/memory-suggestions.js +4 -1
- package/dist/application/search-knowledge.js +19 -3
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +60 -11
- package/dist/domain/diversity.js +64 -0
- package/dist/domain/embeddings.js +148 -6
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/domain/markdown.js +10 -1
- package/dist/domain/scoring.js +42 -0
- package/dist/domain/tokens.js +17 -1
- package/dist/infrastructure/config.js +33 -1
- package/dist/infrastructure/file-index.js +227 -60
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +23 -1
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +537 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +353 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/AGENT_USAGE.md +4 -1
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
export const createChunkFetchJs = () => `
|
|
2
|
+
const fitFromChunk = () => {
|
|
3
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
4
|
+
if (nodes.length === 0) {
|
|
5
|
+
return
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let minX = Infinity
|
|
9
|
+
let minY = Infinity
|
|
10
|
+
let maxX = -Infinity
|
|
11
|
+
let maxY = -Infinity
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
14
|
+
const node = nodes[index]
|
|
15
|
+
const x = Number(node[2])
|
|
16
|
+
const y = Number(node[3])
|
|
17
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
if (x < minX) minX = x
|
|
21
|
+
if (y < minY) minY = y
|
|
22
|
+
if (x > maxX) maxX = x
|
|
23
|
+
if (y > maxY) maxY = y
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const width = Math.max(1, maxX - minX)
|
|
31
|
+
const height = Math.max(1, maxY - minY)
|
|
32
|
+
const scaleX = state.viewport.width / width
|
|
33
|
+
const scaleY = state.viewport.height / height
|
|
34
|
+
const scale = clampScale(Math.min(scaleX, scaleY) * 0.72)
|
|
35
|
+
|
|
36
|
+
state.camera.scale = scale
|
|
37
|
+
state.camera.x = state.viewport.width / 2 - (minX + width / 2) * scale
|
|
38
|
+
state.camera.y = state.viewport.height / 2 - (minY + height / 2) * scale
|
|
39
|
+
updateWorkerCamera()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fetchChunk = async ({ fit } = { fit: false }) => {
|
|
43
|
+
const token = ++state.fetchToken
|
|
44
|
+
if (state.fetchAbortController) {
|
|
45
|
+
state.fetchAbortController.abort()
|
|
46
|
+
}
|
|
47
|
+
const controller = new AbortController()
|
|
48
|
+
state.fetchAbortController = controller
|
|
49
|
+
const worldTopLeft = screenToWorld(0, 0)
|
|
50
|
+
const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
|
|
51
|
+
const x = Math.min(worldTopLeft.x, worldBottomRight.x)
|
|
52
|
+
const y = Math.min(worldTopLeft.y, worldBottomRight.y)
|
|
53
|
+
const w = Math.abs(worldBottomRight.x - worldTopLeft.x)
|
|
54
|
+
const h = Math.abs(worldBottomRight.y - worldTopLeft.y)
|
|
55
|
+
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
x: String(x),
|
|
58
|
+
y: String(y),
|
|
59
|
+
w: String(Math.max(1, w)),
|
|
60
|
+
h: String(Math.max(1, h)),
|
|
61
|
+
scale: String(state.camera.scale),
|
|
62
|
+
nodeBudget: String(getZoomNodeBudget()),
|
|
63
|
+
edgeBudget: String(getZoomEdgeBudget())
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (state.agentId) {
|
|
67
|
+
params.set('agent', state.agentId)
|
|
68
|
+
}
|
|
69
|
+
if (state.contextId) {
|
|
70
|
+
params.set('context', state.contextId)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requestKey = graphStreamRequestKey({ x, y, w, h })
|
|
74
|
+
if (!fit && state.lastChunkRequestKey === requestKey && state.chunk.nodes.length > 0) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const response = await fetch('/api/graph-stream?' + params.toString(), { signal: controller.signal })
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error('Failed to fetch graph stream chunk')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const chunk = await response.json()
|
|
84
|
+
if (controller.signal.aborted) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (token !== state.fetchToken) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
state.graphSignature = typeof chunk.signature === 'string' ? chunk.signature : ''
|
|
92
|
+
state.lastChunkRequestKey = requestKey
|
|
93
|
+
ensureNodePositionsLoaded()
|
|
94
|
+
await syncNodePositionsFromServer()
|
|
95
|
+
state.graphMode = typeof chunk.mode === 'string' ? chunk.mode : 'near'
|
|
96
|
+
const chunkNodes = applyManualNodePositions(chunk.nodes)
|
|
97
|
+
state.chunk = {
|
|
98
|
+
nodes: chunkNodes,
|
|
99
|
+
edges: normalizeList(chunk.edges)
|
|
100
|
+
}
|
|
101
|
+
state.miniMapDirty = true
|
|
102
|
+
state.spatialIndex.key = ''
|
|
103
|
+
const renderChunk = { ...chunk, nodes: chunkNodes }
|
|
104
|
+
state.totals = {
|
|
105
|
+
nodes: Number.isFinite(chunk?.totals?.nodes) ? Number(chunk.totals.nodes) : state.chunk.nodes.length,
|
|
106
|
+
edges: Number.isFinite(chunk?.totals?.edges) ? Number(chunk.totals.edges) : state.chunk.edges.length
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateTotals()
|
|
110
|
+
|
|
111
|
+
if (fit) {
|
|
112
|
+
fitFromChunk()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (state.renderWorker && state.workerReady) {
|
|
116
|
+
state.renderWorker.postMessage({ type: 'chunk', chunk: renderChunk })
|
|
117
|
+
state.renderWorker.postMessage({ type: 'select', id: state.selectedNodeId })
|
|
118
|
+
state.renderWorker.postMessage({ type: 'highlight', ids: Array.from(state.searchResultIds) })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
updateGraphOverlays()
|
|
122
|
+
drawFallback()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const scheduleChunkFetch = ({ fit } = { fit: false }) => {
|
|
126
|
+
if (state.fetchTimer) {
|
|
127
|
+
clearTimeout(state.fetchTimer)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const now = performance.now()
|
|
131
|
+
const recentlyWheeling = now - state.lastWheelAt < 320
|
|
132
|
+
const heavyScene = state.lastVisibleNodes > 1200 || state.lastVisibleEdges > 3500
|
|
133
|
+
const delay = fit ? 0 : (state.pointer.down ? 320 : (recentlyWheeling ? (heavyScene ? 420 : 300) : (heavyScene ? 120 : 72)))
|
|
134
|
+
state.fetchTimer = setTimeout(() => {
|
|
135
|
+
state.fetchTimer = null
|
|
136
|
+
fetchChunk({ fit }).catch((error) => {
|
|
137
|
+
if (error && error.name === 'AbortError') {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
console.error(error)
|
|
141
|
+
})
|
|
142
|
+
}, delay)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const setViewportFromCanvas = () => {
|
|
146
|
+
const rect = canvas.getBoundingClientRect()
|
|
147
|
+
state.viewport.width = Math.max(320, rect.width)
|
|
148
|
+
state.viewport.height = Math.max(320, rect.height)
|
|
149
|
+
state.viewport.ratio = window.devicePixelRatio || 1
|
|
150
|
+
state.miniMapDirty = true
|
|
151
|
+
updateWorkerSize()
|
|
152
|
+
drawFallback()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const pickFallbackNode = (screenX, screenY) => {
|
|
156
|
+
const nodes = spatialCandidates(screenX, screenY)
|
|
157
|
+
if (nodes.length === 0) {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let bestNode = null
|
|
162
|
+
let bestDistance = Infinity
|
|
163
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
164
|
+
const node = nodes[index]
|
|
165
|
+
const id = typeof node[0] === 'string' ? node[0] : ''
|
|
166
|
+
if (!id) continue
|
|
167
|
+
const x = Number(node[2])
|
|
168
|
+
const y = Number(node[3])
|
|
169
|
+
const weight = Number(node[7])
|
|
170
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
171
|
+
const point = worldToScreen(x, y)
|
|
172
|
+
const radius = Math.max(3.2, Math.min(16.5, 5 + (Number.isFinite(weight) ? weight : 0) * 0.65))
|
|
173
|
+
const distance = Math.hypot(screenX - point.x, screenY - point.y)
|
|
174
|
+
if (distance <= radius && distance < bestDistance) {
|
|
175
|
+
bestDistance = distance
|
|
176
|
+
bestNode = node
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return bestNode
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pickFallbackNodeId = (screenX, screenY) => {
|
|
184
|
+
const node = pickFallbackNode(screenX, screenY)
|
|
185
|
+
return typeof node?.[0] === 'string' ? node[0] : ''
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const handlePickedNode = (node) => {
|
|
189
|
+
const nodeId = typeof node?.id === 'string' ? node.id : typeof node?.[0] === 'string' ? node[0] : ''
|
|
190
|
+
if (!nodeId) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const kind = typeof node?.kind === 'string' ? node.kind : nodeKind(node)
|
|
195
|
+
if (kind === 'cluster') {
|
|
196
|
+
const currentScale = state.camera.scale
|
|
197
|
+
const targetScale = currentScale < 0.22 ? 0.28 : Math.min(1.1, currentScale * 1.6)
|
|
198
|
+
focusNodeInViewport(nodeId, targetScale)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
loadNodeDetails(nodeId).catch((error) => console.error(error))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const pickAt = (screenX, screenY) => {
|
|
206
|
+
if (state.rendererMode === 'fallback') {
|
|
207
|
+
const node = pickFallbackNode(screenX, screenY)
|
|
208
|
+
if (node) {
|
|
209
|
+
handlePickedNode(node)
|
|
210
|
+
}
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const requestId = Math.random().toString(36).slice(2)
|
|
219
|
+
state.renderWorker.postMessage({
|
|
220
|
+
type: 'pick',
|
|
221
|
+
requestId,
|
|
222
|
+
x: screenX,
|
|
223
|
+
y: screenY
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const zoomAtPoint = (screenX, screenY, factor) => {
|
|
228
|
+
const clamped = Math.max(0.92, Math.min(1.09, factor))
|
|
229
|
+
const before = screenToWorld(screenX, screenY)
|
|
230
|
+
state.camera.scale = clampScale(state.camera.scale * clamped)
|
|
231
|
+
state.camera.x = screenX - before.x * state.camera.scale
|
|
232
|
+
state.camera.y = screenY - before.y * state.camera.scale
|
|
233
|
+
updateWorkerCamera()
|
|
234
|
+
scheduleChunkFetch()
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export const createControlsJs = () => `
|
|
2
|
+
const setupControls = () => {
|
|
3
|
+
elements.zoomIn.addEventListener('click', () => {
|
|
4
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 1.06)
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
elements.zoomOut.addEventListener('click', () => {
|
|
8
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 0.944)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
elements.fit.addEventListener('click', () => {
|
|
12
|
+
fitFromChunk()
|
|
13
|
+
scheduleChunkFetch()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
elements.releaseNode.addEventListener('click', () => {
|
|
17
|
+
releaseSelectedNodePosition()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
elements.reset.addEventListener('click', () => {
|
|
21
|
+
clearStoredNodePositions()
|
|
22
|
+
clearNodePositionsOnServer()
|
|
23
|
+
state.camera = { x: 0, y: 0, scale: 0.22 }
|
|
24
|
+
updateWorkerCamera()
|
|
25
|
+
scheduleChunkFetch({ fit: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
elements.contentClose.addEventListener('click', () => {
|
|
29
|
+
closeContentDialog()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
elements.copyWikiLink.addEventListener('click', () => {
|
|
33
|
+
copySelectedWikiLink().catch((error) => {
|
|
34
|
+
elements.contentActionStatus.textContent = error instanceof Error ? error.message : String(error)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
elements.suggestNodeLinks.addEventListener('click', () => {
|
|
39
|
+
loadSelectedLinkSuggestions().catch((error) => {
|
|
40
|
+
elements.contentActionStatus.textContent = error instanceof Error ? error.message : String(error)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
elements.contentLinkSuggestions.addEventListener('click', (event) => {
|
|
45
|
+
const button = event.target.closest('button[data-title]')
|
|
46
|
+
if (!button) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const value = '[[' + button.dataset.title + ']]'
|
|
50
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
51
|
+
elements.contentActionStatus.textContent = 'Copied ' + value
|
|
52
|
+
}).catch(() => {
|
|
53
|
+
elements.contentActionStatus.textContent = value
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
elements.contentDialog.addEventListener('click', (event) => {
|
|
58
|
+
if (event.target === elements.contentDialog) {
|
|
59
|
+
closeContentDialog()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
elements.search.addEventListener('input', () => {
|
|
64
|
+
if (state.searchTimer) {
|
|
65
|
+
clearTimeout(state.searchTimer)
|
|
66
|
+
}
|
|
67
|
+
state.searchTimer = setTimeout(() => {
|
|
68
|
+
state.searchTimer = null
|
|
69
|
+
runGraphSearch().catch((error) => console.error(error))
|
|
70
|
+
}, 160)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runGraphSearch = async () => {
|
|
75
|
+
const token = ++state.searchToken
|
|
76
|
+
const query = (elements.search.value || '').trim()
|
|
77
|
+
if (!query) {
|
|
78
|
+
state.searchResultIds = new Set()
|
|
79
|
+
setFocusedNodeIds(new Set())
|
|
80
|
+
if (state.renderWorker && state.workerReady) {
|
|
81
|
+
state.renderWorker.postMessage({ type: 'highlight', ids: [] })
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await fetch('/api/graph-filter?q=' + encodeURIComponent(query) + '&limit=1800' + scopeQuery('&'))
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error('Failed to search graph')
|
|
89
|
+
}
|
|
90
|
+
const payload = await response.json()
|
|
91
|
+
if (token !== state.searchToken) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ids = Array.isArray(payload?.nodeIds) ? payload.nodeIds.filter((id) => typeof id === 'string' && id.length > 0) : []
|
|
96
|
+
state.searchResultIds = new Set(ids)
|
|
97
|
+
setFocusedNodeIds(state.searchResultIds)
|
|
98
|
+
if (state.renderWorker && state.workerReady) {
|
|
99
|
+
state.renderWorker.postMessage({ type: 'highlight', ids })
|
|
100
|
+
}
|
|
101
|
+
if (ids.length > 0 && state.graphMode === 'far') {
|
|
102
|
+
state.camera.scale = Math.max(state.camera.scale, 0.82)
|
|
103
|
+
updateWorkerCamera()
|
|
104
|
+
scheduleChunkFetch()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const loadAgents = async () => {
|
|
109
|
+
const response = await fetch('/api/agents')
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error('Failed to load agents')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const payload = await response.json()
|
|
115
|
+
const agents = Array.isArray(payload?.agents) ? payload.agents : []
|
|
116
|
+
|
|
117
|
+
elements.agent.innerHTML = agents
|
|
118
|
+
.map((agent) => {
|
|
119
|
+
const id = String(agent?.id || '')
|
|
120
|
+
const count = Number.isFinite(agent?.documentCount) ? agent.documentCount : 0
|
|
121
|
+
const label = id === 'shared' ? 'shared' : id
|
|
122
|
+
return '<option value="' + escapeHtml(id) + '">' + escapeHtml(label) + ' (' + count + ')</option>'
|
|
123
|
+
})
|
|
124
|
+
.join('')
|
|
125
|
+
|
|
126
|
+
const preferredAgent = initialAgentFromUrl || readStoredAgent()
|
|
127
|
+
const hasPreferred = preferredAgent && agents.some((agent) => agent?.id === preferredAgent)
|
|
128
|
+
state.agentId = hasPreferred ? preferredAgent : String(agents[0]?.id || '')
|
|
129
|
+
elements.agent.value = state.agentId
|
|
130
|
+
|
|
131
|
+
elements.agent.addEventListener('change', () => {
|
|
132
|
+
state.agentId = elements.agent.value || ''
|
|
133
|
+
writeStoredAgent(state.agentId)
|
|
134
|
+
syncAgentInUrl(state.agentId)
|
|
135
|
+
loadContexts().then(() => scheduleChunkFetch({ fit: true })).catch((error) => console.error(error))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
syncAgentInUrl(state.agentId)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const loadContexts = async () => {
|
|
142
|
+
const response = await fetch('/api/graph-contexts' + (state.agentId ? '?agent=' + encodeURIComponent(state.agentId) : ''))
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error('Failed to load graph contexts')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const payload = await response.json()
|
|
148
|
+
const contexts = Array.isArray(payload?.contexts) ? payload.contexts : []
|
|
149
|
+
const options = [
|
|
150
|
+
'<option value="">All contexts</option>',
|
|
151
|
+
...contexts.map((context) => {
|
|
152
|
+
const id = String(context?.id || '')
|
|
153
|
+
const title = String(context?.title || id || 'Untitled')
|
|
154
|
+
const count = Number.isFinite(context?.nodeCount) ? context.nodeCount : 0
|
|
155
|
+
return '<option value="' + escapeHtml(id) + '">' + escapeHtml(title) + ' (' + count + ')</option>'
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
elements.context.innerHTML = options.join('')
|
|
160
|
+
|
|
161
|
+
const preferredContext = initialContextFromUrl || readStoredContext()
|
|
162
|
+
const hasPreferred = preferredContext && contexts.some((context) => context?.id === preferredContext)
|
|
163
|
+
state.contextId = hasPreferred ? preferredContext : ''
|
|
164
|
+
elements.context.value = state.contextId
|
|
165
|
+
writeStoredContext(state.contextId)
|
|
166
|
+
syncContextInUrl(state.contextId)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const setupContextControl = () => {
|
|
170
|
+
elements.context.addEventListener('change', () => {
|
|
171
|
+
state.contextId = elements.context.value || ''
|
|
172
|
+
state.selectedNodeId = null
|
|
173
|
+
writeStoredContext(state.contextId)
|
|
174
|
+
syncContextInUrl(state.contextId)
|
|
175
|
+
scheduleChunkFetch({ fit: true })
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const createElementsJs = () => `let canvas = document.getElementById('graph')
|
|
2
|
+
let ctx2dFallback = null
|
|
3
|
+
let inputGlobalsBound = false
|
|
4
|
+
const byId = (id) => document.getElementById(id)
|
|
5
|
+
const elements = {
|
|
6
|
+
search: byId('search'),
|
|
7
|
+
agent: byId('agent'),
|
|
8
|
+
context: byId('context'),
|
|
9
|
+
nodeCount: byId('nodeCount'),
|
|
10
|
+
edgeCount: byId('edgeCount'),
|
|
11
|
+
zoomIn: byId('zoomIn'),
|
|
12
|
+
zoomOut: byId('zoomOut'),
|
|
13
|
+
fit: byId('fit'),
|
|
14
|
+
releaseNode: byId('releaseNode'),
|
|
15
|
+
reset: byId('reset'),
|
|
16
|
+
uploadOpen: byId('uploadOpen'),
|
|
17
|
+
uploadDialog: byId('uploadDialog'),
|
|
18
|
+
uploadForm: byId('uploadForm'),
|
|
19
|
+
uploadFile: byId('uploadFile'),
|
|
20
|
+
uploadTitleInput: byId('uploadTitleInput'),
|
|
21
|
+
uploadAllowSensitive: byId('uploadAllowSensitive'),
|
|
22
|
+
uploadClose: byId('uploadClose'),
|
|
23
|
+
uploadSubmit: byId('uploadSubmit'),
|
|
24
|
+
uploadStatus: byId('uploadStatus'),
|
|
25
|
+
labels: byId('graphLabels'),
|
|
26
|
+
tooltip: byId('graphTooltip'),
|
|
27
|
+
miniMap: byId('miniMap'),
|
|
28
|
+
contentDialog: byId('contentDialog'),
|
|
29
|
+
contentTitle: byId('contentTitle'),
|
|
30
|
+
contentPath: byId('contentPath'),
|
|
31
|
+
contentFacts: byId('contentFacts'),
|
|
32
|
+
contentContextLinks: byId('contentContextLinks'),
|
|
33
|
+
contentTags: byId('contentTags'),
|
|
34
|
+
contentOutgoing: byId('contentOutgoing'),
|
|
35
|
+
contentIncoming: byId('contentIncoming'),
|
|
36
|
+
contentBody: byId('contentBody'),
|
|
37
|
+
contentClose: byId('contentClose'),
|
|
38
|
+
copyWikiLink: byId('copyWikiLink'),
|
|
39
|
+
suggestNodeLinks: byId('suggestNodeLinks'),
|
|
40
|
+
contentActionStatus: byId('contentActionStatus'),
|
|
41
|
+
contentLinkSuggestions: byId('contentLinkSuggestions')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const state = {
|
|
45
|
+
camera: {
|
|
46
|
+
x: 0,
|
|
47
|
+
y: 0,
|
|
48
|
+
scale: 0.22
|
|
49
|
+
},
|
|
50
|
+
pointer: {
|
|
51
|
+
down: false,
|
|
52
|
+
moved: false,
|
|
53
|
+
dragging: false,
|
|
54
|
+
dragNodeId: '',
|
|
55
|
+
x: 0,
|
|
56
|
+
y: 0,
|
|
57
|
+
startX: 0,
|
|
58
|
+
startY: 0,
|
|
59
|
+
startWorldX: 0,
|
|
60
|
+
startWorldY: 0,
|
|
61
|
+
nodeStartX: 0,
|
|
62
|
+
nodeStartY: 0,
|
|
63
|
+
worldAnchorX: 0,
|
|
64
|
+
worldAnchorY: 0
|
|
65
|
+
},
|
|
66
|
+
viewport: {
|
|
67
|
+
width: 320,
|
|
68
|
+
height: 320,
|
|
69
|
+
ratio: window.devicePixelRatio || 1
|
|
70
|
+
},
|
|
71
|
+
workerReady: false,
|
|
72
|
+
rendererMode: 'worker',
|
|
73
|
+
renderWorker: null,
|
|
74
|
+
agentId: '',
|
|
75
|
+
contextId: '',
|
|
76
|
+
graphSignature: '',
|
|
77
|
+
graphMode: 'near',
|
|
78
|
+
nodePositionsSignature: '',
|
|
79
|
+
nodePositionsScope: '',
|
|
80
|
+
serverNodePositionsScope: '',
|
|
81
|
+
nodePositions: new Map(),
|
|
82
|
+
hoveredNodeId: '',
|
|
83
|
+
focusedNodeIds: new Set(),
|
|
84
|
+
spatialIndex: {
|
|
85
|
+
key: '',
|
|
86
|
+
cells: new Map()
|
|
87
|
+
},
|
|
88
|
+
miniMapView: null,
|
|
89
|
+
miniMapDirty: true,
|
|
90
|
+
overlayScheduled: false,
|
|
91
|
+
overlayIdleTimer: null,
|
|
92
|
+
chunk: {
|
|
93
|
+
nodes: [],
|
|
94
|
+
edges: []
|
|
95
|
+
},
|
|
96
|
+
selectedNodeId: null,
|
|
97
|
+
searchToken: 0,
|
|
98
|
+
searchTimer: null,
|
|
99
|
+
searchResultIds: new Set(),
|
|
100
|
+
fetchToken: 0,
|
|
101
|
+
fetchTimer: null,
|
|
102
|
+
fetchAbortController: null,
|
|
103
|
+
lastChunkRequestKey: '',
|
|
104
|
+
cameraSyncScheduled: false,
|
|
105
|
+
lastWheelAt: 0,
|
|
106
|
+
lastVisibleNodes: 0,
|
|
107
|
+
lastVisibleEdges: 0,
|
|
108
|
+
totals: {
|
|
109
|
+
nodes: 0,
|
|
110
|
+
edges: 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const zoomRange = {
|
|
115
|
+
min: 0.0002,
|
|
116
|
+
max: 4.5
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const selectedAgentStorageKey = 'brainlink:selected-agent'
|
|
120
|
+
const selectedContextStorageKey = 'brainlink:selected-context'
|
|
121
|
+
const nodePositionsStoragePrefix = 'brainlink:graph-node-positions:'
|
|
122
|
+
`;
|