Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aa9423b
fix(serializer): overwrite import now clears old data before re-popul…
kevintseng Apr 18, 2026
2c4c527
fix(serializer): apply namespace filter at query level, not post-filter
kevintseng Apr 18, 2026
7cbc6c0
feat(core): add neural embedding module (@xenova/transformers, 384-dim)
kevintseng Apr 18, 2026
33e631d
feat(core): integrate neural embeddings into remember and recall
kevintseng Apr 18, 2026
89b58c3
feat(dashboard): add Graph and Lessons tabs with i18n support
kevintseng Apr 18, 2026
a272465
feat: v3.2.0 — neural embeddings, data integrity fixes, dashboard Gra…
kevintseng Apr 18, 2026
bd7e0e4
Merge branch 'main' into develop
kevintseng Apr 18, 2026
1117d0d
feat(dashboard): implement Precision Engineer design system
kevintseng Apr 18, 2026
d3576df
feat(dashboard): add feedback widget with i18n + restore interaction …
kevintseng Apr 18, 2026
40120da
style(design): fix touch targets and search empty state
kevintseng Apr 18, 2026
cbbf42c
feat(api): add /v1/analytics endpoint with health score, timeline, va…
kevintseng Apr 18, 2026
50cfbc4
feat(dashboard): add AnalyticsData type for /v1/analytics endpoint
kevintseng Apr 18, 2026
faf8dae
feat(dashboard): add HealthScore component with factor bars
kevintseng Apr 18, 2026
91b3ddf
feat(dashboard): add ValueMetrics component with coverage bars
kevintseng Apr 18, 2026
e30ebce
feat(dashboard): add CleanupSuggestions component with archive actions
kevintseng Apr 18, 2026
45efb65
feat(dashboard): add MemoryTimeline canvas sparkline component
kevintseng Apr 18, 2026
9c4be09
feat(dashboard): rewrite AnalyticsTab with insights + add i18n for al…
kevintseng Apr 18, 2026
c37af66
test(analytics): add tests for health score, stale detection, timelin…
kevintseng Apr 18, 2026
12457c9
docs: add /v1/analytics endpoint to architecture and API reference
kevintseng Apr 18, 2026
170c136
fix(dashboard): align frontend types with actual /v1/analytics response
kevintseng Apr 18, 2026
157b933
fix(dashboard): use correct i18n key for lessons applied label
kevintseng Apr 18, 2026
05d3784
feat(dashboard): rewrite GraphTab with type filter, search, ego graph…
kevintseng Apr 18, 2026
1c29d06
fix(dashboard): fix GraphTab blank canvas race condition
kevintseng Apr 18, 2026
1f61bac
fix(dashboard): add cooling to force simulation — graph settles after…
kevintseng Apr 18, 2026
3de124c
fix: address code review findings — staleEntities SQL + type safety
kevintseng Apr 18, 2026
0c02013
fix: eliminate all `as any` type casts in dashboard
kevintseng Apr 18, 2026
aac394b
fix: address Codex review findings — datetime, hit-testing, coverage
kevintseng Apr 18, 2026
4e31323
chore: add PRE_RELEASE_CHECKLIST.md to .gitignore
kevintseng Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ docs/guides/

# === AI tool caches ===
.codex/
.gstack/

# === Local project docs (not shipped) ===
PRE_RELEASE_CHECKLIST.md
172 changes: 172 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Design System — MeMesh

## Product Context
- **What this is:** Universal AI memory layer. SQLite-based knowledge graph with FTS5 + neural embeddings.
- **Who it's for:** Developers using AI coding assistants (Claude Code, etc.)
- **Space/industry:** AI development tools, knowledge management
- **Project type:** Developer dashboard (7-tab data tool) + marketing showcase

## Aesthetic Direction
- **Direction:** Precision Engineer
- **Decoration level:** Minimal — no grain, no glow, no blur. Clean borders and color do all the work.
- **Mood:** Professional, trustworthy, sharp. The tool that makes you feel like an expert. Every pixel has a purpose.
- **Reference sites:** Linear (gold standard for dev tools), Warp (terminal aesthetic), OpenMemory (competitor)

