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.
287 lines
15 KiB
JavaScript
287 lines
15 KiB
JavaScript
// ============================================================================
|
|
// Miku Control Panel — Status Module
|
|
// Status display, last prompt, autonomous stats
|
|
// ============================================================================
|
|
|
|
// ===== Status =====
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
const result = await apiCall('/status');
|
|
const statusDiv = document.getElementById('status');
|
|
|
|
if (result.evil_mode !== undefined && result.evil_mode !== evilMode) {
|
|
evilMode = result.evil_mode;
|
|
updateEvilModeUI();
|
|
if (evilMode && result.mood) {
|
|
const moodSelect = document.getElementById('mood');
|
|
if (moodSelect) moodSelect.value = result.mood;
|
|
}
|
|
}
|
|
|
|
if (result.mood) {
|
|
const moodSelect = document.getElementById('mood');
|
|
if (moodSelect && moodSelect.querySelector(`option[value="${result.mood}"]`)) {
|
|
moodSelect.value = result.mood;
|
|
}
|
|
currentMood = result.mood;
|
|
}
|
|
|
|
let serverMoodsHtml = '';
|
|
if (result.server_moods) {
|
|
serverMoodsHtml = '<div style="margin-top: 0.5rem;"><strong>Server Moods:</strong><br>';
|
|
for (const [guildId, mood] of Object.entries(result.server_moods)) {
|
|
const server = servers.find(s => s.guild_id == guildId);
|
|
const serverName = server ? server.guild_name : `Server ${guildId}`;
|
|
const emojiMap = evilMode ? EVIL_MOOD_EMOJIS : MOOD_EMOJIS;
|
|
serverMoodsHtml += `• ${serverName}: ${mood} ${emojiMap[mood] || ''}<br>`;
|
|
}
|
|
serverMoodsHtml += '</div>';
|
|
}
|
|
|
|
const moodEmoji = evilMode ? (EVIL_MOOD_EMOJIS[result.mood] || '') : (MOOD_EMOJIS[result.mood] || '');
|
|
const moodLabel = evilMode ? `😈 ${result.mood} ${moodEmoji}` : `${result.mood} ${moodEmoji}`;
|
|
|
|
statusDiv.innerHTML = `
|
|
<div><strong>Status:</strong> ${result.status}</div>
|
|
<div><strong>DM Mood:</strong> ${moodLabel}</div>
|
|
<div><strong>Servers:</strong> ${result.servers}</div>
|
|
<div><strong>Active Schedulers:</strong> ${result.active_schedulers}</div>
|
|
<div style="margin-top: 0.5rem; padding: 0.5rem; background: #2a2a2a; border-radius: 4px; font-size: 0.9rem;">
|
|
<strong>💬 DM Support:</strong> Users can message Miku directly in DMs. She responds to every DM message using the DM mood (auto-rotating every 2 hours).
|
|
</div>
|
|
${serverMoodsHtml}
|
|
`;
|
|
} catch (error) {
|
|
console.error('Failed to load status:', error);
|
|
}
|
|
}
|
|
|
|
// ===== Last Prompt =====
|
|
|
|
async function loadLastPrompt() {
|
|
const source = localStorage.getItem('miku-prompt-source') || 'cat';
|
|
const promptEl = document.getElementById('last-prompt');
|
|
const infoEl = document.getElementById('prompt-cat-info');
|
|
|
|
try {
|
|
if (source === 'cat') {
|
|
const result = await apiCall('/prompt/cat');
|
|
if (result.timestamp) {
|
|
infoEl.innerHTML = `<strong>User:</strong> ${escapeHtml(result.user || '?')} | <strong>Mood:</strong> ${escapeHtml(result.mood || '?')} | <strong>Time:</strong> ${new Date(result.timestamp).toLocaleString()}`;
|
|
promptEl.textContent = result.full_prompt + `\n\n${'═'.repeat(60)}\n[Cat Response]\n${result.response}`;
|
|
} else {
|
|
infoEl.textContent = '';
|
|
promptEl.textContent = result.full_prompt || 'No Cheshire Cat interaction yet.';
|
|
}
|
|
} else {
|
|
infoEl.textContent = '';
|
|
const result = await apiCall('/prompt');
|
|
promptEl.textContent = result.prompt;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load last prompt:', error);
|
|
}
|
|
}
|
|
|
|
// ===== Autonomous Stats =====
|
|
|
|
async function loadAutonomousStats() {
|
|
const serverSelect = document.getElementById('autonomous-server-select');
|
|
const selectedGuildId = serverSelect.value;
|
|
|
|
if (!selectedGuildId) {
|
|
document.getElementById('autonomous-stats-display').innerHTML = '<p style="color: #aaa;">Please select a server to view autonomous stats.</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await apiCall('/autonomous/stats');
|
|
|
|
if (!data.servers || !data.servers[selectedGuildId]) {
|
|
document.getElementById('autonomous-stats-display').innerHTML = '<p style="color: #ff5555;">Server not found or not initialized.</p>';
|
|
return;
|
|
}
|
|
|
|
const serverData = data.servers[selectedGuildId];
|
|
displayAutonomousStats(serverData);
|
|
} catch (error) {
|
|
console.error('Failed to load autonomous stats:', error);
|
|
}
|
|
}
|
|
|
|
function displayAutonomousStats(data) {
|
|
const container = document.getElementById('autonomous-stats-display');
|
|
|
|
if (!data.context) {
|
|
container.innerHTML = `
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">⚠️ Context Not Initialized</h4>
|
|
<p>This server hasn't had any activity yet. Context tracking will begin once messages are sent.</p>
|
|
<div style="margin-top: 1rem; padding: 1rem; background: #1e1e1e; border-radius: 4px;">
|
|
<strong>Current Mood:</strong> ${data.mood} ${MOOD_EMOJIS[data.mood] || ''}<br>
|
|
<strong>Energy:</strong> ${data.mood_profile.energy}<br>
|
|
<strong>Sociability:</strong> ${data.mood_profile.sociability}<br>
|
|
<strong>Impulsiveness:</strong> ${data.mood_profile.impulsiveness}
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const ctx = data.context;
|
|
const profile = data.mood_profile;
|
|
|
|
const lastActionMin = Math.floor(ctx.time_since_last_action / 60);
|
|
const lastInteractionMin = Math.floor(ctx.time_since_last_interaction / 60);
|
|
|
|
container.innerHTML = `
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">🎭 Mood & Personality Profile</h4>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;">Current Mood</div>
|
|
<div style="font-size: 1.5rem; font-weight: bold;">${data.mood} ${MOOD_EMOJIS[data.mood] || ''}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;">Energy Level</div>
|
|
<div style="font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.energy)}">${(profile.energy * 100).toFixed(0)}%</div>
|
|
<div style="width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;">
|
|
<div style="width: ${profile.energy * 100}%; height: 100%; background: ${getStatColor(profile.energy)}; border-radius: 3px;"></div>
|
|
</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;">Sociability</div>
|
|
<div style="font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.sociability)}">${(profile.sociability * 100).toFixed(0)}%</div>
|
|
<div style="width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;">
|
|
<div style="width: ${profile.sociability * 100}%; height: 100%; background: ${getStatColor(profile.sociability)}; border-radius: 3px;"></div>
|
|
</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;">Impulsiveness</div>
|
|
<div style="font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.impulsiveness)}">${(profile.impulsiveness * 100).toFixed(0)}%</div>
|
|
<div style="width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;">
|
|
<div style="width: ${profile.impulsiveness * 100}%; height: 100%; background: ${getStatColor(profile.impulsiveness)}; border-radius: 3px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">📈 Activity Metrics</h4>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Messages (Last 5 min) <span style="color: #666;">⚡ ephemeral</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #4caf50;">${ctx.messages_last_5min}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Messages (Last Hour) <span style="color: #666;">⚡ ephemeral</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #2196f3;">${ctx.messages_last_hour}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Conversation Momentum <span style="color: #4caf50;">💾 saved</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: ${getMomentumColor(ctx.conversation_momentum)}">${(ctx.conversation_momentum * 100).toFixed(0)}%</div>
|
|
<div style="font-size: 0.75rem; color: #888; margin-top: 0.3rem;">Decays with downtime (half-life: 10min)</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Unique Users Active <span style="color: #666;">⚡ ephemeral</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #ff9800;">${ctx.unique_users_active}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">👥 User Events</h4>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Users Joined Recently</div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #4caf50;">${ctx.users_joined_recently}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Status Changes</div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #2196f3;">${ctx.users_status_changed}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Active Activities</div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #9c27b0;">${ctx.users_started_activity.length}</div>
|
|
${ctx.users_started_activity.length > 0 ? `<div style="font-size: 0.8rem; margin-top: 0.5rem; color: #aaa;">${ctx.users_started_activity.join(', ')}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">⏱️ Timing & Context</h4>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Time Since Last Action <span style="color: #4caf50;">💾 saved</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #ff5722;">${lastActionMin} min</div>
|
|
<div style="font-size: 0.8rem; color: #888;">${ctx.time_since_last_action.toFixed(1)}s</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Time Since Last Interaction <span style="color: #4caf50;">💾 saved</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #ff9800;">${lastInteractionMin} min</div>
|
|
<div style="font-size: 0.8rem; color: #888;">${ctx.time_since_last_interaction.toFixed(1)}s</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Messages Since Last Appearance <span style="color: #4caf50;">💾 saved</span></div>
|
|
<div style="font-size: 1.8rem; font-weight: bold; color: #2196f3;">${ctx.messages_since_last_appearance}</div>
|
|
</div>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa;">Current Time Context <span style="color: #666;">⚡ ephemeral</span></div>
|
|
<div style="font-size: 1.5rem; font-weight: bold; color: #61dafb;">${ctx.hour_of_day}:00</div>
|
|
<div style="font-size: 0.8rem; color: #888;">${ctx.is_weekend ? '📅 Weekend' : '📆 Weekday'}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px;">
|
|
<h4 style="color: #61dafb; margin-top: 0;">🧠 Base Energy Level</h4>
|
|
<div style="background: #1e1e1e; padding: 1rem; border-radius: 4px;">
|
|
<div style="font-size: 0.9rem; color: #aaa; margin-bottom: 0.5rem;">From current mood personality</div>
|
|
<div style="font-size: 2rem; font-weight: bold; color: ${getStatColor(ctx.mood_energy_level)}">${(ctx.mood_energy_level * 100).toFixed(0)}%</div>
|
|
<div style="width: 100%; height: 10px; background: #333; border-radius: 5px; margin-top: 0.5rem;">
|
|
<div style="width: ${ctx.mood_energy_level * 100}%; height: 100%; background: ${getStatColor(ctx.mood_energy_level)}; border-radius: 5px;"></div>
|
|
</div>
|
|
<div style="font-size: 0.85rem; color: #888; margin-top: 0.5rem;">
|
|
💡 Combined with activity metrics to determine action likelihood.<br>
|
|
📝 High energy = shorter wait times, higher action chance.<br>
|
|
💾 <strong>Persisted across restarts</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function getStatColor(value) {
|
|
if (value >= 0.8) return '#4caf50';
|
|
if (value >= 0.6) return '#8bc34a';
|
|
if (value >= 0.4) return '#ffc107';
|
|
if (value >= 0.2) return '#ff9800';
|
|
return '#f44336';
|
|
}
|
|
|
|
function getMomentumColor(value) {
|
|
if (value >= 0.7) return '#4caf50';
|
|
if (value >= 0.4) return '#2196f3';
|
|
return '#9e9e9e';
|
|
}
|
|
|
|
function populateAutonomousServerDropdown() {
|
|
const select = document.getElementById('autonomous-server-select');
|
|
if (!select) return;
|
|
|
|
const currentValue = select.value;
|
|
select.innerHTML = '<option value="">-- Select a server --</option>';
|
|
|
|
servers.forEach(server => {
|
|
const option = document.createElement('option');
|
|
option.value = server.guild_id;
|
|
option.textContent = `${server.guild_name} (${server.guild_id})`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (currentValue && servers.some(s => String(s.guild_id) === currentValue)) {
|
|
select.value = currentValue;
|
|
}
|
|
}
|