Files
miku-discord/bot/static/js/core.js

413 lines
13 KiB
JavaScript
Raw Normal View History

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
// ============================================================================
// 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, '&lt;')
.replace(/>/g, '&gt;');
}
// ============================================================================
// 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();
}
});
});