Files
miku-discord/bot/static/index.html
koko210Serve f27d7f4afe Fix JavaScript integer precision loss for Discord IDs in web UI
- Removed parseInt() calls that were causing Discord snowflake IDs to lose precision
- Discord IDs exceed JavaScript's safe integer limit (2^53-1), causing corruption
- Fixed sendBedtime(), triggerAutonomous(), custom prompt, and addServer() functions
- Keep guild_id and channel_id values as strings throughout the frontend
- Backend FastAPI correctly parses string IDs to Python integers without precision loss
- Resolves issue where wrong server ID was sent (e.g., 1429954521576116200 instead of 1429954521576116337)
2025-12-08 01:14:29 +02:00

3250 lines
118 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<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>
</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>';
console.log('🎭 Populating server dropdowns with', servers.length, 'servers');
// Add server options
servers.forEach(server => {
console.log(`🎭 Adding server to dropdown: ${server.guild_name} (guild_id: ${server.guild_id}, type: ${typeof server.guild_id})`);
const option = document.createElement('option');
option.value = server.guild_id;
option.textContent = server.guild_name;
serverSelect.appendChild(option.cloneNode(true));
manualServerSelect.appendChild(option);
customPromptServerSelect.appendChild(option.cloneNode(true));
});
// Debug: Check what's actually in the manual-server-select dropdown
console.log('🎭 manual-server-select options:');
Array.from(manualServerSelect.options).forEach((opt, idx) => {
console.log(` [${idx}] value="${opt.value}" text="${opt.textContent}"`);
});
// Populate autonomous stats dropdown
populateAutonomousServerDropdown();
}
// Figurine subscribers UI functions (must be global for onclick handlers)
async function refreshFigurineSubscribers() {
try {
console.log('🔄 Figurines: Fetching subscribers...');
const 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() {
// Don't use parseInt() for Discord IDs - they're too large for JS integers
const guildId = document.getElementById('new-guild-id').value.trim();
const guildName = document.getElementById('new-guild-name').value;
const autonomousChannelId = document.getElementById('new-autonomous-channel-id').value.trim();
const autonomousChannelName = document.getElementById('new-autonomous-channel-name').value;
const bedtimeChannelIds = document.getElementById('new-bedtime-channel-ids').value
.split(',').map(id => id.trim()).filter(id => id.length > 0);
const enabledFeatures = [];
if (document.getElementById('feature-autonomous').checked) enabledFeatures.push('autonomous');
if (document.getElementById('feature-bedtime').checked) enabledFeatures.push('bedtime');
if (document.getElementById('feature-monday-video').checked) enabledFeatures.push('monday_video');
if (!guildId || !guildName || !autonomousChannelId || !autonomousChannelName) {
showNotification('Please fill in all required fields', 'error');
return;
}
try {
await apiCall('/servers', 'POST', {
guild_id: guildId,
guild_name: guildName,
autonomous_channel_id: autonomousChannelId,
autonomous_channel_name: autonomousChannelName,
bedtime_channel_ids: bedtimeChannelIds.length > 0 ? bedtimeChannelIds : [autonomousChannelId],
enabled_features: enabledFeatures
});
showNotification('Server added successfully');
loadServers();
// Clear form
document.getElementById('new-guild-id').value = '';
document.getElementById('new-guild-name').value = '';
document.getElementById('new-autonomous-channel-id').value = '';
document.getElementById('new-autonomous-channel-name').value = '';
document.getElementById('new-bedtime-channel-ids').value = '';
} catch (error) {
console.error('Failed to add server:', error);
}
}
async function removeServer(guildId) {
if (!confirm('Are you sure you want to remove this server?')) {
return;
}
try {
await apiCall(`/servers/${guildId}`, 'DELETE');
showNotification('Server removed successfully');
loadServers();
} catch (error) {
console.error('Failed to remove server:', error);
}
}
async function editServer(guildId) {
// For now, just show a notification - you can implement a full edit form later
showNotification('Edit functionality coming soon!');
}
async function repairConfig() {
if (!confirm('This will attempt to repair corrupted server configurations. Are you sure?')) {
return;
}
try {
await apiCall('/servers/repair', 'POST');
showNotification('Configuration repair initiated. Please refresh the page to see updated server list.');
loadServers(); // Reload servers to reflect potential changes
} catch (error) {
console.error('Failed to repair config:', error);
showNotification(error.message || 'Failed to repair configuration', 'error');
}
}
// Populate mood dropdowns with available moods
async function populateMoodDropdowns() {
try {
console.log('🎭 Loading available moods...');
const 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') {
// Don't use parseInt() - Discord IDs are too large for JS integers
endpoint += `?guild_id=${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') {
// Don't use parseInt() - Discord IDs are too large for JS integers
endpoint += `?guild_id=${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;
// Debug logging
console.log('🛏️ sendBedtime() called');
console.log('🛏️ Selected server value:', selectedServer);
console.log('🛏️ Selected server type:', typeof selectedServer);
try {
let endpoint = '/bedtime';
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
// IMPORTANT: Don't use parseInt() - it causes precision loss!
// Keep as string since Discord IDs are too large for JS integers
console.log('🛏️ Using guild_id (as string):', selectedServer);
endpoint += `?guild_id=${selectedServer}`;
}
console.log('🛏️ Final endpoint:', endpoint);
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>