@aman_asmuei/amem 0.22.0 → 0.23.0

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/dist/dashboard.js CHANGED
@@ -74,11 +74,39 @@ a{color:var(--decision);text-decoration:none}
74
74
  .mem-card .mem-meta .tag{background:var(--card);border:1px solid var(--border);border-radius:4px;padding:1px 6px}
75
75
 
76
76
  /* knowledge graph */
77
- #graph-svg{width:100%;height:460px;border-radius:6px;background:var(--bg);cursor:grab}
77
+ .graph-container{position:relative;overflow:hidden;border-radius:6px;background:var(--bg);height:600px}
78
+ #graph-svg{width:100%;height:100%;cursor:grab}
78
79
  #graph-svg:active{cursor:grabbing}
79
- #graph-svg circle{cursor:pointer;transition:r 0.15s}
80
- #graph-svg circle:hover{r:12}
80
+ #graph-svg .node-circle{cursor:pointer;transition:opacity 0.3s,r 0.15s}
81
+ #graph-svg .node-circle:hover{filter:brightness(1.3)}
82
+ #graph-svg .edge-line{transition:opacity 0.3s}
83
+ #graph-svg .node-label{transition:opacity 0.3s;pointer-events:none}
84
+ #graph-svg .edge-label{transition:opacity 0.3s;pointer-events:none}
85
+ .graph-dimmed{opacity:0.12!important}
81
86
  .graph-tooltip{position:absolute;background:var(--card);border:1px solid var(--border);border-radius:6px;padding:10px 14px;font-size:0.8rem;max-width:300px;pointer-events:none;z-index:10;box-shadow:0 4px 12px rgba(0,0,0,0.4)}
87
+ .graph-controls{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;align-items:center}
88
+ .graph-controls input,.graph-controls select{background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:6px 10px;font-size:0.8rem;outline:none}
89
+ .graph-controls input:focus,.graph-controls select:focus{border-color:var(--decision)}
90
+ .graph-controls input{flex:1;min-width:140px}
91
+ .graph-controls button{background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:6px 12px;font-size:0.8rem;cursor:pointer}
92
+ .graph-controls button:hover{border-color:var(--decision);color:var(--decision)}
93
+ .graph-controls button.active{background:var(--decision);color:#fff;border-color:var(--decision)}
94
+ .graph-legend{display:flex;gap:14px;flex-wrap:wrap;padding:8px 0}
95
+ .graph-legend-item{display:flex;align-items:center;gap:5px;font-size:0.75rem;color:var(--muted);cursor:pointer}
96
+ .graph-legend-item:hover{color:var(--text)}
97
+ .graph-legend-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
98
+ .graph-stats{display:flex;gap:14px;font-size:0.75rem;color:var(--muted);padding:4px 0}
99
+ .graph-stats b{color:var(--text)}
100
+ .graph-detail{position:absolute;top:10px;right:10px;width:280px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;z-index:20;box-shadow:0 8px 24px rgba(0,0,0,0.5);max-height:calc(100% - 20px);overflow-y:auto;display:none}
101
+ .graph-detail h3{font-size:0.9rem;margin-bottom:8px;display:flex;align-items:center;gap:8px}
102
+ .graph-detail .close{margin-left:auto;cursor:pointer;color:var(--muted);font-size:1rem;line-height:1}
103
+ .graph-detail .close:hover{color:var(--text)}
104
+ .graph-detail .detail-content{font-size:0.85rem;line-height:1.6;margin-bottom:10px;word-break:break-word}
105
+ .graph-detail .detail-meta{font-size:0.75rem;color:var(--muted);display:flex;flex-direction:column;gap:4px}
106
+ .graph-detail .detail-relations{margin-top:10px;border-top:1px solid var(--border);padding-top:10px}
107
+ .graph-detail .relation-item{display:flex;align-items:center;gap:6px;font-size:0.8rem;padding:4px 0;cursor:pointer;color:var(--text)}
108
+ .graph-detail .relation-item:hover{color:var(--decision)}
109
+ .graph-detail .relation-arrow{color:var(--muted);font-size:0.7rem}
82
110
 
83
111
  /* memory actions */
84
112
  .mem-actions{display:flex;gap:6px;margin-top:6px}
@@ -193,10 +221,29 @@ a{color:var(--decision);text-decoration:none}
193
221
  </div>
194
222
 
195
223
  <!-- Knowledge graph -->
196
- <div class="card" id="graph-card">
224
+ <div class="card full" id="graph-card">
197
225
  <h2>Knowledge Graph</h2>
198
- <div id="graph-tooltip" class="graph-tooltip" style="display:none"></div>
199
- <svg id="graph-svg"></svg>
226
+ <div class="graph-controls">
227
+ <input type="text" id="graph-search" placeholder="Search nodes..."/>
228
+ <select id="graph-type-filter">
229
+ <option value="">All types</option>
230
+ <option value="correction">correction</option>
231
+ <option value="decision">decision</option>
232
+ <option value="pattern">pattern</option>
233
+ <option value="preference">preference</option>
234
+ <option value="topology">topology</option>
235
+ <option value="fact">fact</option>
236
+ </select>
237
+ <button id="graph-reset" title="Reset view">Reset</button>
238
+ <button id="graph-fit" title="Fit all nodes">Fit All</button>
239
+ </div>
240
+ <div class="graph-stats" id="graph-stats"></div>
241
+ <div class="graph-legend" id="graph-legend"></div>
242
+ <div class="graph-container" id="graph-container">
243
+ <div id="graph-tooltip" class="graph-tooltip" style="display:none"></div>
244
+ <svg id="graph-svg"></svg>
245
+ <div class="graph-detail" id="graph-detail"></div>
246
+ </div>
200
247
  </div>
201
248
 
202
249
  <!-- Reminders -->
@@ -342,80 +389,412 @@ a{color:var(--decision);text-decoration:none}
342
389
  renderMemories(list);
343
390
  }
