| """ |
| Comprehensive Admin Dashboard Template |
| Multi-page admin dashboard with navigation |
| """ |
| ADMIN_DASHBOARD_TEMPLATE_STR = ''' |
| <!DOCTYPE html> |
| <html lang="en" dir="ltr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Admin Dashboard - Itma'In</title> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: #f5f5f5; |
| color: #1a1a2e; |
| display: flex; |
| min-height: 100vh; |
| } |
| .sidebar { |
| width: 260px; |
| background: linear-gradient(180deg, #FF6B9D 0%, #FFB3D1 100%); |
| color: white; |
| padding: 24px 0; |
| box-shadow: 2px 0 8px rgba(0,0,0,0.1); |
| position: fixed; |
| height: 100vh; |
| overflow-y: auto; |
| } |
| .sidebar-header { |
| padding: 0 24px 24px; |
| border-bottom: 1px solid rgba(255,255,255,0.2); |
| margin-bottom: 24px; |
| } |
| .sidebar-header h1 { |
| font-size: 20px; |
| font-weight: 600; |
| margin-bottom: 4px; |
| } |
| .sidebar-header p { |
| font-size: 12px; |
| opacity: 0.8; |
| } |
| .nav-menu { |
| list-style: none; |
| } |
| .nav-item { |
| margin: 4px 12px; |
| } |
| .nav-link { |
| display: flex; |
| align-items: center; |
| padding: 12px 16px; |
| color: white; |
| text-decoration: none; |
| border-radius: 8px; |
| transition: all 0.2s; |
| cursor: pointer; |
| } |
| .nav-link:hover { |
| background: rgba(255,255,255,0.15); |
| } |
| .nav-link.active { |
| background: rgba(255,255,255,0.25); |
| font-weight: 600; |
| } |
| .nav-link .icon { |
| margin-right: 12px; |
| font-size: 18px; |
| width: 24px; |
| text-align: center; |
| } |
| .main-content { |
| flex: 1; |
| margin-left: 260px; |
| min-height: 100vh; |
| } |
| .top-bar { |
| background: white; |
| padding: 16px 24px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| position: sticky; |
| top: 0; |
| z-index: 100; |
| } |
| .page-title { |
| font-size: 24px; |
| font-weight: 600; |
| color: #1a1a2e; |
| } |
| .top-actions { |
| display: flex; |
| gap: 12px; |
| align-items: center; |
| } |
| .btn { |
| padding: 8px 16px; |
| border: none; |
| border-radius: 6px; |
| cursor: pointer; |
| font-size: 14px; |
| transition: all 0.2s; |
| } |
| .btn-primary { |
| background: #FF6B9D; |
| color: white; |
| } |
| .btn-primary:hover { |
| background: #FF4D7D; |
| } |
| .btn-secondary { |
| background: #f0f0f0; |
| color: #1a1a2e; |
| } |
| .btn-secondary:hover { |
| background: #e0e0e0; |
| } |
| .content-area { |
| padding: 24px; |
| } |
| .page { |
| display: none; |
| } |
| .page.active { |
| display: block; |
| } |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 20px; |
| margin-bottom: 24px; |
| } |
| .stat-card { |
| background: white; |
| padding: 24px; |
| border-radius: 12px; |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| border-left: 4px solid #FF6B9D; |
| } |
| .stat-value { |
| font-size: 36px; |
| font-weight: 700; |
| color: #FF6B9D; |
| margin-bottom: 8px; |
| } |
| .stat-label { |
| color: #666; |
| font-size: 14px; |
| font-weight: 500; |
| } |
| .card { |
| background: white; |
| padding: 24px; |
| border-radius: 12px; |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| margin-bottom: 24px; |
| } |
| .card-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 20px; |
| padding-bottom: 16px; |
| border-bottom: 2px solid #f0f0f0; |
| } |
| .card-title { |
| font-size: 20px; |
| font-weight: 600; |
| color: #1a1a2e; |
| } |
| .table-container { |
| overflow-x: auto; |
| } |
| .data-table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| .data-table th { |
| background: #f8f8f8; |
| padding: 12px; |
| text-align: left; |
| font-weight: 600; |
| color: #1a1a2e; |
| border-bottom: 2px solid #e0e0e0; |
| } |
| .data-table td { |
| padding: 12px; |
| border-bottom: 1px solid #f0f0f0; |
| } |
| .data-table tr:hover { |
| background: #f8f8f8; |
| } |
| .badge { |
| display: inline-block; |
| padding: 4px 12px; |
| border-radius: 12px; |
| font-size: 12px; |
| font-weight: 600; |
| } |
| .badge-success { |
| background: #d4edda; |
| color: #155724; |
| } |
| .badge-danger { |
| background: #f8d7da; |
| color: #721c24; |
| } |
| .badge-info { |
| background: #d1ecf1; |
| color: #0c5460; |
| } |
| .badge-warning { |
| background: #fff3cd; |
| color: #856404; |
| } |
| .chart-container { |
| position: relative; |
| height: 300px; |
| margin-top: 20px; |
| } |
| .filter-bar { |
| display: flex; |
| gap: 12px; |
| margin-bottom: 20px; |
| flex-wrap: wrap; |
| } |
| .filter-select { |
| padding: 8px 12px; |
| border: 1px solid #ddd; |
| border-radius: 6px; |
| font-size: 14px; |
| min-width: 150px; |
| } |
| .search-box { |
| flex: 1; |
| padding: 8px 12px; |
| border: 1px solid #ddd; |
| border-radius: 6px; |
| font-size: 14px; |
| min-width: 200px; |
| } |
| .audio-player { |
| width: 200px; |
| height: 32px; |
| } |
| .loading { |
| text-align: center; |
| padding: 40px; |
| color: #999; |
| } |
| .empty-state { |
| text-align: center; |
| padding: 60px 20px; |
| color: #999; |
| } |
| .empty-state-icon { |
| font-size: 48px; |
| margin-bottom: 16px; |
| opacity: 0.5; |
| } |
| /* Sidebar always on left, no RTL support needed */ |
| </style> |
| </head> |
| <body> |
| <!-- Sidebar Navigation --> |
| <nav class="sidebar"> |
| <div class="sidebar-header"> |
| <h1>👶💗 Itma'In</h1> |
| <p>Admin Dashboard</p> |
| </div> |
| <ul class="nav-menu"> |
| <li class="nav-item"> |
| <a class="nav-link active" onclick="showPage('dashboard')"> |
| <span class="icon">📊</span> |
| <span>Dashboard</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" onclick="showPage('users')"> |
| <span class="icon">👥</span> |
| <span>Users Management</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" onclick="showPage('predictions')"> |
| <span class="icon">🎤</span> |
| <span>Predictions</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" onclick="showPage('audios-feedback')"> |
| <span class="icon">🎵</span> |
| <span>Audios with Feedback</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" onclick="showPage('positive-feedback')"> |
| <span class="icon">✅</span> |
| <span>Positive Feedback</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" onclick="showPage('negative-feedback')"> |
| <span class="icon">❌</span> |
| <span>Negative Feedback</span> |
| </a> |
| </li> |
| </ul> |
| </nav> |
| |
| <!-- Main Content --> |
| <div class="main-content"> |
| <div class="top-bar"> |
| <h2 class="page-title" id="pageTitle">Dashboard</h2> |
| <div class="top-actions"> |
| <button class="btn btn-secondary" onclick="refreshCurrentPage()">🔄 Refresh</button> |
| <button class="btn btn-secondary" onclick="logout()">Logout</button> |
| </div> |
| </div> |
| |
| <div class="content-area"> |
| <!-- Dashboard Page --> |
| <div id="page-dashboard" class="page active"> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-value" id="statTotalUsers">-</div> |
| <div class="stat-label">Total Users</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statActiveUsers">-</div> |
| <div class="stat-label">Active (7 days)</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statTotalRecordings">-</div> |
| <div class="stat-label">Total Recordings</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statTotalFeedback">-</div> |
| <div class="stat-label">Total Feedback</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statPositiveFeedback">-</div> |
| <div class="stat-label">Positive Feedback</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statNegativeFeedback">-</div> |
| <div class="stat-label">Negative Feedback</div> |
| </div> |
| </div> |
| |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Platform Distribution</h3> |
| </div> |
| <div class="chart-container"> |
| <canvas id="chartPlatform"></canvas> |
| </div> |
| </div> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Language Distribution</h3> |
| </div> |
| <div class="chart-container"> |
| <canvas id="chartLanguage"></canvas> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Feedback by Category</h3> |
| </div> |
| <div class="chart-container"> |
| <canvas id="chartFeedbackCategory"></canvas> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Users Management Page --> |
| <div id="page-users" class="page"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Users Management</h3> |
| <div class="filter-bar"> |
| <input type="text" class="search-box" id="userSearch" placeholder="Search users..." onkeyup="filterUsers()"> |
| <select class="filter-select" id="userPlatformFilter" onchange="loadUsers()"> |
| <option value="">All Platforms</option> |
| <option value="web">Web</option> |
| <option value="android">Android</option> |
| </select> |
| <select class="filter-select" id="userLanguageFilter" onchange="loadUsers()"> |
| <option value="">All Languages</option> |
| <option value="ar">Arabic</option> |
| <option value="fr">French</option> |
| <option value="en">English</option> |
| </select> |
| </div> |
| </div> |
| <div class="table-container"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>ID</th> |
| <th>Baby Name</th> |
| <th>Email/Phone</th> |
| <th>Platform</th> |
| <th>Language</th> |
| <th>Registered</th> |
| <th>Actions</th> |
| </tr> |
| </thead> |
| <tbody id="usersTableBody"> |
| <tr><td colspan="7" class="loading">Loading users...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Predictions Page --> |
| <div id="page-predictions" class="page"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">All Predictions</h3> |
| <div class="filter-bar"> |
| <input type="text" class="search-box" id="predictionSearch" placeholder="Search predictions..." onkeyup="filterPredictions()"> |
| <select class="filter-select" id="predictionCategoryFilter" onchange="loadPredictions()"> |
| <option value="">All Categories</option> |
| <option value="hunger">Hunger</option> |
| <option value="sleep">Sleep</option> |
| <option value="pain">Pain</option> |
| <option value="discomfort">Discomfort</option> |
| <option value="fussiness">Fussiness</option> |
| </select> |
| </div> |
| </div> |
| <div class="table-container"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>User ID</th> |
| <th>Prediction</th> |
| <th>Confidence</th> |
| <th>Model Type</th> |
| </tr> |
| </thead> |
| <tbody id="predictionsTableBody"> |
| <tr><td colspan="5" class="loading">Loading predictions...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Audios with Feedback Page --> |
| <div id="page-audios-feedback" class="page"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Audios with Feedback</h3> |
| <div class="filter-bar"> |
| <select class="filter-select" id="audioFeedbackCategoryFilter" onchange="loadAudiosWithFeedback()"> |
| <option value="">All Categories</option> |
| <option value="hunger">Hunger</option> |
| <option value="sleep">Sleep</option> |
| <option value="pain">Pain</option> |
| <option value="discomfort">Discomfort</option> |
| <option value="fussiness">Fussiness</option> |
| </select> |
| </div> |
| </div> |
| <div class="table-container"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>User ID</th> |
| <th>Predicted</th> |
| <th>Correct</th> |
| <th>Status</th> |
| <th>Confidence</th> |
| <th>Audio</th> |
| </tr> |
| </thead> |
| <tbody id="audiosFeedbackTableBody"> |
| <tr><td colspan="7" class="loading">Loading audios with feedback...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Positive Feedback Page --> |
| <div id="page-positive-feedback" class="page"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Positive Feedback</h3> |
| <div class="filter-bar"> |
| <select class="filter-select" id="positiveCategoryFilter" onchange="loadPositiveFeedback()"> |
| <option value="">All Categories</option> |
| <option value="hunger">Hunger</option> |
| <option value="sleep">Sleep</option> |
| <option value="pain">Pain</option> |
| <option value="discomfort">Discomfort</option> |
| <option value="fussiness">Fussiness</option> |
| </select> |
| </div> |
| </div> |
| <div class="table-container"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>User ID</th> |
| <th>Prediction</th> |
| <th>Confidence</th> |
| <th>Audio</th> |
| </tr> |
| </thead> |
| <tbody id="positiveFeedbackTableBody"> |
| <tr><td colspan="5" class="loading">Loading positive feedback...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <!-- Negative Feedback Page --> |
| <div id="page-negative-feedback" class="page"> |
| <div class="card"> |
| <div class="card-header"> |
| <h3 class="card-title">Negative Feedback</h3> |
| <div class="filter-bar"> |
| <select class="filter-select" id="negativeCategoryFilter" onchange="loadNegativeFeedback()"> |
| <option value="">All Categories</option> |
| <option value="hunger">Hunger</option> |
| <option value="sleep">Sleep</option> |
| <option value="pain">Pain</option> |
| <option value="discomfort">Discomfort</option> |
| <option value="fussiness">Fussiness</option> |
| </select> |
| </div> |
| </div> |
| <div class="table-container"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>User ID</th> |
| <th>Predicted</th> |
| <th>Correct</th> |
| <th>Confidence</th> |
| <th>Audio</th> |
| </tr> |
| </thead> |
| <tbody id="negativeFeedbackTableBody"> |
| <tr><td colspan="6" class="loading">Loading negative feedback...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| const translations = {{TRANSLATIONS}}; |
| const charts = {}; |
| let currentPage = 'dashboard'; |
| |
| function t(key, defaultValue) { |
| return translations[key] || defaultValue || key; |
| } |
| |
| function showPage(pageId) { |
| // Hide all pages |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); |
| document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); |
| |
| // Show selected page |
| document.getElementById('page-' + pageId).classList.add('active'); |
| event.target.closest('.nav-link').classList.add('active'); |
| |
| // Update page title |
| const titles = { |
| 'dashboard': 'Dashboard', |
| 'users': 'Users Management', |
| 'predictions': 'Predictions', |
| 'audios-feedback': 'Audios with Feedback', |
| 'positive-feedback': 'Positive Feedback', |
| 'negative-feedback': 'Negative Feedback' |
| }; |
| document.getElementById('pageTitle').textContent = titles[pageId] || 'Dashboard'; |
| |
| currentPage = pageId; |
| |
| // Load data for the page |
| loadPageData(pageId); |
| } |
| |
| function loadPageData(pageId) { |
| switch(pageId) { |
| case 'dashboard': |
| loadDashboard(); |
| break; |
| case 'users': |
| loadUsers(); |
| break; |
| case 'predictions': |
| loadPredictions(); |
| break; |
| case 'audios-feedback': |
| loadAudiosWithFeedback(); |
| break; |
| case 'positive-feedback': |
| loadPositiveFeedback(); |
| break; |
| case 'negative-feedback': |
| loadNegativeFeedback(); |
| break; |
| } |
| } |
| |
| function refreshCurrentPage() { |
| loadPageData(currentPage); |
| } |
| |
| // Dashboard functions |
| async function loadDashboard() { |
| try { |
| await Promise.all([ |
| loadUserStats(), |
| loadFeedbackStats(), |
| loadPlatformChart(), |
| loadLanguageChart(), |
| loadFeedbackCategoryChart() |
| ]); |
| } catch(e) { |
| console.error('Failed to load dashboard:', e); |
| } |
| } |
| |
| async function loadUserStats() { |
| try { |
| const res = await fetch('/api/admin/users/stats'); |
| const data = await res.json(); |
| document.getElementById('statTotalUsers').textContent = data.total_users || 0; |
| document.getElementById('statActiveUsers').textContent = data.active_users_7d || 0; |
| } catch(e) { |
| console.error('Failed to load user stats:', e); |
| } |
| } |
| |
| async function loadFeedbackStats() { |
| try { |
| const res = await fetch('/api/admin/feedback/stats'); |
| const data = await res.json(); |
| document.getElementById('statTotalRecordings').textContent = data.total_recordings || 0; |
| document.getElementById('statTotalFeedback').textContent = data.total_feedback || 0; |
| document.getElementById('statPositiveFeedback').textContent = data.positive_feedback || 0; |
| document.getElementById('statNegativeFeedback').textContent = data.negative_feedback || 0; |
| } catch(e) { |
| console.error('Failed to load feedback stats:', e); |
| } |
| } |
| |
| async function loadPlatformChart() { |
| try { |
| const res = await fetch('/api/admin/users/platform'); |
| const data = await res.json(); |
| |
| if (charts.platform) charts.platform.destroy(); |
| const ctx = document.getElementById('chartPlatform').getContext('2d'); |
| charts.platform = new Chart(ctx, { |
| type: 'doughnut', |
| data: { |
| labels: Object.keys(data), |
| datasets: [{ |
| data: Object.values(data), |
| backgroundColor: ['#FF6B9D', '#58a6ff', '#3fb950', '#f0c14b'] |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false |
| } |
| }); |
| } catch(e) { |
| console.error('Failed to load platform chart:', e); |
| } |
| } |
| |
| async function loadLanguageChart() { |
| try { |
| const res = await fetch('/api/admin/users/language'); |
| const data = await res.json(); |
| |
| if (charts.language) charts.language.destroy(); |
| const ctx = document.getElementById('chartLanguage').getContext('2d'); |
| charts.language = new Chart(ctx, { |
| type: 'pie', |
| data: { |
| labels: Object.keys(data).map(k => k === 'ar' ? 'Arabic' : k === 'fr' ? 'French' : k === 'en' ? 'English' : k), |
| datasets: [{ |
| data: Object.values(data), |
| backgroundColor: ['#FF6B9D', '#58a6ff', '#3fb950'] |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false |
| } |
| }); |
| } catch(e) { |
| console.error('Failed to load language chart:', e); |
| } |
| } |
| |
| async function loadFeedbackCategoryChart() { |
| try { |
| const res = await fetch('/api/admin/feedback/categories'); |
| const data = await res.json(); |
| |
| if (charts.feedbackCategory) charts.feedbackCategory.destroy(); |
| const ctx = document.getElementById('chartFeedbackCategory').getContext('2d'); |
| charts.feedbackCategory = new Chart(ctx, { |
| type: 'bar', |
| data: { |
| labels: Object.keys(data.category_counts || {}), |
| datasets: [{ |
| label: 'Feedback Count', |
| data: Object.values(data.category_counts || {}), |
| backgroundColor: '#FF6B9D' |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false |
| } |
| }); |
| } catch(e) { |
| console.error('Failed to load feedback category chart:', e); |
| } |
| } |
| |
| // Users Management |
| async function loadUsers() { |
| try { |
| const res = await fetch('/api/admin/users?limit=100'); |
| const data = await res.json(); |
| const tbody = document.getElementById('usersTableBody'); |
| |
| if (!data.users || data.users.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="7" class="empty-state"><div class="empty-state-icon">👥</div>No users found</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = data.users.map(user => { |
| const registered = new Date(user.registration_date).toLocaleDateString(); |
| const contact = user.email || user.phone || '-'; |
| return ` |
| <tr> |
| <td>${user.user_id ? user.user_id.substring(0, 12) + '...' : '-'}</td> |
| <td>${user.baby_name || '-'}</td> |
| <td>${contact}</td> |
| <td><span class="badge badge-info">${user.platform || 'web'}</span></td> |
| <td><span class="badge badge-info">${user.language || 'ar'}</span></td> |
| <td>${registered}</td> |
| <td> |
| <button class="btn btn-secondary" onclick="viewUser('${user.user_id}')">View</button> |
| </td> |
| </tr> |
| `; |
| }).join(''); |
| } catch(e) { |
| console.error('Failed to load users:', e); |
| document.getElementById('usersTableBody').innerHTML = |
| '<tr><td colspan="7" class="empty-state" style="color: red;">Error loading users</td></tr>'; |
| } |
| } |
| |
| function filterUsers() { |
| const search = document.getElementById('userSearch').value.toLowerCase(); |
| const rows = document.querySelectorAll('#usersTableBody tr'); |
| rows.forEach(row => { |
| const text = row.textContent.toLowerCase(); |
| row.style.display = text.includes(search) ? '' : 'none'; |
| }); |
| } |
| |
| // Predictions |
| async function loadPredictions() { |
| try { |
| const res = await fetch('/api/admin/predictions/list?limit=100'); |
| const data = await res.json(); |
| const tbody = document.getElementById('predictionsTableBody'); |
| |
| if (!data.predictions || data.predictions.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="5" class="empty-state"><div class="empty-state-icon">🎤</div>No predictions found</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = data.predictions.map(pred => { |
| const timestamp = new Date(pred.timestamp).toLocaleString(); |
| const userId = pred.user_id ? pred.user_id.substring(0, 12) + '...' : 'Anonymous'; |
| const confidence = pred.confidence ? (pred.confidence).toFixed(1) + '%' : '-'; |
| return ` |
| <tr> |
| <td>${timestamp}</td> |
| <td>${userId}</td> |
| <td><span class="badge badge-info">${pred.prediction || '-'}</span></td> |
| <td>${confidence}</td> |
| <td>${pred.model_type || '-'}</td> |
| </tr> |
| `; |
| }).join(''); |
| } catch(e) { |
| console.error('Failed to load predictions:', e); |
| document.getElementById('predictionsTableBody').innerHTML = |
| '<tr><td colspan="5" class="empty-state" style="color: red;">Error loading predictions</td></tr>'; |
| } |
| } |
| |
| function filterPredictions() { |
| const search = document.getElementById('predictionSearch').value.toLowerCase(); |
| const rows = document.querySelectorAll('#predictionsTableBody tr'); |
| rows.forEach(row => { |
| const text = row.textContent.toLowerCase(); |
| row.style.display = text.includes(search) ? '' : 'none'; |
| }); |
| } |
| |
| // Audios with Feedback |
| async function loadAudiosWithFeedback() { |
| try { |
| const res = await fetch('/api/admin/feedback/list?limit=100'); |
| const data = await res.json(); |
| const tbody = document.getElementById('audiosFeedbackTableBody'); |
| |
| if (!data.feedback || data.feedback.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="7" class="empty-state"><div class="empty-state-icon">🎵</div>No audios with feedback found</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = data.feedback.map(fb => { |
| const timestamp = new Date(fb.timestamp).toLocaleString(); |
| const status = fb.is_correct ? |
| '<span class="badge badge-success">✓ Correct</span>' : |
| '<span class="badge badge-danger">✗ Incorrect</span>'; |
| const confidence = fb.confidence ? (fb.confidence * 100).toFixed(1) + '%' : '-'; |
| const userId = fb.user_id ? fb.user_id.substring(0, 12) + '...' : 'Anonymous'; |
| |
| let audioPlayer = '<span style="color: #999;">No audio</span>'; |
| if (fb.file_path) { |
| const audioUrl = `/api/admin/feedback/audio/${encodeURIComponent(fb.file_path)}`; |
| audioPlayer = ` |
| <audio controls class="audio-player" preload="none" onerror="console.error('Audio error:', this.error, 'URL:', '${audioUrl}')"> |
| <source src="${audioUrl}" type="audio/wav"> |
| Your browser does not support the audio element. |
| </audio> |
| `; |
| } |
| |
| return ` |
| <tr> |
| <td>${timestamp}</td> |
| <td>${userId}</td> |
| <td>${fb.predicted_label || '-'}</td> |
| <td>${fb.correct_label || '-'}</td> |
| <td>${status}</td> |
| <td>${confidence}</td> |
| <td>${audioPlayer}</td> |
| </tr> |
| `; |
| }).join(''); |
| } catch(e) { |
| console.error('Failed to load audios with feedback:', e); |
| document.getElementById('audiosFeedbackTableBody').innerHTML = |
| '<tr><td colspan="7" class="empty-state" style="color: red;">Error loading audios</td></tr>'; |
| } |
| } |
| |
| // Positive Feedback |
| async function loadPositiveFeedback() { |
| try { |
| const res = await fetch('/api/admin/feedback/list?limit=100&is_correct=true'); |
| const data = await res.json(); |
| const tbody = document.getElementById('positiveFeedbackTableBody'); |
| |
| if (!data.feedback || data.feedback.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="5" class="empty-state"><div class="empty-state-icon">✅</div>No positive feedback found</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = data.feedback.map(fb => { |
| const timestamp = new Date(fb.timestamp).toLocaleString(); |
| const userId = fb.user_id ? fb.user_id.substring(0, 12) + '...' : 'Anonymous'; |
| const confidence = fb.confidence ? (fb.confidence * 100).toFixed(1) + '%' : '-'; |
| |
| let audioPlayer = '<span style="color: #999;">No audio</span>'; |
| if (fb.file_path) { |
| const audioUrl = `/api/admin/feedback/audio/${encodeURIComponent(fb.file_path)}`; |
| audioPlayer = ` |
| <audio controls class="audio-player" preload="none" onerror="console.error('Audio error:', this.error, 'URL:', '${audioUrl}')"> |
| <source src="${audioUrl}" type="audio/wav"> |
| Your browser does not support the audio element. |
| </audio> |
| `; |
| } |
| |
| return ` |
| <tr> |
| <td>${timestamp}</td> |
| <td>${userId}</td> |
| <td><span class="badge badge-success">${fb.predicted_label || '-'}</span></td> |
| <td>${confidence}</td> |
| <td>${audioPlayer}</td> |
| </tr> |
| `; |
| }).join(''); |
| } catch(e) { |
| console.error('Failed to load positive feedback:', e); |
| document.getElementById('positiveFeedbackTableBody').innerHTML = |
| '<tr><td colspan="5" class="empty-state" style="color: red;">Error loading positive feedback</td></tr>'; |
| } |
| } |
| |
| // Negative Feedback |
| async function loadNegativeFeedback() { |
| try { |
| const res = await fetch('/api/admin/feedback/list?limit=100&is_correct=false'); |
| const data = await res.json(); |
| const tbody = document.getElementById('negativeFeedbackTableBody'); |
| |
| if (!data.feedback || data.feedback.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><div class="empty-state-icon">❌</div>No negative feedback found</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = data.feedback.map(fb => { |
| const timestamp = new Date(fb.timestamp).toLocaleString(); |
| const userId = fb.user_id ? fb.user_id.substring(0, 12) + '...' : 'Anonymous'; |
| const confidence = fb.confidence ? (fb.confidence * 100).toFixed(1) + '%' : '-'; |
| |
| let audioPlayer = '<span style="color: #999;">No audio</span>'; |
| if (fb.file_path) { |
| const audioUrl = `/api/admin/feedback/audio/${encodeURIComponent(fb.file_path)}`; |
| audioPlayer = ` |
| <audio controls class="audio-player" preload="none" onerror="console.error('Audio error:', this.error, 'URL:', '${audioUrl}')"> |
| <source src="${audioUrl}" type="audio/wav"> |
| Your browser does not support the audio element. |
| </audio> |
| `; |
| } |
| |
| return ` |
| <tr> |
| <td>${timestamp}</td> |
| <td>${userId}</td> |
| <td><span class="badge badge-danger">${fb.predicted_label || '-'}</span></td> |
| <td><span class="badge badge-success">${fb.correct_label || '-'}</span></td> |
| <td>${confidence}</td> |
| <td>${audioPlayer}</td> |
| </tr> |
| `; |
| }).join(''); |
| } catch(e) { |
| console.error('Failed to load negative feedback:', e); |
| document.getElementById('negativeFeedbackTableBody').innerHTML = |
| '<tr><td colspan="6" class="empty-state" style="color: red;">Error loading negative feedback</td></tr>'; |
| } |
| } |
| |
| function viewUser(userId) { |
| alert('User details for: ' + userId); |
| // TODO: Implement user detail modal |
| } |
| |
| function logout() { |
| fetch('/admin/logout') |
| .then(() => window.location.href = '/admin/login') |
| .catch(() => window.location.href = '/admin/login'); |
| } |
| |
| // Initialize |
| window.addEventListener('DOMContentLoaded', () => { |
| loadDashboard(); |
| }); |
| </script> |
| </body> |
| </html> |
| ''' |
|
|
|
|