## Typography
- **Display/Hero:** Satoshi 700 — geometric but warm, sharper than Inter, better character at small sizes. letter-spacing: -0.03em
- **Body:** Satoshi 400/500 — same family throughout, weight differentiates hierarchy
- **UI/Labels:** Satoshi 600 — uppercase, letter-spacing: 0.04em for section labels; 0.08em for smallest labels
- **Data/Tables:** Geist Mono 400/500 — Vercel-made, tabular-nums native, pairs with Satoshi's geometry
- **Code:** Geist Mono 400
- **Loading:** Google Fonts CDN (`family=Satoshi:wght@400;500;600;700` + `family=Geist+Mono:wght@400;500;600`)
- **Scale:**
- Hero: 32px / 700 / -0.03em
- Title: 24px / 700 / -0.02em
- Heading: 18px / 600 / -0.01em
- Subheading: 15px / 600
- Body: 14px / 400 / line-height 1.55
- Small: 13px / 400
- Caption: 12px / 500
- Micro: 11px / 400
- Label: 10px / 600 / uppercase / 0.08em

## Color

### Dark Mode (default)
- **Approach:** Restrained — one accent + neutrals. Color is rare and meaningful.

| Token | Value | Usage |
|-------|-------|-------|
| `--bg-0` | `#080A0C` | Page background |
| `--bg-1` | `#0D1014` | Header, nav, modals |
| `--bg-2` | `#14181D` | Cards, inputs, stat blocks |
| `--bg-card` | `rgba(20,24,29,0.9)` | Memory cards (slight transparency) |
| `--bg-hover` | `#1A1F26` | Hover states |
| `--bg-input` | `#0D1014` | Input fields |
| `--border` | `rgba(0,214,180,0.08)` | Default borders |
| `--border-hover` | `rgba(0,214,180,0.20)` | Active/hover borders |
| `--border-focus` | `#00D6B4` | Focus ring |
| `--border-subtle` | `rgba(0,214,180,0.04)` | Table row separators |
| `--text-0` | `#F0F2F4` | Primary text, headings |
| `--text-1` | `#B8BEC6` | Body text |
| `--text-2` | `#7A828E` | Secondary, metadata |
| `--text-3` | `#4A5260` | Muted, placeholders, labels |
| `--accent` | `#00D6B4` | Primary accent (cyan/teal) |
| `--accent-soft` | `rgba(0,214,180,0.08)` | Accent backgrounds |
| `--accent-hover` | `#00F0CA` | Accent hover state |
| `--accent-dim` | `#009E86` | Accent on light backgrounds |
| `--success` | `#00D6B4` | Same as accent |
| `--success-soft` | `rgba(0,214,180,0.08)` | Success backgrounds |
| `--danger` | `#FF6B6B` | Errors, destructive actions |
| `--danger-soft` | `rgba(255,107,107,0.08)` | Danger backgrounds |
| `--warning` | `#FFB84D` | Warnings, stale indicators |
| `--warning-soft` | `rgba(255,184,77,0.08)` | Warning backgrounds |
| `--info` | `#60A5FA` | Informational |
| `--info-soft` | `rgba(96,165,250,0.08)` | Info backgrounds |

### Light Mode
- **Strategy:** Reduce accent saturation, darken for contrast. Backgrounds flip to warm whites.

| Token | Value |
|-------|-------|
| `--bg-0` | `#F8F9FA` |
| `--bg-1` | `#FFFFFF` |
| `--bg-2` | `#F0F1F3` |
| `--bg-hover` | `#E8EAED` |
| `--text-0` | `#111317` |
| `--text-1` | `#3D4450` |
| `--text-2` | `#6B7280` |
| `--text-3` | `#9CA3AF` |
| `--accent` | `#009E86` |
| `--danger` | `#DC2626` |
| `--warning` | `#D97706` |
| `--info` | `#2563EB` |

