Files
miku-discord/bot/static/js/memories.js
koko210Serve 694590a620 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.
2026-04-29 20:56:49 +03:00

447 lines
17 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 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(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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';
}
});
}