-
đ§ Nova Memory Dashboard
-
-
-
Users
-
loadingâŠ
+
+
+
+
+
+
+
+
No user selected yet.
+
+
+
Waiting for user selection.
+
+
+
+
+
Waiting for user selection.
+
+
+
+
+
+
+
+
+
+
Search results will appear here.
+
+
+
+
-
-
-
+ function renderShortTerm(entries) {
+ if (!entries.length) {
+ shortTermEl.innerHTML = '
No recent short-term entries.
';
+ return;
+ }
+ const table = document.createElement('table');
+ table.innerHTML = `
+
+
+ | Role |
+ Content |
+ When |
+
+
+ `;
+ const body = document.createElement('tbody');
+ entries.slice(-8).reverse().forEach((entry) => {
+ const row = document.createElement('tr');
+ const safeContent = escapeHtml(entry.content);
+ const preview = safeContent.length > 120 ? `${safeContent.slice(0, 120)}âŠ` : safeContent;
+ row.innerHTML = `
+
${entry.role} |
+
${preview} |
+
${formatDate(entry.timestamp)} |
+ `;
+ body.appendChild(row);
+ });
+ table.appendChild(body);
+ shortTermEl.innerHTML = '';
+ shortTermEl.appendChild(table);
+ }
+
+ function renderLongTerm(entries) {
+ if (!entries.length) {
+ longTermEl.innerHTML = '
No long-term memories stored yet.
';
+ return;
+ }
+ const table = document.createElement('table');
+ table.innerHTML = `
+
+
+ | Imp. |
+ Content |
+ When |
+ Actions |
+
+
+ `;
+ const body = document.createElement('tbody');
+ entries.forEach((entry) => {
+ const imp = entry.importance ? entry.importance.toFixed(2) : 'N/A';
+ const row = document.createElement('tr');
+ const safeContent = escapeHtml(entry.content);
+ const preview = safeContent.length > 120 ? `${safeContent.slice(0, 120)}âŠ` : safeContent;
+ row.innerHTML = `
+
${imp} |
+
${preview} |
+
${formatDate(entry.timestamp)} |
+
+
+
+ |
+ `;
+ body.appendChild(row);
+ const editButton = row.querySelector('button[data-action="edit"]');
+ if (editButton) {
+ editButton.addEventListener('click', (event) => {
+ event.stopPropagation();
+ startEdit(entry.id);
+ });
+ }
+ const deleteButton = row.querySelector('button[data-action="delete"]');
+ if (deleteButton) {
+ deleteButton.addEventListener('click', (event) => {
+ event.stopPropagation();
+ deleteEntry(entry.id);
+ });
+ }
+ });
+ table.appendChild(body);
+ longTermEl.innerHTML = '';
+ longTermEl.appendChild(table);
+ }
+
+ async function loadTimeline(userId) {
+ if (!timelineEl) return;
+ try {
+ const { entries } = await fetchJson(`/api/users/${encodeURIComponent(userId)}/timeline`);
+ renderTimeline(entries || []);
+ } catch (err) {
+ timelineEl.innerHTML = `
Timeline unavailable: ${escapeHtml(err.message)}
`;
+ }
+ }
+
+ function renderTimeline(entries) {
+ if (!timelineEl) return;
+ if (!entries || !entries.length) {
+ timelineEl.innerHTML = '
No recall data yet.
';
+ return;
+ }
+ const maxCount = Math.max(...entries.map((entry) => entry.count), 1);
+ timelineEl.innerHTML = '';
+ entries.forEach((entry) => {
+ const bar = document.createElement('div');
+ const height = (entry.count / maxCount) * 100;
+ bar.className = 'timeline-bar';
+ bar.style.height = `${Math.max(height, 6)}%`;
+ bar.innerHTML = `
${entry.count}`;
+ bar.title = `${entry.day}: ${entry.count} hit${entry.count === 1 ? '' : 's'}`;
+ timelineEl.appendChild(bar);
+ });
+ }
+
+ function useSearchResult(content, importance = 0) {
+ editingEntryId = null;
+ contentField.value = content;
+ importanceField.value = Number.isFinite(importance) ? importance.toFixed(2) : '0.5';
+ formTitle.textContent = 'Add long-term memory';
+ formStatus.textContent = 'ready';
+ formCancel.style.display = 'none';
+ contentField.focus();
+ }
+
+ function startEdit(entryId) {
+ const entry = currentLongTerm.find((item) => item.id === entryId);
+ if (!entry) return;
+ selectedUser = selectedUser; // ensure defined
+ editingEntryId = entryId;
+ contentField.value = entry.content;
+ importanceField.value = entry.importance?.toFixed(2) || '0.5';
+ formTitle.textContent = 'Edit long-term memory';
+ formStatus.textContent = 'editing';
+ formCancel.style.display = 'inline-flex';
+ }
+
+ function resetForm() {
+ editingEntryId = null;
+ contentField.value = '';
+ importanceField.value = '0.5';
+ formTitle.textContent = 'Add long-term memory';
+ formStatus.textContent = 'idle';
+ formCancel.style.display = 'none';
+ }
+
+ form.addEventListener('submit', async (event) => {
+ event.preventDefault();
+ if (!selectedUser) return;
+ const content = contentField.value.trim();
+ if (!content) {
+ alert('Please enter some content for the memory.');
+ return;
+ }
+ const importance = parseFloat(importanceField.value);
+ formStatus.textContent = 'savingâŠ';
+ try {
+ await fetchJson(`/api/users/${selectedUser}/long`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: editingEntryId,
+ content,
+ importance: Number.isFinite(importance) ? importance : 0,
+ }),
+ });
+ formStatus.textContent = 'saved';
+ resetForm();
+ showUser(selectedUser);
+ } catch (err) {
+ formStatus.textContent = 'error';
+ alert('Failed to save memory: ' + err.message);
+ }
+ });
+
+ formCancel.addEventListener('click', () => {
+ resetForm();
+ });
+
+ async function deleteEntry(entryId) {
+ if (!selectedUser) return;
+ if (!confirm('Delete this memory permanently?')) return;
+ try {
+ const response = await fetch(`/api/users/${selectedUser}/long/${entryId}`, { method: 'DELETE' });
+ if (!response.ok) throw new Error(response.statusText || 'delete failed');
+ showUser(selectedUser);
+ } catch (err) {
+ alert('Failed to delete memory: ' + err.message);
+ }
+ }
+
+ async function runSearch() {
+ if (!selectedUser) {
+ searchResultsEl.innerHTML = '
Select a user first.
';
+ return;
+ }
+ const query = searchInput.value.trim();
+ if (!query) {
+ searchResultsEl.innerHTML = '
Type a query to search embeddings.
';
+ return;
+ }
+ searchResultsEl.innerHTML = '
SearchingâŠ
';
+ try {
+ const results = await fetchJson(`/api/users/${selectedUser}/search`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query }),
+ });
+ if (!results.length) {
+ searchResultsEl.innerHTML = '
No matching memories found.
';
+ return;
+ }
+ const list = document.createElement('ul');
+ results.forEach((entry) => {
+ const item = document.createElement('li');
+ const score = entry.score ? entry.score.toFixed(3) : 'n/a';
+ const safeContent = escapeHtml(entry.content || '');
+ const preview = safeContent.length > 150 ? `${safeContent.slice(0, 150)}âŠ` : safeContent;
+ item.innerHTML = `
+
${score}
+
${preview}
+ `;
+ const useButton = document.createElement('button');
+ useButton.type = 'button';
+ useButton.className = 'memory-actions use-result';
+ useButton.textContent = 'Use this memory';
+ useButton.addEventListener('click', () => useSearchResult(entry.content || '', entry.importance));
+ item.appendChild(useButton);
+ list.appendChild(item);
+ });
+ searchResultsEl.innerHTML = '';
+ searchResultsEl.appendChild(list);
+ } catch (err) {
+ searchResultsEl.innerHTML = `
Search error: ${err.message}
`;
+ }
+ }
+
+ searchBtn.addEventListener('click', runSearch);
+ searchInput.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ runSearch();
+ }
+ });
+ prevPageBtn.addEventListener('click', async () => {
+ if (!selectedUser || longTermPage <= 1) return;
+ longTermPage -= 1;
+ const info = await loadLongTermPage(selectedUser);
+ detailSubtitle.textContent = `Short-term: ${shortTermCount} entries · Long-term: ${info.total || 0} entries`;
+ searchResultsEl.innerHTML = '
Run a search to inspect stored memories.
';
+ });
+ nextPageBtn.addEventListener('click', async () => {
+ if (!selectedUser || longTermPage >= longTermTotalPages) return;
+ longTermPage += 1;
+ const info = await loadLongTermPage(selectedUser);
+ detailSubtitle.textContent = `Short-term: ${shortTermCount} entries · Long-term: ${info.total || 0} entries`;
+ searchResultsEl.innerHTML = '
Run a search to inspect stored memories.
';
+ });
+
+ loadMood();
+ loadUsers();
+
-
\ No newline at end of file
+