Files
miku-discord/bot/static/index.html
koko210Serve 5bdd907730 refactor: standardize raw fetch() calls to use apiCall() wrapper
Convert 47 raw fetch+response.json+error-handling patterns to use the
centralized apiCall() utility. The 11 remaining raw fetch() calls are
FormData uploads or SSE streaming that require direct fetch access.
2026-03-01 00:14:08 +02:00

5739 lines
217 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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;
position: relative;
}
#logs-content {
white-space: pre-wrap;
word-break: break-word;
}
.log-line { line-height: 1.4; }
.log-line.log-error { color: #ff6b6b; }
.log-line.log-warning { color: #ffd93d; }
.log-line.log-info { color: #0f0; }
.log-line.log-debug { color: #888; }
.logs-paused-indicator {
position: sticky;
top: 0;
background: rgba(50, 50, 0, 0.9);
color: #ffd93d;
text-align: center;
padding: 0.25rem;
font-size: 0.75rem;
cursor: pointer;
z-index: 10;
display: none;
}
select, button, input {
margin: 0.4rem 0.5rem 0.4rem 0;
padding: 0.4rem;
background: #333;
color: #fff;
border: 1px solid #555;
}
.section {
margin-bottom: 2rem;
}
pre {
white-space: pre-wrap;
background: #1e1e1e;
padding: 1rem;
border: 1px solid #333;
}
h1, h3 {
color: #61dafb;
}
#notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #222;
color: #fff;
padding: 1rem;
border: 1px solid #555;
border-radius: 8px;
opacity: 0.95;
display: none;
z-index: 3000;
font-size: 0.9rem;
transition: opacity 0.3s ease;
}
.server-card {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.server-name {
font-size: 1.2rem;
font-weight: bold;
color: #61dafb;
}
.server-actions {
display: flex;
gap: 0.5rem;
}
.feature-tag {
display: inline-block;
background: #444;
padding: 0.2rem 0.5rem;
margin: 0.2rem;
border-radius: 4px;
font-size: 0.8rem;
}
.add-server-form {
background: #1e1e1e;
border: 1px solid #333;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
}
.form-group {
flex: 1;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
}
.checkbox-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dm-users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.dm-user-card {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
transition: all 0.3s ease;
}
.dm-user-card:hover {
border-color: #666;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.dm-user-card h4 {
margin: 0 0 0.5rem 0;
color: #4CAF50;
}
.dm-user-card p {
margin: 0.25rem 0;
font-size: 0.9rem;
}
.dm-user-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Blocked Users Styles */
.blocked-users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.blocked-user-card {
background: #3d2a2a;
border: 1px solid #664444;
border-radius: 8px;
padding: 1rem;
transition: all 0.3s ease;
}
.blocked-user-card:hover {
border-color: #886666;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.blocked-user-card h4 {
margin: 0 0 0.5rem 0;
color: #ff9800;
}
.blocked-user-card p {
margin: 0.25rem 0;
font-size: 0.9rem;
}
.blocked-user-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Conversation View Styles */
.message-reactions {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.reaction-item {
display: inline-flex;
align-items: center;
gap: 0.3rem;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 12px;
padding: 0.2rem 0.5rem;
font-size: 0.85rem;
transition: background 0.2s ease;
}
.reaction-item:hover {
background: rgba(255,255,255,0.12);
}
.reaction-emoji {
font-size: 1rem;
}
.reaction-by {
color: #aaa;
font-size: 0.75rem;
}
.reaction-by.bot-reaction {
color: #61dafb;
}
.reaction-by.user-reaction {
color: #ffa726;
}
.attachment {
margin: 0.25rem 0;
}
.delete-message-btn {
opacity: 0.7;
transition: opacity 0.3s ease;
}
.delete-message-btn:hover {
opacity: 1;
}
.dm-user-actions button {
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
.conversation-view {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
}
.conversations-list {
max-height: 600px;
overflow-y: auto;
margin-top: 1rem;
}
.conversation-message {
background: #333;
border: 1px solid #555;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.conversation-message.user-message {
border-left: 4px solid #4CAF50;
}
.conversation-message.bot-message {
border-left: 4px solid #2196F3;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.sender {
font-weight: bold;
}
.timestamp {
color: #888;
font-size: 0.8rem;
}
.message-content {
margin-bottom: 0.5rem;
line-height: 1.4;
}
.message-attachments {
background: #444;
border-radius: 4px;
padding: 0.5rem;
font-size: 0.9rem;
}
.attachment {
margin: 0.25rem 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.attachment a {
color: #4CAF50;
text-decoration: none;
}
.attachment a:hover {
text-decoration: underline;
}
/* Tab styling */
.tab-container {
margin-bottom: 1rem;
}
.tab-buttons {
display: grid;
grid-template-rows: repeat(2, auto);
grid-auto-flow: column;
grid-auto-columns: max-content;
border-bottom: 2px solid #333;
margin-bottom: 1rem;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #555 #222;
row-gap: 0.05rem;
column-gap: 0.1rem;
padding-bottom: 0.1rem;
}
.tab-buttons::-webkit-scrollbar {
height: 8px;
}
.tab-buttons::-webkit-scrollbar-track {
background: #222;
}
.tab-buttons::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.tab-buttons::-webkit-scrollbar-thumb:hover {
background: #666;
}
.tab-button {
background: #222;
color: #ccc;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
white-space: nowrap;
}
.tab-button:hover {
background: #333;
color: #fff;
}
.tab-button.active {
background: #444;
color: #fff;
border-bottom-color: #4CAF50;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Chat Interface Styles */
.chat-message {
margin-bottom: 1rem;
padding: 1rem;
border-radius: 8px;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-message.user-message {
background: #2a3a4a;
border-left: 4px solid #4CAF50;
margin-left: 2rem;
}
.chat-message.assistant-message {
background: #3a2a3a;
border-left: 4px solid #61dafb;
margin-right: 2rem;
}
.chat-message.error-message {
background: #4a2a2a;
border-left: 4px solid #f44336;
}
.chat-message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.chat-message-sender {
font-weight: bold;
color: #61dafb;
}
.chat-message.user-message .chat-message-sender {
color: #4CAF50;
}
.chat-message-time {
color: #888;
font-size: 0.8rem;
}
.chat-message-content {
color: #ddd;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.chat-typing-indicator {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.5rem;
}
.chat-typing-indicator span {
width: 8px;
height: 8px;
background: #61dafb;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.chat-typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.chat-typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.7; }
30% { transform: translateY(-10px); opacity: 1; }
}
#chat-messages::-webkit-scrollbar {
width: 8px;
}
#chat-messages::-webkit-scrollbar-track {
background: #1e1e1e;
}
#chat-messages::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
#chat-messages::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Evil Mode Styles */
body.evil-mode h1, body.evil-mode h3 {
color: #ff4444;
}
body.evil-mode .tab-button.active {
border-bottom-color: #ff4444;
}
body.evil-mode #evil-mode-toggle {
background: #ff4444;
border-color: #ff4444;
color: #000;
}
body.evil-mode .server-name {
color: #ff4444;
}
body.evil-mode .chat-message-sender {
color: #ff4444;
}
body.evil-mode .chat-message.assistant-message {
border-left-color: #ff4444;
}
body.evil-mode #notification {
border-color: #ff4444;
}
/* Override any blue status text in evil mode */
body.evil-mode [style*="color: #007bff"],
body.evil-mode [style*="color: rgb(0, 123, 255)"] {
color: #ff4444 !important;
}
/* Bipolar Mode Styles */
#bipolar-section {
transition: all 0.3s ease;
}
#bipolar-section h3 {
margin-top: 0;
}
#bipolar-mode-toggle.bipolar-active {
background: #9932CC !important;
border-color: #9932CC !important;
}
/* Responsive breakpoints */
@media (max-width: 1200px) {
.panel { width: 55%; padding: 1.5rem; }
.logs { width: 45%; }
}
@media (max-width: 1024px) {
body { flex-direction: column; }
.panel { width: 100%; padding: 1.5rem; }
.logs {
width: 100%;
height: 300px;
border-left: none;
border-top: 2px solid #333;
}
}
@media (max-width: 768px) {
.panel { padding: 1rem; }
.tab-buttons {
grid-template-rows: none;
grid-auto-flow: row;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
}
.tab-button { font-size: 0.85rem; padding: 0.4rem 0.6rem; }
}
@media (max-width: 480px) {
.panel { padding: 0.5rem; }
.tab-buttons { grid-template-columns: 1fr 1fr; }
.tab-button { font-size: 0.8rem; padding: 0.35rem 0.5rem; }
h1 { font-size: 1.2rem; }
}
</style>
</head>
<body>
<div class="panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div style="display: flex; gap: 1rem; align-items: center;">
<h1 id="panel-title">Miku Control Panel</h1>
<button id="gpu-selector-toggle" onclick="toggleGPU()" style="background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: 2px solid #4a7bc9; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9rem;">
🎮 GPU: NVIDIA
</button>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="bipolar-mode-toggle" onclick="toggleBipolarMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold;">
🔄 Bipolar: OFF
</button>
<button id="evil-mode-toggle" onclick="toggleEvilMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold;">
😈 Evil Mode: OFF
</button>
</div>
</div>
<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" data-tab="tab1" onclick="switchTab('tab1')">Server Management</button>
<button class="tab-button" data-tab="tab2" onclick="switchTab('tab2')">Actions</button>
<button class="tab-button" data-tab="tab3" onclick="switchTab('tab3')">Status</button>
<button class="tab-button" data-tab="tab4" onclick="switchTab('tab4')">⚙️ LLM Settings</button>
<button class="tab-button" data-tab="tab5" onclick="switchTab('tab5')">🎨 Image Generation</button>
<button class="tab-button" data-tab="tab6" onclick="switchTab('tab6')">📊 Autonomous Stats</button>
<button class="tab-button" data-tab="tab7" onclick="switchTab('tab7')">💬 Chat with LLM</button>
<button class="tab-button" data-tab="tab8" onclick="switchTab('tab8')">📞 Voice Call</button>
<button class="tab-button" data-tab="tab9" onclick="switchTab('tab9')">🧠 Memories</button>
<button class="tab-button" onclick="window.location.href='/static/system.html'">🎛️ System Settings</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>
<!-- Engage User Submenu -->
<div style="margin-bottom: 1rem;">
<button onclick="toggleEngageSubmenu()">Engage User ▼</button>
<div id="engage-submenu" style="display: none; margin-left: 1rem; margin-top: 0.5rem; padding: 1rem; background: #1e1e1e; border: 1px solid #444; border-radius: 4px;">
<div style="margin-bottom: 0.5rem;">
<label for="engage-user-id" style="display: block; margin-bottom: 0.3rem;">User ID (leave empty for random):</label>
<input type="text" id="engage-user-id" placeholder="User ID" style="width: 200px;">
</div>
<div style="margin-bottom: 0.5rem;">
<label style="display: block; margin-bottom: 0.3rem;">Engagement Type:</label>
<div style="margin-left: 0.5rem;">
<label style="display: block; margin-bottom: 0.2rem;">
<input type="radio" name="engage-type" value="random" checked> Random (auto-detect)
</label>
<label style="display: block; margin-bottom: 0.2rem;">
<input type="radio" name="engage-type" value="activity"> Activity-based (comment on what they're doing)
</label>
<label style="display: block; margin-bottom: 0.2rem;">
<input type="radio" name="engage-type" value="general"> General conversation
</label>
<label style="display: block; margin-bottom: 0.2rem;">
<input type="radio" name="engage-type" value="status"> Status-based (online/idle/invisible)
</label>
</div>
</div>
<button onclick="triggerEngageUser()">🚀 Engage User</button>
</div>
</div>
<!-- Share Tweet Submenu -->
<div style="margin-bottom: 1rem;">
<button onclick="toggleTweetSubmenu()">Share Tweet ▼</button>
<div id="tweet-submenu" style="display: none; margin-left: 1rem; margin-top: 0.5rem; padding: 1rem; background: #1e1e1e; border: 1px solid #444; border-radius: 4px;">
<div style="margin-bottom: 0.5rem;">
<label for="tweet-url" style="display: block; margin-bottom: 0.3rem;">Tweet URL (leave empty for auto-fetch):</label>
<input type="text" id="tweet-url" placeholder="https://x.com/... or https://twitter.com/... or https://fxtwitter.com/..." style="width: 100%;">
</div>
<button onclick="triggerShareTweet()">🐦 Share Tweet</button>
</div>
</div>
<button onclick="triggerAutonomous('reaction')">React to Message</button>
<button onclick="triggerAutonomous('join-conversation')">Detect and Join Conversation</button>
<button onclick="toggleCustomPrompt()">Custom Prompt</button>
</div>
<!-- Bipolar Mode Section (only visible when bipolar mode is on) -->
<div id="bipolar-section" class="section" style="display: none; border: 2px solid #9932CC; padding: 1rem; border-radius: 8px; background: #1a1a2e;">
<h3 style="color: #9932CC;">🔄 Bipolar Mode Controls</h3>
<p style="font-size: 0.9rem; color: #aaa;">Trigger arguments or dialogues between Regular Miku and Evil Miku</p>
<!-- Persona Dialogue Section -->
<div style="margin-bottom: 2rem; padding: 1rem; background: #252540; border-radius: 8px; border: 1px solid #555;">
<h4 style="color: #6B8EFF; margin-bottom: 0.5rem;">💬 Trigger Persona Dialogue</h4>
<p style="font-size: 0.85rem; color: #999; margin-bottom: 1rem;">Start a natural conversation between the personas (can escalate to argument if tension builds)</p>
<div style="margin-bottom: 1rem;">
<label for="dialogue-message-id">Message ID:</label>
<input type="text" id="dialogue-message-id" placeholder="e.g., 1234567890123456789" style="width: 250px; margin-left: 0.5rem; font-family: monospace;">
</div>
<div style="font-size: 0.8rem; color: #888; margin-bottom: 1rem;">
💡 <strong>Tip:</strong> Right-click any bot response message in Discord and select "Copy Message ID". The opposite persona will analyze it and decide whether to interject.
</div>
<button onclick="triggerPersonaDialogue()" style="background: #6B8EFF; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;">
💬 Trigger Dialogue
</button>
<div id="dialogue-status" style="margin-top: 1rem; font-size: 0.9rem;"></div>
</div>
<!-- Argument Section -->
<div style="padding: 1rem; background: #2e1a2e; border-radius: 8px; border: 1px solid #555;">
<h4 style="color: #9932CC; margin-bottom: 0.5rem;">⚔️ Trigger Argument</h4>
<p style="font-size: 0.85rem; color: #999; margin-bottom: 1rem;">Force an immediate argument (bypasses dialogue system)</p>
<div style="margin-bottom: 1rem; display: flex; gap: 1rem; flex-wrap: wrap;">
<div>
<label for="bipolar-channel-id">Channel ID:</label>
<input type="text" id="bipolar-channel-id" placeholder="e.g., 1234567890123456789" style="width: 200px; margin-left: 0.5rem; font-family: monospace;">
</div>
<div>
<label for="bipolar-message-id">Starting Message ID (optional):</label>
<input type="text" id="bipolar-message-id" placeholder="e.g., 1234567890123456789" style="width: 200px; margin-left: 0.5rem; font-family: monospace;">
</div>
</div>
<div style="font-size: 0.8rem; color: #888; margin-bottom: 1rem;">
💡 <strong>Tip:</strong> Right-click a message in Discord and select "Copy Message ID" (enable Developer Mode in Discord settings).
If a starting message ID is provided, the opposite persona will respond to that message.
</div>
<div style="margin-bottom: 1rem;">
<label for="bipolar-context">Argument Context (optional):</label>
<input type="text" id="bipolar-context" placeholder="e.g., They're fighting about who's the real Miku..." style="width: 100%; margin-top: 0.3rem;">
</div>
<button onclick="triggerBipolarArgument()" style="background: #9932CC; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;">
⚔️ Trigger Argument
</button>
<div id="bipolar-status" style="margin-top: 1rem; font-size: 0.9rem;"></div>
</div>
<!-- Scoreboard Display -->
<div id="bipolar-scoreboard" style="margin-top: 1.5rem; padding: 1rem; background: #0f0f1e; border-radius: 8px; border: 1px solid #444;">
<h4 style="color: #9932CC; margin-bottom: 0.5rem;">🏆 Argument Scoreboard</h4>
<div id="scoreboard-content" style="font-size: 0.9rem;">
<p style="color: #888;">Loading scoreboard...</p>
</div>
<button onclick="loadScoreboard()" style="margin-top: 0.5rem; background: #444; color: #fff; border: none; padding: 0.3rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
🔄 Refresh
</button>
</div>
<div id="active-arguments" style="margin-top: 1rem; display: none;">
<h4>Active Arguments:</h4>
<div id="active-arguments-list"></div>
</div>
</div>
<div class="section">
<h3>🎨 Profile Picture</h3>
<p style="font-size: 0.9rem; color: #aaa;">Change Miku's profile picture using Danbooru search or upload a custom image.</p>
<div style="margin-bottom: 1rem;">
<button onclick="changeProfilePicture()">🎨 Change Profile Picture (Danbooru)</button>
<button onclick="restoreFallbackPfp()">🔄 Restore Original Avatar</button>
</div>
<div style="margin-bottom: 1rem;">
<label for="pfp-upload">Upload Custom Image:</label>
<input type="file" id="pfp-upload" accept="image/*" style="margin-left: 0.5rem;">
<button onclick="uploadCustomPfp()">📤 Upload & Apply</button>
<div style="font-size: 0.8rem; color: #888; margin-top: 0.3rem; margin-left: 0.5rem;">
💡 Supports static images (PNG, JPG) and animated GIFs<br>
⚠️ Animated GIFs require Discord Nitro on the bot account
</div>
</div>
<div id="pfp-status" style="margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;"></div>
<div id="pfp-metadata" style="margin-top: 1rem; background: #1e1e1e; padding: 0.5rem; border: 1px solid #333; display: none;">
<h4 style="margin-top: 0;">Current Profile Picture Info:</h4>
<pre id="pfp-metadata-content" style="margin: 0;"></pre>
</div>
<!-- Role Color Management -->
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #333;">
<h4>🎨 Role Color Management</h4>
<p style="font-size: 0.9rem; color: #aaa;">Manually set Miku's role color or reset to fallback (#86cecb)</p>
<div style="margin-bottom: 1rem; display: flex; gap: 10px; align-items: end;">
<div>
<label for="role-color-hex">Hex Color:</label>
<input type="text" id="role-color-hex" placeholder="#86cecb" maxlength="7" style="width: 100px; font-family: monospace;">
</div>
<button onclick="setCustomRoleColor()">🎨 Apply Color</button>
<button onclick="resetRoleColor()">🔄 Reset to Fallback</button>
</div>
<div id="role-color-status" style="margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;"></div>
</div>
</div>
<div class="section">
<h3>Figurine DM Subscribers</h3>
<!-- Subscriber Management -->
<div style="margin-bottom: 1rem;">
<h4>Subscriber Management</h4>
<div style="margin-bottom: 0.5rem;">
<button onclick="refreshFigurineSubscribers()">🔄 Refresh</button>
</div>
<div style="display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;">
<div>
<label for="figurine-user-id">User ID:</label>
<input type="text" id="figurine-user-id" placeholder="Discord User ID (as string)" />
</div>
<button onclick="addFigurineSubscriber()"> Add Subscriber</button>
</div>
<div id="figurine-subscribers-list"></div>
</div>
<!-- Send to All Subscribers -->
<div style="margin-bottom: 1rem; border-top: 1px solid #444; padding-top: 1rem;">
<h4>Send to All Subscribers</h4>
<div style="margin-bottom: 0.5rem;">
<label for="figurine-tweet-url-all">Tweet URL (optional):</label>
<input type="text" id="figurine-tweet-url-all" placeholder="https://twitter.com/username/status/..." style="width: 300px;" />
</div>
<button onclick="sendFigurineNowToAll()">📨 Send to All Subscribers</button>
<div id="figurine-all-status" style="margin-top: 0.5rem; font-size: 0.9rem;"></div>
</div>
<!-- Send to Single User -->
<div style="border-top: 1px solid #444; padding-top: 1rem;">
<h4>Send to Single User</h4>
<div style="display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;">
<div>
<label for="figurine-single-user-id">User ID:</label>
<input type="text" id="figurine-single-user-id" placeholder="Discord User ID" />
</div>
<div>
<label for="figurine-tweet-url-single">Tweet URL (optional):</label>
<input type="text" id="figurine-tweet-url-single" placeholder="https://twitter.com/username/status/..." style="width: 250px;" />
</div>
<button onclick="sendFigurineToSingleUser()">📨 Send to User</button>
</div>
<div id="figurine-single-status" style="margin-top: 0.5rem; font-size: 0.9rem;"></div>
</div>
</div>
<div class="section">
<h3>Manual Actions</h3>
<div style="margin-bottom: 1rem;">
<label for="manual-server-select">Target Server:</label>
<select id="manual-server-select">
<option value="all">All Servers</option>
</select>
</div>
<button onclick="forceSleep()">Force Sleep</button>
<button onclick="wakeUp()">Wake Up</button>
<button onclick="sendBedtime()">Send Bedtime</button>
<button onclick="resetConversation()">Reset Conversation</button>
</div>
<div class="section" id="custom-prompt-section">
<h3>🎙️ Send Custom Prompt to Miku</h3>
<!-- Target Selection -->
<div style="margin-bottom: 1rem;">
<label for="custom-prompt-target-type">Target Type:</label>
<select id="custom-prompt-target-type" onchange="toggleCustomPromptTarget()" style="margin-right: 1rem;">
<option value="server">Server</option>
<option value="dm">Direct Message</option>
</select>
<!-- Server Selection -->
<span id="custom-prompt-server-section">
<label for="custom-prompt-server-select">Target Server:</label>
<select id="custom-prompt-server-select">
<option value="all">All Servers</option>
</select>
</span>
<!-- DM User ID Input -->
<span id="custom-prompt-dm-section" style="display: none;">
<label for="custom-prompt-user-id">User ID:</label>
<input type="text" id="custom-prompt-user-id" placeholder="Discord User ID" style="width: 200px;" />
</span>
</div>
<div>
<label for="customPrompt">Custom Prompt:</label>
<textarea id="customPrompt" placeholder="e.g. Talk about how nice the weather is today" rows="3" style="width: 100%; margin-top: 0.5rem;"></textarea>
</div>
<div style="margin-top: 0.5rem;">
<label for="customPromptAttachment">Attach File (optional):</label>
<input type="file" id="customPromptAttachment" multiple />
</div>
<button onclick="sendCustomPrompt()" style="margin-top: 0.5rem;">Send Custom Prompt</button>
<p id="customStatus" style="color: green; margin-top: 0.5rem;"></p>
</div>
<div class="section" id="manual-message-section">
<h3>🎭 Send Message as Miku (Manual Override)</h3>
<!-- Webhook Option -->
<div style="margin-bottom: 1rem; padding: 0.5rem; background: #2a2a2a; border-radius: 4px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="manual-use-webhook" onchange="toggleWebhookOptions()" style="margin-right: 0.5rem;" />
<span>Send as Webhook (allows choosing persona)</span>
</label>
<div id="webhook-persona-options" style="display: none; margin-top: 0.5rem; padding-left: 1.5rem;">
<label style="display: block; margin-bottom: 0.3rem;">
<input type="radio" name="webhook-persona" value="miku" checked style="margin-right: 0.5rem;" />
Hatsune Miku 💙 (with mood emoji)
</label>
<label style="display: block;">
<input type="radio" name="webhook-persona" value="evil" style="margin-right: 0.5rem;" />
Evil Miku 😈 (with mood emoji)
</label>
<p style="font-size: 0.8rem; color: #888; margin: 0.3rem 0 0 0;">
Note: Webhooks only work in channels, not DMs. Profile picture and mood emoji will be used.
</p>
</div>
</div>
<!-- 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>
<!-- Reply Configuration -->
<div style="margin-top: 0.5rem;">
<label for="manualReplyMessageId">Reply to Message ID (optional):</label>
<input type="text" id="manualReplyMessageId" placeholder="Enter message ID to reply to..." style="width: 100%;" />
</div>
<div style="margin-top: 0.5rem;">
<label style="margin-right: 1rem;">Mention user in reply:</label>
<label>
<input type="radio" name="manualReplyMention" value="true" checked />
Yes (ping user)
</label>
<label style="margin-left: 1rem;">
<input type="radio" name="manualReplyMention" value="false" />
No (silent reply)
</label>
</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>
<!-- LLM Settings Tab Content -->
<div id="tab4" class="tab-content">
<div class="section">
<h3>⚙️ Language Model Settings</h3>
<p>Configure language model behavior and language mode.</p>
<!-- Language Mode Section -->
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px; border: 2px solid #4a7bc9;">
<h4 style="margin-top: 0; color: #61dafb;">🌐 Language Mode</h4>
<p style="margin: 0.5rem 0; color: #aaa;">Switch Miku between English and Japanese responses.</p>
<div style="margin: 1rem 0;">
<div style="margin-bottom: 1rem;">
<strong>Current Language:</strong> <span id="current-language-display" style="color: #61dafb; font-weight: bold;">English</span>
</div>
<button onclick="toggleLanguageMode()" style="background: #4a7bc9; color: #fff; padding: 0.6rem 1.2rem; border: 2px solid #61dafb; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem;">
🔄 Toggle Language (English ↔ Japanese)
</button>
</div>
<div style="margin-top: 1rem; padding: 1rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #4a7bc9;">
<div style="font-size: 0.9rem;">
<div style="margin-bottom: 0.5rem;"><strong>English Mode:</strong></div>
<ul style="margin: 0 0 0.5rem 0; padding-left: 1.5rem; color: #aaa;">
<li>Uses standard Llama 3.1 model</li>
<li>Responds in English only</li>
</ul>
<div style="margin-bottom: 0.5rem;"><strong>Japanese Mode (日本語):</strong></div>
<ul style="margin: 0 0 0; padding-left: 1.5rem; color: #aaa;">
<li>Uses Llama 3.1 Swallow model (trained for Japanese)</li>
<li>Responds entirely in Japanese</li>
</ul>
</div>
</div>
</div>
<!-- Language Mode Status Section -->
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;">
<h4 style="margin-top: 0;">📊 Current Status</h4>
<div id="language-status-display" style="background: #1a1a1a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;">
<p style="margin: 0.5rem 0;"><strong>Language Mode:</strong> <span id="status-language">English</span></p>
<p style="margin: 0.5rem 0;"><strong>Active Model:</strong> <span id="status-model">llama3.1</span></p>
<p style="margin: 0.5rem 0;"><strong>Available Languages:</strong> English, 日本語 (Japanese)</p>
</div>
<button onclick="refreshLanguageStatus()" style="margin-top: 1rem;">🔄 Refresh Status</button>
</div>
<!-- Information Section -->
<div style="padding: 1rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #ff9800;">
<h4 style="margin-top: 0; color: #ff9800;"> How Language Mode Works</h4>
<ul style="margin: 0.5rem 0; padding-left: 1.5rem; font-size: 0.9rem; color: #aaa;">
<li>English mode uses your default text model for English responses</li>
<li>Japanese mode switches to Swallow and responds only in 日本語</li>
<li>All personality traits, mood system, and features work in both modes</li>
<li>Language mode is global - affects all servers and DMs</li>
<li>Conversation history is preserved across language switches</li>
</ul>
</div>
</div>
</div>
<!-- Image Generation Tab Content -->
<div id="tab5" 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="generateManualImage()" style="margin-right: 0.5rem;">🎨 Generate Image</button>
<div id="manual-image-status" style="margin-top: 0.5rem; font-size: 0.9rem;"></div>
<div id="manual-image-preview" style="margin-top: 1rem; text-align: center;"></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="tab6" 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>
<!-- Chat with LLM Tab Content -->
<div id="tab7" class="tab-content">
<div class="section">
<h3>💬 Chat with LLM</h3>
<p>Direct chat interface with the language models. Test responses, experiment with prompts, or just chat with Miku!</p>
<!-- Configuration Options -->
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem;">
<h4 style="margin-top: 0; color: #61dafb;">⚙️ Chat Configuration</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1rem;">
<!-- Model Selection -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🤖 Model Type:</label>
<div style="display: flex; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-model-type" value="text" checked onchange="toggleChatImageUpload()">
<span style="margin-left: 0.5rem;">💬 Text Model (Fast)</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-model-type" value="vision" onchange="toggleChatImageUpload()">
<span style="margin-left: 0.5rem;">👁️ Vision Model (Images)</span>
</label>
</div>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Text model for conversations, Vision model for image analysis
</div>
</div>
<!-- System Prompt Toggle -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🎭 System Prompt:</label>
<div style="display: flex; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-system-prompt" value="true" checked>
<span style="margin-left: 0.5rem;">✅ Use Miku Personality</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-system-prompt" value="false">
<span style="margin-left: 0.5rem;">❌ Raw LLM (No Prompt)</span>
</label>
</div>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
With prompt: Chat as Miku. Without: Direct LLM responses
</div>
</div>
<!-- Mood Selection -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">😊 Miku's Mood:</label>
<select id="chat-mood-select" style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;">
<option value="neutral" selected>neutral</option>
<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="romantic">💌 romantic</option>
<option value="serious">👔 serious</option>
<option value="shy">👉👈 shy</option>
<option value="silly">🪿 silly</option>
<option value="sleepy">🌙 sleepy</option>
</select>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Choose Miku's emotional state for this conversation
</div>
</div>
</div>
<!-- Image Upload for Vision Model -->
<div id="chat-image-upload-section" style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🖼️ Upload Image:</label>
<input type="file" id="chat-image-file" accept="image/*" style="margin-bottom: 0.5rem;">
<div style="font-size: 0.85rem; color: #aaa;">
Upload an image for the vision model to analyze
</div>
<div id="chat-image-preview" style="margin-top: 0.5rem; display: none;">
<img id="chat-image-preview-img" style="max-width: 200px; max-height: 200px; border: 1px solid #555; border-radius: 4px;">
</div>
</div>
<!-- Clear Chat Button -->
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;">
<button onclick="clearChatHistory()" style="background: #ff9800;">🗑️ Clear Chat History</button>
<span style="margin-left: 1rem; font-size: 0.85rem; color: #aaa;">Remove all messages from this session</span>
</div>
</div>
<!-- Chat Messages Container -->
<div id="chat-messages" style="background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 1rem; min-height: 400px; max-height: 500px; overflow-y: auto; margin-bottom: 1rem;">
<div style="text-align: center; color: #888; padding: 2rem;">
💬 Start chatting with the LLM! Your conversation will appear here.
</div>
</div>
<!-- Chat Input Area -->
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<div style="flex: 1;">
<textarea
id="chat-input"
placeholder="Type your message here..."
rows="3"
style="width: 100%; padding: 0.75rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: inherit; resize: vertical;"
onkeydown="handleChatKeyPress(event)"
></textarea>
</div>
<div>
<button
id="chat-send-btn"
onclick="sendChatMessage()"
style="padding: 1rem 1.5rem; height: 100%; background: #4CAF50; font-size: 1rem; font-weight: bold;"
>
📤 Send
</button>
</div>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: #aaa;">
💡 Tip: Press Ctrl+Enter to send your message quickly
</div>
</div>
</div>
<!-- Tab 8: Voice Call Management -->
<div id="tab8" class="tab-content">
<div class="section">
<h3>📞 Initiate Voice Call</h3>
<p>Start an automated voice chat session with a user. Miku will automatically manage containers, join voice chat, and send an invitation DM.</p>
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem;">
<h4 style="margin-top: 0; color: #61dafb;">⚙️ Voice Call Configuration</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<!-- User ID Input -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">👤 Target User ID:</label>
<input
type="text"
id="voice-user-id"
placeholder="Discord user ID (e.g., 123456789)"
style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"
>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Discord ID of the user to call
</div>
</div>
<!-- Voice Channel ID Input -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🎤 Voice Channel ID:</label>
<input
type="text"
id="voice-channel-id"
placeholder="Discord channel ID (e.g., 987654321)"
style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"
>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Discord ID of the voice channel to join
</div>
</div>
</div>
<!-- Debug Mode Toggle -->
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #1e1e1e; border-radius: 4px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input
type="checkbox"
id="voice-debug-mode"
style="margin-right: 0.7rem; width: 18px; height: 18px; cursor: pointer;"
>
<span style="font-weight: bold;">🐛 Debug Mode</span>
</label>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.5rem; margin-left: 1.7rem;">
When enabled, shows voice transcriptions and responses in text channel. When disabled, voice chat is private.
</div>
</div>
<!-- Call Status Display -->
<div id="voice-call-status" style="background: #1e1e1e; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; display: none;">
<div style="color: #61dafb; font-weight: bold; margin-bottom: 0.5rem;">📊 Call Status:</div>
<div id="voice-call-status-text" style="color: #aaa; font-size: 0.9rem;"></div>
<div id="voice-call-invite-link" style="margin-top: 0.5rem; display: none;">
<strong>Invite Link:</strong> <a id="voice-call-invite-url" href="" target="_blank" style="color: #61dafb;">View Invite</a>
</div>
</div>
<!-- Call Buttons -->
<div style="display: flex; gap: 1rem;">
<button
id="voice-call-btn"
onclick="initiateVoiceCall()"
style="background: #2ecc71; color: #000; padding: 0.7rem 1.5rem; border: 1px solid #27ae60; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem;"
>
📞 Initiate Call
</button>
<button
id="voice-call-cancel-btn"
onclick="cancelVoiceCall()"
style="background: #e74c3c; color: #fff; padding: 0.7rem 1.5rem; border: 1px solid #c0392b; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem; display: none;"
>
🛑 Cancel Call
</button>
</div>
</div>
<!-- Call Information -->
<div style="background: #1a1a2e; padding: 1.5rem; border-radius: 8px; border-left: 3px solid #61dafb;">
<h4 style="margin-top: 0; color: #61dafb;"> How Voice Calls Work</h4>
<ul style="color: #ddd; line-height: 1.8;">
<li><strong>Automatic Setup:</strong> STT and TTS containers start automatically</li>
<li><strong>Warmup Wait:</strong> System waits for both containers to be ready (~30-75 seconds)</li>
<li><strong>VC Join:</strong> Miku joins the specified voice channel</li>
<li><strong>DM Invitation:</strong> User receives a personalized invite DM with a voice channel link</li>
<li><strong>Auto-Listen:</strong> STT automatically starts when user joins</li>
<li><strong>Auto-Leave:</strong> Miku leaves 45 seconds after user disconnects</li>
<li><strong>Timeout:</strong> If user doesn't join within 30 minutes, call is cancelled</li>
</ul>
</div>
<!-- Call History -->
<div style="margin-top: 2rem;">
<h4 style="color: #61dafb; margin-bottom: 1rem;">📋 Recent Calls</h4>
<div id="voice-call-history" style="background: #1e1e1e; border: 1px solid #444; border-radius: 4px; padding: 1rem;">
<div style="text-align: center; color: #888;">No calls yet. Start one above!</div>
</div>
</div>
</div>
</div>
<!-- Tab 9: Memory Management -->
<div id="tab9" class="tab-content">
<div class="section">
<h3>🧠 Cheshire Cat Memory Management</h3>
<p style="color: #aaa; margin-bottom: 1rem;">
Manage Miku's long-term memories powered by the Cheshire Cat AI pipeline.
Memories are stored in Qdrant vector database and used to give Miku persistent knowledge about users.
</p>
<!-- Cat Integration Status -->
<div id="cat-status-section" style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h4 style="margin: 0 0 0.3rem 0;">🐱 Cheshire Cat Status</h4>
<span id="cat-status-indicator" style="color: #888;">Checking...</span>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="cat-toggle-btn" onclick="toggleCatIntegration()" style="background: #333; color: #fff; padding: 0.4rem 0.8rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.85rem;">
Loading...
</button>
<button onclick="refreshMemoryStats()" style="background: #2a5599; color: #fff; padding: 0.4rem 0.8rem; border: none; border-radius: 4px; cursor: pointer;">
🔄 Refresh
</button>
</div>
</div>
</div>
<!-- Memory Statistics -->
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.5rem;">
<div id="stat-episodic" style="background: #1a2332; border: 1px solid #2a5599; border-radius: 8px; padding: 1rem; text-align: center;">
<div style="font-size: 2rem; font-weight: bold; color: #61dafb;" id="stat-episodic-count"></div>
<div style="color: #aaa; font-size: 0.85rem;">📝 Episodic Memories</div>
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">Conversation snippets</div>
</div>
<div id="stat-declarative" style="background: #1a3322; border: 1px solid #2a9955; border-radius: 8px; padding: 1rem; text-align: center;">
<div style="font-size: 2rem; font-weight: bold; color: #6fdc6f;" id="stat-declarative-count"></div>
<div style="color: #aaa; font-size: 0.85rem;">📚 Declarative Facts</div>
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">Learned knowledge</div>
</div>
<div id="stat-procedural" style="background: #332a1a; border: 1px solid #995e2a; border-radius: 8px; padding: 1rem; text-align: center;">
<div style="font-size: 2rem; font-weight: bold; color: #dcb06f;" id="stat-procedural-count"></div>
<div style="color: #aaa; font-size: 0.85rem;">⚙️ Procedural</div>
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">Tools & procedures</div>
</div>
</div>
<!-- Consolidation -->
<div style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">🌙 Memory Consolidation</h4>
<p style="color: #aaa; font-size: 0.85rem; margin-bottom: 0.75rem;">
Trigger the sleep consolidation process: analyzes episodic memories, extracts important facts, and removes trivial entries.
</p>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="consolidate-btn" onclick="triggerConsolidation()" style="background: #5b3a8c; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
🌙 Run Consolidation
</button>
<span id="consolidation-status" style="color: #888; font-size: 0.85rem;"></span>
</div>
<div id="consolidation-result" style="display: none; margin-top: 0.75rem; background: #111; border: 1px solid #333; border-radius: 4px; padding: 0.75rem; font-size: 0.85rem; color: #ccc; white-space: pre-wrap; max-height: 200px; overflow-y: auto;"></div>
</div>
<!-- Declarative Facts Browser -->
<div style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<h4 style="margin: 0;">📚 Declarative Facts</h4>
<div style="display: flex; gap: 0.5rem;">
<button onclick="showCreateMemoryModal('declarative')" style="background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
Add Fact
</button>
<button onclick="loadFacts()" style="background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
🔄 Load Facts
</button>
</div>
</div>
<div style="margin-bottom: 0.5rem;">
<input type="text" id="facts-search" placeholder="🔍 Search facts..."
oninput="filterMemories('facts-list', this.value)"
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
</div>
<div id="facts-list" style="max-height: 400px; overflow-y: auto;">
<div style="text-align: center; color: #666; padding: 2rem;">Click "Load Facts" to view stored knowledge</div>
</div>
</div>
<!-- Episodic Memories Browser -->
<div style="background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<h4 style="margin: 0;">📝 Episodic Memories</h4>
<div style="display: flex; gap: 0.5rem;">
<button onclick="showCreateMemoryModal('episodic')" style="background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
Add Memory
</button>
<button onclick="loadEpisodicMemories()" style="background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
🔄 Load Memories
</button>
</div>
</div>
<div style="margin-bottom: 0.5rem;">
<input type="text" id="episodic-search" placeholder="🔍 Search memories..."
oninput="filterMemories('episodic-list', this.value)"
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
</div>
<div id="episodic-list" style="max-height: 400px; overflow-y: auto;">
<div style="text-align: center; color: #666; padding: 2rem;">Click "Load Memories" to view conversation snippets</div>
</div>
</div>
<!-- DANGER ZONE: Delete All Memories -->
<div style="background: #2e1a1a; border: 2px solid #993333; border-radius: 8px; padding: 1rem;">
<h4 style="margin: 0 0 0.5rem 0; color: #ff6b6b;">⚠️ Danger Zone — Delete All Memories</h4>
<p style="color: #cc9999; font-size: 0.85rem; margin-bottom: 1rem;">
This will permanently erase ALL of Miku's memories — episodic conversations, learned facts, everything.
This action is <strong>irreversible</strong>. Miku will forget everything she has ever learned.
</p>
<!-- Step 1: Initial checkbox -->
<div id="delete-step-1" style="margin-bottom: 0.75rem;">
<label style="cursor: pointer; color: #ff9999;">
<input type="checkbox" id="delete-checkbox-1" onchange="onDeleteStep1Change()">
I understand this will permanently delete all of Miku's memories
</label>
</div>
<!-- Step 2: Second confirmation (hidden initially) -->
<div id="delete-step-2" style="display: none; margin-bottom: 0.75rem;">
<label style="cursor: pointer; color: #ff9999;">
<input type="checkbox" id="delete-checkbox-2" onchange="onDeleteStep2Change()">
I confirm this is irreversible and I want to proceed
</label>
</div>
<!-- Step 3: Type confirmation string (hidden initially) -->
<div id="delete-step-3" style="display: none; margin-bottom: 0.75rem;">
<p style="color: #ff6b6b; font-size: 0.85rem; margin-bottom: 0.5rem;">
Type exactly: <code style="background: #333; padding: 0.2rem 0.4rem; border-radius: 3px; color: #ff9999;">Yes, I am deleting Miku's memories fully.</code>
</p>
<input type="text" id="delete-confirmation-input" placeholder="Type the confirmation string..."
style="width: 100%; padding: 0.5rem; background: #1a1a1a; color: #ff9999; border: 1px solid #993333; border-radius: 4px; font-family: monospace; box-sizing: border-box;"
oninput="onDeleteInputChange()">
</div>
<!-- Final delete button (hidden initially) -->
<div id="delete-step-final" style="display: none;">
<button id="delete-all-btn" onclick="executeDeleteAllMemories()" disabled
style="background: #cc3333; color: #fff; padding: 0.5rem 1.5rem; border: none; border-radius: 4px; cursor: not-allowed; font-weight: bold; opacity: 0.5;">
🗑️ Permanently Delete All Memories
</button>
<button onclick="resetDeleteFlow()" style="background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="logs" id="logs-panel">
<h3>Logs</h3>
<div id="logs-paused-banner" class="logs-paused-indicator" onclick="scrollLogsToBottom()">⏸ Auto-scroll paused — click to resume</div>
<div id="logs-content"></div>
</div>
<div id="notification"></div>
<!-- Edit Memory Modal -->
<div id="edit-memory-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;">
<div style="background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;">
<h3 style="margin: 0 0 1rem 0; color: #61dafb;">✏️ Edit Memory</h3>
<div style="margin-bottom: 1rem;">
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Content:</label>
<textarea id="edit-memory-content" rows="5" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;"></textarea>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Source:</label>
<input type="text" id="edit-memory-source" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
</div>
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button onclick="closeEditMemoryModal()" style="background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">
Cancel
</button>
<button onclick="saveMemoryEdit()" style="background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
💾 Save Changes
</button>
</div>
</div>
</div>
<!-- Create Memory Modal -->
<div id="create-memory-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;">
<div style="background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;">
<input type="hidden" id="create-memory-collection" value="">
<h3 style="margin: 0 0 1rem 0; color: #6fdc6f;" id="create-modal-title"> Create New Memory</h3>
<div style="margin-bottom: 1rem;">
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Content:</label>
<textarea id="create-memory-content" rows="5" placeholder="Enter the fact or memory content..." style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;"></textarea>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">User ID (optional):</label>
<input type="text" id="create-memory-user-id" placeholder="e.g., discord_123456789" style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
<small style="color: #888;">Leave empty for general facts, or specify a Discord user ID for user-specific facts</small>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; color: #ccc; margin-bottom: 0.5rem;">Source (optional):</label>
<input type="text" id="create-memory-source" placeholder="e.g., manual_admin, wiki, etc." style="width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;">
</div>
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button onclick="closeCreateMemoryModal()" style="background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">
Cancel
</button>
<button onclick="saveNewMemory()" style="background: #2a9955; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
✨ Create Memory
</button>
</div>
</div>
</div>
<script>
// Global variables
let currentMood = 'neutral';
let voiceCallActive = false;
let voiceCallHistory = [];
let servers = [];
let evilMode = false;
// Mood emoji mapping
const MOOD_EMOJIS = {
"asleep": "💤",
"neutral": "",
"bubbly": "🫧",
"sleepy": "🌙",
"curious": "👀",
"shy": "👉👈",
"serious": "👔",
"excited": "✨",
"melancholy": "🍷",
"flirty": "🫦",
"romantic": "💌",
"irritated": "😒",
"angry": "💢",
"silly": "🪿"
};
// Evil mood emoji mapping
const EVIL_MOOD_EMOJIS = {
"aggressive": "👿",
"cunning": "🐍",
"sarcastic": "😈",
"evil_neutral": "",
"bored": "🥱",
"manic": "🤪",
"jealous": "💚",
"melancholic": "🌑",
"playful_cruel": "🎭",
"contemptuous": "👑"
};
// 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 matching tab button via data-tab attribute
const activeBtn = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
if (activeBtn) activeBtn.classList.add('active');
// Persist active tab to localStorage
localStorage.setItem('miku-active-tab', tabId);
console.log(`🔄 Switched to ${tabId}`);
if (tabId === 'tab1') {
console.log('🔄 Refreshing figurine subscribers for Server Management tab');
refreshFigurineSubscribers();
}
if (tabId === 'tab9') {
console.log('🧠 Refreshing memory stats for Memories tab');
refreshMemoryStats();
}
}
// ============================================================================
// Initialization — all setup in one DOMContentLoaded
// ============================================================================
let statusInterval, logsInterval, argsInterval;
function startPolling() {
if (!statusInterval) statusInterval = setInterval(loadStatus, 10000);
if (!logsInterval) logsInterval = setInterval(loadLogs, 5000);
if (!argsInterval) argsInterval = setInterval(loadActiveArguments, 5000);
}
function stopPolling() {
clearInterval(statusInterval); statusInterval = null;
clearInterval(logsInterval); logsInterval = null;
clearInterval(argsInterval); argsInterval = null;
}
function initTabState() {
const savedTab = localStorage.getItem('miku-active-tab');
if (savedTab && document.getElementById(savedTab)) {
switchTab(savedTab);
}
}
function initTabWheelScroll() {
const tabButtonsEl = document.querySelector('.tab-buttons');
if (tabButtonsEl) {
tabButtonsEl.addEventListener('wheel', function(e) {
if (e.deltaY !== 0) {
e.preventDefault();
tabButtonsEl.scrollLeft += e.deltaY;
}
}, { passive: false });
}
}
function initVisibilityPolling() {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
console.log('⏸ Tab hidden — polling paused');
} else {
loadStatus(); loadLogs(); loadActiveArguments();
startPolling();
console.log('▶️ Tab visible — polling resumed');
}
});
}
function initChatImagePreview() {
const imageInput = document.getElementById('chat-image-file');
if (imageInput) {
imageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const preview = document.getElementById('chat-image-preview');
const previewImg = document.getElementById('chat-image-preview-img');
previewImg.src = event.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
}
}
function initModalAccessibility() {
const editModal = document.getElementById('edit-memory-modal');
const createModal = document.getElementById('create-memory-modal');
if (editModal) {
editModal.setAttribute('role', 'dialog');
editModal.setAttribute('aria-modal', 'true');
editModal.setAttribute('aria-label', 'Edit Memory');
editModal.addEventListener('click', function(e) {
if (e.target === this) closeEditMemoryModal();
});
}
if (createModal) {
createModal.setAttribute('role', 'dialog');
createModal.setAttribute('aria-modal', 'true');
createModal.setAttribute('aria-label', 'Create Memory');
createModal.addEventListener('click', function(e) {
if (e.target === this) closeCreateMemoryModal();
});
}
}
document.addEventListener('DOMContentLoaded', function() {
// Tab & UI initialization
initTabState();
initTabWheelScroll();
initLogsScrollDetection();
initChatImagePreview();
initModalAccessibility();
// Load initial data
loadStatus();
loadServers();
loadLastPrompt();
loadLogs();
checkEvilModeStatus();
checkBipolarModeStatus();
checkGPUStatus();
refreshLanguageStatus();
refreshFigurineSubscribers();
loadProfilePictureMetadata();
loadVoiceDebugMode();
// Start polling with visibility awareness
initVisibilityPolling();
startPolling();
});
// Utility functions
let notificationTimer = null;
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.style.display = 'block';
notification.style.opacity = '0.95';
if (type === 'error') {
notification.style.backgroundColor = '#d32f2f';
} else if (type === 'success') {
notification.style.backgroundColor = '#2e7d32';
} else {
notification.style.backgroundColor = '#222';
}
if (notificationTimer) clearTimeout(notificationTimer);
notificationTimer = setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
notification.style.display = 'none';
notificationTimer = null;
}, 300);
}, 3000);
}
async function apiCall(endpoint, method = 'GET', data = null) {
try {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(endpoint, options);
const result = await response.json();
if (response.ok) {
return result;
} else {
throw new Error(result.message || 'API call failed');
}
} catch (error) {
console.error('API call error:', error);
showNotification(error.message, 'error');
throw error;
}
}
// Server Management
async function loadServers() {
try {
console.log('🎭 Loading servers...');
const data = await apiCall('/servers');
console.log('🎭 Servers response:', data);
if (data.servers) {
servers = data.servers;
console.log(`🎭 Loaded ${servers.length} servers:`, servers);
// Debug: Log each server's guild_id
servers.forEach((server, index) => {
console.log(`🎭 Server ${index}: guild_id = ${server.guild_id}, name = ${server.guild_name}`);
});
// Debug: Show raw response data
console.log('🎭 Raw API response data:', JSON.stringify(data, null, 2));
// Display servers
displayServers();
populateServerDropdowns();
populateMoodDropdowns(); // Populate mood dropdowns after servers are loaded
} else {
console.warn('🎭 No servers found in response');
servers = [];
}
} catch (error) {
console.error('🎭 Failed to load servers:', error);
servers = [];
}
}
function displayServers() {
const container = document.getElementById('servers-list');
if (servers.length === 0) {
container.innerHTML = '<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 result = await apiCall('/profile-picture/metadata');
if (result.status === 'ok' && result.metadata) {
const metadataDiv = document.getElementById('pfp-metadata');
const metadataContent = document.getElementById('pfp-metadata-content');
metadataContent.textContent = JSON.stringify(result.metadata, null, 2);
metadataDiv.style.display = 'block';
console.log('🎨 Loaded profile picture metadata:', result.metadata);
} else {
console.log('🎨 No profile picture metadata available');
}
} catch (error) {
console.error('🎨 Failed to load profile picture metadata:', error);
}
}
async function populateServerDropdowns() {
const serverSelect = document.getElementById('server-select');
const manualServerSelect = document.getElementById('manual-server-select');
const customPromptServerSelect = document.getElementById('custom-prompt-server-select');
// Clear existing options except "All Servers"
serverSelect.innerHTML = '<option value="all">All Servers</option>';
manualServerSelect.innerHTML = '<option value="all">All Servers</option>';
customPromptServerSelect.innerHTML = '<option value="all">All Servers</option>';
console.log('🎭 Populating server dropdowns with', servers.length, 'servers');
// Add server options
servers.forEach(server => {
console.log(`🎭 Adding server to dropdown: ${server.guild_name} (guild_id: ${server.guild_id}, type: ${typeof server.guild_id})`);
const option = document.createElement('option');
option.value = server.guild_id;
option.textContent = server.guild_name;
serverSelect.appendChild(option.cloneNode(true));
manualServerSelect.appendChild(option);
customPromptServerSelect.appendChild(option.cloneNode(true));
});
// Debug: Check what's actually in the manual-server-select dropdown
console.log('🎭 manual-server-select options:');
Array.from(manualServerSelect.options).forEach((opt, idx) => {
console.log(` [${idx}] value="${opt.value}" text="${opt.textContent}"`);
});
// Populate autonomous stats dropdown
populateAutonomousServerDropdown();
}
// Figurine subscribers UI functions (must be global for onclick handlers)
async function refreshFigurineSubscribers() {
try {
console.log('🔄 Figurines: Fetching subscribers...');
const data = await apiCall('/figurines/subscribers');
console.log('📋 Figurines: Received subscribers:', data);
displayFigurineSubscribers(data.subscribers || []);
showNotification('Subscribers refreshed');
} catch (e) {
console.error('❌ Figurines: Failed to fetch subscribers:', e);
}
}
function displayFigurineSubscribers(subscribers) {
const container = document.getElementById('figurine-subscribers-list');
if (!container) return;
if (!subscribers.length) {
container.innerHTML = '<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 data = await apiCall(`/figurines/subscribers/${uid}`, 'DELETE');
console.log('🗑️ Figurines: Remove subscriber response:', data);
if (data.status === 'ok') {
showNotification('Subscriber removed');
refreshFigurineSubscribers();
} else {
showNotification(data.message || 'Failed to remove subscriber', 'error');
}
} catch (e) {
console.error('❌ Figurines: Failed to remove subscriber:', e);
}
}
async function sendFigurineNowToAll() {
try {
console.log('📨 Figurines: Triggering send to all subscribers...');
const tweetUrl = document.getElementById('figurine-tweet-url-all').value.trim();
const statusDiv = document.getElementById('figurine-all-status');
statusDiv.textContent = 'Sending...';
statusDiv.style.color = evilMode ? '#ff4444' : '#007bff';
const formData = new FormData();
if (tweetUrl) {
formData.append('tweet_url', tweetUrl);
}
const res = await fetch('/figurines/send_now', {
method: 'POST',
body: formData
});
const data = await res.json();
console.log('📨 Figurines: Send to all response:', data);
if (data.status === 'ok') {
showNotification('Figurine DMs queued for all subscribers');
statusDiv.textContent = 'Queued successfully';
statusDiv.style.color = '#28a745';
document.getElementById('figurine-tweet-url-all').value = ''; // Clear input
} else {
showNotification(data.message || 'Bot not ready', 'error');
statusDiv.textContent = 'Failed: ' + (data.message || 'Unknown error');
statusDiv.style.color = '#dc3545';
}
} catch (e) {
console.error('❌ Figurines: Failed to queue figurine DMs for all:', e);
showNotification('Failed to queue figurine DMs', 'error');
document.getElementById('figurine-all-status').textContent = 'Error: ' + e.message;
document.getElementById('figurine-all-status').style.color = '#dc3545';
}
}
async function sendFigurineToSingleUser() {
try {
const userId = document.getElementById('figurine-single-user-id').value.trim();
const tweetUrl = document.getElementById('figurine-tweet-url-single').value.trim();
const statusDiv = document.getElementById('figurine-single-status');
if (!userId) {
showNotification('Enter a user ID', 'error');
return;
}
console.log(`📨 Figurines: Sending to single user ${userId}, tweet: ${tweetUrl || 'random'}`);
statusDiv.textContent = 'Sending...';
statusDiv.style.color = evilMode ? '#ff4444' : '#007bff';
const formData = new FormData();
formData.append('user_id', userId);
if (tweetUrl) {
formData.append('tweet_url', tweetUrl);
}
const res = await fetch('/figurines/send_to_user', {
method: 'POST',
body: formData
});
const data = await res.json();
console.log('📨 Figurines: Send to single user response:', data);
if (data.status === 'ok') {
showNotification(`Figurine DM queued for user ${userId}`);
statusDiv.textContent = 'Queued successfully';
statusDiv.style.color = '#28a745';
document.getElementById('figurine-single-user-id').value = ''; // Clear inputs
document.getElementById('figurine-tweet-url-single').value = '';
} else {
showNotification(data.message || 'Failed to queue DM', 'error');
statusDiv.textContent = 'Failed: ' + (data.message || 'Unknown error');
statusDiv.style.color = '#dc3545';
}
} catch (e) {
console.error('❌ Figurines: Failed to queue figurine DM for single user:', e);
showNotification('Failed to queue figurine DM', 'error');
document.getElementById('figurine-single-status').textContent = 'Error: ' + e.message;
document.getElementById('figurine-single-status').style.color = '#dc3545';
}
}
// Keep the old function for backward compatibility
async function sendFigurineNow() {
return sendFigurineNowToAll();
}
async function addServer() {
// Don't use parseInt() for Discord IDs - they're too large for JS integers
const guildId = document.getElementById('new-guild-id').value.trim();
const guildName = document.getElementById('new-guild-name').value;
const autonomousChannelId = document.getElementById('new-autonomous-channel-id').value.trim();
const autonomousChannelName = document.getElementById('new-autonomous-channel-name').value;
const bedtimeChannelIds = document.getElementById('new-bedtime-channel-ids').value
.split(',').map(id => id.trim()).filter(id => id.length > 0);
const enabledFeatures = [];
if (document.getElementById('feature-autonomous').checked) enabledFeatures.push('autonomous');
if (document.getElementById('feature-bedtime').checked) enabledFeatures.push('bedtime');
if (document.getElementById('feature-monday-video').checked) enabledFeatures.push('monday_video');
if (!guildId || !guildName || !autonomousChannelId || !autonomousChannelName) {
showNotification('Please fill in all required fields', 'error');
return;
}
try {
await apiCall('/servers', 'POST', {
guild_id: guildId,
guild_name: guildName,
autonomous_channel_id: autonomousChannelId,
autonomous_channel_name: autonomousChannelName,
bedtime_channel_ids: bedtimeChannelIds.length > 0 ? bedtimeChannelIds : [autonomousChannelId],
enabled_features: enabledFeatures
});
showNotification('Server added successfully');
loadServers();
// Clear form
document.getElementById('new-guild-id').value = '';
document.getElementById('new-guild-name').value = '';
document.getElementById('new-autonomous-channel-id').value = '';
document.getElementById('new-autonomous-channel-name').value = '';
document.getElementById('new-bedtime-channel-ids').value = '';
} catch (error) {
console.error('Failed to add server:', error);
}
}
async function removeServer(guildId) {
if (!confirm('Are you sure you want to remove this server?')) {
return;
}
try {
await apiCall(`/servers/${guildId}`, 'DELETE');
showNotification('Server removed successfully');
loadServers();
} catch (error) {
console.error('Failed to remove server:', error);
}
}
async function editServer(guildId) {
// For now, just show a notification - you can implement a full edit form later
showNotification('Edit functionality coming soon!');
}
async function repairConfig() {
if (!confirm('This will attempt to repair corrupted server configurations. Are you sure?')) {
return;
}
try {
await apiCall('/servers/repair', 'POST');
showNotification('Configuration repair initiated. Please refresh the page to see updated server list.');
loadServers(); // Reload servers to reflect potential changes
} catch (error) {
console.error('Failed to repair config:', error);
showNotification(error.message || 'Failed to repair configuration', 'error');
}
}
// Populate mood dropdowns with available moods
async function populateMoodDropdowns() {
try {
console.log('🎭 Loading available moods...');
const data = await apiCall('/moods/available');
console.log('🎭 Available moods response:', data);
if (data.moods) {
console.log(`🎭 Found ${data.moods.length} moods:`, data.moods);
// 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}`);
const button = document.querySelector(`button[onclick="resetServerMood('${guildIdStr}')"]`);
const originalText = button ? button.textContent : 'Reset';
try {
// Show loading state
if (button) {
button.textContent = 'Resetting...';
button.disabled = true;
}
await apiCall(`/servers/${guildIdStr}/mood/reset`, 'POST');
showNotification(`Server mood reset to neutral`);
// Reload servers to show updated mood
loadServers();
} catch (error) {
showNotification(`Failed to reset mood: ${error}`, 'error');
} finally {
// Restore button state
if (button) {
button.textContent = originalText;
button.disabled = false;
}
}
}
async function updateBedtimeRange(guildId) {
console.log(`⏰ updateBedtimeRange called with guildId: ${guildId}`);
// Ensure guildId is a string for consistency
const guildIdStr = String(guildId);
// Get the time values from the inputs
const startTimeInput = document.getElementById(`bedtime-start-${guildIdStr}`);
const endTimeInput = document.getElementById(`bedtime-end-${guildIdStr}`);
if (!startTimeInput || !endTimeInput) {
showNotification('Could not find bedtime time inputs', 'error');
return;
}
const startTime = startTimeInput.value; // Format: "HH:MM"
const endTime = endTimeInput.value; // Format: "HH:MM"
if (!startTime || !endTime) {
showNotification('Please enter both start and end times', 'error');
return;
}
// Parse the times
const [startHour, startMinute] = startTime.split(':').map(Number);
const [endHour, endMinute] = endTime.split(':').map(Number);
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
const originalText = button ? button.textContent : 'Update Bedtime Range';
try {
// Show loading state
if (button) {
button.textContent = 'Updating...';
button.disabled = true;
}
// Send the update request
await apiCall(`/servers/${guildIdStr}/bedtime-range`, 'POST', {
bedtime_hour: startHour,
bedtime_minute: startMinute,
bedtime_hour_end: endHour,
bedtime_minute_end: endMinute
});
showNotification(`Bedtime range updated: ${startTime} - ${endTime}`);
// Reload servers to show updated configuration
loadServers();
} catch (error) {
console.error('Failed to update bedtime range:', error);
} finally {
// Restore button state
if (button) {
button.textContent = originalText;
button.disabled = false;
}
}
}
// Mood Management
async function setMood() {
const mood = document.getElementById('mood').value;
try {
// Use different endpoint for evil mode
const endpoint = evilMode ? '/evil-mode/mood' : '/mood';
await apiCall(endpoint, 'POST', { mood: mood });
showNotification(`Mood set to ${mood}`);
currentMood = mood;
} catch (error) {
console.error('Failed to set mood:', error);
}
}
async function resetMood() {
try {
if (evilMode) {
await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' });
showNotification('Evil mood reset to evil_neutral');
currentMood = 'evil_neutral';
document.getElementById('mood').value = 'evil_neutral';
} else {
await apiCall('/mood/reset', 'POST');
showNotification('Mood reset to neutral');
currentMood = 'neutral';
document.getElementById('mood').value = 'neutral';
}
} catch (error) {
console.error('Failed to reset mood:', error);
}
}
async function calmMiku() {
try {
if (evilMode) {
await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' });
showNotification('Evil Miku has been calmed down');
currentMood = 'evil_neutral';
document.getElementById('mood').value = 'evil_neutral';
} else {
await apiCall('/mood/calm', 'POST');
showNotification('Miku has been calmed down');
}
} catch (error) {
console.error('Failed to calm Miku:', error);
}
}
// ===== Language Mode Functions =====
async function refreshLanguageStatus() {
try {
const result = await apiCall('/language');
document.getElementById('current-language-display').textContent =
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
document.getElementById('status-language').textContent =
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
document.getElementById('status-model').textContent = result.current_model;
console.log('Language status:', result);
} catch (error) {
console.error('Failed to get language status:', error);
showNotification('Failed to load language status', 'error');
}
}
async function toggleLanguageMode() {
try {
const result = await apiCall('/language/toggle', 'POST');
// Update UI
document.getElementById('current-language-display').textContent =
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
document.getElementById('status-language').textContent =
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
document.getElementById('status-model').textContent = result.model_now_using;
// Show notification
showNotification(result.message, 'success');
console.log('Language toggled:', result);
} catch (error) {
console.error('Failed to toggle language mode:', error);
showNotification('Failed to toggle language mode', 'error');
}
}
// Evil Mode Functions
async function checkEvilModeStatus() {
try {
const result = await apiCall('/evil-mode');
evilMode = result.evil_mode;
updateEvilModeUI();
// If evil mode is on, set the current evil mood in dropdown
if (evilMode && result.mood) {
const moodSelect = document.getElementById('mood');
moodSelect.value = result.mood;
}
} catch (error) {
console.error('Failed to check evil mode status:', error);
}
}
async function toggleEvilMode() {
try {
const toggleBtn = document.getElementById('evil-mode-toggle');
toggleBtn.disabled = true;
toggleBtn.textContent = '⏳ Switching...';
const result = await apiCall('/evil-mode/toggle', 'POST');
evilMode = result.evil_mode;
updateEvilModeUI();
if (evilMode) {
showNotification('😈 Evil Mode enabled! Evil Miku has awakened...');
} else {
showNotification('🎤 Evil Mode disabled. Normal Miku is back!');
}
} catch (error) {
console.error('Failed to toggle evil mode:', error);
showNotification('Failed to toggle evil mode: ' + error.message, 'error');
}
}
function updateEvilModeUI() {
const body = document.body;
const title = document.getElementById('panel-title');
const toggleBtn = document.getElementById('evil-mode-toggle');
const moodSelect = document.getElementById('mood');
if (evilMode) {
body.classList.add('evil-mode');
title.textContent = 'Evil Miku Control Panel';
toggleBtn.textContent = '😈 Evil Mode: ON';
toggleBtn.disabled = false;
// Switch mood dropdown to evil moods
moodSelect.innerHTML = `
<option value="aggressive">👿 aggressive</option>
<option value="bored">🥱 bored</option>
<option value="contemptuous">👑 contemptuous</option>
<option value="cunning">🐍 cunning</option>
<option value="evil_neutral" selected>evil neutral</option>
<option value="jealous">💚 jealous</option>
<option value="manic">🤪 manic</option>
<option value="melancholic">🌑 melancholic</option>
<option value="playful_cruel">🎭 playful cruel</option>
<option value="sarcastic">😈 sarcastic</option>
`;
} else {
body.classList.remove('evil-mode');
title.textContent = 'Miku Control Panel';
toggleBtn.textContent = '😈 Evil Mode: OFF';
toggleBtn.disabled = false;
// Switch mood dropdown back to normal moods
moodSelect.innerHTML = `
<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>
`;
}
// Update bipolar mode toggle visibility (always visible now)
updateBipolarToggleVisibility();
}
// GPU Selection Management
let selectedGPU = 'nvidia'; // 'nvidia' or 'amd'
async function checkGPUStatus() {
try {
const data = await apiCall('/gpu-status');
selectedGPU = data.gpu || 'nvidia';
updateGPUUI();
} catch (error) {
console.error('Failed to check GPU status:', error);
}
}
async function toggleGPU() {
try {
const toggleBtn = document.getElementById('gpu-selector-toggle');
toggleBtn.disabled = true;
toggleBtn.textContent = '⏳ Switching...';
const result = await apiCall('/gpu-select', 'POST', {
gpu: selectedGPU === 'nvidia' ? 'amd' : 'nvidia'
});
selectedGPU = result.gpu;
updateGPUUI();
const gpuName = selectedGPU === 'nvidia' ? 'NVIDIA GTX 1660' : 'AMD RX 6800';
showNotification(`🎮 Switched to ${gpuName}!`);
} catch (error) {
console.error('Failed to toggle GPU:', error);
showNotification('Failed to switch GPU: ' + error.message, 'error');
toggleBtn.disabled = false;
}
}
function updateGPUUI() {
const toggleBtn = document.getElementById('gpu-selector-toggle');
if (selectedGPU === 'amd') {
toggleBtn.textContent = '🎮 GPU: AMD';
toggleBtn.style.background = '#c91432';
toggleBtn.style.borderColor = '#e91436';
} else {
toggleBtn.textContent = '🎮 GPU: NVIDIA';
toggleBtn.style.background = '#2a5599';
toggleBtn.style.borderColor = '#4a7bc9';
}
toggleBtn.disabled = false;
}
// Bipolar Mode Management
let bipolarMode = false;
async function checkBipolarModeStatus() {
try {
const data = await apiCall('/bipolar-mode');
bipolarMode = data.bipolar_mode;
updateBipolarModeUI();
} catch (error) {
console.error('Failed to check bipolar mode status:', error);
}
}
async function toggleBipolarMode() {
try {
const toggleBtn = document.getElementById('bipolar-mode-toggle');
toggleBtn.disabled = true;
toggleBtn.textContent = '⏳ Switching...';
const result = await apiCall('/bipolar-mode/toggle', 'POST');
bipolarMode = result.bipolar_mode;
updateBipolarModeUI();
if (bipolarMode) {
showNotification('🔄 Bipolar Mode enabled! Both Mikus can now argue...');
} else {
showNotification('🔄 Bipolar Mode disabled.');
}
} catch (error) {
console.error('Failed to toggle bipolar mode:', error);
showNotification('Failed to toggle bipolar mode: ' + error.message, 'error');
}
}
function updateBipolarModeUI() {
const toggleBtn = document.getElementById('bipolar-mode-toggle');
const bipolarSection = document.getElementById('bipolar-section');
if (bipolarMode) {
toggleBtn.textContent = '🔄 Bipolar: ON';
toggleBtn.style.background = '#9932CC';
toggleBtn.style.borderColor = '#9932CC';
toggleBtn.disabled = false;
// Show bipolar controls section
if (bipolarSection) {
bipolarSection.style.display = 'block';
// Load scoreboard when section becomes visible
loadScoreboard();
}
} else {
toggleBtn.textContent = '🔄 Bipolar: OFF';
toggleBtn.style.background = '#333';
toggleBtn.style.borderColor = '#666';
toggleBtn.disabled = false;
// Hide bipolar controls section
if (bipolarSection) {
bipolarSection.style.display = 'none';
}
}
}
function updateBipolarToggleVisibility() {
const bipolarToggle = document.getElementById('bipolar-mode-toggle');
// Always show the bipolar mode toggle
bipolarToggle.style.display = 'block';
}
async function triggerPersonaDialogue() {
const messageIdInput = document.getElementById('dialogue-message-id').value.trim();
const statusDiv = document.getElementById('dialogue-status');
if (!messageIdInput) {
showNotification('Please enter a message ID', 'error');
return;
}
// Validate message ID format (should be numeric)
if (!/^\d+$/.test(messageIdInput)) {
showNotification('Invalid message ID format - should be a number', 'error');
return;
}
try {
statusDiv.innerHTML = '<span style="color: #6B8EFF;">⏳ Analyzing message for dialogue trigger...</span>';
const requestBody = {
message_id: messageIdInput
};
const result = await apiCall('/bipolar-mode/trigger-dialogue', 'POST', requestBody);
if (result.status === 'error') {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${result.message}</span>`;
showNotification(result.message, 'error');
return;
}
statusDiv.innerHTML = `<span style="color: #00ff00;">✅ ${result.message}</span>`;
showNotification(`💬 ${result.message}`);
// Clear the input
document.getElementById('dialogue-message-id').value = '';
} catch (error) {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ Failed to trigger dialogue: ${error.message}</span>`;
showNotification(`Error: ${error.message}`, 'error');
}
}
async function triggerBipolarArgument() {
const channelIdInput = document.getElementById('bipolar-channel-id').value.trim();
const messageIdInput = document.getElementById('bipolar-message-id').value.trim();
const context = document.getElementById('bipolar-context').value;
const statusDiv = document.getElementById('bipolar-status');
if (!channelIdInput) {
showNotification('Please enter a channel ID', 'error');
return;
}
// Validate channel ID format (should be numeric)
if (!/^\d+$/.test(channelIdInput)) {
showNotification('Invalid channel ID format - should be a number', 'error');
return;
}
// Validate message ID format if provided
if (messageIdInput && !/^\d+$/.test(messageIdInput)) {
showNotification('Invalid message ID format - should be a number', 'error');
return;
}
try {
statusDiv.innerHTML = '<span style="color: #9932CC;">⏳ Triggering argument...</span>';
const requestBody = {
channel_id: channelIdInput, // Send as string to avoid JS integer precision issues
context: context
};
if (messageIdInput) {
requestBody.message_id = messageIdInput; // Send as string
}
const result = await apiCall('/bipolar-mode/trigger-argument', 'POST', requestBody);
if (result.status === 'error') {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${result.message}</span>`;
showNotification(result.message, 'error');
return;
}
statusDiv.innerHTML = `<span style="color: #00ff00;">✅ ${result.message}</span>`;
showNotification(`⚔️ Argument triggered!`);
// Clear the inputs
document.getElementById('bipolar-context').value = '';
document.getElementById('bipolar-message-id').value = '';
// Refresh active arguments
loadActiveArguments();
// Refresh scoreboard after triggering
loadScoreboard();
} catch (error) {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${error.message}</span>`;
showNotification('Failed to trigger argument: ' + error.message, 'error');
}
}
async function loadScoreboard() {
const scoreboardContent = document.getElementById('scoreboard-content');
try {
const result = await apiCall('/bipolar-mode/scoreboard', 'GET');
if (result.status === 'error') {
scoreboardContent.innerHTML = `<p style="color: #ff4444;">Failed to load scoreboard</p>`;
return;
}
const { scoreboard } = result;
const total = scoreboard.total_arguments;
if (total === 0) {
scoreboardContent.innerHTML = `<p style="color: #888;">No arguments have been judged yet.</p>`;
return;
}
const mikuPct = total > 0 ? ((scoreboard.miku_wins / total) * 100).toFixed(1) : 0;
const evilPct = total > 0 ? ((scoreboard.evil_wins / total) * 100).toFixed(1) : 0;
let html = `
<div style="display: flex; justify-content: space-between; margin-bottom: 0.8rem;">
<div style="text-align: center; flex: 1;">
<div style="color: #86cecb; font-size: 1.2rem; font-weight: bold;">${scoreboard.miku_wins}</div>
<div style="color: #888; font-size: 0.85rem;">Hatsune Miku</div>
<div style="color: #999; font-size: 0.75rem;">${mikuPct}%</div>
</div>
<div style="align-self: center; color: #666; font-size: 1.2rem;">vs</div>
<div style="text-align: center; flex: 1;">
<div style="color: #D60004; font-size: 1.2rem; font-weight: bold;">${scoreboard.evil_wins}</div>
<div style="color: #888; font-size: 0.85rem;">Evil Miku</div>
<div style="color: #999; font-size: 0.75rem;">${evilPct}%</div>
</div>
</div>
<div style="text-align: center; color: #aaa; font-size: 0.85rem; border-top: 1px solid #333; padding-top: 0.5rem;">
Total Arguments: ${total}
</div>
`;
// Show recent history if available
if (scoreboard.history && scoreboard.history.length > 0) {
html += `<div style="margin-top: 0.8rem; padding-top: 0.8rem; border-top: 1px solid #333;">
<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.3rem;">Recent Results:</div>`;
scoreboard.history.reverse().forEach(entry => {
const winnerName = entry.winner === 'evil' ? 'Evil Miku' : 'Hatsune Miku';
const winnerColor = entry.winner === 'evil' ? '#D60004' : '#86cecb';
const date = new Date(entry.timestamp).toLocaleString();
html += `<div style="font-size: 0.75rem; color: #666; margin-bottom: 0.2rem;">
<span style="color: ${winnerColor};">🏆 ${winnerName}</span> (${entry.exchanges} exchanges) - ${date}
</div>`;
});
html += `</div>`;
}
scoreboardContent.innerHTML = html;
} catch (error) {
scoreboardContent.innerHTML = `<p style="color: #ff4444;">Error loading scoreboard</p>`;
console.error('Scoreboard error:', error);
}
}
async function loadActiveArguments() {
try {
const data = await apiCall('/bipolar-mode/arguments');
const container = document.getElementById('active-arguments');
const list = document.getElementById('active-arguments-list');
if (Object.keys(data.active_arguments).length > 0) {
container.style.display = 'block';
list.innerHTML = '';
for (const [channelId, argData] of Object.entries(data.active_arguments)) {
const div = document.createElement('div');
div.style.background = '#2a2a3e';
div.style.padding = '0.5rem';
div.style.marginBottom = '0.5rem';
div.style.borderRadius = '4px';
div.innerHTML = `
<strong>#${argData.channel_name}</strong><br>
<small>Exchanges: ${argData.exchange_count} | Speaker: ${argData.current_speaker}</small>
`;
list.appendChild(div);
}
} else {
container.style.display = 'none';
}
} catch (error) {
console.error('Failed to load active arguments:', error);
}
}
// Autonomous Actions
async function triggerAutonomous(actionType) {
const selectedServer = document.getElementById('server-select').value;
if (!actionType) {
showNotification('No action type specified', 'error');
return;
}
try {
let endpoint = `/autonomous/${actionType}`;
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
// Don't use parseInt() - Discord IDs are too large for JS integers
endpoint += `?guild_id=${selectedServer}`;
}
const result = await apiCall(endpoint, 'POST');
showNotification(result.message || 'Action triggered successfully');
} catch (error) {
console.error('Failed to trigger autonomous action:', error);
}
}
// Toggle Engage User Submenu
function toggleEngageSubmenu() {
const submenu = document.getElementById('engage-submenu');
if (submenu.style.display === 'none') {
submenu.style.display = 'block';
} else {
submenu.style.display = 'none';
}
}
// Trigger Engage User with parameters
async function triggerEngageUser() {
const selectedServer = document.getElementById('server-select').value;
const userId = document.getElementById('engage-user-id').value.trim();
const engageType = document.querySelector('input[name="engage-type"]:checked').value;
try {
let endpoint = '/autonomous/engage';
const params = new URLSearchParams();
// Add guild_id if a specific server is selected
if (selectedServer !== 'all') {
params.append('guild_id', selectedServer);
}
// Add user_id if specified
if (userId) {
params.append('user_id', userId);
}
// Add engagement_type if not random
if (engageType !== 'random') {
params.append('engagement_type', engageType);
}
// Add manual_trigger flag to bypass cooldown checks
params.append('manual_trigger', 'true');
if (params.toString()) {
endpoint += `?${params.toString()}`;
}
const result = await apiCall(endpoint, 'POST');
showNotification(result.message || 'Engagement triggered successfully');
// Optionally collapse the submenu after successful trigger
// toggleEngageSubmenu();
} catch (error) {
console.error('Failed to trigger user engagement:', error);
}
}
// Toggle Share Tweet Submenu
function toggleTweetSubmenu() {
const submenu = document.getElementById('tweet-submenu');
if (submenu.style.display === 'none') {
submenu.style.display = 'block';
} else {
submenu.style.display = 'none';
}
}
// Trigger Share Tweet with optional URL
async function triggerShareTweet() {
const selectedServer = document.getElementById('server-select').value;
const tweetUrl = document.getElementById('tweet-url').value.trim();
// Validate URL if provided
if (tweetUrl) {
const validDomains = ['x.com', 'twitter.com', 'fxtwitter.com'];
let isValid = false;
try {
const urlObj = new URL(tweetUrl);
const hostname = urlObj.hostname.toLowerCase();
isValid = validDomains.some(domain => hostname === domain || hostname.endsWith('.' + domain));
} catch (e) {
// Invalid URL format
}
if (!isValid) {
showNotification('Invalid tweet URL. Must be from x.com, twitter.com, or fxtwitter.com', 'error');
return;
}
}
try {
let endpoint = '/autonomous/tweet';
const params = new URLSearchParams();
// Add guild_id if a specific server is selected
if (selectedServer !== 'all') {
params.append('guild_id', selectedServer);
}
// Add tweet_url if specified
if (tweetUrl) {
params.append('tweet_url', tweetUrl);
}
if (params.toString()) {
endpoint += `?${params.toString()}`;
}
const result = await apiCall(endpoint, 'POST');
showNotification(result.message || 'Tweet share triggered successfully');
} catch (error) {
console.error('Failed to trigger tweet share:', error);
}
}
// 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 result = await apiCall(url, 'POST');
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!');
} catch (error) {
console.error('Failed to change profile picture:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
}
}
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 result = await apiCall('/profile-picture/restore-fallback', 'POST');
statusDiv.textContent = `${result.message}`;
statusDiv.style.color = 'green';
metadataDiv.style.display = 'none';
showNotification('Original avatar restored successfully!');
} catch (error) {
console.error('Failed to restore fallback avatar:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
}
}
// 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 result = await apiCall('/role-color/reset-fallback', 'POST');
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');
} catch (error) {
console.error('Failed to reset role color:', error);
statusDiv.textContent = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
}
}
// 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 toggleWebhookOptions() {
const useWebhook = document.getElementById('manual-use-webhook').checked;
const webhookOptions = document.getElementById('webhook-persona-options');
const targetType = document.getElementById('manual-target-type');
if (useWebhook) {
webhookOptions.style.display = 'block';
// Webhooks only work in channels, so switch to channel if DM is selected
if (targetType.value === 'dm') {
targetType.value = 'channel';
toggleManualMessageTarget();
}
// Disable DM option when webhook is enabled
targetType.options[1].disabled = true;
} else {
webhookOptions.style.display = 'none';
// Re-enable DM option
targetType.options[1].disabled = false;
}
}
function toggleManualMessageTarget() {
const targetType = document.getElementById('manual-target-type').value;
const channelSection = document.getElementById('manual-channel-section');
const dmSection = document.getElementById('manual-dm-section');
if (targetType === 'dm') {
channelSection.style.display = 'none';
dmSection.style.display = 'block';
} else {
channelSection.style.display = 'block';
dmSection.style.display = 'none';
}
}
async function sendCustomPrompt() {
const prompt = document.getElementById('customPrompt').value.trim();
const targetType = document.getElementById('custom-prompt-target-type').value;
const files = document.getElementById('customPromptAttachment').files;
if (!prompt) {
showNotification('Please enter a custom prompt', 'error');
return;
}
try {
let endpoint;
if (targetType === 'dm') {
// 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`;
} else {
// Server target
const selectedServer = document.getElementById('custom-prompt-server-select').value;
endpoint = '/autonomous/custom';
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
// Don't use parseInt() - Discord IDs are too large for JS integers
endpoint += `?guild_id=${selectedServer}`;
}
}
const result = await apiCall(endpoint, 'POST', { prompt: prompt });
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';
} catch (error) {
console.error('Failed to send custom prompt:', error);
document.getElementById('customStatus').textContent = '❌ Failed to send custom prompt';
document.getElementById('customStatus').style.color = 'red';
}
}
// Manual Actions
async function forceSleep() {
try {
await apiCall('/sleep', 'POST');
showNotification('Miku is now sleeping');
} catch (error) {
console.error('Failed to force sleep:', error);
}
}
async function wakeUp() {
try {
await apiCall('/wake', 'POST');
showNotification('Miku is now awake');
} catch (error) {
console.error('Failed to wake up:', error);
}
}
async function sendBedtime() {
const selectedServer = document.getElementById('manual-server-select').value;
// Debug logging
console.log('🛏️ sendBedtime() called');
console.log('🛏️ Selected server value:', selectedServer);
console.log('🛏️ Selected server type:', typeof selectedServer);
try {
let endpoint = '/bedtime';
// Add guild_id as query parameter if a specific server is selected
if (selectedServer !== 'all') {
// IMPORTANT: Don't use parseInt() - it causes precision loss!
// Keep as string since Discord IDs are too large for JS integers
console.log('🛏️ Using guild_id (as string):', selectedServer);
endpoint += `?guild_id=${selectedServer}`;
}
console.log('🛏️ Final endpoint:', endpoint);
const result = await apiCall(endpoint, 'POST');
showNotification(result.message || 'Bedtime reminder sent successfully');
} catch (error) {
console.error('Failed to send bedtime reminder:', error);
}
}
async function resetConversation() {
const userId = prompt('Enter user ID to reset conversation for:');
if (userId) {
try {
await apiCall('/conversation/reset', 'POST', { user_id: userId });
showNotification('Conversation reset');
} catch (error) {
console.error('Failed to reset conversation:', error);
}
}
}
// Manual Message
async function sendManualMessage() {
const message = document.getElementById('manualMessage').value.trim();
const files = document.getElementById('manualAttachment').files;
const targetType = document.getElementById('manual-target-type').value;
const replyMessageId = document.getElementById('manualReplyMessageId').value.trim();
const replyMention = document.querySelector('input[name="manualReplyMention"]:checked').value === 'true';
const useWebhook = document.getElementById('manual-use-webhook').checked;
const webhookPersona = document.querySelector('input[name="webhook-persona"]:checked')?.value || 'miku';
if (!message) {
showNotification('Please enter a message', 'error');
return;
}
// Webhooks only work in channels
if (useWebhook && targetType === 'dm') {
showNotification('Webhooks only work in channels, not DMs', 'error');
return;
}
let targetId, endpoint;
if (targetType === 'dm') {
targetId = document.getElementById('manualUserId').value.trim();
if (!targetId) {
showNotification('Please enter a user ID for DM', 'error');
return;
}
endpoint = `/dm/${targetId}/manual`;
} else {
targetId = document.getElementById('manualChannelId').value.trim();
if (!targetId) {
showNotification('Please enter a channel ID', 'error');
return;
}
// Use webhook endpoint if webhook is enabled
endpoint = useWebhook ? '/manual/send-webhook' : '/manual/send';
}
try {
const formData = new FormData();
formData.append('message', message);
// Add webhook persona if using webhook
if (useWebhook) {
formData.append('persona', webhookPersona);
}
// Add reply parameters if message ID is provided
if (replyMessageId) {
formData.append('reply_to_message_id', replyMessageId);
formData.append('mention_author', replyMention);
}
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
document.getElementById('manualReplyMessageId').value = ''; // Clear reply message ID
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 result = await apiCall('/image/status');
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' : ''}`;
} catch (error) {
console.error('Failed to check image system status:', error);
document.getElementById('image-status-display').innerHTML = `❌ Error: ${error.message}`;
}
}
async function testImageDetection() {
const message = document.getElementById('detection-test-message').value.trim();
const resultsDiv = document.getElementById('detection-test-results');
if (!message) {
resultsDiv.innerHTML = '❌ Please enter a test message';
resultsDiv.style.color = 'red';
return;
}
try {
resultsDiv.innerHTML = '🔍 Testing detection...';
resultsDiv.style.color = '#4CAF50';
const result = await apiCall('/image/test-detection', 'POST', { message: message });
const detectionIcon = result.is_image_request ? '✅' : '❌';
const detectionText = result.is_image_request ? 'WILL trigger image generation' : 'will NOT trigger image generation';
resultsDiv.innerHTML = `
<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';
} catch (error) {
console.error('Failed to test image detection:', error);
resultsDiv.innerHTML = `❌ Error: ${error.message}`;
resultsDiv.style.color = 'red';
}
}
async function generateManualImage() {
const prompt = document.getElementById('manual-image-prompt').value.trim();
const statusDiv = document.getElementById('manual-image-status');
const previewDiv = document.getElementById('manual-image-preview');
if (!prompt) {
statusDiv.innerHTML = '❌ Please enter an image prompt';
statusDiv.style.color = 'red';
return;
}
try {
// Clear previous preview
previewDiv.innerHTML = '';
statusDiv.innerHTML = '🎨 Generating image... This may take a few minutes.';
statusDiv.style.color = '#4CAF50';
const result = await apiCall('/image/generate', 'POST', { prompt: prompt });
statusDiv.innerHTML = `✅ Image generated successfully!`;
statusDiv.style.color = '#4CAF50';
// Display the generated image
if (result.image_path) {
const filename = result.image_path.split('/').pop();
const imageUrl = `/image/view/${encodeURIComponent(filename)}`;
// Create image element with better error handling
const imgContainer = document.createElement('div');
imgContainer.style.cssText = 'background: #1e1e1e; padding: 1rem; border-radius: 8px; border: 1px solid #333;';
const img = document.createElement('img');
img.src = imageUrl;
img.alt = 'Generated Image';
img.style.cssText = 'max-width: 100%; max-height: 600px; border-radius: 4px; display: block; margin: 0 auto;';
img.onload = function() {
console.log('Image loaded successfully:', imageUrl);
};
img.onerror = function() {
console.error('Failed to load image:', imageUrl);
imgContainer.innerHTML = `
<div style="color: #f44336; padding: 1rem; text-align: center;">
❌ Failed to load image<br>
<span style="font-size: 0.85rem;">Path: ${result.image_path}</span><br>
<span style="font-size: 0.85rem;">URL: ${imageUrl}</span>
</div>
`;
};
imgContainer.appendChild(img);
const pathInfo = document.createElement('div');
pathInfo.style.cssText = 'margin-top: 0.5rem; color: #aaa; font-size: 0.85rem; text-align: center;';
pathInfo.innerHTML = `<strong>File:</strong> ${filename}`;
imgContainer.appendChild(pathInfo);
previewDiv.appendChild(imgContainer);
}
document.getElementById('manual-image-prompt').value = '';
} catch (error) {
console.error('Failed to generate image:', error);
statusDiv.innerHTML = `❌ Error: ${error.message}`;
statusDiv.style.color = 'red';
}
}
// Status and Info
async function loadStatus() {
try {
const result = await apiCall('/status');
const statusDiv = document.getElementById('status');
// Sync evil mode state from server (may change via Discord commands)
if (result.evil_mode !== undefined && result.evil_mode !== evilMode) {
evilMode = result.evil_mode;
updateEvilModeUI();
if (evilMode && result.mood) {
const moodSelect = document.getElementById('mood');
if (moodSelect) moodSelect.value = result.mood;
}
}
// Update mood dropdown selection to match current server mood
if (result.mood) {
const moodSelect = document.getElementById('mood');
if (moodSelect && moodSelect.querySelector(`option[value="${result.mood}"]`)) {
moodSelect.value = result.mood;
}
currentMood = result.mood;
}
let serverMoodsHtml = '';
if (result.server_moods) {
serverMoodsHtml = '<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}`;
const emojiMap = evilMode ? EVIL_MOOD_EMOJIS : MOOD_EMOJIS;
serverMoodsHtml += `${serverName}: ${mood} ${emojiMap[mood] || ''}<br>`;
}
serverMoodsHtml += '</div>';
}
const moodEmoji = evilMode ? (EVIL_MOOD_EMOJIS[result.mood] || '') : (MOOD_EMOJIS[result.mood] || '');
const moodLabel = evilMode ? `😈 ${result.mood} ${moodEmoji}` : `${result.mood} ${moodEmoji}`;
statusDiv.innerHTML = `
<div><strong>Status:</strong> ${result.status}</div>
<div><strong>DM Mood:</strong> ${moodLabel}</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);
}
}
// Log auto-scroll state
let logsAutoScroll = true;
function initLogsScrollDetection() {
const logsPanel = document.getElementById('logs-panel');
if (!logsPanel) return;
logsPanel.addEventListener('scroll', function() {
// If user is within 50px of the bottom, re-enable auto-scroll
const atBottom = logsPanel.scrollHeight - logsPanel.scrollTop - logsPanel.clientHeight < 50;
logsAutoScroll = atBottom;
const banner = document.getElementById('logs-paused-banner');
if (banner) banner.style.display = atBottom ? 'none' : 'block';
});
}
function scrollLogsToBottom() {
const logsPanel = document.getElementById('logs-panel');
if (logsPanel) {
logsPanel.scrollTop = logsPanel.scrollHeight;
logsAutoScroll = true;
const banner = document.getElementById('logs-paused-banner');
if (banner) banner.style.display = 'none';
}
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function classifyLogLine(line) {
const upper = line.toUpperCase();
if (upper.includes(' ERROR ') || upper.includes(' CRITICAL ') || upper.startsWith('ERROR') || upper.startsWith('CRITICAL') || upper.includes('TRACEBACK')) return 'log-error';
if (upper.includes(' WARNING ') || upper.startsWith('WARNING')) return 'log-warning';
if (upper.includes(' DEBUG ') || upper.startsWith('DEBUG')) return 'log-debug';
return 'log-info';
}
async function loadLogs() {
try {
const result = await apiCall('/logs');
const logsContent = document.getElementById('logs-content');
const lines = (result || '').split('\n');
logsContent.innerHTML = lines.map(line => {
if (!line.trim()) return '';
const cls = classifyLogLine(line);
return `<div class="log-line ${cls}">${escapeHtml(line)}</div>`;
}).join('');
// Auto-scroll to bottom if user hasn't scrolled up
if (logsAutoScroll) {
scrollLogsToBottom();
}
} 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 result = await apiCall('/dms/users');
displayDMUsers(result.users);
} catch (error) {
console.error('Failed to load DM users:', error);
}
}
function displayDMUsers(users) {
const container = document.getElementById('dm-users-list');
if (!users || users.length === 0) {
container.innerHTML = '<p>No DM conversations found.</p>';
return;
}
let html = '<div class="dm-users-grid">';
users.forEach(user => {
console.log(`👤 Processing user: ${user.username} (ID: ${user.user_id})`);
const lastMessage = user.last_message ?
`Last: ${user.last_message.content}` :
'No messages yet';
const lastTime = user.last_message ?
new Date(user.last_message.timestamp).toLocaleString() :
'Never';
html += `
<div class="dm-user-card">
<h4>👤 ${user.username}</h4>
<p><strong>ID:</strong> ${user.user_id}</p>
<p><strong>Total Messages:</strong> ${user.total_messages}</p>
<p><strong>User Messages:</strong> ${user.user_messages}</p>
<p><strong>Bot Messages:</strong> ${user.bot_messages}</p>
<p><strong>Last Activity:</strong> ${lastTime}</p>
<p><strong>Last Message:</strong> ${lastMessage}</p>
<div class="dm-user-actions">
<button class="view-chat-btn" data-user-id="${user.user_id}">💬 View Chat</button>
<button class="analyze-user-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #9c27b0;">📊 Analyze</button>
<button class="export-dms-btn" data-user-id="${user.user_id}">📤 Export</button>
<button class="block-user-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #ff9800;">🚫 Block</button>
<button class="delete-all-dms-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #f44336;">🗑️ Delete All</button>
<button class="delete-user-completely-btn" data-user-id="${user.user_id}" data-username="${user.username}" style="background: #d32f2f;">💀 Delete User</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// Add event listeners after HTML is inserted
addDMUserEventListeners();
}
function addDMUserEventListeners() {
// Add event listeners for view chat buttons
document.querySelectorAll('.view-chat-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 View chat clicked for user ID: ${userId} (type: ${typeof userId})`);
viewUserConversations(userId);
});
});
// Add event listeners for export buttons
document.querySelectorAll('.export-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
console.log(`🎯 Export clicked for user ID: ${userId} (type: ${typeof userId})`);
exportUserDMs(userId);
});
});
// Add event listeners for analyze buttons
document.querySelectorAll('.analyze-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Analyze clicked for user ID: ${userId} (type: ${typeof userId})`);
analyzeUserInteraction(userId, username);
});
});
// Add event listeners for block buttons
document.querySelectorAll('.block-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Block clicked for user ID: ${userId} (type: ${typeof userId})`);
blockUser(userId, username);
});
});
// Add event listeners for delete all DMs buttons
document.querySelectorAll('.delete-all-dms-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete all DMs clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteAllUserConversations(userId, username);
});
});
// Add event listeners for delete user completely buttons
document.querySelectorAll('.delete-user-completely-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log(`🎯 Delete user completely clicked for user ID: ${userId} (type: ${typeof userId})`);
deleteUserCompletely(userId, username);
});
});
}
async function viewUserConversations(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
console.log(`🔍 Loading conversations for user ${userIdStr} (type: ${typeof userIdStr})`);
console.log(`🔍 Original userId: ${userId} (type: ${typeof userId})`);
console.log(`🔍 userIdStr: ${userIdStr} (type: ${typeof userIdStr})`);
const result = await apiCall(`/dms/users/${userIdStr}/conversations?limit=100`);
console.log('📡 API Response:', result);
console.log('📡 API URL called:', `/dms/users/${userIdStr}/conversations?limit=100`);
if (result.conversations && result.conversations.length > 0) {
console.log(`✅ Found ${result.conversations.length} conversations`);
displayUserConversations(userIdStr, result.conversations);
} else {
console.log('⚠️ No conversations found in response');
showNotification('No conversations found for this user', 'info');
// Go back to user list
loadDMUsers();
}
} catch (error) {
console.error('Failed to load user conversations:', error);
}
}
function displayUserConversations(userId, conversations) {
console.log(`🎨 Displaying conversations for user ${userId}:`, conversations);
// Create a modal or expand the user card to show conversations
const container = document.getElementById('dm-users-list');
let html = `
<div class="conversation-view">
<button onclick="loadDMUsers()" style="margin-bottom: 1rem;">← Back to DM Users</button>
<h4>💬 Conversations with User ${userId}</h4>
<div class="conversations-list">
`;
if (!conversations || conversations.length === 0) {
html += '<p>No conversations found for this user.</p>';
} else {
conversations.forEach((msg, index) => {
console.log(`📝 Processing message ${index}:`, msg);
const timestamp = new Date(msg.timestamp).toLocaleString();
const sender = msg.is_bot_message ? '🤖 Miku' : '👤 User';
const content = msg.content || '[No text content]';
const messageId = msg.message_id || msg.timestamp; // Use message_id or timestamp as identifier
const escapedContent = content.replace(/'/g, "\\'").replace(/"/g, '\\"');
// Debug: Log message details
console.log(`📝 Message ${index}: id=${messageId}, is_bot=${msg.is_bot_message}, content="${content.substring(0, 30)}..."`);
// Only show delete button for bot messages (Miku can only delete her own messages)
const deleteButton = msg.is_bot_message ?
`<button class="delete-message-btn" onclick="deleteConversation('${userId}', '${messageId}', '${escapedContent}')"
style="background: #f44336; color: white; border: none; padding: 2px 6px; font-size: 12px; border-radius: 3px; margin-left: 10px;"
title="Delete this Miku message (ID: ${messageId})">
🗑️ Delete
</button>` : '';
html += `
<div class="conversation-message ${msg.is_bot_message ? 'bot-message' : 'user-message'}">
<div class="message-header">
<span class="sender">${sender}</span>
<span class="timestamp">${timestamp}</span>
${deleteButton}
</div>
<div class="message-content">${content}</div>
${msg.attachments && msg.attachments.length > 0 ? `
<div class="message-attachments">
<strong>📎 Attachments:</strong>
${msg.attachments.map(att => `
<div class="attachment">
- ${att.filename} (${att.size} bytes)
<a href="${att.url}" target="_blank">🔗 View</a>
</div>
`).join('')}
</div>
` : ''}
${msg.reactions && msg.reactions.length > 0 ? `
<div class="message-reactions">
${msg.reactions.map(reaction => {
const reactionTime = new Date(reaction.added_at).toLocaleString();
const reactorType = reaction.is_bot ? 'bot-reaction' : 'user-reaction';
const reactorLabel = reaction.is_bot ? '🤖 Miku' : `👤 ${reaction.reactor_name}`;
return `
<div class="reaction-item" title="${reactorLabel} reacted at ${reactionTime}">
<span class="reaction-emoji">${reaction.emoji}</span>
<span class="reaction-by ${reactorType}">${reactorLabel}</span>
</div>
`;
}).join('')}
</div>
` : ''}
</div>
`;
});
}
html += `
</div>
</div>
`;
console.log('🎨 Generated HTML:', html);
container.innerHTML = html;
}
async function exportUserDMs(userId) {
try {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
await apiCall(`/dms/users/${userIdStr}/export?format=txt`);
showNotification(`DM export completed for user ${userIdStr}`);
// You could trigger a download here if the file is accessible
} catch (error) {
console.error('Failed to export user DMs:', error);
}
}
async function deleteUserDMs(userId) {
// Ensure userId is always treated as a string
const userIdStr = String(userId);
if (!confirm(`Are you sure you want to delete all DM logs for user ${userIdStr}? This action cannot be undone.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}`, 'DELETE');
showNotification(`Deleted DM logs for user ${userIdStr}`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to delete user DMs:', error);
}
}
// ========== User Blocking & Advanced Deletion Functions ==========
async function blockUser(userId, username) {
const userIdStr = String(userId);
if (!confirm(`Are you sure you want to block ${username} (${userIdStr}) from sending DMs to Miku?`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/block`, 'POST');
showNotification(`${username} has been blocked from sending DMs`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to block user:', error);
}
}
async function unblockUser(userId, username) {
const userIdStr = String(userId);
try {
await apiCall(`/dms/users/${userIdStr}/unblock`, 'POST');
showNotification(`${username} has been unblocked`);
loadBlockedUsers(); // Refresh blocked users list
} catch (error) {
console.error('Failed to unblock user:', error);
}
}
async function deleteAllUserConversations(userId, username) {
const userIdStr = String(userId);
if (!confirm(`⚠️ DELETE ALL CONVERSATIONS with ${username} (${userIdStr})?\n\nThis will:\n• Delete ALL Miku messages from Discord DM\n• Clear all conversation logs\n• Keep the user record\n\nThis action CANNOT be undone!\n\nClick OK to confirm deletion.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/conversations/delete-all`, 'POST');
showNotification(`Bulk deletion queued for ${username} (deleting all Miku messages from Discord and logs)`);
setTimeout(() => {
loadDMUsers(); // Refresh after a delay to allow deletion to process
}, 2000);
} catch (error) {
console.error('Failed to delete conversations:', error);
}
}
async function deleteUserCompletely(userId, username) {
const userIdStr = String(userId);
if (!confirm(`🚨 COMPLETELY DELETE USER ${username} (${userIdStr})?\n\nThis will:\n• Delete ALL conversation history\n• Delete the entire user log file\n• Remove ALL traces of this user\n\nThis action is PERMANENT and CANNOT be undone!\n\nType "${username}" below to confirm:`)) {
return;
}
const confirmName = prompt(`Type the username "${username}" to confirm complete deletion:`);
if (confirmName !== username) {
showNotification('Deletion cancelled - username did not match', 'error');
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/delete-completely`, 'POST');
showNotification(`${username} has been completely deleted from the system`);
loadDMUsers(); // Refresh the list
} catch (error) {
console.error('Failed to delete user completely:', error);
}
}
async function deleteConversation(userId, conversationId, messageContent) {
const userIdStr = String(userId);
if (!confirm(`Delete this Miku message from Discord and logs?\n\n"${messageContent.substring(0, 100)}${messageContent.length > 100 ? '...' : ''}"\n\nThis will:\n• Delete the message from Discord DM\n• Remove it from conversation logs\n\nNote: Only Miku's messages can be deleted.\nThis action cannot be undone.`)) {
return;
}
try {
await apiCall(`/dms/users/${userIdStr}/conversations/${conversationId}/delete`, 'POST');
showNotification('Miku message deletion queued (deleting from both Discord and logs)');
setTimeout(() => {
viewUserConversations(userId); // Refresh after a short delay to allow deletion to process
}, 1000);
} catch (error) {
console.error('Failed to delete conversation:', error);
}
}
async function analyzeUserInteraction(userId, username) {
const userIdStr = String(userId);
if (!confirm(`Run DM interaction analysis for ${username}?\n\nThis will:\n• Analyze their messages from the last 24 hours\n• Generate a sentiment report\n• Send report to bot owner\n\nMinimum 3 messages required for analysis.`)) {
return;
}
try {
showNotification(`Analyzing ${username}'s interactions...`, 'info');
const result = await apiCall(`/dms/users/${userIdStr}/analyze`, 'POST');
if (result.reported) {
showNotification(`✅ Analysis complete! Report sent to bot owner for ${username}`);
} else {
showNotification(`📊 Analysis complete for ${username} (not enough messages or already reported today)`);
}
} catch (error) {
console.error('Failed to analyze user:', error);
}
}
async function runDailyAnalysis() {
if (!confirm('Run the daily DM interaction analysis now?\n\nThis will:\n• Analyze all DM users from the last 24 hours\n• Report one significant interaction to the bot owner\n• Skip users already reported today\n\nNote: This runs automatically at 2 AM daily.')) {
return;
}
try {
showNotification('Starting DM interaction analysis...', 'info');
await apiCall('/dms/analysis/run', 'POST');
showNotification('✅ DM analysis completed! Check bot owner\'s DMs for any reports.');
} catch (error) {
console.error('Failed to run DM analysis:', error);
}
}
async function viewAnalysisReports() {
try {
showNotification('Loading analysis reports...', 'info');
const result = await apiCall('/dms/analysis/reports?limit=50');
displayAnalysisReports(result.reports);
} catch (error) {
console.error('Failed to load reports:', error);
}
}
function displayAnalysisReports(reports) {
const container = document.getElementById('dm-users-list');
if (!reports || reports.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<p>No analysis reports found yet.</p>
<button onclick="loadDMUsers()" style="margin-top: 1rem;">← Back to DM Users</button>
</div>
`;
return;
}
let html = `
<div style="margin-bottom: 1rem;">
<button onclick="loadDMUsers()">← Back to DM Users</button>
<span style="margin-left: 1rem; color: #aaa;">${reports.length} reports found</span>
</div>
<div style="display: grid; gap: 1rem;">
`;
reports.forEach(report => {
const sentimentColor =
report.sentiment_score >= 5 ? '#4caf50' :
report.sentiment_score <= -3 ? '#f44336' :
'#2196f3';
const sentimentEmoji =
report.sentiment_score >= 5 ? '😊' :
report.sentiment_score <= -3 ? '😢' :
'😐';
const timestamp = new Date(report.analyzed_at).toLocaleString();
html += `
<div style="background: #2a2a2a; border-left: 4px solid ${sentimentColor}; padding: 1rem; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;">
<div>
<h4 style="margin: 0 0 0.25rem 0;">${sentimentEmoji} ${report.username}</h4>
<p style="margin: 0; font-size: 0.85rem; color: #aaa;">User ID: ${report.user_id}</p>
</div>
<div style="text-align: right;">
<div style="font-size: 1.2rem; font-weight: bold; color: ${sentimentColor};">
${report.sentiment_score > 0 ? '+' : ''}${report.sentiment_score}/10
</div>
<div style="font-size: 0.75rem; color: #aaa; text-transform: uppercase;">
${report.overall_sentiment}
</div>
</div>
</div>
<div style="margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;">
<strong>Miku's Feelings:</strong>
<p style="margin: 0.5rem 0 0 0; font-style: italic;">"${report.your_feelings}"</p>
</div>
${report.notable_moment ? `
<div style="margin: 0.75rem 0; padding: 0.75rem; background: #1e1e1e; border-radius: 4px;">
<strong>Notable Moment:</strong>
<p style="margin: 0.5rem 0 0 0; font-style: italic;">"${report.notable_moment}"</p>
</div>
` : ''}
${report.key_behaviors && report.key_behaviors.length > 0 ? `
<div style="margin: 0.75rem 0;">
<strong>Key Behaviors:</strong>
<ul style="margin: 0.5rem 0 0 0; padding-left: 1.5rem;">
${report.key_behaviors.slice(0, 5).map(b => `<li>${b}</li>`).join('')}
</ul>
</div>
` : ''}
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #444; font-size: 0.8rem; color: #aaa;">
<span>📅 ${timestamp}</span>
<span style="margin-left: 1rem;">💬 ${report.message_count} messages analyzed</span>
<span style="margin-left: 1rem;">📄 ${report.filename}</span>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function loadBlockedUsers() {
try {
const result = await apiCall('/dms/blocked-users');
// Hide DM users list and show blocked users section
document.getElementById('dm-users-list').style.display = 'none';
document.getElementById('blocked-users-section').style.display = 'block';
displayBlockedUsers(result.blocked_users);
} catch (error) {
console.error('Failed to load blocked users:', error);
}
}
function hideBlockedUsers() {
// Show DM users list and hide blocked users section
document.getElementById('dm-users-list').style.display = 'block';
document.getElementById('blocked-users-section').style.display = 'none';
loadDMUsers(); // Refresh DM users
}
function displayBlockedUsers(blockedUsers) {
const container = document.getElementById('blocked-users-list');
if (!blockedUsers || blockedUsers.length === 0) {
container.innerHTML = '<p>No blocked users.</p>';
return;
}
let html = '<div class="blocked-users-grid">';
blockedUsers.forEach(user => {
html += `
<div class="blocked-user-card">
<h4>🚫 ${user.username}</h4>
<p><strong>ID:</strong> ${user.user_id}</p>
<p><strong>Blocked:</strong> ${new Date(user.blocked_at).toLocaleString()}</p>
<p><strong>Blocked by:</strong> ${user.blocked_by}</p>
<div class="blocked-user-actions">
<button onclick="unblockUser('${user.user_id}', '${user.username}')" style="background: #4caf50;">✅ Unblock</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
async function exportAllDMs() {
try {
const result = await apiCall('/dms/users');
let exportCount = 0;
for (const user of (result.users || [])) {
try {
await exportUserDMs(user.user_id);
exportCount++;
} catch (e) {
console.error(`Failed to export DMs for user ${user.user_id}:`, e);
}
}
showNotification(`Exported DMs for ${exportCount} users`);
} catch (error) {
console.error('Failed to export all DMs:', error);
}
}
// ========== 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 data = await apiCall('/autonomous/stats');
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);
}
}
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;
}
}
// ========== Chat Interface Functions ==========
// Store conversation history for context
let chatConversationHistory = [];
// Toggle image upload section based on model type
function toggleChatImageUpload() {
const modelType = document.querySelector('input[name="chat-model-type"]:checked').value;
const imageUploadSection = document.getElementById('chat-image-upload-section');
if (modelType === 'vision') {
imageUploadSection.style.display = 'block';
} else {
imageUploadSection.style.display = 'none';
}
}
// (Chat image preview + voice debug mode init moved to consolidated DOMContentLoaded)
// Load voice debug mode setting from server
async function loadVoiceDebugMode() {
try {
const data = await apiCall('/voice/debug-mode');
const checkbox = document.getElementById('voice-debug-mode');
if (checkbox && data.debug_mode !== undefined) {
checkbox.checked = data.debug_mode;
}
} catch (error) {
console.error('Failed to load voice debug mode:', error);
}
}
// Handle Enter key in chat input
function handleChatKeyPress(event) {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
sendChatMessage();
}
}
// Clear chat history
function clearChatHistory() {
if (confirm('Are you sure you want to clear all chat messages?')) {
const chatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = `
<div style="text-align: center; color: #888; padding: 2rem;">
💬 Start chatting with the LLM! Your conversation will appear here.
</div>
`;
// Clear conversation history array
chatConversationHistory = [];
showNotification('Chat history cleared');
}
}
// Add a message to the chat display
function addChatMessage(sender, content, isError = false) {
const chatMessages = document.getElementById('chat-messages');
// Remove welcome message if it exists
const welcomeMsg = chatMessages.querySelector('div[style*="text-align: center"]');
if (welcomeMsg) {
welcomeMsg.remove();
}
const messageDiv = document.createElement('div');
const messageClass = isError ? 'error-message' : (sender === 'You' ? 'user-message' : 'assistant-message');
messageDiv.className = `chat-message ${messageClass}`;
const timestamp = new Date().toLocaleTimeString();
messageDiv.innerHTML = `
<div class="chat-message-header">
<span class="chat-message-sender">${escapeHtml(sender)}</span>
<span class="chat-message-time">${timestamp}</span>
</div>
<div class="chat-message-content"></div>
`;
// Set content via textContent to prevent XSS
messageDiv.querySelector('.chat-message-content').textContent = content;
chatMessages.appendChild(messageDiv);
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
}
// Add typing indicator
function showTypingIndicator() {
const chatMessages = document.getElementById('chat-messages');
const typingDiv = document.createElement('div');
typingDiv.id = 'chat-typing-indicator';
typingDiv.className = 'chat-message assistant-message';
typingDiv.innerHTML = `
<div class="chat-message-header">
<span class="chat-message-sender">Miku</span>
<span class="chat-message-time">typing...</span>
</div>
<div class="chat-typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
`;
chatMessages.appendChild(typingDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Remove typing indicator
function hideTypingIndicator() {
const typingIndicator = document.getElementById('chat-typing-indicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
// Send chat message with streaming support
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
showNotification('Please enter a message', 'error');
return;
}
// Get configuration
const modelType = document.querySelector('input[name="chat-model-type"]:checked').value;
const useSystemPrompt = document.querySelector('input[name="chat-system-prompt"]:checked').value === 'true';
const selectedMood = document.getElementById('chat-mood-select').value;
// Get image data if vision model
let imageData = null;
if (modelType === 'vision') {
const imageFile = document.getElementById('chat-image-file').files[0];
if (imageFile) {
try {
imageData = await readFileAsBase64(imageFile);
// Remove data URL prefix if present
if (imageData.includes(',')) {
imageData = imageData.split(',')[1];
}
} catch (error) {
showNotification('Failed to read image file', 'error');
return;
}
}
}
// Disable send button
const sendBtn = document.getElementById('chat-send-btn');
const originalBtnText = sendBtn.innerHTML;
sendBtn.disabled = true;
sendBtn.innerHTML = '⏳ Sending...';
// Add user message to display
addChatMessage('You', message);
// Clear input
input.value = '';
// Show typing indicator
showTypingIndicator();
try {
// Build user message for history
let userMessageContent;
if (modelType === 'vision' && imageData) {
// Vision model with image - store as multimodal content
userMessageContent = [
{
"type": "text",
"text": message
},
{
"type": "image_url",
"image_url": {
"url": `data:image/jpeg;base64,${imageData}`
}
}
];
} else {
// Text-only message
userMessageContent = message;
}
// Prepare request payload with conversation history
const payload = {
message: message,
model_type: modelType,
use_system_prompt: useSystemPrompt,
image_data: imageData,
conversation_history: chatConversationHistory,
mood: selectedMood
};
// Make streaming request
const response = await fetch('/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Hide typing indicator
hideTypingIndicator();
// Create message element for streaming response
const assistantName = useSystemPrompt ? 'Miku' : 'LLM';
const responseDiv = addChatMessage(assistantName, '');
const contentDiv = responseDiv.querySelector('.chat-message-content');
// Read stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete SSE messages
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6);
try {
const data = JSON.parse(dataStr);
if (data.error) {
contentDiv.textContent = `❌ Error: ${data.error}`;
responseDiv.classList.add('error-message');
break;
}
if (data.content) {
fullResponse += data.content;
contentDiv.textContent = fullResponse;
// Auto-scroll
const chatMessages = document.getElementById('chat-messages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
if (data.done) {
break;
}
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
}
}
}
// If no response was received, show error
if (!fullResponse) {
contentDiv.textContent = '❌ No response received from LLM';
responseDiv.classList.add('error-message');
} else {
// Add user message to conversation history
chatConversationHistory.push({
role: "user",
content: userMessageContent
});
// Add assistant response to conversation history
chatConversationHistory.push({
role: "assistant",
content: fullResponse
});
console.log('💬 Conversation history updated:', chatConversationHistory.length, 'messages');
}
} catch (error) {
console.error('Chat error:', error);
hideTypingIndicator();
addChatMessage('Error', `Failed to send message: ${error.message}`, true);
showNotification('Failed to send message', 'error');
} finally {
// Re-enable send button
sendBtn.disabled = false;
sendBtn.innerHTML = originalBtnText;
}
}
// Helper function to read file as base64
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// ============================================================================
// Voice Call Management Functions
// ============================================================================
async function initiateVoiceCall() {
const userId = document.getElementById('voice-user-id').value.trim();
const channelId = document.getElementById('voice-channel-id').value.trim();
const debugMode = document.getElementById('voice-debug-mode').checked;
// Validation
if (!userId) {
showNotification('Please enter a user ID', 'error');
return;
}
if (!channelId) {
showNotification('Please enter a voice channel ID', 'error');
return;
}
// Check if user IDs are valid (numeric)
if (isNaN(userId) || isNaN(channelId)) {
showNotification('User ID and Channel ID must be numeric', 'error');
return;
}
// Set debug mode
try {
const debugFormData = new FormData();
debugFormData.append('enabled', debugMode);
await fetch('/voice/debug-mode', {
method: 'POST',
body: debugFormData
});
} catch (error) {
console.error('Failed to set debug mode:', error);
}
// Disable button and show status
const callBtn = document.getElementById('voice-call-btn');
const cancelBtn = document.getElementById('voice-call-cancel-btn');
const statusDiv = document.getElementById('voice-call-status');
const statusText = document.getElementById('voice-call-status-text');
callBtn.disabled = true;
statusDiv.style.display = 'block';
cancelBtn.style.display = 'inline-block';
voiceCallActive = true;
try {
statusText.innerHTML = '⏳ Starting STT and TTS containers...';
const formData = new FormData();
formData.append('user_id', userId);
formData.append('voice_channel_id', channelId);
const response = await fetch('/voice/call', {
method: 'POST',
body: formData
});
const data = await response.json();
// Check for HTTP error status (422 validation error, etc.)
if (!response.ok) {
let errorMsg = data.error || data.detail || 'Unknown error';
// Handle FastAPI validation errors
if (data.detail && Array.isArray(data.detail)) {
errorMsg = data.detail.map(e => `${e.loc.join('.')}: ${e.msg}`).join(', ');
}
statusText.innerHTML = `❌ Error: ${errorMsg}`;
showNotification(`Voice call failed: ${errorMsg}`, 'error');
callBtn.disabled = false;
cancelBtn.style.display = 'none';
voiceCallActive = false;
return;
}
if (!data.success) {
statusText.innerHTML = `❌ Error: ${data.error}`;
showNotification(`Voice call failed: ${data.error}`, 'error');
callBtn.disabled = false;
cancelBtn.style.display = 'none';
voiceCallActive = false;
return;
}
// Success!
statusText.innerHTML = `✅ Voice call initiated!<br>User ID: ${data.user_id}<br>Channel: ${data.channel_id}`;
// Show invite link
const inviteDiv = document.getElementById('voice-call-invite-link');
const inviteUrl = document.getElementById('voice-call-invite-url');
inviteUrl.href = data.invite_url;
inviteUrl.textContent = data.invite_url;
inviteDiv.style.display = 'block';
// Add to call history
addVoiceCallToHistory(userId, channelId, data.invite_url);
showNotification('Voice call initiated successfully!', 'success');
// Auto-reset after 5 minutes (call should be done by then or timed out)
setTimeout(() => {
if (voiceCallActive) {
resetVoiceCall();
}
}, 300000); // 5 minutes
} catch (error) {
console.error('Voice call error:', error);
statusText.innerHTML = `❌ Error: ${error.message}`;
showNotification(`Voice call error: ${error.message}`, 'error');
callBtn.disabled = false;
cancelBtn.style.display = 'none';
voiceCallActive = false;
}
}
function cancelVoiceCall() {
resetVoiceCall();
showNotification('Voice call cancelled', 'info');
}
function resetVoiceCall() {
const callBtn = document.getElementById('voice-call-btn');
const cancelBtn = document.getElementById('voice-call-cancel-btn');
const statusDiv = document.getElementById('voice-call-status');
callBtn.disabled = false;
cancelBtn.style.display = 'none';
statusDiv.style.display = 'none';
voiceCallActive = false;
// Clear inputs
document.getElementById('voice-user-id').value = '';
document.getElementById('voice-channel-id').value = '';
}
function addVoiceCallToHistory(userId, channelId, inviteUrl) {
const now = new Date();
const timestamp = now.toLocaleTimeString();
const callEntry = {
userId: userId,
channelId: channelId,
inviteUrl: inviteUrl,
timestamp: timestamp
};
voiceCallHistory.unshift(callEntry); // Add to front
// Keep only last 10 calls
if (voiceCallHistory.length > 10) {
voiceCallHistory.pop();
}
updateVoiceCallHistoryDisplay();
}
function updateVoiceCallHistoryDisplay() {
const historyDiv = document.getElementById('voice-call-history');
if (voiceCallHistory.length === 0) {
historyDiv.innerHTML = '<div style="text-align: center; color: #888;">No calls yet. Start one above!</div>';
return;
}
let html = '';
voiceCallHistory.forEach((call, index) => {
html += `
<div style="background: #242424; padding: 0.75rem; margin-bottom: 0.5rem; border-radius: 4px; border-left: 3px solid #61dafb;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${call.timestamp}</strong>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
User: <code>${call.userId}</code> | Channel: <code>${call.channelId}</code>
</div>
</div>
<a href="${call.inviteUrl}" target="_blank" style="color: #61dafb; text-decoration: none; padding: 0.3rem 0.7rem; background: #333; border-radius: 4px; font-size: 0.85rem;">
View Link →
</a>
</div>
</div>
`;
});
historyDiv.innerHTML = html;
}
// ========== Memory Management (Tab 9) ==========
async function refreshMemoryStats() {
try {
// Fetch Cat status
const statusData = await apiCall('/memory/status');
const indicator = document.getElementById('cat-status-indicator');
const toggleBtn = document.getElementById('cat-toggle-btn');
if (statusData.healthy) {
indicator.innerHTML = `<span style="color: #6fdc6f;">● Connected</span> — ${statusData.url}`;
} else {
indicator.innerHTML = `<span style="color: #ff6b6b;">● Disconnected</span> — ${statusData.url}`;
}
if (statusData.circuit_breaker_active) {
indicator.innerHTML += ` <span style="color: #dcb06f;">(circuit breaker active)</span>`;
}
toggleBtn.textContent = statusData.enabled ? '🐱 Cat: ON' : '😿 Cat: OFF';
toggleBtn.style.background = statusData.enabled ? '#2a7a2a' : '#7a2a2a';
toggleBtn.style.borderColor = statusData.enabled ? '#4a9a4a' : '#9a4a4a';
// Fetch memory stats
const statsData = await apiCall('/memory/stats');
if (statsData.success && statsData.collections) {
const collections = {};
statsData.collections.forEach(c => { collections[c.name] = c.vectors_count; });
document.getElementById('stat-episodic-count').textContent = collections['episodic'] ?? '—';
document.getElementById('stat-declarative-count').textContent = collections['declarative'] ?? '—';
document.getElementById('stat-procedural-count').textContent = collections['procedural'] ?? '—';
} else {
document.getElementById('stat-episodic-count').textContent = '—';
document.getElementById('stat-declarative-count').textContent = '—';
document.getElementById('stat-procedural-count').textContent = '—';
}
} catch (err) {
console.error('Error refreshing memory stats:', err);
document.getElementById('cat-status-indicator').innerHTML = '<span style="color: #ff6b6b;">● Error checking status</span>';
}
}
async function toggleCatIntegration() {
try {
const statusData = await apiCall('/memory/status');
const newState = !statusData.enabled;
const formData = new FormData();
formData.append('enabled', newState);
const res = await fetch('/memory/toggle', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
showNotification(`Cheshire Cat ${newState ? 'enabled' : 'disabled'}`, newState ? 'success' : 'info');
refreshMemoryStats();
}
} catch (err) {
showNotification('Failed to toggle Cat integration', 'error');
}
}
async function triggerConsolidation() {
const btn = document.getElementById('consolidate-btn');
const status = document.getElementById('consolidation-status');
const resultDiv = document.getElementById('consolidation-result');
btn.disabled = true;
btn.textContent = '⏳ Running...';
status.textContent = 'Consolidation in progress (this may take a few minutes)...';
resultDiv.style.display = 'none';
try {
const data = await apiCall('/memory/consolidate', 'POST');
if (data.success) {
status.textContent = '✅ Consolidation complete!';
status.style.color = '#6fdc6f';
resultDiv.textContent = data.result || 'Consolidation finished successfully.';
resultDiv.style.display = 'block';
showNotification('Memory consolidation complete', 'success');
refreshMemoryStats();
} else {
status.textContent = '❌ ' + (data.error || 'Consolidation failed');
status.style.color = '#ff6b6b';
}
} catch (err) {
status.textContent = '❌ Error: ' + err.message;
status.style.color = '#ff6b6b';
} finally {
btn.disabled = false;
btn.textContent = '🌙 Run Consolidation';
}
}
async function loadFacts() {
const listDiv = document.getElementById('facts-list');
listDiv.innerHTML = '<div style="text-align: center; color: #888; padding: 1rem;">Loading facts...</div>';
try {
const data = await apiCall('/memory/facts');
if (!data.success || data.count === 0) {
listDiv.innerHTML = '<div style="text-align: center; color: #666; padding: 2rem;">No declarative facts stored yet.</div>';
return;
}
let html = '';
data.facts.forEach((fact, i) => {
const source = fact.metadata?.source || 'unknown';
const when = fact.metadata?.when ? new Date(fact.metadata.when * 1000).toLocaleString() : 'unknown';
const factDataJson = escapeJsonForAttribute(fact);
html += `
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a9955; display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(fact.content)}</div>
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
Source: ${escapeHtml(source)} · ${when}
</div>
</div>
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
<button data-memory='${factDataJson}' onclick='showEditMemoryModalFromButton(this, "declarative", "${fact.id}")'
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
title="Edit this fact">✏️</button>
<button onclick="deleteMemoryPoint('declarative', '${fact.id}', this)"
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
title="Delete this fact">🗑️</button>
</div>
</div>`;
});
listDiv.innerHTML = `<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.5rem;">${data.count} facts loaded</div>` + html;
} catch (err) {
listDiv.innerHTML = `<div style="color: #ff6b6b; padding: 1rem;">Error loading facts: ${err.message}</div>`;
}
}
async function loadEpisodicMemories() {
const listDiv = document.getElementById('episodic-list');
listDiv.innerHTML = '<div style="text-align: center; color: #888; padding: 1rem;">Loading memories...</div>';
try {
const data = await apiCall('/memory/episodic');
if (!data.success || data.count === 0) {
listDiv.innerHTML = '<div style="text-align: center; color: #666; padding: 2rem;">No episodic memories stored yet.</div>';
return;
}
let html = '';
data.memories.forEach((mem, i) => {
const source = mem.metadata?.source || 'unknown';
const when = mem.metadata?.when ? new Date(mem.metadata.when * 1000).toLocaleString() : 'unknown';
const memDataJson = escapeJsonForAttribute(mem);
html += `
<div class="memory-item" style="background: #242424; padding: 0.6rem 0.8rem; margin-bottom: 0.4rem; border-radius: 4px; border-left: 3px solid #2a5599; display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<div style="color: #ddd; font-size: 0.9rem;">${escapeHtml(mem.content)}</div>
<div style="color: #666; font-size: 0.75rem; margin-top: 0.3rem;">
Source: ${escapeHtml(source)} · ${when}
</div>
</div>
<div style="display: flex; gap: 0.3rem; flex-shrink: 0;">
<button data-memory='${memDataJson}' onclick='showEditMemoryModalFromButton(this, "episodic", "${mem.id}")'
style="background: none; border: none; color: #5599cc; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
title="Edit this memory">✏️</button>
<button onclick="deleteMemoryPoint('episodic', '${mem.id}', this)"
style="background: none; border: none; color: #993333; cursor: pointer; padding: 0.2rem 0.4rem; font-size: 0.85rem;"
title="Delete this memory">🗑️</button>
</div>
</div>`;
});
listDiv.innerHTML = `<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.5rem;">${data.count} memories loaded</div>` + html;
} catch (err) {
listDiv.innerHTML = `<div style="color: #ff6b6b; padding: 1rem;">Error loading memories: ${err.message}</div>`;
}
}
async function deleteMemoryPoint(collection, pointId, btnElement) {
if (!confirm(`Delete this ${collection} memory point?`)) return;
try {
const data = await apiCall(`/memory/point/${collection}/${pointId}`, 'DELETE');
if (data.success) {
// Remove the row from the UI
const row = btnElement.closest('div[style*="margin-bottom"]');
if (row) row.remove();
showNotification('Memory point deleted', 'success');
refreshMemoryStats();
} else {
showNotification('Failed to delete: ' + (data.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Failed to delete memory point:', err);
}
}
// Delete All Memories — Multi-step confirmation flow
function onDeleteStep1Change() {
const checked = document.getElementById('delete-checkbox-1').checked;
document.getElementById('delete-step-2').style.display = checked ? 'block' : 'none';
if (!checked) {
document.getElementById('delete-checkbox-2').checked = false;
document.getElementById('delete-step-3').style.display = 'none';
document.getElementById('delete-step-final').style.display = 'none';
document.getElementById('delete-confirmation-input').value = '';
}
}
function onDeleteStep2Change() {
const checked = document.getElementById('delete-checkbox-2').checked;
document.getElementById('delete-step-3').style.display = checked ? 'block' : 'none';
document.getElementById('delete-step-final').style.display = checked ? 'block' : 'none';
if (!checked) {
document.getElementById('delete-confirmation-input').value = '';
updateDeleteButton();
}
}
function onDeleteInputChange() {
updateDeleteButton();
}
function updateDeleteButton() {
const input = document.getElementById('delete-confirmation-input').value;
const expected = "Yes, I am deleting Miku's memories fully.";
const btn = document.getElementById('delete-all-btn');
const match = input === expected;
btn.disabled = !match;
btn.style.cursor = match ? 'pointer' : 'not-allowed';
btn.style.opacity = match ? '1' : '0.5';
}
async function executeDeleteAllMemories() {
const input = document.getElementById('delete-confirmation-input').value;
const expected = "Yes, I am deleting Miku's memories fully.";
if (input !== expected) {
showNotification('Confirmation string does not match', 'error');
return;
}
const btn = document.getElementById('delete-all-btn');
btn.disabled = true;
btn.textContent = '⏳ Deleting...';
try {
const data = await apiCall('/memory/delete', 'POST', { confirmation: input });
if (data.success) {
showNotification('All memories have been permanently deleted', 'success');
resetDeleteFlow();
refreshMemoryStats();
} else {
showNotification('Deletion failed: ' + (data.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Failed to delete all memories:', err);
} finally {
btn.disabled = false;
btn.textContent = '🗑️ Permanently Delete All Memories';
}
}
function resetDeleteFlow() {
document.getElementById('delete-checkbox-1').checked = false;
document.getElementById('delete-checkbox-2').checked = false;
document.getElementById('delete-confirmation-input').value = '';
document.getElementById('delete-step-2').style.display = 'none';
document.getElementById('delete-step-3').style.display = 'none';
document.getElementById('delete-step-final').style.display = 'none';
updateDeleteButton();
}
// Memory Edit/Create Modal Functions
let currentEditMemory = null;
function showEditMemoryModalFromButton(button, collection, pointId) {
const memoryJson = button.getAttribute('data-memory');
// Unescape HTML entities back to JSON
const unescapedJson = memoryJson
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
const memory = JSON.parse(unescapedJson);
showEditMemoryModal(collection, pointId, memory);
}
function showEditMemoryModal(collection, pointId, memoryData) {
const memory = typeof memoryData === 'string' ? JSON.parse(memoryData) : memoryData;
currentEditMemory = { collection, pointId, memory };
const modal = document.getElementById('edit-memory-modal');
const contentField = document.getElementById('edit-memory-content');
const sourceField = document.getElementById('edit-memory-source');
contentField.value = memory.content || '';
sourceField.value = memory.metadata?.source || '';
modal.style.display = 'flex';
}
function closeEditMemoryModal() {
document.getElementById('edit-memory-modal').style.display = 'none';
currentEditMemory = null;
}
async function saveMemoryEdit() {
if (!currentEditMemory) return;
const content = document.getElementById('edit-memory-content').value.trim();
const source = document.getElementById('edit-memory-source').value.trim();
if (!content) {
showNotification('Content cannot be empty', 'error');
return;
}
const { collection, pointId } = currentEditMemory;
const saveBtn = document.querySelector('#edit-memory-modal button[onclick="saveMemoryEdit()"]');
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
try {
const data = await apiCall(`/memory/point/${collection}/${pointId}`, 'PUT', {
content: content,
metadata: { source: source || 'manual_edit' }
});
if (data.success) {
showNotification('Memory updated successfully', 'success');
closeEditMemoryModal();
// Reload the appropriate list
if (collection === 'declarative') {
loadFacts();
} else if (collection === 'episodic') {
loadEpisodicMemories();
}
} else {
showNotification('Failed to update: ' + (data.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Failed to save memory edit:', err);
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Save Changes';
}
}
function showCreateMemoryModal(collection) {
const modal = document.getElementById('create-memory-modal');
document.getElementById('create-memory-collection').value = collection;
document.getElementById('create-memory-content').value = '';
document.getElementById('create-memory-user-id').value = '';
document.getElementById('create-memory-source').value = 'manual';
// Update modal title based on collection type
const title = collection === 'declarative' ? 'Add New Fact' : 'Add New Memory';
document.querySelector('#create-memory-modal h3').textContent = title;
modal.style.display = 'flex';
}
function closeCreateMemoryModal() {
document.getElementById('create-memory-modal').style.display = 'none';
}
// Modal keyboard and backdrop close handlers
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const editModal = document.getElementById('edit-memory-modal');
const createModal = document.getElementById('create-memory-modal');
if (editModal && editModal.style.display !== 'none') closeEditMemoryModal();
if (createModal && createModal.style.display !== 'none') closeCreateMemoryModal();
}
});
// (Modal accessibility init moved to consolidated DOMContentLoaded)
async function saveNewMemory() {
const collection = document.getElementById('create-memory-collection').value;
const content = document.getElementById('create-memory-content').value.trim();
const userId = document.getElementById('create-memory-user-id').value.trim();
const source = document.getElementById('create-memory-source').value.trim();
if (!content) {
showNotification('Content cannot be empty', 'error');
return;
}
const createBtn = document.querySelector('#create-memory-modal button[onclick="saveNewMemory()"]');
createBtn.disabled = true;
createBtn.textContent = 'Creating...';
try {
const data = await apiCall('/memory/create', 'POST', {
collection: collection,
content: content,
user_id: userId || null,
source: source || 'manual',
metadata: {}
});
if (data.success) {
showNotification(`${collection === 'declarative' ? 'Fact' : 'Memory'} created successfully`, 'success');
closeCreateMemoryModal();
// Reload the appropriate list
if (collection === 'declarative') {
loadFacts();
} else if (collection === 'episodic') {
loadEpisodicMemories();
}
refreshMemoryStats();
} else {
showNotification('Failed to create: ' + (data.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Failed to save new memory:', err);
} finally {
createBtn.disabled = false;
createBtn.textContent = 'Create Memory';
}
}
// Search/Filter Function
function filterMemories(listId, searchTerm) {
const listDiv = document.getElementById(listId);
const items = listDiv.querySelectorAll('.memory-item');
const term = searchTerm.toLowerCase().trim();
items.forEach(item => {
const content = item.textContent.toLowerCase();
if (term === '' || content.includes(term)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
function escapeJsonForAttribute(obj) {
// Properly escape JSON for use in HTML attributes
return JSON.stringify(obj)
.replace(/&/g, '&amp;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
</script>
</body>
</html>