diff --git a/bot/static/css/style.css b/bot/static/css/style.css new file mode 100644 index 0000000..395068c --- /dev/null +++ b/bot/static/css/style.css @@ -0,0 +1,872 @@ +body { + margin: 0; + display: flex; + font-family: monospace; + background-color: #121212; + color: #fff; +} + +.panel { + width: 60%; + padding: 2rem; + box-sizing: border-box; +} + +.logs { + width: 40%; + height: 100vh; + background-color: #000; + color: #0f0; + padding: 1rem; + overflow-y: scroll; + font-size: 0.85rem; + border-left: 2px solid #333; + position: relative; +} + +#logs-content { + white-space: pre-wrap; + word-break: break-word; +} + +.log-line { line-height: 1.4; } +.log-line.log-error { color: #ff6b6b; } +.log-line.log-warning { color: #ffd93d; } +.log-line.log-info { color: #0f0; } +.log-line.log-debug { color: #888; } + +.logs-paused-indicator { + position: sticky; + top: 0; + background: rgba(50, 50, 0, 0.9); + color: #ffd93d; + text-align: center; + padding: 0.25rem; + font-size: 0.75rem; + cursor: pointer; + z-index: 10; + display: none; +} + +select, button, input { + margin: 0.4rem 0.5rem 0.4rem 0; + padding: 0.4rem; + background: #333; + color: #fff; + border: 1px solid #555; +} + +.section { + margin-bottom: 2rem; +} + +pre { + white-space: pre-wrap; + background: #1e1e1e; + padding: 1rem; + border: 1px solid #333; +} + +h1, h3 { + color: #61dafb; +} + +#notification { + position: fixed; + bottom: 20px; + right: 20px; + background-color: #222; + color: #fff; + padding: 1rem; + border: 1px solid #555; + border-radius: 8px; + opacity: 0.95; + display: none; + z-index: 3000; + font-size: 0.9rem; + transition: opacity 0.3s ease; +} + +.server-card { + background: #2a2a2a; + border: 1px solid #444; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.server-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.server-name { + font-size: 1.2rem; + font-weight: bold; + color: #61dafb; +} + +.server-actions { + display: flex; + gap: 0.5rem; +} + +.feature-tag { + display: inline-block; + background: #444; + padding: 0.2rem 0.5rem; + margin: 0.2rem; + border-radius: 4px; + font-size: 0.8rem; +} + +.add-server-form { + background: #1e1e1e; + border: 1px solid #333; + padding: 1rem; + margin: 1rem 0; + border-radius: 8px; +} + +.form-row { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + align-items: center; +} + +.form-group { + flex: 1; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #ccc; +} + +.checkbox-group { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.dm-users-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.dm-user-card { + background: #2a2a2a; + border: 1px solid #444; + border-radius: 8px; + padding: 1rem; + transition: all 0.3s ease; +} + +.dm-user-card:hover { + border-color: #666; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.dm-user-card h4 { + margin: 0 0 0.5rem 0; + color: #4CAF50; +} + +.dm-user-card p { + margin: 0.25rem 0; + font-size: 0.9rem; +} + +.dm-user-actions { + margin-top: 1rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Blocked Users Styles */ +.blocked-users-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.blocked-user-card { + background: #3d2a2a; + border: 1px solid #664444; + border-radius: 8px; + padding: 1rem; + transition: all 0.3s ease; +} + +.blocked-user-card:hover { + border-color: #886666; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); +} + +.blocked-user-card h4 { + margin: 0 0 0.5rem 0; + color: #ff9800; +} + +.blocked-user-card p { + margin: 0.25rem 0; + font-size: 0.9rem; +} + +.blocked-user-actions { + margin-top: 1rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Conversation View Styles */ +.message-reactions { + margin-top: 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.reaction-item { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + padding: 0.2rem 0.5rem; + font-size: 0.85rem; + transition: background 0.2s ease; +} + +.reaction-item:hover { + background: rgba(255,255,255,0.12); +} + +.reaction-emoji { + font-size: 1rem; +} + +.reaction-by { + color: #aaa; + font-size: 0.75rem; +} + +.reaction-by.bot-reaction { + color: #61dafb; +} + +.reaction-by.user-reaction { + color: #ffa726; +} + +.attachment { + margin: 0.25rem 0; +} + +.delete-message-btn { + opacity: 0.7; + transition: opacity 0.3s ease; +} + +.delete-message-btn:hover { + opacity: 1; +} + +.dm-user-actions button { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; +} + +.conversation-view { + background: #2a2a2a; + border: 1px solid #444; + border-radius: 8px; + padding: 1rem; +} + +.conversations-list { + max-height: 600px; + overflow-y: auto; + margin-top: 1rem; +} + +.conversation-message { + background: #333; + border: 1px solid #555; + border-radius: 6px; + padding: 0.75rem; + margin-bottom: 0.75rem; +} + +.conversation-message.user-message { + border-left: 4px solid #4CAF50; +} + +.conversation-message.bot-message { + border-left: 4px solid #2196F3; +} + +.message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.sender { + font-weight: bold; +} + +.timestamp { + color: #888; + font-size: 0.8rem; +} + +.message-content { + margin-bottom: 0.5rem; + line-height: 1.4; +} + +.message-attachments { + background: #444; + border-radius: 4px; + padding: 0.5rem; + font-size: 0.9rem; +} + +.attachment { + margin: 0.25rem 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.attachment a { + color: #4CAF50; + text-decoration: none; +} + +.attachment a:hover { + text-decoration: underline; +} + +/* Tab styling */ +.tab-container { + margin-bottom: 1rem; +} + +.tab-buttons { + display: grid; + grid-template-rows: repeat(2, auto); + grid-auto-flow: column; + grid-auto-columns: max-content; + border-bottom: 2px solid #333; + margin-bottom: 1rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + scrollbar-color: #555 #222; + row-gap: 0.05rem; + column-gap: 0.1rem; + padding-bottom: 0.1rem; +} + +.tab-buttons::-webkit-scrollbar { + height: 8px; +} + +.tab-buttons::-webkit-scrollbar-track { + background: #222; +} + +.tab-buttons::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; +} + +.tab-buttons::-webkit-scrollbar-thumb:hover { + background: #666; +} + +.tab-button { + background: #222; + color: #ccc; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + border-bottom: 3px solid transparent; + transition: all 0.3s ease; + white-space: nowrap; +} + +.tab-button:hover { + background: #333; + color: #fff; +} + +.tab-button.active { + background: #444; + color: #fff; + border-bottom-color: #4CAF50; +} + +/* Prompt source toggle buttons */ +.prompt-source-btn { + background: #333; + color: #aaa; +} +.prompt-source-btn.active { + background: #4CAF50; + color: #fff; +} +.prompt-source-btn:hover:not(.active) { + background: #444; + color: #ddd; +} + +/* Mood Activities Editor */ +.act-mood-row { + margin-bottom: 0.5rem; + border: 1px solid #3a3a3a; + border-radius: 4px; + overflow: hidden; +} +.act-mood-header { + cursor: pointer; + user-select: none; + padding: 0.5rem 0.75rem; + background: #2a2a2a; + display: flex; + align-items: center; + gap: 0.5rem; +} +.act-mood-header:hover { background: #333; } +.act-mood-header .act-mood-name { font-weight: bold; min-width: 120px; } +.act-mood-header .act-mood-stats { color: #888; font-size: 0.8rem; } +.act-mood-content { display: none; padding: 0.75rem; background: #1e1e1e; } +.act-entry { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0; + border-bottom: 1px solid #333; +} +.act-entry:last-child { border-bottom: none; } +.act-entry-icon { font-size: 1.1rem; min-width: 24px; text-align: center; } +.act-entry input[type="text"] { flex: 1; } +.act-entry input[type="number"] { width: 55px; } +.act-entry select { width: 130px; } +.act-toolbar { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid #444; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Tab loading spinner */ +.tab-loading-overlay { + display: flex; + align-items: center; + justify-content: center; + padding: 3rem 1rem; + color: #888; + font-size: 1rem; + gap: 0.75rem; +} +.tab-loading-overlay .spinner { + width: 24px; + height: 24px; + border: 3px solid #444; + border-top-color: #4CAF50; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Chat Interface Styles */ +.chat-message { + margin-bottom: 1rem; + padding: 1rem; + border-radius: 8px; + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.chat-message.user-message { + background: #2a3a4a; + border-left: 4px solid #4CAF50; + margin-left: 2rem; +} + +.chat-message.assistant-message { + background: #3a2a3a; + border-left: 4px solid #61dafb; + margin-right: 2rem; +} + +.chat-message.error-message { + background: #4a2a2a; + border-left: 4px solid #f44336; +} + +.chat-message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.chat-message-sender { + font-weight: bold; + color: #61dafb; +} + +.chat-message.user-message .chat-message-sender { + color: #4CAF50; +} + +.chat-message-time { + color: #888; + font-size: 0.8rem; +} + +.chat-message-content { + color: #ddd; + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; +} + +.chat-typing-indicator { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.5rem; +} + +.chat-typing-indicator span { + width: 8px; + height: 8px; + background: #61dafb; + border-radius: 50%; + animation: typing 1.4s infinite; +} + +.chat-typing-indicator span:nth-child(2) { + animation-delay: 0.2s; +} + +.chat-typing-indicator span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.7; } + 30% { transform: translateY(-10px); opacity: 1; } +} + +#chat-messages::-webkit-scrollbar { + width: 8px; +} + +#chat-messages::-webkit-scrollbar-track { + background: #1e1e1e; +} + +#chat-messages::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; +} + +#chat-messages::-webkit-scrollbar-thumb:hover { + background: #666; +} + +/* Evil Mode Styles */ +body.evil-mode h1, body.evil-mode h3 { + color: #ff4444; +} + +body.evil-mode .tab-button.active { + border-bottom-color: #ff4444; +} + +body.evil-mode #evil-mode-toggle { + background: #ff4444; + border-color: #ff4444; + color: #000; +} + +body.evil-mode .server-name { + color: #ff4444; +} + +body.evil-mode .chat-message-sender { + color: #ff4444; +} + +body.evil-mode .chat-message.assistant-message { + border-left-color: #ff4444; +} + +body.evil-mode #notification { + border-color: #ff4444; +} + +/* Override any blue status text in evil mode */ +body.evil-mode [style*="color: #007bff"], +body.evil-mode [style*="color: rgb(0, 123, 255)"] { + color: #ff4444 !important; +} + +/* Bipolar Mode Styles */ +#bipolar-section { + transition: all 0.3s ease; +} + +#bipolar-section h3 { + margin-top: 0; +} + +#bipolar-mode-toggle.bipolar-active { + background: #9932CC !important; + border-color: #9932CC !important; +} + +/* Responsive breakpoints */ +@media (max-width: 1200px) { + .panel { width: 55%; padding: 1.5rem; } + .logs { width: 45%; } +} + +@media (max-width: 1024px) { + body { flex-direction: column; } + .panel { width: 100%; padding: 1.5rem; } + .logs { + width: 100%; + height: 300px; + border-left: none; + border-top: 2px solid #333; + } +} + +@media (max-width: 768px) { + .panel { padding: 1rem; } + .tab-buttons { + grid-template-rows: none; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + } + .tab-button { font-size: 0.85rem; padding: 0.4rem 0.6rem; } +} + +@media (max-width: 480px) { + .panel { padding: 0.5rem; } + .tab-buttons { grid-template-columns: 1fr 1fr; } + .tab-button { font-size: 0.8rem; padding: 0.35rem 0.5rem; } + h1 { font-size: 1.2rem; } +} + +/* Profile Picture Tab Styles */ +.pfp-preview-container { + display: flex; + gap: 2rem; + margin: 1.5rem 0; + align-items: flex-start; + flex-wrap: wrap; +} +.pfp-preview-box { + text-align: center; +} +.pfp-preview-box img { + max-width: 400px; + max-height: 400px; + border: 2px solid #444; + border-radius: 8px; + background: #1e1e1e; +} +.pfp-preview-box .label { + display: block; + margin-bottom: 0.5rem; + color: #aaa; + font-size: 0.9rem; +} +.pfp-crop-container { + max-width: 100%; + max-height: 550px; + background: #111; + border: 2px solid #555; + border-radius: 8px; + overflow: hidden; + margin: 1rem 0; +} +.pfp-crop-container img { + display: block; + max-width: 100%; +} +.crop-mode-toggle { + display: flex; + gap: 1.5rem; + margin: 1rem 0; + align-items: center; +} +.crop-mode-toggle label { + display: flex; + align-items: center; + gap: 0.4rem; + cursor: pointer; + color: #ccc; +} +.crop-mode-toggle input[type="radio"] { + accent-color: #4CAF50; +} +.pfp-description-editor { + width: 100%; + min-height: 120px; + background: #1e1e1e; + color: #ddd; + border: 1px solid #444; + border-radius: 4px; + padding: 0.75rem; + font-family: monospace; + font-size: 0.9rem; + resize: vertical; +} +.pfp-description-editor:focus { + border-color: #61dafb; + outline: none; +} +/* Album / Gallery grid */ +.album-section { + margin: 1.5rem 0; + padding: 1rem; + background: #1a1a2e; + border: 1px solid #444; + border-radius: 8px; +} +.album-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; +} +.album-header h4 { margin: 0; } +.album-toolbar { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + align-items: center; + margin: 0.75rem 0; +} +.album-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 0.75rem; + max-height: 480px; + overflow-y: auto; + padding: 0.25rem; +} +.album-card { + position: relative; + border: 2px solid #444; + border-radius: 6px; + overflow: hidden; + cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s; + background: #111; +} +.album-card:hover { border-color: #61dafb; } +.album-card.selected { border-color: #4CAF50; box-shadow: 0 0 8px rgba(76,175,80,0.4); } +.album-card.checked { border-color: #ff9800; } +.album-card img { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + display: block; +} +.album-card .album-check { + position: absolute; + top: 4px; + left: 4px; + z-index: 2; + accent-color: #ff9800; +} +.album-card .album-card-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0,0,0,0.7); + padding: 2px 4px; + font-size: 0.7rem; + color: #ccc; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.album-card .color-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + border: 1px solid #888; + vertical-align: middle; + margin-right: 3px; +} +.album-detail { + margin-top: 1rem; + padding: 1rem; + background: #222; + border: 1px solid #555; + border-radius: 8px; +} +.album-detail-previews { + display: flex; + gap: 1.5rem; + flex-wrap: wrap; + align-items: flex-start; + margin: 1rem 0; +} +.album-detail-previews .pfp-preview-box img { + max-width: 300px; + max-height: 300px; +} +.album-disk-usage { + font-size: 0.8rem; + color: #888; + margin-left: auto; +} diff --git a/bot/static/index.html b/bot/static/index.html index d4da1ef..c928e66 100644 --- a/bot/static/index.html +++ b/bot/static/index.html @@ -6,880 +6,7 @@ Miku Control Panel - + @@ -2209,4983 +1336,15 @@ - - + + + + + + + + + + - diff --git a/bot/static/js/actions.js b/bot/static/js/actions.js new file mode 100644 index 0000000..ebd7cb2 --- /dev/null +++ b/bot/static/js/actions.js @@ -0,0 +1,432 @@ +// ============================================================================ +// Miku Control Panel — Actions Module +// Autonomous actions, manual actions, custom prompts, reactions +// ============================================================================ + +// ===== Autonomous Actions ===== + +async function triggerAutonomous(actionType) { + const selectedServer = document.getElementById('server-select').value; + + if (!actionType) { + showNotification('No action type specified', 'error'); + return; + } + + try { + let endpoint = `/autonomous/${actionType}`; + + if (selectedServer !== 'all') { + endpoint += `?guild_id=${selectedServer}`; + } + + const result = await apiCall(endpoint, 'POST'); + showNotification(result.message || 'Action triggered successfully'); + } catch (error) { + console.error('Failed to trigger autonomous action:', error); + } +} + +function toggleEngageSubmenu() { + const submenu = document.getElementById('engage-submenu'); + submenu.style.display = submenu.style.display === 'none' ? 'block' : 'none'; +} + +async function triggerEngageUser() { + const selectedServer = document.getElementById('server-select').value; + const userId = document.getElementById('engage-user-id').value.trim(); + const engageType = document.querySelector('input[name="engage-type"]:checked').value; + + try { + let endpoint = '/autonomous/engage'; + const params = new URLSearchParams(); + + if (selectedServer !== 'all') { + params.append('guild_id', selectedServer); + } + + if (userId) { + params.append('user_id', userId); + } + + if (engageType !== 'random') { + params.append('engagement_type', engageType); + } + + params.append('manual_trigger', 'true'); + + if (params.toString()) { + endpoint += `?${params.toString()}`; + } + + const result = await apiCall(endpoint, 'POST'); + showNotification(result.message || 'Engagement triggered successfully'); + } catch (error) { + console.error('Failed to trigger user engagement:', error); + } +} + +function toggleTweetSubmenu() { + const submenu = document.getElementById('tweet-submenu'); + submenu.style.display = submenu.style.display === 'none' ? 'block' : 'none'; +} + +async function triggerShareTweet() { + const selectedServer = document.getElementById('server-select').value; + const tweetUrl = document.getElementById('tweet-url').value.trim(); + + if (tweetUrl) { + const validDomains = ['x.com', 'twitter.com', 'fxtwitter.com']; + let isValid = false; + + try { + const urlObj = new URL(tweetUrl); + const hostname = urlObj.hostname.toLowerCase(); + isValid = validDomains.some(domain => hostname === domain || hostname.endsWith('.' + domain)); + } catch (e) {} + + if (!isValid) { + showNotification('Invalid tweet URL. Must be from x.com, twitter.com, or fxtwitter.com', 'error'); + return; + } + } + + try { + let endpoint = '/autonomous/tweet'; + const params = new URLSearchParams(); + + if (selectedServer !== 'all') { + params.append('guild_id', selectedServer); + } + + if (tweetUrl) { + params.append('tweet_url', tweetUrl); + } + + if (params.toString()) { + endpoint += `?${params.toString()}`; + } + + const result = await apiCall(endpoint, 'POST'); + showNotification(result.message || 'Tweet share triggered successfully'); + } catch (error) { + console.error('Failed to trigger tweet share:', error); + } +} + +// ===== Manual Actions ===== + +async function forceSleep() { + try { + await apiCall('/sleep', 'POST'); + showNotification('Miku is now sleeping'); + } catch (error) { + console.error('Failed to force sleep:', error); + } +} + +async function wakeUp() { + try { + await apiCall('/wake', 'POST'); + showNotification('Miku is now awake'); + } catch (error) { + console.error('Failed to wake up:', error); + } +} + +async function sendBedtime() { + const selectedServer = document.getElementById('manual-server-select').value; + + console.log('🛏ïļ sendBedtime() called'); + console.log('🛏ïļ Selected server value:', selectedServer); + + try { + let endpoint = '/bedtime'; + + if (selectedServer !== 'all') { + console.log('🛏ïļ Using guild_id (as string):', selectedServer); + endpoint += `?guild_id=${selectedServer}`; + } + + console.log('🛏ïļ Final endpoint:', endpoint); + + const result = await apiCall(endpoint, 'POST'); + showNotification(result.message || 'Bedtime reminder sent successfully'); + } catch (error) { + console.error('Failed to send bedtime reminder:', error); + } +} + +async function resetConversation() { + const userId = prompt('Enter user ID to reset conversation for:'); + if (userId) { + try { + await apiCall('/conversation/reset', 'POST', { user_id: userId }); + showNotification('Conversation reset'); + } catch (error) { + console.error('Failed to reset conversation:', error); + } + } +} + +// ===== Manual Message ===== + +async function sendManualMessage() { + const message = document.getElementById('manualMessage').value.trim(); + const files = document.getElementById('manualAttachment').files; + const targetType = document.getElementById('manual-target-type').value; + const replyMessageId = document.getElementById('manualReplyMessageId').value.trim(); + const replyMention = document.querySelector('input[name="manualReplyMention"]:checked').value === 'true'; + const useWebhook = document.getElementById('manual-use-webhook').checked; + const webhookPersona = document.querySelector('input[name="webhook-persona"]:checked')?.value || 'miku'; + + if (!message) { + showNotification('Please enter a message', 'error'); + return; + } + + if (useWebhook && targetType === 'dm') { + showNotification('Webhooks only work in channels, not DMs', 'error'); + return; + } + + let targetId, endpoint; + + if (targetType === 'dm') { + targetId = document.getElementById('manualUserId').value.trim(); + if (!targetId) { + showNotification('Please enter a user ID for DM', 'error'); + return; + } + endpoint = `/dm/${targetId}/manual`; + } else { + targetId = document.getElementById('manualChannelId').value.trim(); + if (!targetId) { + showNotification('Please enter a channel ID', 'error'); + return; + } + endpoint = useWebhook ? '/manual/send-webhook' : '/manual/send'; + } + + try { + const formData = new FormData(); + formData.append('message', message); + + if (useWebhook) { + formData.append('persona', webhookPersona); + } + + if (replyMessageId) { + formData.append('reply_to_message_id', replyMessageId); + formData.append('mention_author', replyMention); + } + + if (targetType === 'dm') { + if (files.length > 0) { + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + } + } else { + formData.append('channel_id', targetId); + if (files.length > 0) { + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + } + } + + const response = await fetch(endpoint, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (response.ok) { + showNotification('Message sent successfully'); + document.getElementById('manualMessage').value = ''; + document.getElementById('manualAttachment').value = ''; + document.getElementById('manualReplyMessageId').value = ''; + if (targetType === 'dm') { + document.getElementById('manualUserId').value = ''; + } else { + document.getElementById('manualChannelId').value = ''; + } + document.getElementById('manualStatus').textContent = '✅ Message sent successfully!'; + document.getElementById('manualStatus').style.color = 'green'; + } else { + throw new Error(result.message || 'Failed to send message'); + } + } catch (error) { + console.error('Failed to send manual message:', error); + showNotification(error.message || 'Failed to send message', 'error'); + document.getElementById('manualStatus').textContent = '❌ Failed to send message'; + document.getElementById('manualStatus').style.color = 'red'; + } +} + +// ===== Custom Prompt ===== + +function toggleCustomPromptTarget() { + const targetType = document.getElementById('custom-prompt-target-type').value; + const serverSection = document.getElementById('custom-prompt-server-section'); + const dmSection = document.getElementById('custom-prompt-dm-section'); + + if (targetType === 'dm') { + serverSection.style.display = 'none'; + dmSection.style.display = 'inline'; + } else { + serverSection.style.display = 'inline'; + dmSection.style.display = 'none'; + } +} + +function toggleWebhookOptions() { + const useWebhook = document.getElementById('manual-use-webhook').checked; + const webhookOptions = document.getElementById('webhook-persona-options'); + const targetType = document.getElementById('manual-target-type'); + + if (useWebhook) { + webhookOptions.style.display = 'block'; + if (targetType.value === 'dm') { + targetType.value = 'channel'; + toggleManualMessageTarget(); + } + targetType.options[1].disabled = true; + } else { + webhookOptions.style.display = 'none'; + targetType.options[1].disabled = false; + } +} + +function toggleManualMessageTarget() { + const targetType = document.getElementById('manual-target-type').value; + const channelSection = document.getElementById('manual-channel-section'); + const dmSection = document.getElementById('manual-dm-section'); + + if (targetType === 'dm') { + channelSection.style.display = 'none'; + dmSection.style.display = 'block'; + } else { + channelSection.style.display = 'block'; + dmSection.style.display = 'none'; + } +} + +async function sendCustomPrompt() { + const prompt = document.getElementById('customPrompt').value.trim(); + const targetType = document.getElementById('custom-prompt-target-type').value; + const files = document.getElementById('customPromptAttachment').files; + + if (!prompt) { + showNotification('Please enter a custom prompt', 'error'); + return; + } + + try { + let endpoint; + + if (targetType === 'dm') { + const userId = document.getElementById('custom-prompt-user-id').value.trim(); + if (!userId) { + showNotification('Please enter a user ID for DM', 'error'); + return; + } + endpoint = `/dm/${userId}/custom`; + } else { + const selectedServer = document.getElementById('custom-prompt-server-select').value; + endpoint = '/autonomous/custom'; + + if (selectedServer !== 'all') { + endpoint += `?guild_id=${selectedServer}`; + } + } + + const result = await apiCall(endpoint, 'POST', { prompt: prompt }); + + showNotification(result.message || 'Custom prompt sent successfully'); + document.getElementById('customPrompt').value = ''; + document.getElementById('customPromptAttachment').value = ''; + if (targetType === 'dm') { + document.getElementById('custom-prompt-user-id').value = ''; + } + document.getElementById('customStatus').textContent = '✅ Custom prompt sent successfully!'; + document.getElementById('customStatus').style.color = 'green'; + } catch (error) { + console.error('Failed to send custom prompt:', error); + document.getElementById('customStatus').textContent = '❌ Failed to send custom prompt'; + document.getElementById('customStatus').style.color = 'red'; + } +} + +function toggleCustomPrompt() { + const customPromptSection = document.getElementById('custom-prompt-section'); + if (customPromptSection) { + customPromptSection.style.display = customPromptSection.style.display === 'none' ? 'block' : 'none'; + } +} + +// ===== Add Reaction ===== + +async function addReactionToMessage() { + const messageId = document.getElementById('reactionMessageId').value.trim(); + const channelId = document.getElementById('reactionChannelId').value.trim(); + const emoji = document.getElementById('reactionEmoji').value.trim(); + const statusElement = document.getElementById('reactionStatus'); + + if (!messageId) { + showNotification('Please enter a message ID', 'error'); + statusElement.textContent = '❌ Message ID is required'; + statusElement.style.color = 'red'; + return; + } + + if (!channelId) { + showNotification('Please enter a channel ID', 'error'); + statusElement.textContent = '❌ Channel ID is required'; + statusElement.style.color = 'red'; + return; + } + + if (!emoji) { + showNotification('Please enter an emoji', 'error'); + statusElement.textContent = '❌ Emoji is required'; + statusElement.style.color = 'red'; + return; + } + + try { + statusElement.textContent = 'âģ Adding reaction...'; + statusElement.style.color = '#61dafb'; + + const formData = new FormData(); + formData.append('message_id', messageId); + formData.append('channel_id', channelId); + formData.append('emoji', emoji); + + const response = await fetch('/messages/react', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (response.ok && result.status === 'ok') { + showNotification(`Reaction ${emoji} added successfully`); + statusElement.textContent = `✅ Reaction ${emoji} added successfully!`; + statusElement.style.color = 'green'; + + document.getElementById('reactionMessageId').value = ''; + document.getElementById('reactionChannelId').value = ''; + document.getElementById('reactionEmoji').value = ''; + } else { + throw new Error(result.message || 'Failed to add reaction'); + } + } catch (error) { + console.error('Failed to add reaction:', error); + showNotification(error.message || 'Failed to add reaction', 'error'); + statusElement.textContent = `❌ ${error.message || 'Failed to add reaction'}`; + statusElement.style.color = 'red'; + } +} diff --git a/bot/static/js/chat.js b/bot/static/js/chat.js new file mode 100644 index 0000000..06ea22d --- /dev/null +++ b/bot/static/js/chat.js @@ -0,0 +1,498 @@ +// ============================================================================ +// Miku Control Panel — Chat Interface + Voice Call Module +// ============================================================================ + +// Toggle image upload section based on model type +function toggleChatImageUpload() { + const modelType = document.querySelector('input[name="chat-model-type"]:checked').value; + const imageUploadSection = document.getElementById('chat-image-upload-section'); + + if (modelType === 'vision') { + imageUploadSection.style.display = 'block'; + } else { + imageUploadSection.style.display = 'none'; + } +} + +// Load voice debug mode setting from server +async function loadVoiceDebugMode() { + try { + const data = await apiCall('/voice/debug-mode'); + const checkbox = document.getElementById('voice-debug-mode'); + if (checkbox && data.debug_mode !== undefined) { + checkbox.checked = data.debug_mode; + } + } catch (error) { + console.error('Failed to load voice debug mode:', error); + } +} + +// Handle Enter key in chat input +function handleChatKeyPress(event) { + if (event.ctrlKey && event.key === 'Enter') { + event.preventDefault(); + sendChatMessage(); + } +} + +// Clear chat history +function clearChatHistory() { + if (confirm('Are you sure you want to clear all chat messages?')) { + const chatMessages = document.getElementById('chat-messages'); + chatMessages.innerHTML = ` +
+ 💎 Start chatting with the LLM! Your conversation will appear here. +
+ `; + // Clear conversation history array + chatConversationHistory = []; + showNotification('Chat history cleared'); + } +} + +// Add a message to the chat display +function addChatMessage(sender, content, isError = false) { + const chatMessages = document.getElementById('chat-messages'); + + // Remove welcome message if it exists + const welcomeMsg = chatMessages.querySelector('div[style*="text-align: center"]'); + if (welcomeMsg) { + welcomeMsg.remove(); + } + + const messageDiv = document.createElement('div'); + const messageClass = isError ? 'error-message' : (sender === 'You' ? 'user-message' : 'assistant-message'); + messageDiv.className = `chat-message ${messageClass}`; + + const timestamp = new Date().toLocaleTimeString(); + + messageDiv.innerHTML = ` +
+ ${escapeHtml(sender)} + ${timestamp} +
+
+ `; + + // Set content via textContent to prevent XSS + messageDiv.querySelector('.chat-message-content').textContent = content; + + chatMessages.appendChild(messageDiv); + + // Scroll to bottom + chatMessages.scrollTop = chatMessages.scrollHeight; + + return messageDiv; +} + +// Add typing indicator +function showTypingIndicator() { + const chatMessages = document.getElementById('chat-messages'); + + const typingDiv = document.createElement('div'); + typingDiv.id = 'chat-typing-indicator'; + typingDiv.className = 'chat-message assistant-message'; + typingDiv.innerHTML = ` +
+ Miku + typing... +
+
+ + + +
+ `; + + chatMessages.appendChild(typingDiv); + chatMessages.scrollTop = chatMessages.scrollHeight; +} + +// Remove typing indicator +function hideTypingIndicator() { + const typingIndicator = document.getElementById('chat-typing-indicator'); + if (typingIndicator) { + typingIndicator.remove(); + } +} + +// Send chat message with streaming support +async function sendChatMessage() { + const input = document.getElementById('chat-input'); + const message = input.value.trim(); + + if (!message) { + showNotification('Please enter a message', 'error'); + return; + } + + // Get configuration + const modelType = document.querySelector('input[name="chat-model-type"]:checked').value; + const useSystemPrompt = document.querySelector('input[name="chat-system-prompt"]:checked').value === 'true'; + const selectedMood = document.getElementById('chat-mood-select').value; + + // Get image data if vision model + let imageData = null; + if (modelType === 'vision') { + const imageFile = document.getElementById('chat-image-file').files[0]; + if (imageFile) { + try { + imageData = await readFileAsBase64(imageFile); + // Remove data URL prefix if present + if (imageData.includes(',')) { + imageData = imageData.split(',')[1]; + } + } catch (error) { + showNotification('Failed to read image file', 'error'); + return; + } + } + } + + // Disable send button + const sendBtn = document.getElementById('chat-send-btn'); + const originalBtnText = sendBtn.innerHTML; + sendBtn.disabled = true; + sendBtn.innerHTML = 'âģ Sending...'; + + // Add user message to display + addChatMessage('You', message); + + // Clear input + input.value = ''; + + // Show typing indicator + showTypingIndicator(); + + try { + // Build user message for history + let userMessageContent; + if (modelType === 'vision' && imageData) { + // Vision model with image - store as multimodal content + userMessageContent = [ + { + "type": "text", + "text": message + }, + { + "type": "image_url", + "image_url": { + "url": `data:image/jpeg;base64,${imageData}` + } + } + ]; + } else { + // Text-only message + userMessageContent = message; + } + + // Prepare request payload with conversation history + const payload = { + message: message, + model_type: modelType, + use_system_prompt: useSystemPrompt, + image_data: imageData, + conversation_history: chatConversationHistory, + mood: selectedMood + }; + + // Make streaming request + const response = await fetch('/chat/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Hide typing indicator + hideTypingIndicator(); + + // Create message element for streaming response + const assistantName = useSystemPrompt ? 'Miku' : 'LLM'; + const responseDiv = addChatMessage(assistantName, ''); + const contentDiv = responseDiv.querySelector('.chat-message-content'); + + // Read stream + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let fullResponse = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // Process complete SSE messages + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.startsWith('data: ')) { + const dataStr = line.slice(6); + try { + const data = JSON.parse(dataStr); + + if (data.error) { + contentDiv.textContent = `❌ Error: ${data.error}`; + responseDiv.classList.add('error-message'); + break; + } + + if (data.content) { + fullResponse += data.content; + contentDiv.textContent = fullResponse; + + // Auto-scroll + const chatMessages = document.getElementById('chat-messages'); + chatMessages.scrollTop = chatMessages.scrollHeight; + } + + if (data.done) { + break; + } + } catch (e) { + console.error('Failed to parse SSE data:', e); + } + } + } + } + + // If no response was received, show error + if (!fullResponse) { + contentDiv.textContent = '❌ No response received from LLM'; + responseDiv.classList.add('error-message'); + } else { + // Add user message to conversation history + chatConversationHistory.push({ + role: "user", + content: userMessageContent + }); + + // Add assistant response to conversation history + chatConversationHistory.push({ + role: "assistant", + content: fullResponse + }); + + console.log('💎 Conversation history updated:', chatConversationHistory.length, 'messages'); + } + + } catch (error) { + console.error('Chat error:', error); + hideTypingIndicator(); + addChatMessage('Error', `Failed to send message: ${error.message}`, true); + showNotification('Failed to send message', 'error'); + } finally { + // Re-enable send button + sendBtn.disabled = false; + sendBtn.innerHTML = originalBtnText; + } +} + +// Helper function to read file as base64 +function readFileAsBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +// ============================================================================ +// Voice Call Management Functions +// ============================================================================ + +async function initiateVoiceCall() { + const userId = document.getElementById('voice-user-id').value.trim(); + const channelId = document.getElementById('voice-channel-id').value.trim(); + const debugMode = document.getElementById('voice-debug-mode').checked; + + // Validation + if (!userId) { + showNotification('Please enter a user ID', 'error'); + return; + } + + if (!channelId) { + showNotification('Please enter a voice channel ID', 'error'); + return; + } + + // Check if user IDs are valid (numeric) + if (isNaN(userId) || isNaN(channelId)) { + showNotification('User ID and Channel ID must be numeric', 'error'); + return; + } + + // Set debug mode + try { + const debugFormData = new FormData(); + debugFormData.append('enabled', debugMode); + await fetch('/voice/debug-mode', { + method: 'POST', + body: debugFormData + }); + } catch (error) { + console.error('Failed to set debug mode:', error); + } + + // Disable button and show status + const callBtn = document.getElementById('voice-call-btn'); + const cancelBtn = document.getElementById('voice-call-cancel-btn'); + const statusDiv = document.getElementById('voice-call-status'); + const statusText = document.getElementById('voice-call-status-text'); + + callBtn.disabled = true; + statusDiv.style.display = 'block'; + cancelBtn.style.display = 'inline-block'; + voiceCallActive = true; + + try { + statusText.innerHTML = 'âģ Starting STT and TTS containers...'; + + const formData = new FormData(); + formData.append('user_id', userId); + formData.append('voice_channel_id', channelId); + + const response = await fetch('/voice/call', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + // Check for HTTP error status (422 validation error, etc.) + if (!response.ok) { + let errorMsg = data.error || data.detail || 'Unknown error'; + // Handle FastAPI validation errors + if (data.detail && Array.isArray(data.detail)) { + errorMsg = data.detail.map(e => `${e.loc.join('.')}: ${e.msg}`).join(', '); + } + statusText.innerHTML = `❌ Error: ${errorMsg}`; + showNotification(`Voice call failed: ${errorMsg}`, 'error'); + callBtn.disabled = false; + cancelBtn.style.display = 'none'; + voiceCallActive = false; + return; + } + + if (!data.success) { + statusText.innerHTML = `❌ Error: ${data.error}`; + showNotification(`Voice call failed: ${data.error}`, 'error'); + callBtn.disabled = false; + cancelBtn.style.display = 'none'; + voiceCallActive = false; + return; + } + + // Success! + statusText.innerHTML = `✅ Voice call initiated!
User ID: ${data.user_id}
Channel: ${data.channel_id}`; + + // Show invite link + const inviteDiv = document.getElementById('voice-call-invite-link'); + const inviteUrl = document.getElementById('voice-call-invite-url'); + inviteUrl.href = data.invite_url; + inviteUrl.textContent = data.invite_url; + inviteDiv.style.display = 'block'; + + // Add to call history + addVoiceCallToHistory(userId, channelId, data.invite_url); + + showNotification('Voice call initiated successfully!', 'success'); + + // Auto-reset after 5 minutes (call should be done by then or timed out) + setTimeout(() => { + if (voiceCallActive) { + resetVoiceCall(); + } + }, 300000); // 5 minutes + + } catch (error) { + console.error('Voice call error:', error); + statusText.innerHTML = `❌ Error: ${error.message}`; + showNotification(`Voice call error: ${error.message}`, 'error'); + callBtn.disabled = false; + cancelBtn.style.display = 'none'; + voiceCallActive = false; + } +} + +function cancelVoiceCall() { + resetVoiceCall(); + showNotification('Voice call cancelled', 'info'); +} + +function resetVoiceCall() { + const callBtn = document.getElementById('voice-call-btn'); + const cancelBtn = document.getElementById('voice-call-cancel-btn'); + const statusDiv = document.getElementById('voice-call-status'); + + callBtn.disabled = false; + cancelBtn.style.display = 'none'; + statusDiv.style.display = 'none'; + voiceCallActive = false; + + // Clear inputs + document.getElementById('voice-user-id').value = ''; + document.getElementById('voice-channel-id').value = ''; +} + +function addVoiceCallToHistory(userId, channelId, inviteUrl) { + const now = new Date(); + const timestamp = now.toLocaleTimeString(); + + const callEntry = { + userId: userId, + channelId: channelId, + inviteUrl: inviteUrl, + timestamp: timestamp + }; + + voiceCallHistory.unshift(callEntry); // Add to front + + // Keep only last 10 calls + if (voiceCallHistory.length > 10) { + voiceCallHistory.pop(); + } + + updateVoiceCallHistoryDisplay(); +} + +function updateVoiceCallHistoryDisplay() { + const historyDiv = document.getElementById('voice-call-history'); + + if (voiceCallHistory.length === 0) { + historyDiv.innerHTML = '
No calls yet. Start one above!
'; + return; + } + + let html = ''; + voiceCallHistory.forEach((call, index) => { + html += ` +
+
+
+ ${call.timestamp} +
+ User: ${call.userId} | Channel: ${call.channelId} +
+
+ + View Link → + +
+
+ `; + }); + + historyDiv.innerHTML = html; +} diff --git a/bot/static/js/core.js b/bot/static/js/core.js new file mode 100644 index 0000000..a96119e --- /dev/null +++ b/bot/static/js/core.js @@ -0,0 +1,412 @@ +// ============================================================================ +// 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, '>'); +} + +// ============================================================================ +// 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 = '
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 `
${escapeHtml(line)}
`; + }).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(); + } + }); +}); diff --git a/bot/static/js/dm.js b/bot/static/js/dm.js new file mode 100644 index 0000000..287e37f --- /dev/null +++ b/bot/static/js/dm.js @@ -0,0 +1,548 @@ +// ============================================================================ +// 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 = '

No DM conversations found.

'; + return; + } + + let html = '
'; + + 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 += ` +
+

ðŸ‘Ī ${user.username}

+

ID: ${user.user_id}

+

Total Messages: ${user.total_messages}

+

User Messages: ${user.user_messages}

+

Bot Messages: ${user.bot_messages}

+

Last Activity: ${lastTime}

+

Last Message: ${lastMessage}

+ +
+ `; + }); + + html += '
'; + 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 = ` +
+ +

💎 Conversations with User ${userId}

+
+ `; + + if (!conversations || conversations.length === 0) { + html += '

No conversations found for this user.

'; + } 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 ? + `` : ''; + + html += ` +
+
+ ${sender} + ${timestamp} + ${deleteButton} +
+
${content}
+ ${msg.attachments && msg.attachments.length > 0 ? ` +
+ 📎 Attachments: + ${msg.attachments.map(att => ` +
+ - ${att.filename} (${att.size} bytes) + 🔗 View +
+ `).join('')} +
+ ` : ''} + ${msg.reactions && msg.reactions.length > 0 ? ` +
+ ${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 ` +
+ ${reaction.emoji} + ${reactorLabel} +
+ `; + }).join('')} +
+ ` : ''} +
+ `; + }); + } + + html += ` +
+
+ `; + + 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 = ` +
+

No analysis reports found yet.

+ +
+ `; + return; + } + + let html = ` +
+ + ${reports.length} reports found +
+
+ `; + + 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 += ` +
+
+
+

${sentimentEmoji} ${report.username}

+

User ID: ${report.user_id}

+
+
+
+ ${report.sentiment_score > 0 ? '+' : ''}${report.sentiment_score}/10 +
+
+ ${report.overall_sentiment} +
+
+
+ +
+ Miku's Feelings: +

"${report.your_feelings}"

+
+ + ${report.notable_moment ? ` +
+ Notable Moment: +

"${report.notable_moment}"

+
+ ` : ''} + + ${report.key_behaviors && report.key_behaviors.length > 0 ? ` +
+ Key Behaviors: +
    + ${report.key_behaviors.slice(0, 5).map(b => `
  • ${b}
  • `).join('')} +
+
+ ` : ''} + +
+ 📅 ${timestamp} + 💎 ${report.message_count} messages analyzed + 📄 ${report.filename} +
+
+ `; + }); + + html += '
'; + 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 = '

No blocked users.

'; + return; + } + + let html = '
'; + + blockedUsers.forEach(user => { + html += ` +
+

ðŸšŦ ${user.username}

+

ID: ${user.user_id}

+

Blocked: ${new Date(user.blocked_at).toLocaleString()}

+

Blocked by: ${user.blocked_by}

+ +
+ `; + }); + + html += '
'; + 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); + } +} diff --git a/bot/static/js/image-gen.js b/bot/static/js/image-gen.js new file mode 100644 index 0000000..4ccb0af --- /dev/null +++ b/bot/static/js/image-gen.js @@ -0,0 +1,127 @@ +// ============================================================================ +// Miku Control Panel — Image Generation Module +// ============================================================================ + +async function checkImageSystemStatus() { + try { + const statusDisplay = document.getElementById('image-status-display'); + statusDisplay.innerHTML = '🔄 Checking system status...'; + + const result = await apiCall('/image/status'); + + const workflowStatus = result.workflow_template_exists ? '✅ Found' : '❌ Missing'; + const comfyuiStatus = result.comfyui_running ? '✅ Running' : '❌ Not running'; + + statusDisplay.innerHTML = ` +System Status: +â€Ē Workflow Template (Miku_BasicWorkflow.json): ${workflowStatus} +â€Ē ComfyUI Server: ${comfyuiStatus} +${result.comfyui_running ? `â€Ē Detected ComfyUI URL: ${result.comfyui_url}` : ''} + +Overall Status: ${result.ready ? '✅ Ready for image generation' : '⚠ïļ Setup required'} + +${!result.workflow_template_exists ? '⚠ïļ Place Miku_BasicWorkflow.json in bot directory\n' : ''}${!result.comfyui_running ? '⚠ïļ Start ComfyUI server on localhost:8188 (bot will auto-detect correct URL)\n' : ''}`; + } catch (error) { + console.error('Failed to check image system status:', error); + document.getElementById('image-status-display').innerHTML = `❌ Error: ${error.message}`; + } +} + +async function testImageDetection() { + const message = document.getElementById('detection-test-message').value.trim(); + const resultsDiv = document.getElementById('detection-test-results'); + + if (!message) { + resultsDiv.innerHTML = '❌ Please enter a test message'; + resultsDiv.style.color = 'red'; + return; + } + + try { + resultsDiv.innerHTML = '🔍 Testing detection...'; + resultsDiv.style.color = '#4CAF50'; + + const result = await apiCall('/image/test-detection', 'POST', { message: message }); + + const detectionIcon = result.is_image_request ? '✅' : '❌'; + const detectionText = result.is_image_request ? 'WILL trigger image generation' : 'will NOT trigger image generation'; + + resultsDiv.innerHTML = ` +Detection Result: ${detectionIcon} This message ${detectionText} +${result.is_image_request ? `
Extracted Prompt: "${result.extracted_prompt}"` : ''} +
Original Message: "${result.original_message}"`; + + resultsDiv.style.color = result.is_image_request ? '#4CAF50' : '#ff9800'; + } catch (error) { + console.error('Failed to test image detection:', error); + resultsDiv.innerHTML = `❌ Error: ${error.message}`; + resultsDiv.style.color = 'red'; + } +} + +async function generateManualImage() { + const prompt = document.getElementById('manual-image-prompt').value.trim(); + const statusDiv = document.getElementById('manual-image-status'); + const previewDiv = document.getElementById('manual-image-preview'); + + if (!prompt) { + statusDiv.innerHTML = '❌ Please enter an image prompt'; + statusDiv.style.color = 'red'; + return; + } + + try { + previewDiv.innerHTML = ''; + + statusDiv.innerHTML = 'ðŸŽĻ Generating image... This may take a few minutes.'; + statusDiv.style.color = '#4CAF50'; + + const result = await apiCall('/image/generate', 'POST', { prompt: prompt }); + + statusDiv.innerHTML = `✅ Image generated successfully!`; + statusDiv.style.color = '#4CAF50'; + + if (result.image_path) { + const filename = result.image_path.split('/').pop(); + const imageUrl = `/image/view/${encodeURIComponent(filename)}`; + + const imgContainer = document.createElement('div'); + imgContainer.style.cssText = 'background: #1e1e1e; padding: 1rem; border-radius: 8px; border: 1px solid #333;'; + + const img = document.createElement('img'); + img.src = imageUrl; + img.alt = 'Generated Image'; + img.style.cssText = 'max-width: 100%; max-height: 600px; border-radius: 4px; display: block; margin: 0 auto;'; + + img.onload = function() { + console.log('Image loaded successfully:', imageUrl); + }; + + img.onerror = function() { + console.error('Failed to load image:', imageUrl); + imgContainer.innerHTML = ` +
+ ❌ Failed to load image
+ Path: ${result.image_path}
+ URL: ${imageUrl} +
+ `; + }; + + imgContainer.appendChild(img); + + const pathInfo = document.createElement('div'); + pathInfo.style.cssText = 'margin-top: 0.5rem; color: #aaa; font-size: 0.85rem; text-align: center;'; + pathInfo.innerHTML = `File: ${filename}`; + imgContainer.appendChild(pathInfo); + + previewDiv.appendChild(imgContainer); + } + + document.getElementById('manual-image-prompt').value = ''; + } catch (error) { + console.error('Failed to generate image:', error); + statusDiv.innerHTML = `❌ Error: ${error.message}`; + statusDiv.style.color = 'red'; + } +} diff --git a/bot/static/js/memories.js b/bot/static/js/memories.js new file mode 100644 index 0000000..1187119 --- /dev/null +++ b/bot/static/js/memories.js @@ -0,0 +1,446 @@ +// ============================================================================ +// 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 = `● Connected — ${statusData.url}`; + } else { + indicator.innerHTML = `● Disconnected — ${statusData.url}`; + } + + if (statusData.circuit_breaker_active) { + indicator.innerHTML += ` (circuit breaker active)`; + } + + 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 = '● Error checking status'; + } +} + +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 = '
Loading facts...
'; + + try { + const data = await apiCall('/memory/facts'); + + if (!data.success || data.count === 0) { + listDiv.innerHTML = '
No declarative facts stored yet.
'; + 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 += ` +
+
+
${escapeHtml(fact.content)}
+
+ Source: ${escapeHtml(source)} · ${when} +
+
+
+ + +
+
`; + }); + + listDiv.innerHTML = `
${data.count} facts loaded
` + html; + } catch (err) { + listDiv.innerHTML = `
Error loading facts: ${err.message}
`; + } +} + +async function loadEpisodicMemories() { + const listDiv = document.getElementById('episodic-list'); + listDiv.innerHTML = '
Loading memories...
'; + + try { + const data = await apiCall('/memory/episodic'); + + if (!data.success || data.count === 0) { + listDiv.innerHTML = '
No episodic memories stored yet.
'; + 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 += ` +
+
+
${escapeHtml(mem.content)}
+
+ Source: ${escapeHtml(source)} · ${when} +
+
+
+ + +
+
`; + }); + + listDiv.innerHTML = `
${data.count} memories loaded
` + html; + } catch (err) { + listDiv.innerHTML = `
Error loading memories: ${err.message}
`; + } +} + +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'; + } + }); +} diff --git a/bot/static/js/modes.js b/bot/static/js/modes.js new file mode 100644 index 0000000..7bc8f13 --- /dev/null +++ b/bot/static/js/modes.js @@ -0,0 +1,396 @@ +// ============================================================================ +// Miku Control Panel — Modes Module +// Evil Mode, GPU Selection, Bipolar Mode +// ============================================================================ + +// ===== Evil Mode Functions ===== + +async function checkEvilModeStatus() { + try { + const result = await apiCall('/evil-mode'); + evilMode = result.evil_mode; + updateEvilModeUI(); + + if (evilMode && result.mood) { + const moodSelect = document.getElementById('mood'); + moodSelect.value = result.mood; + } + } catch (error) { + console.error('Failed to check evil mode status:', error); + } +} + +async function toggleEvilMode() { + try { + const toggleBtn = document.getElementById('evil-mode-toggle'); + toggleBtn.disabled = true; + toggleBtn.textContent = 'âģ Switching...'; + + const result = await apiCall('/evil-mode/toggle', 'POST'); + evilMode = result.evil_mode; + updateEvilModeUI(); + + if (evilMode) { + showNotification('😈 Evil Mode enabled! Evil Miku has awakened...'); + } else { + showNotification('ðŸŽĪ Evil Mode disabled. Normal Miku is back!'); + } + } catch (error) { + console.error('Failed to toggle evil mode:', error); + showNotification('Failed to toggle evil mode: ' + error.message, 'error'); + } +} + +function updateEvilModeUI() { + const body = document.body; + const title = document.getElementById('panel-title'); + const toggleBtn = document.getElementById('evil-mode-toggle'); + const moodSelect = document.getElementById('mood'); + + if (evilMode) { + body.classList.add('evil-mode'); + title.textContent = 'Evil Miku Control Panel'; + toggleBtn.textContent = '😈 Evil Mode: ON'; + toggleBtn.disabled = false; + + moodSelect.innerHTML = ` + + + + + + + + + + + `; + } else { + body.classList.remove('evil-mode'); + title.textContent = 'Miku Control Panel'; + toggleBtn.textContent = '😈 Evil Mode: OFF'; + toggleBtn.disabled = false; + + moodSelect.innerHTML = ` + + + + + + + + + + + + + + + `; + } + + updateBipolarToggleVisibility(); +} + +// ===== GPU Selection Management ===== + +async function checkGPUStatus() { + try { + const data = await apiCall('/gpu-status'); + selectedGPU = data.gpu || 'nvidia'; + updateGPUUI(); + } catch (error) { + console.error('Failed to check GPU status:', error); + } +} + +async function toggleGPU() { + try { + const toggleBtn = document.getElementById('gpu-selector-toggle'); + toggleBtn.disabled = true; + toggleBtn.textContent = 'âģ Switching...'; + + const result = await apiCall('/gpu-select', 'POST', { + gpu: selectedGPU === 'nvidia' ? 'amd' : 'nvidia' + }); + + selectedGPU = result.gpu; + updateGPUUI(); + + const gpuName = selectedGPU === 'nvidia' ? 'NVIDIA GTX 1660' : 'AMD RX 6800'; + showNotification(`ðŸŽŪ Switched to ${gpuName}!`); + } catch (error) { + console.error('Failed to toggle GPU:', error); + showNotification('Failed to switch GPU: ' + error.message, 'error'); + toggleBtn.disabled = false; + } +} + +function updateGPUUI() { + const toggleBtn = document.getElementById('gpu-selector-toggle'); + + if (selectedGPU === 'amd') { + toggleBtn.textContent = 'ðŸŽŪ GPU: AMD'; + toggleBtn.style.background = '#c91432'; + toggleBtn.style.borderColor = '#e91436'; + } else { + toggleBtn.textContent = 'ðŸŽŪ GPU: NVIDIA'; + toggleBtn.style.background = '#2a5599'; + toggleBtn.style.borderColor = '#4a7bc9'; + } + toggleBtn.disabled = false; +} + +// ===== Bipolar Mode Management ===== + +async function checkBipolarModeStatus() { + try { + const data = await apiCall('/bipolar-mode'); + bipolarMode = data.bipolar_mode; + updateBipolarModeUI(); + } catch (error) { + console.error('Failed to check bipolar mode status:', error); + } +} + +async function toggleBipolarMode() { + try { + const toggleBtn = document.getElementById('bipolar-mode-toggle'); + toggleBtn.disabled = true; + toggleBtn.textContent = 'âģ Switching...'; + + const result = await apiCall('/bipolar-mode/toggle', 'POST'); + bipolarMode = result.bipolar_mode; + updateBipolarModeUI(); + + if (bipolarMode) { + showNotification('🔄 Bipolar Mode enabled! Both Mikus can now argue...'); + } else { + showNotification('🔄 Bipolar Mode disabled.'); + } + } catch (error) { + console.error('Failed to toggle bipolar mode:', error); + showNotification('Failed to toggle bipolar mode: ' + error.message, 'error'); + } +} + +function updateBipolarModeUI() { + const toggleBtn = document.getElementById('bipolar-mode-toggle'); + const bipolarSection = document.getElementById('bipolar-section'); + + if (bipolarMode) { + toggleBtn.textContent = '🔄 Bipolar: ON'; + toggleBtn.style.background = '#9932CC'; + toggleBtn.style.borderColor = '#9932CC'; + toggleBtn.disabled = false; + + if (bipolarSection) { + bipolarSection.style.display = 'block'; + loadScoreboard(); + } + } else { + toggleBtn.textContent = '🔄 Bipolar: OFF'; + toggleBtn.style.background = '#333'; + toggleBtn.style.borderColor = '#666'; + toggleBtn.disabled = false; + + if (bipolarSection) { + bipolarSection.style.display = 'none'; + } + } +} + +function updateBipolarToggleVisibility() { + const bipolarToggle = document.getElementById('bipolar-mode-toggle'); + bipolarToggle.style.display = 'block'; +} + +async function triggerPersonaDialogue() { + const messageIdInput = document.getElementById('dialogue-message-id').value.trim(); + const statusDiv = document.getElementById('dialogue-status'); + + if (!messageIdInput) { + showNotification('Please enter a message ID', 'error'); + return; + } + + if (!/^\d+$/.test(messageIdInput)) { + showNotification('Invalid message ID format - should be a number', 'error'); + return; + } + + try { + statusDiv.innerHTML = 'âģ Analyzing message for dialogue trigger...'; + + const requestBody = { + message_id: messageIdInput + }; + + const result = await apiCall('/bipolar-mode/trigger-dialogue', 'POST', requestBody); + + if (result.status === 'error') { + statusDiv.innerHTML = `❌ ${result.message}`; + showNotification(result.message, 'error'); + return; + } + + statusDiv.innerHTML = `✅ ${result.message}`; + showNotification(`💎 ${result.message}`); + + document.getElementById('dialogue-message-id').value = ''; + + } catch (error) { + statusDiv.innerHTML = `❌ Failed to trigger dialogue: ${error.message}`; + showNotification(`Error: ${error.message}`, 'error'); + } +} + +async function triggerBipolarArgument() { + const channelIdInput = document.getElementById('bipolar-channel-id').value.trim(); + const messageIdInput = document.getElementById('bipolar-message-id').value.trim(); + const context = document.getElementById('bipolar-context').value; + const statusDiv = document.getElementById('bipolar-status'); + + if (!channelIdInput) { + showNotification('Please enter a channel ID', 'error'); + return; + } + + if (!/^\d+$/.test(channelIdInput)) { + showNotification('Invalid channel ID format - should be a number', 'error'); + return; + } + + if (messageIdInput && !/^\d+$/.test(messageIdInput)) { + showNotification('Invalid message ID format - should be a number', 'error'); + return; + } + + try { + statusDiv.innerHTML = 'âģ Triggering argument...'; + + const requestBody = { + channel_id: channelIdInput, + context: context + }; + + if (messageIdInput) { + requestBody.message_id = messageIdInput; + } + + const result = await apiCall('/bipolar-mode/trigger-argument', 'POST', requestBody); + + if (result.status === 'error') { + statusDiv.innerHTML = `❌ ${result.message}`; + showNotification(result.message, 'error'); + return; + } + + statusDiv.innerHTML = `✅ ${result.message}`; + showNotification(`⚔ïļ Argument triggered!`); + + document.getElementById('bipolar-context').value = ''; + document.getElementById('bipolar-message-id').value = ''; + + loadActiveArguments(); + loadScoreboard(); + } catch (error) { + statusDiv.innerHTML = `❌ ${error.message}`; + showNotification('Failed to trigger argument: ' + error.message, 'error'); + } +} + +async function loadScoreboard() { + const scoreboardContent = document.getElementById('scoreboard-content'); + + try { + const result = await apiCall('/bipolar-mode/scoreboard', 'GET'); + + if (result.status === 'error') { + scoreboardContent.innerHTML = `

Failed to load scoreboard

`; + return; + } + + const { scoreboard } = result; + const total = scoreboard.total_arguments; + + if (total === 0) { + scoreboardContent.innerHTML = `

No arguments have been judged yet.

`; + return; + } + + const mikuPct = total > 0 ? ((scoreboard.miku_wins / total) * 100).toFixed(1) : 0; + const evilPct = total > 0 ? ((scoreboard.evil_wins / total) * 100).toFixed(1) : 0; + + let html = ` +
+
+
${scoreboard.miku_wins}
+
Hatsune Miku
+
${mikuPct}%
+
+
vs
+
+
${scoreboard.evil_wins}
+
Evil Miku
+
${evilPct}%
+
+
+
+ Total Arguments: ${total} +
+ `; + + if (scoreboard.history && scoreboard.history.length > 0) { + html += `
+
Recent Results:
`; + + scoreboard.history.reverse().forEach(entry => { + const winnerName = entry.winner === 'evil' ? 'Evil Miku' : 'Hatsune Miku'; + const winnerColor = entry.winner === 'evil' ? '#D60004' : '#86cecb'; + const date = new Date(entry.timestamp).toLocaleString(); + + html += `
+ 🏆 ${winnerName} (${entry.exchanges} exchanges) - ${date} +
`; + }); + + html += `
`; + } + + scoreboardContent.innerHTML = html; + } catch (error) { + scoreboardContent.innerHTML = `

Error loading scoreboard

`; + console.error('Scoreboard error:', error); + } +} + +async function loadActiveArguments() { + try { + const data = await apiCall('/bipolar-mode/arguments'); + const container = document.getElementById('active-arguments'); + const list = document.getElementById('active-arguments-list'); + + if (Object.keys(data.active_arguments).length > 0) { + container.style.display = 'block'; + list.innerHTML = ''; + + for (const [channelId, argData] of Object.entries(data.active_arguments)) { + const div = document.createElement('div'); + div.style.background = '#2a2a3e'; + div.style.padding = '0.5rem'; + div.style.marginBottom = '0.5rem'; + div.style.borderRadius = '4px'; + div.innerHTML = ` + #${argData.channel_name}
+ Exchanges: ${argData.exchange_count} | Speaker: ${argData.current_speaker} + `; + list.appendChild(div); + } + } else { + container.style.display = 'none'; + } + } catch (error) { + console.error('Failed to load active arguments:', error); + } +} diff --git a/bot/static/js/profile.js b/bot/static/js/profile.js new file mode 100644 index 0000000..635ea18 --- /dev/null +++ b/bot/static/js/profile.js @@ -0,0 +1,1127 @@ +// ============================================================================ +// Miku Control Panel — Profile Picture, Album & Activities Module +// ============================================================================ + +// ============================================================================ +// Profile Picture Tab (tab11) — Full Management +// ============================================================================ + +// pfpCropper declared in core.js + +function getPfpCropMode() { + const radio = document.querySelector('input[name="pfp-crop-mode"]:checked'); + return radio ? radio.value : 'auto'; +} + +function pfpSetStatus(text, color = '#61dafb') { + const el = document.getElementById('pfp-tab-status'); + if (el) { el.textContent = text; el.style.color = color; } +} + +function pfpRefreshPreviews() { + const t = Date.now(); + const origImg = document.getElementById('pfp-preview-original'); + const curImg = document.getElementById('pfp-preview-current'); + if (origImg) origImg.src = `/profile-picture/image/original?t=${t}`; + if (curImg) curImg.src = `/profile-picture/image/current?t=${t}`; +} + +async function loadPfpTab() { + // Load metadata + try { + const result = await apiCall('/profile-picture/metadata'); + if (result.status === 'ok' && result.metadata) { + const metaDiv = document.getElementById('pfp-tab-metadata'); + const metaContent = document.getElementById('pfp-tab-metadata-content'); + metaContent.textContent = JSON.stringify(result.metadata, null, 2); + metaDiv.style.display = 'block'; + + // Show original dimensions if available + const dimsEl = document.getElementById('pfp-original-dims'); + if (dimsEl && result.metadata.original_width) { + dimsEl.textContent = `${result.metadata.original_width}×${result.metadata.original_height}`; + } + } + } catch (e) { + console.error('Failed to load PFP metadata:', e); + } + + // Load description + try { + const result = await apiCall('/profile-picture/description'); + if (result.status === 'ok') { + document.getElementById('pfp-description-editor').value = result.description || ''; + } + } catch (e) { + console.error('Failed to load PFP description:', e); + } + + // Refresh preview images + pfpRefreshPreviews(); + + // Update album header counts (without opening) + try { + const [listRes, usageRes] = await Promise.all([ + apiCall('/profile-picture/album'), + apiCall('/profile-picture/album/disk-usage') + ]); + if (listRes.status === 'ok') { + albumEntries = listRes.entries || []; + document.getElementById('album-count').textContent = `(${albumEntries.length} entries)`; + if (albumOpen) albumRenderGrid(); + } + if (usageRes.status === 'ok') { + document.getElementById('album-disk-usage').textContent = + `${usageRes.human_readable} · ${usageRes.entry_count} entries`; + } + } catch (e) { + console.error('Failed to load album info:', e); + } +} + +// --- Danbooru Change --- +async function pfpChangeDanbooru() { + const mode = getPfpCropMode(); + const selectedServer = document.getElementById('server-select').value; + pfpSetStatus('âģ Searching Danbooru...'); + + try { + const endpoint = mode === 'manual' ? '/profile-picture/change-no-crop' : '/profile-picture/change'; + const params = new URLSearchParams(); + if (selectedServer !== 'all') params.append('guild_id', selectedServer); + const url = params.toString() ? `${endpoint}?${params.toString()}` : endpoint; + + const result = await apiCall(url, 'POST'); + + if (result.status === 'ok') { + pfpSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Profile picture changed!'); + + // Show metadata + const metaDiv = document.getElementById('pfp-tab-metadata'); + const metaContent = document.getElementById('pfp-tab-metadata-content'); + if (result.metadata) { + metaContent.textContent = JSON.stringify(result.metadata, null, 2); + metaDiv.style.display = 'block'; + } + + pfpRefreshPreviews(); + + // If manual mode, show crop interface + if (mode === 'manual') { + pfpShowCropInterface(); + } + } else { + throw new Error(result.message || 'Unknown error'); + } + } catch (error) { + console.error('PFP Danbooru error:', error); + pfpSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// Keep old function names working (backwards compatibility for autonomous/API callers) +async function changeProfilePicture() { await pfpChangeDanbooru(); } + +// --- Custom Upload --- +async function pfpUploadCustom() { + const fileInput = document.getElementById('pfp-tab-upload'); + const mode = getPfpCropMode(); + const selectedServer = document.getElementById('server-select').value; + + if (!fileInput.files || fileInput.files.length === 0) { + showNotification('Please select an image file first', 'error'); + return; + } + + const file = fileInput.files[0]; + if (!file.type.startsWith('image/')) { + showNotification('Please select a valid image file', 'error'); + return; + } + + pfpSetStatus('âģ Uploading and processing...'); + + try { + const formData = new FormData(); + formData.append('file', file); + + const endpoint = mode === 'manual' ? '/profile-picture/change-no-crop' : '/profile-picture/change'; + let url = endpoint; + if (selectedServer !== 'all') url += `?guild_id=${selectedServer}`; + + const response = await fetch(url, { method: 'POST', body: formData }); + const result = await response.json(); + + if (response.ok && result.status === 'ok') { + pfpSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Image uploaded successfully!'); + fileInput.value = ''; + + if (result.metadata) { + const metaDiv = document.getElementById('pfp-tab-metadata'); + const metaContent = document.getElementById('pfp-tab-metadata-content'); + metaContent.textContent = JSON.stringify(result.metadata, null, 2); + metaDiv.style.display = 'block'; + } + + pfpRefreshPreviews(); + + if (mode === 'manual') { + pfpShowCropInterface(); + } + } else { + throw new Error(result.message || 'Upload failed'); + } + } catch (error) { + console.error('PFP upload error:', error); + pfpSetStatus(`❌ Error: ${error.message}`, 'red'); + showNotification(error.message, 'error'); + } +} + +// Keep old function name working +async function uploadCustomPfp() { await pfpUploadCustom(); } + +// --- Restore Fallback --- +async function pfpRestoreFallback() { + if (!confirm('Are you sure you want to restore the original fallback avatar?')) return; + + pfpSetStatus('âģ Restoring original avatar...'); + + try { + const result = await apiCall('/profile-picture/restore-fallback', 'POST'); + pfpSetStatus(`✅ ${result.message}`, 'green'); + document.getElementById('pfp-tab-metadata').style.display = 'none'; + pfpRefreshPreviews(); + showNotification('Original avatar restored!'); + } catch (error) { + console.error('Restore fallback error:', error); + pfpSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +async function restoreFallbackPfp() { await pfpRestoreFallback(); } + +// --- Crop Interface --- +function pfpShowCropInterface() { + const section = document.getElementById('pfp-crop-section'); + const img = document.getElementById('pfp-crop-image'); + + // Destroy previous cropper if any + if (pfpCropper) { + pfpCropper.destroy(); + pfpCropper = null; + } + + // Load original image + img.src = `/profile-picture/image/original?t=${Date.now()}`; + section.style.display = 'block'; + + img.onload = function() { + pfpCropper = new Cropper(img, { + aspectRatio: 1, + viewMode: 2, + minCropBoxWidth: 64, + minCropBoxHeight: 64, + responsive: true, + autoCropArea: 0.8, + background: true, + guides: true, + center: true, + highlight: true, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: false + }); + }; +} + +function pfpHideCropInterface() { + if (pfpCropper) { + pfpCropper.destroy(); + pfpCropper = null; + } + document.getElementById('pfp-crop-section').style.display = 'none'; +} + +// Re-crop: open crop interface on stored original +function pfpRecrop() { + pfpShowCropInterface(); +} + +async function pfpApplyManualCrop() { + if (!pfpCropper) { + showNotification('No crop region selected', 'error'); + return; + } + + const data = pfpCropper.getData(true); // rounded integers + pfpSetStatus('âģ Applying manual crop...'); + + try { + const response = await fetch('/profile-picture/manual-crop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + x: Math.round(data.x), + y: Math.round(data.y), + width: Math.round(data.width), + height: Math.round(data.height) + }) + }); + const result = await response.json(); + + if (response.ok && result.status === 'ok') { + pfpSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Manual crop applied!'); + pfpHideCropInterface(); + pfpRefreshPreviews(); + + // Refresh metadata + if (result.metadata) { + const metaContent = document.getElementById('pfp-tab-metadata-content'); + const existing = metaContent.textContent ? JSON.parse(metaContent.textContent) : {}; + Object.assign(existing, result.metadata); + metaContent.textContent = JSON.stringify(existing, null, 2); + } + } else { + throw new Error(result.message || 'Crop failed'); + } + } catch (error) { + console.error('Manual crop error:', error); + pfpSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +async function pfpApplyAutoCrop() { + pfpSetStatus('âģ Running auto-crop (face detection)...'); + + try { + const result = await apiCall('/profile-picture/auto-crop', 'POST'); + + if (result.status === 'ok') { + pfpSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Auto-crop applied!'); + pfpHideCropInterface(); + pfpRefreshPreviews(); + } else { + throw new Error(result.message || 'Auto-crop failed'); + } + } catch (error) { + console.error('Auto-crop error:', error); + pfpSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// --- Description --- +async function pfpSaveDescription() { + const descEl = document.getElementById('pfp-description-editor'); + const statusEl = document.getElementById('pfp-desc-status'); + const description = descEl.value.trim(); + + if (!description) { + statusEl.textContent = '⚠ïļ Description cannot be empty'; + statusEl.style.color = 'orange'; + return; + } + + statusEl.textContent = 'âģ Saving description...'; + statusEl.style.color = '#61dafb'; + + try { + const response = await fetch('/profile-picture/description', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description }) + }); + const result = await response.json(); + + if (response.ok && result.status === 'ok') { + statusEl.textContent = '✅ Description saved & injected into Cat memory'; + statusEl.style.color = 'green'; + showNotification('Description saved!'); + } else { + throw new Error(result.message || 'Save failed'); + } + } catch (error) { + console.error('Save description error:', error); + statusEl.textContent = `❌ Error: ${error.message}`; + statusEl.style.color = 'red'; + } +} + +async function pfpRegenerateDescription() { + const statusEl = document.getElementById('pfp-desc-status'); + statusEl.textContent = 'âģ Regenerating description via vision model...'; + statusEl.style.color = '#61dafb'; + + try { + const result = await apiCall('/profile-picture/regenerate-description', 'POST'); + + if (result.status === 'ok' && result.description) { + document.getElementById('pfp-description-editor').value = result.description; + statusEl.textContent = '✅ Description regenerated & saved'; + statusEl.style.color = 'green'; + showNotification('Description regenerated!'); + } else { + throw new Error(result.message || 'Regeneration failed'); + } + } catch (error) { + console.error('Regenerate description error:', error); + statusEl.textContent = `❌ Error: ${error.message}`; + statusEl.style.color = 'red'; + } +} + +// --- Role Color (updated element IDs for tab11) --- +async function setCustomRoleColor() { + const statusDiv = document.getElementById('pfp-tab-role-color-status'); + const hexInput = document.getElementById('pfp-tab-role-color-hex'); + const hexColor = hexInput.value.trim(); + + if (!hexColor) { + statusDiv.textContent = '⚠ïļ Please enter a hex color code'; + statusDiv.style.color = 'orange'; + return; + } + + statusDiv.textContent = 'âģ Updating role colors...'; + statusDiv.style.color = '#61dafb'; + + try { + const formData = new FormData(); + formData.append('hex_color', hexColor); + + const response = await fetch('/role-color/custom', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (response.ok && result.status === 'ok') { + statusDiv.textContent = `✅ ${result.message}`; + statusDiv.style.color = 'green'; + showNotification(`Role color updated to ${result.color.hex}`); + } else { + throw new Error(result.message || 'Failed to update role color'); + } + } catch (error) { + console.error('Failed to set custom role color:', error); + statusDiv.textContent = `❌ Error: ${error.message}`; + statusDiv.style.color = 'red'; + showNotification(error.message || 'Failed to update role color', 'error'); + } +} + +async function resetRoleColor() { + const statusDiv = document.getElementById('pfp-tab-role-color-status'); + + statusDiv.textContent = 'âģ Resetting to fallback color...'; + statusDiv.style.color = '#61dafb'; + + try { + const result = await apiCall('/role-color/reset-fallback', 'POST'); + + statusDiv.textContent = `✅ ${result.message}`; + statusDiv.style.color = 'green'; + + document.getElementById('pfp-tab-role-color-hex').value = '#86cecb'; + + showNotification('Role color reset to fallback #86cecb'); + } catch (error) { + console.error('Failed to reset role color:', error); + statusDiv.textContent = `❌ Error: ${error.message}`; + statusDiv.style.color = 'red'; + } +} + +// ============================================================================ +// Album / Gallery System +// ============================================================================ + +// albumEntries, albumSelectedId, albumChecked, albumCropper, albumOpen declared in core.js + +function albumSetStatus(text, color = '#61dafb') { + const el = document.getElementById('album-status'); + if (el) { el.textContent = text; el.style.color = color; } +} + +function albumToggle() { + albumOpen = !albumOpen; + document.getElementById('album-body').style.display = albumOpen ? 'block' : 'none'; + document.getElementById('album-toggle-icon').textContent = albumOpen ? '▾' : 'â–ķ'; + if (albumOpen) albumLoad(); +} + +async function albumLoad() { + try { + const [listRes, usageRes] = await Promise.all([ + apiCall('/profile-picture/album'), + apiCall('/profile-picture/album/disk-usage') + ]); + if (listRes.status === 'ok') { + albumEntries = listRes.entries || []; + document.getElementById('album-count').textContent = `(${albumEntries.length} entries)`; + albumRenderGrid(); + } + if (usageRes.status === 'ok') { + document.getElementById('album-disk-usage').textContent = + `${usageRes.human_readable} · ${usageRes.entry_count} entries`; + } + } catch (e) { + console.error('Album load error:', e); + } +} + +function albumRenderGrid() { + const grid = document.getElementById('album-grid'); + if (!grid) return; + + if (albumEntries.length === 0) { + grid.innerHTML = '
No album entries yet. Upload images or archive the current PFP.
'; + return; + } + + grid.innerHTML = albumEntries.map(e => { + const id = e.id; + const isSelected = id === albumSelectedId; + const isChecked = albumChecked.has(id); + const colorDot = e.dominant_color + ? `` + : ''; + const label = (e.source || '').replace('custom_upload', 'upload').substring(0, 12); + return `
+ + +
${colorDot}${label}
+
`; + }).join(''); +} + +function albumToggleCheck(id, checked) { + if (checked) albumChecked.add(id); else albumChecked.delete(id); + document.getElementById('album-selected-count').textContent = albumChecked.size; + document.getElementById('album-bulk-delete-btn').disabled = albumChecked.size === 0; + // update card class + const card = document.querySelector(`.album-card[data-id="${id}"]`); + if (card) card.classList.toggle('checked', checked); +} + +async function albumSelectEntry(id) { + albumSelectedId = id; + // highlight card + document.querySelectorAll('.album-card').forEach(c => c.classList.toggle('selected', c.dataset.id === id)); + // show detail + const detail = document.getElementById('album-detail'); + detail.style.display = 'block'; + const t = Date.now(); + document.getElementById('album-detail-original').src = `/profile-picture/album/${id}/image/original?t=${t}`; + document.getElementById('album-detail-cropped').src = `/profile-picture/album/${id}/image/cropped?t=${t}`; + + // load entry metadata + try { + const res = await apiCall(`/profile-picture/album/${id}`); + if (res.status === 'ok' && res.entry) { + const e = res.entry; + document.getElementById('album-detail-dims').textContent = + e.original_width && e.original_height ? `${e.original_width}×${e.original_height}` : ''; + document.getElementById('album-detail-description').value = e.description || ''; + const metaLines = []; + if (e.added_at) metaLines.push(`Added: ${new Date(e.added_at).toLocaleString()}`); + if (e.source) metaLines.push(`Source: ${e.source}`); + if (e.dominant_color) metaLines.push(`Color: ${e.dominant_color.hex}`); + if (e.was_current) metaLines.push('📌 Previously active'); + document.getElementById('album-detail-meta').textContent = metaLines.join(' · '); + } + } catch (e) { + console.error('Album entry load error:', e); + } +} + +function albumCloseDetail() { + document.getElementById('album-detail').style.display = 'none'; + albumSelectedId = null; + albumHideCropInterface(); + document.querySelectorAll('.album-card').forEach(c => c.classList.remove('selected')); +} + +// --- Album Upload --- +async function albumUpload() { + const input = document.getElementById('album-upload'); + if (!input.files || input.files.length === 0) { + showNotification('Select image(s) to add to album', 'error'); + return; + } + + const files = Array.from(input.files); + albumSetStatus(`âģ Adding ${files.length} image(s) to album...`); + + try { + if (files.length === 1) { + const formData = new FormData(); + formData.append('file', files[0]); + const resp = await fetch('/profile-picture/album/add', { method: 'POST', body: formData }); + const result = await resp.json(); + if (result.status === 'ok') { + albumSetStatus(`✅ Added to album`, 'green'); + showNotification('Image added to album!'); + } else { + throw new Error(result.message); + } + } else { + const formData = new FormData(); + files.forEach(f => formData.append('files', f)); + const resp = await fetch('/profile-picture/album/add-batch', { method: 'POST', body: formData }); + const result = await resp.json(); + albumSetStatus(`✅ ${result.message}`, result.failed > 0 ? 'orange' : 'green'); + showNotification(result.message); + } + input.value = ''; + await albumLoad(); + } catch (error) { + console.error('Album upload error:', error); + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +async function albumAddCurrent() { + albumSetStatus('âģ Archiving current PFP...'); + try { + const result = await apiCall('/profile-picture/album/add-current', 'POST'); + if (result.status === 'ok') { + albumSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Current PFP archived to album!'); + await albumLoad(); + } else { + throw new Error(result.message); + } + } catch (error) { + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// --- Album Set as Current --- +async function albumSetAsCurrent() { + if (!albumSelectedId) return; + if (!confirm('Set this album entry as your current Discord profile picture?\nThe current PFP will be archived to the album.')) return; + + albumSetStatus('âģ Setting as current PFP...'); + try { + const result = await apiCall(`/profile-picture/album/${albumSelectedId}/set-current`, 'POST'); + if (result.status === 'ok') { + albumSetStatus(`✅ ${result.message}`, 'green'); + showNotification('Album entry set as current PFP!'); + pfpRefreshPreviews(); + loadPfpTab(); // refresh metadata + description + await albumLoad(); + } else { + throw new Error(result.message); + } + } catch (error) { + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// --- Album Delete --- +async function albumDeleteSelected() { + if (!albumSelectedId) return; + if (!confirm('Delete this album entry permanently?')) return; + + try { + const resp = await fetch(`/profile-picture/album/${albumSelectedId}`, { method: 'DELETE' }); + const result = await resp.json(); + if (result.status === 'ok') { + showNotification('Album entry deleted'); + albumCloseDetail(); + await albumLoad(); + } else { + throw new Error(result.message); + } + } catch (error) { + showNotification(`Error: ${error.message}`, 'error'); + } +} + +async function albumBulkDelete() { + if (albumChecked.size === 0) return; + if (!confirm(`Delete ${albumChecked.size} selected album entries permanently?`)) return; + + albumSetStatus(`âģ Deleting ${albumChecked.size} entries...`); + try { + const resp = await fetch('/profile-picture/album/delete-bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entry_ids: Array.from(albumChecked) }) + }); + const result = await resp.json(); + albumSetStatus(`✅ ${result.message}`, 'green'); + showNotification(result.message); + albumChecked.clear(); + document.getElementById('album-selected-count').textContent = '0'; + document.getElementById('album-bulk-delete-btn').disabled = true; + if (albumSelectedId && !albumEntries.find(e => e.id === albumSelectedId)) { + albumCloseDetail(); + } + await albumLoad(); + } catch (error) { + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// --- Album Crop --- +function albumShowCropInterface() { + if (!albumSelectedId) return; + if (albumCropper) { albumCropper.destroy(); albumCropper = null; } + + const section = document.getElementById('album-crop-section'); + const img = document.getElementById('album-crop-image'); + img.src = `/profile-picture/album/${albumSelectedId}/image/original?t=${Date.now()}`; + section.style.display = 'block'; + + img.onload = function() { + albumCropper = new Cropper(img, { + aspectRatio: 1, + viewMode: 2, + minCropBoxWidth: 64, + minCropBoxHeight: 64, + responsive: true, + autoCropArea: 0.8, + background: true, + guides: true, + center: true, + highlight: true, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: false + }); + }; +} + +function albumHideCropInterface() { + if (albumCropper) { albumCropper.destroy(); albumCropper = null; } + document.getElementById('album-crop-section').style.display = 'none'; +} + +async function albumApplyManualCrop() { + if (!albumCropper || !albumSelectedId) return; + const data = albumCropper.getData(true); + albumSetStatus('âģ Applying crop to album entry...'); + + try { + const resp = await fetch(`/profile-picture/album/${albumSelectedId}/manual-crop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ x: Math.round(data.x), y: Math.round(data.y), width: Math.round(data.width), height: Math.round(data.height) }) + }); + const result = await resp.json(); + if (result.status === 'ok') { + albumSetStatus('✅ Crop applied', 'green'); + showNotification('Album entry cropped!'); + albumHideCropInterface(); + // refresh detail images + const t = Date.now(); + document.getElementById('album-detail-cropped').src = `/profile-picture/album/${albumSelectedId}/image/cropped?t=${t}`; + await albumLoad(); // refresh grid thumbnails + } else { + throw new Error(result.message); + } + } catch (error) { + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +async function albumApplyAutoCrop() { + if (!albumSelectedId) return; + albumSetStatus('âģ Running auto-crop on album entry...'); + + try { + const result = await apiCall(`/profile-picture/album/${albumSelectedId}/auto-crop`, 'POST'); + if (result.status === 'ok') { + albumSetStatus('✅ Auto-crop applied', 'green'); + showNotification('Album entry auto-cropped!'); + albumHideCropInterface(); + const t = Date.now(); + document.getElementById('album-detail-cropped').src = `/profile-picture/album/${albumSelectedId}/image/cropped?t=${t}`; + await albumLoad(); + } else { + throw new Error(result.message); + } + } catch (error) { + albumSetStatus(`❌ Error: ${error.message}`, 'red'); + } +} + +// --- Album Description --- +async function albumSaveDescription() { + if (!albumSelectedId) return; + const description = document.getElementById('album-detail-description').value.trim(); + if (!description) { showNotification('Description cannot be empty', 'error'); return; } + + try { + const resp = await fetch(`/profile-picture/album/${albumSelectedId}/description`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description }) + }); + const result = await resp.json(); + if (result.status === 'ok') { + showNotification('Album entry description saved!'); + } else { + throw new Error(result.message); + } + } catch (error) { + showNotification(`Error: ${error.message}`, 'error'); + } +} + +// ============================================================================ +// MOOD ACTIVITIES EDITOR +// ============================================================================ + +// activitiesData, activitiesOpen, activitiesSections, activitiesEditing, activitiesEditCache declared in core.js + +function activitiesToggle() { + activitiesOpen = !activitiesOpen; + document.getElementById('activities-body').style.display = activitiesOpen ? 'block' : 'none'; + document.getElementById('activities-toggle-icon').textContent = activitiesOpen ? '▾' : 'â–ķ'; + if (activitiesOpen) { + if (!activitiesData) activitiesLoad(); + activityRefreshCurrent(); + } +} + +function activitiesSectionToggle(section) { + activitiesSections[section] = !activitiesSections[section]; + document.getElementById(`activities-${section}-body`).style.display = activitiesSections[section] ? 'block' : 'none'; + document.getElementById(`activities-${section}-icon`).textContent = activitiesSections[section] ? '▾' : 'â–ķ'; +} + +async function activitiesLoad() { + const statusEl = document.getElementById('activities-status'); + statusEl.textContent = 'Loading...'; + try { + activitiesData = await apiCall('/activities'); + const normalMoods = Object.keys(activitiesData.normal || {}); + const evilMoods = Object.keys(activitiesData.evil || {}); + const total = normalMoods.length + evilMoods.length; + document.getElementById('activities-summary').textContent = `(${total} moods configured)`; + activitiesRenderSection('normal'); + activitiesRenderSection('evil'); + statusEl.textContent = ''; + } catch (e) { + statusEl.textContent = 'Failed to load: ' + e.message; + statusEl.style.color = '#e74c3c'; + } +} + +function activitiesRenderSection(section) { + const container = document.getElementById(`activities-${section}-list`); + if (!activitiesData || !activitiesData[section]) { container.innerHTML = '

No data

'; return; } + + const moods = activitiesData[section]; + let html = ''; + for (const [mood, entries] of Object.entries(moods)) { + const key = `${section}/${mood}`; + const isEditing = activitiesEditing[key]; + const songs = entries.filter(e => e.type === 'listening').length; + const games = entries.filter(e => e.type === 'playing').length; + const watches = entries.filter(e => e.type === 'watching').length; + const competes = entries.filter(e => e.type === 'competing').length; + const streams = entries.filter(e => e.type === 'streaming').length; + + let stats = `${songs}ðŸŽĩ ${games}ðŸŽŪ`; + if (watches) stats += ` ${watches}📚`; + if (competes) stats += ` ${competes}🏆`; + if (streams) stats += ` ${streams}ðŸ”ī`; + + html += `
`; + html += `
`; + html += `â–ķ ${mood}`; + html += `${stats}`; + html += `
`; + html += `
`; + + if (isEditing) { + html += activitiesRenderEditForm(section, mood, activitiesEditCache[key] || entries); + } else { + html += activitiesRenderView(section, mood, entries); + } + + html += `
`; + } + container.innerHTML = html; +} + +function activitiesRenderView(section, mood, entries) { + let html = ''; + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const icons = { listening: 'ðŸŽĩ', playing: 'ðŸŽŪ', watching: '📚', competing: '🏆', streaming: 'ðŸ”ī' }; + const labels = { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' }; + const icon = icons[entry.type] || 'ðŸŽŪ'; + const label = labels[entry.type] || 'Playing'; + // Encode entry data for the "Set as Activity" button + const entryData = encodeURIComponent(JSON.stringify({ type: entry.type, name: entry.name, state: entry.state || '', url: entry.url || '' })); + html += `
`; + html += `${icon}`; + html += `${label} ${escapeHtml(entry.name)}`; + if (entry.state) html += ` — ${escapeHtml(entry.state)}`; + html += ``; + html += `weight: ${entry.weight}`; + html += ``; + html += `
`; + } + html += `
`; + html += ``; + html += `
`; + return html; +} + +function activitiesRenderEditForm(section, mood, entries) { + let html = ''; + for (let i = 0; i < entries.length; i++) { + const e = entries[i]; + html += `
`; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += `
`; + } + html += `
`; + html += ``; + html += ``; + html += ``; + html += `
`; + return html; +} + +function activitiesTypeChanged(section, mood, index) { + const typeEl = document.getElementById(`act-type-${section}-${mood}-${index}`); + const urlEl = document.getElementById(`act-url-${section}-${mood}-${index}`); + if (!typeEl || !urlEl) return; + urlEl.style.display = typeEl.value === 'streaming' ? '' : 'none'; +} + +function activitiesMoodToggle(section, mood) { + const el = document.getElementById(`act-content-${section}-${mood}`); + const iconEl = document.getElementById(`act-icon-${section}-${mood}`); + if (!el) return; + const isOpen = el.style.display === 'block'; + el.style.display = isOpen ? 'none' : 'block'; + if (iconEl) iconEl.textContent = isOpen ? 'â–ķ' : '▾'; +} + +function activitiesStartEdit(section, mood) { + const key = `${section}/${mood}`; + const entries = activitiesData[section][mood]; + // Deep clone entries for editing + activitiesEditCache[key] = JSON.parse(JSON.stringify(entries)); + activitiesEditing[key] = true; + activitiesRenderSection(section); + // Auto-expand the mood panel + const el = document.getElementById(`act-content-${section}-${mood}`); + const iconEl = document.getElementById(`act-icon-${section}-${mood}`); + if (el) el.style.display = 'block'; + if (iconEl) iconEl.textContent = '▾'; +} + +function activitiesCancelEdit(section, mood) { + const key = `${section}/${mood}`; + delete activitiesEditing[key]; + delete activitiesEditCache[key]; + activitiesRenderSection(section); +} + +function activitiesAddEntry(section, mood) { + const key = `${section}/${mood}`; + // First, sync current form values to cache + activitiesSyncFormToCache(section, mood); + activitiesEditCache[key].push({ type: 'listening', name: '', state: '', weight: 1 }); + activitiesRenderSection(section); + // Keep the mood panel open + const el = document.getElementById(`act-content-${section}-${mood}`); + if (el) el.style.display = 'block'; +} + +function activitiesRemoveEntry(section, mood, index) { + const key = `${section}/${mood}`; + activitiesSyncFormToCache(section, mood); + activitiesEditCache[key].splice(index, 1); + activitiesRenderSection(section); + const el = document.getElementById(`act-content-${section}-${mood}`); + if (el) el.style.display = 'block'; +} + +function activitiesSyncFormToCache(section, mood) { + const key = `${section}/${mood}`; + const entries = activitiesEditCache[key] || []; + for (let i = 0; i < entries.length; i++) { + const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`); + const nameEl = document.getElementById(`act-name-${section}-${mood}-${i}`); + const stateEl = document.getElementById(`act-state-${section}-${mood}-${i}`); + const urlEl = document.getElementById(`act-url-${section}-${mood}-${i}`); + const weightEl = document.getElementById(`act-weight-${section}-${mood}-${i}`); + if (typeEl) entries[i].type = typeEl.value; + if (nameEl) entries[i].name = nameEl.value; + if (stateEl) entries[i].state = stateEl.value || undefined; + if (urlEl) entries[i].url = urlEl.value || undefined; + if (weightEl) entries[i].weight = parseInt(weightEl.value) || 1; + } + activitiesEditCache[key] = entries; +} + +async function activitiesSave(section, mood) { + const key = `${section}/${mood}`; + activitiesSyncFormToCache(section, mood); + const entries = activitiesEditCache[key]; + + // Client-side validation + for (let i = 0; i < entries.length; i++) { + if (!entries[i].name || !entries[i].name.trim()) { + showNotification(`Entry ${i + 1}: name cannot be empty`, 'error'); + return; + } + if (!entries[i].weight || entries[i].weight < 1) { + showNotification(`Entry ${i + 1}: weight must be at least 1`, 'error'); + return; + } + if (entries[i].type === 'streaming' && !entries[i].url) { + showNotification(`Entry ${i + 1}: streaming requires a URL`, 'error'); + return; + } + } + + try { + await apiCall(`/activities/${section}/${mood}`, 'POST', { activities: entries }); + showNotification(`Saved activities for ${section}/${mood}`, 'success'); + delete activitiesEditing[key]; + delete activitiesEditCache[key]; + // Reload to get fresh data + await activitiesLoad(); + } catch (e) { + showNotification('Save failed: ' + e.message, 'error'); + } +} + +// ============================================================================ +// CURRENT ACTIVITY OVERRIDE +// ============================================================================ + +function _activityTypeIcon(type) { + return { listening: 'ðŸŽĩ', playing: 'ðŸŽŪ', watching: '📚', competing: '🏆', streaming: 'ðŸ”ī' }[type] || 'ðŸŽŪ'; +} + +function _activityTypeLabel(type) { + return { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' }[type] || 'Playing'; +} + +async function activityRefreshCurrent() { + const statusEl = document.getElementById('activity-override-status'); + try { + const data = await apiCall('/activities/current'); + const act = data.activity; + const isOverride = data.manual_override; + + if (act) { + const icon = _activityTypeIcon(act.type); + const label = _activityTypeLabel(act.type); + let html = `${icon} ${label} ${escapeHtml(act.name)}`; + if (act.state) html += ` — ${escapeHtml(act.state)}`; + if (isOverride) html += ` ⚡ MANUAL OVERRIDE (30 min)`; + statusEl.innerHTML = html; + } else { + let html = 'No activity (idle)'; + if (isOverride) html += ' ⚡ MANUAL OVERRIDE'; + statusEl.innerHTML = html; + } + } catch (e) { + statusEl.innerHTML = `Error: ${e.message}`; + } +} + +async function activitySetManual() { + const type = document.getElementById('act-manual-type').value; + const name = document.getElementById('act-manual-name').value.trim(); + const state = document.getElementById('act-manual-state').value.trim(); + const url = document.getElementById('act-manual-url').value.trim(); + + if (!name) { showNotification('Activity name is required', 'error'); return; } + if (type === 'streaming' && !url) { showNotification('Streaming requires a URL', 'error'); return; } + + try { + const body = { type, name }; + if (state) body.state = state; + if (url) body.url = url; + await apiCall('/activities/current', 'POST', body); + showNotification(`Set activity: ${type} ${name}`, 'success'); + await activityRefreshCurrent(); + } catch (e) { + showNotification('Failed to set activity: ' + e.message, 'error'); + } +} + +async function activitySetFromEntry(btnElement) { + const raw = btnElement.getAttribute('data-entry'); + if (!raw) return; + let entry; + try { entry = JSON.parse(decodeURIComponent(raw)); } catch (e) { showNotification('Failed to parse activity data', 'error'); return; } + const type = entry.type; + const name = entry.name; + const state = entry.state || null; + const url = entry.url || null; + + if (!name) { showNotification('Activity name is empty', 'error'); return; } + if (type === 'streaming' && !url) { showNotification('Streaming requires a URL', 'error'); return; } + + try { + const body = { type, name }; + if (state) body.state = state; + if (url) body.url = url; + await apiCall('/activities/current', 'POST', body); + const icon = _activityTypeIcon(type); + showNotification(`${icon} Set activity: ${name}`, 'success'); + await activityRefreshCurrent(); + } catch (e) { + showNotification('Failed to set activity: ' + e.message, 'error'); + } +} + +async function activityClearManual() { + try { + await apiCall('/activities/current', 'DELETE'); + showNotification('Activity cleared (manual override active)', 'success'); + await activityRefreshCurrent(); + } catch (e) { + showNotification('Failed to clear: ' + e.message, 'error'); + } +} + +async function activityReleaseAuto() { + try { + await apiCall('/activities/current/auto', 'POST'); + showNotification('Returned to automatic mode', 'success'); + await activityRefreshCurrent(); + } catch (e) { + showNotification('Failed to release override: ' + e.message, 'error'); + } +} + +// Show/hide URL field when streaming is selected in manual override +document.getElementById('act-manual-type').addEventListener('change', function() { + document.getElementById('act-manual-url').style.display = this.value === 'streaming' ? '' : 'none'; +}); diff --git a/bot/static/js/servers.js b/bot/static/js/servers.js new file mode 100644 index 0000000..e4bf21b --- /dev/null +++ b/bot/static/js/servers.js @@ -0,0 +1,684 @@ +// ===== Server Management Functions ===== + +async function loadServers() { + try { + console.log('🎭 Loading servers...'); + const data = await apiCall('/servers'); + console.log('🎭 Servers response:', data); + + if (data.servers) { + servers = data.servers; + console.log(`🎭 Loaded ${servers.length} servers:`, servers); + + // Debug: Log each server's guild_id + servers.forEach((server, index) => { + console.log(`🎭 Server ${index}: guild_id = ${server.guild_id}, name = ${server.guild_name}`); + }); + + // Debug: Show raw response data + console.log('🎭 Raw API response data:', JSON.stringify(data, null, 2)); + + // Display servers + displayServers(); + populateServerDropdowns(); + populateMoodDropdowns(); // Populate mood dropdowns after servers are loaded + } else { + console.warn('🎭 No servers found in response'); + servers = []; + } + } catch (error) { + console.error('🎭 Failed to load servers:', error); + servers = []; + } +} + +function displayServers() { + const container = document.getElementById('servers-list'); + + if (servers.length === 0) { + container.innerHTML = '

No servers configured

'; + return; + } + + container.innerHTML = servers.map(server => ` +
+
+
${server.guild_name}
+
+ + +
+
+
Guild ID: ${server.guild_id}
+
Autonomous Channel: #${server.autonomous_channel_name} (${server.autonomous_channel_id})
+
Bedtime Channels: ${server.bedtime_channel_ids.join(', ')}
+
Features: + ${server.enabled_features.map(feature => `${feature}`).join('')} +
+
Autonomous Interval: ${server.autonomous_interval_minutes} minutes
+
Conversation Detection: ${server.conversation_detection_interval_minutes} minutes
+
Bedtime Range: ${String(server.bedtime_hour || 21).padStart(2, '0')}:${String(server.bedtime_minute || 0).padStart(2, '0')} - ${String(server.bedtime_hour_end || 23).padStart(2, '0')}:${String(server.bedtime_minute_end || 59).padStart(2, '0')}
+ + +
+

Bedtime Settings

+
+
+ + +
+
+ + +
+
+ +
+ + +
+

Server Mood

+
Current Mood: ${server.current_mood_name || 'neutral'} ${MOOD_EMOJIS[server.current_mood_name] || ''}
+
Sleeping: ${server.is_sleeping ? 'Yes' : 'No'}
+
+ + + +
+
+
+ `).join(''); + + // Debug: Log what element IDs were created + console.log('🎭 Server cards rendered. Checking for mood-select elements:'); + document.querySelectorAll('[id^="mood-select-"]').forEach(el => { + console.log(`🎭 Found mood-select element: ${el.id}`); + }); + + // Populate mood dropdowns after server cards are created + populateMoodDropdowns(); +} + +async function populateServerDropdowns() { + const serverSelect = document.getElementById('server-select'); + const manualServerSelect = document.getElementById('manual-server-select'); + const customPromptServerSelect = document.getElementById('custom-prompt-server-select'); + + // Clear existing options except "All Servers" + serverSelect.innerHTML = ''; + manualServerSelect.innerHTML = ''; + customPromptServerSelect.innerHTML = ''; + + console.log('🎭 Populating server dropdowns with', servers.length, 'servers'); + + // Add server options + servers.forEach(server => { + console.log(`🎭 Adding server to dropdown: ${server.guild_name} (guild_id: ${server.guild_id}, type: ${typeof server.guild_id})`); + + const option = document.createElement('option'); + option.value = server.guild_id; + option.textContent = server.guild_name; + + serverSelect.appendChild(option.cloneNode(true)); + manualServerSelect.appendChild(option); + customPromptServerSelect.appendChild(option.cloneNode(true)); + }); + + // Debug: Check what's actually in the manual-server-select dropdown + console.log('🎭 manual-server-select options:'); + Array.from(manualServerSelect.options).forEach((opt, idx) => { + console.log(` [${idx}] value="${opt.value}" text="${opt.textContent}"`); + }); + + // Populate autonomous stats dropdown + populateAutonomousServerDropdown(); +} + +// Figurine subscribers UI functions (must be global for onclick handlers) +async function refreshFigurineSubscribers() { + try { + console.log('🔄 Figurines: Fetching subscribers...'); + const data = await apiCall('/figurines/subscribers'); + console.log('📋 Figurines: Received subscribers:', data); + displayFigurineSubscribers(data.subscribers || []); + showNotification('Subscribers refreshed'); + } catch (e) { + console.error('❌ Figurines: Failed to fetch subscribers:', e); + } +} + +function displayFigurineSubscribers(subscribers) { + const container = document.getElementById('figurine-subscribers-list'); + if (!container) return; + if (!subscribers.length) { + container.innerHTML = '

No subscribers yet.

'; + return; + } + let html = ''; + container.innerHTML = html; +} + +async function addFigurineSubscriber() { + try { + console.log('➕ Figurines: Adding subscriber...'); + const uid = document.getElementById('figurine-user-id').value.trim(); + if (!uid) { + showNotification('Enter a user ID', 'error'); + return; + } + const form = new FormData(); + form.append('user_id', uid); + const res = await fetch('/figurines/subscribers', { method: 'POST', body: form }); + const data = await res.json(); + console.log('➕ Figurines: Add subscriber response:', data); + if (data.status === 'ok') { + showNotification('Subscriber added'); + document.getElementById('figurine-user-id').value = ''; + refreshFigurineSubscribers(); + } else { + showNotification(data.message || 'Failed to add subscriber', 'error'); + } + } catch (e) { + console.error('❌ Figurines: Failed to add subscriber:', e); + showNotification('Failed to add subscriber', 'error'); + } +} + +async function removeFigurineSubscriber(uid) { + try { + console.log(`🗑ïļ Figurines: Removing subscriber ${uid}...`); + const data = await apiCall(`/figurines/subscribers/${uid}`, 'DELETE'); + console.log('🗑ïļ Figurines: Remove subscriber response:', data); + if (data.status === 'ok') { + showNotification('Subscriber removed'); + refreshFigurineSubscribers(); + } else { + showNotification(data.message || 'Failed to remove subscriber', 'error'); + } + } catch (e) { + console.error('❌ Figurines: Failed to remove subscriber:', e); + } +} + +async function sendFigurineNowToAll() { + try { + console.log('ðŸ“Ļ Figurines: Triggering send to all subscribers...'); + const tweetUrl = document.getElementById('figurine-tweet-url-all').value.trim(); + const statusDiv = document.getElementById('figurine-all-status'); + + statusDiv.textContent = 'Sending...'; + statusDiv.style.color = evilMode ? '#ff4444' : '#007bff'; + + const formData = new FormData(); + if (tweetUrl) { + formData.append('tweet_url', tweetUrl); + } + + const res = await fetch('/figurines/send_now', { + method: 'POST', + body: formData + }); + const data = await res.json(); + + console.log('ðŸ“Ļ Figurines: Send to all response:', data); + if (data.status === 'ok') { + showNotification('Figurine DMs queued for all subscribers'); + statusDiv.textContent = 'Queued successfully'; + statusDiv.style.color = '#28a745'; + document.getElementById('figurine-tweet-url-all').value = ''; // Clear input + } else { + showNotification(data.message || 'Bot not ready', 'error'); + statusDiv.textContent = 'Failed: ' + (data.message || 'Unknown error'); + statusDiv.style.color = '#dc3545'; + } + } catch (e) { + console.error('❌ Figurines: Failed to queue figurine DMs for all:', e); + showNotification('Failed to queue figurine DMs', 'error'); + document.getElementById('figurine-all-status').textContent = 'Error: ' + e.message; + document.getElementById('figurine-all-status').style.color = '#dc3545'; + } +} + +async function sendFigurineToSingleUser() { + try { + const userId = document.getElementById('figurine-single-user-id').value.trim(); + const tweetUrl = document.getElementById('figurine-tweet-url-single').value.trim(); + const statusDiv = document.getElementById('figurine-single-status'); + + if (!userId) { + showNotification('Enter a user ID', 'error'); + return; + } + + console.log(`ðŸ“Ļ Figurines: Sending to single user ${userId}, tweet: ${tweetUrl || 'random'}`); + + statusDiv.textContent = 'Sending...'; + statusDiv.style.color = evilMode ? '#ff4444' : '#007bff'; + + const formData = new FormData(); + formData.append('user_id', userId); + if (tweetUrl) { + formData.append('tweet_url', tweetUrl); + } + + const res = await fetch('/figurines/send_to_user', { + method: 'POST', + body: formData + }); + const data = await res.json(); + + console.log('ðŸ“Ļ Figurines: Send to single user response:', data); + if (data.status === 'ok') { + showNotification(`Figurine DM queued for user ${userId}`); + statusDiv.textContent = 'Queued successfully'; + statusDiv.style.color = '#28a745'; + document.getElementById('figurine-single-user-id').value = ''; // Clear inputs + document.getElementById('figurine-tweet-url-single').value = ''; + } else { + showNotification(data.message || 'Failed to queue DM', 'error'); + statusDiv.textContent = 'Failed: ' + (data.message || 'Unknown error'); + statusDiv.style.color = '#dc3545'; + } + } catch (e) { + console.error('❌ Figurines: Failed to queue figurine DM for single user:', e); + showNotification('Failed to queue figurine DM', 'error'); + document.getElementById('figurine-single-status').textContent = 'Error: ' + e.message; + document.getElementById('figurine-single-status').style.color = '#dc3545'; + } +} + +// Keep the old function for backward compatibility +async function sendFigurineNow() { + return sendFigurineNowToAll(); +} + +async function addServer() { + // Don't use parseInt() for Discord IDs - they're too large for JS integers + const guildId = document.getElementById('new-guild-id').value.trim(); + const guildName = document.getElementById('new-guild-name').value; + const autonomousChannelId = document.getElementById('new-autonomous-channel-id').value.trim(); + const autonomousChannelName = document.getElementById('new-autonomous-channel-name').value; + const bedtimeChannelIds = document.getElementById('new-bedtime-channel-ids').value + .split(',').map(id => id.trim()).filter(id => id.length > 0); + + const enabledFeatures = []; + if (document.getElementById('feature-autonomous').checked) enabledFeatures.push('autonomous'); + if (document.getElementById('feature-bedtime').checked) enabledFeatures.push('bedtime'); + if (document.getElementById('feature-monday-video').checked) enabledFeatures.push('monday_video'); + + if (!guildId || !guildName || !autonomousChannelId || !autonomousChannelName) { + showNotification('Please fill in all required fields', 'error'); + return; + } + + try { + await apiCall('/servers', 'POST', { + guild_id: guildId, + guild_name: guildName, + autonomous_channel_id: autonomousChannelId, + autonomous_channel_name: autonomousChannelName, + bedtime_channel_ids: bedtimeChannelIds.length > 0 ? bedtimeChannelIds : [autonomousChannelId], + enabled_features: enabledFeatures + }); + + showNotification('Server added successfully'); + loadServers(); + + // Clear form + document.getElementById('new-guild-id').value = ''; + document.getElementById('new-guild-name').value = ''; + document.getElementById('new-autonomous-channel-id').value = ''; + document.getElementById('new-autonomous-channel-name').value = ''; + document.getElementById('new-bedtime-channel-ids').value = ''; + } catch (error) { + console.error('Failed to add server:', error); + } +} + +async function removeServer(guildId) { + if (!confirm('Are you sure you want to remove this server?')) { + return; + } + + try { + await apiCall(`/servers/${guildId}`, 'DELETE'); + showNotification('Server removed successfully'); + loadServers(); + } catch (error) { + console.error('Failed to remove server:', error); + } +} + +async function editServer(guildId) { + // For now, just show a notification - you can implement a full edit form later + showNotification('Edit functionality coming soon!'); +} + +async function repairConfig() { + if (!confirm('This will attempt to repair corrupted server configurations. Are you sure?')) { + return; + } + try { + await apiCall('/servers/repair', 'POST'); + showNotification('Configuration repair initiated. Please refresh the page to see updated server list.'); + loadServers(); // Reload servers to reflect potential changes + } catch (error) { + console.error('Failed to repair config:', error); + showNotification(error.message || 'Failed to repair configuration', 'error'); + } +} + +// Populate mood dropdowns with available moods +async function populateMoodDropdowns() { + try { + console.log('🎭 Loading available moods...'); + const data = await apiCall('/moods/available'); + console.log('🎭 Available moods response:', data); + + if (data.moods) { + console.log(`🎭 Found ${data.moods.length} moods:`, data.moods); + const emojiMap = evilMode ? EVIL_MOOD_EMOJIS : MOOD_EMOJIS; + + // Populate the DM mood dropdown (#mood on tab1) + const dmMoodSelect = document.getElementById('mood'); + if (dmMoodSelect) { + dmMoodSelect.innerHTML = ''; + data.moods.forEach(mood => { + const opt = document.createElement('option'); + opt.value = mood; + opt.textContent = `${emojiMap[mood] || ''} ${mood}`.trim(); + if (mood === 'neutral') opt.selected = true; + dmMoodSelect.appendChild(opt); + }); + } + + // Populate the chat mood dropdown (#chat-mood-select on tab7) + const chatMoodSelect = document.getElementById('chat-mood-select'); + if (chatMoodSelect) { + chatMoodSelect.innerHTML = ''; + data.moods.forEach(mood => { + const opt = document.createElement('option'); + opt.value = mood; + opt.textContent = `${emojiMap[mood] || ''} ${mood}`.trim(); + if (mood === 'neutral') opt.selected = true; + chatMoodSelect.appendChild(opt); + }); + } + + // Populate per-server mood dropdowns (mood-select-{guildId}) + document.querySelectorAll('[id^="mood-select-"]').forEach(select => { + // Keep only the first option ("Select Mood...") + while (select.children.length > 1) { + select.removeChild(select.lastChild); + } + }); + + data.moods.forEach(mood => { + const moodOption = document.createElement('option'); + moodOption.value = mood; + moodOption.textContent = `${mood} ${emojiMap[mood] || ''}`; + + document.querySelectorAll('[id^="mood-select-"]').forEach(select => { + select.appendChild(moodOption.cloneNode(true)); + }); + }); + + console.log('🎭 All mood dropdowns populated successfully'); + } else { + console.warn('🎭 No moods found in response'); + } + } catch (error) { + console.error('🎭 Failed to load available moods:', error); + } +} + +// Per-Server Mood Management +async function setServerMood(guildId) { + console.log(`🎭 setServerMood called with guildId: ${guildId} (type: ${typeof guildId})`); + + // Ensure guildId is a string for consistency + const guildIdStr = String(guildId); + console.log(`🎭 Using guildId as string: ${guildIdStr}`); + + // Debug: Check what elements exist + const elementId = `mood-select-${guildIdStr}`; + console.log(`🎭 Looking for element with ID: ${elementId}`); + + const moodSelect = document.getElementById(elementId); + console.log(`🎭 Found element:`, moodSelect); + + if (!moodSelect) { + console.error(`🎭 ERROR: Element with ID '${elementId}' not found!`); + console.log(`🎭 Available mood-select elements:`, document.querySelectorAll('[id^="mood-select-"]')); + showNotification(`Error: Mood selector not found for server ${guildIdStr}`, 'error'); + return; + } + + const selectedMood = moodSelect.value; + + console.log(`🎭 Setting mood for server ${guildIdStr} to ${selectedMood}`); + + if (!selectedMood) { + showNotification('Please select a mood', 'error'); + return; + } + + // Get the button and store original text before any changes + const button = moodSelect.nextElementSibling; + const originalText = button.textContent; + + try { + // Show loading state + button.textContent = 'Changing...'; + button.disabled = true; + + console.log(`🎭 Making API call to /servers/${guildIdStr}/mood with mood: ${selectedMood}`); + const response = await apiCall(`/servers/${guildIdStr}/mood`, 'POST', { mood: selectedMood }); + console.log(`🎭 API response:`, response); + + if (response.status === 'ok') { + showNotification(`Server mood changed to ${selectedMood} ${MOOD_EMOJIS[selectedMood] || ''}`); + + // Reset dropdown selection + moodSelect.value = ''; + + // Reload servers to show updated mood + loadServers(); + } else { + showNotification(`Failed to change mood: ${response.message}`, 'error'); + } + } catch (error) { + console.error(`🎭 Error setting mood:`, error); + showNotification(`Failed to change mood: ${error}`, 'error'); + } finally { + // Restore button state + button.textContent = originalText; + button.disabled = false; + } +} + +async function resetServerMood(guildId) { + console.log(`🎭 resetServerMood called with guildId: ${guildId} (type: ${typeof guildId})`); + + // Ensure guildId is a string for consistency + const guildIdStr = String(guildId); + console.log(`🎭 Using guildId as string: ${guildIdStr}`); + + const button = document.querySelector(`button[onclick="resetServerMood('${guildIdStr}')"]`); + const originalText = button ? button.textContent : 'Reset'; + + try { + // Show loading state + if (button) { + button.textContent = 'Resetting...'; + button.disabled = true; + } + + await apiCall(`/servers/${guildIdStr}/mood/reset`, 'POST'); + showNotification(`Server mood reset to neutral`); + + // Reload servers to show updated mood + loadServers(); + } catch (error) { + showNotification(`Failed to reset mood: ${error}`, 'error'); + } finally { + // Restore button state + if (button) { + button.textContent = originalText; + button.disabled = false; + } + } +} + +async function updateBedtimeRange(guildId) { + console.log(`⏰ updateBedtimeRange called with guildId: ${guildId}`); + + // Ensure guildId is a string for consistency + const guildIdStr = String(guildId); + + // Get the time values from the inputs + const startTimeInput = document.getElementById(`bedtime-start-${guildIdStr}`); + const endTimeInput = document.getElementById(`bedtime-end-${guildIdStr}`); + + if (!startTimeInput || !endTimeInput) { + showNotification('Could not find bedtime time inputs', 'error'); + return; + } + + const startTime = startTimeInput.value; // Format: "HH:MM" + const endTime = endTimeInput.value; // Format: "HH:MM" + + if (!startTime || !endTime) { + showNotification('Please enter both start and end times', 'error'); + return; + } + + // Parse the times + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`); + const originalText = button ? button.textContent : 'Update Bedtime Range'; + + try { + // Show loading state + if (button) { + button.textContent = 'Updating...'; + button.disabled = true; + } + + // Send the update request + await apiCall(`/servers/${guildIdStr}/bedtime-range`, 'POST', { + bedtime_hour: startHour, + bedtime_minute: startMinute, + bedtime_hour_end: endHour, + bedtime_minute_end: endMinute + }); + + showNotification(`Bedtime range updated: ${startTime} - ${endTime}`); + + // Reload servers to show updated configuration + loadServers(); + + } catch (error) { + console.error('Failed to update bedtime range:', error); + } finally { + // Restore button state + if (button) { + button.textContent = originalText; + button.disabled = false; + } + } +} + +// Mood Management +async function setMood() { + const mood = document.getElementById('mood').value; + try { + // Use different endpoint for evil mode + const endpoint = evilMode ? '/evil-mode/mood' : '/mood'; + await apiCall(endpoint, 'POST', { mood: mood }); + showNotification(`Mood set to ${mood}`); + currentMood = mood; + } catch (error) { + console.error('Failed to set mood:', error); + } +} + +async function resetMood() { + try { + if (evilMode) { + await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' }); + showNotification('Evil mood reset to evil_neutral'); + currentMood = 'evil_neutral'; + document.getElementById('mood').value = 'evil_neutral'; + } else { + await apiCall('/mood/reset', 'POST'); + showNotification('Mood reset to neutral'); + currentMood = 'neutral'; + document.getElementById('mood').value = 'neutral'; + } + } catch (error) { + console.error('Failed to reset mood:', error); + } +} + +async function calmMiku() { + try { + if (evilMode) { + await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' }); + showNotification('Evil Miku has been calmed down'); + currentMood = 'evil_neutral'; + document.getElementById('mood').value = 'evil_neutral'; + } else { + await apiCall('/mood/calm', 'POST'); + showNotification('Miku has been calmed down'); + } + } catch (error) { + console.error('Failed to calm Miku:', error); + } +} + +// ===== Language Mode Functions ===== +async function refreshLanguageStatus() { + try { + const result = await apiCall('/language'); + document.getElementById('current-language-display').textContent = + result.language_mode === 'japanese' ? 'æ—ĨæœŽčŠž (Japanese)' : 'English'; + document.getElementById('status-language').textContent = + result.language_mode === 'japanese' ? 'æ—ĨæœŽčŠž (Japanese)' : 'English'; + document.getElementById('status-model').textContent = result.current_model; + + console.log('Language status:', result); + } catch (error) { + console.error('Failed to get language status:', error); + showNotification('Failed to load language status', 'error'); + } +} + +async function toggleLanguageMode() { + try { + const result = await apiCall('/language/toggle', 'POST'); + + // Update UI + document.getElementById('current-language-display').textContent = + result.language_mode === 'japanese' ? 'æ—ĨæœŽčŠž (Japanese)' : 'English'; + document.getElementById('status-language').textContent = + result.language_mode === 'japanese' ? 'æ—ĨæœŽčŠž (Japanese)' : 'English'; + document.getElementById('status-model').textContent = result.model_now_using; + + // Show notification + showNotification(result.message, 'success'); + console.log('Language toggled:', result); + } catch (error) { + console.error('Failed to toggle language mode:', error); + showNotification('Failed to toggle language mode', 'error'); + } +} diff --git a/bot/static/js/status.js b/bot/static/js/status.js new file mode 100644 index 0000000..fd67f7b --- /dev/null +++ b/bot/static/js/status.js @@ -0,0 +1,286 @@ +// ============================================================================ +// 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 = '
Server Moods:
'; + 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] || ''}
`; + } + serverMoodsHtml += '
'; + } + + const moodEmoji = evilMode ? (EVIL_MOOD_EMOJIS[result.mood] || '') : (MOOD_EMOJIS[result.mood] || ''); + const moodLabel = evilMode ? `😈 ${result.mood} ${moodEmoji}` : `${result.mood} ${moodEmoji}`; + + statusDiv.innerHTML = ` +
Status: ${result.status}
+
DM Mood: ${moodLabel}
+
Servers: ${result.servers}
+
Active Schedulers: ${result.active_schedulers}
+
+ 💎 DM Support: Users can message Miku directly in DMs. She responds to every DM message using the DM mood (auto-rotating every 2 hours). +
+ ${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 = `User: ${escapeHtml(result.user || '?')}  |  Mood: ${escapeHtml(result.mood || '?')}  |  Time: ${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 = '

Please select a server to view autonomous stats.

'; + return; + } + + try { + const data = await apiCall('/autonomous/stats'); + + if (!data.servers || !data.servers[selectedGuildId]) { + document.getElementById('autonomous-stats-display').innerHTML = '

Server not found or not initialized.

'; + 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 = ` +
+

⚠ïļ Context Not Initialized

+

This server hasn't had any activity yet. Context tracking will begin once messages are sent.

+
+ Current Mood: ${data.mood} ${MOOD_EMOJIS[data.mood] || ''}
+ Energy: ${data.mood_profile.energy}
+ Sociability: ${data.mood_profile.sociability}
+ Impulsiveness: ${data.mood_profile.impulsiveness} +
+
+ `; + 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 = ` +
+

🎭 Mood & Personality Profile

+
+
+
Current Mood
+
${data.mood} ${MOOD_EMOJIS[data.mood] || ''}
+
+
+
Energy Level
+
${(profile.energy * 100).toFixed(0)}%
+
+
+
+
+
+
Sociability
+
${(profile.sociability * 100).toFixed(0)}%
+
+
+
+
+
+
Impulsiveness
+
${(profile.impulsiveness * 100).toFixed(0)}%
+
+
+
+
+
+
+ +
+

📈 Activity Metrics

+
+
+
Messages (Last 5 min) ⚡ ephemeral
+
${ctx.messages_last_5min}
+
+
+
Messages (Last Hour) ⚡ ephemeral
+
${ctx.messages_last_hour}
+
+
+
Conversation Momentum ðŸ’ū saved
+
${(ctx.conversation_momentum * 100).toFixed(0)}%
+
Decays with downtime (half-life: 10min)
+
+
+
Unique Users Active ⚡ ephemeral
+
${ctx.unique_users_active}
+
+
+
+ +
+

ðŸ‘Ĩ User Events

+
+
+
Users Joined Recently
+
${ctx.users_joined_recently}
+
+
+
Status Changes
+
${ctx.users_status_changed}
+
+
+
Active Activities
+
${ctx.users_started_activity.length}
+ ${ctx.users_started_activity.length > 0 ? `
${ctx.users_started_activity.join(', ')}
` : ''} +
+
+
+ +
+

⏱ïļ Timing & Context

+
+
+
Time Since Last Action ðŸ’ū saved
+
${lastActionMin} min
+
${ctx.time_since_last_action.toFixed(1)}s
+
+
+
Time Since Last Interaction ðŸ’ū saved
+
${lastInteractionMin} min
+
${ctx.time_since_last_interaction.toFixed(1)}s
+
+
+
Messages Since Last Appearance ðŸ’ū saved
+
${ctx.messages_since_last_appearance}
+
+
+
Current Time Context ⚡ ephemeral
+
${ctx.hour_of_day}:00
+
${ctx.is_weekend ? '📅 Weekend' : '📆 Weekday'}
+
+
+
+ +
+

🧠 Base Energy Level

+
+
From current mood personality
+
${(ctx.mood_energy_level * 100).toFixed(0)}%
+
+
+
+
+ ðŸ’Ą Combined with activity metrics to determine action likelihood.
+ 📝 High energy = shorter wait times, higher action chance.
+ ðŸ’ū Persisted across restarts +
+
+
+ `; +} + +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 = ''; + + 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; + } +}