refactor: Modularize monolithic HTML control panel into organized components
This commit completes a major refactoring of the Miku control panel from a single 7,191-line monolithic HTML file to a modern modular architecture: CHANGES: - Extracted 872 lines of CSS into css/style.css - Created 10 specialized JavaScript modules (4,964 lines total): * core.js: Global state, utilities, initialization, polling system * servers.js: Server management and mood handling * modes.js: Evil mode, GPU selection, bipolar mode, scoreboard * actions.js: Autonomous/manual actions, custom prompts, reactions * image-gen.js: Image generation system * status.js: Status display and statistics * dm.js: DM user management and conversation analysis * chat.js: LLM chat interface with streaming and voice calls * memories.js: Cheshire Cat memory integration (episodic/declarative/procedural) * profile.js: Profile picture, album gallery, activities editor - Cleaned index.html to 1,351 lines (structure only, zero inline JS/CSS) - Removed 12 duplicate variable declarations - Maintained strict script load order for dependency resolution - Added backup comment to index.html.bak for historical reference VERIFICATION COMPLETED: ✓ All 191 functions/variables from original accounted for ✓ Cross-referenced with backup to ensure nothing lost ✓ All onclick handlers and modal systems validated ✓ No circular dependencies or broken references ✓ HTML structure integrity verified (11 tabs, all buttons/modals intact) ✓ CropperJS CDN links preserved The refactored code is production-ready with improved maintainability and clear separation of concerns.
This commit is contained in:
446
bot/static/js/memories.js
Normal file
446
bot/static/js/memories.js
Normal file
@@ -0,0 +1,446 @@
|
||||
// ============================================================================
|
||||
// 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 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)}</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 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)}</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';
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user