2025-12-07 17:15:09 +02:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title > Miku Control Panel< / title >
< style >
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;
}
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: 1000;
font-size: 0.9rem;
}
.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 */
.conversation-view {
max-width: 800px;
margin: 0 auto;
}
.conversations-list {
max-height: 600px;
overflow-y: auto;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
background: #222;
}
.conversation-message {
margin-bottom: 1rem;
padding: 0.75rem;
border-radius: 8px;
border-left: 4px solid;
}
.conversation-message.user-message {
background: #2a2a3a;
border-left-color: #4CAF50;
}
.conversation-message.bot-message {
background: #3a2a2a;
border-left-color: #2196F3;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.sender {
font-weight: bold;
color: #fff;
}
.timestamp {
color: #999;
font-size: 0.8rem;
}
.message-content {
color: #ddd;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-attachments {
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255,255,255,0.05);
border-radius: 4px;
font-size: 0.9rem;
}
.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: flex;
border-bottom: 2px solid #333;
margin-bottom: 1rem;
}
.tab-button {
background: #222;
color: #ccc;
border: none;
padding: 0.8rem 1.5rem;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-right: 0.5rem;
transition: all 0.3s ease;
}
.tab-button:hover {
background: #333;
color: #fff;
}
.tab-button.active {
background: #444;
color: #fff;
border-bottom-color: #4CAF50;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
< / style >
< / head >
< body >
< div class = "panel" >
< h1 > Miku Control Panel< / h1 >
< p style = "color: #ccc; margin-bottom: 2rem;" >
💬 < strong > DM Support:< / strong > Users can message Miku directly in DMs. She responds to every message using the DM mood (auto-rotating every 2 hours).
< / p >
<!-- Tab Navigation -->
< div class = "tab-container" >
< div class = "tab-buttons" >
< button class = "tab-button active" onclick = "switchTab('tab1')" > Server Management< / button >
< button class = "tab-button" onclick = "switchTab('tab2')" > Actions< / button >
< button class = "tab-button" onclick = "switchTab('tab3')" > Status< / button >
< button class = "tab-button" onclick = "switchTab('tab4')" > 🎨 Image Generation< / button >
< button class = "tab-button" onclick = "switchTab('tab5')" > 📊 Autonomous Stats< / button >
< / div >
<!-- Tab 1 Content -->
< div id = "tab1" class = "tab-content active" >
< div class = "section" >
< label for = "mood" > Mood:< / label >
< select id = "mood" >
< option value = "angry" > 💢 angry< / option >
< option value = "asleep" > 💤 asleep< / option >
< option value = "bubbly" > 🫧 bubbly< / option >
< option value = "curious" > 👀 curious< / option >
< option value = "excited" > ✨ excited< / option >
< option value = "flirty" > 🫦 flirty< / option >
< option value = "irritated" > 😒 irritated< / option >
< option value = "melancholy" > 🍷 melancholy< / option >
< option value = "neutral" selected > neutral< / option >
< option value = "romantic" > 💌 romantic< / option >
< option value = "serious" > 👔 serious< / option >
< option value = "shy" > 👉👈 shy< / option >
< option value = "silly" > 🪿 silly< / option >
< option value = "sleepy" > 🌙 sleepy< / option >
< / select >
< button onclick = "setMood()" > Set Mood< / button >
< button onclick = "resetMood()" > Reset Mood< / button >
< button onclick = "calmMiku()" > Calm< / button >
< / div >
< div class = "section" >
< h3 > Server Management< / h3 >
< div id = "servers-list" > < / div >
< div class = "add-server-form" >
< h4 > Add New Server< / h4 >
< div class = "form-row" >
< div class = "form-group" >
< label > Guild ID:< / label >
< input type = "number" id = "new-guild-id" placeholder = "Discord Server ID" >
< / div >
< div class = "form-group" >
< label > Server Name:< / label >
< input type = "text" id = "new-guild-name" placeholder = "Server Name" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Autonomous Channel ID:< / label >
< input type = "number" id = "new-autonomous-channel-id" placeholder = "Channel ID" >
< / div >
< div class = "form-group" >
< label > Channel Name:< / label >
< input type = "text" id = "new-autonomous-channel-name" placeholder = "Channel Name" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Bedtime Channel IDs (comma-separated):< / label >
< input type = "text" id = "new-bedtime-channel-ids" placeholder = "Channel IDs" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Enabled Features:< / label >
< div class = "checkbox-group" >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-autonomous" checked >
< label for = "feature-autonomous" > Autonomous< / label >
< / div >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-bedtime" checked >
< label for = "feature-bedtime" > Bedtime< / label >
< / div >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-monday-video" checked >
< label for = "feature-monday-video" > Monday Video< / label >
< / div >
< / div >
< / div >
< / div >
< button onclick = "addServer()" > Add Server< / button >
< / div >
< div style = "margin-top: 1rem;" >
< button onclick = "repairConfig()" style = "background: #ff9800;" > 🔧 Repair Configuration< / button >
< p style = "font-size: 0.9rem; color: #ccc; margin-top: 0.5rem;" >
Use this if you're seeing incorrect server IDs or other configuration issues
< / p >
< / div >
< / div >
< / div >
<!-- Actions Tab Content -->
< div id = "tab2" class = "tab-content" >
< div class = "section" >
< h3 > Autonomous Actions< / h3 >
< div style = "margin-bottom: 1rem;" >
< label for = "server-select" > Target Server:< / label >
< select id = "server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / div >
< button onclick = "triggerAutonomous('general')" > Say Something General< / button >
< button onclick = "triggerAutonomous('engage')" > Engage Random User< / button >
< button onclick = "triggerAutonomous('tweet')" > Share Tweet< / button >
< button onclick = "triggerAutonomous('reaction')" > React to Message< / button >
< button onclick = "toggleCustomPrompt()" > Custom Prompt< / button >
< / div >
< div class = "section" >
< h3 > 🎨 Profile Picture< / h3 >
< p style = "font-size: 0.9rem; color: #aaa;" > Change Miku's profile picture using Danbooru search or upload a custom image.< / p >
< div style = "margin-bottom: 1rem;" >
< button onclick = "changeProfilePicture()" > 🎨 Change Profile Picture (Danbooru)< / button >
< button onclick = "restoreFallbackPfp()" > 🔄 Restore Original Avatar< / button >
< / div >
< div style = "margin-bottom: 1rem;" >
< label for = "pfp-upload" > Upload Custom Image:< / label >
< input type = "file" id = "pfp-upload" accept = "image/*" style = "margin-left: 0.5rem;" >
< button onclick = "uploadCustomPfp()" > 📤 Upload & Apply< / button >
2025-12-07 23:48:12 +02:00
< div style = "font-size: 0.8rem; color: #888; margin-top: 0.3rem; margin-left: 0.5rem;" >
💡 Supports static images (PNG, JPG) and animated GIFs< br >
⚠️ Animated GIFs require Discord Nitro on the bot account
< / div >
2025-12-07 17:15:09 +02:00
< / div >
< div id = "pfp-status" style = "margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;" > < / div >
< div id = "pfp-metadata" style = "margin-top: 1rem; background: #1e1e1e; padding: 0.5rem; border: 1px solid #333; display: none;" >
< h4 style = "margin-top: 0;" > Current Profile Picture Info:< / h4 >
< pre id = "pfp-metadata-content" style = "margin: 0;" > < / pre >
< / div >
<!-- Role Color Management -->
< div style = "margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #333;" >
< h4 > 🎨 Role Color Management< / h4 >
< p style = "font-size: 0.9rem; color: #aaa;" > Manually set Miku's role color or reset to fallback (#86cecb)< / p >
< div style = "margin-bottom: 1rem; display: flex; gap: 10px; align-items: end;" >
< div >
< label for = "role-color-hex" > Hex Color:< / label >
< input type = "text" id = "role-color-hex" placeholder = "#86cecb" maxlength = "7" style = "width: 100px; font-family: monospace;" >
< / div >
< button onclick = "setCustomRoleColor()" > 🎨 Apply Color< / button >
< button onclick = "resetRoleColor()" > 🔄 Reset to Fallback< / button >
< / div >
< div id = "role-color-status" style = "margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;" > < / div >
< / div >
< / div >
< div class = "section" >
< h3 > Figurine DM Subscribers< / h3 >
<!-- Subscriber Management -->
< div style = "margin-bottom: 1rem;" >
< h4 > Subscriber Management< / h4 >
< div style = "margin-bottom: 0.5rem;" >
< button onclick = "refreshFigurineSubscribers()" > 🔄 Refresh< / button >
< / div >
< div style = "display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;" >
< div >
< label for = "figurine-user-id" > User ID:< / label >
< input type = "text" id = "figurine-user-id" placeholder = "Discord User ID (as string)" / >
< / div >
< button onclick = "addFigurineSubscriber()" > ➕ Add Subscriber< / button >
< / div >
< div id = "figurine-subscribers-list" > < / div >
< / div >
<!-- Send to All Subscribers -->
< div style = "margin-bottom: 1rem; border-top: 1px solid #444; padding-top: 1rem;" >
< h4 > Send to All Subscribers< / h4 >
< div style = "margin-bottom: 0.5rem;" >
< label for = "figurine-tweet-url-all" > Tweet URL (optional):< / label >
< input type = "text" id = "figurine-tweet-url-all" placeholder = "https://twitter.com/username/status/..." style = "width: 300px;" / >
< / div >
< button onclick = "sendFigurineNowToAll()" > 📨 Send to All Subscribers< / button >
< div id = "figurine-all-status" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- Send to Single User -->
< div style = "border-top: 1px solid #444; padding-top: 1rem;" >
< h4 > Send to Single User< / h4 >
< div style = "display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;" >
< div >
< label for = "figurine-single-user-id" > User ID:< / label >
< input type = "text" id = "figurine-single-user-id" placeholder = "Discord User ID" / >
< / div >
< div >
< label for = "figurine-tweet-url-single" > Tweet URL (optional):< / label >
< input type = "text" id = "figurine-tweet-url-single" placeholder = "https://twitter.com/username/status/..." style = "width: 250px;" / >
< / div >
< button onclick = "sendFigurineToSingleUser()" > 📨 Send to User< / button >
< / div >
< div id = "figurine-single-status" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
< / div >
< div class = "section" >
< h3 > Manual Actions< / h3 >
< div style = "margin-bottom: 1rem;" >
< label for = "manual-server-select" > Target Server:< / label >
< select id = "manual-server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / div >
< button onclick = "forceSleep()" > Force Sleep< / button >
< button onclick = "wakeUp()" > Wake Up< / button >
< button onclick = "sendBedtime()" > Send Bedtime< / button >
< button onclick = "resetConversation()" > Reset Conversation< / button >
< / div >
< div class = "section" id = "custom-prompt-section" >
< h3 > 🎙️ Send Custom Prompt to Miku< / h3 >
<!-- Target Selection -->
< div style = "margin-bottom: 1rem;" >
< label for = "custom-prompt-target-type" > Target Type:< / label >
< select id = "custom-prompt-target-type" onchange = "toggleCustomPromptTarget()" style = "margin-right: 1rem;" >
< option value = "server" > Server< / option >
< option value = "dm" > Direct Message< / option >
< / select >
<!-- Server Selection -->
< span id = "custom-prompt-server-section" >
< label for = "custom-prompt-server-select" > Target Server:< / label >
< select id = "custom-prompt-server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / span >
<!-- DM User ID Input -->
< span id = "custom-prompt-dm-section" style = "display: none;" >
< label for = "custom-prompt-user-id" > User ID:< / label >
< input type = "text" id = "custom-prompt-user-id" placeholder = "Discord User ID" style = "width: 200px;" / >
< / span >
< / div >
< div >
< label for = "customPrompt" > Custom Prompt:< / label >
< textarea id = "customPrompt" placeholder = "e.g. Talk about how nice the weather is today" rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< div style = "margin-top: 0.5rem;" >
< label for = "customPromptAttachment" > Attach File (optional):< / label >
< input type = "file" id = "customPromptAttachment" multiple / >
< / div >
< button onclick = "sendCustomPrompt()" style = "margin-top: 0.5rem;" > Send Custom Prompt< / button >
< p id = "customStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< div class = "section" id = "manual-message-section" >
< h3 > 🎭 Send Message as Miku (Manual Override)< / h3 >
<!-- Target Selection -->
< div style = "margin-bottom: 1rem;" >
< label for = "manual-target-type" > Target Type:< / label >
< select id = "manual-target-type" onchange = "toggleManualMessageTarget()" >
< option value = "channel" > Channel< / option >
< option value = "dm" > Direct Message< / option >
< / select >
< / div >
< div >
< label for = "manualMessage" > Message:< / label >
< textarea id = "manualMessage" placeholder = "Type the message exactly as Miku should say it..." rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< div style = "margin-top: 0.5rem;" >
< label for = "manualAttachment" > Attach Files (optional):< / label >
< input type = "file" id = "manualAttachment" multiple / >
< / div >
<!-- Channel ID Input -->
< div id = "manual-channel-section" style = "margin-top: 0.5rem;" >
< label for = "manualChannelId" > Channel ID:< / label >
< input type = "text" id = "manualChannelId" placeholder = "Enter channel ID..." style = "width: 100%;" / >
< / div >
<!-- User ID Input -->
< div id = "manual-dm-section" style = "margin-top: 0.5rem; display: none;" >
< label for = "manualUserId" > User ID:< / label >
< input type = "text" id = "manualUserId" placeholder = "Enter user ID for DM..." style = "width: 100%;" / >
< / div >
< button onclick = "sendManualMessage()" style = "margin-top: 0.5rem;" > Send as Miku< / button >
< p id = "manualStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< div class = "section" id = "message-reaction-section" >
< h3 > 😊 Add Reaction to Message< / h3 >
< p style = "color: #ccc; margin-bottom: 1rem;" >
Make Miku react to a specific message with an emoji of your choice.
< / p >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionMessageId" > Message ID:< / label >
< input type = "text" id = "reactionMessageId" placeholder = "Enter message ID (right-click message > Copy ID)" style = "width: 100%; margin-top: 0.5rem;" / >
< / div >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionChannelId" > Channel ID:< / label >
< input type = "text" id = "reactionChannelId" placeholder = "Enter channel ID (right-click channel > Copy ID)" style = "width: 100%; margin-top: 0.5rem;" / >
< / div >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionEmoji" > Emoji:< / label >
< input type = "text" id = "reactionEmoji" placeholder = "Enter emoji (e.g., 💙, 👍, 🎉)" style = "width: 100%; margin-top: 0.5rem;" / >
< p style = "font-size: 0.85rem; color: #aaa; margin-top: 0.25rem;" >
You can use standard emoji or custom server emoji format (:emoji_name: for custom ones)
< / p >
< / div >
< button onclick = "addReactionToMessage()" style = "margin-top: 0.5rem;" > Add Reaction< / button >
< p id = "reactionStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< / div >
<!-- Status Tab Content -->
< div id = "tab3" class = "tab-content" >
< div class = "section" >
< h3 > Status< / h3 >
< div id = "status" > < / div >
< / div >
< div class = "section" >
< h3 > 📱 DM Logs< / h3 >
< div style = "margin-bottom: 1rem;" >
< button onclick = "loadDMUsers()" > 🔄 Refresh DM Users< / button >
< button onclick = "exportAllDMs()" > 📤 Export All DMs< / button >
< button onclick = "loadBlockedUsers()" style = "background: #ff9800;" > 🚫 View Blocked Users< / button >
< / div >
< div style = "margin-bottom: 1rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;" >
< h4 style = "margin-top: 0;" > 📊 DM Interaction Analysis< / h4 >
< button onclick = "runDailyAnalysis()" style = "background: #9c27b0;" > 🔍 Run Daily Analysis Now< / button >
< button onclick = "viewAnalysisReports()" style = "background: #673ab7;" > 📄 View All Reports< / button >
< p style = "font-size: 0.85rem; margin: 0.5rem 0 0 0; color: #aaa;" >
Analysis runs automatically at 2 AM daily. Reports one user per day.
< / p >
< / div >
< div id = "dm-users-list" > < / div >
< div class = "section" id = "blocked-users-section" style = "display: none; margin-top: 2rem;" >
< h4 > 🚫 Blocked Users< / h4 >
< div style = "margin-bottom: 1rem;" >
< button onclick = "hideBlockedUsers()" > ← Back to DM Users< / button >
< / div >
< div id = "blocked-users-list" > < / div >
< / div >
< / div >
< div class = "section" >
< h3 > Last Prompt< / h3 >
< pre id = "last-prompt" > < / pre >
< / div >
< / div >
<!-- Image Generation Tab Content -->
< div id = "tab4" class = "tab-content" >
< div class = "section" >
< h3 > 🎨 Image Generation System< / h3 >
< p > Natural language image generation powered by ComfyUI. Users can ask Miku to create images naturally without commands!< / p >
<!-- Status Section -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > System Status< / h4 >
< div id = "image-system-status" style = "margin-bottom: 1rem;" >
< button onclick = "checkImageSystemStatus()" > 🔄 Check Status< / button >
< / div >
< div id = "image-status-display" style = "background: #2a2a2a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;" > < / div >
< / div >
<!-- Detection Testing -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Test Natural Language Detection< / h4 >
< div style = "margin-bottom: 1rem;" >
< label for = "detection-test-message" > Test Message:< / label >
< textarea id = "detection-test-message" placeholder = "e.g. Hey Miku, I'd like to see you swimming in a pool" rows = "2" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< button onclick = "testImageDetection()" style = "margin-right: 0.5rem;" > 🔍 Test Detection< / button >
< div id = "detection-test-results" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- Manual Image Generation -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Manual Image Generation< / h4 >
< div style = "margin-bottom: 1rem;" >
< label for = "manual-image-prompt" > Image Prompt:< / label >
< textarea id = "manual-image-prompt" placeholder = "Describe the image you want to generate..." rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< button onclick = "generateImage()" style = "margin-right: 0.5rem;" > 🎨 Generate Image< / button >
< div id = "manual-generation-results" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- System Information -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Image Generation Settings< / h4 >
< div style = "background: #2a2a2a; padding: 1rem; border-radius: 4px;" >
< div style = "margin-bottom: 0.5rem;" > < strong > ComfyUI Configuration:< / strong > < / div >
< ul style = "margin: 0; padding-left: 1.5rem;" >
< li > URL: Auto-detected (tries multiple Docker networking options)< / li >
< li > Workflow Template: < code > Miku_BasicWorkflow.json< / code > < / li >
< li > Host Output Directory: < code > /home/koko210Serve/ComfyUI/output/< / code > < / li >
< li > Container Mount Point: < code > /app/ComfyUI/output/< / code > < / li >
< li > Generation Timeout: 300 seconds< / li >
< / ul >
< div style = "margin-top: 1rem; font-size: 0.9rem; color: #aaa;" >
< strong > Note:< / strong > Make sure ComfyUI is running and the workflow template exists in the bot directory.
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Autonomous Stats Tab Content -->
< div id = "tab5" class = "tab-content" >
< div class = "section" >
< h3 > 📊 Autonomous V2 Decision Engine Stats< / h3 >
< p > Real-time monitoring of Miku's autonomous decision-making context and mood-based personality stats.< / p >
< div style = "margin-bottom: 1.5rem;" >
< label for = "autonomous-server-select" > Select Server:< / label >
< select id = "autonomous-server-select" onchange = "loadAutonomousStats()" >
< option value = "" > -- Select a server --< / option >
< / select >
< button onclick = "loadAutonomousStats()" style = "margin-left: 0.5rem;" > 🔄 Refresh< / button >
< / div >
< div id = "autonomous-stats-display" > < / div >
< / div >
< / div >
< / div >
< / div >
< div class = "logs" >
< h3 > Logs< / h3 >
< div id = "logs-content" > < / div >
< / div >
< div id = "notification" > < / div >
< script >
// Global variables
let currentMood = 'neutral';
let servers = [];
// Mood emoji mapping
const MOOD_EMOJIS = {
"asleep": "💤",
"neutral": "",
"bubbly": "🫧",
"sleepy": "🌙",
"curious": "👀",
"shy": "👉👈",
"serious": "👔",
"excited": "✨",
"melancholy": "🍷",
"flirty": "🫦",
"romantic": "💌",
"irritated": "😒",
"angry": "💢",
"silly": "🪿"
};
// Tab switching functionality
function switchTab(tabId) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Remove active class from all tab buttons
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
// Show the selected tab content
document.getElementById(tabId).classList.add('active');
// Add active class to the clicked tab button
event.target.classList.add('active');
console.log(`🔄 Switched to ${tabId}`);
if (tabId === 'tab1') {
console.log('🔄 Refreshing figurine subscribers for Server Management tab');
refreshFigurineSubscribers();
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadStatus();
loadServers();
loadLastPrompt();
loadLogs();
console.log('🚀 DOMContentLoaded - initializing figurine subscribers list');
refreshFigurineSubscribers();
loadProfilePictureMetadata();
// Set up periodic updates
setInterval(loadStatus, 10000);
setInterval(loadLogs, 5000);
});
// Utility functions
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.style.display = 'block';
notification.style.backgroundColor = type === 'error' ? '#d32f2f' : '#222';
setTimeout(() => {
notification.style.display = 'none';
}, 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;
}
}
// Server Management
async function loadServers() {
try {
console.log('🎭 Loading servers...');
const response = await fetch('/servers');
const data = await response.json();
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 = '< p > No servers configured< / p > ';
return;
}
container.innerHTML = servers.map(server => `
< div class = "server-card" >
< div class = "server-header" >
< div class = "server-name" > ${server.guild_name}< / div >
< div class = "server-actions" >
< button onclick = "editServer('${String(server.guild_id)}')" > Edit< / button >
< button onclick = "removeServer('${String(server.guild_id)}')" style = "background: #d32f2f;" > Remove< / button >
< / div >
< / div >
< div > < strong > Guild ID:< / strong > ${server.guild_id}< / div >
< div > < strong > Autonomous Channel:< / strong > #${server.autonomous_channel_name} (${server.autonomous_channel_id})< / div >
< div > < strong > Bedtime Channels:< / strong > ${server.bedtime_channel_ids.join(', ')}< / div >
< div > < strong > Features:< / strong >
${server.enabled_features.map(feature => `< span class = "feature-tag" > ${feature}< / span > `).join('')}
< / div >
< div > < strong > Autonomous Interval:< / strong > ${server.autonomous_interval_minutes} minutes< / div >
< div > < strong > Conversation Detection:< / strong > ${server.conversation_detection_interval_minutes} minutes< / div >
< div > < strong > Bedtime Range:< / strong > ${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')}< / div >
<!-- Bedtime Configuration -->
< div style = "margin-top: 1rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;" >
< h4 style = "margin: 0 0 0.5rem 0; color: #61dafb;" > Bedtime Settings< / h4 >
< div style = "display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem;" >
< div >
< label style = "display: block; font-size: 0.9rem; margin-bottom: 0.2rem;" > Start Time:< / label >
< input type = "time" id = "bedtime-start-${String(server.guild_id)}" value = "${String(server.bedtime_hour || 21).padStart(2, '0')}:${String(server.bedtime_minute || 0).padStart(2, '0')}" style = "padding: 0.3rem; background: #333; color: white; border: 1px solid #555; border-radius: 3px; width: 100%;" >
< / div >
< div >
< label style = "display: block; font-size: 0.9rem; margin-bottom: 0.2rem;" > End Time:< / label >
< input type = "time" id = "bedtime-end-${String(server.guild_id)}" value = "${String(server.bedtime_hour_end || 23).padStart(2, '0')}:${String(server.bedtime_minute_end || 59).padStart(2, '0')}" style = "padding: 0.3rem; background: #333; color: white; border: 1px solid #555; border-radius: 3px; width: 100%;" >
< / div >
< / div >
< button onclick = "updateBedtimeRange('${String(server.guild_id)}')" style = "background: #4caf50;" > Update Bedtime Range< / button >
< / div >
<!-- Per - Server Mood Display -->
< div style = "margin-top: 1rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;" >
< h4 style = "margin: 0 0 0.5rem 0; color: #61dafb;" > Server Mood< / h4 >
< div > < strong > Current Mood:< / strong > ${server.current_mood_name || 'neutral'} ${MOOD_EMOJIS[server.current_mood_name] || ''}< / div >
< div > < strong > Sleeping:< / strong > ${server.is_sleeping ? 'Yes' : 'No'}< / div >
< div style = "margin-top: 0.5rem;" >
< select id = "mood-select-${String(server.guild_id)}" style = "margin-right: 0.5rem; padding: 0.3rem; background: #333; color: white; border: 1px solid #555; border-radius: 3px;" >
< option value = "" > Select Mood...< / option >
< / select >
< button onclick = "setServerMood('${String(server.guild_id)}')" style = "margin-right: 0.5rem;" > Change Mood< / button >
< button onclick = "resetServerMood('${String(server.guild_id)}')" style = "background: #ff9800;" > Reset Mood< / button >
< / div >
< / div >
< / div >
`).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 loadProfilePictureMetadata() {
try {
const response = await fetch('/profile-picture/metadata');
const result = await response.json();
if (response.ok & & result.status === 'ok' & & result.metadata) {
const metadataDiv = document.getElementById('pfp-metadata');
const metadataContent = document.getElementById('pfp-metadata-content');
metadataContent.textContent = JSON.stringify(result.metadata, null, 2);
metadataDiv.style.display = 'block';
console.log('🎨 Loaded profile picture metadata:', result.metadata);
} else {
console.log('🎨 No profile picture metadata available');
}
} catch (error) {
console.error('🎨 Failed to load profile picture metadata:', error);
}
}
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 = '< option value = "all" > All Servers< / option > ';
manualServerSelect.innerHTML = '< option value = "all" > All Servers< / option > ';
customPromptServerSelect.innerHTML = '< option value = "all" > All Servers< / option > ';
// Add server options
servers.forEach(server => {
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));
});
// 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 res = await fetch('/figurines/subscribers');
const data = await res.json();
console.log('📋 Figurines: Received subscribers:', data);
displayFigurineSubscribers(data.subscribers || []);
showNotification('Subscribers refreshed');
} catch (e) {
console.error('❌ Figurines: Failed to fetch subscribers:', e);
showNotification('Failed to load subscribers', 'error');
}
}
function displayFigurineSubscribers(subscribers) {
const container = document.getElementById('figurine-subscribers-list');
if (!container) return;
if (!subscribers.length) {
container.innerHTML = '< p > No subscribers yet.< / p > ';
return;
}
let html = '< ul > ';
subscribers.forEach(uid => {
const uidStr = String(uid);
html += `< li > < code > ${uidStr}< / code > < button onclick = "removeFigurineSubscriber('${uidStr}')" > Remove< / button > < / li > `;
});
html += '< / ul > ';
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 res = await fetch(`/figurines/subscribers/${uid}`, { method: 'DELETE' });
const data = await res.json();
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);
showNotification('Failed to remove subscriber', 'error');
}
}
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 = '#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 = '#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() {
const guildId = parseInt(document.getElementById('new-guild-id').value);
const guildName = document.getElementById('new-guild-name').value;
const autonomousChannelId = parseInt(document.getElementById('new-autonomous-channel-id').value);
const autonomousChannelName = document.getElementById('new-autonomous-channel-name').value;
const bedtimeChannelIds = document.getElementById('new-bedtime-channel-ids').value
.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
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 response = await fetch('/moods/available');
const data = await response.json();
console.log('🎭 Available moods response:', data);
if (data.moods) {
console.log(`🎭 Found ${data.moods.length} moods:`, data.moods);
// Clear existing mood options from all dropdowns (keep "Select Mood..." option)
document.querySelectorAll('[id^="mood-select-"]').forEach(select => {
// Keep only the first option ("Select Mood...")
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
});
// Update all mood dropdowns
data.moods.forEach(mood => {
const moodOption = document.createElement('option');
moodOption.value = mood;
moodOption.textContent = `${mood} ${MOOD_EMOJIS[mood] || ''}`;
// Add to all mood dropdowns
document.querySelectorAll('[id^="mood-select-"]').forEach(select => {
select.appendChild(moodOption.cloneNode(true));
});
});
console.log('🎭 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}`);
try {
// Show loading state
const button = document.querySelector(`button[onclick="resetServerMood('${guildIdStr}')"]`);
const originalText = button.textContent;
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
const button = document.querySelector(`button[onclick="resetServerMood('${guildIdStr}')"]`);
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);
try {
// Show loading state
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
const originalText = button.textContent;
button.textContent = 'Updating...';
button.disabled = true;
// Send the update request
const response = await fetch(`/servers/${guildIdStr}/bedtime-range`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
bedtime_hour: startHour,
bedtime_minute: startMinute,
bedtime_hour_end: endHour,
bedtime_minute_end: endMinute
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
const result = await response.json();
showNotification(`Bedtime range updated: ${startTime} - ${endTime}`);
// Reload servers to show updated configuration
loadServers();
} catch (error) {
console.error('Failed to update bedtime range:', error);
showNotification(error.message || 'Failed to update bedtime range', 'error');
// Restore button state
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
if (button) {
button.textContent = originalText;
button.disabled = false;
}
}
}
// Mood Management
async function setMood() {
const mood = document.getElementById('mood').value;
try {
await apiCall('/mood', 'POST', { mood: mood });
showNotification(`Mood set to ${mood}`);
currentMood = mood;
} catch (error) {
console.error('Failed to set mood:', error);
}
}
async function resetMood() {
try {
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 {
await apiCall('/mood/calm', 'POST');
showNotification('Miku has been calmed down');
} catch (error) {
console.error('Failed to calm Miku:', error);
}
}
// 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}`;
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
endpoint += `?guild_id=${parseInt(selectedServer)}`;
}
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (response.ok) {
showNotification(result.message || 'Action triggered successfully');
} else {
throw new Error(result.message || 'Failed to trigger action');
}
} catch (error) {
console.error('Failed to trigger autonomous action:', error);
showNotification(error.message || 'Failed to trigger action', 'error');
}
}
// Profile Picture Management
async function changeProfilePicture() {
const selectedServer = document.getElementById('server-select').value;
const statusDiv = document.getElementById('pfp-status');
const metadataDiv = document.getElementById('pfp-metadata');
const metadataContent = document.getElementById('pfp-metadata-content');
statusDiv.textContent = '⏳ Searching Danbooru and changing profile picture...';
statusDiv.style.color = '#61dafb';
try {
let endpoint = '/profile-picture/change';
const params = new URLSearchParams();
// Add guild_id parameter if a specific server is selected
if (selectedServer !== 'all') {
params.append('guild_id', selectedServer);
}
const url = params.toString() ? `${endpoint}?${params.toString()}` : endpoint;
const response = await fetch(url, {
method: 'POST'
});
const result = await response.json();
if (response.ok & & result.status === 'ok') {
statusDiv.textContent = `✅ ${result.message}`;
statusDiv.style.color = 'green';
// Display metadata if available
if (result.metadata) {
metadataContent.textContent = JSON.stringify(result.metadata, null, 2);
metadataDiv.style.display = 'block';
}
showNotification('Profile picture changed successfully!');
} else {
throw new Error(result.message || 'Failed to change profile picture');
}
} catch (error) {
console.error('Failed to change profile picture:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
showNotification(error.message || 'Failed to change profile picture', 'error');
}
}
async function uploadCustomPfp() {
const fileInput = document.getElementById('pfp-upload');
const selectedServer = document.getElementById('server-select').value;
const statusDiv = document.getElementById('pfp-status');
const metadataDiv = document.getElementById('pfp-metadata');
const metadataContent = document.getElementById('pfp-metadata-content');
if (!fileInput.files || fileInput.files.length === 0) {
showNotification('Please select an image file first', 'error');
return;
}
const file = fileInput.files[0];
// Validate file type
if (!file.type.startsWith('image/')) {
showNotification('Please select a valid image file', 'error');
return;
}
statusDiv.textContent = '⏳ Uploading and processing custom image...';
statusDiv.style.color = '#61dafb';
try {
const formData = new FormData();
formData.append('file', file);
// Add guild_id parameter if a specific server is selected
let endpoint = '/profile-picture/change';
if (selectedServer !== 'all') {
endpoint += `?guild_id=${selectedServer}`;
}
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok & & result.status === 'ok') {
statusDiv.textContent = `✅ ${result.message}`;
statusDiv.style.color = 'green';
// Display metadata if available
if (result.metadata) {
metadataContent.textContent = JSON.stringify(result.metadata, null, 2);
metadataDiv.style.display = 'block';
}
// Clear file input
fileInput.value = '';
showNotification('Custom profile picture applied successfully!');
} else {
throw new Error(result.message || 'Failed to apply custom profile picture');
}
} catch (error) {
console.error('Failed to upload custom profile picture:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
showNotification(error.message || 'Failed to upload custom profile picture', 'error');
}
}
async function restoreFallbackPfp() {
const statusDiv = document.getElementById('pfp-status');
const metadataDiv = document.getElementById('pfp-metadata');
if (!confirm('Are you sure you want to restore the original fallback avatar?')) {
return;
}
statusDiv.textContent = '⏳ Restoring original avatar...';
statusDiv.style.color = '#61dafb';
try {
const response = await fetch('/profile-picture/restore-fallback', {
method: 'POST'
});
const result = await response.json();
if (response.ok & & result.status === 'ok') {
statusDiv.textContent = `✅ ${result.message}`;
statusDiv.style.color = 'green';
metadataDiv.style.display = 'none';
showNotification('Original avatar restored successfully!');
} else {
throw new Error(result.message || 'Failed to restore fallback avatar');
}
} catch (error) {
console.error('Failed to restore fallback avatar:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
showNotification(error.message || 'Failed to restore fallback avatar', 'error');
}
}
// Role Color Management
async function setCustomRoleColor() {
const statusDiv = document.getElementById('role-color-status');
const hexInput = document.getElementById('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('role-color-status');
statusDiv.textContent = '⏳ Resetting to fallback color...';
statusDiv.style.color = '#61dafb';
try {
const response = await fetch('/role-color/reset-fallback', {
method: 'POST'
});
const result = await response.json();
if (response.ok & & result.status === 'ok') {
statusDiv.textContent = `✅ ${result.message}`;
statusDiv.style.color = 'green';
// Update the input to show fallback color
document.getElementById('role-color-hex').value = '#86cecb';
showNotification('Role color reset to fallback #86cecb');
} else {
throw new Error(result.message || 'Failed to reset role color');
}
} catch (error) {
console.error('Failed to reset role color:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
showNotification(error.message || 'Failed to reset role color', 'error');
}
}
// Toggle functions for custom prompt and manual message target selection
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 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, requestData, response;
if (targetType === 'dm') {
// DM target
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`;
requestData = { prompt: prompt };
response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
} else {
// Server target
const selectedServer = document.getElementById('custom-prompt-server-select').value;
endpoint = '/autonomous/custom';
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
endpoint += `?guild_id=${parseInt(selectedServer)}`;
}
requestData = { prompt: prompt };
response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
}
const result = await response.json();
if (response.ok) {
showNotification(result.message || 'Custom prompt sent successfully');
document.getElementById('customPrompt').value = '';
document.getElementById('customPromptAttachment').value = ''; // Clear file input
if (targetType === 'dm') {
document.getElementById('custom-prompt-user-id').value = '';
}
document.getElementById('customStatus').textContent = '✅ Custom prompt sent successfully!';
document.getElementById('customStatus').style.color = 'green';
} else {
throw new Error(result.message || 'Failed to send custom prompt');
}
} catch (error) {
console.error('Failed to send custom prompt:', error);
showNotification(error.message || 'Failed to send custom prompt', 'error');
document.getElementById('customStatus').textContent = '❌ Failed to send custom prompt';
document.getElementById('customStatus').style.color = 'red';
}
}
// 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;
try {
let endpoint = '/bedtime';
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
endpoint += `?guild_id=${parseInt(selectedServer)}`;
}
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (response.ok) {
showNotification(result.message || 'Bedtime reminder sent successfully');
} else {
throw new Error(result.message || 'Failed to send bedtime reminder');
}
} catch (error) {
console.error('Failed to send bedtime reminder:', error);
showNotification(error.message || '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;
if (!message) {
showNotification('Please enter a message', '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 = '/manual/send';
}
try {
const formData = new FormData();
formData.append('message', message);
if (targetType === 'dm') {
// For DM, the user_id is in the URL path
if (files.length > 0) {
for (let i = 0; i < files.length ; i + + ) {
formData.append('files', files[i]);
}
}
} else {
// For channel, append channel_id to form data
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 = ''; // Clear file input
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';
}
}
// Add Reaction to Message
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');
// Validate inputs
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';
// Clear the form
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';
}
}
// Image Generation Functions
async function checkImageSystemStatus() {
try {
const statusDisplay = document.getElementById('image-status-display');
statusDisplay.innerHTML = '🔄 Checking system status...';
const response = await fetch('/image/status');
const result = await response.json();
if (response.ok) {
const workflowStatus = result.workflow_template_exists ? '✅ Found' : '❌ Missing';
const comfyuiStatus = result.comfyui_running ? '✅ Running' : '❌ Not running';
statusDisplay.innerHTML = `
< strong > System Status:< / strong >
• Workflow Template (Miku_BasicWorkflow.json): ${workflowStatus}
• ComfyUI Server: ${comfyuiStatus}
${result.comfyui_running ? `• Detected ComfyUI URL: ${result.comfyui_url}` : ''}
< strong > Overall Status:< / strong > ${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' : ''}`;
} else {
statusDisplay.innerHTML = `❌ Error checking status: ${result.message}`;
}
} 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 response = await fetch('/image/test-detection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message })
});
const result = await response.json();
if (response.ok) {
const detectionIcon = result.is_image_request ? '✅' : '❌';
const detectionText = result.is_image_request ? 'WILL trigger image generation' : 'will NOT trigger image generation';
resultsDiv.innerHTML = `
< strong > Detection Result:< / strong > ${detectionIcon} This message ${detectionText}
${result.is_image_request ? `< br > < strong > Extracted Prompt:< / strong > "${result.extracted_prompt}"` : ''}
< br > < strong > Original Message:< / strong > "${result.original_message}"`;
resultsDiv.style.color = result.is_image_request ? '#4CAF50' : '#ff9800';
} else {
resultsDiv.innerHTML = `❌ Detection test failed: ${result.message}`;
resultsDiv.style.color = 'red';
}
} 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');
if (!prompt) {
statusDiv.innerHTML = '❌ Please enter an image prompt';
statusDiv.style.color = 'red';
return;
}
try {
statusDiv.innerHTML = '🎨 Generating image... This may take a few minutes.';
statusDiv.style.color = '#4CAF50';
const response = await fetch('/image/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: prompt })
});
const result = await response.json();
if (response.ok) {
statusDiv.innerHTML = `✅ Image generated successfully! Path: ${result.image_path}`;
statusDiv.style.color = '#4CAF50';
document.getElementById('manual-image-prompt').value = '';
} else {
statusDiv.innerHTML = `❌ Failed to generate image: ${result.message}`;
statusDiv.style.color = 'red';
}
} catch (error) {
console.error('Failed to generate image:', error);
statusDiv.innerHTML = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
}
}
// Status and Info
async function loadStatus() {
try {
const result = await apiCall('/status');
const statusDiv = document.getElementById('status');
let serverMoodsHtml = '';
if (result.server_moods) {
serverMoodsHtml = '< div style = "margin-top: 0.5rem;" > < strong > Server Moods:< / strong > < br > ';
for (const [guildId, mood] of Object.entries(result.server_moods)) {
const server = servers.find(s => s.guild_id == guildId);
const serverName = server ? server.guild_name : `Server ${guildId}`;
serverMoodsHtml += `• ${serverName}: ${mood} ${MOOD_EMOJIS[mood] || ''}< br > `;
}
serverMoodsHtml += '< / div > ';
}
statusDiv.innerHTML = `
< div > < strong > Status:< / strong > ${result.status}< / div >
< div > < strong > DM Mood:< / strong > ${result.mood}< / div >
< div > < strong > Servers:< / strong > ${result.servers}< / div >
< div > < strong > Active Schedulers:< / strong > ${result.active_schedulers}< / div >
< div style = "margin-top: 0.5rem; padding: 0.5rem; background: #2a2a2a; border-radius: 4px; font-size: 0.9rem;" >
< strong > 💬 DM Support:< / strong > Users can message Miku directly in DMs. She responds to every DM message using the DM mood (auto-rotating every 2 hours).
< / div >
${serverMoodsHtml}
`;
} catch (error) {
console.error('Failed to load status:', error);
}
}
async function loadLastPrompt() {
try {
const result = await apiCall('/prompt');
document.getElementById('last-prompt').textContent = result.prompt;
} catch (error) {
console.error('Failed to load last prompt:', error);
}
}
async function loadLogs() {
try {
const result = await apiCall('/logs');
document.getElementById('logs-content').textContent = result;
} catch (error) {
console.error('Failed to load logs:', error);
}
}
function toggleCustomPrompt() {
const customPromptSection = document.getElementById('custom-prompt-section');
if (customPromptSection) {
customPromptSection.style.display = customPromptSection.style.display === 'none' ? 'block' : 'none';
}
}
// DM Logs Functions
async function loadDMUsers() {
try {
const response = await fetch('/dms/users');
const result = await response.json();
if (response.ok) {
displayDMUsers(result.users);
} else {
throw new Error(result.message || 'Failed to load DM users');
}
} catch (error) {
console.error('Failed to load DM users:', error);
showNotification(error.message || 'Failed to load DM users', 'error');
}
}
function displayDMUsers(users) {
const container = document.getElementById('dm-users-list');
if (!users || users.length === 0) {
container.innerHTML = '< p > No DM conversations found.< / p > ';
return;
}
let html = '< div class = "dm-users-grid" > ';
users.forEach(user => {
console.log(`👤 Processing user: ${user.username} (ID: ${user.user_id})`);
const lastMessage = user.last_message ?
`Last: ${user.last_message.content}` :
'No messages yet';
const lastTime = user.last_message ?
new Date(user.last_message.timestamp).toLocaleString() :
'Never';
html += `
< div class = "dm-user-card" >
< h4 > 👤 ${user.username}< / h4 >
< p > < strong > ID:< / strong > ${user.user_id}< / p >
< p > < strong > Total Messages:< / strong > ${user.total_messages}< / p >
< p > < strong > User Messages:< / strong > ${user.user_messages}< / p >
< p > < strong > Bot Messages:< / strong > ${user.bot_messages}< / p >
< p > < strong > Last Activity:< / strong > ${lastTime}< / p >
< p > < strong > Last Message:< / strong > ${lastMessage}< / p >
< div class = "dm-user-actions" >
< button class = "view-chat-btn" data-user-id = "${user.user_id}" > 💬 View Chat< / button >
< button class = "analyze-user-btn" data-user-id = "${user.user_id}" data-username = "${user.username}" style = "background: #9c27b0;" > 📊 Analyze< / button >
< button class = "export-dms-btn" data-user-id = "${user.user_id}" > 📤 Export< / button >
< button class = "block-user-btn" data-user-id = "${user.user_id}" data-username = "${user.username}" style = "background: #ff9800;" > 🚫 Block< / button >
< button class = "delete-all-dms-btn" data-user-id = "${user.user_id}" data-username = "${user.username}" style = "background: #f44336;" > 🗑️ Delete All< / button >
< button class = "delete-user-completely-btn" data-user-id = "${user.user_id}" data-username = "${user.username}" style = "background: #d32f2f;" > 💀 Delete User< / button >
< / div >
< / div >
`;
});
html += '< / div > ';
container.innerHTML = html;
// Add event listeners after HTML is inserted
addDMUserEventListeners();
}
function addDMUserEventListeners() {
// Add event listeners for view chat buttons
document.querySelectorAll('.view-chat-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 View chat clicked for user ID: ${userId} (type: ${typeof userId})`);
viewUserConversations(userId);
});
});
// Add event listeners for export buttons
document.querySelectorAll('.export-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 Export clicked for user ID: ${userId} (type: ${typeof userId})`);
exportUserDMs(userId);
});
});
// Add event listeners for analyze buttons
document.querySelectorAll('.analyze-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Analyze clicked for user ID: ${userId} (type: ${typeof userId})`);
analyzeUserInteraction(userId, username);
});
});
// Add event listeners for block buttons
document.querySelectorAll('.block-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Block clicked for user ID: ${userId} (type: ${typeof userId})`);
blockUser(userId, username);
});
});
// Add event listeners for delete all DMs buttons
document.querySelectorAll('.delete-all-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete all DMs clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteAllUserConversations(userId, username);
});
});
// Add event listeners for delete user completely buttons
document.querySelectorAll('.delete-user-completely-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete user completely clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteUserCompletely(userId, username);
});
});
}
async function viewUserConversations(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
console.log(`🔍 Loading conversations for user ${userIdStr} (type: ${typeof userIdStr})`);
console.log(`🔍 Original userId: ${userId} (type: ${typeof userId})`);
console.log(`🔍 userIdStr: ${userIdStr} (type: ${typeof userIdStr})`);
const response = await fetch(`/dms/users/${userIdStr}/conversations?limit=100`);
const result = await response.json();
console.log('📡 API Response:', result);
console.log('📡 API URL called:', `/dms/users/${userIdStr}/conversations?limit=100`);
if (response.ok) {
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();
}
} else {
throw new Error(result.message || 'Failed to load conversations');
}
} catch (error) {
console.error('Failed to load user conversations:', error);
showNotification(error.message || 'Failed to load conversations', 'error');
}
}
function displayUserConversations(userId, conversations) {
console.log(`🎨 Displaying conversations for user ${userId}:`, conversations);
// Create a modal or expand the user card to show conversations
const container = document.getElementById('dm-users-list');
let html = `
< div class = "conversation-view" >
< button onclick = "loadDMUsers()" style = "margin-bottom: 1rem;" > ← Back to DM Users< / button >
< h4 > 💬 Conversations with User ${userId}< / h4 >
< div class = "conversations-list" >
`;
if (!conversations || conversations.length === 0) {
html += '< p > No conversations found for this user.< / p > ';
} else {
conversations.forEach((msg, index) => {
console.log(`📝 Processing message ${index}:`, msg);
const timestamp = new Date(msg.timestamp).toLocaleString();
const sender = msg.is_bot_message ? '🤖 Miku' : '👤 User';
const content = msg.content || '[No text content]';
const messageId = msg.message_id || msg.timestamp; // Use message_id or timestamp as identifier
const escapedContent = content.replace(/'/g, "\\'").replace(/"/g, '\\"');
// Debug: Log message details
console.log(`📝 Message ${index}: id=${messageId}, is_bot=${msg.is_bot_message}, content="${content.substring(0, 30)}..."`);
// Only show delete button for bot messages (Miku can only delete her own messages)
const deleteButton = msg.is_bot_message ?
`< button class = "delete-message-btn" onclick = "deleteConversation('${userId}', '${messageId}', '${escapedContent}')"
style="background: #f44336; color: white; border: none; padding: 2px 6px; font-size: 12px; border-radius: 3px; margin-left: 10px;"
title="Delete this Miku message (ID: ${messageId})">
🗑️ Delete
< / button > ` : '';
html += `
< div class = "conversation-message ${msg.is_bot_message ? 'bot-message' : 'user-message'}" >
< div class = "message-header" >
< span class = "sender" > ${sender}< / span >
< span class = "timestamp" > ${timestamp}< / span >
${deleteButton}
< / div >
< div class = "message-content" > ${content}< / div >
${msg.attachments & & msg.attachments.length > 0 ? `
< div class = "message-attachments" >
< strong > 📎 Attachments:< / strong >
${msg.attachments.map(att => `
< div class = "attachment" >
- ${att.filename} (${att.size} bytes)
< a href = "${att.url}" target = "_blank" > 🔗 View< / a >
< / div >
`).join('')}
< / div >
` : ''}
${msg.reactions & & msg.reactions.length > 0 ? `
< div class = "message-reactions" >
${msg.reactions.map(reaction => {
const reactionTime = new Date(reaction.added_at).toLocaleString();
const reactorType = reaction.is_bot ? 'bot-reaction' : 'user-reaction';
const reactorLabel = reaction.is_bot ? '🤖 Miku' : `👤 ${reaction.reactor_name}`;
return `
< div class = "reaction-item" title = "${reactorLabel} reacted at ${reactionTime}" >
< span class = "reaction-emoji" > ${reaction.emoji}< / span >
< span class = "reaction-by ${reactorType}" > ${reactorLabel}< / span >
< / div >
`;
}).join('')}
< / div >
` : ''}
< / div >
`;
});
}
html += `
< / div >
< / div >
`;
console.log('🎨 Generated HTML:', html);
container.innerHTML = html;
}
async function exportUserDMs(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
const response = await fetch(`/dms/users/${userIdStr}/export?format=txt`);
const result = await response.json();
if (response.ok) {
showNotification(`DM export completed for user ${userIdStr}`);
// You could trigger a download here if the file is accessible
} else {
throw new Error(result.message || 'Failed to export DMs');
}
} catch (error) {
console.error('Failed to export user DMs:', error);
showNotification(error.message || 'Failed to export 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 {
const response = await fetch(`/dms/users/${userIdStr}`, { method: 'DELETE' });
const result = await response.json();
if (response.ok) {
showNotification(`Deleted DM logs for user ${userIdStr}`);
loadDMUsers(); // Refresh the list
} else {
throw new Error(result.message || 'Failed to delete DM logs');
}
} catch (error) {
console.error('Failed to delete user DMs:', error);
showNotification(error.message || 'Failed to delete DM logs', '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 {
const response = await fetch(`/dms/users/${userIdStr}/block`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
showNotification(`${username} has been blocked from sending DMs`);
loadDMUsers(); // Refresh the list
} else {
throw new Error(result.message || 'Failed to block user');
}
} catch (error) {
console.error('Failed to block user:', error);
showNotification(error.message || 'Failed to block user', 'error');
}
}
async function unblockUser(userId, username) {
const userIdStr = String(userId);
try {
const response = await fetch(`/dms/users/${userIdStr}/unblock`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
showNotification(`${username} has been unblocked`);
loadBlockedUsers(); // Refresh blocked users list
} else {
throw new Error(result.message || 'Failed to unblock user');
}
} catch (error) {
console.error('Failed to unblock user:', error);
showNotification(error.message || '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 {
const response = await fetch(`/dms/users/${userIdStr}/conversations/delete-all`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
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);
} else {
throw new Error(result.message || 'Failed to delete conversations');
}
} catch (error) {
console.error('Failed to delete conversations:', error);
showNotification(error.message || '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 {
const response = await fetch(`/dms/users/${userIdStr}/delete-completely`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
showNotification(`${username} has been completely deleted from the system`);
loadDMUsers(); // Refresh the list
} else {
throw new Error(result.message || 'Failed to delete user completely');
}
} catch (error) {
console.error('Failed to delete user completely:', error);
showNotification(error.message || '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 {
const response = await fetch(`/dms/users/${userIdStr}/conversations/${conversationId}/delete`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
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);
} else {
throw new Error(result.message || 'Failed to delete message');
}
} catch (error) {
console.error('Failed to delete conversation:', error);
showNotification(error.message || 'Failed to delete message', '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 response = await fetch(`/dms/users/${userIdStr}/analyze`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
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)`);
}
} else {
throw new Error(result.message || 'Failed to analyze user');
}
} catch (error) {
console.error('Failed to analyze user:', error);
showNotification(error.message || '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');
const response = await fetch('/dms/analysis/run', { method: 'POST' });
const result = await response.json();
if (response.ok) {
showNotification('✅ DM analysis completed! Check bot owner\'s DMs for any reports.');
} else {
throw new Error(result.message || 'Failed to run analysis');
}
} catch (error) {
console.error('Failed to run DM analysis:', error);
showNotification(error.message || 'Failed to run DM analysis', 'error');
}
}
async function viewAnalysisReports() {
try {
showNotification('Loading analysis reports...', 'info');
const response = await fetch('/dms/analysis/reports?limit=50');
const result = await response.json();
if (response.ok) {
displayAnalysisReports(result.reports);
} else {
throw new Error(result.message || 'Failed to load reports');
}
} catch (error) {
console.error('Failed to load reports:', error);
showNotification(error.message || 'Failed to load reports', 'error');
}
}
function displayAnalysisReports(reports) {
const container = document.getElementById('dm-users-list');
if (!reports || reports.length === 0) {
container.innerHTML = `
< div style = "text-align: center; padding: 2rem;" >
< p > No analysis reports found yet.< / p >
< button onclick = "loadDMUsers()" style = "margin-top: 1rem;" > ← Back to DM Users< / button >
< / div >
`;
return;
}
let html = `
< div style = "margin-bottom: 1rem;" >
< button onclick = "loadDMUsers()" > ← Back to DM Users< / button >
< span style = "margin-left: 1rem; color: #aaa;" > ${reports.length} reports found< / span >
< / div >
< div style = "display: grid; gap: 1rem;" >
`;
reports.forEach(report => {
const sentimentColor =
report.sentiment_score >= 5 ? '#4caf50' :
report.sentiment_score < = -3 ? '#f44336' :
'#2196f3';
const sentimentEmoji =
report.sentiment_score >= 5 ? '😊' :
report.sentiment_score < = -3 ? '😢' :
'😐';
const timestamp = new Date(report.analyzed_at).toLocaleString();
html += `
< div style = "background: #2a2a2a; border-left: 4px solid ${sentimentColor}; padding: 1rem; border-radius: 4px;" >
< div style = "display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;" >
< div >
< h4 style = "margin: 0 0 0.25rem 0;" > ${sentimentEmoji} ${report.username}< / h4 >
< p style = "margin: 0; font-size: 0.85rem; color: #aaa;" > User ID: ${report.user_id}< / p >
< / div >
< div style = "text-align: right;" >
< div style = "font-size: 1.2rem; font-weight: bold; color: ${sentimentColor};" >
${report.sentiment_score > 0 ? '+' : ''}${report.sentiment_score}/10
< / div >
< div style = "font-size: 0.75rem; color: #aaa; text-transform: uppercase;" >
${report.overall_sentiment}
< / div >
< / div >
< / div >
< div style = "margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;" >
< strong > Miku's Feelings:< / strong >
< p style = "margin: 0.5rem 0 0 0; font-style: italic;" > "${report.your_feelings}"< / p >
< / div >
${report.notable_moment ? `
< div style = "margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;" >
< strong > Notable Moment:< / strong >
< p style = "margin: 0.5rem 0 0 0; font-style: italic;" > "${report.notable_moment}"< / p >
< / div >
` : ''}
${report.key_behaviors & & report.key_behaviors.length > 0 ? `
< div style = "margin: 0.75rem 0;" >
< strong > Key Behaviors:< / strong >
< ul style = "margin: 0.5rem 0 0 0; padding-left: 1.5rem;" >
${report.key_behaviors.slice(0, 5).map(b => `< li > ${b}< / li > `).join('')}
< / ul >
< / div >
` : ''}
< div style = "margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #444; font-size: 0.8rem; color: #aaa;" >
< span > 📅 ${timestamp}< / span >
< span style = "margin-left: 1rem;" > 💬 ${report.message_count} messages analyzed< / span >
< span style = "margin-left: 1rem;" > 📄 ${report.filename}< / span >
< / div >
< / div >
`;
});
html += '< / div > ';
container.innerHTML = html;
}
async function loadBlockedUsers() {
try {
const response = await fetch('/dms/blocked-users');
const result = await response.json();
if (response.ok) {
// 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);
} else {
throw new Error(result.message || 'Failed to load blocked users');
}
} catch (error) {
console.error('Failed to load blocked users:', error);
showNotification(error.message || 'Failed to load blocked users', 'error');
}
}
function hideBlockedUsers() {
// Show DM users list and hide blocked users section
document.getElementById('dm-users-list').style.display = 'block';
document.getElementById('blocked-users-section').style.display = 'none';
loadDMUsers(); // Refresh DM users
}
function displayBlockedUsers(blockedUsers) {
const container = document.getElementById('blocked-users-list');
if (!blockedUsers || blockedUsers.length === 0) {
container.innerHTML = '< p > No blocked users.< / p > ';
return;
}
let html = '< div class = "blocked-users-grid" > ';
blockedUsers.forEach(user => {
html += `
< div class = "blocked-user-card" >
< h4 > 🚫 ${user.username}< / h4 >
< p > < strong > ID:< / strong > ${user.user_id}< / p >
< p > < strong > Blocked:< / strong > ${new Date(user.blocked_at).toLocaleString()}< / p >
< p > < strong > Blocked by:< / strong > ${user.blocked_by}< / p >
< div class = "blocked-user-actions" >
< button onclick = "unblockUser('${user.user_id}', '${user.username}')" style = "background: #4caf50;" > ✅ Unblock< / button >
< / div >
< / div >
`;
});
html += '< / div > ';
container.innerHTML = html;
}
async function exportAllDMs() {
try {
const response = await fetch('/dms/users');
const result = await response.json();
if (response.ok & & result.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`);
} else {
throw new Error(result.message || 'Failed to load DM users for export');
}
} catch (error) {
console.error('Failed to export all DMs:', error);
showNotification(error.message || 'Failed to export all DMs', 'error');
}
}
// ========== Autonomous Stats Functions ==========
async function loadAutonomousStats() {
const serverSelect = document.getElementById('autonomous-server-select');
const selectedGuildId = serverSelect.value;
if (!selectedGuildId) {
document.getElementById('autonomous-stats-display').innerHTML = '< p style = "color: #aaa;" > Please select a server to view autonomous stats.< / p > ';
return;
}
try {
const response = await fetch('/autonomous/stats');
const data = await response.json();
if (!data.servers || !data.servers[selectedGuildId]) {
document.getElementById('autonomous-stats-display').innerHTML = '< p style = "color: #ff5555;" > Server not found or not initialized.< / p > ';
return;
}
const serverData = data.servers[selectedGuildId];
displayAutonomousStats(serverData);
} catch (error) {
console.error('Failed to load autonomous stats:', error);
showNotification('Failed to load autonomous stats', 'error');
}
}
function displayAutonomousStats(data) {
const container = document.getElementById('autonomous-stats-display');
if (!data.context) {
container.innerHTML = `
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px;" >
< h4 style = "color: #61dafb; margin-top: 0;" > ⚠️ Context Not Initialized< / h4 >
< p > This server hasn't had any activity yet. Context tracking will begin once messages are sent.< / p >
< div style = "margin-top: 1rem; padding: 1rem; background: #1e1e1e; border-radius: 4px;" >
< strong > Current Mood:< / strong > ${data.mood} ${MOOD_EMOJIS[data.mood] || ''}< br >
< strong > Energy:< / strong > ${data.mood_profile.energy}< br >
< strong > Sociability:< / strong > ${data.mood_profile.sociability}< br >
< strong > Impulsiveness:< / strong > ${data.mood_profile.impulsiveness}
< / div >
< / div >
`;
return;
}
const ctx = data.context;
const profile = data.mood_profile;
// Calculate time displays
const lastActionMin = Math.floor(ctx.time_since_last_action / 60);
const lastInteractionMin = Math.floor(ctx.time_since_last_interaction / 60);
container.innerHTML = `
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;" >
< h4 style = "color: #61dafb; margin-top: 0;" > 🎭 Mood & Personality Profile< / h4 >
< div style = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;" > Current Mood< / div >
< div style = "font-size: 1.5rem; font-weight: bold;" > ${data.mood} ${MOOD_EMOJIS[data.mood] || ''}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;" > Energy Level< / div >
< div style = "font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.energy)}" > ${(profile.energy * 100).toFixed(0)}%< / div >
< div style = "width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;" >
< div style = "width: ${profile.energy * 100}%; height: 100%; background: ${getStatColor(profile.energy)}; border-radius: 3px;" > < / div >
< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;" > Sociability< / div >
< div style = "font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.sociability)}" > ${(profile.sociability * 100).toFixed(0)}%< / div >
< div style = "width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;" >
< div style = "width: ${profile.sociability * 100}%; height: 100%; background: ${getStatColor(profile.sociability)}; border-radius: 3px;" > < / div >
< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa; margin-bottom: 0.3rem;" > Impulsiveness< / div >
< div style = "font-size: 1.5rem; font-weight: bold; color: ${getStatColor(profile.impulsiveness)}" > ${(profile.impulsiveness * 100).toFixed(0)}%< / div >
< div style = "width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 0.5rem;" >
< div style = "width: ${profile.impulsiveness * 100}%; height: 100%; background: ${getStatColor(profile.impulsiveness)}; border-radius: 3px;" > < / div >
< / div >
< / div >
< / div >
< / div >
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;" >
< h4 style = "color: #61dafb; margin-top: 0;" > 📈 Activity Metrics< / h4 >
< div style = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Messages (Last 5 min) < span style = "color: #666;" > ⚡ ephemeral< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #4caf50;" > ${ctx.messages_last_5min}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Messages (Last Hour) < span style = "color: #666;" > ⚡ ephemeral< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #2196f3;" > ${ctx.messages_last_hour}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Conversation Momentum < span style = "color: #4caf50;" > 💾 saved< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: ${getMomentumColor(ctx.conversation_momentum)}" > ${(ctx.conversation_momentum * 100).toFixed(0)}%< / div >
< div style = "font-size: 0.75rem; color: #888; margin-top: 0.3rem;" > Decays with downtime (half-life: 10min)< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Unique Users Active < span style = "color: #666;" > ⚡ ephemeral< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #ff9800;" > ${ctx.unique_users_active}< / div >
< / div >
< / div >
< / div >
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;" >
< h4 style = "color: #61dafb; margin-top: 0;" > 👥 User Events< / h4 >
< div style = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Users Joined Recently< / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #4caf50;" > ${ctx.users_joined_recently}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Status Changes< / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #2196f3;" > ${ctx.users_status_changed}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Active Activities< / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #9c27b0;" > ${ctx.users_started_activity.length}< / div >
${ctx.users_started_activity.length > 0 ? `< div style = "font-size: 0.8rem; margin-top: 0.5rem; color: #aaa;" > ${ctx.users_started_activity.join(', ')}< / div > ` : ''}
< / div >
< / div >
< / div >
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;" >
< h4 style = "color: #61dafb; margin-top: 0;" > ⏱️ Timing & Context< / h4 >
< div style = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;" >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Time Since Last Action < span style = "color: #4caf50;" > 💾 saved< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #ff5722;" > ${lastActionMin} min< / div >
< div style = "font-size: 0.8rem; color: #888;" > ${ctx.time_since_last_action.toFixed(1)}s< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Time Since Last Interaction < span style = "color: #4caf50;" > 💾 saved< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #ff9800;" > ${lastInteractionMin} min< / div >
< div style = "font-size: 0.8rem; color: #888;" > ${ctx.time_since_last_interaction.toFixed(1)}s< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Messages Since Last Appearance < span style = "color: #4caf50;" > 💾 saved< / span > < / div >
< div style = "font-size: 1.8rem; font-weight: bold; color: #2196f3;" > ${ctx.messages_since_last_appearance}< / div >
< / div >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa;" > Current Time Context < span style = "color: #666;" > ⚡ ephemeral< / span > < / div >
< div style = "font-size: 1.5rem; font-weight: bold; color: #61dafb;" > ${ctx.hour_of_day}:00< / div >
< div style = "font-size: 0.8rem; color: #888;" > ${ctx.is_weekend ? '📅 Weekend' : '📆 Weekday'}< / div >
< / div >
< / div >
< / div >
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px;" >
< h4 style = "color: #61dafb; margin-top: 0;" > 🧠 Base Energy Level< / h4 >
< div style = "background: #1e1e1e; padding: 1rem; border-radius: 4px;" >
< div style = "font-size: 0.9rem; color: #aaa; margin-bottom: 0.5rem;" > From current mood personality< / div >
< div style = "font-size: 2rem; font-weight: bold; color: ${getStatColor(ctx.mood_energy_level)}" > ${(ctx.mood_energy_level * 100).toFixed(0)}%< / div >
< div style = "width: 100%; height: 10px; background: #333; border-radius: 5px; margin-top: 0.5rem;" >
< div style = "width: ${ctx.mood_energy_level * 100}%; height: 100%; background: ${getStatColor(ctx.mood_energy_level)}; border-radius: 5px;" > < / div >
< / div >
< div style = "font-size: 0.85rem; color: #888; margin-top: 0.5rem;" >
💡 Combined with activity metrics to determine action likelihood.< br >
📝 High energy = shorter wait times, higher action chance.< br >
💾 < strong > Persisted across restarts< / strong >
< / div >
< / div >
< / div >
`;
}
function getStatColor(value) {
if (value >= 0.8) return '#4caf50'; // Green - high
if (value >= 0.6) return '#8bc34a'; // Light green
if (value >= 0.4) return '#ffc107'; // Yellow - medium
if (value >= 0.2) return '#ff9800'; // Orange
return '#f44336'; // Red - low
}
function getMomentumColor(value) {
if (value >= 0.7) return '#4caf50'; // High activity
if (value >= 0.4) return '#2196f3'; // Medium activity
return '#9e9e9e'; // Low activity
}
// Populate autonomous server dropdown when servers load
function populateAutonomousServerDropdown() {
const select = document.getElementById('autonomous-server-select');
if (!select) return;
const currentValue = select.value;
select.innerHTML = '< option value = "" > -- Select a server --< / option > ';
servers.forEach(server => {
const option = document.createElement('option');
option.value = server.guild_id;
option.textContent = `${server.guild_name} (${server.guild_id})`;
select.appendChild(option);
});
// Restore previous selection if it still exists
if (currentValue & & servers.some(s => String(s.guild_id) === currentValue)) {
select.value = currentValue;
}
}
< / script >
< / body >
< / html >