## Spacing
- **Base unit:** 4px
- **Density:** Compact — developers don't like wasted space
- **Scale:**
- `--sp-1`: 2px
- `--sp-2`: 4px
- `--sp-3`: 8px
- `--sp-4`: 12px
- `--sp-5`: 16px
- `--sp-6`: 20px
- `--sp-7`: 24px
- `--sp-8`: 32px
- `--sp-9`: 48px

## Layout
- **Approach:** Grid-disciplined — strict columns, predictable alignment
- **Max content width:** 1100px
- **Page padding:** 24px
- **Grid:** 4-column for stats, auto-fit for responsive
- **Border radius:**
- `--radius-xs`: 4px (tags, small elements)
- `--radius-sm`: 6px (inputs, buttons)
- `--radius`: 8px (cards, panels)
- `--radius-full`: 9999px (badges, pills)

## Motion
- **Approach:** Minimal-functional — only transitions, no animations except loading spinner
- **Easing:** `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out for all interactions)
- **Duration:** 150ms universal. Loading spinner: 600ms linear.
- **What transitions:** border-color, background, color, opacity, box-shadow
- **What does NOT animate:** layout, position, size (no entrance animations, no scroll effects)

## Component Patterns

### Buttons
- **Primary:** solid accent bg, dark text. Hover: lighten.
- **Secondary:** bg-2, border, text-1. Hover: border darkens, bg shifts.
- **Ghost:** transparent, text-2. Hover: bg-hover.
- **Danger:** transparent, danger text, danger border at 20%. Hover: danger-soft bg.
- **Sizes:** sm (5px 10px, 11px), default (9px 16px, 12px), lg (12px 24px, 14px)

### Inputs
- Focus: accent border, no box-shadow glow (clean, not flashy)
- Error: danger border
- Disabled: 40% opacity
- Select: custom chevron SVG, no native appearance

### Cards (Memory entities)
- bg-card with 1px border. Hover: border-hover.
- Head: entity name (600, text-0) + type badge (mono, accent on accent-soft)
- Body: observation text (13px, text-1)
- Footer: tags (mono, 10px) + score (mono, accent, right-aligned)

### Table
- Sticky header: bg-2, uppercase 10px labels
- Row hover: bg-hover
- Mono columns for numeric data (tabular-nums)
- Last row: no bottom border

### Badges
- Pill shape (radius-full), 11px font, 500 weight
- Color variants: accent, success, danger, warning, info, neutral

### Tags
- Small pills (radius-xs), 10px mono, bg-2 with subtle border
- For entity categorization, not status

## Font Blacklist
Never use as primary: Inter, Roboto, Arial, Helvetica, Open Sans, Lato, Montserrat, Poppins

## Anti-patterns
- No purple/violet gradients
- No backdrop-filter blur (except modals overlay if needed)
- No grain or noise textures
- No glow effects on hover
- No entrance animations
- No centered stat cards (left-align values)
- No decorative elements that don't serve function

## Decisions Log
| Date | Decision | Rationale |
|------|----------|-----------|
| 2026-04-18 | Initial design system: Precision Engineer | Competitive research showed all AI tools converge on zinc+blue+Inter. Cyan accent + Satoshi differentiates while staying professional. |
| 2026-04-18 | Chose Satoshi over Inter | Inter is the most overused font in developer tools. Satoshi has similar geometric bones but sharper, better weight distribution at small sizes. |
| 2026-04-18 | Chose Geist Mono over JetBrains Mono | Better tabular-nums support, pairs with Satoshi's geometry. Vercel ecosystem alignment. |
| 2026-04-18 | Cyan #00D6B4 over blue #3b82f6 | 90% of dark dashboards use blue-500. Cyan is more distinctive, evokes graph/data visualization, better contrast on dark backgrounds. |
| 2026-04-18 | 150ms universal transition | Fast enough to feel instant, slow enough to be perceived. No per-component timing variations. |
| 2026-04-18 | Compact 4px spacing | Target users are developers who prefer data density over whitespace. |
4 changes: 3 additions & 1 deletion dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<title>MeMesh LLM Memory — Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,600,700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
Expand Down
8 changes: 2 additions & 6 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AnalyticsTab } from './components/AnalyticsTab';
import { SettingsTab } from './components/SettingsTab';
import { GraphTab } from './components/GraphTab';
import { LessonsTab } from './components/LessonsTab';
import { FeedbackWidget } from './components/FeedbackWidget';
import { api, type HealthData } from './lib/api';
import { initLocale, t } from './lib/i18n';

Expand Down Expand Up @@ -52,12 +53,7 @@ export function App() {
<div class={`panel ${tab === 'Manage' ? 'active' : ''}`}>{tab === 'Manage' && <BrowseTab manage />}</div>
<div class={`panel ${tab === 'Settings' ? 'active' : ''}`}>{tab === 'Settings' && <SettingsTab />}</div>
</div>
<button class="fb-btn" onClick={() => {
const url = 'https://github.com/PCIRCLE-AI/memesh-llm-memory/issues/new?title=' + encodeURIComponent('[Feedback] ') + '&labels=feedback,from-dashboard';
window.open(url, '_blank');
}}>
{t('feedback.button')}
</button>
<FeedbackWidget health={health} />
</div>
);
}
112 changes: 43 additions & 69 deletions dashboard/src/components/AnalyticsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,78 @@
import { useState, useEffect } from 'preact/hooks';
import { api, type StatsData, type Entity } from '../lib/api';
import { useState, useEffect, useCallback } from 'preact/hooks';
import { api, type StatsData, type AnalyticsData } from '../lib/api';
import { HealthScore } from './HealthScore';
import { MemoryTimeline } from './MemoryTimeline';
import { ValueMetrics } from './ValueMetrics';
import { CleanupSuggestions } from './CleanupSuggestions';
import { t } from '../lib/i18n';

export function AnalyticsTab() {
const [stats, setStats] = useState<StatsData | null>(null);
const [entities, setEntities] = useState<Entity[]>([]);
const [analytics, setAnalytics] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const loadData = useCallback(() => {
setLoading(true);
Promise.all([
api<StatsData>('GET', '/v1/stats'),
api<Entity[]>('GET', '/v1/entities?limit=500&status=all'),
]).then(([s, e]) => {
api<AnalyticsData>('GET', '/v1/analytics'),
]).then(([s, a]) => {
setStats(s);
setEntities(e || []);
setAnalytics(a);
}).finally(() => setLoading(false));
}, []);

if (loading) return <div class="empty"><div class="loading" /></div>;
if (!stats) return <div class="error-box">{t('common.error')}: Failed to load analytics</div>;

const now = Date.now();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const monthMs = 30 * 24 * 60 * 60 * 1000;
useEffect(() => { loadData(); }, [loadData]);

const thisWeek = entities.filter((e) => now - new Date(e.created_at).getTime() < weekMs).length;
const stale = entities.filter((e) => {
if (e.archived || e.status === 'archived') return false;
if (!e.last_accessed_at) return true;
return now - new Date(e.last_accessed_at).getTime() > monthMs;
}).length;
const archivedCount = entities.filter((e) => e.archived || e.status === 'archived').length;
const topRecalled = entities
.filter((e) => (e.access_count || 0) > 0)
.sort((a, b) => (b.access_count || 0) - (a.access_count || 0))
.slice(0, 5);
if (loading) return <div class="empty"><div class="loading" /></div>;
if (!stats || !analytics) return <div class="error-box">{t('common.error')}: Failed to load analytics</div>;

return (
<div>
{/* Stats row */}
{/* Row 1: Stats overview */}
<div class="stats-row">
<div class="stat"><div class="stat-val">{stats.totalEntities.toLocaleString()}</div><div class="stat-lbl">{t('analytics.totalMemories')}</div></div>
<div class="stat"><div class="stat-val">{stats.totalObservations.toLocaleString()}</div><div class="stat-lbl">{t('analytics.knowledgeFacts')}</div></div>
<div class="stat"><div class="stat-val">{stats.totalRelations.toLocaleString()}</div><div class="stat-lbl">{t('analytics.connections')}</div></div>
<div class="stat"><div class="stat-val">{stats.totalTags.toLocaleString()}</div><div class="stat-lbl">{t('analytics.topics')}</div></div>
</div>

{/* Insights */}
<div class="card">
<div class="card-title">{t('analytics.insights')}</div>
<div class="insight">
<span class="insight-icon">📝</span>
<span class="insight-text">{t('analytics.thisWeek')}</span>
<span class="insight-val">{thisWeek} {t('analytics.new')}</span>
</div>
<div class="insight">
<span class="insight-icon">💤</span>
<span class="insight-text">{t('analytics.stale')}</span>
<span class="insight-val">{stale}</span>
</div>
<div class="insight">
<span class="insight-icon">📦</span>
<span class="insight-text">{t('analytics.archivedLabel')}</span>
<span class="insight-val">{archivedCount}</span>
</div>
{/* Row 2: Health Score */}
<HealthScore score={analytics.healthScore} factors={analytics.healthFactors} />

{/* Row 3: Memory Timeline */}
<div style={{ marginTop: 8 }}>
<MemoryTimeline data={analytics.timeline} />
</div>

{/* Row 4: Value Metrics */}
<div style={{ marginTop: 8 }}>
<ValueMetrics
totalRecalls={analytics.valueMetrics.totalRecalls}
lessonsWithWarnings={analytics.valueMetrics.lessonsWithWarnings}
lessonCount={analytics.valueMetrics.lessonCount}
typeDistribution={analytics.valueMetrics.typeDistribution}
/>
</div>

{/* Top recalled — show meaningful preview, not raw commit hashes */}
{topRecalled.length > 0 && (
<div class="card">
<div class="card-title">{t('analytics.mostRecalled')}</div>
{topRecalled.map((e, i) => {
// Find the most meaningful observation (skip raw commit/session metadata)
const skipPrefixes = ['Commit:', '[SESSION]', 'Branch:', 'Diff stats:', 'Details:', '[WORK]', '[FOCUS]', '[SUMMARY]', 'Session edited', 'Total tool calls', 'Duration:'];
const preview = (e.observations || []).find(o =>
!skipPrefixes.some(p => o.startsWith(p)) && o.length > 10
) || e.observations?.[0] || e.name;
const truncated = preview.length > 80 ? preview.slice(0, 80) + '…' : preview;
return (
<div key={e.id} class="insight">
<span class="insight-icon" style={{ fontSize: 14, color: 'var(--text-3)' }}>#{i + 1}</span>
<span class="insight-text" style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{truncated}
</span>
<span class="insight-val">{e.access_count}×</span>
</div>
);
})}
</div>
)}
{/* Row 5: Cleanup Suggestions */}
<div style={{ marginTop: 8 }}>
<CleanupSuggestions
staleEntities={analytics.cleanup.staleEntities}
duplicateCandidates={analytics.cleanup.duplicateCandidates}
onRefresh={loadData}
/>
</div>

{/* Topics — filter out internal/system tags, show only user-meaningful ones */}
{/* Row 6: Topics cloud (kept from original) */}
{(() => {
const internalPrefixes = ['auto_saved', 'auto-tracked', 'session_end', 'session:', 'source:', 'scope:', 'date:', 'urgency:'];
const userTags = stats.tagDistribution.filter(tg =>
!internalPrefixes.some(p => tg.tag.startsWith(p)) &&
!/^\d{4}-\d{2}-\d{2}/.test(tg.tag) // filter date-only tags like "2026-03-26"
!/^\d{4}-\d{2}-\d{2}/.test(tg.tag)
);
return userTags.length > 0 ? (
<div class="card">
<div class="card" style={{ marginTop: 8 }}>
<div class="card-title">{t('analytics.topics')}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{userTags.slice(0, 30).map((tg) => (
Expand Down
Loading
Loading