344
391
 
345
- // -- Knowledge Graph (simple force-directed) --
346
- function renderGraph(data){
347
- var svg=$('graph-svg');
348
- var W=svg.clientWidth||600, H=svg.clientHeight||360;
349
- var nodes=data.nodes, edges=data.edges;
350
- if(!nodes.length){setHTML(svg,'<text x="'+W/2+'" y="'+H/2+'" fill="#8b949e" text-anchor="middle" font-size="14">No graph data</text>');return}
392
+ // -- Knowledge Graph (enhanced force-directed with zoom/pan/focus) --
393
+ var graphNodes=[], graphEdges=[], graphNodeMap={}, graphAllNodes=[], graphAllEdges=[];
394
+ var graphZoom={x:0,y:0,scale:1};
395
+ var graphFocusId=null;
396
+ var graphDragNode=null, graphDragBg=false, graphDragStart={x:0,y:0}, graphPanStart={x:0,y:0};
397
+
398
+ function computeNodeDegrees(){
399
+ for(var i=0;i<graphAllNodes.length;i++) graphAllNodes[i].degree=0;
400
+ for(var i=0;i<graphAllEdges.length;i++){
401
+ var a=graphNodeMap[graphAllEdges[i].from];
402
+ var b=graphNodeMap[graphAllEdges[i].to];
403
+ if(a) a.degree=(a.degree||0)+1;
404
+ if(b) b.degree=(b.degree||0)+1;
405
+ }
406
+ }
407
+
408
+ function nodeRadius(n){return Math.max(6,Math.min(20,6+(n.degree||0)*2))}
409
+
410
+ function getNeighborIds(nodeId){
411
+ var ids={};ids[nodeId]=true;
412
+ for(var i=0;i<graphAllEdges.length;i++){
413
+ if(graphAllEdges[i].from===nodeId) ids[graphAllEdges[i].to]=true;
414
+ if(graphAllEdges[i].to===nodeId) ids[graphAllEdges[i].from]=true;
415
+ }
416
+ return ids;
417
+ }
418
+
419
+ function getNodeEdges(nodeId){
420
+ var result=[];
421
+ for(var i=0;i<graphAllEdges.length;i++){
422
+ if(graphAllEdges[i].from===nodeId||graphAllEdges[i].to===nodeId) result.push(graphAllEdges[i]);
423
+ }
424
+ return result;
425
+ }
351
426
 
352
- // assign random positions
427
+ function forceLayout(nodes,edges,W,H){
353
428
  var i,j;
429
+ var nodeMap={};
354
430
  for(i=0;i<nodes.length;i++){
355
- nodes[i].x=W*0.2+Math.random()*W*0.6;
356
- nodes[i].y=H*0.2+Math.random()*H*0.6;
431
+ // Use stable seeded positions based on node id hash
432
+ var h=0;for(var c=0;c<nodes[i].id.length;c++){h=((h<<5)-h)+nodes[i].id.charCodeAt(c);h|=0}
433
+ nodes[i].x=W*0.15+Math.abs(h%1000)/1000*W*0.7;
434
+ nodes[i].y=H*0.15+Math.abs((h*31)%1000)/1000*H*0.7;
357
435
  nodes[i].vx=0;nodes[i].vy=0;
436
+ nodeMap[nodes[i].id]=nodes[i];
358
437
  }
359
- var nodeMap={};
360
- for(i=0;i<nodes.length;i++) nodeMap[nodes[i].id]=nodes[i];
361
438
 
362
- // force simulation
363
- var REPULSION=3000, SPRING=0.06, DAMPING=0.85, DT=1;
364
- for(var iter=0;iter<80;iter++){
365
- // repulsion between all pairs
439
+ var REPULSION=5000,SPRING=0.04,DAMPING=0.82,DT=1;
440
+ var ITERS=nodes.length>200?60:nodes.length>50?100:150;
441
+
442
+ for(var iter=0;iter<ITERS;iter++){
443
+ // Barnes-Hut approximation for large graphs: skip distant pairs
366
444
  for(i=0;i<nodes.length;i++){
367
445
  for(j=i+1;j<nodes.length;j++){
368
- var dx=nodes[j].x-nodes[i].x, dy=nodes[j].y-nodes[i].y;
446
+ var dx=nodes[j].x-nodes[i].x,dy=nodes[j].y-nodes[i].y;
369
447
  var dist2=dx*dx+dy*dy+1;
448
+ if(dist2>400000) continue; // skip very distant pairs
370
449
  var f=REPULSION/dist2;
371
450
  var d=Math.sqrt(dist2);
372
- var fx=f*dx/d, fy=f*dy/d;
451
+ var fx=f*dx/d,fy=f*dy/d;
373
452
  nodes[i].vx-=fx;nodes[i].vy-=fy;
374
453
  nodes[j].vx+=fx;nodes[j].vy+=fy;
375
454
  }
376
455
  }
377
- // attraction along edges
378
456
  for(i=0;i<edges.length;i++){
379
457
  var a=nodeMap[edges[i].from],b=nodeMap[edges[i].to];
380
458
  if(!a||!b) continue;
381
459
  var edx=b.x-a.x,edy=b.y-a.y;
382
- var efx=SPRING*edx,efy=SPRING*edy;
460
+ var strength=edges[i].strength||0.8;
461
+ var efx=SPRING*strength*edx,efy=SPRING*strength*edy;
383
462
  a.vx+=efx;a.vy+=efy;b.vx-=efx;b.vy-=efy;
384
463
  }
385
- // center gravity
386
- for(i=0;i<nodes.length;i++){
387
- nodes[i].vx+=(W/2-nodes[i].x)*0.001;
388
- nodes[i].vy+=(H/2-nodes[i].y)*0.001;
389
- }
390
- // integrate
391
464
  for(i=0;i<nodes.length;i++){
465
+ nodes[i].vx+=(W/2-nodes[i].x)*0.002;
466
+ nodes[i].vy+=(H/2-nodes[i].y)*0.002;
392
467
  nodes[i].vx*=DAMPING;nodes[i].vy*=DAMPING;
393
468
  nodes[i].x+=nodes[i].vx*DT;nodes[i].y+=nodes[i].vy*DT;
394
- nodes[i].x=Math.max(30,Math.min(W-30,nodes[i].x));
395
- nodes[i].y=Math.max(30,Math.min(H-30,nodes[i].y));
469
+ nodes[i].x=Math.max(40,Math.min(W-40,nodes[i].x));
470
+ nodes[i].y=Math.max(40,Math.min(H-40,nodes[i].y));
396
471
  }
397
472
  }
473
+ return nodeMap;
474
+ }
398
475
 
399
- // Store for interactive use
400
- graphNodes=nodes;
401
- graphEdges=edges;
402
- graphNodeMap=nodeMap;
476
+ function renderGraphSvg(){
477
+ var svg=$('graph-svg');
478
+ if(!svg) return;
479
+ var focusNeighbors=graphFocusId?getNeighborIds(graphFocusId):null;
403
480
 
404
- // render SVG elements (all text escaped via esc())
405
- var html='';
406
- for(i=0;i<edges.length;i++){
407
- var ea=nodeMap[edges[i].from],eb=nodeMap[edges[i].to];
481
+ var defs='<defs><marker id="arrow" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6z" fill="#58a6ff" fill-opacity="0.6"/></marker></defs>';
482
+ var html=defs+'<g id="graph-pan" transform="translate('+graphZoom.x+','+graphZoom.y+') scale('+graphZoom.scale+')">';
483
+
484
+ // Edges
485
+ for(var i=0;i<graphEdges.length;i++){
486
+ var e=graphEdges[i];
487
+ var ea=graphNodeMap[e.from],eb=graphNodeMap[e.to];
408
488
  if(!ea||!eb) continue;
409
- var mx=(ea.x+eb.x)/2,my=(ea.y+eb.y)/2;
410
- html+='<line x1="'+ea.x+'" y1="'+ea.y+'" x2="'+eb.x+'" y2="'+eb.y+'" stroke="#58a6ff" stroke-opacity="0.5" stroke-width="'+(1.5+edges[i].strength*2)+'"/>';
411
- if(edges[i].type) html+='<text x="'+mx+'" y="'+(my-4)+'" fill="#c9d1d9" font-size="9" text-anchor="middle" font-weight="600">'+esc(edges[i].type)+'</text>';
489
+ var dim=focusNeighbors&&(!focusNeighbors[e.from]||!focusNeighbors[e.to]);
490
+ var cls='edge-line'+(dim?' graph-dimmed':'');
491
+ var sw=1.5+(e.strength||0.8)*2;
492
+ var opacity=dim?0.1:0.5;
493
+ // Shorten line to avoid overlapping node circles
494
+ var edx=eb.x-ea.x,edy=eb.y-ea.y;
495
+ var elen=Math.sqrt(edx*edx+edy*edy)||1;
496
+ var rA=nodeRadius(ea),rB=nodeRadius(eb);
497
+ var x1=ea.x+edx/elen*(rA+2),y1=ea.y+edy/elen*(rA+2);
498
+ var x2=eb.x-edx/elen*(rB+2),y2=eb.y-edy/elen*(rB+2);
499
+ html+='<line class="'+cls+'" x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#58a6ff" stroke-opacity="'+opacity+'" stroke-width="'+sw+'" marker-end="url(#arrow)"/>';
500
+ if(e.type){
501
+ var mx=(ea.x+eb.x)/2,my=(ea.y+eb.y)/2;
502
+ html+='<text class="edge-label'+(dim?' graph-dimmed':'')+'" x="'+mx+'" y="'+(my-6)+'" fill="#c9d1d9" font-size="9" text-anchor="middle" font-weight="600" opacity="'+(dim?0.1:0.8)+'">'+esc(e.type)+'</text>';
503
+ }
412
504
  }
413
- for(i=0;i<nodes.length;i++){
414
- var color=TYPE_COLORS[nodes[i].type]||'#8b949e';
415
- html+='<circle data-nid="'+esc(nodes[i].id)+'" cx="'+nodes[i].x+'" cy="'+nodes[i].y+'" r="8" fill="'+color+'" stroke="#0d1117" stroke-width="2"/>';
416
- html+='<text x="'+nodes[i].x+'" y="'+(nodes[i].y+20)+'" fill="'+color+'" font-size="10" text-anchor="middle" font-family="-apple-system,sans-serif">'+esc(nodes[i].label.slice(0,24))+'</text>';
505
+
506
+ // Nodes
507
+ for(var i=0;i<graphNodes.length;i++){
508
+ var n=graphNodes[i];
509
+ var color=TYPE_COLORS[n.type]||'#8b949e';
510
+ var r=nodeRadius(n);
511
+ var dim=focusNeighbors&&!focusNeighbors[n.id];
512
+ var cls='node-circle'+(dim?' graph-dimmed':'');
513
+ var focused=graphFocusId===n.id;
514
+ var sw=focused?3:2;
515
+ var stroke=focused?'#fff':'#0d1117';
516
+ html+='<circle class="'+cls+'" data-nid="'+esc(n.id)+'" cx="'+n.x+'" cy="'+n.y+'" r="'+r+'" fill="'+color+'" stroke="'+stroke+'" stroke-width="'+sw+'"/>';
517
+ var labelOpacity=dim?0.1:(r>8?1:0.7);
518
+ html+='<text class="node-label'+(dim?' graph-dimmed':'')+'" x="'+n.x+'" y="'+(n.y+r+14)+'" fill="'+color+'" font-size="'+(r>12?'11':'10')+'" text-anchor="middle" font-family="-apple-system,sans-serif" opacity="'+labelOpacity+'">'+esc(n.label.slice(0,28))+'</text>';
417
519
  }
418
- setHTML(svg, html);
520
+
521
+ html+='</g>';
522
+ setHTML(svg,html);
523
+ }
524
+
525
+ function renderGraphLegend(){
526
+ var el=$('graph-legend');
527
+ if(!el) return;
528
+ var types=['correction','decision','pattern','preference','topology','fact'];
529
+ setHTML(el, types.map(function(t){
530
+ return '<div class="graph-legend-item" data-type="'+t+'"><div class="graph-legend-dot" style="background:'+(TYPE_COLORS[t]||'#8b949e')+'"></div>'+t+'</div>';
531
+ }).join(''));
532
+ }
533
+
534
+ function renderGraphStats(){
535
+ var el=$('graph-stats');
536
+ if(!el) return;
537
+ var connected=0;
538
+ var connectedSet={};
539
+ for(var i=0;i<graphAllEdges.length;i++){
540
+ connectedSet[graphAllEdges[i].from]=true;
541
+ connectedSet[graphAllEdges[i].to]=true;
542
+ }
543
+ connected=Object.keys(connectedSet).length;
544
+ setHTML(el,'<b>'+graphNodes.length+'</b> nodes · <b>'+graphEdges.length+'</b> edges · <b>'+connected+'</b> connected');
545
+ }
546
+
547
+ function showNodeDetail(nodeId){
548
+ var panel=$('graph-detail');
549
+ if(!panel) return;
550
+ var n=graphNodeMap[nodeId];
551
+ if(!n){panel.style.display='none';return}
552
+
553
+ var color=TYPE_COLORS[n.type]||'#8b949e';
554
+ var edges=getNodeEdges(nodeId);
555
+ var html='<h3><span class="type-badge" style="background:'+color+'">'+esc(n.type)+'</span><code class="mono" style="font-size:0.7rem;color:var(--muted)">'+esc(n.id.slice(0,8))+'</code><span class="close" id="detail-close">&times;</span></h3>';
556
+ html+='<div class="detail-content">'+esc(n.fullContent||n.label)+'</div>';
557
+ html+='<div class="detail-meta">';
558
+ html+='<span>Tier: '+(n.tier||'archival')+'</span>';
559
+ html+='<span>Connections: '+(n.degree||0)+'</span>';
560
+ html+='</div>';
561
+
562
+ if(edges.length){
563
+ html+='<div class="detail-relations"><div style="font-size:0.75rem;color:var(--muted);margin-bottom:6px;font-weight:600">RELATIONS</div>';
564
+ for(var i=0;i<edges.length;i++){
565
+ var e=edges[i];
566
+ var otherId=e.from===nodeId?e.to:e.from;
567
+ var other=graphNodeMap[otherId];
568
+ var otherLabel=other?(other.label.slice(0,30)):otherId.slice(0,8);
569
+ var dir=e.from===nodeId?'→':'←';
570
+ html+='<div class="relation-item" data-nid="'+esc(otherId)+'">';
571
+ html+='<span class="relation-arrow">'+dir+'</span>';
572
+ html+='<span style="color:var(--muted);font-size:0.7rem">'+esc(e.type||'related')+'</span> ';
573
+ html+=esc(otherLabel);
574
+ html+='</div>';
575
+ }
576
+ html+='</div>';
577
+ }
578
+
579
+ setHTML(panel,html);
580
+ panel.style.display='block';
581
+
582
+ // Close button
583
+ var closeBtn=$('detail-close');
584
+ if(closeBtn) closeBtn.addEventListener('click',function(){
585
+ panel.style.display='none';
586
+ graphFocusId=null;
587
+ renderGraphSvg();
588
+ });
589
+
590
+ // Click relation to navigate
591
+ var items=panel.querySelectorAll('.relation-item[data-nid]');
592
+ for(var i=0;i<items.length;i++){
593
+ items[i].addEventListener('click',function(){
594
+ var nid=this.dataset.nid;
595
+ if(nid){
596
+ graphFocusId=nid;
597
+ renderGraphSvg();
598
+ showNodeDetail(nid);
599
+ }
600
+ });
601
+ }
602
+ }
603
+
604
+ function filterGraphNodes(){
605
+ var search=($('graph-search')||{}).value||'';
606
+ var typeFilter=($('graph-type-filter')||{}).value||'';
607
+ search=search.toLowerCase();
608
+
609
+ if(!search&&!typeFilter){
610
+ graphNodes=graphAllNodes;
611
+ graphEdges=graphAllEdges;
612
+ } else {
613
+ var visibleIds={};
614
+ graphNodes=graphAllNodes.filter(function(n){
615
+ var matchSearch=!search||n.label.toLowerCase().indexOf(search)!==-1||(n.fullContent||'').toLowerCase().indexOf(search)!==-1;
616
+ var matchType=!typeFilter||n.type===typeFilter;
617
+ if(matchSearch&&matchType){visibleIds[n.id]=true;return true}
618
+ return false;
619
+ });
620
+ graphEdges=graphAllEdges.filter(function(e){return visibleIds[e.from]&&visibleIds[e.to]});
621
+ }
622
+ renderGraphSvg();
623
+ renderGraphStats();
624
+ }
625
+
626
+ function renderGraph(data){
627
+ var svg=$('graph-svg');
628
+ var container=$('graph-container');
629
+ if(!svg||!container) return;
630
+ var W=container.clientWidth||900,H=container.clientHeight||600;
631
+ svg.setAttribute('viewBox','0 0 '+W+' '+H);
632
+
633
+ graphAllNodes=data.nodes;graphAllEdges=data.edges;
634
+ graphNodes=data.nodes;graphEdges=data.edges;
635
+ graphZoom={x:0,y:0,scale:1};
636
+ graphFocusId=null;
637
+
638
+ if(!data.nodes.length){
639
+ setHTML(svg,'<text x="'+W/2+'" y="'+H/2+'" fill="#8b949e" text-anchor="middle" font-size="14">No graph data — use memory_relate to connect memories</text>');
640
+ renderGraphStats();
641
+ renderGraphLegend();
642
+ return;
643
+ }
644
+
645
+ graphNodeMap=forceLayout(data.nodes,data.edges,W,H);
646
+ computeNodeDegrees();
647
+ renderGraphSvg();
648
+ renderGraphStats();
649
+ renderGraphLegend();
650
+ }
651
+
652
+ function setupGraphInteraction(){
653
+ var svg=$('graph-svg');
654
+ var container=$('graph-container');
655
+ var tooltip=$('graph-tooltip');
656
+ if(!svg||!container) return;
657
+
658
+ // Zoom with mouse wheel
659
+ container.addEventListener('wheel',function(e){
660
+ e.preventDefault();
661
+ var rect=container.getBoundingClientRect();
662
+ var mx=e.clientX-rect.left;
663
+ var my=e.clientY-rect.top;
664
+ var delta=e.deltaY>0?0.9:1.1;
665
+ var newScale=Math.max(0.2,Math.min(5,graphZoom.scale*delta));
666
+ // Zoom toward mouse position
667
+ graphZoom.x=mx-(mx-graphZoom.x)*newScale/graphZoom.scale;
668
+ graphZoom.y=my-(my-graphZoom.y)*newScale/graphZoom.scale;
669
+ graphZoom.scale=newScale;
670
+ renderGraphSvg();
671
+ },{passive:false});
672
+
673
+ // Pan with mouse drag on background
674
+ svg.addEventListener('mousedown',function(e){
675
+ if(e.target.tagName==='circle'){
676
+ // Node drag
677
+ var nid=e.target.dataset.nid;
678
+ if(nid) graphDragNode=graphNodeMap[nid];
679
+ if(graphDragNode){
680
+ var rect=container.getBoundingClientRect();
681
+ graphDragStart.x=(e.clientX-rect.left-graphZoom.x)/graphZoom.scale-graphDragNode.x;
682
+ graphDragStart.y=(e.clientY-rect.top-graphZoom.y)/graphZoom.scale-graphDragNode.y;
683
+ }
684
+ return;
685
+ }
686
+ // Background pan
687
+ graphDragBg=true;
688
+ graphPanStart.x=e.clientX-graphZoom.x;
689
+ graphPanStart.y=e.clientY-graphZoom.y;
690
+ svg.style.cursor='grabbing';
691
+ });
692
+
693
+ svg.addEventListener('mousemove',function(e){
694
+ if(graphDragNode){
695
+ var rect=container.getBoundingClientRect();
696
+ graphDragNode.x=(e.clientX-rect.left-graphZoom.x)/graphZoom.scale-graphDragStart.x;
697
+ graphDragNode.y=(e.clientY-rect.top-graphZoom.y)/graphZoom.scale-graphDragStart.y;
698
+ renderGraphSvg();
699
+ } else if(graphDragBg){
700
+ graphZoom.x=e.clientX-graphPanStart.x;
701
+ graphZoom.y=e.clientY-graphPanStart.y;
702
+ renderGraphSvg();
703
+ }
704
+ });
705
+
706
+ document.addEventListener('mouseup',function(){
707
+ graphDragNode=null;
708
+ graphDragBg=false;
709
+ svg.style.cursor='grab';
710
+ });
711
+
712
+ // Click node to focus
713
+ svg.addEventListener('click',function(e){
714
+ var circle=e.target;
715
+ if(circle.tagName!=='circle') {
716
+ // Click background to unfocus
717
+ if(graphFocusId){graphFocusId=null;renderGraphSvg();$('graph-detail').style.display='none'}
718
+ if(tooltip) tooltip.style.display='none';
719
+ return;
720
+ }
721
+ var nid=circle.dataset.nid;
722
+ if(!nid) return;
723
+
724
+ graphFocusId=graphFocusId===nid?null:nid;
725
+ renderGraphSvg();
726
+ if(graphFocusId) showNodeDetail(nid);
727
+ else $('graph-detail').style.display='none';
728
+ });
729
+
730
+ // Hover tooltip
731
+ svg.addEventListener('mouseover',function(e){
732
+ if(e.target.tagName!=='circle'||!tooltip) return;
733
+ var nid=e.target.dataset.nid;
734
+ var n=graphNodeMap[nid];
735
+ if(!n) return;
736
+ var rect=container.getBoundingClientRect();
737
+ tooltip.style.display='block';
738
+ tooltip.style.left=(e.clientX-rect.left+15)+'px';
739
+ tooltip.style.top=(e.clientY-rect.top-10)+'px';
740
+ var color=TYPE_COLORS[n.type]||'#8b949e';
741
+ setHTML(tooltip,
742
+ '<div style="margin-bottom:4px"><span class="type-badge" style="background:'+color+'">'+esc(n.type)+'</span> <code class="mono">'+esc(n.id.slice(0,8))+'</code></div>'+
743
+ '<div style="font-size:0.85rem;margin-bottom:4px">'+esc((n.fullContent||n.label).slice(0,120))+'</div>'+
744
+ '<div style="font-size:0.75rem;color:var(--muted)">Connections: '+(n.degree||0)+' · Tier: '+(n.tier||'archival')+'</div>'
745
+ );
746
+ });
747
+ svg.addEventListener('mouseout',function(e){
748
+ if(e.target.tagName==='circle'&&tooltip) tooltip.style.display='none';
749
+ });
750
+
751
+ // Graph search/filter
752
+ var gsDebounce;
753
+ var gs=$('graph-search');
754
+ if(gs) gs.addEventListener('input',function(){clearTimeout(gsDebounce);gsDebounce=setTimeout(filterGraphNodes,300)});
755
+ var gf=$('graph-type-filter');
756
+ if(gf) gf.addEventListener('change',filterGraphNodes);
757
+
758
+ // Reset button
759
+ var resetBtn=$('graph-reset');
760
+ if(resetBtn) resetBtn.addEventListener('click',function(){
761
+ graphZoom={x:0,y:0,scale:1};
762
+ graphFocusId=null;
763
+ if(gs) gs.value='';
764
+ if(gf) gf.value='';
765
+ graphNodes=graphAllNodes;graphEdges=graphAllEdges;
766
+ $('graph-detail').style.display='none';
767
+ renderGraphSvg();renderGraphStats();
768
+ });
769
+
770
+ // Fit all button
771
+ var fitBtn=$('graph-fit');
772
+ if(fitBtn) fitBtn.addEventListener('click',function(){
773
+ if(!graphNodes.length) return;
774
+ var minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity;
775
+ for(var i=0;i<graphNodes.length;i++){
776
+ minX=Math.min(minX,graphNodes[i].x);maxX=Math.max(maxX,graphNodes[i].x);
777
+ minY=Math.min(minY,graphNodes[i].y);maxY=Math.max(maxY,graphNodes[i].y);
778
+ }
779
+ var container=$('graph-container');
780
+ var cw=container.clientWidth,ch=container.clientHeight;
781
+ var gw=maxX-minX+80,gh=maxY-minY+80;
782
+ var scale=Math.min(cw/gw,ch/gh,2);
783
+ graphZoom.scale=scale;
784
+ graphZoom.x=cw/2-((minX+maxX)/2)*scale;
785
+ graphZoom.y=ch/2-((minY+maxY)/2)*scale;
786
+ renderGraphSvg();
787
+ });
788
+
789
+ // Legend click to filter
790
+ var legend=$('graph-legend');
791
+ if(legend) legend.addEventListener('click',function(e){
792
+ var item=e.target.closest('.graph-legend-item');
793
+ if(!item) return;
794
+ var type=item.dataset.type;
795
+ var gf=$('graph-type-filter');
796
+ if(gf){gf.value=gf.value===type?'':type;filterGraphNodes()}
797
+ });
419
798
  }
420
799
 
421
800
  // -- Reminders --
@@ -459,22 +838,22 @@ a{color:var(--decision);text-decoration:none}
459
838
  renderStats(s);
460
839
  renderTypeBars(s.byType);
461
840
  renderConfBars(s.confidence);
462
- }).catch(function(){});
841
+ }).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
463
842
 
