@vheins/local-memory-mcp 0.3.24 → 0.3.26

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.
Files changed (39) hide show
  1. package/dist/dashboard/public/app.js +323 -82
  2. package/dist/dashboard/public/index.html +265 -95
  3. package/dist/dashboard/public/screenshot.png +0 -0
  4. package/dist/dashboard/server.js +16 -1
  5. package/dist/dashboard/server.js.map +1 -1
  6. package/dist/router.d.ts.map +1 -1
  7. package/dist/router.js +45 -13
  8. package/dist/router.js.map +1 -1
  9. package/dist/storage/sqlite.d.ts +4 -0
  10. package/dist/storage/sqlite.d.ts.map +1 -1
  11. package/dist/storage/sqlite.js +121 -12
  12. package/dist/storage/sqlite.js.map +1 -1
  13. package/dist/tasks.e2e.test.js +82 -3
  14. package/dist/tasks.e2e.test.js.map +1 -1
  15. package/dist/tools/memory.acknowledge.d.ts.map +1 -1
  16. package/dist/tools/memory.acknowledge.js +0 -2
  17. package/dist/tools/memory.acknowledge.js.map +1 -1
  18. package/dist/tools/memory.delete.d.ts.map +1 -1
  19. package/dist/tools/memory.delete.js +0 -2
  20. package/dist/tools/memory.delete.js.map +1 -1
  21. package/dist/tools/memory.search.d.ts.map +1 -1
  22. package/dist/tools/memory.search.js +0 -1
  23. package/dist/tools/memory.search.js.map +1 -1
  24. package/dist/tools/memory.store.d.ts.map +1 -1
  25. package/dist/tools/memory.store.js +0 -2
  26. package/dist/tools/memory.store.js.map +1 -1
  27. package/dist/tools/schemas.d.ts +30 -3
  28. package/dist/tools/schemas.d.ts.map +1 -1
  29. package/dist/tools/schemas.js +16 -4
  30. package/dist/tools/schemas.js.map +1 -1
  31. package/dist/tools/task.bulk-manage.d.ts.map +1 -1
  32. package/dist/tools/task.bulk-manage.js +0 -2
  33. package/dist/tools/task.bulk-manage.js.map +1 -1
  34. package/dist/tools/task.manage.d.ts.map +1 -1
  35. package/dist/tools/task.manage.js +19 -4
  36. package/dist/tools/task.manage.js.map +1 -1
  37. package/dist/types.d.ts +22 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -94,94 +94,95 @@ function getActionColor(action) {
94
94
  return colors[action] || colors.search;
95
95
  }
96
96
 
