🌐 Add web UI dashboard with bot launcher and real-time management
Implement comprehensive Flask web interface for bot configuration, statistics, and memory management. Includes responsive design, auto-refreshing stats, and clear URL logging for easy access.
This commit is contained in:
parent
5f8c93ff69
commit
052570cefb
29 changed files with 1453 additions and 2 deletions
11
Dockerfile
11
Dockerfile
|
|
@ -9,17 +9,24 @@ COPY src/ ./src
|
||||||
COPY src/settings.yml .
|
COPY src/settings.yml .
|
||||||
COPY src/persona.json .
|
COPY src/persona.json .
|
||||||
COPY .env .
|
COPY .env .
|
||||||
|
COPY bot_launcher.py .
|
||||||
|
COPY requirements-webui.txt .
|
||||||
|
|
||||||
# Install dependencies from requirements
|
# Install dependencies from requirements
|
||||||
RUN pip install --no-cache-dir -r src/requirements.txt
|
RUN pip install --no-cache-dir -r src/requirements.txt && \
|
||||||
|
pip install --no-cache-dir -r requirements-webui.txt
|
||||||
|
|
||||||
# Runtime directory where user-editable files will live
|
# Runtime directory where user-editable files will live
|
||||||
ENV PYTHONPATH=/app/src
|
ENV PYTHONPATH=/app/src
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Expose web UI port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
# On first run, populate /app from the fallback template folder
|
# On first run, populate /app from the fallback template folder
|
||||||
|
# Use bot_launcher.py to start both bot and web UI
|
||||||
CMD ["sh", "-c", "\
|
CMD ["sh", "-c", "\
|
||||||
mkdir -p /app && \
|
mkdir -p /app && \
|
||||||
[ -f /app/settings.yml ] || cp -r /opt/template/* /app && \
|
[ -f /app/settings.yml ] || cp -r /opt/template/* /app && \
|
||||||
cd /app && \
|
cd /app && \
|
||||||
python src/bot.py"]
|
python bot_launcher.py"]
|
||||||
|
|
|
||||||
45
bot.log
45
bot.log
|
|
@ -1388,3 +1388,48 @@ Loop thread traceback (most recent call last):
|
||||||
[2025-10-10 12:57:27] [INFO] [migration:107] ✓ User profile operations working
|
[2025-10-10 12:57:27] [INFO] [migration:107] ✓ User profile operations working
|
||||||
[2025-10-10 12:57:27] [INFO] [migration:118] ✓ Memory operations working
|
[2025-10-10 12:57:27] [INFO] [migration:118] ✓ Memory operations working
|
||||||
[2025-10-10 12:57:27] [INFO] [migration:138] ✓ Migration verification completed successfully
|
[2025-10-10 12:57:27] [INFO] [migration:138] ✓ Migration verification completed successfully
|
||||||
|
[2025-10-10 14:52:33] [INFO] [ai:69] 🔁 Modelfile loaded: ../examples/gojo.mod
|
||||||
|
[2025-10-10 14:52:33] [INFO] [database:325] Connected to JSON backend
|
||||||
|
[2025-10-10 14:52:33] [INFO] [database:541] Initialized JSON database backend
|
||||||
|
[2025-10-10 14:52:33] [INFO] [bot:66] 🔍 Loaded MODEL_NAME from .env: gemma3:12b
|
||||||
|
[2025-10-10 14:52:33] [INFO] [bot:68] 🧹 Attempting to clear VRAM before loading gemma3:12b...
|
||||||
|
[2025-10-10 14:52:33] [INFO] [ai:182] 🧹 Sending safe unload request for `gemma3:12b`
|
||||||
|
[2025-10-10 14:52:33] [INFO] [ai:189] 🧽 Ollama unload response: 200 - {"model":"gemma3:12b","created_at":"2025-10-10T18:52:33.307139942Z","response":"","done":true,"done_reason":"unload"}
|
||||||
|
[2025-10-10 14:52:33] [INFO] [ai:162] 🧠 Preloading model: gemma3:12b
|
||||||
|
[2025-10-10 14:52:33] [INFO] [ai:169] 📦 Model pull started successfully.
|
||||||
|
[2025-10-10 14:52:33] [INFO] [bot:72] 🚀 Model `gemma3:12b` preloaded on startup.
|
||||||
|
[2025-10-10 14:52:33] [INFO] [bot:75] ✅ Final model in use: gemma3:12b
|
||||||
|
[2025-10-10 14:52:36] [INFO] [bot:296] Logged in as AI Bot
|
||||||
|
[2025-10-10 14:52:36] [INFO] [scheduler:29] 🛑 Scheduler disabled in config.
|
||||||
|
[2025-10-10 14:54:58] [DEBUG] [autochat:92] 🎲 Reaction skipped (chance 0.10, roll 0.65)
|
||||||
|
[2025-10-10 14:54:58] [DEBUG] [autochat:92] 🎲 Reaction skipped (chance 0.10, roll 0.16)
|
||||||
|
[2025-10-10 14:54:58] [INFO] [autochat:161] 😴 No trigger and engagement is 0 — skipping.
|
||||||
|
[2025-10-10 14:54:58] [INFO] [bot:273] ============================================================ AI Response ============================================================
|
||||||
|
[2025-10-10 14:54:58] [INFO] [bot:274] 🧠 Profile loaded for Miguel (interactions: 2)
|
||||||
|
[2025-10-10 14:54:58] [INFO] [bot:278] 📚 Retrieved 0 messages for context
|
||||||
|
[2025-10-10 14:54:58] [INFO] [ai:162] 🧠 Preloading model: gemma3:12b
|
||||||
|
[2025-10-10 14:54:58] [INFO] [ai:169] 📦 Model pull started successfully.
|
||||||
|
[2025-10-10 14:54:58] [INFO] [ai:132] llm-4a81439a LLM request start model=gemma3:12b user=Miguel context_len=0
|
||||||
|
[2025-10-10 14:54:58] [DEBUG] [ai:253] llm-4a81439a Sending payload to Ollama: model=gemma3:12b user=Miguel
|
||||||
|
[2025-10-10 14:54:58] [DEBUG] [ai:254] llm-4a81439a Payload size=217 chars
|
||||||
|
[2025-10-10 14:55:10] [DEBUG] [ai:262] llm-4a81439a Raw response status=200
|
||||||
|
[2025-10-10 14:55:10] [DEBUG] [ai:263] llm-4a81439a Raw response body={"model":"gemma3:12b","created_at":"2025-10-10T18:55:10.915350904Z","response":"Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exhausting being this incredible. 😉 What about *you*? Don't tell me you're just standing there staring, you should be fanning yourself from all the brilliance.","done":true,"done_reason":"stop","context":[105,2364,107,236820,236909,9731,111038,3048,659,555,1277,236756,3764,5400,699,219769,208651,751,119814,236761,1599,236858,500,16690,236762,236764,8632,236764,532,1378,14620,236764,840,19297,20111,529,822,3272,236761,95419,528,496,4532,15737,236761,15428,19921,611,236789,500,496,9894,21603,236909,643,111038,2887,236787,1217,611,3490,236813,107,236820,236909,111457,111038,106,107,105,4368,107,12932,236764,611,1281,236764,4882,506,808,9783,236829,1972,236761,71721,80318,236764,88307,4677,607,1041,1535,5724,236764,532,6816,1646,496,70908,529,15353,236761,96612,236764,625,236789,236751,93646,1646,672,15269,236761,85345,2900,1003,808,7624,236829,236881,5185,236789,236745,3442,786,611,236789,500,1164,8101,993,47264,236764,611,1374,577,517,4601,5869,699,784,506,102141,236761],"total_duration":12103477338,"load_duration":10524951841,"prompt_eval_count":73,"prompt_eval_duration":555107252,"eval_count":72,"eval_duration":1022930871}
|
||||||
|
[2025-10-10 14:55:10] [INFO] [ai:140] llm-4a81439a LLM response model=gemma3:12b duration=12.125s summary=Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exha
|
||||||
|
[2025-10-10 14:55:10] [DEBUG] [ai:142] llm-4a81439a LLM raw response: {'model': 'gemma3:12b', 'created_at': '2025-10-10T18:55:10.915350904Z', 'response': "Oh, you know, living the *best* life. Floating effortlessly, dazzling everyone with my good looks, and generally being a beacon of awesome. Seriously, it's exhausting being this incredible. 😉 What about *you*? Don't tell me you're just standing there staring, you should be fanning yourself from all the brilliance.", 'done': True, 'done_reason': 'stop', 'context': [105, 2364, 107, 236820, 236909, 9731, 111038, 3048, 659, 555, 1277, 236756, 3764, 5400, 699, 219769, 208651, 751, 119814, 236761, 1599, 236858, 500, 16690, 236762, 236764, 8632, 236764, 532, 1378, 14620, 236764, 840, 19297, 20111, 529, 822, 3272, 236761, 95419, 528, 496, 4532, 15737, 236761, 15428, 19921, 611, 236789, 500, 496, 9894, 21603, 236909, 643, 111038, 2887, 236787, 1217, 611, 3490, 236813, 107, 236820, 236909, 111457, 111038, 106, 107, 105, 4368, 107, 12932, 236764, 611, 1281, 236764, 4882, 506, 808, 9783, 236829, 1972, 236761, 71721, 80318, 236764, 88307, 4677, 607, 1041, 1535, 5724, 236764, 532, 6816, 1646, 496, 70908, 529, 15353, 236761, 96612, 236764, 625, 236789, 236751, 93646, 1646, 672, 15269, 236761, 85345, 2900, 1003, 808, 7624, 236829, 236881, 5185, 236789, 236745, 3442, 786, 611, 236789, 500, 1164, 8101, 993, 47264, 236764, 611, 1374, 577, 517, 4601, 5869, 699, 784, 506, 102141, 236761], 'total_duration': 12103477338, 'load_duration': 10524951841, 'prompt_eval_count': 73, 'prompt_eval_duration': 555107252, 'eval_count': 72, 'eval_duration': 1022930871}
|
||||||
|
[2025-10-10 14:55:11] [DEBUG] [bot:244] on_message: observed own message id=1426281930004889680 channel=1380999713272238151
|
||||||
|
[2025-10-10 15:04:00] [INFO] [database:325] Connected to JSON backend
|
||||||
|
[2025-10-10 15:04:00] [INFO] [database:541] Initialized JSON database backend
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8080
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:315] ==================================================
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:317] Local: http://localhost:8080
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:318] Network: http://192.168.0.144:8080
|
||||||
|
[2025-10-10 15:04:00] [INFO] [webui:319] ==================================================
|
||||||
|
[2025-10-10 15:04:46] [INFO] [database:325] Connected to JSON backend
|
||||||
|
[2025-10-10 15:04:46] [INFO] [database:541] Initialized JSON database backend
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8081
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:315] ==================================================
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:317] Local: http://localhost:8081
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:318] Network: http://192.168.0.144:8081
|
||||||
|
[2025-10-10 15:04:46] [INFO] [webui:319] ==================================================
|
||||||
|
|
|
||||||
89
bot_launcher.py
Normal file
89
bot_launcher.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
bot_launcher.py
|
||||||
|
Launches both the Discord bot and web UI together
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add src directory to path
|
||||||
|
src_path = Path(__file__).parent / 'src'
|
||||||
|
sys.path.insert(0, str(src_path))
|
||||||
|
|
||||||
|
def start_web_ui():
|
||||||
|
"""Start the web UI server in a separate thread"""
|
||||||
|
try:
|
||||||
|
from web_ui import run_web_server
|
||||||
|
port = int(os.getenv('WEB_PORT', 8080))
|
||||||
|
debug = os.getenv('DEBUG', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
run_web_server(host='0.0.0.0', port=port, debug=debug)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to start web UI: {e}")
|
||||||
|
if 'flask' in str(e).lower():
|
||||||
|
print("💡 Install Flask to use the web UI: pip install flask")
|
||||||
|
elif 'port' in str(e).lower() or 'address' in str(e).lower():
|
||||||
|
print(f"💡 Port {port} may be in use. Try a different port with WEB_PORT environment variable")
|
||||||
|
|
||||||
|
def start_discord_bot():
|
||||||
|
"""Start the Discord bot"""
|
||||||
|
try:
|
||||||
|
# Change to src directory for bot execution
|
||||||
|
original_cwd = os.getcwd()
|
||||||
|
os.chdir(src_path)
|
||||||
|
|
||||||
|
print("🤖 Starting Discord bot...")
|
||||||
|
import bot
|
||||||
|
# The bot.py should handle its own execution
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to start Discord bot: {e}")
|
||||||
|
finally:
|
||||||
|
# Restore original working directory
|
||||||
|
if 'original_cwd' in locals():
|
||||||
|
os.chdir(original_cwd)
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Handle shutdown signals"""
|
||||||
|
print("\n🛑 Shutting down...")
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main launcher function"""
|
||||||
|
print("🚀 Delta Bot Launcher")
|
||||||
|
print("=" * 30)
|
||||||
|
|
||||||
|
# Set up signal handlers for graceful shutdown
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Check if web UI should be disabled
|
||||||
|
disable_web = os.getenv('DISABLE_WEB_UI', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
if not disable_web:
|
||||||
|
# Start web UI in a separate thread
|
||||||
|
web_thread = threading.Thread(target=start_web_ui, daemon=True)
|
||||||
|
web_thread.start()
|
||||||
|
|
||||||
|
# Give web UI time to start and display URLs
|
||||||
|
time.sleep(3)
|
||||||
|
else:
|
||||||
|
print("🚫 Web UI disabled by DISABLE_WEB_UI environment variable")
|
||||||
|
|
||||||
|
# Start Discord bot (this will block)
|
||||||
|
try:
|
||||||
|
start_discord_bot()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 Received interrupt signal")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Fatal error: {e}")
|
||||||
|
finally:
|
||||||
|
print("👋 Goodbye!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -9,6 +9,11 @@ services:
|
||||||
- CHANNEL_ID=${CHANNEL_ID}
|
- CHANNEL_ID=${CHANNEL_ID}
|
||||||
- OLLAMA_API=${OLLAMA_API}
|
- OLLAMA_API=${OLLAMA_API}
|
||||||
- MODEL_NAME=${MODEL_NAME}
|
- MODEL_NAME=${MODEL_NAME}
|
||||||
|
- WEB_PORT=8080
|
||||||
|
- DATABASE_BACKEND=sqlite
|
||||||
|
- MEMORY_ENABLED=true
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # Web UI port
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app # Mount host ./data directory as /app in the container
|
- ./data:/app # Mount host ./data directory as /app in the container
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
5
memory.json
Normal file
5
memory.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"conversations": {},
|
||||||
|
"user_memories": {},
|
||||||
|
"global_events": []
|
||||||
|
}
|
||||||
12
requirements-webui.txt
Normal file
12
requirements-webui.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Web UI Dependencies
|
||||||
|
Flask==2.3.3
|
||||||
|
PyYAML==6.0.1
|
||||||
|
|
||||||
|
# Existing bot dependencies (if not already installed)
|
||||||
|
discord.py>=2.3.0
|
||||||
|
requests>=2.31.0
|
||||||
|
python-dateutil>=2.8.2
|
||||||
|
|
||||||
|
# Optional: For better web UI features
|
||||||
|
gunicorn==21.2.0 # Production WSGI server
|
||||||
|
Werkzeug==2.3.7 # WSGI utilities
|
||||||
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/bot.cpython-312.pyc
Normal file
BIN
src/__pycache__/bot.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/database.cpython-311.pyc
Normal file
BIN
src/__pycache__/database.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/enhanced_ai.cpython-311.pyc
Normal file
BIN
src/__pycache__/enhanced_ai.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/memory_manager.cpython-311.pyc
Normal file
BIN
src/__pycache__/memory_manager.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/memory_manager.cpython-312.pyc
Normal file
BIN
src/__pycache__/memory_manager.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/web_ui.cpython-312.pyc
Normal file
BIN
src/__pycache__/web_ui.cpython-312.pyc
Normal file
Binary file not shown.
|
|
@ -0,0 +1,13 @@
|
||||||
|
[2025-10-10 13:29:37] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:30:50] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:31:19] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:31:24] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:45:36] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:53:17] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:53:32] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:54:13] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:54:23] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:55:25] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:04:09] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:04:14] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:05:15] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
24
src/bot.log
24
src/bot.log
|
|
@ -6,3 +6,27 @@
|
||||||
[2025-10-10 13:01:51] [INFO] [database:81] Connected to SQLite database: data/deltabot.db
|
[2025-10-10 13:01:51] [INFO] [database:81] Connected to SQLite database: data/deltabot.db
|
||||||
[2025-10-10 13:01:51] [DEBUG] [database:142] Database tables initialized
|
[2025-10-10 13:01:51] [DEBUG] [database:142] Database tables initialized
|
||||||
[2025-10-10 13:01:51] [INFO] [database:536] Initialized SQLite database backend
|
[2025-10-10 13:01:51] [INFO] [database:536] Initialized SQLite database backend
|
||||||
|
[2025-10-10 13:29:16] [INFO] [database:325] Connected to JSON backend
|
||||||
|
[2025-10-10 13:29:16] [INFO] [database:541] Initialized JSON database backend
|
||||||
|
[2025-10-10 13:29:16] [INFO] [webui:303] Starting web UI server on 0.0.0.0:8080
|
||||||
|
[2025-10-10 13:29:37] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:30:50] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:31:19] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:31:24] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 13:45:36] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:53:17] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:53:32] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:54:13] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:54:23] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 14:55:25] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:04:09] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:04:14] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
[2025-10-10 15:05:13] [INFO] [database:325] Connected to JSON backend
|
||||||
|
[2025-10-10 15:05:13] [INFO] [database:541] Initialized JSON database backend
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:314] Starting web UI server on 0.0.0.0:8082
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:315] ==================================================
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:316] 🌐 WEB UI ACCESS URLS:
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:317] Local: http://localhost:8082
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:318] Network: http://192.168.0.144:8082
|
||||||
|
[2025-10-10 15:05:13] [INFO] [webui:319] ==================================================
|
||||||
|
[2025-10-10 15:05:15] [ERROR] [webui:82] Failed to load settings: [Errno 2] No such file or directory: '/app/src/settings.yml'
|
||||||
|
|
|
||||||
137
src/templates/base.html
Normal file
137
src/templates/base.html
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Delta Bot Dashboard{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.sidebar {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.sidebar .nav-link {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
}
|
||||||
|
.sidebar .nav-link:hover, .sidebar .nav-link.active {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.status-online { color: #28a745; }
|
||||||
|
.status-offline { color: #dc3545; }
|
||||||
|
.status-warning { color: #ffc107; }
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse">
|
||||||
|
<div class="position-sticky pt-3">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h4 class="text-white"><i class="fas fa-robot"></i> Delta Bot</h4>
|
||||||
|
<small class="text-white-50">Management Dashboard</small>
|
||||||
|
</div>
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">
|
||||||
|
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'config' %}active{% endif %}" href="{{ url_for('config') }}">
|
||||||
|
<i class="fas fa-cog"></i> Configuration
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'stats' %}active{% endif %}" href="{{ url_for('stats') }}">
|
||||||
|
<i class="fas fa-chart-bar"></i> Statistics
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'memory' %}active{% endif %}" href="{{ url_for('memory') }}">
|
||||||
|
<i class="fas fa-brain"></i> Memory
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr class="text-white-50">
|
||||||
|
<div class="text-center">
|
||||||
|
<small class="text-white-50">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
{{ db_stats.backend_type|title if db_stats else 'Unknown' }} Backend
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">{% block page_title %}{% endblock %}</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<span class="badge bg-success fs-6" id="status-indicator">
|
||||||
|
<i class="fas fa-circle"></i> Online
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Auto-refresh stats every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
fetch('/api/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update status indicator
|
||||||
|
const statusIndicator = document.getElementById('status-indicator');
|
||||||
|
if (data.bot_stats) {
|
||||||
|
statusIndicator.innerHTML = '<i class="fas fa-circle"></i> Online';
|
||||||
|
statusIndicator.className = 'badge bg-success fs-6';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const statusIndicator = document.getElementById('status-indicator');
|
||||||
|
statusIndicator.innerHTML = '<i class="fas fa-circle"></i> Offline';
|
||||||
|
statusIndicator.className = 'badge bg-danger fs-6';
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
161
src/templates/config.html
Normal file
161
src/templates/config.html
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Configuration - Delta Bot{% endblock %}
|
||||||
|
{% block page_title %}Configuration{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="{{ url_for('save_config') }}">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Database Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-database"></i> Database Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="database_backend" class="form-label">Database Backend</label>
|
||||||
|
<select class="form-select" id="database_backend" name="database_backend">
|
||||||
|
<option value="json" {% if settings.database.backend == 'json' %}selected{% endif %}>JSON Files (Backward Compatible)</option>
|
||||||
|
<option value="sqlite" {% if settings.database.backend == 'sqlite' %}selected{% endif %}>SQLite Database (Recommended)</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">SQLite is recommended for better performance and data integrity.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sqlite_path" class="form-label">SQLite Database Path</label>
|
||||||
|
<input type="text" class="form-control" id="sqlite_path" name="sqlite_path"
|
||||||
|
value="{{ settings.database.sqlite_path or 'data/deltabot.db' }}">
|
||||||
|
<div class="form-text">Path to SQLite database file (only used if SQLite backend is selected).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Memory Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-brain"></i> Memory Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="memory_enabled" name="memory_enabled"
|
||||||
|
value="true" {% if settings.memory.enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="memory_enabled">
|
||||||
|
Enable Memory System
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Allows the bot to remember conversations and user preferences. Disable for privacy-focused deployments.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="importance_threshold" class="form-label">Importance Threshold</label>
|
||||||
|
<input type="range" class="form-range" id="importance_threshold" name="importance_threshold"
|
||||||
|
min="0" max="1" step="0.1" value="{{ settings.memory.importance_threshold or 0.3 }}"
|
||||||
|
oninput="this.nextElementSibling.value = this.value">
|
||||||
|
<output>{{ settings.memory.importance_threshold or 0.3 }}</output>
|
||||||
|
<div class="form-text">Minimum importance score (0.0-1.0) for storing memories. Higher values store fewer memories.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- AutoChat Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-robot"></i> AutoChat Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autochat_enabled" name="autochat_enabled"
|
||||||
|
value="true" {% if settings.autochat.enable_reactions %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="autochat_enabled">
|
||||||
|
Enable Emoji Reactions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Allow the bot to react to messages with emojis.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="reaction_chance" class="form-label">Reaction Chance</label>
|
||||||
|
<input type="range" class="form-range" id="reaction_chance" name="reaction_chance"
|
||||||
|
min="0" max="1" step="0.05" value="{{ settings.autochat.emoji_reaction_chance or 0.1 }}"
|
||||||
|
oninput="this.nextElementSibling.value = (this.value * 100).toFixed(0) + '%'">
|
||||||
|
<output>{{ ((settings.autochat.emoji_reaction_chance or 0.1) * 100)|round|int }}%</output>
|
||||||
|
<div class="form-text">Probability of reacting to any given message.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Context Configuration -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-comments"></i> Context Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="context_enabled" name="context_enabled"
|
||||||
|
value="true" {% if settings.context.enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="context_enabled">
|
||||||
|
Enable Context Tracking
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Include recent messages in AI responses for better context awareness.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="max_messages" class="form-label">Max Context Messages</label>
|
||||||
|
<input type="number" class="form-control" id="max_messages" name="max_messages"
|
||||||
|
min="1" max="50" value="{{ settings.context.max_messages or 15 }}">
|
||||||
|
<div class="form-text">Maximum number of recent messages to include in context (1-50).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<button type="submit" class="btn btn-success btn-lg">
|
||||||
|
<i class="fas fa-save"></i> Save Configuration
|
||||||
|
</button>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Some changes may require restarting the bot to take effect.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Update range slider displays
|
||||||
|
document.querySelectorAll('input[type="range"]').forEach(function(range) {
|
||||||
|
range.addEventListener('input', function() {
|
||||||
|
const output = this.nextElementSibling;
|
||||||
|
if (this.name === 'reaction_chance') {
|
||||||
|
output.textContent = (this.value * 100).toFixed(0) + '%';
|
||||||
|
} else {
|
||||||
|
output.textContent = this.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
191
src/templates/dashboard.html
Normal file
191
src/templates/dashboard.html
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Delta Bot{% endblock %}
|
||||||
|
{% block page_title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Bot Status Cards -->
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.uptime }}</div>
|
||||||
|
<div class="small">Uptime</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-clock fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.message_count }}</div>
|
||||||
|
<div class="small">Messages Processed</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-comments fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="h5 mb-0 font-weight-bold">{{ bot_stats.unique_users }}</div>
|
||||||
|
<div class="small">Active Users</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-users fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="h5 mb-0 font-weight-bold">{{ db_stats.total_users if db_stats.total_users is defined else '?' }}</div>
|
||||||
|
<div class="small">Total Profiles</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-database fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- System Status -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-server"></i> System Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4"><strong>Database Backend:</strong></div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<span class="badge bg-primary">{{ db_stats.backend_type|title if db_stats.backend_type else 'Unknown' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4"><strong>Memory System:</strong></div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% if db_stats.memory_enabled %}
|
||||||
|
<span class="badge bg-success"><i class="fas fa-check"></i> Enabled</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning"><i class="fas fa-times"></i> Disabled</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4"><strong>Last Activity:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ bot_stats.last_activity }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4"><strong>Active Channels:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ bot_stats.active_channels }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-activity"></i> Database Info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if db_stats.error %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> {{ db_stats.error }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-6"><strong>Total Users:</strong></div>
|
||||||
|
<div class="col-sm-6">{{ db_stats.total_users }}</div>
|
||||||
|
</div>
|
||||||
|
{% if db_stats.memory_enabled %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-6"><strong>Stored Memories:</strong></div>
|
||||||
|
<div class="col-sm-6">{{ db_stats.total_memories }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-6"><strong>Conversations:</strong></div>
|
||||||
|
<div class="col-sm-6">{{ db_stats.total_conversations }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i> Memory system is disabled
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-bolt"></i> Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<a href="{{ url_for('config') }}" class="btn btn-primary btn-block w-100">
|
||||||
|
<i class="fas fa-cog"></i> Configure Bot
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<a href="{{ url_for('stats') }}" class="btn btn-info btn-block w-100">
|
||||||
|
<i class="fas fa-chart-bar"></i> View Statistics
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<a href="{{ url_for('memory') }}" class="btn btn-success btn-block w-100">
|
||||||
|
<i class="fas fa-brain"></i> Manage Memory
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<button class="btn btn-warning btn-block w-100" onclick="restartBot()">
|
||||||
|
<i class="fas fa-redo"></i> Restart Bot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function restartBot() {
|
||||||
|
if (confirm('Are you sure you want to restart the bot? This will temporarily disconnect from Discord.')) {
|
||||||
|
// TODO: Implement bot restart functionality
|
||||||
|
alert('Bot restart functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
225
src/templates/memory.html
Normal file
225
src/templates/memory.html
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Memory Management - Delta Bot{% endblock %}
|
||||||
|
{% block page_title %}Memory Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if memory_disabled %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Memory System Disabled</strong><br>
|
||||||
|
The memory system is currently disabled. Enable it in the <a href="{{ url_for('config') }}">configuration</a> to use memory features.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Memory Actions -->
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-tools"></i> Memory Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-warning" onclick="cleanupMemories()">
|
||||||
|
<i class="fas fa-broom"></i> Cleanup Old Memories
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-info" onclick="exportMemories()">
|
||||||
|
<i class="fas fa-download"></i> Export Memories
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" onclick="clearAllMemories()">
|
||||||
|
<i class="fas fa-trash"></i> Clear All Memories
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Users with Memories -->
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-users"></i> Users with Memories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if users %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Memory Count</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.display_name or 'Unknown User' }}</td>
|
||||||
|
<td><span class="badge bg-primary">{{ user.memory_count }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-info" onclick="viewUserMemories('{{ user.user_id }}')">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteUserMemories('{{ user.user_id }}')">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No users with stored memories found.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Memories -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-history"></i> Recent Conversation Memories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if memories %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Content</th>
|
||||||
|
<th>Importance</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for memory in memories %}
|
||||||
|
<tr>
|
||||||
|
<td><small>{{ memory.timestamp[:16] if memory.timestamp else 'Unknown' }}</small></td>
|
||||||
|
<td>{{ memory.display_name or 'Unknown' }}</td>
|
||||||
|
<td>
|
||||||
|
<div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
{{ memory.content[:100] }}{% if memory.content|length > 100 %}...{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% set importance = memory.importance or 0 %}
|
||||||
|
{% if importance >= 0.8 %}
|
||||||
|
<span class="badge bg-danger">High</span>
|
||||||
|
{% elif importance >= 0.5 %}
|
||||||
|
<span class="badge bg-warning">Medium</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Low</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-info" onclick="viewMemoryDetails('{{ memory.id }}')">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteMemory('{{ memory.id }}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No conversation memories found.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Memory Details Modal -->
|
||||||
|
<div class="modal fade" id="memoryModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Memory Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="memoryModalBody">
|
||||||
|
<!-- Memory details will be loaded here -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function cleanupMemories() {
|
||||||
|
const days = prompt('Delete memories older than how many days?', '30');
|
||||||
|
if (days && !isNaN(days)) {
|
||||||
|
fetch('/api/memory/cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ days: parseInt(days) })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(data.message);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Error: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportMemories() {
|
||||||
|
alert('Export functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllMemories() {
|
||||||
|
if (confirm('Are you sure you want to delete ALL memories? This cannot be undone!')) {
|
||||||
|
alert('Clear all memories functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewUserMemories(userId) {
|
||||||
|
alert('View user memories functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUserMemories(userId) {
|
||||||
|
if (confirm('Are you sure you want to delete all memories for this user?')) {
|
||||||
|
alert('Delete user memories functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewMemoryDetails(memoryId) {
|
||||||
|
document.getElementById('memoryModalBody').innerHTML = '<p>Loading memory details...</p>';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('memoryModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// TODO: Fetch memory details from API
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('memoryModalBody').innerHTML = '<p>Memory details functionality will be implemented soon!</p>';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMemory(memoryId) {
|
||||||
|
if (confirm('Are you sure you want to delete this memory?')) {
|
||||||
|
alert('Delete memory functionality will be implemented soon!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
193
src/templates/stats.html
Normal file
193
src/templates/stats.html
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Statistics - Delta Bot{% endblock %}
|
||||||
|
{% block page_title %}Statistics{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Bot Performance Chart -->
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-chart-line"></i> Bot Activity</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="activityChart" width="400" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Users -->
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-crown"></i> Top Active Users</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if bot_stats.most_active_users %}
|
||||||
|
{% for user_id, count in bot_stats.most_active_users %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span>User {{ user_id[:8] }}...</span>
|
||||||
|
<span class="badge bg-primary">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No user activity recorded yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Database Statistics -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-database"></i> Database Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-primary">{{ db_stats.total_users if db_stats.total_users is defined else '?' }}</h3>
|
||||||
|
<p class="mb-0">Total User Profiles</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-success">{{ db_stats.total_memories if db_stats.total_memories is defined else '?' }}</h3>
|
||||||
|
<p class="mb-0">Stored Memories</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-info">{{ db_stats.total_conversations if db_stats.total_conversations is defined else '?' }}</h3>
|
||||||
|
<p class="mb-0">Conversation Records</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-warning">{{ db_stats.backend_type|title if db_stats.backend_type else '?' }}</h3>
|
||||||
|
<p class="mb-0">Backend Type</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Users -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-users"></i> Recent User Activity</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if recent_users %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Interactions</th>
|
||||||
|
<th>Last Seen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in recent_users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.display_name or user.name or 'Unknown' }}</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ user.interactions or 0 }}</span></td>
|
||||||
|
<td><small>{{ user.last_seen[:16] if user.last_seen else 'Never' }}</small></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No user data available.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Information -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-info-circle"></i> System Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Bot Uptime:</strong><br>
|
||||||
|
<span class="text-muted">{{ bot_stats.uptime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Messages Processed:</strong><br>
|
||||||
|
<span class="text-muted">{{ bot_stats.message_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Active Channels:</strong><br>
|
||||||
|
<span class="text-muted">{{ bot_stats.active_channels }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Last Activity:</strong><br>
|
||||||
|
<span class="text-muted">{{ bot_stats.last_activity }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Create activity chart
|
||||||
|
const ctx = document.getElementById('activityChart').getContext('2d');
|
||||||
|
const activityChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['6h ago', '5h ago', '4h ago', '3h ago', '2h ago', '1h ago', 'Now'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Messages',
|
||||||
|
data: [12, 19, 8, 15, 22, 18, 25], // This would be dynamic in real implementation
|
||||||
|
borderColor: 'rgb(102, 126, 234)',
|
||||||
|
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-refresh stats every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
fetch('/api/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update charts and stats here
|
||||||
|
console.log('Stats updated:', data);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching stats:', error));
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,5 +3,16 @@
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"display_name": "Test User",
|
"display_name": "Test User",
|
||||||
"interactions": 5
|
"interactions": 5
|
||||||
|
},
|
||||||
|
"161149541171593216": {
|
||||||
|
"name": "themiloverse",
|
||||||
|
"display_name": "Miguel",
|
||||||
|
"first_seen": "2025-10-10T18:54:58.411792",
|
||||||
|
"last_seen": "2025-10-10T18:54:58.412549",
|
||||||
|
"last_message": "2025-10-10T18:54:58.412549",
|
||||||
|
"interactions": 2,
|
||||||
|
"pronouns": null,
|
||||||
|
"avatar_url": "https://cdn.discordapp.com/avatars/161149541171593216/fb0553a29d9f73175cb6aea24d0e19ec.png?size=1024",
|
||||||
|
"custom_prompt": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
332
src/web_ui.py
Normal file
332
src/web_ui.py
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
"""
|
||||||
|
web_ui.py
|
||||||
|
Web dashboard for Discord bot management
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
# Import bot components
|
||||||
|
import sys
|
||||||
|
sys.path.append('/app/src' if os.path.exists('/app/src') else 'src')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from database import db_manager
|
||||||
|
from memory_manager import memory_manager
|
||||||
|
from logger import setup_logger
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Warning: Could not import bot components: {e}")
|
||||||
|
db_manager = None
|
||||||
|
memory_manager = None
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = secrets.token_hex(16) # Generate random secret key
|
||||||
|
logger = setup_logger("webui") if 'setup_logger' in globals() else None
|
||||||
|
|
||||||
|
class BotStats:
|
||||||
|
"""Track bot statistics"""
|
||||||
|
def __init__(self):
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
self.message_count = 0
|
||||||
|
self.user_interactions = {}
|
||||||
|
self.channel_activity = {}
|
||||||
|
self.last_activity = None
|
||||||
|
|
||||||
|
def record_message(self, user_id, channel_id):
|
||||||
|
self.message_count += 1
|
||||||
|
self.user_interactions[user_id] = self.user_interactions.get(user_id, 0) + 1
|
||||||
|
self.channel_activity[channel_id] = self.channel_activity.get(channel_id, 0) + 1
|
||||||
|
self.last_activity = datetime.now()
|
||||||
|
|
||||||
|
def get_stats(self):
|
||||||
|
uptime = datetime.now() - self.start_time
|
||||||
|
return {
|
||||||
|
'uptime': str(uptime).split('.')[0], # Remove microseconds
|
||||||
|
'message_count': self.message_count,
|
||||||
|
'unique_users': len(self.user_interactions),
|
||||||
|
'active_channels': len(self.channel_activity),
|
||||||
|
'last_activity': self.last_activity.strftime('%Y-%m-%d %H:%M:%S') if self.last_activity else 'Never',
|
||||||
|
'most_active_users': sorted(self.user_interactions.items(), key=lambda x: x[1], reverse=True)[:5],
|
||||||
|
'most_active_channels': sorted(self.channel_activity.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||||
|
}
|
||||||
|
|
||||||
|
bot_stats = BotStats()
|
||||||
|
|
||||||
|
# Authentication decorator (placeholder for now)
|
||||||
|
def require_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# TODO: Implement proper authentication
|
||||||
|
# For now, just check if we're in development mode
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def load_settings():
|
||||||
|
"""Load bot settings from YAML file"""
|
||||||
|
try:
|
||||||
|
settings_path = os.path.join('src', 'settings.yml')
|
||||||
|
if not os.path.exists(settings_path):
|
||||||
|
settings_path = os.path.join('/app/src', 'settings.yml')
|
||||||
|
|
||||||
|
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
except Exception as e:
|
||||||
|
if logger:
|
||||||
|
logger.error(f"Failed to load settings: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_settings(settings):
|
||||||
|
"""Save bot settings to YAML file"""
|
||||||
|
try:
|
||||||
|
settings_path = os.path.join('src', 'settings.yml')
|
||||||
|
if not os.path.exists(settings_path):
|
||||||
|
settings_path = os.path.join('/app/src', 'settings.yml')
|
||||||
|
|
||||||
|
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||||
|
yaml.dump(settings, f, default_flow_style=False, indent=2)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
if logger:
|
||||||
|
logger.error(f"Failed to save settings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_database_stats():
|
||||||
|
"""Get database statistics"""
|
||||||
|
if not db_manager:
|
||||||
|
return {"error": "Database manager not available"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
stats = {
|
||||||
|
'backend_type': db_manager.get_backend_type(),
|
||||||
|
'memory_enabled': db_manager.is_memory_enabled(),
|
||||||
|
'total_users': 0,
|
||||||
|
'total_memories': 0,
|
||||||
|
'total_conversations': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to get counts from database
|
||||||
|
if hasattr(db_manager.backend, 'connection') and db_manager.backend.connection:
|
||||||
|
cursor = db_manager.backend.connection.cursor()
|
||||||
|
|
||||||
|
# Count user profiles
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM user_profiles")
|
||||||
|
stats['total_users'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if db_manager.is_memory_enabled():
|
||||||
|
# Count memories
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM user_memories")
|
||||||
|
stats['total_memories'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Count conversations
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM conversation_memories")
|
||||||
|
stats['total_conversations'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
return stats
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@require_auth
|
||||||
|
def dashboard():
|
||||||
|
"""Main dashboard"""
|
||||||
|
settings = load_settings()
|
||||||
|
db_stats = get_database_stats()
|
||||||
|
bot_stats_data = bot_stats.get_stats()
|
||||||
|
|
||||||
|
return render_template('dashboard.html',
|
||||||
|
settings=settings,
|
||||||
|
db_stats=db_stats,
|
||||||
|
bot_stats=bot_stats_data)
|
||||||
|
|
||||||
|
@app.route('/config')
|
||||||
|
@require_auth
|
||||||
|
def config():
|
||||||
|
"""Configuration page"""
|
||||||
|
settings = load_settings()
|
||||||
|
return render_template('config.html', settings=settings)
|
||||||
|
|
||||||
|
@app.route('/config/save', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def save_config():
|
||||||
|
"""Save configuration changes"""
|
||||||
|
try:
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
# Update database settings
|
||||||
|
if 'database_backend' in request.form:
|
||||||
|
settings.setdefault('database', {})['backend'] = request.form['database_backend']
|
||||||
|
|
||||||
|
if 'sqlite_path' in request.form:
|
||||||
|
settings.setdefault('database', {})['sqlite_path'] = request.form['sqlite_path']
|
||||||
|
|
||||||
|
# Update memory settings
|
||||||
|
if 'memory_enabled' in request.form:
|
||||||
|
settings.setdefault('memory', {})['enabled'] = request.form['memory_enabled'] == 'true'
|
||||||
|
|
||||||
|
if 'importance_threshold' in request.form:
|
||||||
|
settings.setdefault('memory', {})['importance_threshold'] = float(request.form['importance_threshold'])
|
||||||
|
|
||||||
|
# Update autochat settings
|
||||||
|
if 'autochat_enabled' in request.form:
|
||||||
|
settings.setdefault('autochat', {})['enable_reactions'] = request.form['autochat_enabled'] == 'true'
|
||||||
|
|
||||||
|
if 'reaction_chance' in request.form:
|
||||||
|
settings.setdefault('autochat', {})['emoji_reaction_chance'] = float(request.form['reaction_chance'])
|
||||||
|
|
||||||
|
# Update context settings
|
||||||
|
if 'context_enabled' in request.form:
|
||||||
|
settings.setdefault('context', {})['enabled'] = request.form['context_enabled'] == 'true'
|
||||||
|
|
||||||
|
if 'max_messages' in request.form:
|
||||||
|
settings.setdefault('context', {})['max_messages'] = int(request.form['max_messages'])
|
||||||
|
|
||||||
|
# Save settings
|
||||||
|
if save_settings(settings):
|
||||||
|
flash('Configuration saved successfully! Restart the bot to apply changes.', 'success')
|
||||||
|
else:
|
||||||
|
flash('Failed to save configuration.', 'error')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error saving configuration: {str(e)}', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('config'))
|
||||||
|
|
||||||
|
@app.route('/stats')
|
||||||
|
@require_auth
|
||||||
|
def stats():
|
||||||
|
"""Statistics page"""
|
||||||
|
db_stats = get_database_stats()
|
||||||
|
bot_stats_data = bot_stats.get_stats()
|
||||||
|
|
||||||
|
# Get recent user activity if database is available
|
||||||
|
recent_users = []
|
||||||
|
if db_manager and hasattr(db_manager.backend, 'connection'):
|
||||||
|
try:
|
||||||
|
cursor = db_manager.backend.connection.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT name, display_name, interactions, last_seen
|
||||||
|
FROM user_profiles
|
||||||
|
ORDER BY last_seen DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
recent_users = [dict(row) for row in cursor.fetchall()]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render_template('stats.html',
|
||||||
|
db_stats=db_stats,
|
||||||
|
bot_stats=bot_stats_data,
|
||||||
|
recent_users=recent_users)
|
||||||
|
|
||||||
|
@app.route('/memory')
|
||||||
|
@require_auth
|
||||||
|
def memory():
|
||||||
|
"""Memory management page"""
|
||||||
|
if not db_manager or not db_manager.is_memory_enabled():
|
||||||
|
return render_template('memory.html',
|
||||||
|
memories=[],
|
||||||
|
users=[],
|
||||||
|
memory_disabled=True)
|
||||||
|
|
||||||
|
# Get recent memories
|
||||||
|
recent_memories = []
|
||||||
|
users_with_memories = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(db_manager.backend, 'connection'):
|
||||||
|
cursor = db_manager.backend.connection.cursor()
|
||||||
|
|
||||||
|
# Get recent conversation memories
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT cm.*, up.display_name
|
||||||
|
FROM conversation_memories cm
|
||||||
|
LEFT JOIN user_profiles up ON cm.user_id = up.user_id
|
||||||
|
ORDER BY cm.timestamp DESC
|
||||||
|
LIMIT 20
|
||||||
|
""")
|
||||||
|
recent_memories = [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Get users with memories
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT up.user_id, up.display_name, COUNT(um.id) as memory_count
|
||||||
|
FROM user_profiles up
|
||||||
|
LEFT JOIN user_memories um ON up.user_id = um.user_id
|
||||||
|
GROUP BY up.user_id
|
||||||
|
HAVING memory_count > 0
|
||||||
|
ORDER BY memory_count DESC
|
||||||
|
LIMIT 20
|
||||||
|
""")
|
||||||
|
users_with_memories = [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if logger:
|
||||||
|
logger.error(f"Error fetching memory data: {e}")
|
||||||
|
|
||||||
|
return render_template('memory.html',
|
||||||
|
memories=recent_memories,
|
||||||
|
users=users_with_memories,
|
||||||
|
memory_disabled=False)
|
||||||
|
|
||||||
|
@app.route('/api/stats')
|
||||||
|
@require_auth
|
||||||
|
def api_stats():
|
||||||
|
"""API endpoint for real-time stats"""
|
||||||
|
return jsonify({
|
||||||
|
'bot_stats': bot_stats.get_stats(),
|
||||||
|
'db_stats': get_database_stats()
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/memory/cleanup', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def api_memory_cleanup():
|
||||||
|
"""API endpoint to cleanup old memories"""
|
||||||
|
if not memory_manager or not memory_manager.is_enabled():
|
||||||
|
return jsonify({'error': 'Memory system disabled'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
days = int(request.json.get('days', 30))
|
||||||
|
memory_manager.cleanup_old_memories(days)
|
||||||
|
return jsonify({'success': True, 'message': f'Cleaned up memories older than {days} days'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
def run_web_server(host='0.0.0.0', port=8080, debug=False):
|
||||||
|
"""Run the web server"""
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# Get local IP for network access
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
except:
|
||||||
|
local_ip = "localhost"
|
||||||
|
|
||||||
|
if logger:
|
||||||
|
logger.info(f"Starting web UI server on {host}:{port}")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("🌐 WEB UI ACCESS URLS:")
|
||||||
|
logger.info(f" Local: http://localhost:{port}")
|
||||||
|
logger.info(f" Network: http://{local_ip}:{port}")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
else:
|
||||||
|
print("=" * 50)
|
||||||
|
print("🌐 DELTA BOT WEB UI STARTED")
|
||||||
|
print(f" Local: http://localhost:{port}")
|
||||||
|
print(f" Network: http://{local_ip}:{port}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
app.run(host=host, port=port, debug=debug, threaded=True)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.getenv('WEB_PORT', 8080))
|
||||||
|
debug = os.getenv('DEBUG', 'false').lower() == 'true'
|
||||||
|
run_web_server(port=port, debug=debug)
|
||||||
1
user_profiles.json
Normal file
1
user_profiles.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
Loading…
Reference in a new issue