464
843
  fetchJSON('/api/memories?limit=200').then(function(m){
465
844
  allMemories=m;
466
845
  filterMemories();
467
846
  renderTimeline(allMemories);
468
- }).catch(function(){});
847
+ }).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
469
848
 
470
- fetchJSON('/api/graph').then(renderGraph).catch(function(){});
471
- fetchJSON('/api/reminders').then(renderReminders).catch(function(){});
472
- fetchJSON('/api/log?limit=30').then(renderLog).catch(function(){});
473
- fetchJSON('/api/summaries?limit=10').then(renderSummaries).catch(function(){});
849
+ fetchJSON('/api/graph').then(renderGraph).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
850
+ fetchJSON('/api/reminders').then(renderReminders).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
851
+ fetchJSON('/api/log?limit=30').then(renderLog).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
852
+ fetchJSON('/api/summaries?limit=10').then(renderSummaries).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
474
853
  fetchJSON('/api/copilot-preview').then(function(d){
475
854
  var el=$('copilot-preview');
476
855
  if(el) el.textContent=d.markdown||'No memories to export.';
477
- }).catch(function(){});
856
+ }).catch(function(e){ console.error('[amem] Dashboard fetch error:', e); });
478
857
  }
479
858
 
480
859
  window.copyCopilotPreview=function(){
@@ -597,80 +976,8 @@ a{color:var(--decision);text-decoration:none}
597
976
  });