97
- function getBubbleStyle(action) {
98
- const styles = {
99
- search: {
100
- bubble: 'bg-blue-500 dark:bg-blue-600 text-white',
101
- label: 'text-blue-100',
102
- meta: 'text-blue-200',
103
- align: 'items-start',
104
- tail: 'left-2 -bottom-1.5 border-r-blue-500 dark:border-r-blue-600',
105
- },
106
- read: {
107
- bubble: 'bg-emerald-500 dark:bg-emerald-600 text-white',
108
- label: 'text-emerald-100',
109
- meta: 'text-emerald-200',
110
- align: 'items-start',
111
- tail: 'left-2 -bottom-1.5 border-r-emerald-500 dark:border-r-emerald-600',
112
- },
113
- write: {
114
- bubble: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100',
115
- label: 'text-gray-500 dark:text-gray-400',
116
- meta: 'text-gray-400 dark:text-gray-500',
117
- align: 'items-start',
118
- tail: 'left-2 -bottom-1.5 border-r-gray-100 dark:border-r-gray-700',
119
- },
120
- update: {
121
- bubble: 'bg-amber-100 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
122
- label: 'text-amber-600 dark:text-amber-400',
123
- meta: 'text-amber-500 dark:text-amber-500',
124
- align: 'items-start',
125
- tail: 'left-2 -bottom-1.5 border-r-amber-100 dark:border-r-amber-900',
126
- },
127
- delete: {
128
- bubble: 'bg-red-100 dark:bg-red-900 text-red-900 dark:text-red-100',
129
- label: 'text-red-500 dark:text-red-400',
130
- meta: 'text-red-400',
131
- align: 'items-start',
132
- tail: 'left-2 -bottom-1.5 border-r-red-100 dark:border-r-red-900',
133
- },
97
+ function getActionBubbleColor(action) {
98
+ const colors = {
99
+ search: 'from-blue-500 to-blue-600',
100
+ read: 'from-emerald-500 to-emerald-600',
101
+ write: 'from-indigo-500 to-indigo-600',
102
+ update: 'from-amber-500 to-amber-600',
103
+ delete: 'from-rose-500 to-rose-600'
134
104
  };
135
- return styles[action] || styles.search;
105
+ return colors[action] || 'from-slate-500 to-slate-600';
136
106
  }
137
107
 
138
108
  function renderActionBubble(action) {
139
- const s = getBubbleStyle(action.action);
140
- const isRight = false; // Force left alignment
141
-
142
- // Main content line
109
+ // Agent Request Bubble
143
110
  let mainText = '';
144
111
  let subText = '';
145
112
 
146
113
  if (action.action === 'search') {
147
114
  mainText = `🔍 "${action.query || ''}"`;
148
115
  subText = action.result_count != null ? `${action.result_count} result${action.result_count !== 1 ? 's' : ''} found` : '';
116
+ } else if (action.task_id) {
117
+ mainText = action.task_title || action.task_code || action.task_id.substring(0, 8);
118
+ const verb = { write: '💾 Created Task', update: '🔄 Updated Task', delete: '🗑️ Deleted Task' }[action.action] || action.action;
119
+ subText = action.task_code ? `${verb} [${action.task_code}]` : verb;
149
120
  } else {
150
- if (action.task_id) {
151
- mainText = action.task_title || action.task_code || action.task_id.substring(0, 8);
152
- const verb = { write: '💾 Created Task', update: '🔄 Updated Task', delete: '🗑️ Deleted Task' }[action.action] || action.action;
153
- subText = action.task_code ? `${verb} [${action.task_code}]` : verb;
154
- } else {
155
- mainText = action.memory_title
156
- ? action.memory_title
157
- : action.memory_id ? action.memory_id.substring(0, 8) + '…' : '—';
158
- const typeLabel = action.memory_type ? `[${action.memory_type}]` : '';
159
- const verb = { write: '💾 Stored', update: '🔄 Updated', delete: '🗑️ Deleted', read: '📖 Read' }[action.action] || action.action;
160
- subText = [verb, typeLabel].filter(Boolean).join(' ');
161
- }
121
+ mainText = action.memory_title || (action.memory_id ? action.memory_id.substring(0, 8) + '…' : '—');
122
+ const typeLabel = action.memory_type ? `[${action.memory_type}]` : '';
123
+ const verbs = {
124
+ write: '💾 Stored',
125
+ update: '🔄 Updated',
126
+ delete: '🗑️ Deleted',
127
+ read: '📖 Read',
128
+ agent_handoff: '🤝 Handoff',
129
+ agent_registered: '📝 Registration'
130
+ };
131
+ const verb = verbs[action.action] || verbs[action.memory_type] || action.action;
132
+ subText = [verb, typeLabel].filter(Boolean).join(' ');
162
133
  }
163
134
 
164
- const burst = action.burstCount > 1
165
- ? `<span class="ml-1 px-1.5 py-0.5 rounded-full bg-white/20 text-xs font-bold">×${action.burstCount}</span>`
166
- : '';
167
-
168
- return `
169
- <div class="flex flex-col ${s.align} mb-4">
170
- <div class="flex items-center gap-1.5 mb-1 px-1">
171
- <div class="w-5 h-5 rounded-full ${s.bubble} flex items-center justify-center flex-shrink-0">
135
+ const colorGradient = getActionBubbleColor(action.action);
136
+ const agentBubble = `
137
+ <div class="flex flex-col agent-align">
138
+ <div class="chat-bubble chat-bubble-agent bg-gradient-to-br ${colorGradient}">
139
+ <div class="flex items-center gap-1.5 mb-1 opacity-80">
172
140
  <svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">${getActionIcon(action.action)}</svg>
141
+ <span class="text-[10px] font-bold uppercase tracking-wider">${action.action}</span>
173
142
  </div>
174
- <span class="text-xs font-semibold capitalize ${s.label.replace('text-', 'text-').replace('100','500').replace('200','500')}">${action.action}${burst}</span>
143
+ <p class="text-sm font-medium leading-snug break-words">${mainText}</p>
144
+ ${subText ? `<p class="text-[10px] mt-1 opacity-70 font-semibold uppercase tracking-tight">${subText}</p>` : ''}
175
145
  </div>
176
- <div class="relative max-w-[95%]">
177
- <div class="${s.bubble} rounded-2xl rounded-bl-sm px-3 py-2 shadow-sm">
178
- <p class="text-sm font-medium leading-snug break-words">${mainText}</p>
179
- ${subText ? `<p class="text-xs mt-0.5 ${s.meta}">${subText}</p>` : ''}
180
- </div>
181
- </div>
182
- <span class="text-[10px] text-gray-400 dark:text-gray-500 mt-1 px-1">${formatActionDate(action.created_at)}</span>
146
+ <span class="chat-timestamp">${formatActionDate(action.created_at)}</span>
183
147
  </div>
184
148
  `;
149
+
150
+ // MCP Response Bubble (if exists)
151
+ let mcpBubble = '';
152
+ if (action.response) {
153
+ let responseContent = '';
154
+ try {
155
+ const resp = typeof action.response === 'string' ? JSON.parse(action.response) : action.response;
156
+ if (resp.content && Array.isArray(resp.content)) {
157
+ responseContent = resp.content
158
+ .filter(c => c.type === 'text')
159
+ .map(c => c.text)
160
+ .join('\n');
161
+ } else if (resp.message) {
162
+ responseContent = resp.message;
163
+ } else {
164
+ responseContent = JSON.stringify(resp, null, 2);
165
+ }
166
+ } catch (e) {
167
+ responseContent = action.response;
168
+ }
169
+
170
+ mcpBubble = `
171
+ <div class="flex flex-col mcp-align">
172
+ <div class="chat-bubble chat-bubble-mcp">
173
+ <div class="flex items-center gap-1.5 mb-1 opacity-60">
174
+ <div class="w-2 h-2 rounded-full bg-sky-500"></div>
175
+ <span class="text-[10px] font-bold uppercase tracking-wider">MCP REPLY</span>
176
+ </div>
177
+ <div class="markdown-body text-xs prose-sm prose-slate dark:prose-invert">
178
+ ${renderMarkdown(responseContent)}
179
+ </div>
180
+ </div>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ return agentBubble + mcpBubble;
185
186
  }
186
187
 
187
188
  function renderRecentActions() {
@@ -510,6 +511,7 @@ function updateCountdown() {
510
511
  btn.addEventListener('click', () => {
511
512
  const isDark = document.documentElement.classList.toggle('dark');
512
513
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
514
+ scheduleTabIndicatorPosition(currentTab);
513
515
  });
514
516
  }
515
517
  });
@@ -610,6 +612,10 @@ async function loadStats() {
610
612
  document.getElementById('decisionCount').textContent = data.byType?.decision || 0;
611
613
  document.getElementById('mistakeCount').textContent = data.byType?.mistake || 0;
612
614
  document.getElementById('patternCount').textContent = data.byType?.pattern || 0;
615
+ const handoffCountEl = document.getElementById('handoffCount');
616
+ const registeredCountEl = document.getElementById('registeredCount');
617
+ if (handoffCountEl) handoffCountEl.textContent = data.byType?.agent_handoff || 0;
618
+ if (registeredCountEl) registeredCountEl.textContent = data.byType?.agent_registered || 0;
613
619
 
614
620
  // Fill Task stats
615
621
  if (data.taskStats) {
@@ -622,6 +628,15 @@ async function loadStats() {
622
628
  document.getElementById('inProgressStatCount').textContent = data.taskStats.inProgress || 0;
623
629
  document.getElementById('completedStatCount').textContent = data.taskStats.completed || 0;
624
630
  document.getElementById('blockedStatCount').textContent = data.taskStats.blocked || 0;
631
+
632
+ // Also update column headers
633
+ const todoCountEl = document.getElementById('todoCount');
634
+ const inProgressCountEl = document.getElementById('inProgressCount');
635
+ const completedCountEl = document.getElementById('completedCount');
636
+
637
+ if (todoCountEl) todoCountEl.textContent = data.taskStats.todo || 0;
638
+ if (inProgressCountEl) inProgressCountEl.textContent = data.taskStats.inProgress || 0;
639
+ if (completedCountEl) completedCountEl.textContent = data.taskStats.completed || 0;
625
640
 
626
641
  updateTaskStatusChart(data.taskStats);
627
642
  }
@@ -646,16 +661,18 @@ function updateTypeChart(byType) {
646
661
  byType?.decision || 0,
647
662
  byType?.mistake || 0,
648
663
  byType?.code_fact || 0,
649
- byType?.pattern || 0
664
+ byType?.pattern || 0,
665
+ byType?.agent_handoff || 0,
666
+ byType?.agent_registered || 0
650
667
  ];
651
668
 
652
669
  charts.typeChart = new Chart(ctx, {
653
670
  type: 'doughnut',
654
671
  data: {
655
- labels: ['Decision', 'Mistake', 'Code Fact', 'Pattern'],
672
+ labels: ['Decision', 'Mistake', 'Code Fact', 'Pattern', 'Handoff', 'Registration'],
656
673
  datasets: [{
657
674
  data: counts,
658
- backgroundColor: ['#fb7185', '#c084fc', '#38bdf8', '#34d399'],
675
+ backgroundColor: ['#fb7185', '#c084fc', '#38bdf8', '#34d399', '#fb923c', '#a3e635'],
659
676
  borderWidth: 2,
660
677
  borderColor: isDark ? '#1e293b' : '#ffffff',
661
678
  hoverOffset: 12
@@ -1361,6 +1378,7 @@ function renderDetailPanel(data) {
1361
1378
  <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Source Info</div>
1362
1379
  <div class="space-y-2 text-sm">
1363
1380
  <div><strong>Agent:</strong> ${escapeHtml(data.agent || 'unknown')}</div>
1381
+ <div><strong>Role:</strong> ${escapeHtml(data.role || 'unknown')}</div>
1364
1382
  <div><strong>Model:</strong> ${escapeHtml(data.model || 'unknown')}</div>
1365
1383
  <div><strong>Repo:</strong> ${escapeHtml(data.scope?.repo || 'N/A')}</div>
1366
1384
  </div>
@@ -1375,6 +1393,27 @@ function renderDetailPanel(data) {
1375
1393
  </div>
1376
1394
  </div>
1377
1395
 
1396
+ ${data.type === 'agent_registered' ? `
1397
+ <div class="rounded-xl border border-lime-200 dark:border-lime-900 bg-lime-50 dark:bg-lime-900/20 p-4">
1398
+ <div class="text-xs uppercase tracking-wide text-lime-600 dark:text-lime-400 mb-2">Agent Status</div>
1399
+ <div class="flex items-center gap-2">
1400
+ <span class="inline-flex items-center px-2 py-1 rounded text-xs font-bold uppercase ${data.status === 'active' ? 'bg-lime-500 text-white' : 'bg-gray-400 text-white'}">
1401
+ ${escapeHtml(data.status)}
1402
+ </span>
1403
+ </div>
1404
+ </div>
1405
+ ` : ''}
1406
+
1407
+ ${data.type === 'agent_handoff' ? `
1408
+ <div class="rounded-xl border border-orange-200 dark:border-orange-900 bg-orange-50 dark:bg-orange-900/20 p-4">
1409
+ <div class="text-xs uppercase tracking-wide text-orange-600 dark:text-orange-400 mb-2">Handoff Details</div>
1410
+ <div class="space-y-2 text-sm">
1411
+ <div><strong>Completed at:</strong> ${data.completed_at ? new Date(data.completed_at).toLocaleString() : 'Pending'}</div>
1412
+ <div><strong>Task Status:</strong> <span class="capitalize px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">${data.status}</span></div>
1413
+ </div>
1414
+ </div>
1415
+ ` : ''}
1416
+
1378
1417
  ${data.supersedes ? `
1379
1418
  <div class="rounded-xl border border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-900/20 p-4">
1380
1419
  <div class="text-xs uppercase tracking-wide text-blue-500 dark:text-blue-400 mb-2">Supersedes</div>
@@ -1655,6 +1694,7 @@ async function loadData() {
1655
1694
  checkStatus(),
1656
1695
  loadRecentActions(),
1657
1696
  ]);
1697
+ scheduleTabIndicatorPosition(currentTab);
1658
1698
  }
1659
1699
 
1660
1700
  let currentTasks = [];
@@ -1787,7 +1827,7 @@ function renderTaskCards(containerId, tasks, clear = false) {
1787
1827
  ${t.depends_on ? `
1788
1828
  <div class="mt-2 pt-2 border-t border-gray-50 dark:border-gray-600 flex items-center gap-1.5">
1789
1829
  <svg class="w-3 h-3 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
1790
- <span class="text-[10px] font-medium text-amber-600 dark:text-amber-400">Depends on: ${t.depends_on.substring(0, 8)}</span>
1830
+ <span class="text-[10px] font-medium text-amber-600 dark:text-amber-400">Depends on: ${t.depends_on_code || t.depends_on.substring(0, 8)}</span>
1791
1831
  </div>
1792
1832
  ` : ''}
1793
1833
  <div class="mt-3 flex items-center justify-between">
@@ -1855,6 +1895,13 @@ async function showTaskDetail(id) {
1855
1895
  <span class="px-2 py-1 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 text-xs font-bold border border-indigo-500/20">P${task.priority}</span>
1856
1896
  <span class="px-2 py-1 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold border border-slate-200 dark:border-slate-700 uppercase">${task.phase}</span>
1857
1897
  </div>
1898
+ ${task.depends_on ? `
1899
+ <div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700/50 flex items-center gap-2">
1900
+ <svg class="w-3.5 h-3.5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
1901
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Depends on:</div>
1902
+ <span class="text-xs font-mono font-bold text-amber-600 dark:text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded border border-amber-500/20">${task.depends_on_code || task.depends_on.substring(0, 8)}</span>
1903
+ </div>
1904
+ ` : ''}
1858
1905
  </div>
1859
1906
  <div class="p-4 bg-white dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-800">
1860
1907
  <div class="text-[10px] font-bold text-slate-400 uppercase mb-2">Owner</div>
@@ -1877,6 +1924,8 @@ async function showTaskDetail(id) {
1877
1924
  </div>
1878
1925
  </div>
1879
1926
 
1927
+ ${renderTaskComments(task.comments)}
1928
+
1880
1929
  ${task.doc_path ? `
1881
1930
  <div class="p-4 bg-sky-500/5 dark:bg-sky-500/10 rounded-xl border border-sky-500/20">
1882
1931
  <div class="text-[10px] font-bold text-sky-600/60 dark:text-sky-400/60 uppercase mb-2">Documentation</div>
@@ -1912,14 +1961,128 @@ function getPriorityColor(p) {
1912
1961
  return 'text-gray-500 dark:text-gray-400';
1913
1962
  }
1914
1963
 
1964
+ function formatTaskStatusLabel(status) {
1965
+ return String(status || '')
1966
+ .replace(/_/g, ' ')
1967
+ .replace(/\b\w/g, (char) => char.toUpperCase());
1968
+ }
1969
+
1970
+ function renderTaskComments(comments) {
1971
+ if (!comments || comments.length === 0) {
1972
+ return `
1973
+ <div class="p-5 bg-white dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800">
1974
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">History</div>
1975
+ <div class="text-sm italic text-slate-400">No historical comments yet</div>
1976
+ </div>
1977
+ `;
1978
+ }
1979
+
1980
+ return `
1981
+ <div class="p-5 bg-white dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800">
1982
+ <div class="flex items-center justify-between gap-3 mb-4">
1983
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">History</div>
1984
+ <div class="text-[10px] text-slate-400">${comments.length} comment${comments.length === 1 ? '' : 's'}</div>
1985
+ </div>
1986
+ <div class="space-y-4">
1987
+ ${comments.map((item) => `
1988
+ <div class="relative pl-5">
1989
+ <div class="absolute left-0 top-1.5 h-full w-px bg-slate-200 dark:bg-slate-700"></div>
1990
+ <div class="absolute left-[-4px] top-1.5 w-2.5 h-2.5 rounded-full bg-sky-500 shadow-[0_0_0_4px_rgba(14,165,233,0.12)]"></div>
1991
+ <div class="rounded-xl border border-slate-100 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-900/40 p-4">
1992
+ <div class="flex flex-wrap items-center gap-2 mb-2">
1993
+ <span class="text-xs font-bold text-slate-800 dark:text-slate-100">${escapeHtml(item.agent || 'unknown')}</span>
1994
+ <span class="text-[10px] text-slate-400">•</span>
1995
+ <span class="text-[11px] text-slate-500 dark:text-slate-400">${escapeHtml(item.model || 'unknown')}</span>
1996
+ <span class="ml-auto text-[10px] text-slate-400">${new Date(item.created_at).toLocaleString()}</span>
1997
+ </div>
1998
+ ${(item.previous_status || item.next_status) ? `
1999
+ <div class="flex flex-wrap items-center gap-2 mb-3 text-[10px] font-bold uppercase tracking-wide">
2000
+ <span class="px-2 py-1 rounded-lg bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300">${escapeHtml(formatTaskStatusLabel(item.previous_status || 'note'))}</span>
2001
+ <span class="text-slate-400">→</span>
2002
+ <span class="px-2 py-1 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 border border-sky-500/20">${escapeHtml(formatTaskStatusLabel(item.next_status || 'note'))}</span>
2003
+ </div>
2004
+ ` : ''}
2005
+ <div class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed markdown-body">
2006
+ ${renderMarkdown(item.comment)}
2007
+ </div>
2008
+ </div>
2009
+ </div>
2010
+ `).join('')}
2011
+ </div>
2012
+ </div>
2013
+ `;
2014
+ }
2015
+
1915
2016
  let currentTab = localStorage.getItem('activeTab') || 'dashboard';
1916
2017
 
2018
+ function syncTabIndicatorTheme(indicator) {
2019
+ const isDark = document.documentElement.classList.contains('dark');
2020
+ indicator.style.background = isDark
2021
+ ? 'linear-gradient(135deg, rgba(14,165,233,0.95) 0%, rgba(79,70,229,0.92) 100%)'
2022
+ : 'linear-gradient(135deg, #0ea5e9 0%, #2563eb 100%)';
2023
+ indicator.style.border = isDark
2024
+ ? '1px solid rgba(59, 130, 246, 0.32)'
2025
+ : '1px solid rgba(37, 99, 235, 0.18)';
2026
+ indicator.style.boxShadow = isDark
2027
+ ? '0 12px 28px rgba(14,165,233,0.2), inset 0 1px 0 rgba(255,255,255,0.12)'
2028
+ : '0 10px 24px rgba(37,99,235,0.24), inset 0 1px 0 rgba(255,255,255,0.28)';
2029
+ }
2030
+
2031
+ function syncActiveTabButtonTheme(button, isActive) {
2032
+ if (!button) return;
2033
+
2034
+ const isDark = document.documentElement.classList.contains('dark');
2035
+
2036
+ if (!isActive) {
2037
+ button.style.color = '';
2038
+ button.style.background = '';
2039
+ button.style.border = '';
2040
+ button.style.boxShadow = '';
2041
+ return;
2042
+ }
2043
+
2044
+ button.style.color = isDark ? '#ffffff' : '#0f172a';
2045
+ button.style.background = isDark
2046
+ ? 'linear-gradient(135deg, rgba(14,165,233,0.95) 0%, rgba(79,70,229,0.92) 100%)'
2047
+ : 'rgba(255,255,255,0.96)';
2048
+ button.style.border = isDark
2049
+ ? '1px solid rgba(59, 130, 246, 0.32)'
2050
+ : '1px solid rgba(148, 163, 184, 0.22)';
2051
+ button.style.boxShadow = isDark
2052
+ ? '0 12px 28px rgba(14,165,233,0.2), inset 0 1px 0 rgba(255,255,255,0.12)'
2053
+ : '0 10px 24px rgba(148,163,184,0.16), inset 0 -3px 0 rgba(37,99,235,0.9)';
2054
+ }
2055
+
2056
+ function positionTabIndicator(tab) {
2057
+ const indicator = document.getElementById('tabIndicator');
2058
+ const rail = document.getElementById('tabRail');
2059
+ const targetButton = document.getElementById(`${tab}TabBtn`);
2060
+
2061
+ if (!indicator || !rail || !targetButton) return;
2062
+ syncTabIndicatorTheme(indicator);
2063
+
2064
+ const railRect = rail.getBoundingClientRect();
2065
+ const buttonRect = targetButton.getBoundingClientRect();
2066
+ const left = buttonRect.left - railRect.left;
2067
+
2068
+ indicator.style.left = `${left}px`;
2069
+ indicator.style.width = `${buttonRect.width}px`;
2070
+ indicator.style.transform = 'none';
2071
+ }
2072
+
2073
+ function scheduleTabIndicatorPosition(tab = currentTab) {
2074
+ requestAnimationFrame(() => {
2075
+ requestAnimationFrame(() => {
2076
+ positionTabIndicator(tab);
2077
+ });
2078
+ });
2079
+ }
2080
+
1917
2081
  function switchTab(tab) {
1918
2082
  const dashTab = document.getElementById('dashboardTabBtn');
1919
2083
  const memTab = document.getElementById('memoriesTabBtn');
1920
2084
  const taskTab = document.getElementById('tasksTabBtn');
1921
2085
  const refTab = document.getElementById('referenceTabBtn');
1922
- const indicator = document.getElementById('tabIndicator');
1923
2086
 
1924
2087
  const dashContent = document.getElementById('dashboardContent');
1925
2088
  const memContent = document.getElementById('memoriesContent');
@@ -1933,23 +2096,21 @@ function switchTab(tab) {
1933
2096
  const contents = [dashContent, memContent, taskContent, refContent]; const targetId = tab + 'TabBtn';
1934
2097
  const targetContentId = tab + 'Content';
1935
2098
 
1936
- // Update indicator
1937
- if (indicator) {
1938
- if (tab === 'dashboard') indicator.style.transform = 'translateX(0)';
1939
- if (tab === 'memories') indicator.style.transform = 'translateX(100%)';
1940
- if (tab === 'tasks') indicator.style.transform = 'translateX(200%)';
1941
- if (tab === 'reference') indicator.style.transform = 'translateX(300%)';
1942
- }
2099
+ scheduleTabIndicatorPosition(tab);
1943
2100
 
1944
2101
  // Update button states
1945
2102
  tabs.forEach(t => {
1946
2103
  if (t) {
1947
2104
  if (t.id === targetId) {
2105
+ t.classList.add('tab-active');
1948
2106
  t.classList.add('text-white');
1949
2107
  t.classList.remove('text-gray-500', 'dark:text-gray-400');
2108
+ syncActiveTabButtonTheme(t, true);
1950
2109
  } else {
2110
+ t.classList.remove('tab-active');
1951
2111
  t.classList.remove('text-white');
1952
2112
  t.classList.add('text-gray-500', 'dark:text-gray-400');
2113
+ syncActiveTabButtonTheme(t, false);
1953
2114
  }
1954
2115
  }
1955
2116
  });
@@ -1973,6 +2134,19 @@ function switchTab(tab) {
1973
2134
  syncStickyOffsets();
1974
2135
  }
1975
2136
 
2137
+ window.addEventListener('resize', () => {
2138
+ scheduleTabIndicatorPosition(currentTab);
2139
+ });
2140
+
2141
+ if (typeof ResizeObserver !== 'undefined') {
2142
+ const rail = document.getElementById('tabRail');
2143
+ if (rail) {
2144
+ new ResizeObserver(() => {
2145
+ scheduleTabIndicatorPosition(currentTab);
2146
+ }).observe(rail);
2147
+ }
2148
+ }
2149
+
1976
2150
  window.loadTasks = loadTasks;
1977
2151
  window.switchTab = switchTab;
1978
2152
  window.charts = charts;
@@ -2219,3 +2393,70 @@ switchTab(currentTab);
2219
2393
  syncStickyOffsets();
2220
2394
  startCountdown();
2221
2395
  setInterval(checkStatus, 30000);
2396
+
2397
+ // Memories Filter & Popover logic
2398
+ function toggleFilterPopover() {
2399
+ const popover = document.getElementById('filterPopover');
2400
+ popover.classList.toggle('hidden');
2401
+ updateActiveFilterCount();
2402
+ }
2403
+
2404
+ function toggleExportPopover() {
2405
+ const popover = document.getElementById('exportPopover');
2406
+ popover.classList.toggle('hidden');
2407
+ }
2408
+
2409
+ function updateActiveFilterCount() {
2410
+ const type = document.getElementById('typeFilter').value;
2411
+ const minImp = document.getElementById('minImportanceFilter').value;
2412
+ const maxImp = document.getElementById('maxImportanceFilter').value;
2413
+ let count = 0;
2414
+ if (type) count++;
2415
+ if (minImp) count++;
2416
+ if (maxImp) count++;
2417
+
2418
+ const badge = document.getElementById('activeFilterCount');
2419
+ if (badge) {
2420
+ if (count > 0) {
2421
+ badge.innerText = count;
2422
+ badge.classList.remove('hidden');
2423
+ } else {
2424
+ badge.classList.add('hidden');
2425
+ }
2426
+ }
2427
+ }
2428
+
2429
+ function resetFilters() {
2430
+ document.getElementById('typeFilter').value = '';
2431
+ document.getElementById('minImportanceFilter').value = '';
2432
+ document.getElementById('maxImportanceFilter').value = '';
2433
+ updateActiveFilterCount();
2434
+ currentPage = 1;
2435
+ loadMemories();
2436
+ }
2437
+
2438
+ document.addEventListener('click', (e) => {
2439
+ const filterPopover = document.getElementById('filterPopover');
2440
+ const filterBtn = document.getElementById('filterPopoverBtn');
2441
+ const exportPopover = document.getElementById('exportPopover');
2442
+ const exportBtn = document.getElementById('exportPopoverBtn');
2443
+
2444
+ if (filterPopover && !filterPopover.contains(e.target) && !filterBtn.contains(e.target)) {
2445
+ filterPopover.classList.add('hidden');
2446
+ }
2447
+ if (exportPopover && !exportPopover.contains(e.target) && !exportBtn.contains(e.target)) {
2448
+ exportPopover.classList.add('hidden');
2449
+ }
2450
+ });
2451
+
2452
+ window.toggleFilterPopover = toggleFilterPopover;
2453
+ window.toggleExportPopover = toggleExportPopover;
2454
+ window.resetFilters = resetFilters;
2455
+
2456
+ // Auto-hide popovers on Escape key
2457
+ document.addEventListener('keydown', (e) => {
2458
+ if (e.key === 'Escape') {
2459
+ document.getElementById('filterPopover')?.classList.add('hidden');
2460
+ document.getElementById('exportPopover')?.classList.add('hidden');
2461
+ }
2462
+ });