BBPlease / src /admin_dashboard_template.py
hamza-ksr's picture
Upload src/admin_dashboard_template.py with huggingface_hub
3a4c2e1 verified
Raw
History Blame Contribute Delete
40.9 kB
"""
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>
'''