598
977
  };
599
978
 
600
- // -- Interactive Graph (drag, click-to-inspect) --
601
- var dragNode=null, dragOffset={x:0,y:0}, graphNodes=[], graphNodeMap={}, graphEdges=[];
602
-
603
- function makeGraphInteractive(){
604
- var svg=$('graph-svg');
605
- var tooltip=$('graph-tooltip');
606
- if(!svg||!tooltip) return;
607
-
608
- svg.addEventListener('mousedown',function(e){
609
- var circle=e.target;
610
- if(circle.tagName!=='circle') return;
611
- var nid=circle.dataset.nid;
612
- if(!nid) return;
613
- dragNode=graphNodeMap[nid];
614
- if(!dragNode) return;
615
- var rect=svg.getBoundingClientRect();
616
- dragOffset.x=e.clientX-rect.left-dragNode.x;
617
- dragOffset.y=e.clientY-rect.top-dragNode.y;
618
- e.preventDefault();
619
- });
620
-
621
- svg.addEventListener('mousemove',function(e){
622
- if(!dragNode) return;
623
- var rect=svg.getBoundingClientRect();
624
- dragNode.x=e.clientX-rect.left-dragOffset.x;
625
- dragNode.y=e.clientY-rect.top-dragOffset.y;
626
- redrawGraph();
627
- });
628
-
629
- document.addEventListener('mouseup',function(){dragNode=null});
630
-
631
- svg.addEventListener('click',function(e){
632
- var circle=e.target;
633
- if(circle.tagName!=='circle') return;
634
- var nid=circle.dataset.nid;
635
- if(!nid) return;
636
- var n=graphNodeMap[nid];
637
- if(!n) return;
638
- var rect=svg.getBoundingClientRect();
639
- tooltip.style.display='block';
640
- tooltip.style.left=(e.clientX-rect.left+15)+'px';
641
- tooltip.style.top=(e.clientY-rect.top-10)+'px';
642
- setHTML(tooltip,
643
- '<div style="margin-bottom:4px"><span class="type-badge" style="background:'+(TYPE_COLORS[n.type]||'#8b949e')+'">'+esc(n.type)+'</span> <code class="mono">'+esc(n.id.slice(0,8))+'</code></div>'+
644
- '<div style="font-size:0.85rem;margin-bottom:6px">'+esc(n.fullContent||n.label)+'</div>'+
645
- '<div style="font-size:0.75rem;color:var(--muted)">Tier: '+(n.tier||'archival')+'</div>'
646
- );
647
- });
648
-
649
- document.addEventListener('click',function(e){
650
- if(e.target.tagName!=='circle'&&!tooltip.contains(e.target)) tooltip.style.display='none';
651
- });
652
- }
653
-
654
- function redrawGraph(){
655
- var svg=$('graph-svg');
656
- var html='';
657
- for(var i=0;i<graphEdges.length;i++){
658
- var ea=graphNodeMap[graphEdges[i].from],eb=graphNodeMap[graphEdges[i].to];
659
- if(!ea||!eb) continue;
660
- var mx=(ea.x+eb.x)/2,my=(ea.y+eb.y)/2;
661
- html+='<line x1="'+ea.x+'" y1="'+ea.y+'" x2="'+eb.x+'" y2="'+eb.y+'" stroke="#30363d" stroke-width="'+(1+graphEdges[i].strength*2)+'"/>';
662
- if(graphEdges[i].type) html+='<text x="'+mx+'" y="'+(my-4)+'" fill="#8b949e" font-size="9" text-anchor="middle">'+esc(graphEdges[i].type)+'</text>';
663
- }
664
- for(var i=0;i<graphNodes.length;i++){
665
- var color=TYPE_COLORS[graphNodes[i].type]||'#8b949e';
666
- html+='<circle data-nid="'+esc(graphNodes[i].id)+'" cx="'+graphNodes[i].x+'" cy="'+graphNodes[i].y+'" r="8" fill="'+color+'" stroke="#0d1117" stroke-width="2"/>';
667
- html+='<text x="'+graphNodes[i].x+'" y="'+(graphNodes[i].y+20)+'" fill="'+color+'" font-size="10" text-anchor="middle" font-family="-apple-system,sans-serif">'+esc(graphNodes[i].label.slice(0,24))+'</text>';
668
- }
669
- setHTML(svg,html);
670
- }
671
-
672
979
  // -- Init --
673
- makeGraphInteractive();
980
+ setupGraphInteraction();
674
981
  loadAll();
675
982
  setInterval(loadAll,30000);
676
983
  })();