@andespindola/brainlink 0.1.0-beta.53 → 0.1.0-beta.55
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.
|
@@ -18,6 +18,7 @@ const transformCoordinateLimit = 20_000_000
|
|
|
18
18
|
const hoverHitTestIntervalMs = 64
|
|
19
19
|
const overviewClusterMaxCount = 1400
|
|
20
20
|
const zoomRecoveryGuardMs = 1500
|
|
21
|
+
const zoomCapTargetViewportShare = 0.72
|
|
21
22
|
const state = {
|
|
22
23
|
graph: { nodes: [], edges: [] },
|
|
23
24
|
nodes: [],
|
|
@@ -53,6 +54,7 @@ const state = {
|
|
|
53
54
|
macroCenter: { x: 0, y: 0 },
|
|
54
55
|
macroRepresentative: null,
|
|
55
56
|
primaryHub: null,
|
|
57
|
+
hubNeighborDistance: Number.POSITIVE_INFINITY,
|
|
56
58
|
filterWorker: null,
|
|
57
59
|
filterReady: false,
|
|
58
60
|
lastHoverHitAt: 0,
|
|
@@ -101,6 +103,39 @@ const initialAgentFromUrl = (() => {
|
|
|
101
103
|
}
|
|
102
104
|
})()
|
|
103
105
|
|
|
106
|
+
const selectedAgentStorageKey = 'brainlink:selected-agent'
|
|
107
|
+
|
|
108
|
+
const readStoredAgent = () => {
|
|
109
|
+
try {
|
|
110
|
+
const value = window.localStorage.getItem(selectedAgentStorageKey)?.trim() ?? ''
|
|
111
|
+
return value.length > 0 ? value : ''
|
|
112
|
+
} catch {
|
|
113
|
+
return ''
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const writeStoredAgent = (agentId) => {
|
|
118
|
+
try {
|
|
119
|
+
if (!agentId) {
|
|
120
|
+
window.localStorage.removeItem(selectedAgentStorageKey)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
window.localStorage.setItem(selectedAgentStorageKey, agentId)
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const syncAgentInUrl = (agentId) => {
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(window.location.href)
|
|
130
|
+
if (agentId && agentId.trim().length > 0) {
|
|
131
|
+
url.searchParams.set('agent', agentId)
|
|
132
|
+
} else {
|
|
133
|
+
url.searchParams.delete('agent')
|
|
134
|
+
}
|
|
135
|
+
window.history.replaceState({}, '', url.toString())
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
|
|
104
139
|
const agentQuery = (separator = '?') => state.agentId ? separator + 'agent=' + encodeURIComponent(state.agentId) : ''
|
|
105
140
|
|
|
106
141
|
const setGraphStatus = text => {
|
|
@@ -199,6 +234,19 @@ const resize = () => {
|
|
|
199
234
|
const normalizeQuery = value => value.trim().toLowerCase()
|
|
200
235
|
const hubNodeRetentionLimit = 2
|
|
201
236
|
const hubNodePattern = /\b(memory\s*hub|knowledge\s*hub|hub|moc|map|memory\s*map|mapa)\b/i
|
|
237
|
+
const memoryHubPathPattern = /\bmemory[-_\s]*hub\b/i
|
|
238
|
+
|
|
239
|
+
const hubNodeScore = node => {
|
|
240
|
+
const title = node.title.trim().toLowerCase()
|
|
241
|
+
if (title === 'memory hub') return 6
|
|
242
|
+
if (title === 'knowledge hub') return 5
|
|
243
|
+
if (memoryHubPathPattern.test(node.path || '')) return 4
|
|
244
|
+
if (node.tags.some(tag => tag.trim().toLowerCase() === 'memory-hub')) return 3
|
|
245
|
+
if (/\bmoc\b/i.test(node.title)) return 2
|
|
246
|
+
return hubNodePattern.test(node.title) || hubNodePattern.test(node.path || '') || node.tags.some(tag => hubNodePattern.test(tag))
|
|
247
|
+
? 1
|
|
248
|
+
: 0
|
|
249
|
+
}
|
|
202
250
|
|
|
203
251
|
const localFilteredNodes = query =>
|
|
204
252
|
state.nodes.filter(node =>
|
|
@@ -213,8 +261,10 @@ const rankedHubNodes = () => {
|
|
|
213
261
|
}
|
|
214
262
|
|
|
215
263
|
const byTitleAndDegree = [...state.nodes]
|
|
216
|
-
.filter(node =>
|
|
264
|
+
.filter(node => hubNodeScore(node) > 0)
|
|
217
265
|
.sort((left, right) => {
|
|
266
|
+
const byHubScore = hubNodeScore(right) - hubNodeScore(left)
|
|
267
|
+
if (byHubScore !== 0) return byHubScore
|
|
218
268
|
const byDegree = (state.nodeDegrees.get(right.id) ?? 0) - (state.nodeDegrees.get(left.id) ?? 0)
|
|
219
269
|
if (byDegree !== 0) return byDegree
|
|
220
270
|
return left.title.localeCompare(right.title)
|
|
@@ -259,7 +309,10 @@ const resolveMacroRepresentative = (nodes) => {
|
|
|
259
309
|
return null
|
|
260
310
|
}
|
|
261
311
|
|
|
262
|
-
|
|
312
|
+
const hubCandidate = state.primaryHub && nodes.some(node => node.id === state.primaryHub.id)
|
|
313
|
+
? state.primaryHub
|
|
314
|
+
: null
|
|
315
|
+
let best = hubCandidate ?? nodes[0]
|
|
263
316
|
let bestDegree = state.nodeDegrees.get(best.id) ?? 0
|
|
264
317
|
|
|
265
318
|
for (let index = 1; index < nodes.length; index += 1) {
|
|
@@ -274,6 +327,24 @@ const resolveMacroRepresentative = (nodes) => {
|
|
|
274
327
|
return best
|
|
275
328
|
}
|
|
276
329
|
|
|
330
|
+
const nearestHubNeighborDistance = (hub, nodes) => {
|
|
331
|
+
if (!hub || nodes.length <= 1) {
|
|
332
|
+
return Number.POSITIVE_INFINITY
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let minimum = Number.POSITIVE_INFINITY
|
|
336
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
337
|
+
const node = nodes[index]
|
|
338
|
+
if (node.id === hub.id) continue
|
|
339
|
+
const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
|
|
340
|
+
if (distance < minimum) {
|
|
341
|
+
minimum = distance
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return minimum
|
|
346
|
+
}
|
|
347
|
+
|
|
277
348
|
const recomputeVisibility = () => {
|
|
278
349
|
const nodes = filteredNodes()
|
|
279
350
|
const ids = new Set(nodes.map(node => node.id))
|
|
@@ -289,15 +360,17 @@ const recomputeVisibility = () => {
|
|
|
289
360
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
290
361
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
291
362
|
state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
|
|
363
|
+
const primaryHub = rankedHubNodes()[0] ?? null
|
|
364
|
+
state.primaryHub = primaryHub
|
|
365
|
+
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
292
366
|
const bounds = graphBounds(nodes)
|
|
293
367
|
state.macroCenter = bounds
|
|
294
368
|
? {
|
|
295
|
-
x: (bounds.minX + bounds.maxX) / 2,
|
|
296
|
-
y: (bounds.minY + bounds.maxY) / 2
|
|
369
|
+
x: primaryHub ? primaryHub.x : (bounds.minX + bounds.maxX) / 2,
|
|
370
|
+
y: primaryHub ? primaryHub.y : (bounds.minY + bounds.maxY) / 2
|
|
297
371
|
}
|
|
298
372
|
: { x: 0, y: 0 }
|
|
299
373
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
300
|
-
state.primaryHub = rankedHubNodes()[0] ?? null
|
|
301
374
|
markRenderDirty()
|
|
302
375
|
}
|
|
303
376
|
|
|
@@ -608,23 +681,61 @@ const ensureHubNodesInRenderedSet = (nodes) => {
|
|
|
608
681
|
return nodes
|
|
609
682
|
}
|
|
610
683
|
|
|
611
|
-
const maxNodes = Math.max(renderNodeBudget, nodes.length)
|
|
684
|
+
const maxNodes = Math.max(Math.min(renderNodeBudget, nodes.length), 1)
|
|
612
685
|
const ids = new Set(nodes.map((node) => node.id))
|
|
613
686
|
const hubs = rankedHubNodes()
|
|
614
687
|
const merged = [...nodes]
|
|
615
688
|
|
|
616
|
-
for (let index = 0; index < hubs.length
|
|
689
|
+
for (let index = 0; index < hubs.length; index += 1) {
|
|
617
690
|
const hub = hubs[index]
|
|
618
|
-
if (
|
|
691
|
+
if (ids.has(hub.id)) {
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (merged.length < maxNodes) {
|
|
619
696
|
merged.push(hub)
|
|
620
697
|
ids.add(hub.id)
|
|
698
|
+
continue
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const replacementIndex = merged.findIndex((node) => !hubs.some((candidate) => candidate.id === node.id))
|
|
702
|
+
if (replacementIndex >= 0) {
|
|
703
|
+
ids.delete(merged[replacementIndex].id)
|
|
704
|
+
merged[replacementIndex] = hub
|
|
705
|
+
ids.add(hub.id)
|
|
621
706
|
}
|
|
622
707
|
}
|
|
623
708
|
|
|
624
709
|
return merged
|
|
625
710
|
}
|
|
626
711
|
|
|
627
|
-
const
|
|
712
|
+
const zoomCapByNodeCount = (nodeCount) => {
|
|
713
|
+
if (nodeCount > 50000) return 0.88
|
|
714
|
+
if (nodeCount > 20000) return 1.15
|
|
715
|
+
if (nodeCount > 6000) return 1.65
|
|
716
|
+
if (nodeCount > 2000) return 2.2
|
|
717
|
+
return zoomRange.max
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const zoomCapByHubDistance = (distance) => {
|
|
721
|
+
if (!Number.isFinite(distance) || distance <= 0) {
|
|
722
|
+
return zoomRange.max
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const rect = canvas.getBoundingClientRect()
|
|
726
|
+
const viewportWidth = Math.max(rect.width, 320)
|
|
727
|
+
const viewportHeight = Math.max(rect.height, 320)
|
|
728
|
+
const reference = Math.max(220, Math.min(viewportWidth, viewportHeight) * zoomCapTargetViewportShare)
|
|
729
|
+
return Math.max(0.3, Math.min(zoomRange.max, reference / distance))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const currentZoomMax = () => {
|
|
733
|
+
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
734
|
+
const capped = Math.min(zoomCapByNodeCount(nodeCount), zoomCapByHubDistance(state.hubNeighborDistance))
|
|
735
|
+
return Math.max(zoomRange.min * 2, capped)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const clampScale = value => Math.max(zoomRange.min, Math.min(currentZoomMax(), value))
|
|
628
739
|
const isFiniteNumber = value => Number.isFinite(value)
|
|
629
740
|
const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
|
|
630
741
|
const clampTransformCoordinate = value => {
|
|
@@ -1090,7 +1201,7 @@ const computeRenderVisibility = () => {
|
|
|
1090
1201
|
if (shouldRenderMacroGalaxy) {
|
|
1091
1202
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1092
1203
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
1093
|
-
const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1204
|
+
const representative = state.primaryHub ?? state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1094
1205
|
if (representative) {
|
|
1095
1206
|
state.renderClusters = [
|
|
1096
1207
|
{
|
|
@@ -1338,6 +1449,13 @@ const render = now => {
|
|
|
1338
1449
|
ctx.lineWidth = 1.4 / safeScale
|
|
1339
1450
|
ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
|
|
1340
1451
|
ctx.stroke()
|
|
1452
|
+
if (isMacro && cluster.representative?.title) {
|
|
1453
|
+
ctx.fillStyle = '#edf2f7'
|
|
1454
|
+
ctx.font = 12 / safeScale + 'px Inter, system-ui, sans-serif'
|
|
1455
|
+
ctx.textAlign = 'center'
|
|
1456
|
+
ctx.textBaseline = 'top'
|
|
1457
|
+
ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
|
|
1458
|
+
}
|
|
1341
1459
|
// Keep cluster markers minimal and faster to draw on large graphs.
|
|
1342
1460
|
})
|
|
1343
1461
|
} else {
|
|
@@ -1545,6 +1663,8 @@ const bindEvents = () => {
|
|
|
1545
1663
|
})
|
|
1546
1664
|
elements.agent.addEventListener('change', event => {
|
|
1547
1665
|
state.agentId = event.target.value
|
|
1666
|
+
writeStoredAgent(state.agentId)
|
|
1667
|
+
syncAgentInUrl(state.agentId)
|
|
1548
1668
|
state.selected = null
|
|
1549
1669
|
state.nodeDetails = new Map()
|
|
1550
1670
|
resetContentFilter()
|
|
@@ -1671,7 +1791,7 @@ const loadAgents = async () => {
|
|
|
1671
1791
|
const response = await fetch('/api/agents')
|
|
1672
1792
|
const payload = await response.json()
|
|
1673
1793
|
const agents = Array.isArray(payload.agents) ? payload.agents : []
|
|
1674
|
-
const preferredAgent = state.agentId || initialAgentFromUrl
|
|
1794
|
+
const preferredAgent = state.agentId || initialAgentFromUrl || readStoredAgent()
|
|
1675
1795
|
const currentExists = agents.some(agent => agent.id === preferredAgent)
|
|
1676
1796
|
const selected = currentExists
|
|
1677
1797
|
? preferredAgent
|
|
@@ -1679,6 +1799,8 @@ const loadAgents = async () => {
|
|
|
1679
1799
|
const signature = JSON.stringify(agents.map(agent => [agent.id, agent.documentCount]))
|
|
1680
1800
|
|
|
1681
1801
|
state.agentId = selected
|
|
1802
|
+
writeStoredAgent(selected)
|
|
1803
|
+
syncAgentInUrl(selected)
|
|
1682
1804
|
if (signature !== state.agentsSignature) {
|
|
1683
1805
|
const formatAgentLabel = (agent) => agent.id
|
|
1684
1806
|
elements.agent.innerHTML = agents.length
|
package/package.json
CHANGED