memorix 0.6.5 → 0.7.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.
@@ -57,6 +57,10 @@ const i18n = {
57
57
  concepts: 'Concepts',
58
58
  files: 'Files Modified',
59
59
  clickToExpand: 'Click to expand',
60
+ vectorSearch: 'Vector Search',
61
+ fulltextOnly: 'Fulltext Only',
62
+ enabled: 'Enabled',
63
+ typeDistribution: 'Type Distribution',
60
64
 
61
65
  // Retention
62
66
  memoryRetention: 'Memory Retention',
@@ -132,6 +136,10 @@ const i18n = {
132
136
  concepts: '概念',
133
137
  files: '相关文件',
134
138
  clickToExpand: '点击展开',
139
+ vectorSearch: '向量搜索',
140
+ fulltextOnly: '仅全文搜索',
141
+ enabled: '已启用',
142
+ typeDistribution: '类型分布',
135
143
 
136
144
  // Retention
137
145
  memoryRetention: '记忆衰减',
@@ -398,6 +406,11 @@ async function loadDashboard() {
398
406
  <div class="stat-label">${t('nextId')}</div>
399
407
  <div class="stat-value">#${stats.nextId}</div>
400
408
  </div>
409
+ <div class="stat-card" data-accent="${stats.embedding?.enabled ? 'cyan' : 'amber'}">
410
+ <div class="stat-label">${t('vectorSearch')}</div>
411
+ <div class="stat-value" style="font-size: 18px;">${stats.embedding?.enabled ? '✓ ' + t('enabled') : t('fulltextOnly')}</div>
412
+ ${stats.embedding?.provider ? `<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px; font-family: var(--font-mono);">${stats.embedding.provider} (${stats.embedding.dimensions}d)</div>` : ''}
413
+ </div>
401
414
  </div>
402
415
 
403
416
  <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
@@ -406,16 +419,23 @@ async function loadDashboard() {
406
419
  <span class="panel-title">${t('observationTypes')}</span>
407
420
  </div>
408
421
  <div class="panel-body">
409
- ${typeEntries.length > 0 ? typeEntries.map(([type, count]) => `
410
- <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
411
- <span style="width: 20px; text-align: center;">${typeIcons[type] || '❓'}</span>
412
- <span style="width: 120px; font-size: 12px; color: var(--text-secondary);">${type}</span>
413
- <div style="flex: 1; height: 6px; background: rgba(255,255,255,0.04); border-radius: 3px; overflow: hidden;">
414
- <div style="width: ${(count / maxTypeCount) * 100}%; height: 100%; background: var(--type-${type}, var(--accent-cyan)); border-radius: 3px;"></div>
422
+ ${typeEntries.length > 0 ? `
423
+ <div style="display: flex; gap: 20px; align-items: flex-start;">
424
+ <canvas id="type-pie-chart" width="140" height="140" style="flex-shrink: 0;"></canvas>
425
+ <div style="flex: 1;">
426
+ ${typeEntries.map(([type, count]) => `
427
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
428
+ <span style="width: 18px; text-align: center; font-size: 13px;">${typeIcons[type] || '❓'}</span>
429
+ <span style="width: 110px; font-size: 11px; color: var(--text-secondary);">${type}</span>
430
+ <div style="flex: 1; height: 5px; background: rgba(128,128,128,0.1); border-radius: 3px; overflow: hidden;">
431
+ <div style="width: ${(count / maxTypeCount) * 100}%; height: 100%; background: var(--type-${type}, var(--accent-cyan)); border-radius: 3px;"></div>
432
+ </div>
433
+ <span style="font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); min-width: 22px; text-align: right;">${count}</span>
434
+ </div>
435
+ `).join('')}
415
436
  </div>
416
- <span style="font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); min-width: 24px; text-align: right;">${count}</span>
417
437
  </div>
418
- `).join('') : `<p style="color: var(--text-muted); font-size: 13px;">${t('noObservationsYet')}</p>`}
438
+ ` : `<p style="color: var(--text-muted); font-size: 13px;">${t('noObservationsYet')}</p>`}
419
439
  </div>
420
440
  </div>
421
441
 
@@ -442,6 +462,55 @@ async function loadDashboard() {
442
462
  </div>
443
463
  </div>
444
464
  `;
465
+
466
+ // Render pie chart if data exists
467
+ if (typeEntries.length > 0) {
468
+ requestAnimationFrame(() => renderPieChart('type-pie-chart', typeEntries, typeIcons));
469
+ }
470
+ }
471
+
472
+ /** Draw a mini donut chart on a canvas */
473
+ function renderPieChart(canvasId, entries, icons) {
474
+ const canvas = document.getElementById(canvasId);
475
+ if (!canvas) return;
476
+ const ctx = canvas.getContext('2d');
477
+ const dpr = window.devicePixelRatio || 1;
478
+ const size = 140;
479
+ canvas.width = size * dpr;
480
+ canvas.height = size * dpr;
481
+ canvas.style.width = size + 'px';
482
+ canvas.style.height = size + 'px';
483
+ ctx.scale(dpr, dpr);
484
+
485
+ const cx = size / 2, cy = size / 2, r = 54, inner = 34;
486
+ const total = entries.reduce((s, e) => s + e[1], 0);
487
+ const colors = [
488
+ '#06b6d4', '#a855f7', '#f59e0b', '#22c55e',
489
+ '#3b82f6', '#ef4444', '#ec4899', '#f97316', '#6366f1',
490
+ ];
491
+
492
+ let angle = -Math.PI / 2;
493
+ entries.forEach(([type, count], i) => {
494
+ const slice = (count / total) * Math.PI * 2;
495
+ ctx.beginPath();
496
+ ctx.moveTo(cx + inner * Math.cos(angle), cy + inner * Math.sin(angle));
497
+ ctx.arc(cx, cy, r, angle, angle + slice);
498
+ ctx.arc(cx, cy, inner, angle + slice, angle, true);
499
+ ctx.closePath();
500
+ ctx.fillStyle = colors[i % colors.length];
501
+ ctx.fill();
502
+ angle += slice;
503
+ });
504
+
505
+ // Center text
506
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim() || '#fff';
507
+ ctx.font = 'bold 20px system-ui';
508
+ ctx.textAlign = 'center';
509
+ ctx.textBaseline = 'middle';
510
+ ctx.fillText(total, cx, cy - 6);
511
+ ctx.font = '10px system-ui';
512
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim() || '#888';
513
+ ctx.fillText('total', cx, cy + 10);
445
514
  }
446
515
 
447
516
  // ============================================================
@@ -720,13 +789,13 @@ function renderGraph(graph) {
720
789
  legend.className = 'graph-legend';
721
790
  legend.style.cssText = `
722
791
  position: absolute; top: 12px; right: 12px; z-index: 10;
723
- background: var(--glass-bg, rgba(15,15,30,0.85));
792
+ background: var(--bg-card);
724
793
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
725
- border: 1px solid var(--glass-border, rgba(255,255,255,0.08));
794
+ border: 1px solid var(--border-medium);
726
795
  border-radius: 12px; padding: 12px 14px; min-width: 140px;
727
796
  font-family: 'Inter', sans-serif; font-size: 11px;
728
- color: var(--text-secondary, #94a3b8);
729
- box-shadow: 0 4px 24px rgba(0,0,0,0.3);
797
+ color: var(--text-secondary);
798
+ box-shadow: 0 4px 24px rgba(0,0,0,0.12);
730
799
  transition: opacity 0.3s;
731
800
  `;
732
801
 
@@ -736,7 +805,7 @@ function renderGraph(graph) {
736
805
 
737
806
  // Title
738
807
  const title = document.createElement('div');
739
- title.style.cssText = 'font-weight: 600; font-size: 11px; margin-bottom: 8px; color: var(--text-primary, #e2e8f0); letter-spacing: 0.5px; text-transform: uppercase;';
808
+ title.style.cssText = 'font-weight: 600; font-size: 11px; margin-bottom: 8px; color: var(--text-primary); letter-spacing: 0.5px; text-transform: uppercase;';
740
809
  title.textContent = t('legend') || 'Legend';
741
810
  legend.appendChild(title);
742
811
 
@@ -764,7 +833,7 @@ function renderGraph(graph) {
764
833
 
765
834
  // Hover to highlight same-type nodes
766
835
  row.addEventListener('mouseenter', () => {
767
- row.style.background = 'rgba(255,255,255,0.06)';
836
+ row.style.background = 'var(--bg-card-hover)';
768
837
  nodes.forEach(n => { n._dimmed = n.type !== type; });
769
838
  draw();
770
839
  });
@@ -779,7 +848,7 @@ function renderGraph(graph) {
779
848
 
780
849
  // Stats footer
781
850
  const stats = document.createElement('div');
782
- stats.style.cssText = 'margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 10px; opacity: 0.5;';
851
+ stats.style.cssText = 'margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-subtle); font-size: 10px; opacity: 0.5;';
783
852
  stats.textContent = `${nodes.length} nodes · ${edges.length} edges`;
784
853
  legend.appendChild(stats);
785
854
 
@@ -1067,6 +1136,7 @@ function renderObsList() {
1067
1136
  list.innerHTML = filtered.map(obs => {
1068
1137
  const isLow = isLowQualityObs(obs.title || '');
1069
1138
  const isSelected = selectedIds.has(obs.id);
1139
+ const hl = (text) => obsFilter ? escapeHtml(text).replace(new RegExp(`(${escapeHtml(obsFilter).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>') : escapeHtml(text);
1070
1140
  return `
1071
1141
  <div class="obs-card${isLow ? ' low-quality' : ''}" data-obs-id="${obs.id}">
1072
1142
  <div class="obs-card-header" onclick="toggleObsDetail(${obs.id})">
@@ -1076,19 +1146,19 @@ function renderObsList() {
1076
1146
  ${typeIcons[obs.type] || '❓'} ${obs.type || 'unknown'}
1077
1147
  </span>
1078
1148
  ${isLow ? '<span class="low-quality-badge">low quality</span>' : ''}
1079
- <span class="obs-card-title">${escapeHtml(obs.title || t('untitled'))}</span>
1149
+ <span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
1080
1150
  <span class="obs-expand-icon">▼</span>
1081
1151
  </div>
1082
1152
  <div class="obs-card-meta">
1083
- <span>📁 ${escapeHtml(obs.entityName || 'unknown')}</span>
1153
+ <span>📁 ${hl(obs.entityName || 'unknown')}</span>
1084
1154
  ${obs.createdAt ? `<span>🕐 ${formatTime(obs.createdAt)}</span>` : ''}
1085
1155
  ${obs.accessCount ? `<span>👁 ${obs.accessCount}</span>` : ''}
1086
1156
  </div>
1087
1157
  <div class="obs-detail" id="obs-detail-${obs.id}" style="display:none;">
1088
- ${obs.narrative ? `<div class="obs-detail-section"><label>${t('narrative')}</label><div class="obs-card-narrative">${escapeHtml(obs.narrative)}</div></div>` : ''}
1089
- ${obs.facts && obs.facts.length > 0 ? `<div class="obs-detail-section"><label>${t('facts')}</label><div class="obs-card-facts">${obs.facts.map(f => `<span class="fact-tag">${escapeHtml(f)}</span>`).join('')}</div></div>` : ''}
1090
- ${obs.concepts && obs.concepts.length > 0 ? `<div class="obs-detail-section"><label>${t('concepts')}</label><div class="obs-card-facts">${obs.concepts.map(c => `<span class="fact-tag concept-tag">${escapeHtml(c)}</span>`).join('')}</div></div>` : ''}
1091
- ${obs.filesModified && obs.filesModified.length > 0 ? `<div class="obs-detail-section"><label>${t('files')}</label><div class="obs-card-facts">${obs.filesModified.map(f => `<span class="fact-tag file-tag">${escapeHtml(f)}</span>`).join('')}</div></div>` : ''}
1158
+ ${obs.narrative ? `<div class="obs-detail-section"><label>${t('narrative')}</label><div class="obs-card-narrative">${hl(obs.narrative)}</div></div>` : ''}
1159
+ ${obs.facts && obs.facts.length > 0 ? `<div class="obs-detail-section"><label>${t('facts')}</label><div class="obs-card-facts">${obs.facts.map(f => `<span class="fact-tag">${hl(f)}</span>`).join('')}</div></div>` : ''}
1160
+ ${obs.concepts && obs.concepts.length > 0 ? `<div class="obs-detail-section"><label>${t('concepts')}</label><div class="obs-card-facts">${obs.concepts.map(c => `<span class="fact-tag concept-tag">${hl(c)}</span>`).join('')}</div></div>` : ''}
1161
+ ${obs.filesModified && obs.filesModified.length > 0 ? `<div class="obs-detail-section"><label>${t('files')}</label><div class="obs-card-facts">${obs.filesModified.map(f => `<span class="fact-tag file-tag">${hl(f)}</span>`).join('')}</div></div>` : ''}
1092
1162
  <div class="obs-detail-actions">
1093
1163
  <button class="delete-btn" onclick="deleteObs(${obs.id}, event)">
1094
1164
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M6 7v5M10 7v5M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
@@ -1332,4 +1332,17 @@ body {
1332
1332
  background: rgba(239, 68, 68, 0.1);
1333
1333
  border-color: var(--accent-red);
1334
1334
  opacity: 1;
1335
+ }
1336
+
1337
+ /* Search highlight */
1338
+ mark {
1339
+ background: rgba(245, 158, 11, 0.25);
1340
+ color: inherit;
1341
+ padding: 1px 2px;
1342
+ border-radius: 2px;
1343
+ font-weight: 600;
1344
+ }
1345
+
1346
+ [data-theme="light"] mark {
1347
+ background: rgba(245, 158, 11, 0.35);
1335
1348
  }