@vheins/local-memory-mcp 0.1.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/DASHBOARD.md +129 -0
- package/HYBRID_SEARCH.md +204 -0
- package/IMPLEMENTATION.md +159 -0
- package/README.md +175 -0
- package/dist/capabilities.d.ts +22 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/dashboard/dashboard.test.d.ts +2 -0
- package/dist/dashboard/dashboard.test.d.ts.map +1 -0
- package/dist/dashboard/dashboard.test.js +362 -0
- package/dist/dashboard/dashboard.test.js.map +1 -0
- package/dist/dashboard/public/app.js +1187 -0
- package/dist/dashboard/public/chart.js +0 -0
- package/dist/dashboard/public/index.html +967 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +297 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +181 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/client.test.d.ts +2 -0
- package/dist/mcp/client.test.d.ts.map +1 -0
- package/dist/mcp/client.test.js +130 -0
- package/dist/mcp/client.test.js.map +1 -0
- package/dist/prompts/registry.d.ts +39 -0
- package/dist/prompts/registry.d.ts.map +1 -0
- package/dist/prompts/registry.js +90 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/resources/index.d.ts +17 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +100 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/index.test.d.ts +2 -0
- package/dist/resources/index.test.d.ts.map +1 -0
- package/dist/resources/index.test.js +96 -0
- package/dist/resources/index.test.js.map +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +60 -0
- package/dist/router.js.map +1 -0
- package/dist/router.test.d.ts +2 -0
- package/dist/router.test.d.ts.map +1 -0
- package/dist/router.test.js +113 -0
- package/dist/router.test.js.map +1 -0
- package/dist/search_memory_example.d.ts +3 -0
- package/dist/search_memory_example.d.ts.map +1 -0
- package/dist/search_memory_example.js +56 -0
- package/dist/search_memory_example.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +91 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/sqlite.d.ts +95 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +537 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/sqlite.test.d.ts +2 -0
- package/dist/storage/sqlite.test.d.ts.map +1 -0
- package/dist/storage/sqlite.test.js +358 -0
- package/dist/storage/sqlite.test.js.map +1 -0
- package/dist/storage/vectors.stub.d.ts +12 -0
- package/dist/storage/vectors.stub.d.ts.map +1 -0
- package/dist/storage/vectors.stub.js +88 -0
- package/dist/storage/vectors.stub.js.map +1 -0
- package/dist/store_memory_example.d.ts +3 -0
- package/dist/store_memory_example.d.ts.map +1 -0
- package/dist/store_memory_example.js +69 -0
- package/dist/store_memory_example.js.map +1 -0
- package/dist/test_quotes_client.d.ts +3 -0
- package/dist/test_quotes_client.d.ts.map +1 -0
- package/dist/test_quotes_client.js +72 -0
- package/dist/test_quotes_client.js.map +1 -0
- package/dist/tools/memory.delete.d.ts +9 -0
- package/dist/tools/memory.delete.d.ts.map +1 -0
- package/dist/tools/memory.delete.js +22 -0
- package/dist/tools/memory.delete.js.map +1 -0
- package/dist/tools/memory.recap.d.ts +4 -0
- package/dist/tools/memory.recap.d.ts.map +1 -0
- package/dist/tools/memory.recap.js +42 -0
- package/dist/tools/memory.recap.js.map +1 -0
- package/dist/tools/memory.search.d.ts +5 -0
- package/dist/tools/memory.search.d.ts.map +1 -0
- package/dist/tools/memory.search.js +192 -0
- package/dist/tools/memory.search.js.map +1 -0
- package/dist/tools/memory.search.test.d.ts +2 -0
- package/dist/tools/memory.search.test.d.ts.map +1 -0
- package/dist/tools/memory.search.test.js +181 -0
- package/dist/tools/memory.search.test.js.map +1 -0
- package/dist/tools/memory.store.d.ts +5 -0
- package/dist/tools/memory.store.d.ts.map +1 -0
- package/dist/tools/memory.store.js +41 -0
- package/dist/tools/memory.store.js.map +1 -0
- package/dist/tools/memory.summarize.d.ts +4 -0
- package/dist/tools/memory.summarize.d.ts.map +1 -0
- package/dist/tools/memory.summarize.js +13 -0
- package/dist/tools/memory.summarize.js.map +1 -0
- package/dist/tools/memory.update.d.ts +5 -0
- package/dist/tools/memory.update.d.ts.map +1 -0
- package/dist/tools/memory.update.js +31 -0
- package/dist/tools/memory.update.js.map +1 -0
- package/dist/tools/schemas.d.ts +334 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +251 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/git-scope.d.ts +8 -0
- package/dist/utils/git-scope.d.ts.map +1 -0
- package/dist/utils/git-scope.js +38 -0
- package/dist/utils/git-scope.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +2 -0
- package/dist/utils/logger.test.d.ts.map +1 -0
- package/dist/utils/logger.test.js +84 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/mcp-response.d.ts +44 -0
- package/dist/utils/mcp-response.d.ts.map +1 -0
- package/dist/utils/mcp-response.js +81 -0
- package/dist/utils/mcp-response.js.map +1 -0
- package/dist/utils/normalize.d.ts +4 -0
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js +51 -0
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/normalize.test.d.ts +2 -0
- package/dist/utils/normalize.test.d.ts.map +1 -0
- package/dist/utils/normalize.test.js +159 -0
- package/dist/utils/normalize.test.js.map +1 -0
- package/dist/utils/query-expander.d.ts +2 -0
- package/dist/utils/query-expander.d.ts.map +1 -0
- package/dist/utils/query-expander.js +50 -0
- package/dist/utils/query-expander.js.map +1 -0
- package/dist/utils/query-expander.test.d.ts +2 -0
- package/dist/utils/query-expander.test.d.ts.map +1 -0
- package/dist/utils/query-expander.test.js +35 -0
- package/dist/utils/query-expander.test.js.map +1 -0
- package/docs/PRD.md +199 -0
- package/docs/PROMPT-agent.md +139 -0
- package/docs/SPEC-git-scope.md +172 -0
- package/docs/SPEC-heuristics.md +199 -0
- package/docs/SPEC-server.md +243 -0
- package/docs/SPEC-skeleton.md +255 -0
- package/docs/SPEC-sqlite-schema.md +183 -0
- package/docs/SPEC-tool-schema.md +201 -0
- package/docs/SPEC-vector-search.md +198 -0
- package/docs/TEST-scenarios.md +179 -0
- package/package.json +43 -0
- package/scripts/update-null-titles-ai.mjs +272 -0
- package/scripts/update-titles-batch.mjs +71 -0
- package/scripts/update-titles.mjs +66 -0
- package/seed-data.mjs +151 -0
- package/src/capabilities.ts +22 -0
- package/src/dashboard/dashboard.test.ts +546 -0
- package/src/dashboard/public/app.js +1187 -0
- package/src/dashboard/public/chart.js +0 -0
- package/src/dashboard/public/index.html +967 -0
- package/src/dashboard/server.ts +347 -0
- package/src/mcp/client.test.ts +164 -0
- package/src/mcp/client.ts +212 -0
- package/src/prompts/registry.ts +89 -0
- package/src/resources/index.test.ts +132 -0
- package/src/resources/index.ts +113 -0
- package/src/router.test.ts +145 -0
- package/src/router.ts +80 -0
- package/src/server.ts +99 -0
- package/src/storage/sqlite.test.ts +504 -0
- package/src/storage/sqlite.ts +688 -0
- package/src/storage/vectors.stub.ts +101 -0
- package/src/tools/memory.delete.ts +37 -0
- package/src/tools/memory.recap.ts +61 -0
- package/src/tools/memory.search.test.ts +276 -0
- package/src/tools/memory.search.ts +244 -0
- package/src/tools/memory.store.ts +56 -0
- package/src/tools/memory.summarize.ts +23 -0
- package/src/tools/memory.update.ts +46 -0
- package/src/tools/schemas.ts +261 -0
- package/src/types.ts +36 -0
- package/src/utils/git-scope.ts +42 -0
- package/src/utils/logger.test.ts +125 -0
- package/src/utils/logger.ts +53 -0
- package/src/utils/mcp-response.ts +116 -0
- package/src/utils/normalize.test.ts +203 -0
- package/src/utils/normalize.ts +53 -0
- package/src/utils/query-expander.test.ts +40 -0
- package/src/utils/query-expander.ts +60 -0
- package/storage/.gitkeep +5 -0
- package/test.sh +48 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
let currentRepo = null;
|
|
2
|
+
let currentMemories = [];
|
|
3
|
+
let currentPage = 1;
|
|
4
|
+
let pageSize = 10;
|
|
5
|
+
let totalPages = 1;
|
|
6
|
+
let totalItems = 0;
|
|
7
|
+
let selectedIds = new Set();
|
|
8
|
+
let currentPaginatedData = [];
|
|
9
|
+
let charts = {};
|
|
10
|
+
let lastSyncTime = Date.now();
|
|
11
|
+
let countdownSeconds = 30;
|
|
12
|
+
let countdownInterval = null;
|
|
13
|
+
let recentActions = [];
|
|
14
|
+
let activeEditMemoryId = null;
|
|
15
|
+
let currentDrawerMemoryId = null;
|
|
16
|
+
let availableRepos = [];
|
|
17
|
+
let isRepoSidebarCollapsed = false;
|
|
18
|
+
let pinnedRepoOrder = [];
|
|
19
|
+
let draggedPinnedRepo = null;
|
|
20
|
+
|
|
21
|
+
async function loadRecentActions() {
|
|
22
|
+
try {
|
|
23
|
+
const url = currentRepo
|
|
24
|
+
? `/api/recent-actions?repo=${encodeURIComponent(currentRepo)}&limit=50`
|
|
25
|
+
: '/api/recent-actions?limit=50';
|
|
26
|
+
const response = await fetch(url);
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
recentActions = data.actions || [];
|
|
29
|
+
renderRecentActions();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('Failed to load recent actions:', err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatActionDate(dateStr) {
|
|
36
|
+
const date = new Date(dateStr);
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const diff = Math.floor((now - date) / 1000);
|
|
39
|
+
if (diff < 60) return `${diff}s ago`;
|
|
40
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
41
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
42
|
+
return date.toLocaleDateString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getActionIcon(action) {
|
|
46
|
+
const icons = {
|
|
47
|
+
search: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>',
|
|
48
|
+
read: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>',
|
|
49
|
+
write: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>',
|
|
50
|
+
update: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>',
|
|
51
|
+
delete: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>'
|
|
52
|
+
};
|
|
53
|
+
return icons[action] || icons.search;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getActionColor(action) {
|
|
57
|
+
const colors = {
|
|
58
|
+
search: 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400',
|
|
59
|
+
read: 'bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400',
|
|
60
|
+
write: 'bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400',
|
|
61
|
+
update: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-600 dark:text-yellow-400',
|
|
62
|
+
delete: 'bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400'
|
|
63
|
+
};
|
|
64
|
+
return colors[action] || colors.search;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderRecentActions() {
|
|
68
|
+
const container = document.getElementById('recentQueries');
|
|
69
|
+
if (recentActions.length === 0) {
|
|
70
|
+
container.innerHTML = '<div class="text-gray-500 text-sm">No recent actions</div>';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
container.innerHTML = recentActions.map((action) => `
|
|
74
|
+
<div class="recent-action-item flex items-start gap-3 p-3 rounded-xl transition-colors">
|
|
75
|
+
<div class="w-8 h-8 rounded-full ${getActionColor(action.action)} flex items-center justify-center flex-shrink-0">
|
|
76
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
77
|
+
${getActionIcon(action.action)}
|
|
78
|
+
</svg>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex-1 min-w-0">
|
|
81
|
+
<div class="flex items-center gap-2">
|
|
82
|
+
<p class="text-sm font-medium text-gray-800 dark:text-gray-200 capitalize">${action.action}</p>
|
|
83
|
+
${action.burstCount > 1 ? `<span class="px-2 py-0.5 rounded-full bg-gray-200 dark:bg-gray-600 text-xs font-semibold text-gray-700 dark:text-gray-200">×${action.burstCount}</span>` : ''}
|
|
84
|
+
</div>
|
|
85
|
+
<p class="text-xs text-gray-500 truncate">${action.query || (action.memory_id ? 'Memory: ' + action.memory_id.substring(0, 8) : '-')}</p>
|
|
86
|
+
<p class="text-xs text-gray-400">${formatActionDate(action.created_at)}</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`).join('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function searchFromRecent(query) {
|
|
93
|
+
document.getElementById('searchInput').value = query;
|
|
94
|
+
currentPage = 1;
|
|
95
|
+
loadMemories();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getRepoInitials(repo) {
|
|
99
|
+
return repo
|
|
100
|
+
.split(/[\/\-_.]/)
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.slice(0, 2)
|
|
103
|
+
.map((part) => part[0]?.toUpperCase() || '')
|
|
104
|
+
.join('')
|
|
105
|
+
.slice(0, 2) || 'RP';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getRepoLastUpdatedLabel(repoMeta) {
|
|
109
|
+
if (!repoMeta?.last_updated_at) return 'No updates yet';
|
|
110
|
+
return `Updated ${formatDate(repoMeta.last_updated_at)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyRepoSidebarState() {
|
|
114
|
+
const layout = document.getElementById('appLayout');
|
|
115
|
+
const icon = document.getElementById('repoSidebarCollapseIcon');
|
|
116
|
+
const button = document.getElementById('repoSidebarCollapseToggle');
|
|
117
|
+
if (!layout || !icon || !button) return;
|
|
118
|
+
|
|
119
|
+
layout.classList.toggle('repo-sidebar-collapsed', isRepoSidebarCollapsed);
|
|
120
|
+
icon.style.transform = isRepoSidebarCollapsed ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
121
|
+
button.title = isRepoSidebarCollapsed ? 'Expand repositories' : 'Collapse repositories';
|
|
122
|
+
button.setAttribute('aria-label', button.title);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toggleRepoSidebarCollapse() {
|
|
126
|
+
isRepoSidebarCollapsed = !isRepoSidebarCollapsed;
|
|
127
|
+
localStorage.setItem('repoSidebarCollapsed', isRepoSidebarCollapsed ? '1' : '0');
|
|
128
|
+
applyRepoSidebarState();
|
|
129
|
+
renderRepoSidebar();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function updateCollapsedRepoSummary() {
|
|
133
|
+
const initials = document.getElementById('repoCollapsedSummaryInitials');
|
|
134
|
+
const count = document.getElementById('repoCollapsedSummaryCount');
|
|
135
|
+
const button = document.getElementById('repoCollapsedSummaryButton');
|
|
136
|
+
if (!initials || !count || !button) return;
|
|
137
|
+
|
|
138
|
+
const activeRepo = availableRepos.find((item) => item.repo === currentRepo);
|
|
139
|
+
initials.textContent = getRepoInitials(currentRepo || 'RP');
|
|
140
|
+
count.textContent = String(activeRepo?.memory_count ?? 0);
|
|
141
|
+
button.title = activeRepo
|
|
142
|
+
? `${activeRepo.repo} • ${activeRepo.memory_count} memories`
|
|
143
|
+
: 'Active repository';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function persistPinnedRepos() {
|
|
147
|
+
localStorage.setItem('pinnedRepos', JSON.stringify(pinnedRepoOrder));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function initPinnedRepos() {
|
|
151
|
+
try {
|
|
152
|
+
const raw = localStorage.getItem('pinnedRepos');
|
|
153
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
154
|
+
pinnedRepoOrder = Array.isArray(parsed) ? parsed : [];
|
|
155
|
+
} catch {
|
|
156
|
+
pinnedRepoOrder = [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isRepoPinned(repo) {
|
|
161
|
+
return pinnedRepoOrder.includes(repo);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getOrderedPinnedRepos(repos) {
|
|
165
|
+
return repos
|
|
166
|
+
.filter((item) => isRepoPinned(item.repo))
|
|
167
|
+
.sort((a, b) => pinnedRepoOrder.indexOf(a.repo) - pinnedRepoOrder.indexOf(b.repo));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function togglePinnedRepo(repo, event) {
|
|
171
|
+
if (event) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!repo) return;
|
|
177
|
+
|
|
178
|
+
if (isRepoPinned(repo)) pinnedRepoOrder = pinnedRepoOrder.filter((item) => item !== repo);
|
|
179
|
+
else pinnedRepoOrder.push(repo);
|
|
180
|
+
|
|
181
|
+
persistPinnedRepos();
|
|
182
|
+
renderRepoSidebar();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function startPinnedRepoDrag(repo, event) {
|
|
186
|
+
if (!isRepoPinned(repo)) return;
|
|
187
|
+
draggedPinnedRepo = repo;
|
|
188
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
189
|
+
event.dataTransfer.setData('text/plain', repo);
|
|
190
|
+
event.currentTarget.classList.add('dragging');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function overPinnedRepoDrag(repo, event) {
|
|
194
|
+
if (!draggedPinnedRepo || draggedPinnedRepo === repo || !isRepoPinned(repo)) return;
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
event.dataTransfer.dropEffect = 'move';
|
|
197
|
+
event.currentTarget.classList.add('drag-target');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function leavePinnedRepoDrag(event) {
|
|
201
|
+
event.currentTarget.classList.remove('drag-target');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dropPinnedRepo(repo, event) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
event.currentTarget.classList.remove('drag-target');
|
|
207
|
+
|
|
208
|
+
const draggedRepo = draggedPinnedRepo || event.dataTransfer.getData('text/plain');
|
|
209
|
+
if (!draggedRepo || draggedRepo === repo || !isRepoPinned(draggedRepo) || !isRepoPinned(repo)) return;
|
|
210
|
+
|
|
211
|
+
const nextOrder = pinnedRepoOrder.filter((item) => item !== draggedRepo);
|
|
212
|
+
const targetIndex = nextOrder.indexOf(repo);
|
|
213
|
+
nextOrder.splice(targetIndex, 0, draggedRepo);
|
|
214
|
+
pinnedRepoOrder = nextOrder;
|
|
215
|
+
draggedPinnedRepo = null;
|
|
216
|
+
persistPinnedRepos();
|
|
217
|
+
renderRepoSidebar();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function endPinnedRepoDrag(event) {
|
|
221
|
+
draggedPinnedRepo = null;
|
|
222
|
+
event.currentTarget.classList.remove('dragging');
|
|
223
|
+
document.querySelectorAll('.repo-item.drag-target').forEach((el) => el.classList.remove('drag-target'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderRepoSidebar() {
|
|
227
|
+
const desktopQuery = document.getElementById('repoSearchInput')?.value?.trim().toLowerCase() || '';
|
|
228
|
+
const mobileQuery = document.getElementById('repoSearchInputMobile')?.value?.trim().toLowerCase() || '';
|
|
229
|
+
const query = desktopQuery || mobileQuery;
|
|
230
|
+
const repos = availableRepos.filter((item) => item.repo.toLowerCase().includes(query));
|
|
231
|
+
const pinnedItems = getOrderedPinnedRepos(repos);
|
|
232
|
+
const unpinnedItems = repos.filter((item) => !isRepoPinned(item.repo));
|
|
233
|
+
|
|
234
|
+
const renderItems = (items) => items.map((item) => `
|
|
235
|
+
<div class="repo-item ${item.repo === currentRepo ? 'active' : ''} ${isRepoPinned(item.repo) ? 'pinned-item' : ''}" onclick="setCurrentRepo('${item.repo.replace(/'/g, "\\'")}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault(); setCurrentRepo('${item.repo.replace(/'/g, "\\'")}')}" title="${item.repo} • ${item.memory_count} memories" role="button" tabindex="0" ${isRepoPinned(item.repo) ? `draggable="true" ondragstart="startPinnedRepoDrag('${item.repo.replace(/'/g, "\\'")}', event)" ondragover="overPinnedRepoDrag('${item.repo.replace(/'/g, "\\'")}', event)" ondragleave="leavePinnedRepoDrag(event)" ondrop="dropPinnedRepo('${item.repo.replace(/'/g, "\\'")}', event)" ondragend="endPinnedRepoDrag(event)"` : ''}>
|
|
236
|
+
<span class="repo-avatar">${getRepoInitials(item.repo)}${isRepoPinned(item.repo) ? '<span class="repo-pinned-mark">★</span>' : ''}</span>
|
|
237
|
+
<span class="repo-item-copy min-w-0 flex-1">
|
|
238
|
+
<span class="flex items-center justify-between gap-3">
|
|
239
|
+
<span class="flex items-center gap-2 min-w-0">
|
|
240
|
+
${isRepoPinned(item.repo) ? `
|
|
241
|
+
<span class="repo-drag-handle" title="Drag to reorder pinned repositories" aria-hidden="true">
|
|
242
|
+
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
|
|
243
|
+
<path d="M7 4a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3ZM7 8.5a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3ZM7 13a1.5 1.5 0 110 3 1.5 1.5 0 010-3Zm6 0a1.5 1.5 0 110 3 1.5 1.5 0 010-3Z"/>
|
|
244
|
+
</svg>
|
|
245
|
+
</span>
|
|
246
|
+
` : ''}
|
|
247
|
+
<span class="block truncate text-sm font-semibold text-gray-900 dark:text-gray-100">${item.repo}</span>
|
|
248
|
+
</span>
|
|
249
|
+
<span class="text-[11px] font-semibold text-sky-700 dark:text-sky-200">${item.memory_count}</span>
|
|
250
|
+
</span>
|
|
251
|
+
<span class="block truncate text-xs text-gray-500 dark:text-gray-400">${item.repo === currentRepo ? 'Active repository' : getRepoLastUpdatedLabel(item)}</span>
|
|
252
|
+
</span>
|
|
253
|
+
<button class="repo-item-pin ${isRepoPinned(item.repo) ? 'pinned' : ''}" onclick="togglePinnedRepo('${item.repo.replace(/'/g, "\\'")}', event)" title="${isRepoPinned(item.repo) ? 'Unpin repository' : 'Pin repository'}" aria-label="${isRepoPinned(item.repo) ? 'Unpin repository' : 'Pin repository'}">
|
|
254
|
+
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
255
|
+
<path d="M12.586 2.586a2 2 0 0 1 2.828 0l2 2a2 2 0 0 1 0 2.828l-1.793 1.793-.914 4.57a1 1 0 0 1-.271.51l-1.414 1.414a1 1 0 0 1-1.414 0l-2.122-2.121-4.172 4.171a1 1 0 1 1-1.414-1.414l4.171-4.172-2.12-2.12a1 1 0 0 1 0-1.415l1.413-1.414a1 1 0 0 1 .51-.27l4.57-.915 1.792-1.793Z"/>
|
|
256
|
+
</svg>
|
|
257
|
+
</button>
|
|
258
|
+
${item.repo === currentRepo ? '<span class="repo-active-dot"></span>' : ''}
|
|
259
|
+
</div>
|
|
260
|
+
`).join('');
|
|
261
|
+
|
|
262
|
+
const renderGroups = () => {
|
|
263
|
+
const groups = [];
|
|
264
|
+
if (pinnedItems.length > 0) {
|
|
265
|
+
groups.push(`
|
|
266
|
+
<section class="repo-group">
|
|
267
|
+
<div class="repo-group-label">Pinned</div>
|
|
268
|
+
<div class="space-y-2">${renderItems(pinnedItems)}</div>
|
|
269
|
+
</section>
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
if (unpinnedItems.length > 0) {
|
|
273
|
+
groups.push(`
|
|
274
|
+
<section class="repo-group">
|
|
275
|
+
<div class="repo-group-label">${pinnedItems.length > 0 ? 'All Repositories' : 'Repositories'}</div>
|
|
276
|
+
<div class="space-y-2">${renderItems(unpinnedItems)}</div>
|
|
277
|
+
</section>
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
return groups.join('');
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
document.getElementById('repoCountBadge').textContent = String(availableRepos.length);
|
|
284
|
+
document.getElementById('currentRepoLabel').textContent = currentRepo || 'No repository';
|
|
285
|
+
updateCollapsedRepoSummary();
|
|
286
|
+
|
|
287
|
+
if (repos.length === 0) {
|
|
288
|
+
document.getElementById('repoSidebarList').innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-4">No repositories found.</div>';
|
|
289
|
+
const mobile = document.getElementById('repoSidebarListMobile');
|
|
290
|
+
if (mobile) mobile.innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-4">No repositories found.</div>';
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
document.getElementById('repoSidebarList').innerHTML = renderGroups();
|
|
295
|
+
const mobile = document.getElementById('repoSidebarListMobile');
|
|
296
|
+
if (mobile) mobile.innerHTML = renderGroups();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function setCurrentRepo(repo) {
|
|
300
|
+
if (!repo || repo === currentRepo) return;
|
|
301
|
+
currentRepo = repo;
|
|
302
|
+
currentPage = 1;
|
|
303
|
+
selectedIds.clear();
|
|
304
|
+
localStorage.setItem('selectedRepo', currentRepo);
|
|
305
|
+
renderRepoSidebar();
|
|
306
|
+
closeRepoSidebarDrawer();
|
|
307
|
+
await Promise.all([
|
|
308
|
+
loadStats(),
|
|
309
|
+
loadMemories(),
|
|
310
|
+
loadRecentActions(),
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function openRepoSidebarDrawer() {
|
|
315
|
+
document.getElementById('repoSidebarDrawer').classList.remove('hidden');
|
|
316
|
+
document.body.classList.add('drawer-open');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function closeRepoSidebarDrawer() {
|
|
320
|
+
document.getElementById('repoSidebarDrawer').classList.add('hidden');
|
|
321
|
+
if (!document.getElementById('memoryDrawer') || document.getElementById('memoryDrawer').classList.contains('hidden')) {
|
|
322
|
+
document.body.classList.remove('drawer-open');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function initTheme() {
|
|
327
|
+
const saved = localStorage.getItem('theme') || 'light';
|
|
328
|
+
document.documentElement.classList.toggle('dark', saved === 'dark');
|
|
329
|
+
document.getElementById('themeToggle').textContent = saved === 'dark' ? '☀️' : '🌙';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function initRepoSidebarState() {
|
|
333
|
+
isRepoSidebarCollapsed = localStorage.getItem('repoSidebarCollapsed') === '1';
|
|
334
|
+
applyRepoSidebarState();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function startCountdown() {
|
|
338
|
+
if (countdownInterval) clearInterval(countdownInterval);
|
|
339
|
+
countdownSeconds = 30;
|
|
340
|
+
updateCountdown();
|
|
341
|
+
|
|
342
|
+
countdownInterval = setInterval(() => {
|
|
343
|
+
countdownSeconds--;
|
|
344
|
+
if (countdownSeconds <= 0) {
|
|
345
|
+
countdownSeconds = 30;
|
|
346
|
+
loadData();
|
|
347
|
+
}
|
|
348
|
+
updateCountdown();
|
|
349
|
+
}, 1000);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function updateCountdown() {
|
|
353
|
+
const fill = document.getElementById('countdownFill');
|
|
354
|
+
const status = document.getElementById('syncStatus');
|
|
355
|
+
const percent = (countdownSeconds / 30) * 100;
|
|
356
|
+
fill.style.width = percent + '%';
|
|
357
|
+
|
|
358
|
+
if (countdownSeconds <= 5) {
|
|
359
|
+
fill.style.background = '#ef4444';
|
|
360
|
+
} else if (countdownSeconds <= 10) {
|
|
361
|
+
fill.style.background = '#f97316';
|
|
362
|
+
} else {
|
|
363
|
+
fill.style.background = '#3b82f6';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
status.textContent = `Synced ${countdownSeconds}s ago`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
document.getElementById('themeToggle').addEventListener('click', () => {
|
|
370
|
+
const isDark = document.documentElement.classList.toggle('dark');
|
|
371
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
372
|
+
document.getElementById('themeToggle').textContent = isDark ? '☀️' : '🌙';
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
document.addEventListener('keydown', (e) => {
|
|
376
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
|
|
377
|
+
|
|
378
|
+
if (e.key === 'Escape') closeDrawer();
|
|
379
|
+
else if (e.key === 'r' || e.key === 'R') loadData();
|
|
380
|
+
else if (e.key === '/') {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
document.getElementById('searchInput')?.focus();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
async function checkStatus() {
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch('/api/health');
|
|
389
|
+
const data = await response.json();
|
|
390
|
+
const dot = document.getElementById('statusDot');
|
|
391
|
+
const text = document.getElementById('statusText');
|
|
392
|
+
|
|
393
|
+
if (data.connected) {
|
|
394
|
+
dot.classList.remove('bg-gray-400');
|
|
395
|
+
dot.classList.add('bg-green-500');
|
|
396
|
+
text.textContent = 'Connected';
|
|
397
|
+
} else {
|
|
398
|
+
dot.classList.remove('bg-green-500');
|
|
399
|
+
dot.classList.add('bg-gray-400');
|
|
400
|
+
text.textContent = 'Disconnected';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const summary = document.getElementById('memorySummaryLabel');
|
|
404
|
+
if (summary) {
|
|
405
|
+
summary.textContent = `${data.memoryCount || 0} memories indexed`;
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.error('Status check failed:', err);
|
|
409
|
+
const dot = document.getElementById('statusDot');
|
|
410
|
+
dot.classList.remove('bg-green-500');
|
|
411
|
+
dot.classList.add('bg-gray-400');
|
|
412
|
+
document.getElementById('statusText').textContent = 'Error';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function loadRepos() {
|
|
417
|
+
try {
|
|
418
|
+
const response = await fetch('/api/repos');
|
|
419
|
+
const data = await response.json();
|
|
420
|
+
if (data.repos && data.repos.length > 0) {
|
|
421
|
+
availableRepos = data.repos;
|
|
422
|
+
const savedRepo = localStorage.getItem('selectedRepo');
|
|
423
|
+
const repoNames = availableRepos.map((item) => item.repo);
|
|
424
|
+
if (savedRepo && repoNames.includes(savedRepo)) {
|
|
425
|
+
currentRepo = savedRepo;
|
|
426
|
+
} else if (!currentRepo) {
|
|
427
|
+
currentRepo = availableRepos[0].repo;
|
|
428
|
+
}
|
|
429
|
+
localStorage.setItem('selectedRepo', currentRepo);
|
|
430
|
+
renderRepoSidebar();
|
|
431
|
+
} else {
|
|
432
|
+
availableRepos = [];
|
|
433
|
+
currentRepo = null;
|
|
434
|
+
renderRepoSidebar();
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error('Failed to load repos:', err);
|
|
438
|
+
availableRepos = [];
|
|
439
|
+
document.getElementById('repoSidebarList').innerHTML = '<div class="text-sm text-red-500 px-3 py-4">Failed to load repositories.</div>';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function loadStats() {
|
|
444
|
+
try {
|
|
445
|
+
const url = currentRepo ? `/api/stats?repo=${encodeURIComponent(currentRepo)}` : '/api/stats';
|
|
446
|
+
const response = await fetch(url);
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
|
|
449
|
+
document.getElementById('totalCount').textContent = data.total;
|
|
450
|
+
document.getElementById('avgImportance').textContent = data.avgImportance || '0';
|
|
451
|
+
document.getElementById('totalHits').textContent = data.totalHitCount || '0';
|
|
452
|
+
document.getElementById('expiringSoon').textContent = data.expiringSoon || '0';
|
|
453
|
+
document.getElementById('codeFactCount').textContent = data.byType?.code_fact || 0;
|
|
454
|
+
document.getElementById('decisionCount').textContent = data.byType?.decision || 0;
|
|
455
|
+
document.getElementById('mistakeCount').textContent = data.byType?.mistake || 0;
|
|
456
|
+
document.getElementById('patternCount').textContent = data.byType?.pattern || 0;
|
|
457
|
+
|
|
458
|
+
updateTypeChart(data.byType);
|
|
459
|
+
updateTimeSeriesChart(data.timeSeries || {});
|
|
460
|
+
updateScatterChart(data.scatterData || []);
|
|
461
|
+
updateTopMemoriesChart(data.topMemories);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.error('Failed to load stats:', err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function updateTypeChart(byType) {
|
|
468
|
+
const ctx = document.getElementById('typeChart');
|
|
469
|
+
if (!window.Chart) {
|
|
470
|
+
ctx.parentElement.innerHTML = '<div class="p-8 text-center text-gray-500">Chart.js not available</div>';
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (charts.typeChart) charts.typeChart.destroy();
|
|
474
|
+
|
|
475
|
+
const types = Object.keys(byType || {});
|
|
476
|
+
const counts = Object.values(byType || {});
|
|
477
|
+
|
|
478
|
+
charts.typeChart = new Chart(ctx, {
|
|
479
|
+
type: 'pie',
|
|
480
|
+
data: {
|
|
481
|
+
labels: types.map(t => t.replace('_', ' ')),
|
|
482
|
+
datasets: [{
|
|
483
|
+
data: counts,
|
|
484
|
+
backgroundColor: ['#38bdf8', '#fb7185', '#a78bfa', '#34d399'],
|
|
485
|
+
borderColor: 'rgba(255,255,255,0.72)',
|
|
486
|
+
borderWidth: 2
|
|
487
|
+
}]
|
|
488
|
+
},
|
|
489
|
+
options: { responsive: true, maintainAspectRatio: true }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function updateTopMemoriesChart(memories = []) {
|
|
494
|
+
const ctx = document.getElementById('topMemoriesChart');
|
|
495
|
+
if (!window.Chart) {
|
|
496
|
+
ctx.parentElement.innerHTML = '<div class="p-8 text-center text-gray-500">Chart.js not available</div>';
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (charts.topMemoriesChart) charts.topMemoriesChart.destroy();
|
|
500
|
+
|
|
501
|
+
charts.topMemoriesChart = new Chart(ctx, {
|
|
502
|
+
type: 'bar',
|
|
503
|
+
data: {
|
|
504
|
+
labels: memories.map((m, i) => `#${i + 1}`),
|
|
505
|
+
datasets: [{
|
|
506
|
+
label: 'Hit Count',
|
|
507
|
+
data: memories.map(m => m.hit_count || m.importance),
|
|
508
|
+
backgroundColor: ['#38bdf8', '#60a5fa', '#22d3ee', '#7dd3fc', '#93c5fd', '#67e8f9', '#38bdf8', '#60a5fa', '#22d3ee', '#7dd3fc'],
|
|
509
|
+
borderRadius: 10
|
|
510
|
+
}]
|
|
511
|
+
},
|
|
512
|
+
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true } } }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function updateTimeSeriesChart(timeSeries) {
|
|
517
|
+
const ctx = document.getElementById('timeSeriesChart');
|
|
518
|
+
if (!window.Chart) return;
|
|
519
|
+
if (charts.timeSeriesChart) charts.timeSeriesChart.destroy();
|
|
520
|
+
|
|
521
|
+
const labels = Object.keys(timeSeries).slice(-14);
|
|
522
|
+
const data = Object.values(timeSeries).slice(-14);
|
|
523
|
+
|
|
524
|
+
charts.timeSeriesChart = new Chart(ctx, {
|
|
525
|
+
type: 'line',
|
|
526
|
+
data: {
|
|
527
|
+
labels,
|
|
528
|
+
datasets: [{
|
|
529
|
+
label: 'Memories Created',
|
|
530
|
+
data,
|
|
531
|
+
borderColor: '#22d3ee',
|
|
532
|
+
backgroundColor: 'rgba(34, 211, 238, 0.16)',
|
|
533
|
+
pointBackgroundColor: '#7dd3fc',
|
|
534
|
+
pointBorderColor: '#e0f2fe',
|
|
535
|
+
fill: true,
|
|
536
|
+
tension: 0.35
|
|
537
|
+
}]
|
|
538
|
+
},
|
|
539
|
+
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function updateScatterChart(scatterData) {
|
|
544
|
+
const ctx = document.getElementById('scatterChart');
|
|
545
|
+
if (!window.Chart) return;
|
|
546
|
+
if (charts.scatterChart) charts.scatterChart.destroy();
|
|
547
|
+
|
|
548
|
+
charts.scatterChart = new Chart(ctx, {
|
|
549
|
+
type: 'scatter',
|
|
550
|
+
data: {
|
|
551
|
+
datasets: [{
|
|
552
|
+
label: 'Memories',
|
|
553
|
+
data: scatterData,
|
|
554
|
+
backgroundColor: 'rgba(96, 165, 250, 0.85)',
|
|
555
|
+
borderColor: '#a78bfa',
|
|
556
|
+
pointRadius: 4.5,
|
|
557
|
+
pointHoverRadius: 6
|
|
558
|
+
}]
|
|
559
|
+
},
|
|
560
|
+
options: { responsive: true, maintainAspectRatio: true, scales: { x: { title: { display: true, text: 'Importance' }, min: 0, max: 6 }, y: { title: { display: true, text: 'Hit Count' }, beginAtZero: true } } }
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function loadMemories() {
|
|
565
|
+
if (!currentRepo) {
|
|
566
|
+
document.getElementById('tableContainer').innerHTML = '<div class="text-gray-500 py-12">Please select a repository</div>';
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
renderTableSkeleton();
|
|
572
|
+
const typeFilter = document.getElementById('typeFilter').value;
|
|
573
|
+
const search = document.getElementById('searchInput').value.trim();
|
|
574
|
+
const minImportance = document.getElementById('minImportanceFilter').value;
|
|
575
|
+
const maxImportance = document.getElementById('maxImportanceFilter').value;
|
|
576
|
+
|
|
577
|
+
let url = `/api/memories?repo=${encodeURIComponent(currentRepo)}&page=${currentPage}&pageSize=${pageSize}&sortBy=${encodeURIComponent(sortColumn)}&sortOrder=${encodeURIComponent(sortOrder)}`;
|
|
578
|
+
if (typeFilter) url += `&type=${typeFilter}`;
|
|
579
|
+
if (search) url += `&search=${encodeURIComponent(search)}`;
|
|
580
|
+
if (minImportance) url += `&minImportance=${encodeURIComponent(minImportance)}`;
|
|
581
|
+
if (maxImportance) url += `&maxImportance=${encodeURIComponent(maxImportance)}`;
|
|
582
|
+
|
|
583
|
+
const response = await fetch(url);
|
|
584
|
+
const data = await response.json();
|
|
585
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load memories');
|
|
586
|
+
currentMemories = data.memories;
|
|
587
|
+
currentPaginatedData = currentMemories;
|
|
588
|
+
totalItems = data.pagination?.totalItems || currentMemories.length;
|
|
589
|
+
totalPages = data.pagination?.totalPages || 1;
|
|
590
|
+
updatePaginationControls(totalItems);
|
|
591
|
+
renderTable(currentMemories);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error('Failed to load memories:', err);
|
|
594
|
+
document.getElementById('errorMessage').innerHTML = `<div class="p-4 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded mb-4">Failed to load memories: ${err.message}</div>`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderTableSkeleton() {
|
|
599
|
+
const container = document.getElementById('tableContainer');
|
|
600
|
+
container.innerHTML = `
|
|
601
|
+
<div class="overflow-x-auto max-h-[68vh]">
|
|
602
|
+
<table class="w-full border-collapse sticky-table-header table-animate">
|
|
603
|
+
<thead>
|
|
604
|
+
<tr class="border-b-2 border-gray-200 dark:border-gray-700">
|
|
605
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"></th>
|
|
606
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Memory</th>
|
|
607
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Type</th>
|
|
608
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Priority</th>
|
|
609
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Usage</th>
|
|
610
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Freshness</th>
|
|
611
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold sticky-actions">Actions</th>
|
|
612
|
+
</tr>
|
|
613
|
+
</thead>
|
|
614
|
+
<tbody>
|
|
615
|
+
${Array.from({ length: Math.min(pageSize, 6) }).map(() => `
|
|
616
|
+
<tr class="border-b border-gray-100 dark:border-gray-700">
|
|
617
|
+
<td class="p-3"><div class="skeleton h-4 w-4"></div></td>
|
|
618
|
+
<td class="p-3"><div class="skeleton h-4 w-52 mb-2"></div><div class="skeleton h-3 w-36"></div></td>
|
|
619
|
+
<td class="p-3"><div class="skeleton h-6 w-20"></div></td>
|
|
620
|
+
<td class="p-3"><div class="skeleton h-6 w-12 mb-2"></div><div class="skeleton h-3 w-12"></div></td>
|
|
621
|
+
<td class="p-3"><div class="skeleton h-4 w-16 mb-2"></div><div class="skeleton h-3 w-14"></div></td>
|
|
622
|
+
<td class="p-3"><div class="skeleton h-4 w-14 mb-2"></div><div class="skeleton h-3 w-12"></div></td>
|
|
623
|
+
<td class="p-3 sticky-actions"><div class="flex gap-2"><div class="skeleton h-7 w-14"></div><div class="skeleton h-7 w-14"></div></div></td>
|
|
624
|
+
</tr>
|
|
625
|
+
`).join('')}
|
|
626
|
+
</tbody>
|
|
627
|
+
</table>
|
|
628
|
+
</div>
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function escapeHtml(text) {
|
|
633
|
+
const div = document.createElement('div');
|
|
634
|
+
div.textContent = text;
|
|
635
|
+
return div.innerHTML;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function formatDate(dateStr) {
|
|
639
|
+
const date = new Date(dateStr);
|
|
640
|
+
const now = new Date();
|
|
641
|
+
const diff = Math.floor((now - date) / 1000);
|
|
642
|
+
if (diff < 60) return `${diff}s ago`;
|
|
643
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
644
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
645
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function renderTable(memories) {
|
|
649
|
+
const container = document.getElementById('tableContainer');
|
|
650
|
+
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
|
|
651
|
+
const paginated = memories;
|
|
652
|
+
|
|
653
|
+
if (paginated.length === 0) {
|
|
654
|
+
container.innerHTML = '<div class="text-gray-500 py-12">No memories found matching your filters</div>';
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function highlightText(text, query) {
|
|
659
|
+
if (!query) return escapeHtml(text);
|
|
660
|
+
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
661
|
+
return escapeHtml(text).replace(regex, '<mark>$1</mark>');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const table = document.createElement('div');
|
|
665
|
+
table.className = 'overflow-x-auto max-h-[68vh]';
|
|
666
|
+
table.innerHTML = `
|
|
667
|
+
<table class="w-full border-collapse sticky-table-header table-animate">
|
|
668
|
+
<thead>
|
|
669
|
+
<tr class="border-b-2 border-gray-200 dark:border-gray-700">
|
|
670
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
|
|
671
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('title')" data-sort="title">Memory <span class="sort-icon"></span></th>
|
|
672
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('type')" data-sort="type">Type <span class="sort-icon"></span></th>
|
|
673
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('importance')" data-sort="importance">Priority <span class="sort-icon"></span></th>
|
|
674
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('hit_count')" data-sort="hit_count">Usage <span class="sort-icon"></span></th>
|
|
675
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold cursor-pointer" onclick="sortTable('created_at')" data-sort="created_at">Freshness <span class="sort-icon"></span></th>
|
|
676
|
+
<th class="text-left p-3 bg-gray-50 dark:bg-gray-700 font-semibold">Actions</th>
|
|
677
|
+
</tr>
|
|
678
|
+
</thead>
|
|
679
|
+
<tbody>
|
|
680
|
+
${paginated.map(m => `
|
|
681
|
+
<tr id="row-${m.id}" class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
682
|
+
<td class="p-3"><input type="checkbox" class="row-checkbox" value="${m.id}" ${selectedIds.has(m.id) ? 'checked' : ''} onchange="toggleSelect('${m.id}')"></td>
|
|
683
|
+
<td class="p-3 min-w-[18rem]">
|
|
684
|
+
<button onclick="openDrawer('${m.id}')" class="text-left font-semibold text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${highlightText(getDisplayTitle(m), searchQuery)}</button>
|
|
685
|
+
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
686
|
+
<span class="font-mono">${m.id.substring(0, 8)}</span>
|
|
687
|
+
<span class="mx-1">•</span>
|
|
688
|
+
<span>${m.scope?.repo || 'Unknown repo'}</span>
|
|
689
|
+
</div>
|
|
690
|
+
</td>
|
|
691
|
+
<td class="p-3"><span class="table-chip type-${m.type}">${formatTypeLabel(m.type)}</span></td>
|
|
692
|
+
<td class="p-3">
|
|
693
|
+
<div class="metric-badge ${getImportanceBadgeClass(m.importance)}">${m.importance}/5</div>
|
|
694
|
+
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">${getImportanceLabel(m.importance)}</div>
|
|
695
|
+
</td>
|
|
696
|
+
<td class="p-3">
|
|
697
|
+
<div class="font-semibold">${formatUsageCount(m.hit_count)}</div>
|
|
698
|
+
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">${formatRecallRate(m.recall_rate)}</div>
|
|
699
|
+
</td>
|
|
700
|
+
<td class="p-3">
|
|
701
|
+
<div class="font-medium">${formatDate(m.created_at)}</div>
|
|
702
|
+
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">Updated ${formatDate(m.updated_at)}</div>
|
|
703
|
+
</td>
|
|
704
|
+
<td class="p-3 sticky-actions">
|
|
705
|
+
<div class="flex flex-wrap gap-2">
|
|
706
|
+
<button onclick="openDrawer('${m.id}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium">Open</button>
|
|
707
|
+
<button onclick="startInlineEdit('${m.id}')" class="px-3 py-1.5 bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200 rounded hover:bg-amber-200 dark:hover:bg-amber-900/60 text-xs font-medium">Edit</button>
|
|
708
|
+
</div>
|
|
709
|
+
</td>
|
|
710
|
+
</tr>
|
|
711
|
+
`).join('')}
|
|
712
|
+
</tbody>
|
|
713
|
+
</table>
|
|
714
|
+
`;
|
|
715
|
+
|
|
716
|
+
container.innerHTML = '';
|
|
717
|
+
container.appendChild(table);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
let searchDebounce = null;
|
|
721
|
+
document.getElementById('searchInput').addEventListener('input', () => {
|
|
722
|
+
if (searchDebounce) clearTimeout(searchDebounce);
|
|
723
|
+
searchDebounce = setTimeout(() => {
|
|
724
|
+
currentPage = 1;
|
|
725
|
+
loadMemories();
|
|
726
|
+
}, 300);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
['typeFilter', 'minImportanceFilter', 'maxImportanceFilter'].forEach(id => {
|
|
730
|
+
document.getElementById(id).addEventListener('change', () => {
|
|
731
|
+
currentPage = 1;
|
|
732
|
+
loadMemories();
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
function changePageSize() {
|
|
737
|
+
pageSize = parseInt(document.getElementById('pageSizeSelect').value);
|
|
738
|
+
currentPage = 1;
|
|
739
|
+
loadMemories();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function goToPage(page) {
|
|
743
|
+
if (page < 1 || page > totalPages) return;
|
|
744
|
+
currentPage = page;
|
|
745
|
+
loadMemories();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function updatePaginationControls(totalItems) {
|
|
749
|
+
totalPages = Math.max(1, totalPages || Math.ceil(totalItems / pageSize) || 1);
|
|
750
|
+
if (currentPage > totalPages) currentPage = totalPages;
|
|
751
|
+
|
|
752
|
+
document.getElementById('firstPageBtn').disabled = currentPage <= 1;
|
|
753
|
+
document.getElementById('prevPageBtn').disabled = currentPage <= 1;
|
|
754
|
+
document.getElementById('nextPageBtn').disabled = currentPage >= totalPages;
|
|
755
|
+
document.getElementById('lastPageBtn').disabled = currentPage >= totalPages;
|
|
756
|
+
|
|
757
|
+
const start = totalItems > 0 ? ((currentPage - 1) * pageSize) + 1 : 0;
|
|
758
|
+
const end = Math.min(currentPage * pageSize, totalItems);
|
|
759
|
+
document.getElementById('paginationInfo').textContent = totalItems > 0 ? `Showing ${start}-${end} of ${totalItems}` : 'No results';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let sortColumn = 'hit_count';
|
|
763
|
+
let sortOrder = 'desc';
|
|
764
|
+
|
|
765
|
+
function formatTypeLabel(type) {
|
|
766
|
+
return type.replace('_', ' ');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function getDisplayTitle(memory) {
|
|
770
|
+
if (memory.title && memory.title.trim()) {
|
|
771
|
+
return memory.title.trim();
|
|
772
|
+
}
|
|
773
|
+
return memory.content.length > 80 ? `${memory.content.substring(0, 77)}...` : memory.content;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function getContentPreview(memory) {
|
|
777
|
+
const text = memory.content.replace(/\s+/g, ' ').trim();
|
|
778
|
+
return text.length > 220 ? `${text.substring(0, 217)}...` : text;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getImportanceLabel(importance) {
|
|
782
|
+
if (importance >= 5) return 'Critical';
|
|
783
|
+
if (importance >= 4) return 'High';
|
|
784
|
+
if (importance >= 3) return 'Medium';
|
|
785
|
+
if (importance >= 2) return 'Low';
|
|
786
|
+
return 'Minor';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function getImportanceBadgeClass(importance) {
|
|
790
|
+
if (importance >= 5) return 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-200';
|
|
791
|
+
if (importance >= 4) return 'bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-200';
|
|
792
|
+
if (importance >= 3) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-200';
|
|
793
|
+
if (importance >= 2) return 'bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-200';
|
|
794
|
+
return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function formatUsageCount(hitCount) {
|
|
798
|
+
const value = hitCount || 0;
|
|
799
|
+
if (value === 0) return 'Unused';
|
|
800
|
+
if (value === 1) return '1 hit';
|
|
801
|
+
return `${value} hits`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function formatRecallRate(recallRate) {
|
|
805
|
+
if (!recallRate) return 'Not recalled yet';
|
|
806
|
+
return `${(recallRate * 100).toFixed(1)}% recall`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function sortTable(column) {
|
|
810
|
+
if (sortColumn === column) {
|
|
811
|
+
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
|
812
|
+
} else {
|
|
813
|
+
sortColumn = column;
|
|
814
|
+
sortOrder = 'desc';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
document.querySelectorAll('th[data-sort]').forEach(th => {
|
|
818
|
+
th.classList.remove('sorted-asc', 'sorted-desc');
|
|
819
|
+
if (th.dataset.sort === column) {
|
|
820
|
+
th.classList.add(sortOrder === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
currentPage = 1;
|
|
825
|
+
loadMemories();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
document.querySelectorAll('th[data-sort]').forEach(th => {
|
|
829
|
+
if (th.dataset.sort === sortColumn) {
|
|
830
|
+
th.classList.add(sortOrder === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
function toggleSelectAll() {
|
|
835
|
+
const checked = document.getElementById('selectAll').checked;
|
|
836
|
+
currentPaginatedData.forEach(m => {
|
|
837
|
+
if (checked) selectedIds.add(m.id);
|
|
838
|
+
else selectedIds.delete(m.id);
|
|
839
|
+
});
|
|
840
|
+
updateBulkBar();
|
|
841
|
+
renderTable(currentMemories);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function toggleSelect(id) {
|
|
845
|
+
if (selectedIds.has(id)) selectedIds.delete(id);
|
|
846
|
+
else selectedIds.add(id);
|
|
847
|
+
updateBulkBar();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function updateBulkBar() {
|
|
851
|
+
const bar = document.getElementById('bulkActionBar');
|
|
852
|
+
if (selectedIds.size > 0) {
|
|
853
|
+
bar.classList.remove('hidden');
|
|
854
|
+
document.getElementById('selectedCount').textContent = `${selectedIds.size} selected`;
|
|
855
|
+
} else {
|
|
856
|
+
bar.classList.add('hidden');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function clearSelection() {
|
|
861
|
+
selectedIds.clear();
|
|
862
|
+
updateBulkBar();
|
|
863
|
+
renderTable(currentMemories);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function renderDetailPanel(data) {
|
|
867
|
+
const isEditing = activeEditMemoryId === data.id;
|
|
868
|
+
return `
|
|
869
|
+
<div class="space-y-5">
|
|
870
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-4">
|
|
871
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2">Summary</div>
|
|
872
|
+
<p class="text-sm leading-6 text-gray-700 dark:text-gray-300">${escapeHtml(getContentPreview(data))}</p>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
875
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
|
876
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Memory Info</div>
|
|
877
|
+
<div class="space-y-2 text-sm">
|
|
878
|
+
<div><strong>Type:</strong> ${formatTypeLabel(data.type)}</div>
|
|
879
|
+
<div><strong>ID:</strong> <span class="font-mono">${data.id}</span></div>
|
|
880
|
+
<div><strong>Repo:</strong> ${escapeHtml(data.scope?.repo || 'N/A')}</div>
|
|
881
|
+
<div><strong>Priority:</strong> ${data.importance}/5 (${getImportanceLabel(data.importance)})</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
|
885
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Usage</div>
|
|
886
|
+
<div class="space-y-2 text-sm">
|
|
887
|
+
<div><strong>Hit Count:</strong> ${data.hit_count || 0}</div>
|
|
888
|
+
<div><strong>Recall Count:</strong> ${data.recall_count || 0}</div>
|
|
889
|
+
<div><strong>Recall Rate:</strong> ${formatRecallRate(data.recall_rate)}</div>
|
|
890
|
+
<div><strong>Last Used:</strong> ${data.last_used_at ? new Date(data.last_used_at).toLocaleString() : 'Never'}</div>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
|
895
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Timeline</div>
|
|
896
|
+
<div class="space-y-2 text-sm">
|
|
897
|
+
<div><strong>Created:</strong> ${new Date(data.created_at).toLocaleString()}</div>
|
|
898
|
+
<div><strong>Updated:</strong> ${data.updated_at ? new Date(data.updated_at).toLocaleString() : 'N/A'}</div>
|
|
899
|
+
<div><strong>Expires:</strong> ${data.expires_at ? new Date(data.expires_at).toLocaleString() : 'Never'}</div>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
|
903
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">Full Content</div>
|
|
904
|
+
<pre class="whitespace-pre-wrap text-sm leading-6">${escapeHtml(data.content)}</pre>
|
|
905
|
+
</div>
|
|
906
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-700 p-4">
|
|
907
|
+
<div class="flex items-center justify-between mb-3">
|
|
908
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Quick Edit</div>
|
|
909
|
+
${isEditing
|
|
910
|
+
? '<button onclick="cancelDrawerEdit()" class="text-xs px-3 py-1.5 rounded bg-gray-200 dark:bg-gray-700">Cancel</button>'
|
|
911
|
+
: `<button onclick="startDrawerEdit('${data.id}')" class="text-xs px-3 py-1.5 rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200">Edit</button>`}
|
|
912
|
+
</div>
|
|
913
|
+
${isEditing ? renderDrawerEditForm(data) : `<p class="text-sm text-gray-500 dark:text-gray-400">Edit title, content, and priority from this panel without leaving the current table context.</p>`}
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function renderDrawerEditForm(data) {
|
|
920
|
+
return `
|
|
921
|
+
<div class="space-y-3">
|
|
922
|
+
<input id="drawer-edit-title" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" value="${escapeHtml(data.title || '')}" placeholder="Memory title">
|
|
923
|
+
<textarea id="drawer-edit-content" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700" rows="6">${escapeHtml(data.content)}</textarea>
|
|
924
|
+
<div class="flex items-center gap-3">
|
|
925
|
+
<label class="text-sm text-gray-600 dark:text-gray-400">Priority</label>
|
|
926
|
+
<input type="number" id="drawer-edit-importance" value="${data.importance}" min="1" max="5" class="w-20 p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
927
|
+
</div>
|
|
928
|
+
<div class="flex gap-2">
|
|
929
|
+
<button onclick="saveDrawerEdit('${data.id}')" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Save</button>
|
|
930
|
+
<button onclick="cancelDrawerEdit()" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded text-sm">Cancel</button>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function openDrawer(id) {
|
|
937
|
+
try {
|
|
938
|
+
currentDrawerMemoryId = id;
|
|
939
|
+
document.getElementById('drawerTitle').textContent = 'Loading...';
|
|
940
|
+
document.getElementById('drawerBody').innerHTML = `
|
|
941
|
+
<div class="space-y-4">
|
|
942
|
+
<div class="skeleton h-20 w-full"></div>
|
|
943
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
944
|
+
<div class="skeleton h-36 w-full"></div>
|
|
945
|
+
<div class="skeleton h-36 w-full"></div>
|
|
946
|
+
</div>
|
|
947
|
+
<div class="skeleton h-24 w-full"></div>
|
|
948
|
+
<div class="skeleton h-64 w-full"></div>
|
|
949
|
+
</div>
|
|
950
|
+
`;
|
|
951
|
+
document.getElementById('memoryDrawer').classList.remove('hidden');
|
|
952
|
+
document.body.classList.add('drawer-open');
|
|
953
|
+
const response = await fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`);
|
|
954
|
+
const data = await response.json();
|
|
955
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load memory');
|
|
956
|
+
document.getElementById('drawerTitle').textContent = getDisplayTitle(data);
|
|
957
|
+
document.getElementById('drawerBody').innerHTML = renderDetailPanel(data);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
closeDrawer();
|
|
960
|
+
showToast('Failed to load details: ' + err.message, 'error');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function closeDrawer() {
|
|
965
|
+
document.getElementById('memoryDrawer').classList.add('hidden');
|
|
966
|
+
if (!document.getElementById('repoSidebarDrawer') || document.getElementById('repoSidebarDrawer').classList.contains('hidden')) {
|
|
967
|
+
document.body.classList.remove('drawer-open');
|
|
968
|
+
}
|
|
969
|
+
activeEditMemoryId = null;
|
|
970
|
+
currentDrawerMemoryId = null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function startDrawerEdit(id) {
|
|
974
|
+
activeEditMemoryId = id;
|
|
975
|
+
openDrawer(id);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function cancelDrawerEdit() {
|
|
979
|
+
const id = currentDrawerMemoryId;
|
|
980
|
+
activeEditMemoryId = null;
|
|
981
|
+
if (id) {
|
|
982
|
+
openDrawer(id);
|
|
983
|
+
} else {
|
|
984
|
+
closeDrawer();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function saveDrawerEdit(id) {
|
|
989
|
+
const title = document.getElementById('drawer-edit-title').value.trim();
|
|
990
|
+
const content = document.getElementById('drawer-edit-content').value;
|
|
991
|
+
const importance = parseInt(document.getElementById('drawer-edit-importance').value, 10);
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
const response = await fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, {
|
|
995
|
+
method: 'PUT',
|
|
996
|
+
headers: { 'Content-Type': 'application/json' },
|
|
997
|
+
body: JSON.stringify({ title, content, importance })
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
if (!response.ok) {
|
|
1001
|
+
const data = await response.json();
|
|
1002
|
+
throw new Error(data.error || 'Failed to update');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
showToast('Memory updated successfully', 'success');
|
|
1006
|
+
activeEditMemoryId = null;
|
|
1007
|
+
await Promise.all([loadMemories(), loadStats(), loadRecentActions()]);
|
|
1008
|
+
await openDrawer(id);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
showToast('Failed to update: ' + err.message, 'error');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function startInlineEdit(id) {
|
|
1015
|
+
activeEditMemoryId = id;
|
|
1016
|
+
await openDrawer(id);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function saveInlineEdit(id) {
|
|
1020
|
+
await saveDrawerEdit(id);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function cancelInlineEdit(id) {
|
|
1024
|
+
cancelDrawerEdit();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function bulkUpdateImportance() {
|
|
1028
|
+
const importance = parseInt(document.getElementById('bulkImportanceSelect').value);
|
|
1029
|
+
const ids = Array.from(selectedIds);
|
|
1030
|
+
|
|
1031
|
+
try {
|
|
1032
|
+
await Promise.all(ids.map(id =>
|
|
1033
|
+
fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, {
|
|
1034
|
+
method: 'PUT',
|
|
1035
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1036
|
+
body: JSON.stringify({ importance })
|
|
1037
|
+
})
|
|
1038
|
+
));
|
|
1039
|
+
|
|
1040
|
+
showToast(`Updated ${ids.length} memories`, 'success');
|
|
1041
|
+
clearSelection();
|
|
1042
|
+
loadData();
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
showToast('Bulk update failed: ' + err.message, 'error');
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function showBulkDeleteConfirm() {
|
|
1049
|
+
if (!confirm(`Delete ${selectedIds.size} selected memories? This cannot be undone.`)) return;
|
|
1050
|
+
|
|
1051
|
+
const ids = Array.from(selectedIds);
|
|
1052
|
+
try {
|
|
1053
|
+
await Promise.all(ids.map(id =>
|
|
1054
|
+
fetch(`/api/memories/${id}?repo=${encodeURIComponent(currentRepo)}`, { method: 'DELETE' })
|
|
1055
|
+
));
|
|
1056
|
+
|
|
1057
|
+
showToast(`Deleted ${ids.length} memories`, 'success');
|
|
1058
|
+
clearSelection();
|
|
1059
|
+
loadData();
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
showToast('Bulk delete failed: ' + err.message, 'error');
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function showToast(message, type = 'info') {
|
|
1066
|
+
const container = document.getElementById('toastContainer');
|
|
1067
|
+
const toast = document.createElement('div');
|
|
1068
|
+
const colors = { success: 'bg-green-600', error: 'bg-red-600', info: 'bg-blue-600' };
|
|
1069
|
+
toast.className = `${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg max-w-xs`;
|
|
1070
|
+
toast.textContent = message;
|
|
1071
|
+
container.appendChild(toast);
|
|
1072
|
+
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
toast.classList.add('removing');
|
|
1075
|
+
setTimeout(() => toast.remove(), 300);
|
|
1076
|
+
}, 3000);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function exportData(format) {
|
|
1080
|
+
const data = currentMemories.map(m => ({
|
|
1081
|
+
id: m.id,
|
|
1082
|
+
type: m.type,
|
|
1083
|
+
content: m.content,
|
|
1084
|
+
importance: m.importance,
|
|
1085
|
+
hit_count: m.hit_count,
|
|
1086
|
+
created_at: m.created_at
|
|
1087
|
+
}));
|
|
1088
|
+
|
|
1089
|
+
let content, filename, type;
|
|
1090
|
+
if (format === 'json') {
|
|
1091
|
+
content = JSON.stringify(data, null, 2);
|
|
1092
|
+
filename = 'memories.json';
|
|
1093
|
+
type = 'application/json';
|
|
1094
|
+
} else {
|
|
1095
|
+
const headers = ['id', 'type', 'content', 'importance', 'hit_count', 'created_at'];
|
|
1096
|
+
content = [headers.join(','), ...data.map(m => headers.map(h => `"${(m[h] || '').toString().replace(/"/g, '""')}"`).join(','))].join('\n');
|
|
1097
|
+
filename = 'memories.csv';
|
|
1098
|
+
type = 'text/csv';
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const blob = new Blob([content], { type });
|
|
1102
|
+
const url = URL.createObjectURL(blob);
|
|
1103
|
+
const a = document.createElement('a');
|
|
1104
|
+
a.href = url;
|
|
1105
|
+
a.download = filename;
|
|
1106
|
+
a.click();
|
|
1107
|
+
URL.revokeObjectURL(url);
|
|
1108
|
+
showToast(`Exported as ${format.toUpperCase()}`, 'success');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function archiveExpired() {
|
|
1112
|
+
try {
|
|
1113
|
+
const response = await fetch('/api/archive-expired', { method: 'POST' });
|
|
1114
|
+
const data = await response.json();
|
|
1115
|
+
showToast(`Archived ${data.archived || 0} expired memories`, 'success');
|
|
1116
|
+
loadData();
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
showToast('Archive failed: ' + err.message, 'error');
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function loadData() {
|
|
1123
|
+
await loadRepos();
|
|
1124
|
+
await Promise.all([
|
|
1125
|
+
loadStats(),
|
|
1126
|
+
loadMemories(),
|
|
1127
|
+
checkStatus(),
|
|
1128
|
+
loadRecentActions(),
|
|
1129
|
+
]);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
document.getElementById('repoSearchInput').addEventListener('input', () => {
|
|
1133
|
+
renderRepoSidebar();
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
window.setCurrentRepo = setCurrentRepo;
|
|
1137
|
+
window.closeRepoSidebarDrawer = closeRepoSidebarDrawer;
|
|
1138
|
+
window.togglePinnedRepo = togglePinnedRepo;
|
|
1139
|
+
window.startPinnedRepoDrag = startPinnedRepoDrag;
|
|
1140
|
+
window.overPinnedRepoDrag = overPinnedRepoDrag;
|
|
1141
|
+
window.leavePinnedRepoDrag = leavePinnedRepoDrag;
|
|
1142
|
+
window.dropPinnedRepo = dropPinnedRepo;
|
|
1143
|
+
window.endPinnedRepoDrag = endPinnedRepoDrag;
|
|
1144
|
+
|
|
1145
|
+
document.getElementById('repoSearchInput').addEventListener('keydown', (e) => {
|
|
1146
|
+
if (e.key === 'Enter') {
|
|
1147
|
+
const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
|
|
1148
|
+
if (firstMatch) {
|
|
1149
|
+
setCurrentRepo(firstMatch.repo);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
document.getElementById('repoSearchInputMobile').addEventListener('input', () => {
|
|
1155
|
+
renderRepoSidebar();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
document.getElementById('repoSearchInputMobile').addEventListener('keydown', (e) => {
|
|
1159
|
+
if (e.key === 'Enter') {
|
|
1160
|
+
const firstMatch = availableRepos.find((item) => item.repo.toLowerCase().includes(e.target.value.trim().toLowerCase()));
|
|
1161
|
+
if (firstMatch) {
|
|
1162
|
+
setCurrentRepo(firstMatch.repo);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
document.getElementById('repoNavToggle').addEventListener('click', () => {
|
|
1168
|
+
openRepoSidebarDrawer();
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
document.getElementById('repoSidebarCollapseToggle').addEventListener('click', () => {
|
|
1172
|
+
toggleRepoSidebarCollapse();
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
document.getElementById('repoCollapsedSummaryButton').addEventListener('click', () => {
|
|
1176
|
+
if (isRepoSidebarCollapsed) {
|
|
1177
|
+
toggleRepoSidebarCollapse();
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
initTheme();
|
|
1182
|
+
initRepoSidebarState();
|
|
1183
|
+
initPinnedRepos();
|
|
1184
|
+
renderRecentActions();
|
|
1185
|
+
loadData();
|
|
1186
|
+
startCountdown();
|
|
1187
|
+
setInterval(checkStatus, 30000);
|