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

549 lines
21 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 — DM Management Module
// ============================================================================
async function loadDMUsers() {
try {
const result = await apiCall('/dms/users');
displayDMUsers(result.users);
} catch (error) {
console.error('Failed to load DM users:', error);
}
}
function displayDMUsers(users) {
const container = document.getElementById('dm-users-list');
if (!users || users.length === 0) {
container.innerHTML = '<p>No DM conversations found.</p>';
return;
}
let html = '<div class="dm-users-grid">';
users.forEach(user => {
console.log(`👤 Processing user: ${user.username} (ID: ${user.user_id})`);
const lastMessage = user.last_message ?
`Last: ${user.last_message.content}` :
'No messages yet';
const lastTime = user.last_message ?
new Date(user.last_message.timestamp).toLocaleString() :
'Never';
html += `
<div class="dm-user-card">
<h4>👤 ${user.username}</h4>
<p><strong>ID:</strong> ${user.user_id}</p>
<p><strong>Total Messages:</strong> ${user.total_messages}</p>
<p><strong>User Messages:</strong> ${user.user_messages}</p>
<p><strong>Bot Messages:</strong> ${user.bot_messages}</p>
<p><strong>Last Activity:</strong> ${lastTime}</p>
<p><strong>Last Message:</strong> ${lastMessage}</p>
<div class="dm-user-actions">
<button class="view-chat-btn" data-user-id="${user.user_id}">💬 View Chat</button>
<button class="analyze-user-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #9c27b0;">📊 Analyze</button>
<button class="export-dms-btn" data-user-id="${user.user_id}">📤 Export</button>
<button class="block-user-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #ff9800;">🚫 Block</button>
<button class="delete-all-dms-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #f44336;">🗑 Delete All</button>
<button class="delete-user-completely-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #d32f2f;">💀 Delete User</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// Add event listeners after HTML is inserted
addDMUserEventListeners();
}
function addDMUserEventListeners() {
// Add event listeners for view chat buttons
document.querySelectorAll('.view-chat-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 View chat clicked for user ID: ${userId} (type: ${typeof userId})`);
viewUserConversations(userId);
});
});
// Add event listeners for export buttons
document.querySelectorAll('.export-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 Export clicked for user ID: ${userId} (type: ${typeof userId})`);
exportUserDMs(userId);
});
});
// Add event listeners for analyze buttons
document.querySelectorAll('.analyze-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Analyze clicked for user ID: ${userId} (type: ${typeof userId})`);
analyzeUserInteraction(userId, username);
});
});
// Add event listeners for block buttons
document.querySelectorAll('.block-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Block clicked for user ID: ${userId} (type: ${typeof userId})`);
blockUser(userId, username);
});
});
// Add event listeners for delete all DMs buttons
document.querySelectorAll('.delete-all-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete all DMs clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteAllUserConversations(userId, username);
});
});
// Add event listeners for delete user completely buttons
document.querySelectorAll('.delete-user-completely-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete user completely clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteUserCompletely(userId, username);
});
});
}
async function viewUserConversations(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
console.log(`🔍 Loading conversations for user ${userIdStr} (type: ${typeof userIdStr})`);
console.log(`🔍 Original userId: ${userId} (type: ${typeof userId})`);
console.log(`🔍 userIdStr: ${userIdStr} (type: ${typeof userIdStr})`);
const result = await apiCall(`/dms/users/${userIdStr}/conversations?limit=100`);
console.log('📡 API Response:', result);
console.log('📡 API URL called:', `/dms/users/${userIdStr}/conversations?limit=100`);
if (result.conversations && result.conversations.length > 0) {
console.log(`✅ Found ${result.conversations.length} conversations`);
displayUserConversations(userIdStr, result.conversations);
} else {
console.log('⚠️ No conversations found in response');
showNotification('No conversations found for this user', 'info');
// Go back to user list
loadDMUsers();
}
} catch (error) {
console.error('Failed to load user conversations:', error);
}
}
function displayUserConversations(userId, conversations) {
console.log(`🎨 Displaying conversations for user ${userId}:`, conversations);
// Create a modal or expand the user card to show conversations
const container = document.getElementById('dm-users-list');
let html = `
<div class="conversation-view">
<button onclick="loadDMUsers()" style="margin-bottom: 1rem;"> Back to DM Users</button>
<h4>💬 Conversations with User ${userId}</h4>
<div class="conversations-list">
`;
if (!conversations || conversations.length === 0) {
html += '<p>No conversations found for this user.</p>';
} else {
conversations.forEach((msg, index) => {
console.log(`📝 Processing message ${index}:`, msg);
const timestamp = new Date(msg.timestamp).toLocaleString();
const sender = msg.is_bot_message ? '🤖 Miku' : '👤 User';
const content = msg.content || '[No text content]';
const messageId = msg.message_id || msg.timestamp; // Use message_id or timestamp as identifier
const escapedContent = content.replace(/'/g, "\\'").replace(/"/g, '\\"');
// Debug: Log message details
console.log(`📝 Message ${index}: id=${messageId}, is_bot=${msg.is_bot_message}, content="${content.substring(0, 30)}..."`);
// Only show delete button for bot messages (Miku can only delete her own messages)
const deleteButton = msg.is_bot_message ?
`<button class="delete-message-btn" onclick="deleteConversation('${userId}', '${messageId}', '${escapedContent}')"
style="background: #f44336; color: white; border: none; padding: 2px 6px; font-size: 12px; border-radius: 3px; margin-left: 10px;"
title="Delete this Miku message (ID: ${messageId})">
🗑 Delete
</button>` : '';
html += `
<div class="conversation-message ${msg.is_bot_message ? 'bot-message' : 'user-message'}">
<div class="message-header">
<span class="sender">${sender}</span>
<span class="timestamp">${timestamp}</span>
${deleteButton}
</div>
<div class="message-content">${content}</div>
${msg.attachments && msg.attachments.length > 0 ? `
<div class="message-attachments">
<strong>📎 Attachments:</strong>
${msg.attachments.map(att => `
<div class="attachment">
- ${att.filename} (${att.size} bytes)
<a href="${att.url}" target="_blank">🔗 View</a>
</div>
`).join('')}
</div>
` : ''}
${msg.reactions && msg.reactions.length > 0 ? `
<div class="message-reactions">
${msg.reactions.map(reaction => {
const reactionTime = new Date(reaction.added_at).toLocaleString();
const reactorType = reaction.is_bot ? 'bot-reaction' : 'user-reaction';
const reactorLabel = reaction.is_bot ? '🤖 Miku' : `👤 ${reaction.reactor_name}`;
return `
<div class="reaction-item" title="${reactorLabel} reacted at ${reactionTime}">
<span class="reaction-emoji">${reaction.emoji}</span>
<span class="reaction-by ${reactorType}">${reactorLabel}</span>
</div>
`;
}).join('')}
</div>
` : ''}
</div>
`;
});
}
html += `
</div>
</div>
`;
console.log('🎨 Generated HTML:', html);
container.innerHTML = html;
}
async function exportUserDMs(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
await apiCall(`/dms/users/${userIdStr}/export?format=txt`);
showNotification(`DM export completed for user ${userIdStr}`);
// You could trigger a download here if the file is accessible
} catch (error) {
console.error('Failed to export user DMs:', error);
}
}
async function deleteUserDMs(userId) {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
if (!confirm(`Are you sure you want to delete all DM logs for user ${userIdStr}? This action cannot be undone.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}`, 'DELETE');
showNotification(`Deleted DM logs for user ${userIdStr}`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to delete user DMs:', error);
}
}
// ========== User Blocking & Advanced Deletion Functions ==========
async function blockUser(userId, username) {
const userIdStr = String(userId);
if (!confirm(`Are you sure you want to block ${username} (${userIdStr}) from sending DMs to Miku?`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/block`, 'POST');
showNotification(`${username} has been blocked from sending DMs`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to block user:', error);
}
}
async function unblockUser(userId, username) {
const userIdStr = String(userId);
try {
await apiCall(`/dms/users/${userIdStr}/unblock`, 'POST');
showNotification(`${username} has been unblocked`);
loadBlockedUsers(); // Refresh blocked users list
} catch (error) {
console.error('Failed to unblock user:', error);
}
}
async function deleteAllUserConversations(userId, username) {
const userIdStr = String(userId);
if (!confirm(`⚠️ DELETE ALL CONVERSATIONS with ${username} (${userIdStr})?\n\nThis will:\n• Delete ALL Miku messages from Discord DM\n• Clear all conversation logs\n• Keep the user record\n\nThis action CANNOT be undone!\n\nClick OK to confirm deletion.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/conversations/delete-all`, 'POST');
showNotification(`Bulk deletion queued for ${username} (deleting all Miku messages from Discord and logs)`);
setTimeout(() => {
loadDMUsers(); // Refresh after a delay to allow deletion to process
}, 2000);
} catch (error) {
console.error('Failed to delete conversations:', error);
}
}
async function deleteUserCompletely(userId, username) {
const userIdStr = String(userId);
if (!confirm(`🚨 COMPLETELY DELETE USER ${username} (${userIdStr})?\n\nThis will:\n• Delete ALL conversation history\n• Delete the entire user log file\n• Remove ALL traces of this user\n\nThis action is PERMANENT and CANNOT be undone!\n\nType "${username}" below to confirm:`)) {
return;
}
const confirmName = prompt(`Type the username "${username}" to confirm complete deletion:`);
if (confirmName !== username) {
showNotification('Deletion cancelled - username did not match', 'error');
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/delete-completely`, 'POST');
showNotification(`${username} has been completely deleted from the system`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to delete user completely:', error);
}
}
async function deleteConversation(userId, conversationId, messageContent) {
const userIdStr = String(userId);
if (!confirm(`Delete this Miku message from Discord and logs?\n\n"${messageContent.substring(0, 100)}${messageContent.length > 100 ? '...' : ''}"\n\nThis will:\n• Delete the message from Discord DM\n• Remove it from conversation logs\n\nNote: Only Miku's messages can be deleted.\nThis action cannot be undone.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/conversations/${conversationId}/delete`, 'POST');
showNotification('Miku message deletion queued (deleting from both Discord and logs)');
setTimeout(() => {
viewUserConversations(userId); // Refresh after a short delay to allow deletion to process
}, 1000);
} catch (error) {
console.error('Failed to delete conversation:', error);
}
}
async function analyzeUserInteraction(userId, username) {
const userIdStr = String(userId);
if (!confirm(`Run DM interaction analysis for ${username}?\n\nThis will:\n• Analyze their messages from the last 24 hours\n• Generate a sentiment report\n• Send report to bot owner\n\nMinimum 3 messages required for analysis.`)) {
return;
}
try {
showNotification(`Analyzing ${username}'s interactions...`, 'info');
const result = await apiCall(`/dms/users/${userIdStr}/analyze`, 'POST');
if (result.reported) {
showNotification(`✅ Analysis complete! Report sent to bot owner for ${username}`);
} else {
showNotification(`📊 Analysis complete for ${username} (not enough messages or already reported today)`);
}
} catch (error) {
console.error('Failed to analyze user:', error);
}
}
async function runDailyAnalysis() {
if (!confirm('Run the daily DM interaction analysis now?\n\nThis will:\n• Analyze all DM users from the last 24 hours\n• Report one significant interaction to the bot owner\n• Skip users already reported today\n\nNote: This runs automatically at 2 AM daily.')) {
return;
}
try {
showNotification('Starting DM interaction analysis...', 'info');
await apiCall('/dms/analysis/run', 'POST');
showNotification('✅ DM analysis completed! Check bot owner\'s DMs for any reports.');
} catch (error) {
console.error('Failed to run DM analysis:', error);
}
}
async function viewAnalysisReports() {
try {
showNotification('Loading analysis reports...', 'info');
const result = await apiCall('/dms/analysis/reports?limit=50');
displayAnalysisReports(result.reports);
} catch (error) {
console.error('Failed to load reports:', error);
}
}
function displayAnalysisReports(reports) {
const container = document.getElementById('dm-users-list');
if (!reports || reports.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<p>No analysis reports found yet.</p>
<button onclick="loadDMUsers()" style="margin-top: 1rem;"> Back to DM Users</button>
</div>
`;
return;
}
let html = `
<div style="margin-bottom: 1rem;">
<button onclick="loadDMUsers()"> Back to DM Users</button>
<span style="margin-left: 1rem; color: #aaa;">${reports.length} reports found</span>
</div>
<div style="display: grid; gap: 1rem;">
`;
reports.forEach(report => {
const sentimentColor =
report.sentiment_score >= 5 ? '#4caf50' :
report.sentiment_score <= -3 ? '#f44336' :
'#2196f3';
const sentimentEmoji =
report.sentiment_score >= 5 ? '😊' :
report.sentiment_score <= -3 ? '😢' :
'😐';
const timestamp = new Date(report.analyzed_at).toLocaleString();
html += `
<div style="background: #2a2a2a; border-left: 4px solid ${sentimentColor}; padding: 1rem; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;">
<div>
<h4 style="margin: 0 0 0.25rem 0;">${sentimentEmoji} ${report.username}</h4>
<p style="margin: 0; font-size: 0.85rem; color: #aaa;">User ID: ${report.user_id}</p>
</div>
<div style="text-align: right;">
<div style="font-size: 1.2rem; font-weight: bold; color: ${sentimentColor};">
${report.sentiment_score > 0 ? '+' : ''}${report.sentiment_score}/10
</div>
<div style="font-size: 0.75rem; color: #aaa; text-transform: uppercase;">
${report.overall_sentiment}
</div>
</div>
</div>
<div style="margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;">
<strong>Miku's Feelings:</strong>
<p style="margin: 0.5rem 0 0 0; font-style: italic;">"${report.your_feelings}"</p>
</div>
${report.notable_moment ? `
<div style="margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;">
<strong>Notable Moment:</strong>
<p style="margin: 0.5rem 0 0 0; font-style: italic;">"${report.notable_moment}"</p>
</div>
` : ''}
${report.key_behaviors && report.key_behaviors.length > 0 ? `
<div style="margin: 0.75rem 0;">
<strong>Key Behaviors:</strong>
<ul style="margin: 0.5rem 0 0 0; padding-left: 1.5rem;">
${report.key_behaviors.slice(0, 5).map(b => `<li>${b}</li>`).join('')}
</ul>
</div>
` : ''}
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #444; font-size: 0.8rem; color: #aaa;">
<span>📅 ${timestamp}</span>
<span style="margin-left: 1rem;">💬 ${report.message_count} messages analyzed</span>
<span style="margin-left: 1rem;">📄 ${report.filename}</span>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function loadBlockedUsers() {
try {
const result = await apiCall('/dms/blocked-users');
// Hide DM users list and show blocked users section
document.getElementById('dm-users-list').style.display = 'none';
document.getElementById('blocked-users-section').style.display = 'block';
displayBlockedUsers(result.blocked_users);
} catch (error) {
console.error('Failed to load blocked users:', error);
}
}
function hideBlockedUsers() {
// Show DM users list and hide blocked users section
document.getElementById('dm-users-list').style.display = 'block';
document.getElementById('blocked-users-section').style.display = 'none';
loadDMUsers(); // Refresh DM users
}
function displayBlockedUsers(blockedUsers) {
const container = document.getElementById('blocked-users-list');
if (!blockedUsers || blockedUsers.length === 0) {
container.innerHTML = '<p>No blocked users.</p>';
return;
}
let html = '<div class="blocked-users-grid">';
blockedUsers.forEach(user => {
html += `
<div class="blocked-user-card">
<h4>🚫 ${user.username}</h4>
<p><strong>ID:</strong> ${user.user_id}</p>
<p><strong>Blocked:</strong> ${new Date(user.blocked_at).toLocaleString()}</p>
<p><strong>Blocked by:</strong> ${user.blocked_by}</p>
<div class="blocked-user-actions">
<button onclick="unblockUser('${user.user_id}', '${user.username}')" style="background: #4caf50;"> Unblock</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function exportAllDMs() {
try {
const result = await apiCall('/dms/users');
let exportCount = 0;
for (const user of (result.users || [])) {
try {
await exportUserDMs(user.user_id);
exportCount++;
} catch (e) {
console.error(`Failed to export DMs for user ${user.user_id}:`, e);
}
}
showNotification(`Exported DMs for ${exportCount} users`);
} catch (error) {
console.error('Failed to export all DMs:', error);
}
}