// ============================================================================ // Miku Control Panel — Memory Management Module // ============================================================================ async function refreshMemoryStats() { try { // Fetch Cat status const statusData = await apiCall('/memory/status'); const indicator = document.getElementById('cat-status-indicator'); const toggleBtn = document.getElementById('cat-toggle-btn'); if (statusData.healthy) { indicator.innerHTML = `● Connected — ${statusData.url}`; } else { indicator.innerHTML = `● Disconnected — ${statusData.url}`; } if (statusData.circuit_breaker_active) { indicator.innerHTML += ` (circuit breaker active)`; } toggleBtn.textContent = statusData.enabled ? '🐱 Cat: ON' : '😿 Cat: OFF'; toggleBtn.style.background = statusData.enabled ? '#2a7a2a' : '#7a2a2a'; toggleBtn.style.borderColor = statusData.enabled ? '#4a9a4a' : '#9a4a4a'; // Fetch memory stats const statsData = await apiCall('/memory/stats'); if (statsData.success && statsData.collections) { const collections = {}; statsData.collections.forEach(c => { collections[c.name] = c.vectors_count; }); document.getElementById('stat-episodic-count').textContent = collections['episodic'] ?? '—'; document.getElementById('stat-declarative-count').textContent = collections['declarative'] ?? '—'; document.getElementById('stat-procedural-count').textContent = collections['procedural'] ?? '—'; } else { document.getElementById('stat-episodic-count').textContent = '—'; document.getElementById('stat-declarative-count').textContent = '—'; document.getElementById('stat-procedural-count').textContent = '—'; } } catch (err) { console.error('Error refreshing memory stats:', err); document.getElementById('cat-status-indicator').innerHTML = '● Error checking status'; } } async function toggleCatIntegration() { try { const statusData = await apiCall('/memory/status'); const newState = !statusData.enabled; const formData = new FormData(); formData.append('enabled', newState); const res = await fetch('/memory/toggle', { method: 'POST', body: formData }); const data = await res.json(); if (data.success) { showNotification(`Cheshire Cat ${newState ? 'enabled' : 'disabled'}`, newState ? 'success' : 'info'); refreshMemoryStats(); } } catch (err) { showNotification('Failed to toggle Cat integration', 'error'); } } async function triggerConsolidation() { const btn = document.getElementById('consolidate-btn'); const status = document.getElementById('consolidation-status'); const resultDiv = document.getElementById('consolidation-result'); btn.disabled = true; btn.textContent = '⏳ Running...'; status.textContent = 'Consolidation in progress (this may take a few minutes)...'; resultDiv.style.display = 'none'; try { const data = await apiCall('/memory/consolidate', 'POST'); if (data.success) { status.textContent = '✅ Consolidation complete!'; status.style.color = '#6fdc6f'; resultDiv.textContent = data.result || 'Consolidation finished successfully.'; resultDiv.style.display = 'block'; showNotification('Memory consolidation complete', 'success'); refreshMemoryStats(); } else { status.textContent = '❌ ' + (data.error || 'Consolidation failed'); status.style.color = '#ff6b6b'; } } catch (err) { status.textContent = '❌ Error: ' + err.message; status.style.color = '#ff6b6b'; } finally { btn.disabled = false; btn.textContent = '🌙 Run Consolidation'; } } async function loadFacts() { const listDiv = document.getElementById('facts-list'); listDiv.innerHTML = '
Loading facts...
'; try { const data = await apiCall('/memory/facts'); if (!data.success || data.count === 0) { listDiv.innerHTML = '
No declarative facts stored yet.
'; return; } let html = ''; data.facts.forEach((fact, i) => { const source = fact.metadata?.source || 'unknown'; const when = fact.metadata?.when ? new Date(fact.metadata.when * 1000).toLocaleString() : 'unknown'; const factDataJson = escapeJsonForAttribute(fact); html += `
${escapeHtml(fact.content)}
Source: ${escapeHtml(source)} · ${when}
`; }); listDiv.innerHTML = `
${data.count} facts loaded
` + html; } catch (err) { listDiv.innerHTML = `
Error loading facts: ${err.message}
`; } } async function loadEpisodicMemories() { const listDiv = document.getElementById('episodic-list'); listDiv.innerHTML = '
Loading memories...
'; try { const data = await apiCall('/memory/episodic'); if (!data.success || data.count === 0) { listDiv.innerHTML = '
No episodic memories stored yet.
'; return; } let html = ''; data.memories.forEach((mem, i) => { const source = mem.metadata?.source || 'unknown'; const when = mem.metadata?.when ? new Date(mem.metadata.when * 1000).toLocaleString() : 'unknown'; const memDataJson = escapeJsonForAttribute(mem); html += `
${escapeHtml(mem.content)}
Source: ${escapeHtml(source)} · ${when}
`; }); listDiv.innerHTML = `
${data.count} memories loaded
` + html; } catch (err) { listDiv.innerHTML = `
Error loading memories: ${err.message}
`; } } async function deleteMemoryPoint(collection, pointId, btnElement) { if (!confirm(`Delete this ${collection} memory point?`)) return; try { const data = await apiCall(`/memory/point/${collection}/${pointId}`, 'DELETE'); if (data.success) { // Remove the row from the UI const row = btnElement.closest('div[style*="margin-bottom"]'); if (row) row.remove(); showNotification('Memory point deleted', 'success'); refreshMemoryStats(); } else { showNotification('Failed to delete: ' + (data.error || 'Unknown error'), 'error'); } } catch (err) { console.error('Failed to delete memory point:', err); } } // Delete All Memories — Multi-step confirmation flow function onDeleteStep1Change() { const checked = document.getElementById('delete-checkbox-1').checked; document.getElementById('delete-step-2').style.display = checked ? 'block' : 'none'; if (!checked) { document.getElementById('delete-checkbox-2').checked = false; document.getElementById('delete-step-3').style.display = 'none'; document.getElementById('delete-step-final').style.display = 'none'; document.getElementById('delete-confirmation-input').value = ''; } } function onDeleteStep2Change() { const checked = document.getElementById('delete-checkbox-2').checked; document.getElementById('delete-step-3').style.display = checked ? 'block' : 'none'; document.getElementById('delete-step-final').style.display = checked ? 'block' : 'none'; if (!checked) { document.getElementById('delete-confirmation-input').value = ''; updateDeleteButton(); } } function onDeleteInputChange() { updateDeleteButton(); } function updateDeleteButton() { const input = document.getElementById('delete-confirmation-input').value; const expected = "Yes, I am deleting Miku's memories fully."; const btn = document.getElementById('delete-all-btn'); const match = input === expected; btn.disabled = !match; btn.style.cursor = match ? 'pointer' : 'not-allowed'; btn.style.opacity = match ? '1' : '0.5'; } async function executeDeleteAllMemories() { const input = document.getElementById('delete-confirmation-input').value; const expected = "Yes, I am deleting Miku's memories fully."; if (input !== expected) { showNotification('Confirmation string does not match', 'error'); return; } const btn = document.getElementById('delete-all-btn'); btn.disabled = true; btn.textContent = '⏳ Deleting...'; try { const data = await apiCall('/memory/delete', 'POST', { confirmation: input }); if (data.success) { showNotification('All memories have been permanently deleted', 'success'); resetDeleteFlow(); refreshMemoryStats(); } else { showNotification('Deletion failed: ' + (data.error || 'Unknown error'), 'error'); } } catch (err) { console.error('Failed to delete all memories:', err); } finally { btn.disabled = false; btn.textContent = '🗑️ Permanently Delete All Memories'; } } function resetDeleteFlow() { document.getElementById('delete-checkbox-1').checked = false; document.getElementById('delete-checkbox-2').checked = false; document.getElementById('delete-confirmation-input').value = ''; document.getElementById('delete-step-2').style.display = 'none'; document.getElementById('delete-step-3').style.display = 'none'; document.getElementById('delete-step-final').style.display = 'none'; updateDeleteButton(); } // Memory Edit/Create Modal Functions // currentEditMemory declared in core.js function showEditMemoryModalFromButton(button, collection, pointId) { const memoryJson = button.getAttribute('data-memory'); // Unescape HTML entities back to JSON const unescapedJson = memoryJson .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); const memory = JSON.parse(unescapedJson); showEditMemoryModal(collection, pointId, memory); } function showEditMemoryModal(collection, pointId, memoryData) { const memory = typeof memoryData === 'string' ? JSON.parse(memoryData) : memoryData; currentEditMemory = { collection, pointId, memory }; const modal = document.getElementById('edit-memory-modal'); const contentField = document.getElementById('edit-memory-content'); const sourceField = document.getElementById('edit-memory-source'); contentField.value = memory.content || ''; sourceField.value = memory.metadata?.source || ''; modal.style.display = 'flex'; } function closeEditMemoryModal() { document.getElementById('edit-memory-modal').style.display = 'none'; currentEditMemory = null; } async function saveMemoryEdit() { if (!currentEditMemory) return; const content = document.getElementById('edit-memory-content').value.trim(); const source = document.getElementById('edit-memory-source').value.trim(); if (!content) { showNotification('Content cannot be empty', 'error'); return; } const { collection, pointId } = currentEditMemory; const saveBtn = document.querySelector('#edit-memory-modal button[onclick="saveMemoryEdit()"]'); saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; try { const data = await apiCall(`/memory/point/${collection}/${pointId}`, 'PUT', { content: content, metadata: { source: source || 'manual_edit' } }); if (data.success) { showNotification('Memory updated successfully', 'success'); closeEditMemoryModal(); // Reload the appropriate list if (collection === 'declarative') { loadFacts(); } else if (collection === 'episodic') { loadEpisodicMemories(); } } else { showNotification('Failed to update: ' + (data.error || 'Unknown error'), 'error'); } } catch (err) { console.error('Failed to save memory edit:', err); } finally { saveBtn.disabled = false; saveBtn.textContent = 'Save Changes'; } } function showCreateMemoryModal(collection) { const modal = document.getElementById('create-memory-modal'); document.getElementById('create-memory-collection').value = collection; document.getElementById('create-memory-content').value = ''; document.getElementById('create-memory-user-id').value = ''; document.getElementById('create-memory-source').value = 'manual'; // Update modal title based on collection type const title = collection === 'declarative' ? 'Add New Fact' : 'Add New Memory'; document.querySelector('#create-memory-modal h3').textContent = title; modal.style.display = 'flex'; } function closeCreateMemoryModal() { document.getElementById('create-memory-modal').style.display = 'none'; } // Modal keyboard and backdrop close handlers document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { const editModal = document.getElementById('edit-memory-modal'); const createModal = document.getElementById('create-memory-modal'); if (editModal && editModal.style.display !== 'none') closeEditMemoryModal(); if (createModal && createModal.style.display !== 'none') closeCreateMemoryModal(); } }); async function saveNewMemory() { const collection = document.getElementById('create-memory-collection').value; const content = document.getElementById('create-memory-content').value.trim(); const userId = document.getElementById('create-memory-user-id').value.trim(); const source = document.getElementById('create-memory-source').value.trim(); if (!content) { showNotification('Content cannot be empty', 'error'); return; } const createBtn = document.querySelector('#create-memory-modal button[onclick="saveNewMemory()"]'); createBtn.disabled = true; createBtn.textContent = 'Creating...'; try { const data = await apiCall('/memory/create', 'POST', { collection: collection, content: content, user_id: userId || null, source: source || 'manual', metadata: {} }); if (data.success) { showNotification(`${collection === 'declarative' ? 'Fact' : 'Memory'} created successfully`, 'success'); closeCreateMemoryModal(); // Reload the appropriate list if (collection === 'declarative') { loadFacts(); } else if (collection === 'episodic') { loadEpisodicMemories(); } refreshMemoryStats(); } else { showNotification('Failed to create: ' + (data.error || 'Unknown error'), 'error'); } } catch (err) { console.error('Failed to save new memory:', err); } finally { createBtn.disabled = false; createBtn.textContent = 'Create Memory'; } } // Search/Filter Function function filterMemories(listId, searchTerm) { const listDiv = document.getElementById(listId); const items = listDiv.querySelectorAll('.memory-item'); const term = searchTerm.toLowerCase().trim(); items.forEach(item => { const content = item.textContent.toLowerCase(); if (term === '' || content.includes(term)) { item.style.display = 'flex'; } else { item.style.display = 'none'; } }); }