Show a CSS spinner overlay when switching to Autonomous Stats (tab6), Memories (tab9), and DM Management (tab10). Spinner only shows on first visit when content is empty, removed after data loads.
5858 lines
221 KiB
HTML
5858 lines
221 KiB
HTML
<!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;
|
||
}
|
||
|
||
/* Prompt source toggle buttons */
|
||
.prompt-source-btn {
|
||
background: #333;
|
||
color: #aaa;
|
||
}
|
||
.prompt-source-btn.active {
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
}
|
||
.prompt-source-btn:hover:not(.active) {
|
||
background: #444;
|
||
color: #ddd;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Tab loading spinner */
|
||
.tab-loading-overlay {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3rem 1rem;
|
||
color: #888;
|
||
font-size: 1rem;
|
||
gap: 0.75rem;
|
||
}
|
||
.tab-loading-overlay .spinner {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 3px solid #444;
|
||
border-top-color: #4CAF50;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Chat Interface Styles */
|
||
.chat-message {
|
||
margin-bottom: 1rem;
|
||
padding: 1rem;
|
||
border-radius: 8px;
|
||
animation: fadeIn 0.3s ease-in;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.chat-message.user-message {
|
||
background: #2a3a4a;
|
||
border-left: 4px solid #4CAF50;
|
||
margin-left: 2rem;
|
||
}
|
||
|
||
.chat-message.assistant-message {
|
||
background: #3a2a3a;
|
||
border-left: 4px solid #61dafb;
|
||
margin-right: 2rem;
|
||
}
|
||
|
||
.chat-message.error-message {
|
||
background: #4a2a2a;
|
||
border-left: 4px solid #f44336;
|
||
}
|
||
|
||
.chat-message-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.chat-message-sender {
|
||
font-weight: bold;
|
||
color: #61dafb;
|
||
}
|
||
|
||
.chat-message.user-message .chat-message-sender {
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.chat-message-time {
|
||
color: #888;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.chat-message-content {
|
||
color: #ddd;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.chat-typing-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.chat-typing-indicator span {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #61dafb;
|
||
border-radius: 50%;
|
||
animation: typing 1.4s infinite;
|
||
}
|
||
|
||
.chat-typing-indicator span:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.chat-typing-indicator span:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
@keyframes typing {
|
||
0%, 60%, 100% { transform: translateY(0); opacity: 0.7; }
|
||
30% { transform: translateY(-10px); opacity: 1; }
|
||
}
|
||
|
||
#chat-messages::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
#chat-messages::-webkit-scrollbar-track {
|
||
background: #1e1e1e;
|
||
}
|
||
|
||
#chat-messages::-webkit-scrollbar-thumb {
|
||
background: #555;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
#chat-messages::-webkit-scrollbar-thumb:hover {
|
||
background: #666;
|
||
}
|
||
|
||
/* Evil Mode Styles */
|
||
body.evil-mode h1, body.evil-mode h3 {
|
||
color: #ff4444;
|
||
}
|
||
|
||
body.evil-mode .tab-button.active {
|
||
border-bottom-color: #ff4444;
|
||
}
|
||
|
||
body.evil-mode #evil-mode-toggle {
|
||
background: #ff4444;
|
||
border-color: #ff4444;
|
||
color: #000;
|
||
}
|
||
|
||
body.evil-mode .server-name {
|
||
color: #ff4444;
|
||
}
|
||
|
||
body.evil-mode .chat-message-sender {
|
||
color: #ff4444;
|
||
}
|
||
|
||
body.evil-mode .chat-message.assistant-message {
|
||
border-left-color: #ff4444;
|
||
}
|
||
|
||
body.evil-mode #notification {
|
||
border-color: #ff4444;
|
||
}
|
||
|
||
/* Override any blue status text in evil mode */
|
||
body.evil-mode [style*="color: #007bff"],
|
||
body.evil-mode [style*="color: rgb(0, 123, 255)"] {
|
||
color: #ff4444 !important;
|
||
}
|
||
|
||
/* Bipolar Mode Styles */
|
||
#bipolar-section {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
#bipolar-section h3 {
|
||
margin-top: 0;
|
||
}
|
||
|
||
#bipolar-mode-toggle.bipolar-active {
|
||
background: #9932CC !important;
|
||
border-color: #9932CC !important;
|
||
}
|
||
|
||
/* Responsive breakpoints */
|
||
@media (max-width: 1200px) {
|
||
.panel { width: 55%; padding: 1.5rem; }
|
||
.logs { width: 45%; }
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
body { flex-direction: column; }
|
||
.panel { width: 100%; padding: 1.5rem; }
|
||
.logs {
|
||
width: 100%;
|
||
height: 300px;
|
||
border-left: none;
|
||
border-top: 2px solid #333;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.panel { padding: 1rem; }
|
||
.tab-buttons {
|
||
grid-template-rows: none;
|
||
grid-auto-flow: row;
|
||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||
}
|
||
.tab-button { font-size: 0.85rem; padding: 0.4rem 0.6rem; }
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.panel { padding: 0.5rem; }
|
||
.tab-buttons { grid-template-columns: 1fr 1fr; }
|
||
.tab-button { font-size: 0.8rem; padding: 0.35rem 0.5rem; }
|
||
h1 { font-size: 1.2rem; }
|
||
}
|
||
</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="tab10" onclick="switchTab('tab10')">📱 DM Management</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="">Loading moods...</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>Last Prompt</h3>
|
||
<div style="margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.75rem;">
|
||
<label style="font-size: 0.9rem; color: #aaa;">Source:</label>
|
||
<div style="display: inline-flex; border-radius: 6px; overflow: hidden; border: 1px solid #444;">
|
||
<button id="prompt-src-cat" class="prompt-source-btn active" onclick="switchPromptSource('cat')"
|
||
style="padding: 0.4rem 1rem; border: none; cursor: pointer; font-size: 0.85rem; transition: all 0.2s;">
|
||
🐱 Cheshire Cat
|
||
</button>
|
||
<button id="prompt-src-fallback" class="prompt-source-btn" onclick="switchPromptSource('fallback')"
|
||
style="padding: 0.4rem 1rem; border: none; cursor: pointer; font-size: 0.85rem; transition: all 0.2s;">
|
||
🤖 Bot Fallback
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="prompt-cat-info" style="margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa;"></div>
|
||
<pre id="last-prompt" style="white-space: pre-wrap; word-break: break-word;"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DM Management Tab Content -->
|
||
<div id="tab10" class="tab-content">
|
||
<div class="section">
|
||
<h3>📱 DM Management</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>
|
||
|
||
<!-- 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>
|
||
</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 === 'tab3') {
|
||
loadStatus();
|
||
loadLastPrompt();
|
||
}
|
||
if (tabId === 'tab6') {
|
||
showTabLoading('tab6');
|
||
loadAutonomousStats().finally(() => hideTabLoading('tab6'));
|
||
}
|
||
if (tabId === 'tab9') {
|
||
console.log('🧠 Refreshing memory stats for Memories tab');
|
||
showTabLoading('tab9');
|
||
refreshMemoryStats().finally(() => hideTabLoading('tab9'));
|
||
}
|
||
if (tabId === 'tab10') {
|
||
console.log('📱 Loading DM users for DM Management tab');
|
||
showTabLoading('tab10');
|
||
loadDMUsers().finally(() => hideTabLoading('tab10'));
|
||
}
|
||
}
|
||
|
||
function showTabLoading(tabId) {
|
||
const tab = document.getElementById(tabId);
|
||
if (!tab) return;
|
||
// Only show if no data loaded yet (first visit)
|
||
if (tab.querySelector('.tab-loading-overlay')) return;
|
||
const sections = tab.querySelectorAll('.section');
|
||
const hasContent = Array.from(sections).some(s => s.querySelector('[id]')?.innerHTML?.trim());
|
||
if (hasContent) return; // Data already loaded, skip spinner
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'tab-loading-overlay';
|
||
overlay.innerHTML = '<div class="spinner"></div> Loading...';
|
||
tab.prepend(overlay);
|
||
}
|
||
|
||
function hideTabLoading(tabId) {
|
||
const tab = document.getElementById(tabId);
|
||
if (!tab) return;
|
||
const overlay = tab.querySelector('.tab-loading-overlay');
|
||
if (overlay) overlay.remove();
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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();
|
||
initPromptSourceToggle();
|
||
|
||
// Load initial data
|
||
loadStatus();
|
||
loadServers();
|
||
populateMoodDropdowns(); // Populate DM and chat mood dropdowns immediately
|
||
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);
|
||
const emojiMap = evilMode ? EVIL_MOOD_EMOJIS : MOOD_EMOJIS;
|
||
|
||
// Populate the DM mood dropdown (#mood on tab1)
|
||
const dmMoodSelect = document.getElementById('mood');
|
||
if (dmMoodSelect) {
|
||
dmMoodSelect.innerHTML = '';
|
||
data.moods.forEach(mood => {
|
||
const opt = document.createElement('option');
|
||
opt.value = mood;
|
||
opt.textContent = `${emojiMap[mood] || ''} ${mood}`.trim();
|
||
if (mood === 'neutral') opt.selected = true;
|
||
dmMoodSelect.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
// Populate the chat mood dropdown (#chat-mood-select on tab7)
|
||
const chatMoodSelect = document.getElementById('chat-mood-select');
|
||
if (chatMoodSelect) {
|
||
chatMoodSelect.innerHTML = '';
|
||
data.moods.forEach(mood => {
|
||
const opt = document.createElement('option');
|
||
opt.value = mood;
|
||
opt.textContent = `${emojiMap[mood] || ''} ${mood}`.trim();
|
||
if (mood === 'neutral') opt.selected = true;
|
||
chatMoodSelect.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
// Populate per-server mood dropdowns (mood-select-{guildId})
|
||
document.querySelectorAll('[id^="mood-select-"]').forEach(select => {
|
||
// Keep only the first option ("Select Mood...")
|
||
while (select.children.length > 1) {
|
||
select.removeChild(select.lastChild);
|
||
}
|
||
});
|
||
|
||
data.moods.forEach(mood => {
|
||
const moodOption = document.createElement('option');
|
||
moodOption.value = mood;
|
||
moodOption.textContent = `${mood} ${emojiMap[mood] || ''}`;
|
||
|
||
document.querySelectorAll('[id^="mood-select-"]').forEach(select => {
|
||
select.appendChild(moodOption.cloneNode(true));
|
||
});
|
||
});
|
||
|
||
console.log('🎭 All mood dropdowns populated successfully');
|
||
} else {
|
||
console.warn('🎭 No moods found in response');
|
||
}
|
||
} catch (error) {
|
||
console.error('🎭 Failed to load available moods:', error);
|
||
}
|
||
}
|
||
|
||
// Per-Server Mood Management
|
||
async function setServerMood(guildId) {
|
||
console.log(`🎭 setServerMood called with guildId: ${guildId} (type: ${typeof guildId})`);
|
||
|
||
// Ensure guildId is a string for consistency
|
||
const guildIdStr = String(guildId);
|
||
console.log(`🎭 Using guildId as string: ${guildIdStr}`);
|
||
|
||
// Debug: Check what elements exist
|
||
const elementId = `mood-select-${guildIdStr}`;
|
||
console.log(`🎭 Looking for element with ID: ${elementId}`);
|
||
|
||
const moodSelect = document.getElementById(elementId);
|
||
console.log(`🎭 Found element:`, moodSelect);
|
||
|
||
if (!moodSelect) {
|
||
console.error(`🎭 ERROR: Element with ID '${elementId}' not found!`);
|
||
console.log(`🎭 Available mood-select elements:`, document.querySelectorAll('[id^="mood-select-"]'));
|
||
showNotification(`Error: Mood selector not found for server ${guildIdStr}`, 'error');
|
||
return;
|
||
}
|
||
|
||
const selectedMood = moodSelect.value;
|
||
|
||
console.log(`🎭 Setting mood for server ${guildIdStr} to ${selectedMood}`);
|
||
|
||
if (!selectedMood) {
|
||
showNotification('Please select a mood', 'error');
|
||
return;
|
||
}
|
||
|
||
// Get the button and store original text before any changes
|
||
const button = moodSelect.nextElementSibling;
|
||
const originalText = button.textContent;
|
||
|
||
try {
|
||
// Show loading state
|
||
button.textContent = 'Changing...';
|
||
button.disabled = true;
|
||
|
||
console.log(`🎭 Making API call to /servers/${guildIdStr}/mood with mood: ${selectedMood}`);
|
||
const response = await apiCall(`/servers/${guildIdStr}/mood`, 'POST', { mood: selectedMood });
|
||
console.log(`🎭 API response:`, response);
|
||
|
||
if (response.status === 'ok') {
|
||
showNotification(`Server mood changed to ${selectedMood} ${MOOD_EMOJIS[selectedMood] || ''}`);
|
||
|
||
// Reset dropdown selection
|
||
moodSelect.value = '';
|
||
|
||
// Reload servers to show updated mood
|
||
loadServers();
|
||
} else {
|
||
showNotification(`Failed to change mood: ${response.message}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error(`🎭 Error setting mood:`, error);
|
||
showNotification(`Failed to change mood: ${error}`, 'error');
|
||
} finally {
|
||
// Restore button state
|
||
button.textContent = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function resetServerMood(guildId) {
|
||
console.log(`🎭 resetServerMood called with guildId: ${guildId} (type: ${typeof guildId})`);
|
||
|
||
// Ensure guildId is a string for consistency
|
||
const guildIdStr = String(guildId);
|
||
console.log(`🎭 Using guildId as string: ${guildIdStr}`);
|
||
|
||
const button = document.querySelector(`button[onclick="resetServerMood('${guildIdStr}')"]`);
|
||
const originalText = button ? button.textContent : 'Reset';
|
||
|
||
try {
|
||
// Show loading state
|
||
if (button) {
|
||
button.textContent = 'Resetting...';
|
||
button.disabled = true;
|
||
}
|
||
|
||
await apiCall(`/servers/${guildIdStr}/mood/reset`, 'POST');
|
||
showNotification(`Server mood reset to neutral`);
|
||
|
||
// Reload servers to show updated mood
|
||
loadServers();
|
||
} catch (error) {
|
||
showNotification(`Failed to reset mood: ${error}`, 'error');
|
||
} finally {
|
||
// Restore button state
|
||
if (button) {
|
||
button.textContent = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function updateBedtimeRange(guildId) {
|
||
console.log(`⏰ updateBedtimeRange called with guildId: ${guildId}`);
|
||
|
||
// Ensure guildId is a string for consistency
|
||
const guildIdStr = String(guildId);
|
||
|
||
// Get the time values from the inputs
|
||
const startTimeInput = document.getElementById(`bedtime-start-${guildIdStr}`);
|
||
const endTimeInput = document.getElementById(`bedtime-end-${guildIdStr}`);
|
||
|
||
if (!startTimeInput || !endTimeInput) {
|
||
showNotification('Could not find bedtime time inputs', 'error');
|
||
return;
|
||
}
|
||
|
||
const startTime = startTimeInput.value; // Format: "HH:MM"
|
||
const endTime = endTimeInput.value; // Format: "HH:MM"
|
||
|
||
if (!startTime || !endTime) {
|
||
showNotification('Please enter both start and end times', 'error');
|
||
return;
|
||
}
|
||
|
||
// Parse the times
|
||
const [startHour, startMinute] = startTime.split(':').map(Number);
|
||
const [endHour, endMinute] = endTime.split(':').map(Number);
|
||
|
||
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
|
||
const originalText = button ? button.textContent : 'Update Bedtime Range';
|
||
|
||
try {
|
||
// Show loading state
|
||
if (button) {
|
||
button.textContent = 'Updating...';
|
||
button.disabled = true;
|
||
}
|
||
|
||
// Send the update request
|
||
await apiCall(`/servers/${guildIdStr}/bedtime-range`, 'POST', {
|
||
bedtime_hour: startHour,
|
||
bedtime_minute: startMinute,
|
||
bedtime_hour_end: endHour,
|
||
bedtime_minute_end: endMinute
|
||
});
|
||
|
||
showNotification(`Bedtime range updated: ${startTime} - ${endTime}`);
|
||
|
||
// Reload servers to show updated configuration
|
||
loadServers();
|
||
|
||
} catch (error) {
|
||
console.error('Failed to update bedtime range:', error);
|
||
} finally {
|
||
// Restore button state
|
||
if (button) {
|
||
button.textContent = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Mood Management
|
||
async function setMood() {
|
||
const mood = document.getElementById('mood').value;
|
||
try {
|
||
// Use different endpoint for evil mode
|
||
const endpoint = evilMode ? '/evil-mode/mood' : '/mood';
|
||
await apiCall(endpoint, 'POST', { mood: mood });
|
||
showNotification(`Mood set to ${mood}`);
|
||
currentMood = mood;
|
||
} catch (error) {
|
||
console.error('Failed to set mood:', error);
|
||
}
|
||
}
|
||
|
||
async function resetMood() {
|
||
try {
|
||
if (evilMode) {
|
||
await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' });
|
||
showNotification('Evil mood reset to evil_neutral');
|
||
currentMood = 'evil_neutral';
|
||
document.getElementById('mood').value = 'evil_neutral';
|
||
} else {
|
||
await apiCall('/mood/reset', 'POST');
|
||
showNotification('Mood reset to neutral');
|
||
currentMood = 'neutral';
|
||
document.getElementById('mood').value = 'neutral';
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to reset mood:', error);
|
||
}
|
||
}
|
||
|
||
async function calmMiku() {
|
||
try {
|
||
if (evilMode) {
|
||
await apiCall('/evil-mode/mood', 'POST', { mood: 'evil_neutral' });
|
||
showNotification('Evil Miku has been calmed down');
|
||
currentMood = 'evil_neutral';
|
||
document.getElementById('mood').value = 'evil_neutral';
|
||
} else {
|
||
await apiCall('/mood/calm', 'POST');
|
||
showNotification('Miku has been calmed down');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to calm Miku:', error);
|
||
}
|
||
}
|
||
|
||
// ===== Language Mode Functions =====
|
||
async function refreshLanguageStatus() {
|
||
try {
|
||
const result = await apiCall('/language');
|
||
document.getElementById('current-language-display').textContent =
|
||
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
|
||
document.getElementById('status-language').textContent =
|
||
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
|
||
document.getElementById('status-model').textContent = result.current_model;
|
||
|
||
console.log('Language status:', result);
|
||
} catch (error) {
|
||
console.error('Failed to get language status:', error);
|
||
showNotification('Failed to load language status', 'error');
|
||
}
|
||
}
|
||
|
||
async function toggleLanguageMode() {
|
||
try {
|
||
const result = await apiCall('/language/toggle', 'POST');
|
||
|
||
// Update UI
|
||
document.getElementById('current-language-display').textContent =
|
||
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
|
||
document.getElementById('status-language').textContent =
|
||
result.language_mode === 'japanese' ? '日本語 (Japanese)' : 'English';
|
||
document.getElementById('status-model').textContent = result.model_now_using;
|
||
|
||
// Show notification
|
||
showNotification(result.message, 'success');
|
||
console.log('Language toggled:', result);
|
||
} catch (error) {
|
||
console.error('Failed to toggle language mode:', error);
|
||
showNotification('Failed to toggle language mode', 'error');
|
||
}
|
||
}
|
||
|
||
// 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() {
|
||
const source = localStorage.getItem('miku-prompt-source') || 'cat';
|
||
const promptEl = document.getElementById('last-prompt');
|
||
const infoEl = document.getElementById('prompt-cat-info');
|
||
|
||
try {
|
||
if (source === 'cat') {
|
||
const result = await apiCall('/prompt/cat');
|
||
if (result.timestamp) {
|
||
infoEl.innerHTML = `<strong>User:</strong> ${escapeHtml(result.user || '?')} | <strong>Mood:</strong> ${escapeHtml(result.mood || '?')} | <strong>Time:</strong> ${new Date(result.timestamp).toLocaleString()}`;
|
||
promptEl.textContent = `[User message → Cat]\n${result.prompt}\n\n[Cat response]\n${result.response}`;
|
||
} else {
|
||
infoEl.textContent = '';
|
||
promptEl.textContent = result.prompt || 'No Cheshire Cat interaction yet.';
|
||
}
|
||
} else {
|
||
infoEl.textContent = '';
|
||
const result = await apiCall('/prompt');
|
||
promptEl.textContent = result.prompt;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load last prompt:', error);
|
||
}
|
||
}
|
||
|
||
function switchPromptSource(source) {
|
||
localStorage.setItem('miku-prompt-source', source);
|
||
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.getElementById(`prompt-src-${source}`).classList.add('active');
|
||
loadLastPrompt();
|
||
}
|
||
|
||
function initPromptSourceToggle() {
|
||
const saved = localStorage.getItem('miku-prompt-source') || 'cat';
|
||
document.querySelectorAll('.prompt-source-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.getElementById(`prompt-src-${saved}`).classList.add('active');
|
||
}
|
||
|
||
// 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(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&');
|
||
const memory = JSON.parse(unescapedJson);
|
||
showEditMemoryModal(collection, pointId, memory);
|
||
}
|
||
|
||
function showEditMemoryModal(collection, pointId, memoryData) {
|
||
const memory = typeof memoryData === 'string' ? JSON.parse(memoryData) : memoryData;
|
||
currentEditMemory = { collection, pointId, memory };
|
||
|
||
const modal = document.getElementById('edit-memory-modal');
|
||
const contentField = document.getElementById('edit-memory-content');
|
||
const sourceField = document.getElementById('edit-memory-source');
|
||
|
||
contentField.value = memory.content || '';
|
||
sourceField.value = memory.metadata?.source || '';
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function closeEditMemoryModal() {
|
||
document.getElementById('edit-memory-modal').style.display = 'none';
|
||
currentEditMemory = null;
|
||
}
|
||
|
||
async function saveMemoryEdit() {
|
||
if (!currentEditMemory) return;
|
||
|
||
const content = document.getElementById('edit-memory-content').value.trim();
|
||
const source = document.getElementById('edit-memory-source').value.trim();
|
||
|
||
if (!content) {
|
||
showNotification('Content cannot be empty', 'error');
|
||
return;
|
||
}
|
||
|
||
const { collection, pointId } = currentEditMemory;
|
||
const saveBtn = document.querySelector('#edit-memory-modal button[onclick="saveMemoryEdit()"]');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Saving...';
|
||
|
||
try {
|
||
const data = await apiCall(`/memory/point/${collection}/${pointId}`, 'PUT', {
|
||
content: content,
|
||
metadata: { source: source || 'manual_edit' }
|
||
});
|
||
|
||
if (data.success) {
|
||
showNotification('Memory updated successfully', 'success');
|
||
closeEditMemoryModal();
|
||
// Reload the appropriate list
|
||
if (collection === 'declarative') {
|
||
loadFacts();
|
||
} else if (collection === 'episodic') {
|
||
loadEpisodicMemories();
|
||
}
|
||
} else {
|
||
showNotification('Failed to update: ' + (data.error || 'Unknown error'), 'error');
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to save memory edit:', err);
|
||
} finally {
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = 'Save Changes';
|
||
}
|
||
}
|
||
|
||
function showCreateMemoryModal(collection) {
|
||
const modal = document.getElementById('create-memory-modal');
|
||
document.getElementById('create-memory-collection').value = collection;
|
||
document.getElementById('create-memory-content').value = '';
|
||
document.getElementById('create-memory-user-id').value = '';
|
||
document.getElementById('create-memory-source').value = 'manual';
|
||
|
||
// Update modal title based on collection type
|
||
const title = collection === 'declarative' ? 'Add New Fact' : 'Add New Memory';
|
||
document.querySelector('#create-memory-modal h3').textContent = title;
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function closeCreateMemoryModal() {
|
||
document.getElementById('create-memory-modal').style.display = 'none';
|
||
}
|
||
|
||
// Modal keyboard and backdrop close handlers
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
const editModal = document.getElementById('edit-memory-modal');
|
||
const createModal = document.getElementById('create-memory-modal');
|
||
if (editModal && editModal.style.display !== 'none') closeEditMemoryModal();
|
||
if (createModal && createModal.style.display !== 'none') closeCreateMemoryModal();
|
||
}
|
||
});
|
||
|
||
// (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, '&')
|
||
.replace(/'/g, ''')
|
||
.replace(/"/g, '"')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
|