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.
447 lines
17 KiB
JavaScript
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(/"/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';
|
|
}
|
|
});
|
|
}
|