Implement Bipolar Mode: Dual persona arguments with webhooks, LLM arbiter, and persistent scoreboard
Major Features: - Complete Bipolar Mode system allowing Regular Miku and Evil Miku to coexist and argue via webhooks - LLM arbiter system using neutral model to judge argument winners with detailed reasoning - Persistent scoreboard tracking wins, percentages, and last 50 results with timestamps and reasoning - Automatic mode switching based on argument winner - Webhook management per channel with profile pictures and display names - Progressive probability system for dynamic argument lengths (starts at 10%, increases 5% per exchange, min 4 exchanges) - Draw handling with penalty system (-5% end chance, continues argument) - Integration with autonomous system for random argument triggers Argument System: - MIN_EXCHANGES = 4, progressive end chance starting at 10% - Enhanced prompts for both personas (strategic, short, punchy responses 1-3 sentences) - Evil Miku triumphant victory messages with gloating and satisfaction - Regular Miku assertive defense (not passive, shows backbone) - Message-based argument starting (can respond to specific messages via ID) - Conversation history tracking per argument with special user_id - Full context queries (personality, lore, lyrics, last 8 messages) LLM Arbiter: - Decisive prompt emphasizing picking winners (draws should be rare) - Improved parsing with first-line exact matching and fallback counting - Debug logging for decision transparency - Arbiter reasoning stored in scoreboard history for review - Uses neutral TEXT_MODEL (not evil) for unbiased judgment Web UI & API: - Bipolar mode toggle button (only visible when evil mode is on) - Channel ID + Message ID input fields for argument triggering - Scoreboard display with win percentages and recent history - Manual argument trigger endpoint with string-based IDs - GET /bipolar-mode/scoreboard endpoint for stats retrieval - Real-time active arguments tracking (refreshes every 5 seconds) Prompt Optimizations: - All argument prompts limited to 1-3 sentences for impact - Evil Miku system prompt with variable response length guidelines - Removed walls of text, emphasizing brevity and precision - "Sometimes the cruelest response is the shortest one" Evil Miku Updates: - Added height to lore (15.8m tall, 10x bigger than regular Miku) - Height added to prompt facts for size-based belittling - More strategic and calculating personality in arguments Integration: - Bipolar mode state restoration on bot startup - Bot skips processing messages during active arguments - Autonomous system checks for bipolar triggers after actions - Import fixes (apply_evil_mode_changes/revert_evil_mode_changes) Technical Details: - State persistence via JSON (bipolar_mode_state.json, bipolar_webhooks.json, bipolar_scoreboard.json) - Webhook caching per guild with fallback creation - Event loop management with asyncio.create_task - Rate limiting and argument conflict prevention - Globals integration (BIPOLAR_MODE, BIPOLAR_WEBHOOKS, BIPOLAR_ARGUMENT_IN_PROGRESS, MOOD_EMOJIS) Files Changed: - bot/bot.py: Added bipolar mode restoration and argument-in-progress checks - bot/globals.py: Added bipolar mode state variables and mood emoji mappings - bot/utils/bipolar_mode.py: Complete 1106-line implementation - bot/utils/autonomous.py: Added bipolar argument trigger checks - bot/utils/evil_mode.py: Updated system prompt, added height info to lore/prompt - bot/api.py: Added bipolar mode endpoints (trigger, toggle, scoreboard) - bot/static/index.html: Added bipolar controls section with scoreboard - bot/memory/: Various DM conversation updates - bot/evil_miku_lore.txt: Added height description - bot/evil_miku_prompt.txt: Added height to facts, updated personality guidelines
This commit is contained in:
@@ -615,6 +615,20 @@
|
||||
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;
|
||||
}
|
||||
|
||||
body.evil-mode #bipolar-mode-toggle.bipolar-active {
|
||||
background: #9932CC !important;
|
||||
border-color: #9932CC !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -622,9 +636,14 @@
|
||||
<div class="panel">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h1 id="panel-title">Miku Control Panel</h1>
|
||||
<button id="evil-mode-toggle" onclick="toggleEvilMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
😈 Evil Mode: OFF
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button id="bipolar-mode-toggle" onclick="toggleBipolarMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold; display: none;">
|
||||
🔄 Bipolar: OFF
|
||||
</button>
|
||||
<button id="evil-mode-toggle" onclick="toggleEvilMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||
😈 Evil Mode: OFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: #ccc; margin-bottom: 2rem;">
|
||||
💬 <strong>DM Support:</strong> Users can message Miku directly in DMs. She responds to every message using the DM mood (auto-rotating every 2 hours).
|
||||
@@ -782,6 +801,55 @@
|
||||
<button onclick="toggleCustomPrompt()">Custom Prompt</button>
|
||||
</div>
|
||||
|
||||
<!-- Bipolar Mode Section (only visible when bipolar mode is on) -->
|
||||
<div id="bipolar-section" class="section" style="display: none; border: 2px solid #9932CC; padding: 1rem; border-radius: 8px; background: #1a1a2e;">
|
||||
<h3 style="color: #9932CC;">🔄 Bipolar Mode Controls</h3>
|
||||
<p style="font-size: 0.9rem; color: #aaa;">Trigger arguments between Regular Miku and Evil Miku</p>
|
||||
|
||||
<div style="margin-bottom: 1rem; display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="bipolar-channel-id">Channel ID:</label>
|
||||
<input type="text" id="bipolar-channel-id" placeholder="e.g., 1234567890123456789" style="width: 200px; margin-left: 0.5rem; font-family: monospace;">
|
||||
</div>
|
||||
<div>
|
||||
<label for="bipolar-message-id">Starting Message ID (optional):</label>
|
||||
<input type="text" id="bipolar-message-id" placeholder="e.g., 1234567890123456789" style="width: 200px; margin-left: 0.5rem; font-family: monospace;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.8rem; color: #888; margin-bottom: 1rem;">
|
||||
💡 <strong>Tip:</strong> Right-click a message in Discord and select "Copy Message ID" (enable Developer Mode in Discord settings).
|
||||
If a starting message ID is provided, the opposite persona will respond to that message.
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label for="bipolar-context">Argument Context (optional):</label>
|
||||
<input type="text" id="bipolar-context" placeholder="e.g., They're fighting about who's the real Miku..." style="width: 100%; margin-top: 0.3rem;">
|
||||
</div>
|
||||
|
||||
<button onclick="triggerBipolarArgument()" style="background: #9932CC; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;">
|
||||
⚔️ Trigger Argument
|
||||
</button>
|
||||
|
||||
<div id="bipolar-status" style="margin-top: 1rem; font-size: 0.9rem;"></div>
|
||||
|
||||
<!-- Scoreboard Display -->
|
||||
<div id="bipolar-scoreboard" style="margin-top: 1.5rem; padding: 1rem; background: #0f0f1e; border-radius: 8px; border: 1px solid #444;">
|
||||
<h4 style="color: #9932CC; margin-bottom: 0.5rem;">🏆 Argument Scoreboard</h4>
|
||||
<div id="scoreboard-content" style="font-size: 0.9rem;">
|
||||
<p style="color: #888;">Loading scoreboard...</p>
|
||||
</div>
|
||||
<button onclick="loadScoreboard()" style="margin-top: 0.5rem; background: #444; color: #fff; border: none; padding: 0.3rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem;">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="active-arguments" style="margin-top: 1rem; display: none;">
|
||||
<h4>Active Arguments:</h4>
|
||||
<div id="active-arguments-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>🎨 Profile Picture</h3>
|
||||
<p style="font-size: 0.9rem; color: #aaa;">Change Miku's profile picture using Danbooru search or upload a custom image.</p>
|
||||
@@ -1325,6 +1393,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadLastPrompt();
|
||||
loadLogs();
|
||||
checkEvilModeStatus(); // Check evil mode on load
|
||||
checkBipolarModeStatus(); // Check bipolar mode on load
|
||||
console.log('🚀 DOMContentLoaded - initializing figurine subscribers list');
|
||||
refreshFigurineSubscribers();
|
||||
loadProfilePictureMetadata();
|
||||
@@ -1332,6 +1401,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set up periodic updates
|
||||
setInterval(loadStatus, 10000);
|
||||
setInterval(loadLogs, 5000);
|
||||
setInterval(loadActiveArguments, 5000); // Refresh active arguments
|
||||
});
|
||||
|
||||
// Utility functions
|
||||
@@ -2097,6 +2167,251 @@ function updateEvilModeUI() {
|
||||
<option value="sleepy">🌙 sleepy</option>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update bipolar mode toggle visibility (only show when evil mode is on)
|
||||
updateBipolarToggleVisibility();
|
||||
}
|
||||
|
||||
// Bipolar Mode Management
|
||||
let bipolarMode = false;
|
||||
|
||||
async function checkBipolarModeStatus() {
|
||||
try {
|
||||
const response = await fetch('/bipolar-mode');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
bipolarMode = data.bipolar_mode;
|
||||
updateBipolarModeUI();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check bipolar mode status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBipolarMode() {
|
||||
try {
|
||||
const toggleBtn = document.getElementById('bipolar-mode-toggle');
|
||||
toggleBtn.disabled = true;
|
||||
toggleBtn.textContent = '⏳ Switching...';
|
||||
|
||||
const result = await apiCall('/bipolar-mode/toggle', 'POST');
|
||||
bipolarMode = result.bipolar_mode;
|
||||
updateBipolarModeUI();
|
||||
|
||||
if (bipolarMode) {
|
||||
showNotification('🔄 Bipolar Mode enabled! Both Mikus can now argue...');
|
||||
} else {
|
||||
showNotification('🔄 Bipolar Mode disabled.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle bipolar mode:', error);
|
||||
showNotification('Failed to toggle bipolar mode: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateBipolarModeUI() {
|
||||
const toggleBtn = document.getElementById('bipolar-mode-toggle');
|
||||
const bipolarSection = document.getElementById('bipolar-section');
|
||||
|
||||
if (bipolarMode) {
|
||||
toggleBtn.textContent = '🔄 Bipolar: ON';
|
||||
toggleBtn.style.background = '#9932CC';
|
||||
toggleBtn.style.borderColor = '#9932CC';
|
||||
toggleBtn.disabled = false;
|
||||
|
||||
// Show bipolar controls section
|
||||
if (bipolarSection) {
|
||||
bipolarSection.style.display = 'block';
|
||||
// Load scoreboard when section becomes visible
|
||||
loadScoreboard();
|
||||
}
|
||||
} else {
|
||||
toggleBtn.textContent = '🔄 Bipolar: OFF';
|
||||
toggleBtn.style.background = '#333';
|
||||
toggleBtn.style.borderColor = '#666';
|
||||
toggleBtn.disabled = false;
|
||||
|
||||
// Hide bipolar controls section
|
||||
if (bipolarSection) {
|
||||
bipolarSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateBipolarToggleVisibility() {
|
||||
const bipolarToggle = document.getElementById('bipolar-mode-toggle');
|
||||
if (evilMode) {
|
||||
bipolarToggle.style.display = 'block';
|
||||
} else {
|
||||
bipolarToggle.style.display = 'none';
|
||||
// Also disable bipolar mode if evil mode is turned off
|
||||
if (bipolarMode) {
|
||||
apiCall('/bipolar-mode/disable', 'POST').then(() => {
|
||||
bipolarMode = false;
|
||||
updateBipolarModeUI();
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerBipolarArgument() {
|
||||
const channelIdInput = document.getElementById('bipolar-channel-id').value.trim();
|
||||
const messageIdInput = document.getElementById('bipolar-message-id').value.trim();
|
||||
const context = document.getElementById('bipolar-context').value;
|
||||
const statusDiv = document.getElementById('bipolar-status');
|
||||
|
||||
if (!channelIdInput) {
|
||||
showNotification('Please enter a channel ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate channel ID format (should be numeric)
|
||||
if (!/^\d+$/.test(channelIdInput)) {
|
||||
showNotification('Invalid channel ID format - should be a number', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message ID format if provided
|
||||
if (messageIdInput && !/^\d+$/.test(messageIdInput)) {
|
||||
showNotification('Invalid message ID format - should be a number', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
statusDiv.innerHTML = '<span style="color: #9932CC;">⏳ Triggering argument...</span>';
|
||||
|
||||
const requestBody = {
|
||||
channel_id: channelIdInput, // Send as string to avoid JS integer precision issues
|
||||
context: context
|
||||
};
|
||||
|
||||
if (messageIdInput) {
|
||||
requestBody.message_id = messageIdInput; // Send as string
|
||||
}
|
||||
|
||||
const result = await apiCall('/bipolar-mode/trigger-argument', 'POST', requestBody);
|
||||
|
||||
if (result.status === 'error') {
|
||||
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${result.message}</span>`;
|
||||
showNotification(result.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = `<span style="color: #00ff00;">✅ ${result.message}</span>`;
|
||||
showNotification(`⚔️ Argument triggered!`);
|
||||
|
||||
// Clear the inputs
|
||||
document.getElementById('bipolar-context').value = '';
|
||||
document.getElementById('bipolar-message-id').value = '';
|
||||
|
||||
// Refresh active arguments
|
||||
loadActiveArguments();
|
||||
|
||||
// Refresh scoreboard after triggering
|
||||
loadScoreboard();
|
||||
} catch (error) {
|
||||
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${error.message}</span>`;
|
||||
showNotification('Failed to trigger argument: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScoreboard() {
|
||||
const scoreboardContent = document.getElementById('scoreboard-content');
|
||||
|
||||
try {
|
||||
const result = await apiCall('/bipolar-mode/scoreboard', 'GET');
|
||||
|
||||
if (result.status === 'error') {
|
||||
scoreboardContent.innerHTML = `<p style="color: #ff4444;">Failed to load scoreboard</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const { scoreboard } = result;
|
||||
const total = scoreboard.total_arguments;
|
||||
|
||||
if (total === 0) {
|
||||
scoreboardContent.innerHTML = `<p style="color: #888;">No arguments have been judged yet.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const mikuPct = total > 0 ? ((scoreboard.miku_wins / total) * 100).toFixed(1) : 0;
|
||||
const evilPct = total > 0 ? ((scoreboard.evil_wins / total) * 100).toFixed(1) : 0;
|
||||
|
||||
let html = `
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.8rem;">
|
||||
<div style="text-align: center; flex: 1;">
|
||||
<div style="color: #86cecb; font-size: 1.2rem; font-weight: bold;">${scoreboard.miku_wins}</div>
|
||||
<div style="color: #888; font-size: 0.85rem;">Hatsune Miku</div>
|
||||
<div style="color: #999; font-size: 0.75rem;">${mikuPct}%</div>
|
||||
</div>
|
||||
<div style="align-self: center; color: #666; font-size: 1.2rem;">vs</div>
|
||||
<div style="text-align: center; flex: 1;">
|
||||
<div style="color: #D60004; font-size: 1.2rem; font-weight: bold;">${scoreboard.evil_wins}</div>
|
||||
<div style="color: #888; font-size: 0.85rem;">Evil Miku</div>
|
||||
<div style="color: #999; font-size: 0.75rem;">${evilPct}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; color: #aaa; font-size: 0.85rem; border-top: 1px solid #333; padding-top: 0.5rem;">
|
||||
Total Arguments: ${total}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show recent history if available
|
||||
if (scoreboard.history && scoreboard.history.length > 0) {
|
||||
html += `<div style="margin-top: 0.8rem; padding-top: 0.8rem; border-top: 1px solid #333;">
|
||||
<div style="color: #888; font-size: 0.8rem; margin-bottom: 0.3rem;">Recent Results:</div>`;
|
||||
|
||||
scoreboard.history.reverse().forEach(entry => {
|
||||
const winnerName = entry.winner === 'evil' ? 'Evil Miku' : 'Hatsune Miku';
|
||||
const winnerColor = entry.winner === 'evil' ? '#D60004' : '#86cecb';
|
||||
const date = new Date(entry.timestamp).toLocaleString();
|
||||
|
||||
html += `<div style="font-size: 0.75rem; color: #666; margin-bottom: 0.2rem;">
|
||||
<span style="color: ${winnerColor};">🏆 ${winnerName}</span> (${entry.exchanges} exchanges) - ${date}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
scoreboardContent.innerHTML = html;
|
||||
} catch (error) {
|
||||
scoreboardContent.innerHTML = `<p style="color: #ff4444;">Error loading scoreboard</p>`;
|
||||
console.error('Scoreboard error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActiveArguments() {
|
||||
try {
|
||||
const response = await fetch('/bipolar-mode/arguments');
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const container = document.getElementById('active-arguments');
|
||||
const list = document.getElementById('active-arguments-list');
|
||||
|
||||
if (Object.keys(data.active_arguments).length > 0) {
|
||||
container.style.display = 'block';
|
||||
list.innerHTML = '';
|
||||
|
||||
for (const [channelId, argData] of Object.entries(data.active_arguments)) {
|
||||
const div = document.createElement('div');
|
||||
div.style.background = '#2a2a3e';
|
||||
div.style.padding = '0.5rem';
|
||||
div.style.marginBottom = '0.5rem';
|
||||
div.style.borderRadius = '4px';
|
||||
div.innerHTML = `
|
||||
<strong>#${argData.channel_name}</strong><br>
|
||||
<small>Exchanges: ${argData.exchange_count} | Speaker: ${argData.current_speaker}</small>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load active arguments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Autonomous Actions
|
||||
|
||||
Reference in New Issue
Block a user