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.
413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
// ============================================================================
|
|
// Miku Control Panel — Core Module
|
|
// Global variables, utility functions, tab switching, initialization, polling
|
|
// ============================================================================
|
|
|
|
// Global variables
|
|
let currentMood = 'neutral';
|
|
let voiceCallActive = false;
|
|
let voiceCallHistory = [];
|
|
let servers = [];
|
|
let evilMode = false;
|
|
let bipolarMode = false;
|
|
let selectedGPU = 'nvidia';
|
|
let chatConversationHistory = [];
|
|
let pfpCropper = null;
|
|
let albumEntries = [];
|
|
let albumSelectedId = null;
|
|
let albumChecked = new Set();
|
|
let albumCropper = null;
|
|
let albumOpen = false;
|
|
let activitiesData = null;
|
|
let activitiesOpen = false;
|
|
let activitiesSections = { normal: false, evil: false };
|
|
let activitiesEditing = {};
|
|
let activitiesEditCache = {};
|
|
let currentEditMemory = null;
|
|
let logsAutoScroll = true;
|
|
let notificationTimer = null;
|
|
let statusInterval = null;
|
|
let logsInterval = null;
|
|
let argsInterval = null;
|
|
|
|
// Mood emoji mapping
|
|
const MOOD_EMOJIS = {
|
|
"asleep": "💤",
|
|
"neutral": "",
|
|
"bubbly": "🫧",
|
|
"sleepy": "🌙",
|
|
"curious": "👀",
|
|
"shy": "👉👈",
|
|
"serious": "👔",
|
|
"excited": "✨",
|
|
"melancholy": "🍷",
|
|
"flirty": "🫦",
|
|
"romantic": "💌",
|
|
"irritated": "😒",
|
|
"angry": "💢",
|
|
"silly": "🪿"
|
|
};
|
|
|
|
// Evil mood emoji mapping
|
|
const EVIL_MOOD_EMOJIS = {
|
|
"aggressive": "👿",
|
|
"cunning": "🐍",
|
|
"sarcastic": "😈",
|
|
"evil_neutral": "",
|
|
"bored": "🥱",
|
|
"manic": "🤪",
|
|
"jealous": "💚",
|
|
"melancholic": "🌑",
|
|
"playful_cruel": "🎭",
|
|
"contemptuous": "👑"
|
|
};
|
|
|
|
// ============================================================================
|
|
// Utility functions
|
|
// ============================================================================
|
|
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.getElementById('notification');
|
|
notification.textContent = message;
|
|
notification.style.display = 'block';
|
|
notification.style.opacity = '0.95';
|
|
|
|
if (type === 'error') {
|
|
notification.style.backgroundColor = '#d32f2f';
|
|
} else if (type === 'success') {
|
|
notification.style.backgroundColor = '#2e7d32';
|
|
} else {
|
|
notification.style.backgroundColor = '#222';
|
|
}
|
|
|
|
if (notificationTimer) clearTimeout(notificationTimer);
|
|
notificationTimer = setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
setTimeout(() => {
|
|
notification.style.display = 'none';
|
|
notificationTimer = null;
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
async function apiCall(endpoint, method = 'GET', data = null) {
|
|
try {
|
|
const options = {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
};
|
|
|
|
if (data) {
|
|
options.body = JSON.stringify(data);
|
|
}
|
|
|
|
const response = await fetch(endpoint, options);
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
return result;
|
|
} else {
|
|
throw new Error(result.message || 'API call failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('API call error:', error);
|
|
showNotification(error.message, 'error');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function escapeJsonForAttribute(obj) {
|
|
return JSON.stringify(obj)
|
|
.replace(/&/g, '&')
|
|
.replace(/'/g, ''')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tab switching
|
|
// ============================================================================
|
|
|
|
function switchTab(tabId) {
|
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active');
|
|
});
|
|
|
|
document.getElementById(tabId).classList.add('active');
|
|
|
|
const activeBtn = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
|
|
if (activeBtn) activeBtn.classList.add('active');
|
|
|
|
localStorage.setItem('miku-active-tab', tabId);
|
|
|
|
console.log(`🔄 Switched to ${tabId}`);
|
|
if (tabId === 'tab1') {
|
|
console.log('🔄 Refreshing figurine subscribers for Server Management tab');
|
|
refreshFigurineSubscribers();
|
|
}
|
|
if (tabId === 'tab3') {
|
|
loadStatus();
|
|
loadLastPrompt();
|
|
}
|
|
if (tabId === 'tab6') {
|
|
showTabLoading('tab6');
|
|
loadAutonomousStats().finally(() => hideTabLoading('tab6'));
|
|
}
|
|
if (tabId === 'tab9') {
|
|
console.log('🧠 Refreshing memory stats for Memories tab');
|
|
showTabLoading('tab9');
|
|
refreshMemoryStats().finally(() => hideTabLoading('tab9'));
|
|
}
|
|
if (tabId === 'tab10') {
|
|
console.log('📱 Loading DM users for DM Management tab');
|
|
showTabLoading('tab10');
|
|
loadDMUsers().finally(() => hideTabLoading('tab10'));
|
|
}
|
|
if (tabId === 'tab11') {
|
|
console.log('🖼️ Loading Profile Picture tab');
|
|
loadPfpTab();
|
|
}
|
|
}
|
|
|
|
function showTabLoading(tabId) {
|
|
const tab = document.getElementById(tabId);
|
|
if (!tab) return;
|
|
if (tab.querySelector('.tab-loading-overlay')) return;
|
|
const sections = tab.querySelectorAll('.section');
|
|
const hasContent = Array.from(sections).some(s => s.querySelector('[id]')?.innerHTML?.trim());
|
|
if (hasContent) return;
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'tab-loading-overlay';
|
|
overlay.innerHTML = '<div class="spinner"></div> Loading...';
|
|
tab.prepend(overlay);
|
|
}
|
|
|
|
function hideTabLoading(tabId) {
|
|
const tab = document.getElementById(tabId);
|
|
if (!tab) return;
|
|
const overlay = tab.querySelector('.tab-loading-overlay');
|
|
if (overlay) overlay.remove();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Polling
|
|
// ============================================================================
|
|
|
|
function startPolling() {
|
|
if (!statusInterval) statusInterval = setInterval(loadStatus, 10000);
|
|
if (!logsInterval) logsInterval = setInterval(loadLogs, 5000);
|
|
if (!argsInterval) argsInterval = setInterval(loadActiveArguments, 5000);
|
|
}
|
|
|
|
function stopPolling() {
|
|
clearInterval(statusInterval); statusInterval = null;
|
|
clearInterval(logsInterval); logsInterval = null;
|
|
clearInterval(argsInterval); argsInterval = null;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Initialization helpers
|
|
// ============================================================================
|
|
|
|
function initTabState() {
|
|
const savedTab = localStorage.getItem('miku-active-tab');
|
|
if (savedTab && document.getElementById(savedTab)) {
|
|
switchTab(savedTab);
|
|
}
|
|
}
|
|
|
|
function initTabWheelScroll() {
|
|
const tabButtonsEl = document.querySelector('.tab-buttons');
|
|
if (tabButtonsEl) {
|
|
tabButtonsEl.addEventListener('wheel', function(e) {
|
|
if (e.deltaY !== 0) {
|
|
e.preventDefault();
|
|
tabButtonsEl.scrollLeft += e.deltaY;
|
|
}
|
|
}, { passive: false });
|
|
}
|
|
}
|
|
|
|
function initVisibilityPolling() {
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
stopPolling();
|
|
console.log('⏸ Tab hidden — polling paused');
|
|
} else {
|
|
loadStatus(); loadLogs(); loadActiveArguments();
|
|
startPolling();
|
|
console.log('▶️ Tab visible — polling resumed');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initChatImagePreview() {
|
|
const imageInput = document.getElementById('chat-image-file');
|
|
if (imageInput) {
|
|
imageInput.addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
const preview = document.getElementById('chat-image-preview');
|
|
const previewImg = document.getElementById('chat-image-preview-img');
|
|
previewImg.src = event.target.result;
|
|
preview.style.display = 'block';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function initModalAccessibility() {
|
|
const editModal = document.getElementById('edit-memory-modal');
|
|
const createModal = document.getElementById('create-memory-modal');
|
|
if (editModal) {
|
|
editModal.setAttribute('role', 'dialog');
|
|
editModal.setAttribute('aria-modal', 'true');
|
|
editModal.setAttribute('aria-label', 'Edit Memory');
|
|
editModal.addEventListener('click', function(e) {
|
|
if (e.target === this) closeEditMemoryModal();
|
|
});
|
|
}
|
|
if (createModal) {
|
|
createModal.setAttribute('role', 'dialog');
|
|
createModal.setAttribute('aria-modal', 'true');
|
|
createModal.setAttribute('aria-label', 'Create Memory');
|
|
createModal.addEventListener('click', function(e) {
|
|
if (e.target === this) closeCreateMemoryModal();
|
|
});
|
|
}
|
|
}
|
|
|
|
function initPromptSourceToggle() {
|
|
const saved = localStorage.getItem('miku-prompt-source') || 'cat';
|
|
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById(`prompt-src-${saved}`).classList.add('active');
|
|
}
|
|
|
|
function initLogsScrollDetection() {
|
|
const logsPanel = document.getElementById('logs-panel');
|
|
if (!logsPanel) return;
|
|
logsPanel.addEventListener('scroll', function() {
|
|
const atBottom = logsPanel.scrollHeight - logsPanel.scrollTop - logsPanel.clientHeight < 50;
|
|
logsAutoScroll = atBottom;
|
|
const banner = document.getElementById('logs-paused-banner');
|
|
if (banner) banner.style.display = atBottom ? 'none' : 'block';
|
|
});
|
|
}
|
|
|
|
function scrollLogsToBottom() {
|
|
const logsPanel = document.getElementById('logs-panel');
|
|
if (logsPanel) {
|
|
logsPanel.scrollTop = logsPanel.scrollHeight;
|
|
logsAutoScroll = true;
|
|
const banner = document.getElementById('logs-paused-banner');
|
|
if (banner) banner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Log functions
|
|
// ============================================================================
|
|
|
|
function classifyLogLine(line) {
|
|
const upper = line.toUpperCase();
|
|
if (upper.includes(' ERROR ') || upper.includes(' CRITICAL ') || upper.startsWith('ERROR') || upper.startsWith('CRITICAL') || upper.includes('TRACEBACK')) return 'log-error';
|
|
if (upper.includes(' WARNING ') || upper.startsWith('WARNING')) return 'log-warning';
|
|
if (upper.includes(' DEBUG ') || upper.startsWith('DEBUG')) return 'log-debug';
|
|
return 'log-info';
|
|
}
|
|
|
|
async function loadLogs() {
|
|
try {
|
|
const result = await apiCall('/logs');
|
|
const logsContent = document.getElementById('logs-content');
|
|
const lines = (result || '').split('\n');
|
|
logsContent.innerHTML = lines.map(line => {
|
|
if (!line.trim()) return '';
|
|
const cls = classifyLogLine(line);
|
|
return `<div class="log-line ${cls}">${escapeHtml(line)}</div>`;
|
|
}).join('');
|
|
|
|
if (logsAutoScroll) {
|
|
scrollLogsToBottom();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load logs:', error);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Prompt source toggle (shared between core and status modules)
|
|
// ============================================================================
|
|
|
|
function switchPromptSource(source) {
|
|
localStorage.setItem('miku-prompt-source', source);
|
|
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById(`prompt-src-${source}`).classList.add('active');
|
|
loadLastPrompt();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Profile picture metadata (stub — actual loading in profile.js)
|
|
// ============================================================================
|
|
|
|
async function loadProfilePictureMetadata() {
|
|
// Delegated to PFP tab loader — only runs if tab11 is active
|
|
}
|
|
|
|
// ============================================================================
|
|
// DOMContentLoaded — main initialization
|
|
// ============================================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initTabState();
|
|
initTabWheelScroll();
|
|
initLogsScrollDetection();
|
|
initChatImagePreview();
|
|
initModalAccessibility();
|
|
initPromptSourceToggle();
|
|
|
|
loadStatus();
|
|
loadServers();
|
|
populateMoodDropdowns();
|
|
loadLastPrompt();
|
|
loadLogs();
|
|
checkEvilModeStatus();
|
|
checkBipolarModeStatus();
|
|
checkGPUStatus();
|
|
refreshLanguageStatus();
|
|
refreshFigurineSubscribers();
|
|
loadProfilePictureMetadata();
|
|
loadVoiceDebugMode();
|
|
|
|
initVisibilityPolling();
|
|
startPolling();
|
|
|
|
// Modal keyboard close handler
|
|
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();
|
|
}
|
|
});
|
|
});
|