superlocalmemory 3.0.17 → 3.0.19

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/ui/js/memories.js CHANGED
@@ -55,11 +55,14 @@ function renderMemoriesTable(memories, showScores) {
55
55
  + '</div></div></td>';
56
56
  }
57
57
 
58
+ var memId = mem.memory_id || mem.id;
59
+ var expandBtnHtml = '<button class="btn btn-sm btn-outline-secondary expand-facts-btn ms-1" data-memory-id="' + escapeHtml(String(memId)) + '" title="View atomic facts">&#9660;</button>';
60
+
58
61
  rows += '<tr data-mem-idx="' + idx + '">'
59
62
  + '<td>' + escapeHtml(String(mem.id)) + '</td>'
60
63
  + '<td><span class="badge bg-primary">' + escapeHtml(mem.category || 'None') + '</span></td>'
61
64
  + '<td><small>' + escapeHtml(mem.project_name || '-') + '</small></td>'
62
- + '<td class="memory-content" title="' + escapeHtml(content) + '">' + escapeHtml(contentPreview) + '</td>'
65
+ + '<td class="memory-content" title="' + escapeHtml(content) + '">' + escapeHtml(contentPreview) + expandBtnHtml + '</td>'
63
66
  + scoreCell
64
67
  + '<td><span class="badge bg-' + importanceClass + ' badge-importance">' + escapeHtml(String(importance)) + '</span></td>'
65
68
  + '<td>' + escapeHtml(String(mem.cluster_id || '-')) + '</td>'
