2025-12-07 17:15:09 +02:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
2026-02-28 23:14:32 +02:00
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-05-02 16:08:47 +03:00
< meta http-equiv = "Cache-Control" content = "no-cache, no-store, must-revalidate" >
< meta http-equiv = "Pragma" content = "no-cache" >
< meta http-equiv = "Expires" content = "0" >
2025-12-07 17:15:09 +02:00
< title > Miku Control Panel< / title >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
< link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css" >
< script src = "https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js" > < / script >
2026-05-02 16:08:47 +03:00
< link rel = "stylesheet" href = "/static/css/style.css?v=20260502" >
2025-12-07 17:15:09 +02:00
< / head >
< body >
< div class = "panel" >
2026-01-02 17:11:58 +02:00
< div style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;" >
2026-01-09 00:03:59 +02:00
< div style = "display: flex; gap: 1rem; align-items: center;" >
< h1 id = "panel-title" > Miku Control Panel< / h1 >
< button id = "gpu-selector-toggle" onclick = "toggleGPU()" style = "background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: 2px solid #4a7bc9; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9rem;" >
🎮 GPU: NVIDIA
< / button >
< / div >
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
2026-01-06 13:57:59 +02:00
< div style = "display: flex; gap: 0.5rem; align-items: center;" >
2026-01-06 23:48:14 +02:00
< 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;" >
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
2026-01-06 13:57:59 +02:00
🔄 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 >
2026-01-02 17:11:58 +02:00
< / div >
2025-12-07 17:15:09 +02:00
< p style = "color: #ccc; margin-bottom: 2rem;" >
💬 < strong > DM Support:< / strong > Users can message Miku directly in DMs. She responds to every message using the DM mood (auto-rotating every 2 hours).
< / p >
<!-- Tab Navigation -->
< div class = "tab-container" >
< div class = "tab-buttons" >
2026-02-28 22:59:12 +02:00
< button class = "tab-button active" data-tab = "tab1" onclick = "switchTab('tab1')" > Server Management< / button >
< button class = "tab-button" data-tab = "tab2" onclick = "switchTab('tab2')" > Actions< / button >
< button class = "tab-button" data-tab = "tab3" onclick = "switchTab('tab3')" > Status< / button >
2026-03-01 00:26:22 +02:00
< button class = "tab-button" data-tab = "tab10" onclick = "switchTab('tab10')" > 📱 DM Management< / button >
2026-02-28 22:59:12 +02:00
< button class = "tab-button" data-tab = "tab4" onclick = "switchTab('tab4')" > ⚙️ LLM Settings< / button >
< button class = "tab-button" data-tab = "tab5" onclick = "switchTab('tab5')" > 🎨 Image Generation< / button >
< button class = "tab-button" data-tab = "tab6" onclick = "switchTab('tab6')" > 📊 Autonomous Stats< / button >
< button class = "tab-button" data-tab = "tab7" onclick = "switchTab('tab7')" > 💬 Chat with LLM< / button >
< button class = "tab-button" data-tab = "tab8" onclick = "switchTab('tab8')" > 📞 Voice Call< / button >
< button class = "tab-button" data-tab = "tab9" onclick = "switchTab('tab9')" > 🧠 Memories< / button >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
< button class = "tab-button" data-tab = "tab11" onclick = "switchTab('tab11')" > 🖼️ Profile Picture< / button >
feat: Implement comprehensive non-hierarchical logging system
- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window
Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler
All settings persist across container restarts via JSON config.
2026-01-10 20:46:19 +02:00
< button class = "tab-button" onclick = "window.location.href='/static/system.html'" > 🎛️ System Settings< / button >
< / div >
2025-12-07 17:15:09 +02:00
<!-- Tab 1 Content -->
< div id = "tab1" class = "tab-content active" >
< div class = "section" >
< label for = "mood" > Mood:< / label >
< select id = "mood" >
2026-03-01 00:28:07 +02:00
< option value = "" > Loading moods...< / option >
2025-12-07 17:15:09 +02:00
< / select >
< button onclick = "setMood()" > Set Mood< / button >
< button onclick = "resetMood()" > Reset Mood< / button >
< button onclick = "calmMiku()" > Calm< / button >
< / div >
< div class = "section" >
< h3 > Server Management< / h3 >
< div id = "servers-list" > < / div >
< div class = "add-server-form" >
< h4 > Add New Server< / h4 >
< div class = "form-row" >
< div class = "form-group" >
< label > Guild ID:< / label >
< input type = "number" id = "new-guild-id" placeholder = "Discord Server ID" >
< / div >
< div class = "form-group" >
< label > Server Name:< / label >
< input type = "text" id = "new-guild-name" placeholder = "Server Name" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Autonomous Channel ID:< / label >
< input type = "number" id = "new-autonomous-channel-id" placeholder = "Channel ID" >
< / div >
< div class = "form-group" >
< label > Channel Name:< / label >
< input type = "text" id = "new-autonomous-channel-name" placeholder = "Channel Name" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Bedtime Channel IDs (comma-separated):< / label >
< input type = "text" id = "new-bedtime-channel-ids" placeholder = "Channel IDs" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Enabled Features:< / label >
< div class = "checkbox-group" >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-autonomous" checked >
< label for = "feature-autonomous" > Autonomous< / label >
< / div >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-bedtime" checked >
< label for = "feature-bedtime" > Bedtime< / label >
< / div >
< div class = "checkbox-item" >
< input type = "checkbox" id = "feature-monday-video" checked >
< label for = "feature-monday-video" > Monday Video< / label >
< / div >
< / div >
< / div >
< / div >
< button onclick = "addServer()" > Add Server< / button >
< / div >
< div style = "margin-top: 1rem;" >
< button onclick = "repairConfig()" style = "background: #ff9800;" > 🔧 Repair Configuration< / button >
< p style = "font-size: 0.9rem; color: #ccc; margin-top: 0.5rem;" >
Use this if you're seeing incorrect server IDs or other configuration issues
< / p >
< / div >
< / div >
< / div >
<!-- Actions Tab Content -->
< div id = "tab2" class = "tab-content" >
< div class = "section" >
< h3 > Autonomous Actions< / h3 >
< div style = "margin-bottom: 1rem;" >
< label for = "server-select" > Target Server:< / label >
< select id = "server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / div >
< button onclick = "triggerAutonomous('general')" > Say Something General< / button >
2025-12-16 23:13:19 +02:00
<!-- Engage User Submenu -->
< div style = "margin-bottom: 1rem;" >
< button onclick = "toggleEngageSubmenu()" > Engage User ▼< / button >
< div id = "engage-submenu" style = "display: none; margin-left: 1rem; margin-top: 0.5rem; padding: 1rem; background: #1e1e1e; border: 1px solid #444; border-radius: 4px;" >
< div style = "margin-bottom: 0.5rem;" >
< label for = "engage-user-id" style = "display: block; margin-bottom: 0.3rem;" > User ID (leave empty for random):< / label >
< input type = "text" id = "engage-user-id" placeholder = "User ID" style = "width: 200px;" >
< / div >
< div style = "margin-bottom: 0.5rem;" >
< label style = "display: block; margin-bottom: 0.3rem;" > Engagement Type:< / label >
< div style = "margin-left: 0.5rem;" >
< label style = "display: block; margin-bottom: 0.2rem;" >
< input type = "radio" name = "engage-type" value = "random" checked > Random (auto-detect)
< / label >
< label style = "display: block; margin-bottom: 0.2rem;" >
< input type = "radio" name = "engage-type" value = "activity" > Activity-based (comment on what they're doing)
< / label >
< label style = "display: block; margin-bottom: 0.2rem;" >
< input type = "radio" name = "engage-type" value = "general" > General conversation
< / label >
< label style = "display: block; margin-bottom: 0.2rem;" >
< input type = "radio" name = "engage-type" value = "status" > Status-based (online/idle/invisible)
< / label >
< / div >
< / div >
< button onclick = "triggerEngageUser()" > 🚀 Engage User< / button >
< / div >
< / div >
2026-02-20 00:53:42 +02:00
<!-- Share Tweet Submenu -->
< div style = "margin-bottom: 1rem;" >
< button onclick = "toggleTweetSubmenu()" > Share Tweet ▼< / button >
< div id = "tweet-submenu" style = "display: none; margin-left: 1rem; margin-top: 0.5rem; padding: 1rem; background: #1e1e1e; border: 1px solid #444; border-radius: 4px;" >
< div style = "margin-bottom: 0.5rem;" >
< label for = "tweet-url" style = "display: block; margin-bottom: 0.3rem;" > Tweet URL (leave empty for auto-fetch):< / label >
< input type = "text" id = "tweet-url" placeholder = "https://x.com/... or https://twitter.com/... or https://fxtwitter.com/..." style = "width: 100%;" >
< / div >
< button onclick = "triggerShareTweet()" > 🐦 Share Tweet< / button >
< / div >
< / div >
2025-12-07 17:15:09 +02:00
< button onclick = "triggerAutonomous('reaction')" > React to Message< / button >
2025-12-10 14:57:59 +02:00
< button onclick = "triggerAutonomous('join-conversation')" > Detect and Join Conversation< / button >
2025-12-07 17:15:09 +02:00
< button onclick = "toggleCustomPrompt()" > Custom Prompt< / button >
< / div >
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
2026-01-06 13:57:59 +02:00
<!-- 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 >
2026-01-09 00:03:59 +02:00
< p style = "font-size: 0.9rem; color: #aaa;" > Trigger arguments or dialogues between Regular Miku and Evil Miku< / p >
<!-- Persona Dialogue Section -->
< div style = "margin-bottom: 2rem; padding: 1rem; background: #252540; border-radius: 8px; border: 1px solid #555;" >
< h4 style = "color: #6B8EFF; margin-bottom: 0.5rem;" > 💬 Trigger Persona Dialogue< / h4 >
< p style = "font-size: 0.85rem; color: #999; margin-bottom: 1rem;" > Start a natural conversation between the personas (can escalate to argument if tension builds)< / p >
< div style = "margin-bottom: 1rem;" >
< label for = "dialogue-message-id" > Message ID:< / label >
< input type = "text" id = "dialogue-message-id" placeholder = "e.g., 1234567890123456789" style = "width: 250px; margin-left: 0.5rem; font-family: monospace;" >
< / div >
< div style = "font-size: 0.8rem; color: #888; margin-bottom: 1rem;" >
💡 < strong > Tip:< / strong > Right-click any bot response message in Discord and select "Copy Message ID". The opposite persona will analyze it and decide whether to interject.
< / div >
< button onclick = "triggerPersonaDialogue()" style = "background: #6B8EFF; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;" >
💬 Trigger Dialogue
< / button >
< div id = "dialogue-status" style = "margin-top: 1rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- Argument Section -->
< div style = "padding: 1rem; background: #2e1a2e; border-radius: 8px; border: 1px solid #555;" >
< h4 style = "color: #9932CC; margin-bottom: 0.5rem;" > ⚔️ Trigger Argument< / h4 >
< p style = "font-size: 0.85rem; color: #999; margin-bottom: 1rem;" > Force an immediate argument (bypasses dialogue system)< / p >
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
2026-01-06 13:57:59 +02:00
< 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;" >
2026-04-30 12:30:49 +03:00
< label for = "bipolar-context" > What should they argue about? (optional):< / label >
< input type = "text" id = "bipolar-context" placeholder = "e.g., Who's the real Miku? Whether kindness is weakness. A petty grudge..." style = "width: 100%; margin-top: 0.3rem;" >
2026-04-30 12:07:28 +03:00
< div style = "font-size: 0.75rem; color: #777; margin-top: 0.2rem;" >
2026-04-30 12:30:49 +03:00
Leave blank for a random topic. Write anything to set the argument's theme.
2026-04-30 12:07:28 +03:00
< / div >
< / div >
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
2026-01-06 13:57:59 +02:00
< 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 >
2026-01-09 00:03:59 +02:00
< / div >
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
2026-01-06 13:57:59 +02:00
<!-- 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 >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
2025-12-07 17:15:09 +02:00
< div class = "section" >
< h3 > Figurine DM Subscribers< / h3 >
<!-- Subscriber Management -->
< div style = "margin-bottom: 1rem;" >
< h4 > Subscriber Management< / h4 >
< div style = "margin-bottom: 0.5rem;" >
< button onclick = "refreshFigurineSubscribers()" > 🔄 Refresh< / button >
< / div >
< div style = "display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;" >
< div >
< label for = "figurine-user-id" > User ID:< / label >
< input type = "text" id = "figurine-user-id" placeholder = "Discord User ID (as string)" / >
< / div >
< button onclick = "addFigurineSubscriber()" > ➕ Add Subscriber< / button >
< / div >
< div id = "figurine-subscribers-list" > < / div >
< / div >
<!-- Send to All Subscribers -->
< div style = "margin-bottom: 1rem; border-top: 1px solid #444; padding-top: 1rem;" >
< h4 > Send to All Subscribers< / h4 >
< div style = "margin-bottom: 0.5rem;" >
< label for = "figurine-tweet-url-all" > Tweet URL (optional):< / label >
< input type = "text" id = "figurine-tweet-url-all" placeholder = "https://twitter.com/username/status/..." style = "width: 300px;" / >
< / div >
< button onclick = "sendFigurineNowToAll()" > 📨 Send to All Subscribers< / button >
< div id = "figurine-all-status" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- Send to Single User -->
< div style = "border-top: 1px solid #444; padding-top: 1rem;" >
< h4 > Send to Single User< / h4 >
< div style = "display: flex; gap: 10px; align-items: end; margin-bottom: 0.5rem;" >
< div >
< label for = "figurine-single-user-id" > User ID:< / label >
< input type = "text" id = "figurine-single-user-id" placeholder = "Discord User ID" / >
< / div >
< div >
< label for = "figurine-tweet-url-single" > Tweet URL (optional):< / label >
< input type = "text" id = "figurine-tweet-url-single" placeholder = "https://twitter.com/username/status/..." style = "width: 250px;" / >
< / div >
< button onclick = "sendFigurineToSingleUser()" > 📨 Send to User< / button >
< / div >
< div id = "figurine-single-status" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
< / div >
< div class = "section" >
< h3 > Manual Actions< / h3 >
< div style = "margin-bottom: 1rem;" >
< label for = "manual-server-select" > Target Server:< / label >
< select id = "manual-server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / div >
< button onclick = "forceSleep()" > Force Sleep< / button >
< button onclick = "wakeUp()" > Wake Up< / button >
< button onclick = "sendBedtime()" > Send Bedtime< / button >
< button onclick = "resetConversation()" > Reset Conversation< / button >
< / div >
< div class = "section" id = "custom-prompt-section" >
< h3 > 🎙️ Send Custom Prompt to Miku< / h3 >
<!-- Target Selection -->
< div style = "margin-bottom: 1rem;" >
< label for = "custom-prompt-target-type" > Target Type:< / label >
< select id = "custom-prompt-target-type" onchange = "toggleCustomPromptTarget()" style = "margin-right: 1rem;" >
< option value = "server" > Server< / option >
< option value = "dm" > Direct Message< / option >
< / select >
<!-- Server Selection -->
< span id = "custom-prompt-server-section" >
< label for = "custom-prompt-server-select" > Target Server:< / label >
< select id = "custom-prompt-server-select" >
< option value = "all" > All Servers< / option >
< / select >
< / span >
<!-- DM User ID Input -->
< span id = "custom-prompt-dm-section" style = "display: none;" >
< label for = "custom-prompt-user-id" > User ID:< / label >
< input type = "text" id = "custom-prompt-user-id" placeholder = "Discord User ID" style = "width: 200px;" / >
< / span >
< / div >
< div >
< label for = "customPrompt" > Custom Prompt:< / label >
< textarea id = "customPrompt" placeholder = "e.g. Talk about how nice the weather is today" rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< div style = "margin-top: 0.5rem;" >
< label for = "customPromptAttachment" > Attach File (optional):< / label >
< input type = "file" id = "customPromptAttachment" multiple / >
< / div >
< button onclick = "sendCustomPrompt()" style = "margin-top: 0.5rem;" > Send Custom Prompt< / button >
< p id = "customStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< div class = "section" id = "manual-message-section" >
< h3 > 🎭 Send Message as Miku (Manual Override)< / h3 >
2026-01-07 10:21:46 +02:00
<!-- Webhook Option -->
< div style = "margin-bottom: 1rem; padding: 0.5rem; background: #2a2a2a; border-radius: 4px;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "checkbox" id = "manual-use-webhook" onchange = "toggleWebhookOptions()" style = "margin-right: 0.5rem;" / >
< span > Send as Webhook (allows choosing persona)< / span >
< / label >
< div id = "webhook-persona-options" style = "display: none; margin-top: 0.5rem; padding-left: 1.5rem;" >
< label style = "display: block; margin-bottom: 0.3rem;" >
< input type = "radio" name = "webhook-persona" value = "miku" checked style = "margin-right: 0.5rem;" / >
Hatsune Miku 💙 (with mood emoji)
< / label >
< label style = "display: block;" >
< input type = "radio" name = "webhook-persona" value = "evil" style = "margin-right: 0.5rem;" / >
Evil Miku 😈 (with mood emoji)
< / label >
< p style = "font-size: 0.8rem; color: #888; margin: 0.3rem 0 0 0;" >
Note: Webhooks only work in channels, not DMs. Profile picture and mood emoji will be used.
< / p >
< / div >
< / div >
2025-12-07 17:15:09 +02:00
<!-- Target Selection -->
< div style = "margin-bottom: 1rem;" >
< label for = "manual-target-type" > Target Type:< / label >
< select id = "manual-target-type" onchange = "toggleManualMessageTarget()" >
< option value = "channel" > Channel< / option >
< option value = "dm" > Direct Message< / option >
< / select >
< / div >
< div >
< label for = "manualMessage" > Message:< / label >
< textarea id = "manualMessage" placeholder = "Type the message exactly as Miku should say it..." rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< div style = "margin-top: 0.5rem;" >
< label for = "manualAttachment" > Attach Files (optional):< / label >
< input type = "file" id = "manualAttachment" multiple / >
< / div >
<!-- Channel ID Input -->
< div id = "manual-channel-section" style = "margin-top: 0.5rem;" >
< label for = "manualChannelId" > Channel ID:< / label >
< input type = "text" id = "manualChannelId" placeholder = "Enter channel ID..." style = "width: 100%;" / >
< / div >
<!-- User ID Input -->
< div id = "manual-dm-section" style = "margin-top: 0.5rem; display: none;" >
< label for = "manualUserId" > User ID:< / label >
< input type = "text" id = "manualUserId" placeholder = "Enter user ID for DM..." style = "width: 100%;" / >
< / div >
2025-12-14 16:41:02 +02:00
<!-- Reply Configuration -->
< div style = "margin-top: 0.5rem;" >
< label for = "manualReplyMessageId" > Reply to Message ID (optional):< / label >
< input type = "text" id = "manualReplyMessageId" placeholder = "Enter message ID to reply to..." style = "width: 100%;" / >
< / div >
< div style = "margin-top: 0.5rem;" >
< label style = "margin-right: 1rem;" > Mention user in reply:< / label >
< label >
< input type = "radio" name = "manualReplyMention" value = "true" checked / >
Yes (ping user)
< / label >
< label style = "margin-left: 1rem;" >
< input type = "radio" name = "manualReplyMention" value = "false" / >
No (silent reply)
< / label >
< / div >
2025-12-07 17:15:09 +02:00
< button onclick = "sendManualMessage()" style = "margin-top: 0.5rem;" > Send as Miku< / button >
< p id = "manualStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< div class = "section" id = "message-reaction-section" >
< h3 > 😊 Add Reaction to Message< / h3 >
< p style = "color: #ccc; margin-bottom: 1rem;" >
Make Miku react to a specific message with an emoji of your choice.
< / p >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionMessageId" > Message ID:< / label >
< input type = "text" id = "reactionMessageId" placeholder = "Enter message ID (right-click message > Copy ID)" style = "width: 100%; margin-top: 0.5rem;" / >
< / div >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionChannelId" > Channel ID:< / label >
< input type = "text" id = "reactionChannelId" placeholder = "Enter channel ID (right-click channel > Copy ID)" style = "width: 100%; margin-top: 0.5rem;" / >
< / div >
< div style = "margin-bottom: 1rem;" >
< label for = "reactionEmoji" > Emoji:< / label >
< input type = "text" id = "reactionEmoji" placeholder = "Enter emoji (e.g., 💙, 👍, 🎉)" style = "width: 100%; margin-top: 0.5rem;" / >
< p style = "font-size: 0.85rem; color: #aaa; margin-top: 0.25rem;" >
You can use standard emoji or custom server emoji format (:emoji_name: for custom ones)
< / p >
< / div >
< button onclick = "addReactionToMessage()" style = "margin-top: 0.5rem;" > Add Reaction< / button >
< p id = "reactionStatus" style = "color: green; margin-top: 0.5rem;" > < / p >
< / div >
< / div >
<!-- Status Tab Content -->
< div id = "tab3" class = "tab-content" >
2026-03-01 00:26:22 +02:00
< div class = "section" >
< h3 > Status< / h3 >
< div id = "status" > < / div >
< / div >
2026-04-24 13:46:04 +03:00
<!-- Mood Activities Section (collapsible) -->
< div class = "section" >
< div style = "cursor: pointer; user-select: none;" onclick = "activitiesToggle()" >
< h3 style = "display: inline;" > < span id = "activities-toggle-icon" > ▶< / span > 🎵 Mood Activities< / h3 >
< span id = "activities-summary" style = "color: #888; font-size: 0.85rem; margin-left: 0.5rem;" > < / span >
< / div >
< div id = "activities-body" style = "display: none; margin-top: 1rem;" >
< div style = "margin-bottom: 1rem; display: flex; gap: 0.5rem; align-items: center;" >
< button onclick = "activitiesLoad()" style = "background: #4a7bc9;" > 🔄 Reload from Disk< / button >
< span id = "activities-status" style = "font-size: 0.85rem; color: #61dafb;" > < / span >
< / div >
Refactor activity system: energy-based probability, manual override, all 5 activity types
- Rewrite utils/activities.py with mood energy-driven activity probability
(high-energy moods like excited/bubbly show activity ~80-85% of the time,
low-energy moods like sleepy/melancholy only ~15-25%)
- Add manual override system with 30-min auto-expiry for Web UI control
- Support all 5 Discord activity types: listening, playing, watching,
competing, streaming (with purple LIVE badge via discord.Streaming)
- Add current activity tracking (get_current_activity)
- Add force=True param to update_bot_presence for on_ready (bot.py)
- Add 4 new API routes for manual override:
GET/POST/DELETE /activities/current, POST /activities/current/auto
- Expand activities.yaml from 139 to 157 entries, adding watching,
competing, and streaming entries across 11 moods
- Update Web UI: activity type dropdown with all 5 types, conditional
URL field for streaming, 'Current Activity' override panel with
set/clear/auto controls, type-aware icons and labels
2026-04-27 23:39:18 +03:00
<!-- Current Activity Override Panel -->
< div style = "margin-bottom: 1rem; padding: 0.75rem; background: #252535; border-radius: 6px; border: 1px solid #3a3a5a;" >
< h4 style = "margin: 0 0 0.5rem 0; color: #61dafb; font-size: 0.95rem;" > 🎯 Current Activity< / h4 >
< div id = "activity-override-status" style = "margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa;" >
Loading...
< / div >
< div style = "display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem;" >
< button onclick = "activityRefreshCurrent()" style = "background: #555; font-size: 0.8rem; padding: 0.3rem 0.6rem;" > 🔄 Refresh< / button >
< button onclick = "activityReleaseAuto()" style = "background: #27ae60; font-size: 0.8rem; padding: 0.3rem 0.6rem;" > 🌀 Return to Auto< / button >
< / div >
< details style = "margin-top: 0.5rem;" >
< summary style = "cursor: pointer; font-size: 0.85rem; color: #61dafb;" > ✏️ Manual Override< / summary >
< div style = "margin-top: 0.5rem; display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center;" >
< select id = "act-manual-type" style = "width: 120px; padding: 0.3rem;" >
< option value = "listening" > 🎵 Listening< / option >
< option value = "playing" > 🎮 Playing< / option >
< option value = "watching" > 📺 Watching< / option >
< option value = "competing" > 🏆 Competing< / option >
< option value = "streaming" > 🔴 Streaming< / option >
< / select >
< input type = "text" id = "act-manual-name" placeholder = "Activity name" style = "flex: 2; min-width: 120px; padding: 0.3rem;" >
< input type = "text" id = "act-manual-state" placeholder = "Detail (optional)" style = "flex: 1; min-width: 80px; padding: 0.3rem;" >
< input type = "text" id = "act-manual-url" placeholder = "URL (streaming)" style = "flex: 1; min-width: 80px; padding: 0.3rem; display: none;" >
< button onclick = "activitySetManual()" style = "background: #e67e22; font-size: 0.8rem; padding: 0.3rem 0.8rem;" > Set< / button >
< button onclick = "activityClearManual()" style = "background: #c0392b; font-size: 0.8rem; padding: 0.3rem 0.8rem;" > Clear< / button >
< / div >
< / details >
< / div >
2026-04-24 13:46:04 +03:00
<!-- Normal Moods subsection -->
< div style = "margin-bottom: 1.5rem;" >
< div style = "cursor: pointer; user-select: none; padding: 0.5rem; background: #2a2a2a; border-radius: 4px; margin-bottom: 0.5rem;" onclick = "activitiesSectionToggle('normal')" >
< strong > < span id = "activities-normal-icon" > ▶< / span > 😇 Normal Moods< / strong >
< / div >
< div id = "activities-normal-body" style = "display: none; padding-left: 0.5rem;" >
< div id = "activities-normal-list" > < / div >
< / div >
< / div >
<!-- Evil Moods subsection -->
< div style = "margin-bottom: 1.5rem;" >
< div style = "cursor: pointer; user-select: none; padding: 0.5rem; background: #2a2a2a; border-radius: 4px; margin-bottom: 0.5rem;" onclick = "activitiesSectionToggle('evil')" >
< strong > < span id = "activities-evil-icon" > ▶< / span > 😈 Evil Moods< / strong >
< / div >
< div id = "activities-evil-body" style = "display: none; padding-left: 0.5rem;" >
< div id = "activities-evil-list" > < / div >
< / div >
< / div >
< / div >
< / div >
2026-04-24 13:59:01 +03:00
2026-05-02 15:19:10 +03:00
< div class = "section" id = "prompt-history-section" >
< div class = "prompt-history-header" style = "display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;" >
< h3 style = "margin: 0; cursor: pointer;" onclick = "togglePromptHistoryCollapse()" id = "prompt-history-toggle" >
▼ Prompt History
< / h3 >
< button onclick = "loadPromptHistory()" title = "Refresh" style = "background: none; border: 1px solid #444; color: #aaa; cursor: pointer; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.85rem;" > 🔄< / button >
< / div >
< div id = "prompt-history-body" >
<!-- Source filter + history selector row -->
< div style = "margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;" >
< label style = "font-size: 0.9rem; color: #aaa;" > Source:< / label >
< div style = "display: inline-flex; border-radius: 6px; overflow: hidden; border: 1px solid #444;" >
< button id = "prompt-src-all" class = "prompt-source-btn active" onclick = "switchPromptSource('all')"
style="padding: 0.4rem 0.8rem; border: none; cursor: pointer; font-size: 0.85rem; transition: all 0.2s;">
All
< / button >
< button id = "prompt-src-cat" class = "prompt-source-btn" onclick = "switchPromptSource('cat')"
style="padding: 0.4rem 0.8rem; border: none; cursor: pointer; font-size: 0.85rem; transition: all 0.2s;">
🐱 Cat
< / button >
< button id = "prompt-src-fallback" class = "prompt-source-btn" onclick = "switchPromptSource('fallback')"
style="padding: 0.4rem 0.8rem; border: none; cursor: pointer; font-size: 0.85rem; transition: all 0.2s;">
🤖 Fallback
< / button >
< / div >
< select id = "prompt-history-select" onchange = "selectPromptEntry(this.value)" style = "background: #2a2a2a; color: #ddd; border: 1px solid #444; padding: 0.35rem 0.5rem; border-radius: 4px; font-size: 0.85rem; min-width: 280px;" >
< option value = "" > -- No prompts yet --< / option >
< / select >
< / div >
<!-- Metadata bar -->
< div id = "prompt-metadata" style = "margin-bottom: 0.5rem; font-size: 0.82rem; color: #888; display: flex; flex-wrap: wrap; gap: 0.3rem 1rem;" > < / div >
<!-- Toolbar: copy + truncate toggle -->
< div style = "margin-bottom: 0.5rem; display: flex; align-items: center; gap: 1rem;" >
< button onclick = "copyPromptToClipboard()" title = "Copy full prompt to clipboard" style = "background: #333; border: 1px solid #555; color: #aaa; cursor: pointer; padding: 0.25rem 0.6rem; border-radius: 4px; font-size: 0.8rem;" > 📋 Copy< / button >
< label style = "font-size: 0.82rem; color: #aaa; cursor: pointer; display: flex; align-items: center; gap: 0.3rem;" >
< input type = "checkbox" id = "prompt-truncate-toggle" onchange = "toggleMiddleTruncation()" >
Truncate from middle
< / label >
< / div >
<!-- Prompt display subsections -->
2026-05-02 15:55:19 +03:00
< div id = "prompt-display" style = "max-height: 60vh; overflow-y: auto; min-height: 3rem;" > < / div >
2026-05-02 15:45:54 +03:00
<!-- Hidden buffer for copy - to - clipboard raw text -->
< pre id = "last-prompt" style = "display: none;" > < / pre >
2026-04-24 13:59:01 +03:00
< / div >
< / div >
2026-03-01 00:26:22 +02:00
< / div >
2025-12-07 17:15:09 +02:00
2026-03-01 00:26:22 +02:00
<!-- DM Management Tab Content -->
< div id = "tab10" class = "tab-content" >
2025-12-07 17:15:09 +02:00
< div class = "section" >
2026-03-01 00:26:22 +02:00
< h3 > 📱 DM Management< / h3 >
2025-12-07 17:15:09 +02:00
< div style = "margin-bottom: 1rem;" >
< button onclick = "loadDMUsers()" > 🔄 Refresh DM Users< / button >
< button onclick = "exportAllDMs()" > 📤 Export All DMs< / button >
< button onclick = "loadBlockedUsers()" style = "background: #ff9800;" > 🚫 View Blocked Users< / button >
< / div >
< div style = "margin-bottom: 1rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;" >
< h4 style = "margin-top: 0;" > 📊 DM Interaction Analysis< / h4 >
< button onclick = "runDailyAnalysis()" style = "background: #9c27b0;" > 🔍 Run Daily Analysis Now< / button >
< button onclick = "viewAnalysisReports()" style = "background: #673ab7;" > 📄 View All Reports< / button >
< p style = "font-size: 0.85rem; margin: 0.5rem 0 0 0; color: #aaa;" >
Analysis runs automatically at 2 AM daily. Reports one user per day.
< / p >
< / div >
< div id = "dm-users-list" > < / div >
< div class = "section" id = "blocked-users-section" style = "display: none; margin-top: 2rem;" >
< h4 > 🚫 Blocked Users< / h4 >
< div style = "margin-bottom: 1rem;" >
< button onclick = "hideBlockedUsers()" > ← Back to DM Users< / button >
< / div >
< div id = "blocked-users-list" > < / div >
< / div >
< / div >
< / div >
2026-01-23 15:02:36 +02:00
<!-- LLM Settings Tab Content -->
2025-12-07 17:15:09 +02:00
< div id = "tab4" class = "tab-content" >
2026-01-23 15:02:36 +02:00
< div class = "section" >
< h3 > ⚙️ Language Model Settings< / h3 >
< p > Configure language model behavior and language mode.< / p >
<!-- Language Mode Section -->
< div style = "margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px; border: 2px solid #4a7bc9;" >
< h4 style = "margin-top: 0; color: #61dafb;" > 🌐 Language Mode< / h4 >
< p style = "margin: 0.5rem 0; color: #aaa;" > Switch Miku between English and Japanese responses.< / p >
< div style = "margin: 1rem 0;" >
< div style = "margin-bottom: 1rem;" >
< strong > Current Language:< / strong > < span id = "current-language-display" style = "color: #61dafb; font-weight: bold;" > English< / span >
< / div >
< button onclick = "toggleLanguageMode()" style = "background: #4a7bc9; color: #fff; padding: 0.6rem 1.2rem; border: 2px solid #61dafb; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem;" >
🔄 Toggle Language (English ↔ Japanese)
< / button >
< / div >
< div style = "margin-top: 1rem; padding: 1rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #4a7bc9;" >
< div style = "font-size: 0.9rem;" >
< div style = "margin-bottom: 0.5rem;" > < strong > English Mode:< / strong > < / div >
< ul style = "margin: 0 0 0.5rem 0; padding-left: 1.5rem; color: #aaa;" >
< li > Uses standard Llama 3.1 model< / li >
< li > Responds in English only< / li >
< / ul >
< div style = "margin-bottom: 0.5rem;" > < strong > Japanese Mode (日本語):< / strong > < / div >
< ul style = "margin: 0 0 0; padding-left: 1.5rem; color: #aaa;" >
< li > Uses Llama 3.1 Swallow model (trained for Japanese)< / li >
< li > Responds entirely in Japanese< / li >
< / ul >
< / div >
< / div >
< / div >
<!-- Language Mode Status Section -->
< div style = "margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;" >
< h4 style = "margin-top: 0;" > 📊 Current Status< / h4 >
< div id = "language-status-display" style = "background: #1a1a1a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;" >
< p style = "margin: 0.5rem 0;" > < strong > Language Mode:< / strong > < span id = "status-language" > English< / span > < / p >
< p style = "margin: 0.5rem 0;" > < strong > Active Model:< / strong > < span id = "status-model" > llama3.1< / span > < / p >
< p style = "margin: 0.5rem 0;" > < strong > Available Languages:< / strong > English, 日本語 (Japanese)< / p >
< / div >
< button onclick = "refreshLanguageStatus()" style = "margin-top: 1rem;" > 🔄 Refresh Status< / button >
< / div >
<!-- Information Section -->
< div style = "padding: 1rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #ff9800;" >
< h4 style = "margin-top: 0; color: #ff9800;" > ℹ ️ How Language Mode Works< / h4 >
< ul style = "margin: 0.5rem 0; padding-left: 1.5rem; font-size: 0.9rem; color: #aaa;" >
< li > English mode uses your default text model for English responses< / li >
< li > Japanese mode switches to Swallow and responds only in 日本語< / li >
< li > All personality traits, mood system, and features work in both modes< / li >
< li > Language mode is global - affects all servers and DMs< / li >
< li > Conversation history is preserved across language switches< / li >
< / ul >
< / div >
< / div >
< / div >
<!-- Image Generation Tab Content -->
< div id = "tab5" class = "tab-content" >
2025-12-07 17:15:09 +02:00
< div class = "section" >
< h3 > 🎨 Image Generation System< / h3 >
< p > Natural language image generation powered by ComfyUI. Users can ask Miku to create images naturally without commands!< / p >
<!-- Status Section -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > System Status< / h4 >
< div id = "image-system-status" style = "margin-bottom: 1rem;" >
< button onclick = "checkImageSystemStatus()" > 🔄 Check Status< / button >
< / div >
< div id = "image-status-display" style = "background: #2a2a2a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;" > < / div >
< / div >
<!-- Detection Testing -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Test Natural Language Detection< / h4 >
< div style = "margin-bottom: 1rem;" >
< label for = "detection-test-message" > Test Message:< / label >
< textarea id = "detection-test-message" placeholder = "e.g. Hey Miku, I'd like to see you swimming in a pool" rows = "2" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
< button onclick = "testImageDetection()" style = "margin-right: 0.5rem;" > 🔍 Test Detection< / button >
< div id = "detection-test-results" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< / div >
<!-- Manual Image Generation -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Manual Image Generation< / h4 >
< div style = "margin-bottom: 1rem;" >
< label for = "manual-image-prompt" > Image Prompt:< / label >
< textarea id = "manual-image-prompt" placeholder = "Describe the image you want to generate..." rows = "3" style = "width: 100%; margin-top: 0.5rem;" > < / textarea >
< / div >
2025-12-13 00:36:35 +02:00
< button onclick = "generateManualImage()" style = "margin-right: 0.5rem;" > 🎨 Generate Image< / button >
< div id = "manual-image-status" style = "margin-top: 0.5rem; font-size: 0.9rem;" > < / div >
< div id = "manual-image-preview" style = "margin-top: 1rem; text-align: center;" > < / div >
2025-12-07 17:15:09 +02:00
< / div >
<!-- System Information -->
< div style = "margin-bottom: 1.5rem;" >
< h4 > Image Generation Settings< / h4 >
< div style = "background: #2a2a2a; padding: 1rem; border-radius: 4px;" >
< div style = "margin-bottom: 0.5rem;" > < strong > ComfyUI Configuration:< / strong > < / div >
< ul style = "margin: 0; padding-left: 1.5rem;" >
< li > URL: Auto-detected (tries multiple Docker networking options)< / li >
< li > Workflow Template: < code > Miku_BasicWorkflow.json< / code > < / li >
< li > Host Output Directory: < code > /home/koko210Serve/ComfyUI/output/< / code > < / li >
< li > Container Mount Point: < code > /app/ComfyUI/output/< / code > < / li >
< li > Generation Timeout: 300 seconds< / li >
< / ul >
< div style = "margin-top: 1rem; font-size: 0.9rem; color: #aaa;" >
< strong > Note:< / strong > Make sure ComfyUI is running and the workflow template exists in the bot directory.
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Autonomous Stats Tab Content -->
2026-01-23 15:02:36 +02:00
< div id = "tab6" class = "tab-content" >
2025-12-07 17:15:09 +02:00
< div class = "section" >
< h3 > 📊 Autonomous V2 Decision Engine Stats< / h3 >
< p > Real-time monitoring of Miku's autonomous decision-making context and mood-based personality stats.< / p >
< div style = "margin-bottom: 1.5rem;" >
< label for = "autonomous-server-select" > Select Server:< / label >
< select id = "autonomous-server-select" onchange = "loadAutonomousStats()" >
< option value = "" > -- Select a server --< / option >
< / select >
< button onclick = "loadAutonomousStats()" style = "margin-left: 0.5rem;" > 🔄 Refresh< / button >
< / div >
< div id = "autonomous-stats-display" > < / div >
< / div >
< / div >
2025-12-13 00:23:03 +02:00
<!-- Chat with LLM Tab Content -->
2026-01-23 15:02:36 +02:00
< div id = "tab7" class = "tab-content" >
2025-12-13 00:23:03 +02:00
< div class = "section" >
< h3 > 💬 Chat with LLM< / h3 >
< p > Direct chat interface with the language models. Test responses, experiment with prompts, or just chat with Miku!< / p >
<!-- Configuration Options -->
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem;" >
< h4 style = "margin-top: 0; color: #61dafb;" > ⚙️ Chat Configuration< / h4 >
< div style = "display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1rem;" >
<!-- Model Selection -->
< div >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 🤖 Model Type:< / label >
< div style = "display: flex; gap: 1rem;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "radio" name = "chat-model-type" value = "text" checked onchange = "toggleChatImageUpload()" >
< span style = "margin-left: 0.5rem;" > 💬 Text Model (Fast)< / span >
< / label >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "radio" name = "chat-model-type" value = "vision" onchange = "toggleChatImageUpload()" >
< span style = "margin-left: 0.5rem;" > 👁️ Vision Model (Images)< / span >
< / label >
< / div >
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;" >
Text model for conversations, Vision model for image analysis
< / div >
< / div >
<!-- System Prompt Toggle -->
< div >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 🎭 System Prompt:< / label >
< div style = "display: flex; gap: 1rem;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "radio" name = "chat-system-prompt" value = "true" checked >
< span style = "margin-left: 0.5rem;" > ✅ Use Miku Personality< / span >
< / label >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "radio" name = "chat-system-prompt" value = "false" >
< span style = "margin-left: 0.5rem;" > ❌ Raw LLM (No Prompt)< / span >
< / label >
< / div >
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;" >
With prompt: Chat as Miku. Without: Direct LLM responses
< / div >
< / div >
<!-- Mood Selection -->
< div >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 😊 Miku's Mood:< / label >
< select id = "chat-mood-select" style = "width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;" >
< option value = "neutral" selected > neutral< / option >
< / select >
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;" >
Choose Miku's emotional state for this conversation
< / div >
< / div >
< / div >
<!-- Image Upload for Vision Model -->
< div id = "chat-image-upload-section" style = "display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;" >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 🖼️ Upload Image:< / label >
< input type = "file" id = "chat-image-file" accept = "image/*" style = "margin-bottom: 0.5rem;" >
< div style = "font-size: 0.85rem; color: #aaa;" >
Upload an image for the vision model to analyze
< / div >
< div id = "chat-image-preview" style = "margin-top: 0.5rem; display: none;" >
< img id = "chat-image-preview-img" style = "max-width: 200px; max-height: 200px; border: 1px solid #555; border-radius: 4px;" >
< / div >
< / div >
<!-- Clear Chat Button -->
< div style = "margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;" >
< button onclick = "clearChatHistory()" style = "background: #ff9800;" > 🗑️ Clear Chat History< / button >
< span style = "margin-left: 1rem; font-size: 0.85rem; color: #aaa;" > Remove all messages from this session< / span >
< / div >
< / div >
<!-- Chat Messages Container -->
< div id = "chat-messages" style = "background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 1rem; min-height: 400px; max-height: 500px; overflow-y: auto; margin-bottom: 1rem;" >
< div style = "text-align: center; color: #888; padding: 2rem;" >
💬 Start chatting with the LLM! Your conversation will appear here.
< / div >
< / div >
<!-- Chat Input Area -->
< div style = "display: flex; gap: 0.5rem; align-items: flex-end;" >
< div style = "flex: 1;" >
< textarea
id="chat-input"
placeholder="Type your message here..."
rows="3"
style="width: 100%; padding: 0.75rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: inherit; resize: vertical;"
onkeydown="handleChatKeyPress(event)"
>< / textarea >
< / div >
< div >
< button
id="chat-send-btn"
onclick="sendChatMessage()"
style="padding: 1rem 1.5rem; height: 100%; background: #4CAF50; font-size: 1rem; font-weight: bold;"
>
📤 Send
< / button >
< / div >
< / div >
< div style = "margin-top: 0.5rem; font-size: 0.85rem; color: #aaa;" >
💡 Tip: Press Ctrl+Enter to send your message quickly
< / div >
< / div >
< / div >
2026-01-23 15:02:36 +02:00
<!-- Tab 8: Voice Call Management -->
< div id = "tab8" class = "tab-content" >
2026-01-20 23:06:17 +02:00
< div class = "section" >
< h3 > 📞 Initiate Voice Call< / h3 >
< p > Start an automated voice chat session with a user. Miku will automatically manage containers, join voice chat, and send an invitation DM.< / p >
< div style = "background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem;" >
< h4 style = "margin-top: 0; color: #61dafb;" > ⚙️ Voice Call Configuration< / h4 >
< div style = "display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;" >
<!-- User ID Input -->
< div >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 👤 Target User ID:< / label >
< input
type="text"
id="voice-user-id"
placeholder="Discord user ID (e.g., 123456789)"
style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"
>
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;" >
Discord ID of the user to call
< / div >
< / div >
<!-- Voice Channel ID Input -->
< div >
< label style = "display: block; margin-bottom: 0.5rem; font-weight: bold;" > 🎤 Voice Channel ID:< / label >
< input
type="text"
id="voice-channel-id"
placeholder="Discord channel ID (e.g., 987654321)"
style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"
>
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;" >
Discord ID of the voice channel to join
< / div >
< / div >
< / div >
<!-- Debug Mode Toggle -->
< div style = "margin-bottom: 1.5rem; padding: 1rem; background: #1e1e1e; border-radius: 4px;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input
type="checkbox"
id="voice-debug-mode"
style="margin-right: 0.7rem; width: 18px; height: 18px; cursor: pointer;"
>
< span style = "font-weight: bold;" > 🐛 Debug Mode< / span >
< / label >
< div style = "font-size: 0.85rem; color: #aaa; margin-top: 0.5rem; margin-left: 1.7rem;" >
When enabled, shows voice transcriptions and responses in text channel. When disabled, voice chat is private.
< / div >
< / div >
<!-- Call Status Display -->
< div id = "voice-call-status" style = "background: #1e1e1e; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; display: none;" >
< div style = "color: #61dafb; font-weight: bold; margin-bottom: 0.5rem;" > 📊 Call Status:< / div >
< div id = "voice-call-status-text" style = "color: #aaa; font-size: 0.9rem;" > < / div >
< div id = "voice-call-invite-link" style = "margin-top: 0.5rem; display: none;" >
< strong > Invite Link:< / strong > < a id = "voice-call-invite-url" href = "" target = "_blank" style = "color: #61dafb;" > View Invite< / a >
< / div >
< / div >
<!-- Call Buttons -->
< div style = "display: flex; gap: 1rem;" >
< button
id="voice-call-btn"
onclick="initiateVoiceCall()"
style="background: #2ecc71; color: #000; padding: 0.7rem 1.5rem; border: 1px solid #27ae60; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem;"
>
📞 Initiate Call
< / button >
< button
id="voice-call-cancel-btn"
onclick="cancelVoiceCall()"
style="background: #e74c3c; color: #fff; padding: 0.7rem 1.5rem; border: 1px solid #c0392b; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem; display: none;"
>
🛑 Cancel Call
< / button >
< / div >
< / div >
<!-- Call Information -->
< div style = "background: #1a1a2e; padding: 1.5rem; border-radius: 8px; border-left: 3px solid #61dafb;" >
< h4 style = "margin-top: 0; color: #61dafb;" > ℹ ️ How Voice Calls Work< / h4 >
< ul style = "color: #ddd; line-height: 1.8;" >
< li > < strong > Automatic Setup:< / strong > STT and TTS containers start automatically< / li >
< li > < strong > Warmup Wait:< / strong > System waits for both containers to be ready (~30-75 seconds)< / li >
< li > < strong > VC Join:< / strong > Miku joins the specified voice channel< / li >
< li > < strong > DM Invitation:< / strong > User receives a personalized invite DM with a voice channel link< / li >
< li > < strong > Auto-Listen:< / strong > STT automatically starts when user joins< / li >
< li > < strong > Auto-Leave:< / strong > Miku leaves 45 seconds after user disconnects< / li >
< li > < strong > Timeout:< / strong > If user doesn't join within 30 minutes, call is cancelled< / li >
< / ul >
< / div >
<!-- Call History -->
< div style = "margin-top: 2rem;" >
< h4 style = "color: #61dafb; margin-bottom: 1rem;" > 📋 Recent Calls< / h4 >
< div id = "voice-call-history" style = "background: #1e1e1e; border: 1px solid #444; border-radius: 4px; padding: 1rem;" >
< div style = "text-align: center; color: #888;" > No calls yet. Start one above!< / div >
< / div >
< / div >
< / div >
< / div >
Phase 3: Unified Cheshire Cat integration with WebSocket-based per-user isolation
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
2026-02-07 20:22:03 +02:00
<!-- Tab 9: Memory Management -->
< div id = "tab9" class = "tab-content" >
< div class = "section" >
< h3 > 🧠 Cheshire Cat Memory Management< / h3 >
< p style = "color: #aaa; margin-bottom: 1rem;" >
Manage Miku's long-term memories powered by the Cheshire Cat AI pipeline.
Memories are stored in Qdrant vector database and used to give Miku persistent knowledge about users.
< / p >
<!-- Cat Integration Status -->
< div id = "cat-status-section" style = "background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;" >
< div style = "display: flex; justify-content: space-between; align-items: center;" >
< div >
< h4 style = "margin: 0 0 0.3rem 0;" > 🐱 Cheshire Cat Status< / h4 >
< span id = "cat-status-indicator" style = "color: #888;" > Checking...< / span >
< / div >
< div style = "display: flex; gap: 0.5rem; align-items: center;" >
< button id = "cat-toggle-btn" onclick = "toggleCatIntegration()" style = "background: #333; color: #fff; padding: 0.4rem 0.8rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.85rem;" >
Loading...
< / button >
< button onclick = "refreshMemoryStats()" style = "background: #2a5599; color: #fff; padding: 0.4rem 0.8rem; border: none; border-radius: 4px; cursor: pointer;" >
🔄 Refresh
< / button >
< / div >
< / div >
< / div >
<!-- Memory Statistics -->
< div style = "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.5rem;" >
< div id = "stat-episodic" style = "background: #1a2332; border: 1px solid #2a5599; border-radius: 8px; padding: 1rem; text-align: center;" >
< div style = "font-size: 2rem; font-weight: bold; color: #61dafb;" id = "stat-episodic-count" > —< / div >
< div style = "color: #aaa; font-size: 0.85rem;" > 📝 Episodic Memories< / div >
< div style = "color: #666; font-size: 0.75rem; margin-top: 0.3rem;" > Conversation snippets< / div >
< / div >
< div id = "stat-declarative" style = "background: #1a3322; border: 1px solid #2a9955; border-radius: 8px; padding: 1rem; text-align: center;" >
< div style = "font-size: 2rem; font-weight: bold; color: #6fdc6f;" id = "stat-declarative-count" > —< / div >
< div style = "color: #aaa; font-size: 0.85rem;" > 📚 Declarative Facts< / div >
< div style = "color: #666; font-size: 0.75rem; margin-top: 0.3rem;" > Learned knowledge< / div >
< / div >
< div id = "stat-procedural" style = "background: #332a1a; border: 1px solid #995e2a; border-radius: 8px; padding: 1rem; text-align: center;" >
< div style = "font-size: 2rem; font-weight: bold; color: #dcb06f;" id = "stat-procedural-count" > —< / div >
< div style = "color: #aaa; font-size: 0.85rem;" > ⚙️ Procedural< / div >
< div style = "color: #666; font-size: 0.75rem; margin-top: 0.3rem;" > Tools & procedures< / div >
< / div >
< / div >
<!-- Consolidation -->
< div style = "background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;" >
< h4 style = "margin: 0 0 0.5rem 0;" > 🌙 Memory Consolidation< / h4 >
< p style = "color: #aaa; font-size: 0.85rem; margin-bottom: 0.75rem;" >
Trigger the sleep consolidation process: analyzes episodic memories, extracts important facts, and removes trivial entries.
< / p >
< div style = "display: flex; gap: 0.5rem; align-items: center;" >
< button id = "consolidate-btn" onclick = "triggerConsolidation()" style = "background: #5b3a8c; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;" >
🌙 Run Consolidation
< / button >
< span id = "consolidation-status" style = "color: #888; font-size: 0.85rem;" > < / span >
< / div >
< div id = "consolidation-result" style = "display: none; margin-top: 0.75rem; background: #111; border: 1px solid #333; border-radius: 4px; padding: 0.75rem; font-size: 0.85rem; color: #ccc; white-space: pre-wrap; max-height: 200px; overflow-y: auto;" > < / div >
< / div >
<!-- Declarative Facts Browser -->
< div style = "background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;" >
< div style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;" >
< h4 style = "margin: 0;" > 📚 Declarative Facts< / h4 >
2026-02-10 21:41:28 +02:00
< div style = "display: flex; gap: 0.5rem;" >
< button onclick = "showCreateMemoryModal('declarative')" style = "background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;" >
➕ Add Fact
< / button >
< button onclick = "loadFacts()" style = "background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;" >
🔄 Load Facts
< / button >
< / div >
< / div >
< div style = "margin-bottom: 0.5rem;" >
< input type = "text" id = "facts-search" placeholder = "🔍 Search facts..."
oninput="filterMemories('facts-list', this.value)"
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
Phase 3: Unified Cheshire Cat integration with WebSocket-based per-user isolation
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
2026-02-07 20:22:03 +02:00
< / div >
< div id = "facts-list" style = "max-height: 400px; overflow-y: auto;" >
< div style = "text-align: center; color: #666; padding: 2rem;" > Click "Load Facts" to view stored knowledge< / div >
< / div >
< / div >
<!-- Episodic Memories Browser -->
< div style = "background: #1a1a2e; border: 1px solid #444; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;" >
< div style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;" >
< h4 style = "margin: 0;" > 📝 Episodic Memories< / h4 >
2026-02-10 21:41:28 +02:00
< div style = "display: flex; gap: 0.5rem;" >
< button onclick = "showCreateMemoryModal('episodic')" style = "background: #2a9955; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;" >
➕ Add Memory
< / button >
< button onclick = "loadEpisodicMemories()" style = "background: #2a5599; color: #fff; padding: 0.3rem 0.7rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem;" >
🔄 Load Memories
< / button >
< / div >
< / div >
< div style = "margin-bottom: 0.5rem;" >
< input type = "text" id = "episodic-search" placeholder = "🔍 Search memories..."
oninput="filterMemories('episodic-list', this.value)"
style="width: 100%; padding: 0.4rem; background: #242424; color: #fff; border: 1px solid #444; border-radius: 4px; box-sizing: border-box;">
Phase 3: Unified Cheshire Cat integration with WebSocket-based per-user isolation
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
2026-02-07 20:22:03 +02:00
< / div >
< div id = "episodic-list" style = "max-height: 400px; overflow-y: auto;" >
< div style = "text-align: center; color: #666; padding: 2rem;" > Click "Load Memories" to view conversation snippets< / div >
< / div >
< / div >
<!-- DANGER ZONE: Delete All Memories -->
< div style = "background: #2e1a1a; border: 2px solid #993333; border-radius: 8px; padding: 1rem;" >
< h4 style = "margin: 0 0 0.5rem 0; color: #ff6b6b;" > ⚠️ Danger Zone — Delete All Memories< / h4 >
< p style = "color: #cc9999; font-size: 0.85rem; margin-bottom: 1rem;" >
This will permanently erase ALL of Miku's memories — episodic conversations, learned facts, everything.
This action is < strong > irreversible< / strong > . Miku will forget everything she has ever learned.
< / p >
<!-- Step 1: Initial checkbox -->
< div id = "delete-step-1" style = "margin-bottom: 0.75rem;" >
< label style = "cursor: pointer; color: #ff9999;" >
< input type = "checkbox" id = "delete-checkbox-1" onchange = "onDeleteStep1Change()" >
I understand this will permanently delete all of Miku's memories
< / label >
< / div >
<!-- Step 2: Second confirmation (hidden initially) -->
< div id = "delete-step-2" style = "display: none; margin-bottom: 0.75rem;" >
< label style = "cursor: pointer; color: #ff9999;" >
< input type = "checkbox" id = "delete-checkbox-2" onchange = "onDeleteStep2Change()" >
I confirm this is irreversible and I want to proceed
< / label >
< / div >
<!-- Step 3: Type confirmation string (hidden initially) -->
< div id = "delete-step-3" style = "display: none; margin-bottom: 0.75rem;" >
< p style = "color: #ff6b6b; font-size: 0.85rem; margin-bottom: 0.5rem;" >
Type exactly: < code style = "background: #333; padding: 0.2rem 0.4rem; border-radius: 3px; color: #ff9999;" > Yes, I am deleting Miku's memories fully.< / code >
< / p >
< input type = "text" id = "delete-confirmation-input" placeholder = "Type the confirmation string..."
style="width: 100%; padding: 0.5rem; background: #1a1a1a; color: #ff9999; border: 1px solid #993333; border-radius: 4px; font-family: monospace; box-sizing: border-box;"
oninput="onDeleteInputChange()">
< / div >
<!-- Final delete button (hidden initially) -->
< div id = "delete-step-final" style = "display: none;" >
< button id = "delete-all-btn" onclick = "executeDeleteAllMemories()" disabled
style="background: #cc3333; color: #fff; padding: 0.5rem 1.5rem; border: none; border-radius: 4px; cursor: not-allowed; font-weight: bold; opacity: 0.5;">
🗑️ Permanently Delete All Memories
< / button >
< button onclick = "resetDeleteFlow()" style = "background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;" >
Cancel
< / button >
< / div >
< / div >
< / div >
< / div >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
<!-- Tab 11: Profile Picture Management -->
< div id = "tab11" class = "tab-content" >
< div class = "section" >
< h3 > 🖼️ Profile Picture Management< / h3 >
< p style = "font-size: 0.9rem; color: #aaa;" > Change, crop, and manage Miku's profile picture. Edit descriptions and role colors.< / p >
<!-- Action Buttons -->
< div style = "margin-bottom: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;" >
< button onclick = "pfpChangeDanbooru()" > 🎨 Change (Danbooru)< / button >
< button onclick = "pfpRestoreFallback()" > 🔄 Restore Original Avatar< / button >
< / div >
<!-- Upload Section -->
< div style = "margin-bottom: 1rem;" >
< label for = "pfp-tab-upload" > Upload Custom Image:< / label >
< input type = "file" id = "pfp-tab-upload" accept = "image/*" style = "margin-left: 0.5rem;" >
< button onclick = "pfpUploadCustom()" > 📤 Upload & Apply< / button >
< div style = "font-size: 0.8rem; color: #888; margin-top: 0.3rem; margin-left: 0.5rem;" >
💡 Supports static images (PNG, JPG) and animated GIFs |
⚠️ Animated GIFs require Discord Nitro on the bot account
< / div >
< / div >
2026-03-31 15:09:57 +03:00
<!-- Album / Gallery -->
< div class = "album-section" >
< div class = "album-header" onclick = "albumToggle()" >
< h4 > < span id = "album-toggle-icon" > ▶< / span > 📚 Profile Picture Album < span id = "album-count" style = "color: #888; font-weight: normal;" > < / span > < / h4 >
< span class = "album-disk-usage" id = "album-disk-usage" > < / span >
< / div >
< div id = "album-body" style = "display: none;" >
<!-- Toolbar -->
< div class = "album-toolbar" >
< input type = "file" id = "album-upload" accept = "image/*" multiple style = "max-width: 220px;" >
< button onclick = "albumUpload()" > 📤 Add to Album< / button >
< button onclick = "albumAddCurrent()" > 📌 Archive Current PFP< / button >
< button onclick = "albumBulkDelete()" style = "background: #c0392b;" id = "album-bulk-delete-btn" disabled > 🗑️ Delete Selected (< span id = "album-selected-count" > 0< / span > )< / button >
< div id = "album-status" style = "font-size: 0.85rem; color: #61dafb; margin-left: 0.5rem;" > < / div >
< / div >
<!-- Grid -->
< div class = "album-grid" id = "album-grid" > < / div >
<!-- Detail panel (shown when an entry is clicked) -->
< div class = "album-detail" id = "album-detail" style = "display: none;" >
< div style = "display: flex; justify-content: space-between; align-items: center;" >
< h4 style = "margin: 0;" > Selected Entry< / h4 >
< button onclick = "albumCloseDetail()" style = "background: #555; padding: 0.3rem 0.6rem;" > ✖ Close< / button >
< / div >
< div class = "album-detail-previews" >
< div class = "pfp-preview-box" >
< span class = "label" > 📷 Original< / span >
< img id = "album-detail-original" src = "" alt = "Original" style = "cursor: pointer;" onclick = "albumShowCropInterface()" title = "Click to crop" >
< div id = "album-detail-dims" style = "font-size: 0.8rem; color: #666; margin-top: 0.3rem;" > < / div >
< / div >
< div class = "pfp-preview-box" >
< span class = "label" > 🎯 Cropped< / span >
< img id = "album-detail-cropped" src = "" alt = "Cropped" style = "border-radius: 50%; max-width: 256px; max-height: 256px;" >
< / div >
< / div >
<!-- Album entry crop interface -->
< div id = "album-crop-section" style = "display: none; margin: 1rem 0; padding: 1rem; background: #1a1a2e; border: 1px solid #444; border-radius: 8px;" >
< h4 style = "margin-top: 0;" > ✂️ Crop Album Entry< / h4 >
< div class = "pfp-crop-container" >
< img id = "album-crop-image" src = "" alt = "Crop source" >
< / div >
< div style = "margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap;" >
< button onclick = "albumApplyManualCrop()" style = "background: #4CAF50; color: #fff; font-weight: bold;" > ✂️ Apply Crop< / button >
< button onclick = "albumApplyAutoCrop()" > 🤖 Auto Crop< / button >
< button onclick = "albumHideCropInterface()" style = "background: #666;" > ✖ Cancel< / button >
< / div >
< / div >
<!-- Description -->
< div style = "margin-top: 1rem;" >
< label style = "color: #aaa;" > 📝 Description:< / label >
< textarea id = "album-detail-description" class = "pfp-description-editor" style = "min-height: 80px;" > < / textarea >
< div style = "margin-top: 0.5rem; display: flex; gap: 0.5rem;" >
< button onclick = "albumSaveDescription()" style = "background: #4CAF50; color: #fff;" > 💾 Save< / button >
< / div >
< / div >
<!-- Actions -->
< div style = "margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;" >
< button onclick = "albumSetAsCurrent()" style = "background: #2196F3; color: #fff; font-weight: bold;" > 🖼️ Set as Current PFP< / button >
< button onclick = "albumDeleteSelected()" style = "background: #c0392b; color: #fff;" > 🗑️ Delete Entry< / button >
< / div >
< div id = "album-detail-meta" style = "margin-top: 0.75rem; font-size: 0.8rem; color: #888;" > < / div >
< / div >
< / div >
< / div >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
<!-- Crop Mode Toggle -->
< div class = "crop-mode-toggle" >
< span style = "color: #61dafb; font-weight: bold;" > Crop Mode:< / span >
< label >
< input type = "radio" name = "pfp-crop-mode" value = "auto" checked >
🤖 Auto (face detection)
< / label >
< label >
< input type = "radio" name = "pfp-crop-mode" value = "manual" >
✂️ Manual
< / label >
< / div >
<!-- Status -->
< div id = "pfp-tab-status" style = "margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;" > < / div >
<!-- Manual Crop Interface (hidden by default) -->
< div id = "pfp-crop-section" style = "display: none; margin: 1rem 0; padding: 1rem; background: #1a1a2e; border: 1px solid #444; border-radius: 8px;" >
< h4 style = "margin-top: 0;" > ✂️ Manual Crop< / h4 >
< p style = "font-size: 0.85rem; color: #aaa; margin-bottom: 0.5rem;" >
Drag to select a square crop region. Discord avatars are displayed as circles, so keep the subject centered.
< / p >
< div class = "pfp-crop-container" >
< img id = "pfp-crop-image" src = "" alt = "Crop source" >
< / div >
< div style = "margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap;" >
< button onclick = "pfpApplyManualCrop()" style = "background: #4CAF50; color: #fff; font-weight: bold;" > ✂️ Apply Crop< / button >
< button onclick = "pfpApplyAutoCrop()" > 🤖 Use Auto Crop Instead< / button >
< button onclick = "pfpHideCropInterface()" style = "background: #666;" > ✖ Cancel< / button >
< / div >
< / div >
<!-- Image Previews -->
< div class = "pfp-preview-container" >
< div class = "pfp-preview-box" >
< span class = "label" > 📷 Original (full resolution)< / span >
< img id = "pfp-preview-original" src = "" alt = "Original" style = "cursor: pointer;" onclick = "pfpRecrop()" title = "Click to re-crop" >
< div id = "pfp-original-dims" style = "font-size: 0.8rem; color: #666; margin-top: 0.3rem;" > < / div >
< / div >
< div class = "pfp-preview-box" >
< span class = "label" > 🎯 Current Avatar (cropped)< / span >
< img id = "pfp-preview-current" src = "" alt = "Current avatar" style = "border-radius: 50%; max-width: 256px; max-height: 256px;" >
< div style = "font-size: 0.8rem; color: #666; margin-top: 0.3rem;" > 512× 512 · displayed as circle< / div >
2026-03-31 15:16:40 +03:00
< button onclick = "pfpRecrop()" style = "margin-top: 0.5rem;" > ✂️ Re-crop Current< / button >
feat: add Profile Picture Management tab with manual crop, description editor
- profile_picture_manager.py:
- Add ORIGINAL_PATH constant; save full-res original before every crop
- Add skip_crop param to change_profile_picture() for manual crop workflow
- Add manual_crop(x,y,w,h) method with Discord avatar update + role color sync
- Add auto_crop_only() to re-run face-detection crop on stored original
- Add update_description() with Cheshire Cat declarative memory re-injection
- Add regenerate_description() via vision model
- Skip crop step if image is already at/below 512x512
- api.py:
- GET /profile-picture/image/original — serve full-res original (no-cache)
- GET /profile-picture/image/current — serve current cropped avatar (no-cache)
- POST /profile-picture/change-no-crop — acquire image, skip auto-crop
- POST /profile-picture/manual-crop — apply crop coords {x,y,width,height}
- POST /profile-picture/auto-crop — re-run intelligent crop on original
- POST /profile-picture/description — save freeform description + Cat inject
- POST /profile-picture/regenerate-description — re-generate via vision model
- GET /profile-picture/description — fetch current description text
- index.html:
- Add new tab11 '🖼️ Profile Picture Management'
- Remove PFP + role color sections from Actions tab (tab2)
- Add Cropper.js 1.6.2 via CDN for manual square crop
- Tab layout: action buttons, file upload, auto/manual crop toggle,
Cropper.js interface, side-by-side original/cropped previews,
role color management, freeform description editor, metadata box (bottom)
- Wire switchTab hook for tab11 → loadPfpTab()
- All new JS functions: pfpChangeDanbooru, pfpUploadCustom, pfpRestoreFallback,
pfpShowCropInterface, pfpApplyManualCrop, pfpApplyAutoCrop, pfpSaveDescription,
pfpRegenerateDescription, pfpRefreshPreviews, setCustomRoleColor, resetRoleColor
2026-03-30 15:10:19 +03:00
< / div >
< / div >
<!-- Role Color Management -->
< div style = "margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #333;" >
< h4 > 🎨 Role Color Management< / h4 >
< p style = "font-size: 0.9rem; color: #aaa;" > Manually set Miku's role color or reset to fallback (#86cecb)< / p >
< div style = "margin-bottom: 1rem; display: flex; gap: 10px; align-items: end;" >
< div >
< label for = "pfp-tab-role-color-hex" > Hex Color:< / label >
< input type = "text" id = "pfp-tab-role-color-hex" placeholder = "#86cecb" maxlength = "7" style = "width: 100px; font-family: monospace;" >
< / div >
< button onclick = "setCustomRoleColor()" > 🎨 Apply Color< / button >
< button onclick = "resetRoleColor()" > 🔄 Reset to Fallback< / button >
< / div >
< div id = "pfp-tab-role-color-status" style = "margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;" > < / div >
< / div >
<!-- Description Editor -->
< div style = "margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #333;" >
< h4 > 📝 Profile Picture Description< / h4 >
< p style = "font-size: 0.9rem; color: #aaa;" > Edit the description used for context when users ask about Miku's avatar. Saved to Cheshire Cat memory.< / p >
< textarea id = "pfp-description-editor" class = "pfp-description-editor" placeholder = "Loading description..." > < / textarea >
< div style = "margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap;" >
< button onclick = "pfpSaveDescription()" style = "background: #4CAF50; color: #fff;" > 💾 Save Description< / button >
< button onclick = "pfpRegenerateDescription()" > 🔄 Re-generate (Vision Model)< / button >
< / div >
< div id = "pfp-desc-status" style = "margin-top: 0.5rem; font-size: 0.9rem; color: #61dafb;" > < / div >
< / div >
<!-- Metadata (bottom) -->
< div id = "pfp-tab-metadata" style = "margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #333; background: #1e1e1e; padding: 0.5rem; border: 1px solid #333; display: none;" >
< h4 style = "margin-top: 0;" > Current Profile Picture Info:< / h4 >
< pre id = "pfp-tab-metadata-content" style = "margin: 0;" > < / pre >
< / div >
< / div >
< / div >
fix: Phase 3 bug fixes - memory APIs, username visibility, web UI layout, Docker
**Critical Bug Fixes:**
1. Per-user memory isolation bug
- Changed CatAdapter from HTTP POST to WebSocket /ws/{user_id}
- User_id now comes from URL path parameter (true per-user isolation)
- Verified: Different users can't see each other's memories
2. Memory API 405 errors
- Replaced non-existent Cat endpoint calls with Qdrant direct queries
- get_memory_points(): Now uses POST /collections/{collection}/points/scroll
- delete_memory_point(): Now uses POST /collections/{collection}/points/delete
3. Memory stats showing null counts
- Reimplemented get_memory_stats() to query Qdrant directly
- Now returns accurate counts: episodic: 20, declarative: 6, procedural: 4
4. Miku couldn't see usernames
- Modified discord_bridge before_cat_reads_message hook
- Prepends [Username says:] to every message text
- LLM now knows who is texting: [Alice says:] Hello Miku!
5. Web UI Memory tab layout
- Tab9 was positioned outside .tab-container div (showed to the right)
- Moved tab9 HTML inside container, before closing divs
- Memory tab now displays below tab buttons like other tabs
**Code Changes:**
bot/utils/cat_client.py:
- Line 25: Logger name changed to 'llm' (available component)
- get_memory_stats() (lines 256-285): Query Qdrant directly via HTTP GET
- get_memory_points() (lines 275-310): Use Qdrant POST /points/scroll
- delete_memory_point() (lines 350-370): Use Qdrant POST /points/delete
cat-plugins/discord_bridge/discord_bridge.py:
- Fixed .pop() → .get() (UserMessage is Pydantic BaseModelDict)
- Added before_cat_reads_message logic to prepend [Username says:]
- Message format: [Alice says:] message content
Dockerfile.llamaswap-rocm:
- Lines 37-44: Added conditional check for UI directory
- if [ -d ui ] before npm install && npm run build
- Fixes build failure when llama-swap UI dir doesn't exist
bot/static/index.html:
- Moved tab9 from lines 1554-1688 (outside container)
- To position before container closing divs (now inside)
- Memory tab button at line 673: 🧠 Memories
**Testing & Verification:**
✅ Per-user isolation verified (Docker exec test)
✅ Memory stats showing real counts (curl test)
✅ Memory API working (facts/episodic loading)
✅ Web UI layout fixed (tab displays correctly)
✅ All 5 services running (llama-swap, llama-swap-amd, qdrant, cat, bot)
✅ Username prepending working (message context for LLM)
**Result:** All Phase 3 critical bugs fixed and verified working.
2026-02-07 23:27:15 +02:00
< / div >
< / div >
2026-02-28 23:05:26 +02:00
< div class = "logs" id = "logs-panel" >
2025-12-07 17:15:09 +02:00
< h3 > Logs< / h3 >
2026-02-28 23:05:26 +02:00
< div id = "logs-paused-banner" class = "logs-paused-indicator" onclick = "scrollLogsToBottom()" > ⏸ Auto-scroll paused — click to resume< / div >
2025-12-07 17:15:09 +02:00
< div id = "logs-content" > < / div >
< / div >
< div id = "notification" > < / div >
2026-02-10 21:41:28 +02:00
<!-- Edit Memory Modal -->
< div id = "edit-memory-modal" style = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;" >
< div style = "background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;" >
< h3 style = "margin: 0 0 1rem 0; color: #61dafb;" > ✏️ Edit Memory< / h3 >
< div style = "margin-bottom: 1rem;" >
< label style = "display: block; color: #ccc; margin-bottom: 0.5rem;" > Content:< / label >
< textarea id = "edit-memory-content" rows = "5" style = "width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;" > < / textarea >
< / div >
< div style = "margin-bottom: 1rem;" >
< label style = "display: block; color: #ccc; margin-bottom: 0.5rem;" > Source:< / label >
< input type = "text" id = "edit-memory-source" style = "width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;" >
< / div >
< div style = "display: flex; gap: 0.5rem; justify-content: flex-end;" >
< button onclick = "closeEditMemoryModal()" style = "background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;" >
Cancel
< / button >
< button onclick = "saveMemoryEdit()" style = "background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;" >
💾 Save Changes
< / button >
< / div >
< / div >
< / div >
<!-- Create Memory Modal -->
< div id = "create-memory-modal" style = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: center; justify-content: center;" >
< div style = "background: #1e1e1e; border: 2px solid #555; border-radius: 8px; padding: 2rem; max-width: 600px; width: 90%;" >
2026-02-28 23:13:50 +02:00
< input type = "hidden" id = "create-memory-collection" value = "" >
2026-02-10 21:41:28 +02:00
< h3 style = "margin: 0 0 1rem 0; color: #6fdc6f;" id = "create-modal-title" > ➕ Create New Memory< / h3 >
< div style = "margin-bottom: 1rem;" >
< label style = "display: block; color: #ccc; margin-bottom: 0.5rem;" > Content:< / label >
< textarea id = "create-memory-content" rows = "5" placeholder = "Enter the fact or memory content..." style = "width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: monospace; box-sizing: border-box; resize: vertical;" > < / textarea >
< / div >
< div style = "margin-bottom: 1rem;" >
< label style = "display: block; color: #ccc; margin-bottom: 0.5rem;" > User ID (optional):< / label >
< input type = "text" id = "create-memory-user-id" placeholder = "e.g., discord_123456789" style = "width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;" >
< small style = "color: #888;" > Leave empty for general facts, or specify a Discord user ID for user-specific facts< / small >
< / div >
< div style = "margin-bottom: 1rem;" >
< label style = "display: block; color: #ccc; margin-bottom: 0.5rem;" > Source (optional):< / label >
< input type = "text" id = "create-memory-source" placeholder = "e.g., manual_admin, wiki, etc." style = "width: 100%; padding: 0.5rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;" >
< / div >
< div style = "display: flex; gap: 0.5rem; justify-content: flex-end;" >
< button onclick = "closeCreateMemoryModal()" style = "background: #444; color: #ccc; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;" >
Cancel
< / button >
< button onclick = "saveNewMemory()" style = "background: #2a9955; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;" >
✨ Create Memory
< / button >
< / div >
< / div >
< / div >
2026-05-02 16:08:47 +03:00
< script src = "/static/js/core.js?v=20260502" > < / script >
< script src = "/static/js/servers.js?v=20260502" > < / script >
< script src = "/static/js/modes.js?v=20260502" > < / script >
< script src = "/static/js/actions.js?v=20260502" > < / script >
< script src = "/static/js/image-gen.js?v=20260502" > < / script >
< script src = "/static/js/status.js?v=20260502" > < / script >
< script src = "/static/js/dm.js?v=20260502" > < / script >
< script src = "/static/js/chat.js?v=20260502" > < / script >
< script src = "/static/js/memories.js?v=20260502" > < / script >
< script src = "/static/js/profile.js?v=20260502" > < / script >
2025-12-07 17:15:09 +02:00
< / body >
< / html >