@andespindola/brainlink 0.1.0-beta.143 → 0.1.0-beta.144

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.
@@ -62,6 +62,14 @@ export const createClientHtml = () => `<!doctype html>
62
62
  <button id="contentClose" type="button">Close</button>
63
63
  </header>
64
64
  <div class="content-meta">
65
+ <section class="content-meta-section">
66
+ <h3>Facts</h3>
67
+ <ul id="contentFacts"></ul>
68
+ </section>
69
+ <section class="content-meta-section">
70
+ <h3>Context Links</h3>
71
+ <ul id="contentContextLinks"></ul>
72
+ </section>
65
73
  <section class="content-meta-section">
66
74
  <h3>Tags</h3>
67
75
  <div id="contentTags" class="tags"></div>
@@ -14,6 +14,8 @@ const elements = {
14
14
  contentDialog: byId('contentDialog'),
15
15
  contentTitle: byId('contentTitle'),
16
16
  contentPath: byId('contentPath'),
17
+ contentFacts: byId('contentFacts'),
18
+ contentContextLinks: byId('contentContextLinks'),
17
19
  contentTags: byId('contentTags'),
18
20
  contentOutgoing: byId('contentOutgoing'),
19
21
  contentIncoming: byId('contentIncoming'),
@@ -279,6 +281,71 @@ const list = (items) => {
279
281
  .join('')
280
282
  }
281
283
 
284
+ const extractContextLinks = (content) => {
285
+ if (typeof content !== 'string' || content.length === 0) {
286
+ return []
287
+ }
288
+ const lines = content.split(/\\r?\\n/)
289
+ let start = -1
290
+ for (let index = 0; index < lines.length; index += 1) {
291
+ if (/^##\\s+context\\s+links\\b/i.test(lines[index].trim())) {
292
+ start = index + 1
293
+ break
294
+ }
295
+ }
296
+ if (start < 0) {
297
+ return []
298
+ }
299
+
300
+ const links = []
301
+ for (let index = start; index < lines.length; index += 1) {
302
+ const line = lines[index].trim()
303
+ if (!line) {
304
+ continue
305
+ }
306
+ if (/^#{1,6}\\s+/.test(line)) {
307
+ break
308
+ }
309
+ const match = line.match(/\\[\\[([^\\]]+)\\]\\]/)
310
+ if (!match) {
311
+ continue
312
+ }
313
+ const title = match[1].trim()
314
+ if (!title) {
315
+ continue
316
+ }
317
+ const priorityMatch = line.match(/#(critical|important)\\b|priority:\\s*(high|critical)/i)
318
+ const priority = priorityMatch ? String(priorityMatch[1] || priorityMatch[2] || 'normal').toLowerCase() : 'normal'
319
+ links.push({ title, priority })
320
+ }
321
+ return links
322
+ }
323
+
324
+ const buildFacts = (node, outgoingCount, incomingCount) => {
325
+ const content = typeof node?.content === 'string' ? node.content : ''
326
+ const words = content.trim().length > 0 ? content.trim().split(/\\s+/).length : 0
327
+ return [
328
+ { label: 'Agent', value: typeof node?.agentId === 'string' && node.agentId ? node.agentId : 'shared' },
329
+ { label: 'Words', value: String(words) },
330
+ { label: 'Chars', value: String(content.length) },
331
+ { label: 'Outgoing', value: String(outgoingCount) },
332
+ { label: 'Backlinks', value: String(incomingCount) }
333
+ ]
334
+ }
335
+
336
+ const listFacts = (facts) => facts
337
+ .map((fact) => '<li><strong>' + escapeHtml(fact.label) + ':</strong> <small>' + escapeHtml(fact.value) + '</small></li>')
338
+ .join('')
339
+
340
+ const listContextLinks = (links) => {
341
+ if (!Array.isArray(links) || links.length === 0) {
342
+ return '<li><small>No context links found.</small></li>'
343
+ }
344
+ return links
345
+ .map((link) => '<li><span>' + escapeHtml(link.title) + '</span><small>' + escapeHtml(link.priority || 'normal') + '</small></li>')
346
+ .join('')
347
+ }
348
+
282
349
  const linkedNodes = (node) => {
283
350
  const nodeById = new Map((state.chunk.nodes || []).map((item) => [item[0], item]))
284
351
  const edges = normalizeList(state.chunk.edges)
@@ -342,6 +409,10 @@ const loadNodeDetails = async (nodeId) => {
342
409
  : '<span>No tags</span>'
343
410
 
344
411
  const related = linkedNodes(node)
412
+ const contextLinks = extractContextLinks(node.content)
413
+ const facts = buildFacts(node, related.outgoing.length, related.incoming.length)
414
+ elements.contentFacts.innerHTML = listFacts(facts)
415
+ elements.contentContextLinks.innerHTML = listContextLinks(contextLinks)
345
416
  elements.contentOutgoing.innerHTML = list(related.outgoing)
346
417
  elements.contentIncoming.innerHTML = list(related.incoming)
347
418
  elements.contentBody.textContent = typeof node.content === 'string' ? node.content : ''
@@ -25,10 +25,14 @@ const nodeIndexById = new Map()
25
25
  const highlightedIds = new Set()
26
26
  let selectedNodeId = null
27
27
  let dirty = true
28
- let running = false
28
+ let renderScheduled = false
29
29
  let hoverX = null
30
30
  let hoverY = null
31
31
  let lastFrameAt = 0
32
+ let lastVisibleEdges = 0
33
+ let edgePositionsBuffer = new Float32Array(0)
34
+ let pointPositionsBuffer = new Float32Array(0)
35
+ let pointSizesBuffer = new Float32Array(0)
32
36
 
33
37
  const defaultTheme = {
34
38
  node: [0.68, 0.72, 0.78, 1],
@@ -133,6 +137,14 @@ const initWebGl = () => {
133
137
  return true
134
138
  }
135
139
 
140
+ const ensureFloat32Capacity = (buffer, neededLength) => {
141
+ if (buffer.length >= neededLength) {
142
+ return buffer
143
+ }
144
+ const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
145
+ return new Float32Array(next)
146
+ }
147
+
136
148
  const resizeCanvas = (width, height, ratio) => {
137
149
  viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
138
150
  viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
@@ -146,6 +158,7 @@ const resizeCanvas = (width, height, ratio) => {
146
158
  gl.viewport(0, 0, canvas.width, canvas.height)
147
159
  }
148
160
  dirty = true
161
+ requestRender()
149
162
  }
150
163
 
151
164
  const toScreenPoint = (x, y) => {
@@ -262,7 +275,9 @@ const cullVisibleNodes = () => {
262
275
  const drawEdges = () => {
263
276
  if (!gl || state.edgeCount === 0) return
264
277
 
265
- const positions = []
278
+ edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
279
+ let cursor = 0
280
+ let visibleEdges = 0
266
281
  for (let index = 0; index < state.edgeCount; index += 1) {
267
282
  const source = state.edgeSource[index]
268
283
  const target = state.edgeTarget[index]
@@ -271,50 +286,61 @@ const drawEdges = () => {
271
286
  }
272
287
  const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
273
288
  const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
274
- positions.push(sx, sy, tx, ty)
289
+ edgePositionsBuffer[cursor] = sx
290
+ edgePositionsBuffer[cursor + 1] = sy
291
+ edgePositionsBuffer[cursor + 2] = tx
292
+ edgePositionsBuffer[cursor + 3] = ty
293
+ cursor += 4
294
+ visibleEdges += 1
275
295
  }
276
296
 
277
- if (positions.length === 0) return
297
+ lastVisibleEdges = visibleEdges
298
+ if (cursor === 0) return
278
299
 
279
300
  gl.useProgram(lineProgram)
280
301
  gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
281
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
302
+ gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
282
303
  gl.enableVertexAttribArray(linePositionLocation)
283
304
  gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
284
305
  gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
285
306
  gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
286
307
  gl.lineWidth(1)
287
- gl.drawArrays(gl.LINES, 0, positions.length / 2)
308
+ gl.drawArrays(gl.LINES, 0, cursor / 2)
288
309
  }
289
310
 
290
311
  const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
291
312
  if (!gl || state.nodeCount === 0) return
292
313
 
293
- const positions = []
294
- const sizes = []
314
+ pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
315
+ pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
316
+ let positionCursor = 0
317
+ let sizeCursor = 0
295
318
  for (let index = 0; index < state.nodeCount; index += 1) {
296
319
  if (!predicate(index)) continue
297
320
  const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
298
- positions.push(sx, sy)
299
- sizes.push(Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost))
321
+ pointPositionsBuffer[positionCursor] = sx
322
+ pointPositionsBuffer[positionCursor + 1] = sy
323
+ pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
324
+ positionCursor += 2
325
+ sizeCursor += 1
300
326
  }
301
327
 
302
- if (positions.length === 0) return
328
+ if (positionCursor === 0) return
303
329
 
304
330
  gl.useProgram(pointProgram)
305
331
  gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
306
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
332
+ gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
307
333
  gl.enableVertexAttribArray(pointPositionLocation)
308
334
  gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
309
335
 
310
336
  gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
311
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sizes), gl.STREAM_DRAW)
337
+ gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
312
338
  gl.enableVertexAttribArray(pointSizeLocation)
313
339
  gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
314
340
 
315
341
  gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
316
342
  gl.uniform4fv(pointColorLocation, color)
317
- gl.drawArrays(gl.POINTS, 0, positions.length / 2)
343
+ gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
318
344
  }
319
345
 
320
346
  const clear = () => {
@@ -325,16 +351,20 @@ const clear = () => {
325
351
  }
326
352
 
327
353
  const renderFrame = (now) => {
354
+ renderScheduled = false
355
+ if (!dirty) {
356
+ return
357
+ }
358
+
328
359
  const delta = now - lastFrameAt
329
360
  const minInterval = state.nodeCount > 20000 ? 22 : 16
330
- if (delta < minInterval && running) {
331
- scheduleNextFrame()
361
+ if (delta < minInterval) {
362
+ requestRender()
332
363
  return
333
364
  }
334
365
  lastFrameAt = now
335
366
 
336
367
  if (!gl) {
337
- scheduleNextFrame()
338
368
  return
339
369
  }
340
370
 
@@ -376,15 +406,17 @@ const renderFrame = (now) => {
376
406
  }
377
407
  return count
378
408
  })(),
379
- visibleEdges: state.edgeCount
409
+ visibleEdges: lastVisibleEdges
380
410
  })
381
411
  dirty = false
382
412
  }
383
-
384
- scheduleNextFrame()
385
413
  }
386
414
 
387
- const scheduleNextFrame = () => {
415
+ const requestRender = () => {
416
+ if (renderScheduled) {
417
+ return
418
+ }
419
+ renderScheduled = true
388
420
  const raf = self.requestAnimationFrame
389
421
  if (typeof raf === 'function') {
390
422
  raf.call(self, renderFrame)
@@ -401,6 +433,7 @@ const setCamera = (nextCamera) => {
401
433
  camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
402
434
  camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
403
435
  dirty = true
436
+ requestRender()
404
437
  }
405
438
 
406
439
  const worldAtScreen = (screenX, screenY) => {
@@ -453,6 +486,7 @@ const setHighlights = (ids) => {
453
486
  state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
454
487
  }
455
488
  dirty = true
489
+ requestRender()
456
490
  }
457
491
 
458
492
  const setSelected = (id) => {
@@ -461,6 +495,7 @@ const setSelected = (id) => {
461
495
  state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
462
496
  }
463
497
  dirty = true
498
+ requestRender()
464
499
  }
465
500
 
466
501
  self.onmessage = (event) => {
@@ -481,8 +516,7 @@ self.onmessage = (event) => {
481
516
  }
482
517
  resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
483
518
  setCamera(payload.camera)
484
- running = true
485
- scheduleNextFrame()
519
+ requestRender()
486
520
  postMessage({ type: 'ready' })
487
521
  return
488
522
  }
@@ -499,6 +533,7 @@ self.onmessage = (event) => {
499
533
 
500
534
  if (payload.type === 'chunk') {
501
535
  loadChunk(payload.chunk)
536
+ requestRender()
502
537
  return
503
538
  }
504
539
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.143",
3
+ "version": "0.1.0-beta.144",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",