Step 1 of memory system overhaul: persona tagging. - discord_bridge: tag user messages with 'persona' metadata at storage time - memory_consolidation: tag Miku's own responses with 'persona' metadata - memory_consolidation: tag declarative facts with source persona during extraction - memory_consolidation: pass persona context to LLM extraction prompt - memory_consolidation: annotate cross-persona facts in prompt injection (e.g., '(learned as Evil Miku)' when Evil facts appear for Normal Miku) - Web UI: show persona badge (🎤 Miku / 😈 Evil Miku) on facts and episodic memories in the Memory Management tab This lets both personas know which version of Miku each memory came from, enabling Evil Miku to distinguish her own memories from Normal Miku's.
455 lines
18 KiB
JavaScript
455 lines
18 KiB
JavaScript
// ============================================================================
|
|
// 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 = `<span style="color: #6fdc6f;">● Connected</span> — ${statusData.url}`;
|
|
} else {
|
|
indicator.innerHTML = `<span style="color: #ff6b6b;">● Disconnected</span> — ${statusData.url}`;
|
|
}
|
|
|
|
if (statusData.circuit_breaker_active) {
|
|
indicator.innerHTML += ` <span style="color: #dcb06f;">(circuit breaker active)</span>`;
|
|
}
|
|
|
|
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 = '<span style="color: #ff6b6b;">● Error checking status</span>';
|
|
}
|
|
}
|
|
|
|
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 = '<div style="text-align: center; color: #888; padding: 1rem;">Loading facts...</div>';
|
|
|
|
try {
|
|
const data = await apiCall('/memory/facts');
|
|
|
|
if (!data.success || data.count === 0) {
|
|
listDiv.innerHTML = '<div style="text-align: center; color: #666; padding: 2rem;">No declarative facts stored yet.</div>';
|
|
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 persona = fact.metadata?.persona || 'miku';
|
|
const personaBadge = persona === 'evil_miku'
|
|
? '<span style="background: #7a2a2a; color: #ff9999; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 3px; margin-left: 0.3rem;">😈 Evil Miku</span>'
|
|
: '<span style="background: #2a5a7a; color: #99ccff; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 3px; margin-left: 0.3rem;">🎤 Miku</span>';
|
|
const factDataJson = escapeJsonForAttribute(fact);
|
|
html += `
|
|
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a9955; display: flex; justify-content: space-between; align-items: flex-start;">
|
|
<div style="flex: 1;">
|
|
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(fact.content)}${personaBadge}</div>
|
|
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
|
|
Source: ${escapeHtml(source)} · ${when}
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
|
|
<button data-memory='${factDataJson}' onclick='showEditMemoryModalFromButton(this, "declarative", "${fact.id}")'
|
|
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
|
title="Edit this fact">✏️</button>
|
|
<button onclick="deleteMemoryPoint('declarative', '${fact.id}', this)"
|
|
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
|
title="Delete this fact">🗑️</button>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
listDiv.innerHTML = `<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.5rem;">${data.count} facts loaded</div>` + html;
|
|
} catch (err) {
|
|
listDiv.innerHTML = `<div style="color: #ff6b6b; padding: 1rem;">Error loading facts: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
async function loadEpisodicMemories() {
|
|
const listDiv = document.getElementById('episodic-list');
|
|
listDiv.innerHTML = '<div style="text-align: center; color: #888; padding: 1rem;">Loading memories...</div>';
|
|
|
|
try {
|
|
const data = await apiCall('/memory/episodic');
|
|
|
|
if (!data.success || data.count === 0) {
|
|
listDiv.innerHTML = '<div style="text-align: center; color: #666; padding: 2rem;">No episodic memories stored yet.</div>';
|
|
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 persona = mem.metadata?.persona || 'miku';
|
|
const personaBadge = persona === 'evil_miku'
|
|
? '<span style="background: #7a2a2a; color: #ff9999; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 3px; margin-left: 0.3rem;">😈 Evil Miku</span>'
|
|
: '<span style="background: #2a5a7a; color: #99ccff; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 3px; margin-left: 0.3rem;">🎤 Miku</span>';
|
|
const memDataJson = escapeJsonForAttribute(mem);
|
|
html += `
|
|
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a5599; display: flex; justify-content: space-between; align-items: flex-start;">
|
|
<div style="flex: 1;">
|
|
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(mem.content)}${personaBadge}</div>
|
|
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
|
|
Source: ${escapeHtml(source)} · ${when}
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
|
|
<button data-memory='${memDataJson}' onclick='showEditMemoryModalFromButton(this, "episodic", "${mem.id}")'
|
|
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
|
title="Edit this memory">✏️</button>
|
|
<button onclick="deleteMemoryPoint('episodic', '${mem.id}', this)"
|
|
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
|
|
title="Delete this memory">🗑️</button>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
listDiv.innerHTML = `<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.5rem;">${data.count} memories loaded</div>` + html;
|
|
} catch (err) {
|
|
listDiv.innerHTML = `<div style="color: #ff6b6b; padding: 1rem;">Error loading memories: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
});
|
|
}
|