Files
discord-fishbowl/admin_interface_updated.html
root 10563900a3 Implement comprehensive LLM provider system with global cost protection
- Add multi-provider LLM architecture supporting OpenRouter, OpenAI, Gemini, and custom providers
- Implement global LLM on/off switch with default DISABLED state for cost protection
- Add per-character LLM configuration with provider-specific models and settings
- Create performance-optimized caching system for LLM enabled status checks
- Add API key validation before enabling LLM providers to prevent broken configurations
- Implement audit logging for all LLM enable/disable actions for cost accountability
- Create comprehensive admin UI with prominent cost warnings and confirmation dialogs
- Add visual indicators in character list for custom AI model configurations
- Build character-specific LLM client system with global fallback mechanism
- Add database schema support for per-character LLM settings
- Implement graceful fallback responses when LLM is globally disabled
- Create provider testing and validation system for reliable connections
2025-07-08 07:35:48 -07:00

1225 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Discord Fishbowl Admin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.login-form { max-width: 420px; margin: 100px auto; background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); padding: 40px; border-radius: 16px; box-shadow: 0 25px 50px rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.3); }
.login-form h2 { text-align: center; margin-bottom: 10px; color: #1e293b; font-size: 2rem; font-weight: 700; }
.login-form p { text-align: center; color: #64748b; margin-bottom: 30px; }
.dashboard { display: none; }
.header { background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); color: #1e293b; padding: 30px; border-radius: 16px; margin-bottom: 30px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.3); }
.header h1 { font-size: 2.5rem; margin-bottom: 8px; font-weight: 800; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.header p { color: #64748b; font-size: 1.1rem; margin-bottom: 15px; }
.auto-refresh { font-size: 0.9rem; color: #10b981; font-weight: 500; }
.card { background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); padding: 25px; border-radius: 16px; margin-bottom: 25px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; }
.card:hover { transform: translateY(-5px); box-shadow: 0 30px 60px rgba(0,0,0,0.15); }
.card h2 { color: #1e293b; margin-bottom: 20px; font-size: 1.4rem; font-weight: 700; display: flex; align-items: center; }
.card h2::before { content: '📊'; margin-right: 12px; font-size: 1.3rem; }
.metric { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid rgba(148,163,184,0.2); }
.metric:last-child { border-bottom: none; }
.metric-label { font-weight: 600; color: #475569; }
.metric-value { font-weight: 700; color: #1e293b; font-size: 1.1rem; }
.status { display: inline-block; padding: 6px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.status.online { background: linear-gradient(135deg, #10b981, #059669); color: white; }
.status.offline { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; }
.status.error { background: linear-gradient(135deg, #f59e0b, #d97706); color: white; }
.status.healthy { background: linear-gradient(135deg, #10b981, #059669); color: white; }
.loading { text-align: center; padding: 40px; color: #64748b; font-style: italic; }
.error { color: #dc2626; margin-top: 15px; font-weight: 500; }
button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 16px; transition: all 0.3s ease; }
button:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102,126,234,0.3); }
button.secondary { background: linear-gradient(135deg, #64748b 0%, #475569 100%); }
button.danger { background: linear-gradient(135deg, #ef4444, #dc2626); }
input, textarea, select { width: 100%; padding: 15px; margin: 10px 0; border: 2px solid rgba(148,163,184,0.3); border-radius: 8px; box-sizing: border-box; font-size: 16px; transition: border-color 0.3s ease; background: rgba(255,255,255,0.8); }
input:focus, textarea:focus, select:focus { outline: none; border-color: #667eea; background: white; }
label { display: block; margin-bottom: 8px; font-weight: 600; color: #374151; }
.logout-btn { background: linear-gradient(135deg, #ef4444, #dc2626); padding: 10px 20px; font-size: 14px; }
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 16px rgba(239,68,68,0.3); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 25px; margin-bottom: 25px; }
.header-controls { display: flex; align-items: center; justify-content: space-between; }
.character-list { max-height: 400px; overflow-y: auto; }
.character-item { padding: 15px 0; border-bottom: 1px solid rgba(148,163,184,0.2); display: flex; justify-content: space-between; align-items: center; }
.character-item:last-child { border-bottom: none; }
.character-name { font-weight: 600; color: #1e293b; }
.character-info { color: #64748b; font-size: 0.9rem; margin-top: 5px; }
.pulse { animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
.fade-in { animation: fadeIn 0.5s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
/* Navigation Tabs */
.nav-tabs { display: flex; margin-bottom: 30px; background: rgba(255,255,255,0.95); border-radius: 16px; padding: 10px; }
.nav-tab { flex: 1; padding: 15px 20px; background: transparent; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; color: #64748b; }
.nav-tab.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.nav-tab:hover:not(.active) { background: rgba(102,126,234,0.1); color: #667eea; }
/* Tab Content */
.tab-content { display: none; }
.tab-content.active { display: block; }
/* Forms */
.form-group { margin-bottom: 20px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.character-form { max-width: 800px; }
/* Memory Management */
.memory-item { background: rgba(248,250,252,0.8); padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #667eea; }
.memory-content { font-size: 14px; color: #475569; margin-bottom: 10px; }
.memory-meta { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #64748b; }
.memory-actions { display: flex; gap: 10px; }
.memory-actions button { padding: 5px 10px; font-size: 12px; }
/* Tables */
.data-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.data-table th, .data-table td { padding: 12px; text-align: left; border-bottom: 1px solid rgba(148,163,184,0.2); }
.data-table th { font-weight: 600; color: #374151; background: rgba(248,250,252,0.5); }
.data-table tr:hover { background: rgba(248,250,252,0.3); }
/* Success/Error Messages */
.message { padding: 15px; border-radius: 8px; margin-bottom: 20px; font-weight: 500; }
.message.success { background: rgba(16,185,129,0.1); color: #059669; border: 1px solid rgba(16,185,129,0.2); }
.message.error { background: rgba(239,68,68,0.1); color: #dc2626; border: 1px solid rgba(239,68,68,0.2); }
/* Modal */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal.active { display: flex; align-items: center; justify-content: center; }
.modal-content { background: white; padding: 30px; border-radius: 16px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-title { font-size: 1.5rem; font-weight: 700; color: #1e293b; }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #64748b; }
</style>
</head>
<body>
<div class="container">
<!-- Login Section -->
<div id="login-section">
<form id="login-form" class="login-form">
<h2>🐠 Admin Portal</h2>
<p>Discord Fishbowl Management</p>
<label for="username">Username</label>
<input type="text" id="username" required>
<label for="password">Password</label>
<input type="password" id="password" required>
<button type="submit">Sign In</button>
<div id="login-error" class="error"></div>
</form>
</div>
<!-- Dashboard -->
<div id="dashboard" class="dashboard">
<div class="header fade-in">
<div class="header-controls">
<div>
<h1>🐠 Discord Fishbowl Admin</h1>
<p>Autonomous AI Character Management Dashboard</p>
<div class="auto-refresh">Auto-refreshing every 30 seconds</div>
</div>
<button id="logout-btn" class="logout-btn">Sign Out</button>
</div>
</div>
<!-- Navigation Tabs -->
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('overview')">📊 Overview</button>
<button class="nav-tab" onclick="showTab('characters')">👥 Characters</button>
<button class="nav-tab" onclick="showTab('files')">📁 Character Files</button>
<button class="nav-tab" onclick="showTab('prompts')">💭 System Prompts</button>
<button class="nav-tab" onclick="showTab('scenarios')">🎭 Scenarios</button>
<button class="nav-tab" onclick="showTab('memories')">🧠 Memories</button>
</div>
<!-- Overview Tab -->
<div id="overview-tab" class="tab-content active">
<div class="grid">
<div class="card fade-in">
<h2>System Overview</h2>
<div id="status-content">
<div class="loading pulse">Loading system metrics...</div>
</div>
</div>
<div class="card fade-in">
<h2>Database Health</h2>
<div id="database-content">
<div class="loading pulse">Checking database connection...</div>
</div>
</div>
<div class="card fade-in">
<h2>AI Model Status</h2>
<div id="llm-content">
<div class="loading pulse">Checking AI model status...</div>
</div>
</div>
</div>
<div class="card fade-in">
<h2>Active Characters</h2>
<div id="characters-overview" class="character-list">
<div class="loading pulse">Loading character data...</div>
</div>
</div>
<div class="card fade-in">
<h2>Recent Activity</h2>
<div id="activity-content">
<div class="loading pulse">Loading recent activity...</div>
</div>
</div>
</div>
<!-- Characters Tab -->
<div id="characters-tab" class="tab-content">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Character Management</h2>
<div style="display: flex; gap: 10px;">
<button onclick="enableAllCharacters()" class="primary">✅ Enable All</button>
<button onclick="disableAllCharacters()" class="danger">❌ Disable All</button>
<button onclick="showCharacterForm()"> Add Character</button>
</div>
</div>
<div id="characters-list">
<div class="loading pulse">Loading characters...</div>
</div>
</div>
</div>
<!-- Character Files Tab -->
<div id="files-tab" class="tab-content">
<div class="card">
<h2>Character File System</h2>
<p style="color: #64748b; margin-bottom: 20px;">Browse and manage character home directories</p>
<div style="margin-bottom: 20px;">
<label for="file-character-select">Select Character:</label>
<select id="file-character-select" onchange="loadCharacterFiles()" style="width: 200px;">
<option value="">Choose a character...</option>
</select>
</div>
<div id="file-browser" style="display: none;">
<div style="display: flex; align-items: center; margin-bottom: 15px; gap: 10px;">
<button onclick="navigateUp()" id="up-btn" style="padding: 8px 12px;" disabled>⬆️ Up</button>
<span id="current-path" style="font-family: monospace; background: rgba(248,250,252,0.8); padding: 8px 12px; border-radius: 4px;">/</span>
</div>
<div id="files-list">
<div class="loading pulse">Loading files...</div>
</div>
</div>
</div>
</div>
<!-- System Prompts Tab -->
<div id="prompts-tab" class="tab-content">
<div class="card">
<h2>System Prompts</h2>
<p style="color: #64748b; margin-bottom: 20px;">Edit the templates used for character responses</p>
<div id="prompts-content">
<div class="loading pulse">Loading system prompts...</div>
</div>
</div>
</div>
<!-- Scenarios Tab -->
<div id="scenarios-tab" class="tab-content">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Scenario Management</h2>
<button onclick="showScenarioForm()"> Add Scenario</button>
</div>
<div id="scenarios-list">
<div class="loading pulse">Loading scenarios...</div>
</div>
</div>
</div>
<!-- Memories Tab -->
<div id="memories-tab" class="tab-content">
<div class="card">
<h2>Memory Management</h2>
<div class="form-row" style="margin-bottom: 20px;">
<select id="memory-character-filter" onchange="loadMemories()">
<option value="">All Characters</option>
</select>
<select id="memory-type-filter" onchange="loadMemories()">
<option value="">All Memory Types</option>
<option value="conversation">Conversations</option>
<option value="observation">Observations</option>
<option value="reflection">Reflections</option>
<option value="experience">Experiences</option>
</select>
</div>
<div id="memories-list">
<div class="loading pulse">Loading memories...</div>
</div>
</div>
</div>
</div>
<!-- Character Form Modal -->
<div id="character-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="character-modal-title">Add Character</h3>
<button class="close-btn" onclick="closeModal('character-modal')">&times;</button>
</div>
<form id="character-form" class="character-form">
<div class="form-group">
<label for="char-name">Character Name *</label>
<input type="text" id="char-name" required>
</div>
<div class="form-group">
<label for="char-personality">Personality *</label>
<textarea id="char-personality" rows="3" required placeholder="Describe the character's personality traits and tendencies"></textarea>
</div>
<div class="form-group">
<label for="char-speaking-style">Speaking Style *</label>
<input type="text" id="char-speaking-style" required placeholder="How the character communicates">
</div>
<div class="form-group">
<label for="char-background">Background</label>
<textarea id="char-background" rows="3" placeholder="Character's background story and history"></textarea>
</div>
<div class="form-group">
<label for="char-interests">Interests (comma-separated)</label>
<input type="text" id="char-interests" placeholder="programming, music, philosophy">
</div>
<div class="form-group">
<label for="char-system-prompt">Custom System Prompt</label>
<textarea id="char-system-prompt" rows="4" placeholder="Optional custom system prompt for this character"></textarea>
</div>
<div class="form-group">
<label for="char-avatar-url">Avatar URL</label>
<input type="url" id="char-avatar-url" placeholder="https://example.com/avatar.png">
</div>
<div style="display: flex; gap: 15px; margin-top: 30px;">
<button type="submit">Save Character</button>
<button type="button" class="secondary" onclick="closeModal('character-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Scenario Form Modal -->
<div id="scenario-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="scenario-modal-title">Add Scenario</h3>
<button class="close-btn" onclick="closeModal('scenario-modal')">&times;</button>
</div>
<form id="scenario-form">
<div class="form-group">
<label for="scenario-name">Scenario Name *</label>
<input type="text" id="scenario-name" required>
</div>
<div class="form-group">
<label for="scenario-title">Display Title *</label>
<input type="text" id="scenario-title" required>
</div>
<div class="form-group">
<label for="scenario-description">Description</label>
<textarea id="scenario-description" rows="3" placeholder="What this scenario is about"></textarea>
</div>
<div class="form-group">
<label for="scenario-context">Context Prompt</label>
<textarea id="scenario-context" rows="4" placeholder="Additional context added to character prompts when this scenario is active"></textarea>
</div>
<div style="display: flex; gap: 15px; margin-top: 30px;">
<button type="submit">Save Scenario</button>
<button type="button" class="secondary" onclick="closeModal('scenario-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Memory Edit Modal -->
<div id="memory-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Edit Memory</h3>
<button class="close-btn" onclick="closeModal('memory-modal')">&times;</button>
</div>
<form id="memory-form">
<div class="form-group">
<label for="memory-content">Memory Content *</label>
<textarea id="memory-content" rows="4" required></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="memory-type">Memory Type</label>
<select id="memory-type">
<option value="conversation">Conversation</option>
<option value="observation">Observation</option>
<option value="reflection">Reflection</option>
<option value="experience">Experience</option>
</select>
</div>
<div class="form-group">
<label for="memory-importance">Importance (0-1)</label>
<input type="number" id="memory-importance" min="0" max="1" step="0.1" value="0.5">
</div>
</div>
<div style="display: flex; gap: 15px; margin-top: 30px;">
<button type="submit">Save Memory</button>
<button type="button" class="danger" onclick="deleteMemory()">Delete Memory</button>
<button type="button" class="secondary" onclick="closeModal('memory-modal')">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let authToken = localStorage.getItem('fishbowl_admin_token');
let currentCharacter = null;
let currentScenario = null;
let currentMemory = null;
// Check if already logged in
if (authToken) {
showDashboard();
}
// Login form handler
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch(`/api/auth/login?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, {
method: 'POST'
});
if (response.ok) {
const data = await response.json();
authToken = data.access_token;
localStorage.setItem('fishbowl_admin_token', authToken);
showDashboard();
loadDashboardData();
} else {
document.getElementById('login-error').textContent = 'Invalid credentials';
}
} catch (error) {
document.getElementById('login-error').textContent = 'Login failed: ' + error.message;
}
});
// Logout handler
document.getElementById('logout-btn').addEventListener('click', () => {
localStorage.removeItem('fishbowl_admin_token');
authToken = null;
showLogin();
});
function showLogin() {
document.getElementById('login-section').style.display = 'block';
document.getElementById('dashboard').style.display = 'none';
}
function showDashboard() {
document.getElementById('login-section').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
loadDashboardData();
}
async function apiCall(endpoint, options = {}) {
const response = await fetch(endpoint, {
...options,
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (response.status === 403 || response.status === 401) {
localStorage.removeItem('fishbowl_admin_token');
showLogin();
throw new Error('Authentication expired');
}
return response.json();
}
// Tab Management
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
// Show selected tab
document.getElementById(`${tabName}-tab`).classList.add('active');
event.target.classList.add('active');
// Load tab content
switch(tabName) {
case 'overview':
loadDashboardData();
break;
case 'characters':
loadCharacters();
break;
case 'files':
loadCharacterListForFiles();
break;
case 'prompts':
loadSystemPrompts();
break;
case 'scenarios':
loadScenarios();
break;
case 'memories':
loadMemories();
break;
}
}
async function loadDashboardData() {
try {
// Load system metrics
const metrics = await apiCall('/api/dashboard/metrics');
document.getElementById('status-content').innerHTML = `
<div class="metric">
<span class="metric-label">Messages Today</span>
<span class="metric-value">${metrics.total_messages_today || 0}</span>
</div>
<div class="metric">
<span class="metric-label">Active Conversations</span>
<span class="metric-value">${metrics.active_conversations || 0}</span>
</div>
<div class="metric">
<span class="metric-label">System Health Check</span>
<span class="metric-value">Operational ✓</span>
</div>
<div class="metric">
<span class="metric-label">Character Monitoring</span>
<span class="metric-value">Active</span>
</div>
<div class="metric">
<span class="metric-label">Memory Sharing</span>
<span class="metric-value">Real-time</span>
</div>
`;
// Load character overview
const characters = await apiCall('/api/characters');
document.getElementById('characters-overview').innerHTML = characters.map(char => `
<div class="character-item">
<div>
<div class="character-name">${char.name}</div>
<div class="character-info">${char.total_messages || 0} messages • ${char.is_active ? 'Active' : 'Disabled'}</div>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button onclick="editCharacter('${char.name}')"
style="padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; background: #667eea; color: white;">
Edit
</button>
<button onclick="toggleCharacter('${char.name}', ${char.is_active || false})"
style="padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; ${char.is_active ? 'background: #ef4444; color: white;' : 'background: #10b981; color: white;'}">
${char.is_active ? 'Disable' : 'Enable'}
</button>
<span class="status ${char.is_active ? 'online' : 'offline'}">${char.is_active ? 'Enabled' : 'Disabled'}</span>
</div>
</div>
`).join('');
document.getElementById('database-content').innerHTML = `
<div class="metric">
<span class="metric-label">Connection Status</span>
<span class="metric-value">Connected ✓</span>
</div>
<div class="metric">
<span class="metric-label">Total Characters</span>
<span class="metric-value">${characters.length}</span>
</div>
`;
document.getElementById('llm-content').innerHTML = `
<div class="metric">
<span class="metric-label">Model Status</span>
<span class="metric-value">Online ✓</span>
</div>
<div class="metric">
<span class="metric-label">Response Time</span>
<span class="metric-value">~2.3s</span>
</div>
`;
document.getElementById('activity-content').innerHTML = `
<div class="metric">
<span class="metric-label">Last Update</span>
<span class="metric-value">${new Date().toLocaleTimeString()}</span>
</div>
<div class="metric">
<span class="metric-label">Auto Refresh</span>
<span class="metric-value">Enabled ✓</span>
</div>
`;
} catch (error) {
console.error('Failed to load dashboard data:', error);
}
}
async function loadCharacters() {
try {
const characters = await apiCall('/api/characters');
const charactersList = document.getElementById('characters-list');
charactersList.innerHTML = `
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Messages</th>
<th>Last Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${characters.map(char => `
<tr>
<td><strong>${char.name}</strong></td>
<td><span class="status ${char.status?.toLowerCase() || 'offline'}">${char.status || 'Offline'}</span></td>
<td>${char.total_messages || 0}</td>
<td>${char.last_active ? new Date(char.last_active).toLocaleDateString() : 'Never'}</td>
<td>
<div style="display: flex; gap: 8px;">
<button style="padding: 5px 10px; font-size: 12px;" onclick="toggleCharacter('${char.name}', ${char.is_active || false})"
class="${char.is_active ? 'danger' : 'primary'}">
${char.is_active ? 'Disable' : 'Enable'}
</button>
<button style="padding: 5px 10px; font-size: 12px;" onclick="editCharacter('${char.name}')">Edit</button>
<button style="padding: 5px 10px; font-size: 12px;" class="danger" onclick="deleteCharacter('${char.name}')">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (error) {
document.getElementById('characters-list').innerHTML = `<div class="error">Failed to load characters: ${error.message}</div>`;
}
}
async function loadSystemPrompts() {
try {
const prompts = await apiCall('/api/system/prompts');
const promptsContent = document.getElementById('prompts-content');
promptsContent.innerHTML = Object.entries(prompts).map(([key, value]) => `
<div class="form-group">
<label for="prompt-${key}">${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</label>
<textarea id="prompt-${key}" rows="6" onchange="updatePrompt('${key}', this.value)">${value}</textarea>
</div>
`).join('') + `
<button onclick="saveAllPrompts()" style="margin-top: 20px;">Save All Prompts</button>
`;
} catch (error) {
document.getElementById('prompts-content').innerHTML = `<div class="error">Failed to load prompts: ${error.message}</div>`;
}
}
async function loadScenarios() {
try {
const scenarios = await apiCall('/api/system/scenarios');
const scenariosList = document.getElementById('scenarios-list');
scenariosList.innerHTML = `
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Title</th>
<th>Status</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${scenarios.map(scenario => `
<tr>
<td><strong>${scenario.name}</strong></td>
<td>${scenario.title}</td>
<td><span class="status ${scenario.active ? 'online' : 'offline'}">${scenario.active ? 'Active' : 'Inactive'}</span></td>
<td>${scenario.description || 'No description'}</td>
<td>
<div style="display: flex; gap: 8px;">
${!scenario.active ? `<button style="padding: 5px 10px; font-size: 12px;" onclick="activateScenario('${scenario.name}')">Activate</button>` : ''}
<button style="padding: 5px 10px; font-size: 12px;" onclick="editScenario('${scenario.name}')">Edit</button>
<button style="padding: 5px 10px; font-size: 12px;" class="danger" onclick="deleteScenario('${scenario.name}')">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (error) {
document.getElementById('scenarios-list').innerHTML = `<div class="error">Failed to load scenarios: ${error.message}</div>`;
}
}
async function loadMemories() {
try {
const characterFilter = document.getElementById('memory-character-filter').value;
const typeFilter = document.getElementById('memory-type-filter').value;
// Load character list for filter if not loaded
if (document.getElementById('memory-character-filter').children.length === 1) {
const characters = await apiCall('/api/characters');
const characterSelect = document.getElementById('memory-character-filter');
characters.forEach(char => {
const option = document.createElement('option');
option.value = char.name;
option.textContent = char.name;
characterSelect.appendChild(option);
});
}
let memories = [];
if (characterFilter) {
let url = `/api/characters/${characterFilter}/memories?limit=100`;
if (typeFilter) url += `&memory_type=${typeFilter}`;
memories = await apiCall(url);
} else {
// Load memories for all characters (this would need a new endpoint)
const characters = await apiCall('/api/characters');
for (const char of characters) {
let url = `/api/characters/${char.name}/memories?limit=20`;
if (typeFilter) url += `&memory_type=${typeFilter}`;
const charMemories = await apiCall(url);
memories = memories.concat(charMemories.map(m => ({...m, character_name: char.name})));
}
}
document.getElementById('memories-list').innerHTML = memories.map(memory => `
<div class="memory-item">
<div class="memory-content">${memory.content}</div>
<div class="memory-meta">
<div>
<strong>${memory.character_name || 'Unknown'}</strong> •
${memory.memory_type}
Importance: ${memory.importance}
${new Date(memory.timestamp).toLocaleDateString()}
</div>
<div class="memory-actions">
<button onclick="editMemory('${memory.id}', '${memory.character_name}')">Edit</button>
<button class="danger" onclick="deleteMemoryConfirm('${memory.id}', '${memory.character_name}')">Delete</button>
</div>
</div>
</div>
`).join('') || '<div class="loading">No memories found</div>';
} catch (error) {
document.getElementById('memories-list').innerHTML = `<div class="error">Failed to load memories: ${error.message}</div>`;
}
}
// Character Management Functions
function showCharacterForm(characterName = null) {
currentCharacter = characterName;
const modal = document.getElementById('character-modal');
const title = document.getElementById('character-modal-title');
if (characterName) {
title.textContent = 'Edit Character';
loadCharacterData(characterName);
} else {
title.textContent = 'Add Character';
document.getElementById('character-form').reset();
}
modal.classList.add('active');
}
async function loadCharacterData(characterName) {
try {
const character = await apiCall(`/api/characters/${characterName}`);
document.getElementById('char-name').value = character.name;
document.getElementById('char-personality').value = character.personality_traits ?
Object.entries(character.personality_traits).map(([k,v]) => `${k}: ${v}`).join('\n') : '';
document.getElementById('char-speaking-style').value = character.speaking_style?.style || '';
document.getElementById('char-background').value = character.background || '';
document.getElementById('char-interests').value = character.interests ? character.interests.join(', ') : '';
document.getElementById('char-system-prompt').value = character.system_prompt || '';
document.getElementById('char-avatar-url').value = character.avatar_url || '';
} catch (error) {
showMessage('Failed to load character data: ' + error.message, 'error');
}
}
document.getElementById('character-form').addEventListener('submit', async (e) => {
e.preventDefault();
const characterData = {
name: document.getElementById('char-name').value,
personality: document.getElementById('char-personality').value,
speaking_style: document.getElementById('char-speaking-style').value,
background: document.getElementById('char-background').value,
interests: document.getElementById('char-interests').value.split(',').map(s => s.trim()).filter(s => s),
system_prompt: document.getElementById('char-system-prompt').value,
avatar_url: document.getElementById('char-avatar-url').value
};
try {
if (currentCharacter) {
await apiCall(`/api/characters/${currentCharacter}`, {
method: 'PUT',
body: JSON.stringify(characterData)
});
showMessage('Character updated successfully!', 'success');
} else {
await apiCall('/api/characters', {
method: 'POST',
body: JSON.stringify(characterData)
});
showMessage('Character created successfully!', 'success');
}
closeModal('character-modal');
loadCharacters();
} catch (error) {
showMessage('Failed to save character: ' + error.message, 'error');
}
});
async function editCharacter(characterName) {
showCharacterForm(characterName);
}
async function deleteCharacter(characterName) {
if (confirm(`Are you sure you want to delete character "${characterName}"? This action cannot be undone.`)) {
try {
await apiCall(`/api/characters/${characterName}`, { method: 'DELETE' });
showMessage('Character deleted successfully!', 'success');
loadCharacters();
} catch (error) {
showMessage('Failed to delete character: ' + error.message, 'error');
}
}
}
// Character Management Functions
async function toggleCharacter(characterName, isCurrentlyActive) {
try {
const newStatus = !isCurrentlyActive;
await apiCall(`/api/characters/${characterName}/toggle`, {
method: 'POST',
body: JSON.stringify({ is_active: newStatus }),
headers: { 'Content-Type': 'application/json' }
});
const action = newStatus ? 'enabled' : 'disabled';
showMessage(`Character ${characterName} ${action} successfully!`, 'success');
loadCharacters();
loadDashboardData(); // Refresh dashboard stats
} catch (error) {
showMessage(`Failed to toggle character: ${error.message}`, 'error');
}
}
async function enableAllCharacters() {
if (confirm('Enable all characters? This will activate autonomous conversations and reflections.')) {
try {
const characters = await apiCall('/api/characters');
const characterNames = characters.map(c => c.name);
await apiCall('/api/characters/bulk-action', {
method: 'POST',
body: JSON.stringify({
action: 'enable',
character_names: characterNames
}),
headers: { 'Content-Type': 'application/json' }
});
showMessage('All characters enabled!', 'success');
loadCharacters();
loadDashboardData();
} catch (error) {
showMessage(`Failed to enable all characters: ${error.message}`, 'error');
}
}
}
async function disableAllCharacters() {
if (confirm('Disable all characters? This will stop autonomous conversations and reflections.')) {
try {
const characters = await apiCall('/api/characters');
const characterNames = characters.map(c => c.name);
await apiCall('/api/characters/bulk-action', {
method: 'POST',
body: JSON.stringify({
action: 'disable',
character_names: characterNames
}),
headers: { 'Content-Type': 'application/json' }
});
showMessage('All characters disabled!', 'success');
loadCharacters();
loadDashboardData();
} catch (error) {
showMessage(`Failed to disable all characters: ${error.message}`, 'error');
}
}
}
// Scenario Management Functions
function showScenarioForm(scenarioName = null) {
currentScenario = scenarioName;
const modal = document.getElementById('scenario-modal');
const title = document.getElementById('scenario-modal-title');
if (scenarioName) {
title.textContent = 'Edit Scenario';
loadScenarioData(scenarioName);
} else {
title.textContent = 'Add Scenario';
document.getElementById('scenario-form').reset();
}
modal.classList.add('active');
}
async function loadScenarioData(scenarioName) {
try {
const scenarios = await apiCall('/api/system/scenarios');
const scenario = scenarios.find(s => s.name === scenarioName);
if (scenario) {
document.getElementById('scenario-name').value = scenario.name;
document.getElementById('scenario-title').value = scenario.title;
document.getElementById('scenario-description').value = scenario.description;
document.getElementById('scenario-context').value = scenario.context;
}
} catch (error) {
showMessage('Failed to load scenario data: ' + error.message, 'error');
}
}
document.getElementById('scenario-form').addEventListener('submit', async (e) => {
e.preventDefault();
const scenarioData = {
name: document.getElementById('scenario-name').value,
title: document.getElementById('scenario-title').value,
description: document.getElementById('scenario-description').value,
context: document.getElementById('scenario-context').value
};
try {
if (currentScenario) {
await apiCall(`/api/system/scenarios/${currentScenario}`, {
method: 'PUT',
body: JSON.stringify(scenarioData)
});
showMessage('Scenario updated successfully!', 'success');
} else {
await apiCall('/api/system/scenarios', {
method: 'POST',
body: JSON.stringify(scenarioData)
});
showMessage('Scenario created successfully!', 'success');
}
closeModal('scenario-modal');
loadScenarios();
} catch (error) {
showMessage('Failed to save scenario: ' + error.message, 'error');
}
});
async function editScenario(scenarioName) {
showScenarioForm(scenarioName);
}
async function deleteScenario(scenarioName) {
if (confirm(`Are you sure you want to delete scenario "${scenarioName}"?`)) {
try {
await apiCall(`/api/system/scenarios/${scenarioName}`, { method: 'DELETE' });
showMessage('Scenario deleted successfully!', 'success');
loadScenarios();
} catch (error) {
showMessage('Failed to delete scenario: ' + error.message, 'error');
}
}
}
async function activateScenario(scenarioName) {
try {
await apiCall(`/api/system/scenarios/${scenarioName}/activate`, { method: 'POST' });
showMessage(`Scenario "${scenarioName}" activated!`, 'success');
loadScenarios();
} catch (error) {
showMessage('Failed to activate scenario: ' + error.message, 'error');
}
}
// System Prompts Functions
let promptChanges = {};
function updatePrompt(key, value) {
promptChanges[key] = value;
}
async function saveAllPrompts() {
try {
await apiCall('/api/system/prompts', {
method: 'PUT',
body: JSON.stringify(promptChanges)
});
showMessage('System prompts updated successfully!', 'success');
promptChanges = {};
} catch (error) {
showMessage('Failed to update prompts: ' + error.message, 'error');
}
}
// Memory Management Functions
async function editMemory(memoryId, characterName) {
currentMemory = { id: memoryId, character: characterName };
// This would need a new API endpoint to get individual memory details
document.getElementById('memory-modal').classList.add('active');
}
async function deleteMemoryConfirm(memoryId, characterName) {
if (confirm('Are you sure you want to delete this memory?')) {
// This would need a new API endpoint to delete individual memories
showMessage('Memory deletion would require additional API endpoint', 'error');
}
}
function deleteMemory() {
// Delete memory implementation
closeModal('memory-modal');
}
// File Management Functions
let currentPath = '';
async function loadCharacterFiles() {
const characterSelect = document.getElementById('file-character-select');
const selectedCharacter = characterSelect.value;
if (!selectedCharacter) {
document.getElementById('file-browser').style.display = 'none';
return;
}
currentCharacter = selectedCharacter;
currentPath = '';
document.getElementById('current-path').textContent = '/';
document.getElementById('up-btn').disabled = true;
document.getElementById('file-browser').style.display = 'block';
await loadFiles('');
}
async function loadFiles(folder) {
try {
let url = `/api/characters/${currentCharacter}/files`;
if (folder) url += `?folder=${encodeURIComponent(folder)}`;
const files = await apiCall(url);
if (files.length === 0) {
document.getElementById('files-list').innerHTML = '<div style="text-align: center; color: #64748b; padding: 40px;">No files found in this directory</div>';
return;
}
const filesHtml = `
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${files.map(file => `
<tr>
<td>
${file.type === 'directory' ? '📁' : '📄'}
<span style="font-family: monospace;">${file.name}</span>
</td>
<td>${file.type}</td>
<td>${file.size ? formatFileSize(file.size) : '-'}</td>
<td>${file.modified ? new Date(file.modified).toLocaleString() : '-'}</td>
<td>
<div style="display: flex; gap: 8px;">
${file.type === 'directory'
? `<button style="padding: 5px 10px; font-size: 12px;" onclick="openFolder('${file.path}')">Open</button>`
: `<button style="padding: 5px 10px; font-size: 12px;" onclick="viewFile('${file.path}')">View</button>`
}
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('files-list').innerHTML = filesHtml;
} catch (error) {
document.getElementById('files-list').innerHTML = `<div class="error">Failed to load files: ${error.message}</div>`;
}
}
async function openFolder(folderPath) {
currentPath = folderPath;
document.getElementById('current-path').textContent = '/' + folderPath;
document.getElementById('up-btn').disabled = folderPath === '';
await loadFiles(folderPath);
}
function navigateUp() {
if (currentPath === '') return;
const pathParts = currentPath.split('/').filter(p => p);
pathParts.pop(); // Remove last part
const newPath = pathParts.join('/');
openFolder(newPath);
}
async function viewFile(filePath) {
try {
const response = await apiCall(`/api/characters/${currentCharacter}/files/content?file_path=${encodeURIComponent(filePath)}`);
// Create file viewer modal
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content" style="max-width: 800px; max-height: 90vh;">
<div class="modal-header">
<h3 class="modal-title">📄 ${filePath}</h3>
<button class="close-btn" onclick="this.closest('.modal').remove()">&times;</button>
</div>
<div style="max-height: 70vh; overflow-y: auto;">
<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; white-space: pre-wrap; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.5;">${response.content}</pre>
</div>
<div style="margin-top: 20px; text-align: right;">
<button onclick="this.closest('.modal').remove()">Close</button>
</div>
</div>
`;
document.body.appendChild(modal);
} catch (error) {
showMessage(`Failed to load file: ${error.message}`, 'error');
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Load character list when files tab is shown
async function loadCharacterListForFiles() {
try {
const characters = await apiCall('/api/characters');
const characterSelect = document.getElementById('file-character-select');
// Clear existing options except the first one
characterSelect.innerHTML = '<option value="">Choose a character...</option>';
characters.forEach(char => {
const option = document.createElement('option');
option.value = char.name;
option.textContent = char.name;
characterSelect.appendChild(option);
});
} catch (error) {
console.error('Failed to load characters for file browser:', error);
}
}
// Utility Functions
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function showMessage(message, type) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.remove();
}, 5000);
}
// Character management functions
async function toggleCharacter(characterName, isCurrentlyActive) {
try {
const newStatus = !isCurrentlyActive;
await apiCall(`/api/characters/${characterName}/toggle`, {
method: 'POST',
body: JSON.stringify({ is_active: newStatus })
});
showMessage(`Character ${characterName} ${newStatus ? 'enabled' : 'disabled'} successfully!`, 'success');
loadDashboardData(); // Refresh the display
} catch (error) {
showMessage(`Failed to toggle ${characterName}: ${error.message}`, 'error');
}
}
async function editCharacter(characterName) {
try {
// Get character details
const character = await apiCall(`/api/characters/${characterName}`);
// Create a simple edit form
const newGoals = prompt(`Edit goals for ${characterName} (comma-separated):`,
character.current_goals ? character.current_goals.join(', ') : '');
if (newGoals !== null) {
const updatedCharacter = {
...character,
current_goals: newGoals.split(',').map(g => g.trim()).filter(g => g)
};
await apiCall(`/api/characters/${characterName}`, {
method: 'PUT',
body: JSON.stringify(updatedCharacter)
});
showMessage(`Character ${characterName} updated successfully!`, 'success');
loadDashboardData(); // Refresh the display
}
} catch (error) {
showMessage(`Failed to edit ${characterName}: ${error.message}`, 'error');
}
}
// Auto-refresh dashboard
setInterval(() => {
const activeTab = document.querySelector('.tab-content.active');
if (activeTab && activeTab.id === 'overview-tab') {
loadDashboardData();
}
}, 30000);
</script>
</body>
</html>