superlocalmemory 2.7.3 → 2.7.4
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/CHANGELOG.md +17 -0
- package/hooks/post-recall-hook.js +53 -0
- package/mcp_server.py +348 -17
- package/package.json +1 -1
- package/skills/slm-recall/SKILL.md +1 -0
- package/src/auto_backup.py +64 -31
- package/src/learning/adaptive_ranker.py +70 -1
- package/src/learning/feature_extractor.py +71 -17
- package/src/learning/feedback_collector.py +114 -0
- package/src/learning/learning_db.py +158 -34
- package/src/learning/tests/test_adaptive_ranker.py +5 -4
- package/src/learning/tests/test_aggregator.py +4 -3
- package/src/learning/tests/test_feedback_collector.py +7 -4
- package/src/learning/tests/test_signal_inference.py +399 -0
- package/src/learning/tests/test_synthetic_bootstrap.py +1 -1
- package/ui/index.html +38 -0
- package/ui/js/feedback.js +333 -0
- package/ui/js/learning.js +117 -0
- package/ui/js/modal.js +22 -1
- package/ui/js/profiles.js +8 -0
- package/ui/js/settings.js +58 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuperLocalMemory V2 - Feedback Module (v2.7.4)
|
|
3
|
+
* Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
4
|
+
* Licensed under MIT License
|
|
5
|
+
*
|
|
6
|
+
* Collects implicit and explicit feedback signals from dashboard
|
|
7
|
+
* interactions. All data stays 100% local.
|
|
8
|
+
*
|
|
9
|
+
* Signals:
|
|
10
|
+
* thumbs_up - User clicks thumbs up on a memory card
|
|
11
|
+
* thumbs_down - User clicks thumbs down on a memory card
|
|
12
|
+
* pin - User pins/bookmarks a memory
|
|
13
|
+
* dwell_time - Time spent viewing memory detail modal
|
|
14
|
+
* search_click - User clicks a search result (positive for clicked)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Module state
|
|
18
|
+
var feedbackState = {
|
|
19
|
+
lastSearchQuery: '',
|
|
20
|
+
modalOpenTime: null,
|
|
21
|
+
modalMemoryId: null,
|
|
22
|
+
searchResultIds: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Record explicit feedback (thumbs up/down, pin).
|
|
27
|
+
* Sends POST /api/feedback to backend.
|
|
28
|
+
*/
|
|
29
|
+
function recordFeedback(memoryId, feedbackType, query) {
|
|
30
|
+
if (!memoryId || !feedbackType) return;
|
|
31
|
+
|
|
32
|
+
fetch('/api/feedback', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {'Content-Type': 'application/json'},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
memory_id: memoryId,
|
|
37
|
+
feedback_type: feedbackType,
|
|
38
|
+
query: query || feedbackState.lastSearchQuery || '',
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
.then(function(r) { return r.json(); })
|
|
42
|
+
.then(function(data) {
|
|
43
|
+
if (data.success) {
|
|
44
|
+
showFeedbackToast(feedbackType, memoryId);
|
|
45
|
+
refreshFeedbackStats();
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.catch(function() {
|
|
49
|
+
// Silent failure — feedback should never break UI
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start tracking dwell time when a memory detail modal opens.
|
|
55
|
+
*/
|
|
56
|
+
function startDwellTracking(memoryId) {
|
|
57
|
+
feedbackState.modalOpenTime = Date.now();
|
|
58
|
+
feedbackState.modalMemoryId = memoryId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop tracking and record dwell time when modal closes.
|
|
63
|
+
*/
|
|
64
|
+
function stopDwellTracking() {
|
|
65
|
+
if (!feedbackState.modalOpenTime || !feedbackState.modalMemoryId) return;
|
|
66
|
+
|
|
67
|
+
var dwellMs = Date.now() - feedbackState.modalOpenTime;
|
|
68
|
+
var dwellSeconds = dwellMs / 1000;
|
|
69
|
+
var memId = feedbackState.modalMemoryId;
|
|
70
|
+
|
|
71
|
+
feedbackState.modalOpenTime = null;
|
|
72
|
+
feedbackState.modalMemoryId = null;
|
|
73
|
+
|
|
74
|
+
// Only record if meaningful (>1s)
|
|
75
|
+
if (dwellSeconds < 1.0) return;
|
|
76
|
+
|
|
77
|
+
fetch('/api/feedback/dwell', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {'Content-Type': 'application/json'},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
memory_id: memId,
|
|
82
|
+
dwell_time: dwellSeconds,
|
|
83
|
+
query: feedbackState.lastSearchQuery || '',
|
|
84
|
+
}),
|
|
85
|
+
})
|
|
86
|
+
.then(function(r) { return r.json(); })
|
|
87
|
+
.catch(function() {
|
|
88
|
+
// Silent failure
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Track which search results are displayed.
|
|
94
|
+
*/
|
|
95
|
+
function trackSearchResults(query, resultIds) {
|
|
96
|
+
feedbackState.lastSearchQuery = query;
|
|
97
|
+
feedbackState.searchResultIds = resultIds || [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create feedback buttons (thumbs up/down + pin) for a memory card.
|
|
102
|
+
* Returns a DOM element containing the buttons.
|
|
103
|
+
* Uses safe DOM methods (no innerHTML with user data).
|
|
104
|
+
*/
|
|
105
|
+
function createFeedbackButtons(memoryId, query) {
|
|
106
|
+
var container = document.createElement('div');
|
|
107
|
+
container.className = 'feedback-buttons d-flex gap-1 align-items-center';
|
|
108
|
+
container.setAttribute('role', 'group');
|
|
109
|
+
container.setAttribute('aria-label', 'Feedback for memory ' + memoryId);
|
|
110
|
+
|
|
111
|
+
// Thumbs up
|
|
112
|
+
var upBtn = document.createElement('button');
|
|
113
|
+
upBtn.className = 'btn btn-outline-success btn-sm feedback-btn';
|
|
114
|
+
var upIcon = document.createElement('i');
|
|
115
|
+
upIcon.className = 'bi bi-hand-thumbs-up';
|
|
116
|
+
upBtn.appendChild(upIcon);
|
|
117
|
+
upBtn.title = 'This memory was useful';
|
|
118
|
+
upBtn.setAttribute('aria-label', 'Mark as useful');
|
|
119
|
+
upBtn.onclick = function(e) {
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
recordFeedback(memoryId, 'thumbs_up', query);
|
|
122
|
+
upBtn.classList.remove('btn-outline-success');
|
|
123
|
+
upBtn.classList.add('btn-success');
|
|
124
|
+
upBtn.disabled = true;
|
|
125
|
+
downBtn.disabled = true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Thumbs down
|
|
129
|
+
var downBtn = document.createElement('button');
|
|
130
|
+
downBtn.className = 'btn btn-outline-danger btn-sm feedback-btn';
|
|
131
|
+
var downIcon = document.createElement('i');
|
|
132
|
+
downIcon.className = 'bi bi-hand-thumbs-down';
|
|
133
|
+
downBtn.appendChild(downIcon);
|
|
134
|
+
downBtn.title = 'Not useful';
|
|
135
|
+
downBtn.setAttribute('aria-label', 'Mark as not useful');
|
|
136
|
+
downBtn.onclick = function(e) {
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
recordFeedback(memoryId, 'thumbs_down', query);
|
|
139
|
+
downBtn.classList.remove('btn-outline-danger');
|
|
140
|
+
downBtn.classList.add('btn-danger');
|
|
141
|
+
upBtn.disabled = true;
|
|
142
|
+
downBtn.disabled = true;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Pin/Bookmark
|
|
146
|
+
var pinBtn = document.createElement('button');
|
|
147
|
+
pinBtn.className = 'btn btn-outline-warning btn-sm feedback-btn';
|
|
148
|
+
var pinIcon = document.createElement('i');
|
|
149
|
+
pinIcon.className = 'bi bi-pin-angle';
|
|
150
|
+
pinBtn.appendChild(pinIcon);
|
|
151
|
+
pinBtn.title = 'Pin this memory';
|
|
152
|
+
pinBtn.setAttribute('aria-label', 'Pin memory');
|
|
153
|
+
pinBtn.onclick = function(e) {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
recordFeedback(memoryId, 'pin', query);
|
|
156
|
+
pinBtn.classList.remove('btn-outline-warning');
|
|
157
|
+
pinBtn.classList.add('btn-warning');
|
|
158
|
+
pinBtn.disabled = true;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
container.appendChild(upBtn);
|
|
162
|
+
container.appendChild(downBtn);
|
|
163
|
+
container.appendChild(pinBtn);
|
|
164
|
+
return container;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Show a small toast notification after feedback.
|
|
169
|
+
*/
|
|
170
|
+
function showFeedbackToast(type, memoryId) {
|
|
171
|
+
var messages = {
|
|
172
|
+
thumbs_up: 'Thanks! This helps improve future results.',
|
|
173
|
+
thumbs_down: 'Noted. Results will improve over time.',
|
|
174
|
+
pin: 'Memory pinned!',
|
|
175
|
+
};
|
|
176
|
+
var msg = messages[type] || 'Feedback recorded';
|
|
177
|
+
|
|
178
|
+
// Use existing showToast if available
|
|
179
|
+
if (typeof showToast === 'function') {
|
|
180
|
+
showToast(msg, 'success');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Fetch and render feedback stats (progress bar, signal count).
|
|
186
|
+
*/
|
|
187
|
+
function refreshFeedbackStats() {
|
|
188
|
+
fetch('/api/feedback/stats')
|
|
189
|
+
.then(function(r) { return r.json(); })
|
|
190
|
+
.then(function(data) {
|
|
191
|
+
renderFeedbackProgress(data);
|
|
192
|
+
})
|
|
193
|
+
.catch(function() {});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render the feedback progress bar using safe DOM methods.
|
|
198
|
+
*/
|
|
199
|
+
function renderFeedbackProgress(stats) {
|
|
200
|
+
var container = document.getElementById('feedback-progress');
|
|
201
|
+
if (!container) return;
|
|
202
|
+
|
|
203
|
+
var total = stats.total_signals || 0;
|
|
204
|
+
var phase = stats.ranking_phase || 'baseline';
|
|
205
|
+
var progress = stats.progress || 0;
|
|
206
|
+
var target = stats.target || 200;
|
|
207
|
+
|
|
208
|
+
var phaseLabels = {
|
|
209
|
+
baseline: 'Baseline (collecting data)',
|
|
210
|
+
rule_based: 'Rule-Based (learning your preferences)',
|
|
211
|
+
ml_model: 'ML-Powered (fully personalized)',
|
|
212
|
+
};
|
|
213
|
+
var phaseColors = {
|
|
214
|
+
baseline: 'bg-secondary',
|
|
215
|
+
rule_based: 'bg-info',
|
|
216
|
+
ml_model: 'bg-success',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Clear container
|
|
220
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
221
|
+
|
|
222
|
+
// Header row
|
|
223
|
+
var headerRow = document.createElement('div');
|
|
224
|
+
headerRow.className = 'd-flex justify-content-between align-items-center mb-1';
|
|
225
|
+
|
|
226
|
+
var label = document.createElement('small');
|
|
227
|
+
label.className = 'text-muted';
|
|
228
|
+
var icon = document.createElement('i');
|
|
229
|
+
icon.className = 'bi bi-graph-up me-1';
|
|
230
|
+
label.appendChild(icon);
|
|
231
|
+
label.appendChild(document.createTextNode('Learning Progress'));
|
|
232
|
+
|
|
233
|
+
var badge = document.createElement('span');
|
|
234
|
+
badge.className = 'badge ' + (phaseColors[phase] || 'bg-secondary');
|
|
235
|
+
badge.textContent = phaseLabels[phase] || phase;
|
|
236
|
+
|
|
237
|
+
headerRow.appendChild(label);
|
|
238
|
+
headerRow.appendChild(badge);
|
|
239
|
+
container.appendChild(headerRow);
|
|
240
|
+
|
|
241
|
+
// Progress bar
|
|
242
|
+
var progressOuter = document.createElement('div');
|
|
243
|
+
progressOuter.className = 'progress';
|
|
244
|
+
progressOuter.style.height = '8px';
|
|
245
|
+
|
|
246
|
+
var progressBar = document.createElement('div');
|
|
247
|
+
progressBar.className = 'progress-bar ' + (phaseColors[phase] || 'bg-secondary');
|
|
248
|
+
progressBar.setAttribute('role', 'progressbar');
|
|
249
|
+
progressBar.style.width = progress + '%';
|
|
250
|
+
progressBar.setAttribute('aria-valuenow', String(total));
|
|
251
|
+
progressBar.setAttribute('aria-valuemin', '0');
|
|
252
|
+
progressBar.setAttribute('aria-valuemax', String(target));
|
|
253
|
+
|
|
254
|
+
progressOuter.appendChild(progressBar);
|
|
255
|
+
container.appendChild(progressOuter);
|
|
256
|
+
|
|
257
|
+
// Count text
|
|
258
|
+
var countText = document.createElement('small');
|
|
259
|
+
countText.className = 'text-muted';
|
|
260
|
+
countText.textContent = total + '/' + target + ' signals';
|
|
261
|
+
container.appendChild(countText);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create the privacy notice banner using safe DOM methods.
|
|
266
|
+
*/
|
|
267
|
+
function createPrivacyNotice() {
|
|
268
|
+
var container = document.getElementById('privacy-notice');
|
|
269
|
+
if (!container) return;
|
|
270
|
+
|
|
271
|
+
var alert = document.createElement('div');
|
|
272
|
+
alert.className = 'alert alert-light border d-flex align-items-center py-2 mb-3';
|
|
273
|
+
alert.setAttribute('role', 'alert');
|
|
274
|
+
|
|
275
|
+
var lockIcon = document.createElement('i');
|
|
276
|
+
lockIcon.className = 'bi bi-shield-lock me-2 text-success';
|
|
277
|
+
|
|
278
|
+
var text = document.createElement('small');
|
|
279
|
+
text.appendChild(document.createTextNode('Learning is 100% local. Your behavioral data never leaves this machine. '));
|
|
280
|
+
|
|
281
|
+
var link = document.createElement('a');
|
|
282
|
+
link.href = '#';
|
|
283
|
+
link.className = 'ms-1';
|
|
284
|
+
link.textContent = 'Learn more';
|
|
285
|
+
link.onclick = function(e) {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
showPrivacyDetails();
|
|
288
|
+
};
|
|
289
|
+
text.appendChild(link);
|
|
290
|
+
|
|
291
|
+
alert.appendChild(lockIcon);
|
|
292
|
+
alert.appendChild(text);
|
|
293
|
+
container.appendChild(alert);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Show privacy details in an alert (safe, no raw HTML injection).
|
|
298
|
+
*/
|
|
299
|
+
function showPrivacyDetails() {
|
|
300
|
+
var info = 'What data is collected:\n' +
|
|
301
|
+
'- Which memories you find useful (thumbs up/down)\n' +
|
|
302
|
+
'- Time spent viewing memory details\n' +
|
|
303
|
+
'- Search patterns (queries are hashed, never stored as raw text)\n\n' +
|
|
304
|
+
'Where it is stored:\n' +
|
|
305
|
+
'~/.claude-memory/learning.db (local SQLite file)\n\n' +
|
|
306
|
+
'How to delete it:\n' +
|
|
307
|
+
'Use "Reset Learning Data" in Settings, or run:\n' +
|
|
308
|
+
'rm ~/.claude-memory/learning.db';
|
|
309
|
+
alert(info);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reset all learning data.
|
|
314
|
+
*/
|
|
315
|
+
function resetLearningData() {
|
|
316
|
+
if (!confirm('Reset all learning data? Your memories will be preserved.')) return;
|
|
317
|
+
|
|
318
|
+
fetch('/api/learning/reset', {method: 'POST'})
|
|
319
|
+
.then(function(r) { return r.json(); })
|
|
320
|
+
.then(function(data) {
|
|
321
|
+
if (data.success) {
|
|
322
|
+
if (typeof showToast === 'function') showToast('Learning data reset', 'success');
|
|
323
|
+
refreshFeedbackStats();
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
.catch(function() {});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Initialize: load stats on page ready
|
|
330
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
331
|
+
createPrivacyNotice();
|
|
332
|
+
refreshFeedbackStats();
|
|
333
|
+
});
|
package/ui/js/learning.js
CHANGED
|
@@ -24,12 +24,129 @@ function renderLearningStatus(data) {
|
|
|
24
24
|
renderFeedbackCount(data.stats);
|
|
25
25
|
renderEngagementHealth(data.engagement);
|
|
26
26
|
renderProgressBar(data.stats ? data.stats.feedback_count : 0);
|
|
27
|
+
renderWhatWeLearned(data);
|
|
27
28
|
renderTechPreferences(data.tech_preferences || []);
|
|
28
29
|
renderWorkflowPatterns(data.workflow_patterns || []);
|
|
29
30
|
renderSourceQuality(data.source_scores || {});
|
|
30
31
|
renderPrivacyStats(data.stats);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function renderWhatWeLearned(data) {
|
|
35
|
+
var container = document.getElementById('what-we-learned-content');
|
|
36
|
+
var profileBadge = document.getElementById('learned-profile-badge');
|
|
37
|
+
if (!container) return;
|
|
38
|
+
container.textContent = '';
|
|
39
|
+
|
|
40
|
+
// Show active profile
|
|
41
|
+
if (profileBadge && data.stats && data.stats.active_profile) {
|
|
42
|
+
profileBadge.textContent = data.stats.active_profile;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var insights = [];
|
|
46
|
+
|
|
47
|
+
// Collect tech preference insights
|
|
48
|
+
var techPrefs = data.tech_preferences || [];
|
|
49
|
+
var highConfTech = techPrefs.filter(function(p) { return p.confidence >= 0.6; });
|
|
50
|
+
if (highConfTech.length > 0) {
|
|
51
|
+
var techNames = highConfTech.map(function(p) { return p.value; }).slice(0, 5);
|
|
52
|
+
insights.push({
|
|
53
|
+
icon: 'bi-cpu',
|
|
54
|
+
color: 'text-primary',
|
|
55
|
+
text: 'You prefer: ' + techNames.join(', '),
|
|
56
|
+
detail: highConfTech.length + ' tech preferences learned',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Collect workflow insights
|
|
61
|
+
var workflows = data.workflow_patterns || [];
|
|
62
|
+
var sequences = workflows.filter(function(w) { return w.type === 'sequence'; });
|
|
63
|
+
var temporal = workflows.filter(function(w) { return w.type === 'temporal'; });
|
|
64
|
+
if (sequences.length > 0) {
|
|
65
|
+
insights.push({
|
|
66
|
+
icon: 'bi-diagram-3',
|
|
67
|
+
color: 'text-info',
|
|
68
|
+
text: sequences.length + ' workflow sequence' + (sequences.length > 1 ? 's' : '') + ' detected',
|
|
69
|
+
detail: 'Common patterns in how you work',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (temporal.length > 0) {
|
|
73
|
+
insights.push({
|
|
74
|
+
icon: 'bi-clock',
|
|
75
|
+
color: 'text-warning',
|
|
76
|
+
text: temporal.length + ' time-based pattern' + (temporal.length > 1 ? 's' : '') + ' found',
|
|
77
|
+
detail: 'When you tend to work on what',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Source quality insights
|
|
82
|
+
var sources = data.source_scores || {};
|
|
83
|
+
var sourceCount = Object.keys(sources).length;
|
|
84
|
+
if (sourceCount > 0) {
|
|
85
|
+
var bestSource = Object.entries(sources).sort(function(a, b) { return b[1] - a[1]; })[0];
|
|
86
|
+
insights.push({
|
|
87
|
+
icon: 'bi-trophy',
|
|
88
|
+
color: 'text-success',
|
|
89
|
+
text: 'Best source: ' + bestSource[0] + ' (' + Math.round(bestSource[1] * 100) + '% quality)',
|
|
90
|
+
detail: sourceCount + ' sources tracked',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Feedback volume insight
|
|
95
|
+
var feedbackCount = (data.stats && data.stats.feedback_count) || 0;
|
|
96
|
+
if (feedbackCount > 0) {
|
|
97
|
+
var phase = data.ranking_phase || 'baseline';
|
|
98
|
+
var phaseLabel = {baseline: 'collecting data', rule_based: 'learning your preferences', ml_model: 'fully personalized'}[phase] || phase;
|
|
99
|
+
insights.push({
|
|
100
|
+
icon: 'bi-graph-up',
|
|
101
|
+
color: 'text-success',
|
|
102
|
+
text: feedbackCount + ' feedback signals collected',
|
|
103
|
+
detail: 'Currently ' + phaseLabel,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Render insights
|
|
108
|
+
if (insights.length === 0) {
|
|
109
|
+
var empty = document.createElement('div');
|
|
110
|
+
empty.className = 'text-center text-muted py-3';
|
|
111
|
+
var emptyIcon = document.createElement('i');
|
|
112
|
+
emptyIcon.className = 'bi bi-lightbulb';
|
|
113
|
+
emptyIcon.style.fontSize = '2rem';
|
|
114
|
+
empty.appendChild(emptyIcon);
|
|
115
|
+
var emptyText = document.createElement('p');
|
|
116
|
+
emptyText.className = 'mt-2 mb-0 small';
|
|
117
|
+
emptyText.textContent = 'Start using recall and giving feedback. SuperLocalMemory will learn your preferences automatically.';
|
|
118
|
+
empty.appendChild(emptyText);
|
|
119
|
+
container.appendChild(empty);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (var i = 0; i < insights.length; i++) {
|
|
124
|
+
var insight = insights[i];
|
|
125
|
+
var row = document.createElement('div');
|
|
126
|
+
row.className = 'd-flex align-items-start mb-2 p-2 rounded';
|
|
127
|
+
row.style.backgroundColor = 'var(--bs-body-bg)';
|
|
128
|
+
|
|
129
|
+
var icon = document.createElement('i');
|
|
130
|
+
icon.className = 'bi ' + insight.icon + ' ' + insight.color + ' me-3';
|
|
131
|
+
icon.style.fontSize = '1.3rem';
|
|
132
|
+
icon.style.marginTop = '2px';
|
|
133
|
+
|
|
134
|
+
var textDiv = document.createElement('div');
|
|
135
|
+
var mainText = document.createElement('div');
|
|
136
|
+
mainText.className = 'fw-semibold';
|
|
137
|
+
mainText.textContent = insight.text;
|
|
138
|
+
var detailText = document.createElement('small');
|
|
139
|
+
detailText.className = 'text-muted';
|
|
140
|
+
detailText.textContent = insight.detail;
|
|
141
|
+
textDiv.appendChild(mainText);
|
|
142
|
+
textDiv.appendChild(detailText);
|
|
143
|
+
|
|
144
|
+
row.appendChild(icon);
|
|
145
|
+
row.appendChild(textDiv);
|
|
146
|
+
container.appendChild(row);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
33
150
|
function renderPhase(data) {
|
|
34
151
|
var phaseEl = document.getElementById('learning-phase');
|
|
35
152
|
var phaseDetail = document.getElementById('learning-phase-detail');
|
package/ui/js/modal.js
CHANGED
|
@@ -142,9 +142,26 @@ function openMemoryDetail(mem) {
|
|
|
142
142
|
body.appendChild(actionsDiv);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// v2.7.4: Add feedback buttons to modal body
|
|
146
|
+
if (typeof createFeedbackButtons === 'function' && mem && mem.id) {
|
|
147
|
+
var feedbackDiv = document.createElement('div');
|
|
148
|
+
feedbackDiv.className = 'mt-3 pt-2 border-top';
|
|
149
|
+
var feedbackLabel = document.createElement('small');
|
|
150
|
+
feedbackLabel.className = 'text-muted d-block mb-1';
|
|
151
|
+
feedbackLabel.textContent = 'Was this memory useful?';
|
|
152
|
+
feedbackDiv.appendChild(feedbackLabel);
|
|
153
|
+
feedbackDiv.appendChild(createFeedbackButtons(mem.id));
|
|
154
|
+
body.appendChild(feedbackDiv);
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
var modalEl = document.getElementById('memoryDetailModal');
|
|
146
158
|
var modal = new bootstrap.Modal(modalEl);
|
|
147
159
|
|
|
160
|
+
// v2.7.4: Start dwell time tracking
|
|
161
|
+
if (typeof startDwellTracking === 'function' && mem && mem.id) {
|
|
162
|
+
startDwellTracking(mem.id);
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
// Focus first interactive element when modal opens
|
|
149
166
|
modalEl.addEventListener('shown.bs.modal', function() {
|
|
150
167
|
const firstButton = modalEl.querySelector('button, a[href]');
|
|
@@ -153,8 +170,12 @@ function openMemoryDetail(mem) {
|
|
|
153
170
|
}
|
|
154
171
|
}, { once: true });
|
|
155
172
|
|
|
156
|
-
// Return focus when modal closes
|
|
173
|
+
// Return focus when modal closes + stop dwell tracking
|
|
157
174
|
modalEl.addEventListener('hidden.bs.modal', function() {
|
|
175
|
+
// v2.7.4: Stop dwell time tracking
|
|
176
|
+
if (typeof stopDwellTracking === 'function') {
|
|
177
|
+
stopDwellTracking();
|
|
178
|
+
}
|
|
158
179
|
if (window.lastFocusedElement && typeof window.lastFocusedElement.focus === 'function') {
|
|
159
180
|
window.lastFocusedElement.focus();
|
|
160
181
|
window.lastFocusedElement = null;
|
package/ui/js/profiles.js
CHANGED
|
@@ -190,6 +190,14 @@ async function switchProfile(profileName) {
|
|
|
190
190
|
loadStats();
|
|
191
191
|
loadGraph();
|
|
192
192
|
loadProfilesTable();
|
|
193
|
+
// v2.7.4: Reload ALL tabs for new profile
|
|
194
|
+
if (typeof loadLearning === 'function') loadLearning();
|
|
195
|
+
if (typeof refreshFeedbackStats === 'function') refreshFeedbackStats();
|
|
196
|
+
if (typeof loadLearningDataStats === 'function') loadLearningDataStats();
|
|
197
|
+
if (typeof loadAgents === 'function') loadAgents();
|
|
198
|
+
if (typeof loadMemories === 'function') loadMemories();
|
|
199
|
+
if (typeof loadTimeline === 'function') loadTimeline();
|
|
200
|
+
if (typeof loadEvents === 'function') loadEvents();
|
|
193
201
|
var activeTab = document.querySelector('#mainTabs .nav-link.active');
|
|
194
202
|
if (activeTab) activeTab.click();
|
|
195
203
|
} else {
|
package/ui/js/settings.js
CHANGED
|
@@ -5,6 +5,61 @@ async function loadSettings() {
|
|
|
5
5
|
loadProfilesTable();
|
|
6
6
|
loadBackupStatus();
|
|
7
7
|
loadBackupList();
|
|
8
|
+
loadLearningDataStats();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function loadLearningDataStats() {
|
|
12
|
+
try {
|
|
13
|
+
var response = await fetch('/api/feedback/stats');
|
|
14
|
+
var data = await response.json();
|
|
15
|
+
var container = document.getElementById('learning-data-stats');
|
|
16
|
+
if (!container) return;
|
|
17
|
+
|
|
18
|
+
container.textContent = '';
|
|
19
|
+
var row = document.createElement('div');
|
|
20
|
+
row.className = 'row g-2';
|
|
21
|
+
|
|
22
|
+
var stats = [
|
|
23
|
+
{ value: String(data.total_signals || 0), label: 'Feedback Signals' },
|
|
24
|
+
{ value: data.ranking_phase || 'baseline', label: 'Ranking Phase' },
|
|
25
|
+
{ value: Math.round(data.progress || 0) + '%', label: 'ML Progress' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
stats.forEach(function(s) {
|
|
29
|
+
var col = document.createElement('div');
|
|
30
|
+
col.className = 'col-4';
|
|
31
|
+
var stat = document.createElement('div');
|
|
32
|
+
stat.className = 'backup-stat';
|
|
33
|
+
var val = document.createElement('div');
|
|
34
|
+
val.className = 'value';
|
|
35
|
+
val.textContent = s.value;
|
|
36
|
+
var lbl = document.createElement('div');
|
|
37
|
+
lbl.className = 'label';
|
|
38
|
+
lbl.textContent = s.label;
|
|
39
|
+
stat.appendChild(val);
|
|
40
|
+
stat.appendChild(lbl);
|
|
41
|
+
col.appendChild(stat);
|
|
42
|
+
row.appendChild(col);
|
|
43
|
+
});
|
|
44
|
+
container.appendChild(row);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Silent — learning stats are optional
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function backupLearningDb() {
|
|
51
|
+
try {
|
|
52
|
+
var response = await fetch('/api/learning/backup', { method: 'POST' });
|
|
53
|
+
var data = await response.json();
|
|
54
|
+
if (data.success) {
|
|
55
|
+
showToast('Learning DB backed up: ' + (data.filename || 'learning.db.bak'));
|
|
56
|
+
} else {
|
|
57
|
+
showToast('Backup created at ~/.claude-memory/learning.db.bak');
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Fallback: just tell user the manual path
|
|
61
|
+
showToast('Manual backup: cp ~/.claude-memory/learning.db ~/.claude-memory/learning.db.bak');
|
|
62
|
+
}
|
|
8
63
|
}
|
|
9
64
|
|
|
10
65
|
async function loadBackupStatus() {
|
|
@@ -92,7 +147,9 @@ async function saveBackupConfig() {
|
|
|
92
147
|
})
|
|
93
148
|
});
|
|
94
149
|
var data = await response.json();
|
|
95
|
-
|
|
150
|
+
// API wraps status inside data.status on configure response
|
|
151
|
+
var status = data.status || data;
|
|
152
|
+
renderBackupStatus(status);
|
|
96
153
|
showToast('Backup settings saved');
|
|
97
154
|
} catch (error) {
|
|
98
155
|
console.error('Error saving backup config:', error);
|