Compare commits
2 Commits
2d7acd7850
...
694590a620
| Author | SHA1 | Date | |
|---|---|---|---|
| 694590a620 | |||
| 6080fe170f |
@@ -505,10 +505,6 @@ normal:
|
|||||||
name: Gintama
|
name: Gintama
|
||||||
weight: 1
|
weight: 1
|
||||||
state: Comedy Anime
|
state: Comedy Anime
|
||||||
test:
|
|
||||||
- type: playing
|
|
||||||
name: G
|
|
||||||
weight: 2
|
|
||||||
evil:
|
evil:
|
||||||
aggressive:
|
aggressive:
|
||||||
- type: listening
|
- type: listening
|
||||||
|
|||||||
@@ -138,8 +138,11 @@ async def on_ready():
|
|||||||
|
|
||||||
# Set initial Discord presence based on current mood
|
# Set initial Discord presence based on current mood
|
||||||
try:
|
try:
|
||||||
from utils.activities import update_bot_presence
|
from utils.activities import update_bot_presence, is_manual_override_active
|
||||||
if globals.EVIL_MODE:
|
# On reconnect, don't overwrite an active manual override
|
||||||
|
if is_manual_override_active():
|
||||||
|
logger.info("Manual override active on ready, preserving it")
|
||||||
|
elif globals.EVIL_MODE:
|
||||||
await update_bot_presence(globals.EVIL_DM_MOOD, is_evil=True, force=True)
|
await update_bot_presence(globals.EVIL_DM_MOOD, is_evil=True, force=True)
|
||||||
else:
|
else:
|
||||||
await update_bot_presence(globals.DM_MOOD, is_evil=False, force=True)
|
await update_bot_presence(globals.DM_MOOD, is_evil=False, force=True)
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ async def set_mood_activities(section: str, mood: str, request: Request):
|
|||||||
if section not in ("normal", "evil"):
|
if section not in ("normal", "evil"):
|
||||||
return JSONResponse(status_code=400, content={"error": "Section must be 'normal' or 'evil'"})
|
return JSONResponse(status_code=400, content={"error": "Section must be 'normal' or 'evil'"})
|
||||||
|
|
||||||
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
|
||||||
|
|
||||||
activities = data.get("activities")
|
activities = data.get("activities")
|
||||||
|
|
||||||
if activities is None:
|
if activities is None:
|
||||||
@@ -97,12 +101,24 @@ async def set_current_activity(request: Request):
|
|||||||
Body: {"type": "listening"|"playing"|"watching"|"competing"|"streaming",
|
Body: {"type": "listening"|"playing"|"watching"|"competing"|"streaming",
|
||||||
"name": "...", "state": "..." (optional), "url": "..." (required for streaming)}
|
"name": "...", "state": "..." (optional), "url": "..." (required for streaming)}
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
|
||||||
|
|
||||||
activity_type = data.get("type", "").lower().strip()
|
activity_type = data.get("type", "").lower().strip()
|
||||||
name = data.get("name", "").strip()
|
name = data.get("name", "").strip()
|
||||||
state = data.get("state") or None
|
state = data.get("state") or None
|
||||||
url = data.get("url") or None
|
url = data.get("url") or None
|
||||||
|
|
||||||
|
# Pre-validate before passing to activity module
|
||||||
|
if not activity_type:
|
||||||
|
return JSONResponse(status_code=400, content={"error": "'type' is required"})
|
||||||
|
if not name:
|
||||||
|
return JSONResponse(status_code=400, content={"error": "'name' is required"})
|
||||||
|
if len(name) > 128:
|
||||||
|
return JSONResponse(status_code=400, content={"error": f"'name' exceeds 128 characters ({len(name)})"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from utils.activities import set_activity_manual
|
from utils.activities import set_activity_manual
|
||||||
await set_activity_manual(activity_type, name, state=state, url=url)
|
await set_activity_manual(activity_type, name, state=state, url=url)
|
||||||
|
|||||||
872
bot/static/css/style.css
Normal file
872
bot/static/css/style.css
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mood Activities Editor */
|
||||||
|
.act-mood-row {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.act-mood-header {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.act-mood-header:hover { background: #333; }
|
||||||
|
.act-mood-header .act-mood-name { font-weight: bold; min-width: 120px; }
|
||||||
|
.act-mood-header .act-mood-stats { color: #888; font-size: 0.8rem; }
|
||||||
|
.act-mood-content { display: none; padding: 0.75rem; background: #1e1e1e; }
|
||||||
|
.act-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
.act-entry:last-child { border-bottom: none; }
|
||||||
|
.act-entry-icon { font-size: 1.1rem; min-width: 24px; text-align: center; }
|
||||||
|
.act-entry input[type="text"] { flex: 1; }
|
||||||
|
.act-entry input[type="number"] { width: 55px; }
|
||||||
|
.act-entry select { width: 130px; }
|
||||||
|
.act-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Picture Tab Styles */
|
||||||
|
.pfp-preview-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.pfp-preview-box {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.pfp-preview-box img {
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
border: 2px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
.pfp-preview-box .label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.pfp-crop-container {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 550px;
|
||||||
|
background: #111;
|
||||||
|
border: 2px solid #555;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.pfp-crop-container img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.crop-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.crop-mode-toggle label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.crop-mode-toggle input[type="radio"] {
|
||||||
|
accent-color: #4CAF50;
|
||||||
|
}
|
||||||
|
.pfp-description-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ddd;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.pfp-description-editor:focus {
|
||||||
|
border-color: #61dafb;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
/* Album / Gallery grid */
|
||||||
|
.album-section {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.album-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.album-header h4 { margin: 0; }
|
||||||
|
.album-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
.album-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
.album-card {
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.album-card:hover { border-color: #61dafb; }
|
||||||
|
.album-card.selected { border-color: #4CAF50; box-shadow: 0 0 8px rgba(76,175,80,0.4); }
|
||||||
|
.album-card.checked { border-color: #ff9800; }
|
||||||
|
.album-card img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.album-card .album-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
accent-color: #ff9800;
|
||||||
|
}
|
||||||
|
.album-card .album-card-info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #ccc;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.album-card .color-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #888;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
.album-detail {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.album-detail-previews {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.album-detail-previews .pfp-preview-box img {
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
.album-disk-usage {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
432
bot/static/js/actions.js
Normal file
432
bot/static/js/actions.js
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Actions Module
|
||||||
|
// Autonomous actions, manual actions, custom prompts, reactions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ===== 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}`;
|
||||||
|
|
||||||
|
if (selectedServer !== 'all') {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEngageSubmenu() {
|
||||||
|
const submenu = document.getElementById('engage-submenu');
|
||||||
|
submenu.style.display = submenu.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (selectedServer !== 'all') {
|
||||||
|
params.append('guild_id', selectedServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
params.append('user_id', userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engageType !== 'random') {
|
||||||
|
params.append('engagement_type', engageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.append('manual_trigger', 'true');
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
endpoint += `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiCall(endpoint, 'POST');
|
||||||
|
showNotification(result.message || 'Engagement triggered successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to trigger user engagement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTweetSubmenu() {
|
||||||
|
const submenu = document.getElementById('tweet-submenu');
|
||||||
|
submenu.style.display = submenu.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerShareTweet() {
|
||||||
|
const selectedServer = document.getElementById('server-select').value;
|
||||||
|
const tweetUrl = document.getElementById('tweet-url').value.trim();
|
||||||
|
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (selectedServer !== 'all') {
|
||||||
|
params.append('guild_id', selectedServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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;
|
||||||
|
|
||||||
|
console.log('🛏️ sendBedtime() called');
|
||||||
|
console.log('🛏️ Selected server value:', selectedServer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let endpoint = '/bedtime';
|
||||||
|
|
||||||
|
if (selectedServer !== 'all') {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
endpoint = useWebhook ? '/manual/send-webhook' : '/manual/send';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('message', message);
|
||||||
|
|
||||||
|
if (useWebhook) {
|
||||||
|
formData.append('persona', webhookPersona);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyMessageId) {
|
||||||
|
formData.append('reply_to_message_id', replyMessageId);
|
||||||
|
formData.append('mention_author', replyMention);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType === 'dm') {
|
||||||
|
if (files.length > 0) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('files', files[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 = '';
|
||||||
|
document.getElementById('manualReplyMessageId').value = '';
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Custom Prompt =====
|
||||||
|
|
||||||
|
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';
|
||||||
|
if (targetType.value === 'dm') {
|
||||||
|
targetType.value = 'channel';
|
||||||
|
toggleManualMessageTarget();
|
||||||
|
}
|
||||||
|
targetType.options[1].disabled = true;
|
||||||
|
} else {
|
||||||
|
webhookOptions.style.display = 'none';
|
||||||
|
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') {
|
||||||
|
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 {
|
||||||
|
const selectedServer = document.getElementById('custom-prompt-server-select').value;
|
||||||
|
endpoint = '/autonomous/custom';
|
||||||
|
|
||||||
|
if (selectedServer !== 'all') {
|
||||||
|
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 = '';
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCustomPrompt() {
|
||||||
|
const customPromptSection = document.getElementById('custom-prompt-section');
|
||||||
|
if (customPromptSection) {
|
||||||
|
customPromptSection.style.display = customPromptSection.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Add Reaction =====
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
498
bot/static/js/chat.js
Normal file
498
bot/static/js/chat.js
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Chat Interface + Voice Call Module
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
412
bot/static/js/core.js
Normal file
412
bot/static/js/core.js
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Core Module
|
||||||
|
// Global variables, utility functions, tab switching, initialization, polling
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
let currentMood = 'neutral';
|
||||||
|
let voiceCallActive = false;
|
||||||
|
let voiceCallHistory = [];
|
||||||
|
let servers = [];
|
||||||
|
let evilMode = false;
|
||||||
|
let bipolarMode = false;
|
||||||
|
let selectedGPU = 'nvidia';
|
||||||
|
let chatConversationHistory = [];
|
||||||
|
let pfpCropper = null;
|
||||||
|
let albumEntries = [];
|
||||||
|
let albumSelectedId = null;
|
||||||
|
let albumChecked = new Set();
|
||||||
|
let albumCropper = null;
|
||||||
|
let albumOpen = false;
|
||||||
|
let activitiesData = null;
|
||||||
|
let activitiesOpen = false;
|
||||||
|
let activitiesSections = { normal: false, evil: false };
|
||||||
|
let activitiesEditing = {};
|
||||||
|
let activitiesEditCache = {};
|
||||||
|
let currentEditMemory = null;
|
||||||
|
let logsAutoScroll = true;
|
||||||
|
let notificationTimer = null;
|
||||||
|
let statusInterval = null;
|
||||||
|
let logsInterval = null;
|
||||||
|
let argsInterval = null;
|
||||||
|
|
||||||
|
// 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": "👑"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Utility functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJsonForAttribute(obj) {
|
||||||
|
return JSON.stringify(obj)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tab switching
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function switchTab(tabId) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.tab-button').forEach(button => {
|
||||||
|
button.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
|
||||||
|
const activeBtn = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
|
||||||
|
if (activeBtn) activeBtn.classList.add('active');
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
if (tabId === 'tab11') {
|
||||||
|
console.log('🖼️ Loading Profile Picture tab');
|
||||||
|
loadPfpTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTabLoading(tabId) {
|
||||||
|
const tab = document.getElementById(tabId);
|
||||||
|
if (!tab) return;
|
||||||
|
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;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Polling
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initialization helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLogsScrollDetection() {
|
||||||
|
const logsPanel = document.getElementById('logs-panel');
|
||||||
|
if (!logsPanel) return;
|
||||||
|
logsPanel.addEventListener('scroll', function() {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Log functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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('');
|
||||||
|
|
||||||
|
if (logsAutoScroll) {
|
||||||
|
scrollLogsToBottom();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Prompt source toggle (shared between core and status modules)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Profile picture metadata (stub — actual loading in profile.js)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function loadProfilePictureMetadata() {
|
||||||
|
// Delegated to PFP tab loader — only runs if tab11 is active
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DOMContentLoaded — main initialization
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initTabState();
|
||||||
|
initTabWheelScroll();
|
||||||
|
initLogsScrollDetection();
|
||||||
|
initChatImagePreview();
|
||||||
|
initModalAccessibility();
|
||||||
|
initPromptSourceToggle();
|
||||||
|
|
||||||
|
loadStatus();
|
||||||
|
loadServers();
|
||||||
|
populateMoodDropdowns();
|
||||||
|
loadLastPrompt();
|
||||||
|
loadLogs();
|
||||||
|
checkEvilModeStatus();
|
||||||
|
checkBipolarModeStatus();
|
||||||
|
checkGPUStatus();
|
||||||
|
refreshLanguageStatus();
|
||||||
|
refreshFigurineSubscribers();
|
||||||
|
loadProfilePictureMetadata();
|
||||||
|
loadVoiceDebugMode();
|
||||||
|
|
||||||
|
initVisibilityPolling();
|
||||||
|
startPolling();
|
||||||
|
|
||||||
|
// Modal keyboard close handler
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
548
bot/static/js/dm.js
Normal file
548
bot/static/js/dm.js
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — DM Management Module
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
bot/static/js/image-gen.js
Normal file
127
bot/static/js/image-gen.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Image Generation Module
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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';
|
||||||
|
|
||||||
|
if (result.image_path) {
|
||||||
|
const filename = result.image_path.split('/').pop();
|
||||||
|
const imageUrl = `/image/view/${encodeURIComponent(filename)}`;
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
446
bot/static/js/memories.js
Normal file
446
bot/static/js/memories.js
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Memory Management Module
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
// currentEditMemory declared in core.js
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
396
bot/static/js/modes.js
Normal file
396
bot/static/js/modes.js
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Modes Module
|
||||||
|
// Evil Mode, GPU Selection, Bipolar Mode
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ===== Evil Mode Functions =====
|
||||||
|
|
||||||
|
async function checkEvilModeStatus() {
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/evil-mode');
|
||||||
|
evilMode = result.evil_mode;
|
||||||
|
updateEvilModeUI();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBipolarToggleVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== GPU Selection Management =====
|
||||||
|
|
||||||
|
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 =====
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (bipolarSection) {
|
||||||
|
bipolarSection.style.display = 'block';
|
||||||
|
loadScoreboard();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toggleBtn.textContent = '🔄 Bipolar: OFF';
|
||||||
|
toggleBtn.style.background = '#333';
|
||||||
|
toggleBtn.style.borderColor = '#666';
|
||||||
|
toggleBtn.disabled = false;
|
||||||
|
|
||||||
|
if (bipolarSection) {
|
||||||
|
bipolarSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBipolarToggleVisibility() {
|
||||||
|
const bipolarToggle = document.getElementById('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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(channelIdInput)) {
|
||||||
|
showNotification('Invalid channel ID format - should be a number', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
context: context
|
||||||
|
};
|
||||||
|
|
||||||
|
if (messageIdInput) {
|
||||||
|
requestBody.message_id = messageIdInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
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!`);
|
||||||
|
|
||||||
|
document.getElementById('bipolar-context').value = '';
|
||||||
|
document.getElementById('bipolar-message-id').value = '';
|
||||||
|
|
||||||
|
loadActiveArguments();
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1127
bot/static/js/profile.js
Normal file
1127
bot/static/js/profile.js
Normal file
File diff suppressed because it is too large
Load Diff
684
bot/static/js/servers.js
Normal file
684
bot/static/js/servers.js
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
// ===== Server Management Functions =====
|
||||||
|
|
||||||
|
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 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
286
bot/static/js/status.js
Normal file
286
bot/static/js/status.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Miku Control Panel — Status Module
|
||||||
|
// Status display, last prompt, autonomous stats
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ===== Status =====
|
||||||
|
|
||||||
|
async function loadStatus() {
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/status');
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Last Prompt =====
|
||||||
|
|
||||||
|
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 = result.full_prompt + `\n\n${'═'.repeat(60)}\n[Cat Response]\n${result.response}`;
|
||||||
|
} else {
|
||||||
|
infoEl.textContent = '';
|
||||||
|
promptEl.textContent = result.full_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Autonomous Stats =====
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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';
|
||||||
|
if (value >= 0.6) return '#8bc34a';
|
||||||
|
if (value >= 0.4) return '#ffc107';
|
||||||
|
if (value >= 0.2) return '#ff9800';
|
||||||
|
return '#f44336';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMomentumColor(value) {
|
||||||
|
if (value >= 0.7) return '#4caf50';
|
||||||
|
if (value >= 0.4) return '#2196f3';
|
||||||
|
return '#9e9e9e';
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentValue && servers.some(s => String(s.guild_id) === currentValue)) {
|
||||||
|
select.value = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ Supports 5 activity types: listening, playing, watching, competing, streaming.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import yaml
|
import yaml
|
||||||
import discord
|
import discord
|
||||||
@@ -22,6 +24,9 @@ logger = get_logger('activity')
|
|||||||
|
|
||||||
ACTIVITIES_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "activities.yaml")
|
ACTIVITIES_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "activities.yaml")
|
||||||
|
|
||||||
|
# Discord activity name character limit
|
||||||
|
DISCORD_ACTIVITY_NAME_MAX = 128
|
||||||
|
|
||||||
# All valid activity types
|
# All valid activity types
|
||||||
VALID_ACTIVITY_TYPES = {"listening", "playing", "watching", "competing", "streaming"}
|
VALID_ACTIVITY_TYPES = {"listening", "playing", "watching", "competing", "streaming"}
|
||||||
|
|
||||||
@@ -56,6 +61,9 @@ ACTIVITY_PROBABILITY = {
|
|||||||
"manic": 0.85,
|
"manic": 0.85,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Thread lock for all shared mutable state ──
|
||||||
|
_state_lock = threading.Lock()
|
||||||
|
|
||||||
# ── Manual override state ──
|
# ── Manual override state ──
|
||||||
_manual_override = False
|
_manual_override = False
|
||||||
_manual_override_until = 0.0 # Unix timestamp; 0 = no override
|
_manual_override_until = 0.0 # Unix timestamp; 0 = no override
|
||||||
@@ -74,9 +82,10 @@ _cache_mtime = 0.0
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _load_activities(force=False):
|
def _load_activities(force=False):
|
||||||
"""Load activities.yaml with file-mtime-based caching."""
|
"""Load activities.yaml with file-mtime-based caching. Returns a deep copy."""
|
||||||
global _activities_cache, _cache_mtime
|
global _activities_cache, _cache_mtime
|
||||||
|
|
||||||
|
with _state_lock:
|
||||||
try:
|
try:
|
||||||
mtime = os.path.getmtime(ACTIVITIES_FILE)
|
mtime = os.path.getmtime(ACTIVITIES_FILE)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -84,7 +93,9 @@ def _load_activities(force=False):
|
|||||||
return {"normal": {}, "evil": {}}
|
return {"normal": {}, "evil": {}}
|
||||||
|
|
||||||
if not force and _activities_cache is not None and mtime == _cache_mtime:
|
if not force and _activities_cache is not None and mtime == _cache_mtime:
|
||||||
return _activities_cache
|
# Return a deep copy so callers cannot mutate the cache
|
||||||
|
import copy
|
||||||
|
return copy.deepcopy(_activities_cache)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(ACTIVITIES_FILE, "r", encoding="utf-8") as f:
|
with open(ACTIVITIES_FILE, "r", encoding="utf-8") as f:
|
||||||
@@ -92,19 +103,36 @@ def _load_activities(force=False):
|
|||||||
_activities_cache = data
|
_activities_cache = data
|
||||||
_cache_mtime = mtime
|
_cache_mtime = mtime
|
||||||
logger.debug(f"Loaded activities from {ACTIVITIES_FILE}")
|
logger.debug(f"Loaded activities from {ACTIVITIES_FILE}")
|
||||||
return data
|
import copy
|
||||||
|
return copy.deepcopy(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load activities file: {e}")
|
logger.error(f"Failed to load activities file: {e}")
|
||||||
return _activities_cache or {"normal": {}, "evil": {}}
|
if _activities_cache is not None:
|
||||||
|
import copy
|
||||||
|
return copy.deepcopy(_activities_cache)
|
||||||
|
return {"normal": {}, "evil": {}}
|
||||||
|
|
||||||
|
|
||||||
def save_activities(data: dict):
|
def save_activities(data: dict):
|
||||||
"""Write the full activities dict back to YAML."""
|
"""Write the full activities dict back to YAML using atomic write (temp + rename)."""
|
||||||
global _activities_cache, _cache_mtime
|
global _activities_cache, _cache_mtime
|
||||||
|
|
||||||
|
with _state_lock:
|
||||||
try:
|
try:
|
||||||
with open(ACTIVITIES_FILE, "w", encoding="utf-8") as f:
|
# Atomic write: write to temp file in same directory, then rename
|
||||||
|
dir_name = os.path.dirname(ACTIVITIES_FILE)
|
||||||
|
fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".yaml.tmp")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||||
|
os.replace(tmp_path, ACTIVITIES_FILE)
|
||||||
|
except BaseException:
|
||||||
|
# Clean up temp file on failure
|
||||||
|
try:
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
_activities_cache = data
|
_activities_cache = data
|
||||||
_cache_mtime = os.path.getmtime(ACTIVITIES_FILE)
|
_cache_mtime = os.path.getmtime(ACTIVITIES_FILE)
|
||||||
@@ -119,7 +147,7 @@ def save_activities(data: dict):
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def get_all_activities() -> dict:
|
def get_all_activities() -> dict:
|
||||||
"""Return the full activities dict (normal + evil sections)."""
|
"""Return the full activities dict (normal + evil sections). Returns a deep copy."""
|
||||||
return _load_activities()
|
return _load_activities()
|
||||||
|
|
||||||
|
|
||||||
@@ -151,12 +179,16 @@ def set_activities_for_mood(mood_name: str, is_evil: bool, activities: list):
|
|||||||
)
|
)
|
||||||
if not entry.get("name") or not isinstance(entry["name"], str):
|
if not entry.get("name") or not isinstance(entry["name"], str):
|
||||||
raise ValueError(f"Entry {i} must have a non-empty string 'name'")
|
raise ValueError(f"Entry {i} must have a non-empty string 'name'")
|
||||||
|
if len(entry["name"]) > DISCORD_ACTIVITY_NAME_MAX:
|
||||||
|
raise ValueError(f"Entry {i} name exceeds {DISCORD_ACTIVITY_NAME_MAX} characters")
|
||||||
if not isinstance(entry.get("weight", 0), int) or entry.get("weight", 0) < 1:
|
if not isinstance(entry.get("weight", 0), int) or entry.get("weight", 0) < 1:
|
||||||
raise ValueError(f"Entry {i} weight must be a positive integer")
|
raise ValueError(f"Entry {i} weight must be a positive integer")
|
||||||
if "state" in entry and entry["state"] is not None and not isinstance(entry["state"], str):
|
if "state" in entry and entry["state"] is not None and not isinstance(entry["state"], str):
|
||||||
raise ValueError(f"Entry {i} 'state' must be a string if provided")
|
raise ValueError(f"Entry {i} 'state' must be a string if provided")
|
||||||
if "url" in entry and entry["url"] is not None and not isinstance(entry["url"], str):
|
if "url" in entry and entry["url"] is not None and not isinstance(entry["url"], str):
|
||||||
raise ValueError(f"Entry {i} 'url' must be a string if provided")
|
raise ValueError(f"Entry {i} 'url' must be a string if provided")
|
||||||
|
if entry.get("type") == "streaming" and not entry.get("url"):
|
||||||
|
raise ValueError(f"Entry {i} is streaming type but has no url")
|
||||||
|
|
||||||
section = "evil" if is_evil else "normal"
|
section = "evil" if is_evil else "normal"
|
||||||
data = _load_activities()
|
data = _load_activities()
|
||||||
@@ -173,18 +205,43 @@ def set_activities_for_mood(mood_name: str, is_evil: bool, activities: list):
|
|||||||
def pick_activity_for_mood(mood_name: str, is_evil: bool = False):
|
def pick_activity_for_mood(mood_name: str, is_evil: bool = False):
|
||||||
"""Pick a weighted-random activity for a mood.
|
"""Pick a weighted-random activity for a mood.
|
||||||
|
|
||||||
|
Validates entries and skips malformed ones with a warning.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: {"type": ..., "name": ..., "state": ..., "url": ...}
|
dict: {"type": ..., "name": ..., "state": ..., "url": ...}
|
||||||
state and url may be None.
|
state and url may be None.
|
||||||
Returns None if mood has no entries.
|
Returns None if mood has no valid entries.
|
||||||
"""
|
"""
|
||||||
activities = get_activities_for_mood(mood_name, is_evil)
|
activities = get_activities_for_mood(mood_name, is_evil)
|
||||||
|
|
||||||
if not activities:
|
if not activities:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
weights = [entry.get("weight", 1) for entry in activities]
|
# Validate entries, skipping malformed ones
|
||||||
chosen = random.choices(activities, weights=weights, k=1)[0]
|
valid = []
|
||||||
|
weights = []
|
||||||
|
for i, entry in enumerate(activities):
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
logger.warning(f"Skipping non-dict entry {i} in {'evil/' if is_evil else ''}{mood_name}")
|
||||||
|
continue
|
||||||
|
if "type" not in entry or "name" not in entry:
|
||||||
|
logger.warning(f"Skipping entry {i} missing 'type' or 'name' in {'evil/' if is_evil else ''}{mood_name}: {entry}")
|
||||||
|
continue
|
||||||
|
if entry["type"] not in VALID_ACTIVITY_TYPES:
|
||||||
|
logger.warning(f"Skipping entry {i} with unrecognized type '{entry['type']}' in {'evil/' if is_evil else ''}{mood_name}")
|
||||||
|
continue
|
||||||
|
w = entry.get("weight", 1)
|
||||||
|
if not isinstance(w, int) or w < 1:
|
||||||
|
logger.warning(f"Skipping entry {i} with invalid weight {w} in {'evil/' if is_evil else ''}{mood_name}")
|
||||||
|
continue
|
||||||
|
valid.append(entry)
|
||||||
|
weights.append(w)
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
logger.warning(f"No valid entries for {'evil/' if is_evil else ''}{mood_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
chosen = random.choices(valid, weights=weights, k=1)[0]
|
||||||
return {
|
return {
|
||||||
"type": chosen["type"],
|
"type": chosen["type"],
|
||||||
"name": chosen["name"],
|
"name": chosen["name"],
|
||||||
@@ -208,7 +265,8 @@ def should_have_activity(mood_name: str) -> bool:
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def is_manual_override_active() -> bool:
|
def is_manual_override_active() -> bool:
|
||||||
"""Check if a manual override is in effect (hasn't expired)."""
|
"""Check if a manual override is in effect (hasn't expired). Thread-safe."""
|
||||||
|
with _state_lock:
|
||||||
global _manual_override
|
global _manual_override
|
||||||
if not _manual_override:
|
if not _manual_override:
|
||||||
return False
|
return False
|
||||||
@@ -220,15 +278,18 @@ def is_manual_override_active() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def set_manual_override(duration: int = MANUAL_OVERRIDE_DURATION):
|
def set_manual_override(duration: int = MANUAL_OVERRIDE_DURATION):
|
||||||
"""Activate manual override for the given duration (seconds)."""
|
"""Activate manual override for the given duration (seconds). Thread-safe."""
|
||||||
|
with _state_lock:
|
||||||
global _manual_override, _manual_override_until
|
global _manual_override, _manual_override_until
|
||||||
_manual_override = True
|
_manual_override = True
|
||||||
_manual_override_until = time.time() + duration
|
expiry = time.time() + duration
|
||||||
logger.info(f"Manual override activated for {duration}s")
|
_manual_override_until = expiry
|
||||||
|
logger.info(f"Manual override activated for {duration}s (expires at {time.strftime('%H:%M:%S', time.localtime(expiry))})")
|
||||||
|
|
||||||
|
|
||||||
def clear_manual_override():
|
def clear_manual_override():
|
||||||
"""Deactivate manual override immediately."""
|
"""Deactivate manual override immediately. Thread-safe."""
|
||||||
|
with _state_lock:
|
||||||
global _manual_override, _manual_override_until
|
global _manual_override, _manual_override_until
|
||||||
_manual_override = False
|
_manual_override = False
|
||||||
_manual_override_until = 0.0
|
_manual_override_until = 0.0
|
||||||
@@ -240,13 +301,15 @@ def clear_manual_override():
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def get_current_activity():
|
def get_current_activity():
|
||||||
"""Return the current activity dict or None if idle."""
|
"""Return the current activity dict or None if idle. Thread-safe."""
|
||||||
|
with _state_lock:
|
||||||
return _current_activity
|
return _current_activity
|
||||||
|
|
||||||
|
|
||||||
def _set_current_activity(activity_dict):
|
def _set_current_activity(activity_dict):
|
||||||
"""Update the tracked current activity."""
|
"""Update the tracked current activity. Thread-safe."""
|
||||||
global _current_activity
|
global _current_activity
|
||||||
|
with _state_lock:
|
||||||
_current_activity = activity_dict
|
_current_activity = activity_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -255,10 +318,13 @@ def _set_current_activity(activity_dict):
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _build_activity(payload: dict):
|
def _build_activity(payload: dict):
|
||||||
"""Build a discord.Activity (or discord.Streaming) from a payload dict."""
|
"""Build a discord.Activity (or discord.Streaming) from a payload dict.
|
||||||
|
|
||||||
|
Logs a warning if the activity type is unrecognized (falls back to playing).
|
||||||
|
"""
|
||||||
atype = payload["type"]
|
atype = payload["type"]
|
||||||
name = payload["name"]
|
name = payload["name"]
|
||||||
state = payload.get("state")
|
state = payload.get("state") or None # normalize empty string to None
|
||||||
url = payload.get("url")
|
url = payload.get("url")
|
||||||
|
|
||||||
if atype == "streaming" and url:
|
if atype == "streaming" and url:
|
||||||
@@ -271,8 +337,12 @@ def _build_activity(payload: dict):
|
|||||||
"competing": discord.ActivityType.competing,
|
"competing": discord.ActivityType.competing,
|
||||||
"streaming": discord.ActivityType.streaming, # fallback without url
|
"streaming": discord.ActivityType.streaming, # fallback without url
|
||||||
}
|
}
|
||||||
|
resolved_type = type_map.get(atype)
|
||||||
|
if resolved_type is None:
|
||||||
|
logger.warning(f"Unrecognized activity type '{atype}', falling back to 'playing'")
|
||||||
|
resolved_type = discord.ActivityType.playing
|
||||||
return discord.Activity(
|
return discord.Activity(
|
||||||
type=type_map.get(atype, discord.ActivityType.playing),
|
type=resolved_type,
|
||||||
name=name,
|
name=name,
|
||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
@@ -351,20 +421,22 @@ async def update_bot_presence(mood_name: str, is_evil: bool = False, force: bool
|
|||||||
logger.info(f"Set presence: {label} (mood={'evil/' if is_evil else ''}{mood_name})")
|
logger.info(f"Set presence: {label} (mood={'evil/' if is_evil else ''}{mood_name})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to update bot presence: {e}")
|
logger.error(f"Failed to update bot presence: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
async def set_activity_manual(activity_type: str, name: str, state: str = None, url: str = None):
|
async def set_activity_manual(activity_type: str, name: str, state: str = None, url: str = None):
|
||||||
"""Manually set the bot's activity (bypasses mood system).
|
"""Manually set the bot's activity (bypasses mood system).
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: if activity_type is invalid or streaming lacks url
|
ValueError: if activity_type is invalid, name too long, or streaming lacks url
|
||||||
RuntimeError: if bot is not ready
|
RuntimeError: if bot is not ready
|
||||||
"""
|
"""
|
||||||
if activity_type not in VALID_ACTIVITY_TYPES:
|
if activity_type not in VALID_ACTIVITY_TYPES:
|
||||||
raise ValueError(f"Invalid type '{activity_type}', must be one of: {', '.join(sorted(VALID_ACTIVITY_TYPES))}")
|
raise ValueError(f"Invalid type '{activity_type}', must be one of: {', '.join(sorted(VALID_ACTIVITY_TYPES))}")
|
||||||
if not name or not isinstance(name, str):
|
if not name or not isinstance(name, str):
|
||||||
raise ValueError("name must be a non-empty string")
|
raise ValueError("name must be a non-empty string")
|
||||||
|
if len(name) > DISCORD_ACTIVITY_NAME_MAX:
|
||||||
|
raise ValueError(f"name exceeds {DISCORD_ACTIVITY_NAME_MAX} characters")
|
||||||
if activity_type == "streaming" and not url:
|
if activity_type == "streaming" and not url:
|
||||||
raise ValueError("streaming type requires a url")
|
raise ValueError("streaming type requires a url")
|
||||||
|
|
||||||
@@ -401,13 +473,20 @@ async def clear_activity_manual():
|
|||||||
|
|
||||||
|
|
||||||
async def release_manual_override():
|
async def release_manual_override():
|
||||||
"""Release manual override and immediately recalculate presence from current mood."""
|
"""Release manual override and immediately recalculate presence from current mood.
|
||||||
|
|
||||||
|
Uses force=True so the bot always gets an activity instead of potentially
|
||||||
|
going idle right away (which would be confusing UX after clicking "Return to Auto").
|
||||||
|
"""
|
||||||
clear_manual_override()
|
clear_manual_override()
|
||||||
|
try:
|
||||||
if globals.EVIL_MODE:
|
if globals.EVIL_MODE:
|
||||||
mood = globals.EVIL_DM_MOOD
|
mood = globals.EVIL_DM_MOOD
|
||||||
is_evil = True
|
is_evil = True
|
||||||
else:
|
else:
|
||||||
mood = globals.DM_MOOD
|
mood = globals.DM_MOOD
|
||||||
is_evil = False
|
is_evil = False
|
||||||
await update_bot_presence(mood, is_evil=is_evil, force=False)
|
await update_bot_presence(mood, is_evil=is_evil, force=True)
|
||||||
logger.info(f"Released manual override, recalculated for mood={'evil/' if is_evil else ''}{mood}")
|
logger.info(f"Released manual override, set presence for mood={'evil/' if is_evil else ''}{mood}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to recalculate presence after releasing override: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user