@andespindola/brainlink 0.1.0-beta.159 → 0.1.0-beta.160

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.
@@ -116,6 +116,11 @@ select {
116
116
  inset: 0;
117
117
  pointer-events: none;
118
118
  overflow: hidden;
119
+ transition: opacity 120ms ease;
120
+ }
121
+
122
+ .graph-labels.is-stale {
123
+ opacity: 0;
119
124
  }
120
125
 
121
126
  .graph-label {
@@ -338,12 +343,14 @@ li small {
338
343
 
339
344
  .content-dialog {
340
345
  position: fixed;
341
- top: 74px;
342
- right: 16px;
346
+ top: max(12px, env(safe-area-inset-top));
347
+ right: max(12px, env(safe-area-inset-right));
343
348
  margin: 0;
344
349
  width: min(760px, calc(100vw - 32px));
345
- height: min(calc(100svh - 96px), 920px);
346
- max-height: calc(100svh - 96px);
350
+ height: min(calc(100vh - 24px), 920px);
351
+ height: min(calc(100dvh - 24px), 920px);
352
+ max-height: calc(100vh - 24px);
353
+ max-height: calc(100dvh - 24px);
347
354
  padding: 0;
348
355
  border: 1px solid var(--line);
349
356
  border-radius: 8px;
@@ -363,6 +370,7 @@ li small {
363
370
  grid-template-rows: auto auto minmax(0, 1fr);
364
371
  height: 100%;
365
372
  max-height: 100%;
373
+ min-height: 0;
366
374
  }
367
375
 
368
376
  .content-dialog header {
@@ -372,6 +380,7 @@ li small {
372
380
  gap: 18px;
373
381
  padding: 22px;
374
382
  border-bottom: 1px solid var(--line);
383
+ background: var(--panel);
375
384
  }
376
385
 
377
386
  .content-dialog h2,
@@ -392,23 +401,42 @@ li small {
392
401
  overflow-wrap: anywhere;
393
402
  }
394
403
 
395
- .content-dialog button {
404
+ #contentClose {
396
405
  flex: 0 0 auto;
406
+ width: 38px;
397
407
  height: 38px;
398
- padding: 0 14px;
408
+ padding: 0;
399
409
  border: 1px solid var(--line);
400
410
  border-radius: 8px;
401
411
  background: var(--panel-strong);
402
412
  color: var(--text);
403
413
  cursor: pointer;
414
+ font-size: 22px;
415
+ line-height: 1;
404
416
  }
405
417
 
406
- .content-dialog button:hover,
407
- .content-dialog button:focus {
418
+ #contentClose:hover,
419
+ #contentClose:focus {
408
420
  border-color: var(--accent);
409
421
  color: var(--accent);
410
422
  }
411
423
 
424
+ .content-dialog li button {
425
+ width: 100%;
426
+ height: auto;
427
+ padding: 0;
428
+ border: 0;
429
+ border-radius: 0;
430
+ background: transparent;
431
+ color: var(--text);
432
+ text-align: left;
433
+ }
434
+
435
+ .content-dialog li button:hover,
436
+ .content-dialog li button:focus {
437
+ color: var(--accent);
438
+ }
439
+
412
440
  .content-meta {
413
441
  display: grid;
414
442
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -497,19 +525,27 @@ li small {
497
525
  order: 4;
498
526
  }
499
527
 
528
+ .content-dialog {
529
+ inset: 8px;
530
+ inset: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
531
+ width: auto;
532
+ height: auto;
533
+ max-width: none;
534
+ max-height: none;
535
+ }
536
+
500
537
  .content-dialog header {
501
- align-items: stretch;
502
- flex-direction: column;
538
+ align-items: flex-start;
539
+ gap: 12px;
540
+ padding: 14px;
503
541
  }
504
542
 
505
- .content-dialog {
506
- top: auto;
507
- right: 12px;
508
- left: 12px;
509
- bottom: 28px;
510
- width: auto;
511
- height: min(calc(100svh - 150px), 760px);
512
- max-height: calc(100svh - 150px);
543
+ .content-dialog h2 {
544
+ font-size: 16px;
545
+ }
546
+
547
+ .content-dialog p {
548
+ font-size: 12px;
513
549
  }
514
550
 
515
551
  .metric-chip {
@@ -518,5 +554,16 @@ li small {
518
554
 
519
555
  .content-meta {
520
556
  grid-template-columns: 1fr;
557
+ max-height: 34dvh;
558
+ overflow: auto;
559
+ padding: 10px 14px;
560
+ }
561
+
562
+ .content-meta-section:last-child {
563
+ grid-column: auto;
564
+ }
565
+
566
+ .content-dialog .note-content {
567
+ padding: 12px;
521
568
  }
522
569
  }`;
@@ -66,7 +66,7 @@ export const createClientHtml = () => `<!doctype html>
66
66
  <h2 id="contentTitle">Selected note</h2>
67
67
  <p id="contentPath"></p>
68
68
  </div>
69
- <button id="contentClose" type="button">Close</button>
69
+ <button id="contentClose" type="button" aria-label="Close node details" title="Close node details">&times;</button>
70
70
  </header>
71
71
  <div class="content-meta">
72
72
  <section class="content-meta-section">
@@ -75,6 +75,7 @@ const state = {
75
75
  miniMapView: null,
76
76
  miniMapDirty: true,
77
77
  overlayScheduled: false,
78
+ overlayIdleTimer: null,
78
79
  chunk: {
79
80
  nodes: [],
80
81
  edges: []
@@ -86,6 +87,7 @@ const state = {
86
87
  fetchToken: 0,
87
88
  fetchTimer: null,
88
89
  fetchAbortController: null,
90
+ lastChunkRequestKey: '',
89
91
  cameraSyncScheduled: false,
90
92
  lastWheelAt: 0,
91
93
  lastVisibleNodes: 0,
@@ -396,6 +398,30 @@ const getZoomEdgeBudget = () => {
396
398
  return 26000
397
399
  }
398
400
 
401
+ const zoomDetailBand = () => {
402
+ const scale = state.camera.scale
403
+ if (scale < 0.06) return 'far'
404
+ if (scale < 0.12) return 'wide'
405
+ if (scale < 0.24) return 'mid'
406
+ if (scale < 0.7) return 'near'
407
+ return 'detail'
408
+ }
409
+
410
+ const graphStreamRequestKey = ({ x, y, w, h }) => {
411
+ const grid = Math.max(80, Math.min(720, Math.max(w, h) / 6))
412
+ return [
413
+ state.agentId || '*',
414
+ state.contextId || '*',
415
+ zoomDetailBand(),
416
+ getZoomNodeBudget(),
417
+ getZoomEdgeBudget(),
418
+ Math.round(x / grid),
419
+ Math.round(y / grid),
420
+ Math.round(w / grid),
421
+ Math.round(h / grid)
422
+ ].join(':')
423
+ }
424
+
399
425
  const screenToWorld = (screenX, screenY) => ({
400
426
  x: (screenX - state.camera.x) / state.camera.scale,
401
427
  y: (screenY - state.camera.y) / state.camera.scale
@@ -727,6 +753,8 @@ const drawMiniMap = () => {
727
753
  ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
728
754
  }
729
755
 
756
+ const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
757
+
730
758
  const updateGraphOverlays = () => {
731
759
  if (state.overlayScheduled) {
732
760
  return
@@ -734,6 +762,17 @@ const updateGraphOverlays = () => {
734
762
  state.overlayScheduled = true
735
763
  requestAnimationFrame(() => {
736
764
  state.overlayScheduled = false
765
+ if (shouldDeferGraphOverlays()) {
766
+ elements.labels?.classList.add('is-stale')
767
+ if (!state.overlayIdleTimer) {
768
+ state.overlayIdleTimer = setTimeout(() => {
769
+ state.overlayIdleTimer = null
770
+ updateGraphOverlays()
771
+ }, 170)
772
+ }
773
+ return
774
+ }
775
+ elements.labels?.classList.remove('is-stale')
737
776
  drawLabels()
738
777
  if (state.miniMapDirty) {
739
778
  drawMiniMap()
@@ -947,6 +986,11 @@ const fetchChunk = async ({ fit } = { fit: false }) => {
947
986
  params.set('context', state.contextId)
948
987
  }
949
988
 
989
+ const requestKey = graphStreamRequestKey({ x, y, w, h })
990
+ if (!fit && state.lastChunkRequestKey === requestKey && state.chunk.nodes.length > 0) {
991
+ return
992
+ }
993
+
950
994
  const response = await fetch('/api/graph-stream?' + params.toString(), { signal: controller.signal })
951
995
  if (!response.ok) {
952
996
  throw new Error('Failed to fetch graph stream chunk')
@@ -961,6 +1005,7 @@ const fetchChunk = async ({ fit } = { fit: false }) => {
961
1005
  }
962
1006
 
963
1007
  state.graphSignature = typeof chunk.signature === 'string' ? chunk.signature : ''
1008
+ state.lastChunkRequestKey = requestKey
964
1009
  ensureNodePositionsLoaded()
965
1010
  await syncNodePositionsFromServer()
966
1011
  state.graphMode = typeof chunk.mode === 'string' ? chunk.mode : 'near'
@@ -1000,8 +1045,9 @@ const scheduleChunkFetch = ({ fit } = { fit: false }) => {
1000
1045
  }
1001
1046
 
1002
1047
  const now = performance.now()
1003
- const recentlyWheeling = now - state.lastWheelAt < 180
1004
- const delay = fit ? 0 : (state.pointer.down ? 260 : (recentlyWheeling ? 160 : 48))
1048
+ const recentlyWheeling = now - state.lastWheelAt < 320
1049
+ const heavyScene = state.lastVisibleNodes > 1200 || state.lastVisibleEdges > 3500
1050
+ const delay = fit ? 0 : (state.pointer.down ? 320 : (recentlyWheeling ? (heavyScene ? 420 : 300) : (heavyScene ? 120 : 72)))
1005
1051
  state.fetchTimer = setTimeout(() => {
1006
1052
  state.fetchTimer = null
1007
1053
  fetchChunk({ fit }).catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.159",
3
+ "version": "0.1.0-beta.160",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",