@andespindola/brainlink 0.1.0-beta.162 → 0.1.0-beta.164

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.
@@ -1,13 +1,13 @@
1
1
  export const createClientCss = () => `:root {
2
2
  color-scheme: dark;
3
- --bg: #0d0f12;
4
- --panel: #15191f;
5
- --panel-strong: #1c222b;
6
- --line: #29313c;
7
- --text: #edf2f7;
8
- --muted: #99a5b5;
9
- --accent: #35d0a2;
10
- --accent-weak: rgba(53, 208, 162, 0.14);
3
+ --bg: #071019;
4
+ --panel: #0d1823;
5
+ --panel-strong: #112130;
6
+ --line: rgba(143, 172, 204, 0.18);
7
+ --text: #edf4ff;
8
+ --muted: #97a9bd;
9
+ --accent: #5aa8ff;
10
+ --accent-weak: rgba(90, 168, 255, 0.18);
11
11
  --danger: #ff6b6b;
12
12
  }
13
13
 
@@ -63,7 +63,8 @@ select {
63
63
  min-height: 72px;
64
64
  padding: 10px 16px;
65
65
  border-bottom: 1px solid var(--line);
66
- background: linear-gradient(180deg, rgba(17, 21, 27, 0.96) 0%, rgba(17, 21, 27, 0.86) 100%);
66
+ background: rgba(9, 18, 28, 0.92);
67
+ box-shadow: 0 1px 14px rgba(1, 6, 13, 0.42);
67
68
  backdrop-filter: blur(8px);
68
69
  }
69
70
 
@@ -81,9 +82,15 @@ select {
81
82
  position: relative;
82
83
  width: 100%;
83
84
  height: 100%;
85
+ touch-action: none;
86
+ user-select: none;
87
+ -webkit-user-select: none;
84
88
  background:
85
- radial-gradient(circle at 18% 20%, rgba(53, 208, 162, 0.12), transparent 28rem),
86
- linear-gradient(135deg, #0d0f12 0%, #12161c 55%, #0a0d10 100%);
89
+ linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
90
+ linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
91
+ radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
92
+ #08131d;
93
+ background-size: 28px 28px, 28px 28px, auto;
87
94
  overflow: hidden;
88
95
  }
89
96
 
@@ -93,6 +100,7 @@ select {
93
100
  inset: 0;
94
101
  width: 100%;
95
102
  height: 100%;
103
+ touch-action: none;
96
104
  }
97
105
 
98
106
  #graph {
@@ -126,23 +134,23 @@ select {
126
134
  .graph-label {
127
135
  position: absolute;
128
136
  max-width: 220px;
129
- transform: translate(-50%, calc(-100% - 10px));
130
- padding: 4px 7px;
131
- border: 1px solid rgba(129, 146, 170, 0.28);
137
+ transform: translate(-50%, calc(-100% - 12px));
138
+ padding: 4px 8px;
139
+ border: 1px solid rgba(143, 172, 204, 0.18);
132
140
  border-radius: 6px;
133
- background: rgba(13, 16, 20, 0.78);
141
+ background: rgba(7, 16, 25, 0.92);
134
142
  color: var(--text);
135
143
  font-size: 11px;
136
144
  line-height: 1.25;
137
145
  white-space: nowrap;
138
146
  overflow: hidden;
139
147
  text-overflow: ellipsis;
140
- box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
148
+ box-shadow: 0 12px 28px rgba(1, 6, 13, 0.36);
141
149
  }
142
150
 
143
151
  .graph-label.is-focused {
144
- border-color: rgba(53, 208, 162, 0.72);
145
- color: #dffbf3;
152
+ border-color: rgba(90, 168, 255, 0.56);
153
+ color: #d7e9ff;
146
154
  }
147
155
 
148
156
  .graph-tooltip {
@@ -152,12 +160,12 @@ select {
152
160
  padding: 8px 10px;
153
161
  border: 1px solid var(--line);
154
162
  border-radius: 6px;
155
- background: rgba(13, 16, 20, 0.94);
163
+ background: rgba(9, 18, 28, 0.96);
156
164
  color: var(--text);
157
165
  font-size: 12px;
158
166
  line-height: 1.35;
159
167
  pointer-events: none;
160
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38);
168
+ box-shadow: 0 18px 44px rgba(1, 6, 13, 0.42);
161
169
  }
162
170
 
163
171
  .graph-tooltip strong,
@@ -179,10 +187,10 @@ select {
179
187
  z-index: 3;
180
188
  width: 180px;
181
189
  height: 120px;
182
- border: 1px solid rgba(129, 146, 170, 0.28);
190
+ border: 1px solid rgba(143, 172, 204, 0.18);
183
191
  border-radius: 8px;
184
- background: rgba(13, 16, 20, 0.78);
185
- box-shadow: 0 16px 42px rgba(0, 0, 0, 0.38);
192
+ background: rgba(8, 19, 29, 0.84);
193
+ box-shadow: 0 18px 40px rgba(1, 6, 13, 0.36);
186
194
  }
187
195
 
188
196
  .eyebrow {
@@ -215,7 +223,7 @@ select {
215
223
  border: 1px solid var(--line);
216
224
  border-radius: 8px;
217
225
  outline: none;
218
- background: rgba(21, 25, 31, 0.88);
226
+ background: rgba(12, 24, 36, 0.94);
219
227
  color: var(--text);
220
228
  padding: 0 14px;
221
229
  }
@@ -236,7 +244,7 @@ select {
236
244
  height: 38px;
237
245
  border: 1px solid var(--line);
238
246
  border-radius: 8px;
239
- background: rgba(21, 25, 31, 0.88);
247
+ background: rgba(12, 24, 36, 0.94);
240
248
  color: var(--text);
241
249
  cursor: pointer;
242
250
  }
@@ -257,7 +265,7 @@ select {
257
265
  padding: 10px 12px;
258
266
  border: 1px solid var(--line);
259
267
  border-radius: 10px;
260
- background: rgba(21, 25, 31, 0.88);
268
+ background: rgba(12, 24, 36, 0.94);
261
269
  display: grid;
262
270
  gap: 3px;
263
271
  }
@@ -332,7 +340,7 @@ li small {
332
340
  padding: 12px;
333
341
  border: 1px solid var(--line);
334
342
  border-radius: 8px;
335
- background: #101419;
343
+ background: #091521;
336
344
  color: var(--text);
337
345
  white-space: pre-wrap;
338
346
  overflow: auto;
@@ -362,7 +370,8 @@ li small {
362
370
  border-radius: 8px;
363
371
  background: var(--panel);
364
372
  color: var(--text);
365
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
373
+ box-shadow: 0 24px 80px rgba(23, 32, 51, 0.22);
374
+ backdrop-filter: blur(10px);
366
375
  overflow: hidden;
367
376
  }
368
377
 
@@ -466,7 +475,7 @@ li small {
466
475
  padding: 10px;
467
476
  border: 1px solid var(--line);
468
477
  border-radius: 8px;
469
- background: var(--panel-strong);
478
+ background: #091521;
470
479
  display: grid;
471
480
  grid-template-rows: auto minmax(0, 1fr);
472
481
  gap: 8px;
@@ -506,7 +515,7 @@ li small {
506
515
  display: flex;
507
516
  align-items: center;
508
517
  justify-content: center;
509
- background: transparent;
518
+ background: linear-gradient(180deg, rgba(7, 16, 25, 0), rgba(7, 16, 25, 0.84));
510
519
  }
511
520
 
512
521
  .app-footer small {
@@ -369,14 +369,41 @@ const parseColor = (hex) => {
369
369
  }
370
370
 
371
371
  const graphTheme = {
372
- node: parseColor('#aeb8c5'),
373
- nodeCluster: parseColor('#6bb7e8'),
374
- nodeHighlight: parseColor('#f5c24a'),
375
- nodeSelected: parseColor('#ffffff'),
376
- edge: [0.58, 0.64, 0.74, 0.24],
377
- edgeHeavy: [0.78, 0.84, 0.92, 0.44],
378
- clear: parseColor('#0d0f12')
379
- }
372
+ node: parseColor('#5aa8ff'),
373
+ nodeCluster: parseColor('#3f7fbd'),
374
+ nodeHighlight: parseColor('#ffcb67'),
375
+ nodeSelected: parseColor('#edf4ff'),
376
+ nodePalette: [
377
+ parseColor('#5aa8ff'),
378
+ parseColor('#5ecf92'),
379
+ parseColor('#ffb65c'),
380
+ parseColor('#ff7dac'),
381
+ parseColor('#a88fff'),
382
+ parseColor('#59d0dd'),
383
+ parseColor('#ff8f6a'),
384
+ parseColor('#a4b3c3'),
385
+ parseColor('#c9945f'),
386
+ parseColor('#7cb6ff')
387
+ ],
388
+ edge: [0.59, 0.71, 0.83, 0.14],
389
+ edgeHeavy: [0.59, 0.71, 0.83, 0.3],
390
+ clear: parseColor('#08131d')
391
+ }
392
+
393
+ const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
394
+
395
+ const segmentColorIndex = (segment) => {
396
+ const value = String(segment || '')
397
+ let hash = 0
398
+ for (let index = 0; index < value.length; index += 1) {
399
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
400
+ }
401
+ return Math.abs(hash) % segmentPalette.length
402
+ }
403
+
404
+ const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
405
+ const nodeKind = (node) => node?.[6] === 'cluster' ? 'cluster' : 'node'
406
+ const isRealGraphNode = (node) => nodeKind(node) === 'node'
380
407
 
381
408
  const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
382
409
 
@@ -518,7 +545,7 @@ const drawFallback = () => {
518
545
  canvas.width = Math.floor(width * ratio)
519
546
  canvas.height = Math.floor(height * ratio)
520
547
  ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
521
- ctx2dFallback.fillStyle = '#0d0f12'
548
+ ctx2dFallback.fillStyle = '#08131d'
522
549
  ctx2dFallback.fillRect(0, 0, width, height)
523
550
 
524
551
  const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
@@ -528,7 +555,7 @@ const drawFallback = () => {
528
555
  nodeById.set(nodes[i][0], nodes[i])
529
556
  }
530
557
 
531
- ctx2dFallback.strokeStyle = 'rgba(150,165,190,0.2)'
558
+ ctx2dFallback.strokeStyle = 'rgba(151,181,212,0.18)'
532
559
  ctx2dFallback.lineWidth = 1
533
560
  for (let i = 0; i < edges.length; i += 1) {
534
561
  const edge = edges[i]
@@ -547,16 +574,16 @@ const drawFallback = () => {
547
574
  const node = nodes[i]
548
575
  const p = worldToScreen(node[2], node[3])
549
576
  const selected = state.selectedNodeId === node[0]
550
- const color = node[6] === 'cluster' ? '#6bb7e8' : '#aeb8c5'
577
+ const color = segmentColor(node[5] || node[4] || node[1])
551
578
  const radius = Math.max(2.4, Math.min(14, 4 + node[7] * 0.55))
552
579
 
553
580
  ctx2dFallback.beginPath()
554
- ctx2dFallback.fillStyle = selected ? '#ffffff' : color
581
+ ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
555
582
  ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
556
583
  ctx2dFallback.fill()
557
584
  }
558
585
 
559
- ctx2dFallback.fillStyle = '#edf2f7'
586
+ ctx2dFallback.fillStyle = '#97a9bd'
560
587
  ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
561
588
  ctx2dFallback.textAlign = 'center'
562
589
  ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
@@ -645,6 +672,28 @@ const updateNodePositionInChunk = (nodeId, x, y) => {
645
672
  updateGraphOverlays()
646
673
  }
647
674
 
675
+ const focusNodeInViewport = (nodeId, nextScale = null) => {
676
+ const node = nodeByIdFromChunk().get(nodeId)
677
+ if (!node) {
678
+ return false
679
+ }
680
+
681
+ const x = Number(node[2])
682
+ const y = Number(node[3])
683
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
684
+ return false
685
+ }
686
+
687
+ if (Number.isFinite(nextScale)) {
688
+ state.camera.scale = clampScale(Number(nextScale))
689
+ }
690
+ state.camera.x = state.viewport.width / 2 - x * state.camera.scale
691
+ state.camera.y = state.viewport.height / 2 - y * state.camera.scale
692
+ updateWorkerCamera()
693
+ scheduleChunkFetch()
694
+ return true
695
+ }
696
+
648
697
  const showTooltip = (node, pointer) => {
649
698
  if (!elements.tooltip || !node) {
650
699
  return
@@ -718,7 +767,7 @@ const drawMiniMap = () => {
718
767
  miniMap.height = Math.floor(height * ratio)
719
768
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
720
769
  ctx.clearRect(0, 0, width, height)
721
- ctx.fillStyle = 'rgba(13, 16, 20, 0.86)'
770
+ ctx.fillStyle = 'rgba(8, 19, 29, 0.88)'
722
771
  ctx.fillRect(0, 0, width, height)
723
772
 
724
773
  const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
@@ -738,7 +787,7 @@ const drawMiniMap = () => {
738
787
  })
739
788
  state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
740
789
 
741
- ctx.fillStyle = 'rgba(174, 184, 197, 0.62)'
790
+ ctx.fillStyle = 'rgba(90, 168, 255, 0.62)'
742
791
  nodes.forEach((node) => {
743
792
  const point = toMini(Number(node[2]), Number(node[3]))
744
793
  ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
@@ -748,7 +797,7 @@ const drawMiniMap = () => {
748
797
  const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
749
798
  const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
750
799
  const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
751
- ctx.strokeStyle = 'rgba(53, 208, 162, 0.86)'
800
+ ctx.strokeStyle = 'rgba(90, 168, 255, 0.86)'
752
801
  ctx.lineWidth = 1
753
802
  ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
754
803
  }
@@ -1103,11 +1152,28 @@ const pickFallbackNodeId = (screenX, screenY) => {
1103
1152
  return typeof node?.[0] === 'string' ? node[0] : ''
1104
1153
  }
1105
1154
 
1155
+ const handlePickedNode = (node) => {
1156
+ const nodeId = typeof node?.id === 'string' ? node.id : typeof node?.[0] === 'string' ? node[0] : ''
1157
+ if (!nodeId) {
1158
+ return
1159
+ }
1160
+
1161
+ const kind = typeof node?.kind === 'string' ? node.kind : nodeKind(node)
1162
+ if (kind === 'cluster') {
1163
+ const currentScale = state.camera.scale
1164
+ const targetScale = currentScale < 0.22 ? 0.28 : Math.min(1.1, currentScale * 1.6)
1165
+ focusNodeInViewport(nodeId, targetScale)
1166
+ return
1167
+ }
1168
+
1169
+ loadNodeDetails(nodeId).catch((error) => console.error(error))
1170
+ }
1171
+
1106
1172
  const pickAt = (screenX, screenY) => {
1107
1173
  if (state.rendererMode === 'fallback') {
1108
- const nodeId = pickFallbackNodeId(screenX, screenY)
1109
- if (nodeId) {
1110
- loadNodeDetails(nodeId).catch((error) => console.error(error))
1174
+ const node = pickFallbackNode(screenX, screenY)
1175
+ if (node) {
1176
+ handlePickedNode(node)
1111
1177
  }
1112
1178
  return
1113
1179
  }
@@ -1145,6 +1211,19 @@ const resolvePointer = (event) => {
1145
1211
 
1146
1212
  const setupInput = () => {
1147
1213
  const dragActivationDistance = 6
1214
+ const resetPointerState = (pointerId = null) => {
1215
+ state.pointer.down = false
1216
+ state.pointer.dragging = false
1217
+ state.pointer.dragNodeId = ''
1218
+ canvas.classList.remove('is-node-dragging')
1219
+ if (pointerId !== null) {
1220
+ try {
1221
+ if (canvas.hasPointerCapture(pointerId)) {
1222
+ canvas.releasePointerCapture(pointerId)
1223
+ }
1224
+ } catch {}
1225
+ }
1226
+ }
1148
1227
 
1149
1228
  canvas.addEventListener('wheel', (event) => {
1150
1229
  event.preventDefault()
@@ -1155,9 +1234,10 @@ const setupInput = () => {
1155
1234
  }, { passive: false })
1156
1235
 
1157
1236
  canvas.addEventListener('pointerdown', (event) => {
1237
+ event.preventDefault()
1158
1238
  const pointer = resolvePointer(event)
1159
1239
  const candidateNode = pickFallbackNode(pointer.x, pointer.y)
1160
- const candidateNodeId = candidateNode?.[6] === 'node' && typeof candidateNode?.[0] === 'string' ? candidateNode[0] : ''
1240
+ const candidateNodeId = isRealGraphNode(candidateNode) && typeof candidateNode?.[0] === 'string' ? candidateNode[0] : ''
1161
1241
  const candidateX = Number(candidateNode?.[2])
1162
1242
  const candidateY = Number(candidateNode?.[3])
1163
1243
  const world = screenToWorld(pointer.x, pointer.y)
@@ -1175,10 +1255,15 @@ const setupInput = () => {
1175
1255
  state.pointer.nodeStartY = candidateNodeId && Number.isFinite(candidateY) ? candidateY : 0
1176
1256
  state.pointer.worldAnchorX = world.x
1177
1257
  state.pointer.worldAnchorY = world.y
1178
- canvas.setPointerCapture(event.pointerId)
1258
+ try {
1259
+ canvas.setPointerCapture(event.pointerId)
1260
+ } catch {}
1179
1261
  })
1180
1262
 
1181
1263
  canvas.addEventListener('pointermove', (event) => {
1264
+ if (state.pointer.down) {
1265
+ event.preventDefault()
1266
+ }
1182
1267
  const pointer = resolvePointer(event)
1183
1268
 
1184
1269
  if (state.pointer.down) {
@@ -1216,7 +1301,7 @@ const setupInput = () => {
1216
1301
  }
1217
1302
 
1218
1303
  const hovered = pickFallbackNode(pointer.x, pointer.y)
1219
- const hoveredId = hovered?.[6] === 'node' && typeof hovered?.[0] === 'string' ? hovered[0] : ''
1304
+ const hoveredId = isRealGraphNode(hovered) && typeof hovered?.[0] === 'string' ? hovered[0] : ''
1220
1305
  if (state.hoveredNodeId !== hoveredId) {
1221
1306
  state.hoveredNodeId = hoveredId
1222
1307
  canvas.classList.toggle('is-node-hover', Boolean(hoveredId))
@@ -1235,11 +1320,7 @@ const setupInput = () => {
1235
1320
  const shouldPick = !state.pointer.dragging && distanceFromStart < dragActivationDistance
1236
1321
  const shouldRefreshAfterDrag = state.pointer.dragging
1237
1322
  const shouldPersistNodePosition = state.pointer.dragging && Boolean(state.pointer.dragNodeId)
1238
- state.pointer.down = false
1239
- state.pointer.dragging = false
1240
- canvas.classList.remove('is-node-dragging')
1241
- state.pointer.dragNodeId = ''
1242
- canvas.releasePointerCapture(event.pointerId)
1323
+ resetPointerState(event.pointerId)
1243
1324
 
1244
1325
  if (shouldPick) {
1245
1326
  pickAt(pointer.x, pointer.y)
@@ -1262,6 +1343,16 @@ const setupInput = () => {
1262
1343
  updateGraphOverlays()
1263
1344
  })
1264
1345
 
1346
+ canvas.addEventListener('pointercancel', (event) => {
1347
+ resetPointerState(event.pointerId)
1348
+ hideTooltip()
1349
+ updateGraphOverlays()
1350
+ })
1351
+
1352
+ canvas.addEventListener('lostpointercapture', () => {
1353
+ resetPointerState()
1354
+ })
1355
+
1265
1356
  elements.miniMap.addEventListener('click', (event) => {
1266
1357
  if (!state.miniMapView) {
1267
1358
  return
@@ -1482,7 +1573,7 @@ const setupRenderWorker = () => {
1482
1573
 
1483
1574
  if (payload.type === 'pick-result') {
1484
1575
  if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
1485
- loadNodeDetails(payload.node.id).catch((error) => console.error(error))
1576
+ handlePickedNode(payload.node)
1486
1577
  }
1487
1578
  return
1488
1579
  }
@@ -14,6 +14,7 @@ const state = {
14
14
  y: new Float32Array(0),
15
15
  relevance: new Float32Array(0),
16
16
  radius: new Float32Array(0),
17
+ colorIndex: new Uint8Array(0),
17
18
  visible: new Uint8Array(0),
18
19
  highlighted: new Uint8Array(0),
19
20
  focused: new Uint8Array(0),
@@ -39,13 +40,25 @@ let pointPositionsBuffer = new Float32Array(0)
39
40
  let pointSizesBuffer = new Float32Array(0)
40
41
 
41
42
  const defaultTheme = {
42
- node: [0.68, 0.72, 0.78, 1],
43
- nodeCluster: [0.42, 0.76, 0.92, 1],
44
- nodeHighlight: [0.95, 0.76, 0.22, 1],
45
- nodeSelected: [0.99, 0.99, 1, 1],
46
- edge: [0.58, 0.64, 0.74, 0.24],
47
- edgeHeavy: [0.78, 0.84, 0.92, 0.44],
48
- clear: [0.05, 0.06, 0.08, 1]
43
+ node: [0.30, 0.56, 0.85, 1],
44
+ nodeCluster: [0.18, 0.44, 0.71, 1],
45
+ nodeHighlight: [0.95, 0.70, 0.25, 1],
46
+ nodeSelected: [0.09, 0.13, 0.20, 1],
47
+ nodePalette: [
48
+ [0.30, 0.56, 0.85, 1],
49
+ [0.40, 0.73, 0.43, 1],
50
+ [0.94, 0.64, 0.23, 1],
51
+ [0.85, 0.37, 0.55, 1],
52
+ [0.55, 0.45, 0.85, 1],
53
+ [0.33, 0.75, 0.77, 1],
54
+ [0.93, 0.42, 0.34, 1],
55
+ [0.60, 0.65, 0.70, 1],
56
+ [0.72, 0.51, 0.33, 1],
57
+ [0.44, 0.62, 0.85, 1]
58
+ ],
59
+ edge: [0.23, 0.31, 0.42, 0.18],
60
+ edgeHeavy: [0.23, 0.31, 0.42, 0.34],
61
+ clear: [0.96, 0.97, 0.98, 1]
49
62
  }
50
63
 
51
64
  const theme = { ...defaultTheme }
@@ -181,6 +194,7 @@ const ensureNodeCapacity = (count) => {
181
194
  state.y = new Float32Array(nextCapacity)
182
195
  state.relevance = new Float32Array(nextCapacity)
183
196
  state.radius = new Float32Array(nextCapacity)
197
+ state.colorIndex = new Uint8Array(nextCapacity)
184
198
  state.visible = new Uint8Array(nextCapacity)
185
199
  state.highlighted = new Uint8Array(nextCapacity)
186
200
  state.focused = new Uint8Array(nextCapacity)
@@ -199,11 +213,21 @@ const ensureEdgeCapacity = (count) => {
199
213
  }
200
214
 
201
215
  const nodeRadius = (relevance, kind) => {
202
- const base = kind === 'cluster' ? 7.8 : 4.6
203
- const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
216
+ const base = kind === 'cluster' ? 8.8 : 5.4
217
+ const modifier = Math.min(5.6, Math.max(0, relevance * 0.62))
204
218
  return base + modifier
205
219
  }
206
220
 
221
+ const segmentColorIndex = (segment) => {
222
+ const value = String(segment || '')
223
+ let hash = 0
224
+ for (let index = 0; index < value.length; index += 1) {
225
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
226
+ }
227
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
228
+ return Math.abs(hash) % palette.length
229
+ }
230
+
207
231
  const loadChunk = (chunk) => {
208
232
  const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
209
233
  const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
@@ -223,6 +247,7 @@ const loadChunk = (chunk) => {
223
247
  const title = typeof row?.[1] === 'string' ? row[1] : id
224
248
  const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
225
249
  const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
250
+ const segment = typeof row?.[5] === 'string' ? row[5] : ''
226
251
  const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
227
252
  const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
228
253
 
@@ -233,6 +258,7 @@ const loadChunk = (chunk) => {
233
258
  state.y[index] = y
234
259
  state.relevance[index] = relevance
235
260
  state.radius[index] = nodeRadius(relevance, kind)
261
+ state.colorIndex[index] = segmentColorIndex(segment || title)
236
262
  state.visible[index] = 0
237
263
  state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
238
264
  state.focused[index] = focusedIds.has(id) ? 1 : 0
@@ -349,6 +375,13 @@ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
349
375
  gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
350
376
  }
351
377
 
378
+ const drawColoredNodeLayer = (predicate, radiusBoost = 1) => {
379
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
380
+ for (let colorIndex = 0; colorIndex < palette.length; colorIndex += 1) {
381
+ drawNodeLayer((index) => predicate(index) && state.colorIndex[index] === colorIndex, palette[colorIndex], radiusBoost)
382
+ }
383
+ }
384
+
352
385
  const clear = () => {
353
386
  if (!gl || !canvas) return
354
387
  gl.viewport(0, 0, canvas.width, canvas.height)
@@ -398,15 +431,13 @@ const renderFrame = (now) => {
398
431
  scheduleSettledRender(now)
399
432
  }
400
433
 
401
- drawNodeLayer(
402
- (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
403
- theme.node,
434
+ drawColoredNodeLayer(
435
+ (index) => state.visible[index] === 1 && state.kinds[index] !== 'cluster' && state.selected[index] === 0 && state.highlighted[index] === 0 && state.focused[index] === 0,
404
436
  1
405
437
  )
406
438
 
407
- drawNodeLayer(
439
+ drawColoredNodeLayer(
408
440
  (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
409
- theme.nodeCluster,
410
441
  1.15
411
442
  )
412
443
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.162",
3
+ "version": "0.1.0-beta.164",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",