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