- 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
1225 lines
62 KiB
HTML
1225 lines
62 KiB
HTML
<!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')">×</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')">×</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')">×</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()">×</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> |