@andespindola/brainlink 0.1.0-beta.48 → 0.1.0-beta.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -82,8 +82,9 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
82
82
|
- Local HTTP API.
|
|
83
83
|
- Realtime graph UI with agent selector and colored knowledge groups.
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
|
-
- Large graph layout API automatically uses compact payload encoding
|
|
85
|
+
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
86
86
|
- Zoomed-out graph LOD now clusters dense regions and progressively expands nodes as zoom increases.
|
|
87
|
+
- Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
|
|
87
88
|
- Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
|
|
88
89
|
- Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
|
|
89
90
|
|
|
@@ -2,13 +2,14 @@ export const createClientJs = () => `const canvas = document.getElementById('gra
|
|
|
2
2
|
const ctx = canvas.getContext('2d')
|
|
3
3
|
const largeGraphNodeThreshold = 4000
|
|
4
4
|
const massiveGraphNodeThreshold = 20000
|
|
5
|
-
const largeGraphEdgeRenderLimit =
|
|
5
|
+
const largeGraphEdgeRenderLimit = 120000
|
|
6
6
|
const renderNodeBudget = 900
|
|
7
7
|
const renderEdgeBudget = 2400
|
|
8
8
|
const clusterActivationNodeThreshold = 600
|
|
9
9
|
const clusterZoomThreshold = 0.18
|
|
10
10
|
const macroGalaxyZoomThreshold = 0.012
|
|
11
11
|
const massiveAutoFitMacroScale = 0.006
|
|
12
|
+
const defaultMacroScale = 0.006
|
|
12
13
|
const clusterCellPixelSize = 64
|
|
13
14
|
const minNodePixelRadius = 2.3
|
|
14
15
|
const viewportPaddingPx = 280
|
|
@@ -609,7 +610,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
609
610
|
return { min: 0.0008, max: 0.24 }
|
|
610
611
|
}
|
|
611
612
|
|
|
612
|
-
const fitView = (options = { useFiltered: true }) => {
|
|
613
|
+
const fitView = (options = { useFiltered: true, macro: false }) => {
|
|
613
614
|
const rect = canvas.getBoundingClientRect()
|
|
614
615
|
const width = Math.max(rect.width, 320)
|
|
615
616
|
const height = Math.max(rect.height, 320)
|
|
@@ -640,9 +641,12 @@ const fitView = (options = { useFiltered: true }) => {
|
|
|
640
641
|
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
|
|
641
642
|
const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
|
|
642
643
|
const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
644
|
+
const macroScale = nodes.length > massiveGraphNodeThreshold ? massiveAutoFitMacroScale : defaultMacroScale
|
|
645
|
+
const scale = options.macro && nodes.length > 1
|
|
646
|
+
? clampScale(Math.min(baselineScale, macroScale))
|
|
647
|
+
: nodes.length > massiveGraphNodeThreshold
|
|
648
|
+
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
649
|
+
: baselineScale
|
|
646
650
|
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
647
651
|
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
648
652
|
|
|
@@ -656,7 +660,7 @@ const fitView = (options = { useFiltered: true }) => {
|
|
|
656
660
|
markRenderDirty()
|
|
657
661
|
}
|
|
658
662
|
|
|
659
|
-
const resetView = () => fitView({ useFiltered: false })
|
|
663
|
+
const resetView = () => fitView({ useFiltered: false, macro: true })
|
|
660
664
|
|
|
661
665
|
const createLayout = graph => {
|
|
662
666
|
const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
|
|
@@ -1005,6 +1009,32 @@ const computeRenderVisibility = () => {
|
|
|
1005
1009
|
state.lastViewportKey = viewportKey
|
|
1006
1010
|
state.renderVisibilityDirty = false
|
|
1007
1011
|
|
|
1012
|
+
const shouldRenderMacroGalaxy =
|
|
1013
|
+
state.transform.scale <= macroGalaxyZoomThreshold && state.visibleNodes.length > 1
|
|
1014
|
+
|
|
1015
|
+
if (shouldRenderMacroGalaxy) {
|
|
1016
|
+
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1017
|
+
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
1018
|
+
const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1019
|
+
if (representative) {
|
|
1020
|
+
state.renderClusters = [
|
|
1021
|
+
{
|
|
1022
|
+
id: 'macro-galaxy',
|
|
1023
|
+
x: state.macroCenter.x,
|
|
1024
|
+
y: state.macroCenter.y,
|
|
1025
|
+
count: sourceNodes.length,
|
|
1026
|
+
representative
|
|
1027
|
+
}
|
|
1028
|
+
]
|
|
1029
|
+
state.renderNodes = [representative]
|
|
1030
|
+
} else {
|
|
1031
|
+
state.renderClusters = []
|
|
1032
|
+
state.renderNodes = []
|
|
1033
|
+
}
|
|
1034
|
+
state.renderEdges = []
|
|
1035
|
+
return
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1008
1038
|
if (state.visibleNodes.length <= 2000) {
|
|
1009
1039
|
state.renderNodes = state.visibleNodes
|
|
1010
1040
|
state.renderClusters = []
|
|
@@ -1016,26 +1046,6 @@ const computeRenderVisibility = () => {
|
|
|
1016
1046
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
1017
1047
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1018
1048
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
1019
|
-
if (state.transform.scale <= macroGalaxyZoomThreshold) {
|
|
1020
|
-
const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1021
|
-
if (representative) {
|
|
1022
|
-
state.renderClusters = [
|
|
1023
|
-
{
|
|
1024
|
-
id: 'macro-galaxy',
|
|
1025
|
-
x: state.macroCenter.x,
|
|
1026
|
-
y: state.macroCenter.y,
|
|
1027
|
-
count: sourceNodes.length,
|
|
1028
|
-
representative
|
|
1029
|
-
}
|
|
1030
|
-
]
|
|
1031
|
-
state.renderNodes = [representative]
|
|
1032
|
-
} else {
|
|
1033
|
-
state.renderClusters = []
|
|
1034
|
-
state.renderNodes = []
|
|
1035
|
-
}
|
|
1036
|
-
state.renderEdges = []
|
|
1037
|
-
return
|
|
1038
|
-
}
|
|
1039
1049
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
1040
1050
|
const sampled = sourceNodes.length > sampleLimit
|
|
1041
1051
|
? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), sourceNodes)
|
|
@@ -1325,6 +1335,8 @@ const fetchNodeDetails = async node => {
|
|
|
1325
1335
|
return detail
|
|
1326
1336
|
}
|
|
1327
1337
|
|
|
1338
|
+
const wait = async (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds))
|
|
1339
|
+
|
|
1328
1340
|
const openContentDialog = async node => {
|
|
1329
1341
|
if (!node) return
|
|
1330
1342
|
elements.contentTitle.textContent = node.title || 'Loading...'
|
|
@@ -1340,19 +1352,32 @@ const openContentDialog = async node => {
|
|
|
1340
1352
|
elements.contentDialog.showModal()
|
|
1341
1353
|
}
|
|
1342
1354
|
|
|
1355
|
+
const applyDetailToDialog = detail => {
|
|
1356
|
+
elements.contentTitle.textContent = detail.title
|
|
1357
|
+
elements.contentPath.textContent = detail.path
|
|
1358
|
+
elements.contentTags.innerHTML = detail.tags.length
|
|
1359
|
+
? detail.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
|
|
1360
|
+
: '<span>No tags</span>'
|
|
1361
|
+
elements.contentBody.textContent = detail.content
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1343
1364
|
try {
|
|
1344
1365
|
const detailedNode = await fetchNodeDetails(node)
|
|
1345
1366
|
if (state.selected?.id !== node.id) {
|
|
1346
1367
|
return
|
|
1347
1368
|
}
|
|
1348
|
-
|
|
1349
|
-
elements.contentPath.textContent = detailedNode.path
|
|
1350
|
-
elements.contentTags.innerHTML = detailedNode.tags.length
|
|
1351
|
-
? detailedNode.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
|
|
1352
|
-
: '<span>No tags</span>'
|
|
1353
|
-
elements.contentBody.textContent = detailedNode.content
|
|
1369
|
+
applyDetailToDialog(detailedNode)
|
|
1354
1370
|
} catch {
|
|
1355
|
-
|
|
1371
|
+
try {
|
|
1372
|
+
await wait(120)
|
|
1373
|
+
const retriedNode = await fetchNodeDetails(node)
|
|
1374
|
+
if (state.selected?.id !== node.id) {
|
|
1375
|
+
return
|
|
1376
|
+
}
|
|
1377
|
+
applyDetailToDialog(retriedNode)
|
|
1378
|
+
} catch {
|
|
1379
|
+
elements.contentBody.textContent = 'Unable to load note content.'
|
|
1380
|
+
}
|
|
1356
1381
|
}
|
|
1357
1382
|
}
|
|
1358
1383
|
|
|
@@ -63,22 +63,62 @@ const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
|
|
|
63
63
|
return 30_000;
|
|
64
64
|
return compactGraphLayoutEdgeLimit;
|
|
65
65
|
};
|
|
66
|
+
const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
|
|
67
|
+
const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
|
|
68
|
+
const selectCompactEdges = (layout, limit) => {
|
|
69
|
+
const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
|
|
70
|
+
if (resolvedEdges.length <= limit) {
|
|
71
|
+
return resolvedEdges;
|
|
72
|
+
}
|
|
73
|
+
const bestEdgeByEndpoint = new Map();
|
|
74
|
+
for (let index = 0; index < resolvedEdges.length; index += 1) {
|
|
75
|
+
const edge = resolvedEdges[index];
|
|
76
|
+
const endpoints = [edge.source, edge.target];
|
|
77
|
+
for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
|
|
78
|
+
const endpoint = endpoints[endpointIndex];
|
|
79
|
+
const previous = bestEdgeByEndpoint.get(endpoint);
|
|
80
|
+
if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
|
|
81
|
+
bestEdgeByEndpoint.set(endpoint, edge);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const selected = new Map();
|
|
86
|
+
for (const edge of bestEdgeByEndpoint.values()) {
|
|
87
|
+
selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
|
|
88
|
+
}
|
|
89
|
+
if (selected.size > limit) {
|
|
90
|
+
return Array.from(selected.values())
|
|
91
|
+
.sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
|
|
92
|
+
.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
|
|
95
|
+
for (let index = 0; index < byWeight.length; index += 1) {
|
|
96
|
+
if (selected.size >= limit) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const edge = byWeight[index];
|
|
100
|
+
const key = edgeKey(edge.source, edge.target, edge.priority);
|
|
101
|
+
if (!selected.has(key)) {
|
|
102
|
+
selected.set(key, edge);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return Array.from(selected.values());
|
|
106
|
+
};
|
|
66
107
|
const stripLayoutContent = (layout) => ({
|
|
67
108
|
...layout,
|
|
68
109
|
nodes: layout.nodes.map(({ content, ...node }) => node)
|
|
69
110
|
});
|
|
70
111
|
const compactLayoutPayload = (layout) => {
|
|
71
112
|
const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
|
|
113
|
+
const compactEdges = selectCompactEdges(layout, edgeLimit);
|
|
72
114
|
const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
|
|
73
|
-
const
|
|
74
|
-
.sort((left, right) => (right.weight ?? 1) - (left.weight ?? 1))
|
|
75
|
-
.slice(0, edgeLimit)
|
|
115
|
+
const compactEdgeRows = compactEdges
|
|
76
116
|
.map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
|
|
77
117
|
return {
|
|
78
118
|
compact: true,
|
|
79
119
|
layout: {
|
|
80
120
|
nodes: compactNodes,
|
|
81
|
-
edges:
|
|
121
|
+
edges: compactEdgeRows
|
|
82
122
|
},
|
|
83
123
|
totals: {
|
|
84
124
|
nodes: layout.nodes.length,
|
package/package.json
CHANGED