@@ -87,8 +90,18 @@ function renderMemoriesTable(memories, showScores) {
87
90
  table.addEventListener('click', function(e) {
88
91
  var th = e.target.closest('th.sortable');
89
92
  if (th) { handleSort(th); return; }
93
+
94
+ // Expand facts button
95
+ var expandBtn = e.target.closest('.expand-facts-btn');
96
+ if (expandBtn) {
97
+ e.stopPropagation();
98
+ var memId = expandBtn.getAttribute('data-memory-id');
99
+ toggleFactsExpansion(expandBtn, memId);
100
+ return;
101
+ }
102
+
90
103
  var row = e.target.closest('tr[data-mem-idx]');
91
- if (row) {
104
+ if (row && !e.target.closest('.expand-facts-btn')) {
92
105
  var idx = parseInt(row.getAttribute('data-mem-idx'), 10);
93
106
  if (window._slmMemories && window._slmMemories[idx]) {
94
107
  openMemoryDetail(window._slmMemories[idx]);
@@ -169,6 +182,56 @@ function scrollToMemory(memoryId) {
169
182
  scrollToMemoryInTable(memoryId);
170
183
  }
171
184
 
185
+ async function toggleFactsExpansion(btn, memoryId) {
186
+ var row = btn.closest('tr');
187
+ if (!row) return;
188
+ var existingExpansion = row.nextElementSibling;
189
+ if (existingExpansion && existingExpansion.classList.contains('facts-expansion-row')) {
190
+ existingExpansion.remove();
191
+ btn.innerHTML = '&#9660;';
192
+ return;
193
+ }
194
+
195
+ btn.innerHTML = '&#8987;';
196
+ try {
197
+ var resp = await fetch('/api/memories/' + encodeURIComponent(memoryId) + '/facts');
198
+ var data = await resp.json();
199
+ var expRow = document.createElement('tr');
200
+ expRow.className = 'facts-expansion-row';
201
+ var expCell = document.createElement('td');
202
+ expCell.colSpan = 8;
203
+ expCell.style.cssText = 'background:#f8f9fa; padding:8px 16px;';
204
+
205
+ if (data.facts && data.facts.length > 0) {
206
+ var label = document.createElement('small');
207
+ label.className = 'text-muted';
208
+ label.textContent = data.fact_count + ' atomic facts:';
209
+ expCell.appendChild(label);
210
+
211
+ data.facts.forEach(function(f, i) {
212
+ var fDiv = document.createElement('div');
213
+ fDiv.className = 'small py-1' + (i < data.facts.length - 1 ? ' border-bottom' : '');
214
+ var badge = document.createElement('span');
215
+ badge.className = 'badge bg-secondary me-1';
216
+ badge.style.fontSize = '0.65rem';
217
+ badge.textContent = f.fact_type;
218
+ fDiv.appendChild(badge);
219
+ fDiv.appendChild(document.createTextNode(f.content.substring(0, 200)));
220
+ expCell.appendChild(fDiv);
221
+ });
222
+ } else {
223
+ expCell.textContent = 'No atomic facts found for this memory.';
224
+ }
225
+
226
+ expRow.appendChild(expCell);
227
+ row.parentNode.insertBefore(expRow, row.nextSibling);
228
+ btn.innerHTML = '&#9650;';
229
+ } catch (err) {
230
+ btn.innerHTML = '&#9660;';
231
+ console.error('Failed to load facts:', err);
232
+ }
233
+ }
234
+
172
235
  function scrollToMemoryInTable(memoryId) {
173
236
  const memId = String(memoryId);
174
237
 
package/ui/js/modal.js CHANGED
@@ -6,8 +6,11 @@
6
6
 
7
7
  var currentMemoryDetail = null;
8
8
 
9
- function openMemoryDetail(mem) {
9
+ function openMemoryDetail(mem, source) {
10
+ // source: 'graph', 'recall', 'memories', or undefined
10
11
  currentMemoryDetail = mem;
12
+ var fromGraph = source === 'graph';
13
+ var fromRecall = source === 'recall';
11
14
  var body = document.getElementById('memory-detail-body');
12
15
  if (!mem) {
13
16
  body.textContent = 'No memory data';
@@ -62,55 +65,89 @@ function openMemoryDetail(mem) {
62
65
 
63
66
  body.appendChild(dl);
64
67
 
65
- // Graph action buttons (v2.6.5)
66
- if (mem.cluster_id || mem.id) {
68
+ // Context-aware action buttons
69
+ if (mem.id) {
67
70
  body.appendChild(document.createElement('hr'));
68
71
 
69
72
  var actionsDiv = document.createElement('div');
70
73
  actionsDiv.className = 'memory-detail-graph-actions';
71
74
  actionsDiv.style.cssText = 'display:flex; gap:10px; flex-wrap:wrap;';
72
75
 
73
- // Button 1: View Full Memory (navigate to Memories tab)
74
- var viewBtn = document.createElement('button');
75
- viewBtn.className = 'btn btn-primary btn-sm';
76
- var viewIcon = document.createElement('i');
77
- viewIcon.className = 'bi bi-journal-text';
78
- viewBtn.appendChild(viewIcon);
79
- viewBtn.appendChild(document.createTextNode(' View Full Memory'));
80
- viewBtn.onclick = function() {
81
- modal.hide();
82
- if (typeof navigateToMemoryTab === 'function') {
83
- navigateToMemoryTab(mem.id);
84
- } else {
85
- // Fallback: just switch tab
86
- const memoriesTab = document.querySelector('a[href="#memories"]');
87
- if (memoriesTab) memoriesTab.click();
88
- }
89
- };
90
- actionsDiv.appendChild(viewBtn);
91
-
92
- // Button 2: Expand Neighbors (show connected nodes in graph)
93
- var expandBtn = document.createElement('button');
94
- expandBtn.className = 'btn btn-outline-secondary btn-sm';
95
- var expandIcon = document.createElement('i');
96
- expandIcon.className = 'bi bi-diagram-3';
97
- expandBtn.appendChild(expandIcon);
98
- expandBtn.appendChild(document.createTextNode(' Expand Neighbors'));
99
- expandBtn.onclick = function() {
100
- modal.hide();
101
- // Switch to Graph tab
102
- const graphTab = document.querySelector('a[href="#graph"]');
103
- if (graphTab) graphTab.click();
104
- // Expand neighbors after a delay
105
- setTimeout(function() {
106
- if (typeof expandNeighbors === 'function') {
107
- expandNeighbors(mem.id);
108
- }
109
- }, 500);
110
- };
111
- actionsDiv.appendChild(expandBtn);
76
+ // "View Original Memory" shown on Recall Lab + Memories, hidden on Graph
77
+ // (On Graph the node IS the memory; on Recall Lab we have a fact, not the original)
78
+ if (!fromGraph) {
79
+ var viewBtn = document.createElement('button');
80
+ viewBtn.className = 'btn btn-primary btn-sm';
81
+ viewBtn.innerHTML = '<i class="bi bi-journal-text"></i> View Original Memory';
82
+ viewBtn.onclick = function() {
83
+ var mid = mem.memory_id || mem.id;
84
+ viewBtn.disabled = true;
85
+ viewBtn.textContent = 'Loading...';
86
+ fetch('/api/memories/' + encodeURIComponent(mid) + '/facts')
87
+ .then(function(r) { return r.json(); })
88
+ .then(function(data) {
89
+ if (data.ok && data.original_content) {
90
+ contentDiv.textContent = '';
91
+ var origLabel = document.createElement('small');
92
+ origLabel.className = 'text-muted d-block mb-1';
93
+ origLabel.textContent = 'Original memory (' + (data.fact_count || 0) + ' atomic facts extracted):';
94
+ contentDiv.appendChild(origLabel);
95
+ var origText = document.createElement('div');
96
+ origText.style.cssText = 'white-space:pre-wrap;background:#f8f9fa;padding:10px;border-radius:6px;margin-bottom:8px;';
97
+ origText.textContent = data.original_content;
98
+ contentDiv.appendChild(origText);
99
+ if (data.facts && data.facts.length > 0) {
100
+ var toggle = document.createElement('button');
101
+ toggle.className = 'btn btn-sm btn-outline-secondary mb-2';
102
+ toggle.textContent = 'Show atomic facts (' + data.facts.length + ')';
103
+ var factsDiv = document.createElement('div');
104
+ factsDiv.style.display = 'none';
105
+ data.facts.forEach(function(f) {
106
+ var fDiv = document.createElement('div');
107
+ fDiv.className = 'small py-1 border-bottom';
108
+ var badge = document.createElement('span');
109
+ badge.className = 'badge bg-secondary me-1';
110
+ badge.style.fontSize = '0.6rem';
111
+ badge.textContent = f.fact_type;
112
+ fDiv.appendChild(badge);
113
+ fDiv.appendChild(document.createTextNode(f.content));
114
+ factsDiv.appendChild(fDiv);
115
+ });
116
+ toggle.onclick = function() {
117
+ var hidden = factsDiv.style.display === 'none';
118
+ factsDiv.style.display = hidden ? 'block' : 'none';
119
+ toggle.textContent = hidden ? 'Hide atomic facts' : 'Show atomic facts (' + data.facts.length + ')';
120
+ };
121
+ contentDiv.appendChild(toggle);
122
+ contentDiv.appendChild(factsDiv);
123
+ }
124
+ viewBtn.textContent = 'Showing original';
125
+ } else {
126
+ viewBtn.textContent = 'Not available';
127
+ }
128
+ }).catch(function() {
129
+ viewBtn.textContent = 'Failed to load';
130
+ viewBtn.disabled = false;
131
+ });
132
+ };
133
+ actionsDiv.appendChild(viewBtn);
134
+ }
112
135
 
113
- // Button 3: Filter to Cluster (show only this cluster in graph)
136
+ // "Expand Neighbors" shown on Graph, hidden elsewhere (no graph context)
137
+ if (fromGraph) {
138
+ var expandBtn = document.createElement('button');
139
+ expandBtn.className = 'btn btn-outline-secondary btn-sm';
140
+ expandBtn.innerHTML = '<i class="bi bi-diagram-3"></i> Expand Neighbors';
141
+ expandBtn.onclick = function() {
142
+ modal.hide();
143
+ setTimeout(function() {
144
+ if (typeof expandNeighbors === 'function') expandNeighbors(mem.id);
145
+ }, 300);
146
+ };
147
+ actionsDiv.appendChild(expandBtn);
148
+ }
149
+
150
+ // "Filter to Cluster" — always available if cluster exists
114
151
  if (mem.cluster_id) {
115
152
  var filterBtn = document.createElement('button');
116
153
  filterBtn.className = 'btn btn-outline-info btn-sm';
@@ -139,6 +176,63 @@ function openMemoryDetail(mem) {
139
176
  actionsDiv.appendChild(filterBtn);
140
177
  }
141
178
 
179
+ // Edit button — always available
180
+ var editBtn = document.createElement('button');
181
+ editBtn.className = 'btn btn-outline-warning btn-sm';
182
+ editBtn.innerHTML = '<i class="bi bi-pencil"></i> Edit';
183
+ editBtn.onclick = function() {
184
+ var currentText = contentDiv.textContent;
185
+ var textarea = document.createElement('textarea');
186
+ textarea.className = 'form-control mb-2';
187
+ textarea.rows = 4;
188
+ textarea.value = currentText;
189
+ contentDiv.textContent = '';
190
+ contentDiv.appendChild(textarea);
191
+ var saveBtn = document.createElement('button');
192
+ saveBtn.className = 'btn btn-sm btn-success me-1';
193
+ saveBtn.textContent = 'Save';
194
+ saveBtn.onclick = function() {
195
+ var newContent = textarea.value.trim();
196
+ if (!newContent) return;
197
+ fetch('/api/memories/' + encodeURIComponent(mem.id), {
198
+ method: 'PATCH',
199
+ headers: {'Content-Type': 'application/json'},
200
+ body: JSON.stringify({content: newContent})
201
+ }).then(function(r) { return r.json(); }).then(function(d) {
202
+ if (d.success) {
203
+ contentDiv.textContent = newContent;
204
+ mem.content = newContent;
205
+ if (typeof showToast === 'function') showToast('Memory updated');
206
+ }
207
+ });
208
+ };
209
+ var cancelBtn = document.createElement('button');
210
+ cancelBtn.className = 'btn btn-sm btn-secondary';
211
+ cancelBtn.textContent = 'Cancel';
212
+ cancelBtn.onclick = function() { contentDiv.textContent = currentText; };
213
+ contentDiv.appendChild(saveBtn);
214
+ contentDiv.appendChild(cancelBtn);
215
+ };
216
+ actionsDiv.appendChild(editBtn);
217
+
218
+ // Delete button — always available
219
+ var deleteBtn = document.createElement('button');
220
+ deleteBtn.className = 'btn btn-outline-danger btn-sm';
221
+ deleteBtn.innerHTML = '<i class="bi bi-trash"></i> Delete';
222
+ deleteBtn.onclick = function() {
223
+ if (!confirm('Delete this memory? This cannot be undone.')) return;
224
+ fetch('/api/memories/' + encodeURIComponent(mem.id), {method: 'DELETE'})
225
+ .then(function(r) { return r.json(); })
226
+ .then(function(d) {
227
+ if (d.success) {
228
+ modal.hide();
229
+ if (typeof showToast === 'function') showToast('Memory deleted');
230
+ if (typeof loadMemories === 'function') setTimeout(loadMemories, 300);
231
+ }
232
+ });
233
+ };
234
+ actionsDiv.appendChild(deleteBtn);
235
+
142
236
  body.appendChild(actionsDiv);
143
237
  }
144
238
 
@@ -1,4 +1,4 @@
1
- // SuperLocalMemory V3 — Recall Lab with Pagination
1
+ // SuperLocalMemory V3 — Recall Lab with Dual-Display + Pagination
2
2
  // Part of Qualixar | https://superlocalmemory.com
3
3
 
4
4
  var recallLabState = {
@@ -6,6 +6,7 @@ var recallLabState = {
6
6
  page: 0,
7
7
  perPage: 10,
8
8
  query: '',
9
+ synthesis: '',
9
10
  };
10
11
 
11
12
  document.getElementById('recall-lab-search')?.addEventListener('click', function() {
@@ -16,13 +17,13 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
16
17
  recallLabState.page = 0;
17
18
  var perPageEl = document.getElementById('recall-lab-per-page');
18
19
  recallLabState.perPage = perPageEl ? parseInt(perPageEl.value) : 10;
19
- var fetchLimit = Math.max(recallLabState.perPage * 5, 50); // Fetch up to 5 pages
20
+ var fetchLimit = Math.max(recallLabState.perPage * 5, 50);
20
21
 
21
22
  var resultsDiv = document.getElementById('recall-lab-results');
22
23
  var metaDiv = document.getElementById('recall-lab-meta');
23
24
  resultsDiv.textContent = '';
24
25
  var spinner = document.createElement('div');
25
- spinner.className = 'text-center';
26
+ spinner.className = 'text-center py-4';
26
27
  var spinnerInner = document.createElement('div');
27
28
  spinnerInner.className = 'spinner-border text-primary';
28
29
  spinner.appendChild(spinnerInner);
@@ -31,7 +32,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
31
32
  fetch('/api/v3/recall/trace', {
32
33
  method: 'POST',
33
34
  headers: {'Content-Type': 'application/json'},
34
- body: JSON.stringify({query: query, limit: fetchLimit})
35
+ body: JSON.stringify({query: query, limit: fetchLimit, synthesize: true})
35
36
  }).then(function(r) { return r.json(); }).then(function(data) {
36
37
  if (data.error) {
37
38
  resultsDiv.textContent = '';
@@ -50,6 +51,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
50
51
  appendMetaField(metaDiv, 'Time: ', (data.retrieval_time_ms || 0).toFixed(0) + 'ms');
51
52
 
52
53
  recallLabState.allResults = data.results || [];
54
+ recallLabState.synthesis = data.synthesis || '';
53
55
 
54
56
  if (recallLabState.allResults.length === 0) {
55
57
  resultsDiv.textContent = '';
@@ -80,6 +82,20 @@ function renderRecallPage() {
80
82
  var pageResults = results.slice(start, end);
81
83
  var totalPages = Math.ceil(results.length / recallLabState.perPage);
82
84
 
85
+ // Synthesis banner (Mode B/C only)
86
+ if (recallLabState.synthesis && recallLabState.page === 0) {
87
+ var synBanner = document.createElement('div');
88
+ synBanner.className = 'alert alert-light border-start border-4 border-primary mb-3';
89
+ var synTitle = document.createElement('strong');
90
+ synTitle.textContent = 'AI Summary';
91
+ synBanner.appendChild(synTitle);
92
+ synBanner.appendChild(document.createElement('br'));
93
+ var synText = document.createElement('span');
94
+ synText.textContent = recallLabState.synthesis;
95
+ synBanner.appendChild(synText);
96
+ resultsDiv.appendChild(synBanner);
97
+ }
98
+
83
99
  var listGroup = document.createElement('div');
84
100
  listGroup.className = 'list-group';
85
101
 
@@ -87,60 +103,108 @@ function renderRecallPage() {
87
103
  var globalIndex = start + i;
88
104
  var channels = r.channel_scores || {};
89
105
  var maxChannel = Math.max(channels.semantic || 0, channels.bm25 || 0, channels.entity_graph || 0, channels.temporal || 0) || 1;
106
+ var hasSource = r.source_content && r.source_content.length > 0;
107
+ var displayText = hasSource ? r.source_content : r.content;
90
108
 
91
109
  var item = document.createElement('div');
92
- item.className = 'list-group-item list-group-item-action';
110
+ item.className = 'list-group-item';
111
+
112
+ // Score badge row
113
+ var scoreRow = document.createElement('div');
114
+ scoreRow.className = 'd-flex justify-content-between align-items-center mb-1';
115
+ var numLabel = document.createElement('strong');
116
+ numLabel.textContent = '#' + (globalIndex + 1);
117
+ scoreRow.appendChild(numLabel);
118
+ var scoreBadges = document.createElement('div');
119
+ scoreBadges.innerHTML = '<span class="badge bg-primary me-1">Score: ' + r.score + '</span>' +
120
+ '<span class="badge bg-secondary me-1">Trust: ' + r.trust_score + '</span>' +
121
+ '<span class="badge bg-outline-info" style="border:1px solid #0dcaf0;color:#0dcaf0;">Conf: ' + r.confidence + '</span>';
122
+ scoreRow.appendChild(scoreBadges);
123
+ item.appendChild(scoreRow);
124
+
125
+ // Original memory text (primary display)
126
+ var contentDiv = document.createElement('div');
127
+ contentDiv.className = 'mb-2';
128
+ contentDiv.style.cssText = 'white-space:pre-wrap; line-height:1.5;';
129
+ var truncated = displayText.length > 500 ? displayText.substring(0, 500) + '...' : displayText;
130
+ contentDiv.textContent = truncated;
131
+ item.appendChild(contentDiv);
132
+
133
+ // Expandable atomic fact section (only if source differs from content)
134
+ if (hasSource && r.content !== r.source_content) {
135
+ var expandBtn = document.createElement('button');
136
+ expandBtn.className = 'btn btn-sm btn-outline-secondary mb-2';
137
+ expandBtn.textContent = 'Show matched fact + channels';
138
+ var factSection = document.createElement('div');
139
+ factSection.style.display = 'none';
140
+ factSection.className = 'border-top pt-2 mt-1';
141
+
142
+ var factLabel = document.createElement('small');
143
+ factLabel.className = 'text-muted d-block mb-1';
144
+ factLabel.textContent = 'Matched atomic fact:';
145
+ factSection.appendChild(factLabel);
146
+
147
+ var factContent = document.createElement('div');
148
+ factContent.className = 'small bg-light p-2 rounded mb-2';
149
+ factContent.textContent = r.content;
150
+ factSection.appendChild(factContent);
151
+
152
+ // Channel bars
153
+ factSection.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
154
+ factSection.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
155
+ factSection.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
156
+ factSection.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
157
+
158
+ expandBtn.addEventListener('click', function() {
159
+ var visible = factSection.style.display !== 'none';
160
+ factSection.style.display = visible ? 'none' : 'block';
161
+ expandBtn.textContent = visible ? 'Show matched fact + channels' : 'Hide matched fact';
162
+ });
163
+
164
+ item.appendChild(expandBtn);
165
+ item.appendChild(factSection);
166
+ } else {
167
+ // No source_content — show channel bars inline
168
+ var barsDiv = document.createElement('div');
169
+ barsDiv.className = 'mt-1';
170
+ barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
171
+ barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
172
+ barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
173
+ barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
174
+ item.appendChild(barsDiv);
175
+ }
176
+
177
+ // Click for detail modal
93
178
  item.style.cursor = 'pointer';
94
- item.title = 'Click to view full memory';
95
179
  (function(result) {
96
- item.addEventListener('click', function() {
180
+ contentDiv.addEventListener('click', function() {
97
181
  if (typeof openMemoryDetail === 'function') {
98
182
  openMemoryDetail({
99
183
  id: result.fact_id,
100
- content: result.content,
184
+ memory_id: result.memory_id,
185
+ content: result.source_content || result.content,
101
186
  score: result.score,
102
187
  importance: Math.round((result.confidence || 0.5) * 10),
103
188
  category: 'recall',
104
- tags: Object.keys(result.channel_scores || {}).join(', '),
105
189
  created_at: null,
106
190
  trust_score: result.trust_score,
107
191
  channel_scores: result.channel_scores
108
- });
192
+ }, 'recall'); // source='recall': show View Original, hide Expand Neighbors
109
193
  }
110
194
  });
111
195
  })(r);
112
196
 
113
- var header = document.createElement('h6');
114
- header.className = 'mb-1';
115
- header.textContent = (globalIndex + 1) + '. ' + (r.content || '').substring(0, 200);
116
- item.appendChild(header);
117
-
118
- var meta = document.createElement('small');
119
- meta.className = 'text-muted';
120
- meta.textContent = 'Score: ' + r.score + ' | Trust: ' + r.trust_score + ' | Confidence: ' + r.confidence;
121
- item.appendChild(meta);
122
-
123
- var barsDiv = document.createElement('div');
124
- barsDiv.className = 'mt-2';
125
- barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
126
- barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
127
- barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
128
- barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
129
- item.appendChild(barsDiv);
130
-
131
197
  listGroup.appendChild(item);
132
198
  });
133
199
  resultsDiv.appendChild(listGroup);
134
200
 
135
- // Pagination controls
201
+ // Pagination
136
202
  if (totalPages > 1) {
137
203
  var nav = document.createElement('nav');
138
204
  nav.className = 'mt-3';
139
- nav.setAttribute('aria-label', 'Recall results pagination');
140
205
  var ul = document.createElement('ul');
141
206
  ul.className = 'pagination justify-content-center';
142
207
 
143
- // Prev
144
208
  var prevLi = document.createElement('li');
145
209
  prevLi.className = 'page-item' + (recallLabState.page === 0 ? ' disabled' : '');
146
210
  var prevA = document.createElement('a');
@@ -149,15 +213,11 @@ function renderRecallPage() {
149
213
  prevA.textContent = 'Previous';
150
214
  prevA.addEventListener('click', function(e) {
151
215
  e.preventDefault();
152
- if (recallLabState.page > 0) {
153
- recallLabState.page--;
154
- renderRecallPage();
155
- }
216
+ if (recallLabState.page > 0) { recallLabState.page--; renderRecallPage(); }
156
217
  });
157
218
  prevLi.appendChild(prevA);
158
219
  ul.appendChild(prevLi);
159
220
 
160
- // Page numbers
161
221
  for (var p = 0; p < totalPages; p++) {
162
222
  var li = document.createElement('li');
163
223
  li.className = 'page-item' + (p === recallLabState.page ? ' active' : '');
@@ -176,7 +236,6 @@ function renderRecallPage() {
176
236
  ul.appendChild(li);
177
237
  }
178
238
 
179
- // Next
180
239
  var nextLi = document.createElement('li');
181
240
  nextLi.className = 'page-item' + (recallLabState.page >= totalPages - 1 ? ' disabled' : '');
182
241
  var nextA = document.createElement('a');
@@ -185,18 +244,13 @@ function renderRecallPage() {
185
244
  nextA.textContent = 'Next';
186
245
  nextA.addEventListener('click', function(e) {
187
246
  e.preventDefault();
188
- if (recallLabState.page < totalPages - 1) {
189
- recallLabState.page++;
190
- renderRecallPage();
191
- }
247
+ if (recallLabState.page < totalPages - 1) { recallLabState.page++; renderRecallPage(); }
192
248
  });
193
249
  nextLi.appendChild(nextA);
194
250
  ul.appendChild(nextLi);
195
-
196
251
  nav.appendChild(ul);
197
252
  resultsDiv.appendChild(nav);
198
253
 
199
- // Page info
200
254
  var info = document.createElement('div');
201
255
  info.className = 'text-center text-muted small';
202
256
  info.textContent = 'Showing ' + (start + 1) + '-' + end + ' of ' + results.length + ' results';
@@ -205,8 +259,7 @@ function renderRecallPage() {
205
259
  }
206
260
 
207
261
  function appendMetaField(parent, label, value) {
208
- var text = document.createTextNode(label);
209
- parent.appendChild(text);
262
+ parent.appendChild(document.createTextNode(label));
210
263
  var strong = document.createElement('strong');
211
264
  strong.textContent = value;
212
265
  parent.appendChild(strong);
@@ -234,7 +287,6 @@ function buildChannelBar(name, score, max, color) {
234
287
  return row;
235
288
  }
236
289
 
237
- // Enter key support
238
290
  document.getElementById('recall-lab-query')?.addEventListener('keydown', function(e) {
239
291
  if (e.key === 'Enter') document.getElementById('recall-lab-search')?.click();
240
292
  });