r-vasanthkumar73-dev's picture
Deploying backend and frontend folder modules.
099d157 verified
Raw
History Blame Contribute Delete
29.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instructor Nexus — Student Performance</title>
<link rel="stylesheet" href="/css/styles.css">
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet">
<style>
.perf-content {
padding: 32px;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 32px;
}
.overview-card {
padding: 24px;
border-radius: 16px;
background: var(--surface-container-low);
border: 1px solid rgba(59, 73, 75, 0.1);
transition: all 0.3s;
}
.overview-card:hover {
border-color: rgba(0, 240, 255, 0.15);
transform: translateY(-2px);
}
.overview-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.overview-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--outline);
font-weight: 600;
margin-bottom: 4px;
}
.overview-value {
font-family: var(--font-headline);
font-size: 2rem;
font-weight: 800;
}
.sections-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 32px;
}
.session-table {
width: 100%;
border-collapse: collapse;
}
.session-table th {
text-align: left;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--outline);
font-weight: 600;
padding: 12px;
border-bottom: 1px solid rgba(59, 73, 75, 0.15);
}
.session-table td {
padding: 12px;
border-bottom: 1px solid rgba(59, 73, 75, 0.08);
font-size: 13px;
color: var(--on-surface-variant);
}
.session-table tr:hover td {
background: rgba(0, 240, 255, 0.02);
}
.chart-area {
height: 200px;
position: relative;
margin-top: 16px;
overflow: hidden;
}
.chart-area canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.empty-state {
text-align: center;
padding: 48px;
color: var(--outline);
}
.empty-state .material-symbols-outlined {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.student-id-bar {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 32px;
}
.toast {
visibility: hidden;
min-width: 250px;
background-color: #ff4a4a;
color: #fff;
text-align: center;
border-radius: 8px;
padding: 16px;
position: fixed;
z-index: 1000;
left: 50%;
bottom: 30px;
transform: translateX(-50%);
font-family: var(--font-headline);
font-weight: 600;
font-size: 14px;
box-shadow: 0 4px 12px rgba(255, 74, 74, 0.3);
transition: opacity 0.3s, bottom 0.3s;
opacity: 0;
}
.toast.show {
visibility: visible;
opacity: 1;
bottom: 50px;
}
@media (max-width: 1024px) {
.overview-grid {
grid-template-columns: repeat(2, 1fr);
}
.sections-grid {
grid-template-columns: 1fr;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-logo">S</div>
<nav class="sidebar-nav">
<a href="/" class="sidebar-link"><span class="material-symbols-outlined">dashboard</span><span
class="label">Home</span></a>
<a href="/live" class="sidebar-link"><span class="material-symbols-outlined">sensors</span><span
class="label">Live</span></a>
<a href="/scan" class="sidebar-link"><span class="material-symbols-outlined">analytics</span><span
class="label">Scan</span></a>
<a href="/stats" class="sidebar-link active"><span class="material-symbols-outlined"
style="font-variation-settings:'FILL' 1;">school</span><span class="label">Stats</span></a>
</nav>
</aside>
<header class="topbar">
<div style="display:flex;align-items:center;gap:16px;">
<h1 class="topbar-title gradient-text-tertiary">Instructor Nexus</h1>
<div class="status-pill"><span class="status-dot" style="background:var(--tertiary-fixed-dim);"></span><span
class="status-text" style="color:var(--tertiary-fixed-dim);">Performance Hub</span></div>
</div>
<button class="btn-secondary" onclick="refreshData(event)"><span class="material-symbols-outlined"
style="font-size:18px;">refresh</span> Refresh Data</button>
</header>
<main class="main-content">
<div class="perf-content">
<div class="animate-fade-in">
<h2 style="font-family:var(--font-headline);font-size:1.75rem;font-weight:700;margin-bottom:8px;">
Session Performance Tracker</h2>
<p style="color:var(--on-surface-variant);font-size:0.9rem;margin-bottom:24px;">Monitor current and
historical engagement, emotion trends, and drill down into specific sessions.</p>
</div>
<!-- Session ID Input -->
<div class="student-id-bar">
<input class="sentinel-input" id="student-id-input" placeholder="Enter Session ID (leave empty for all)"
value="" style="max-width:300px;">
<button class="btn-primary" onclick="loadPerformance(event)"><span class="material-symbols-outlined"
style="font-size:18px;">search</span> Load Data</button>
</div>
<!-- Overview Cards -->
<div class="overview-grid stagger-children" id="overview-cards">
<div class="overview-card">
<div class="overview-icon" style="background:rgba(0,240,255,0.1);"><span
class="material-symbols-outlined" style="color:var(--primary-container);">monitoring</span>
</div>
<div class="overview-label">Total Sessions</div>
<div class="overview-value" style="color:var(--primary-container);" id="ov-sessions"></div>
<div id="ov-session-id" style="font-size:11px;color:var(--outline);margin-top:4px;">Global Data</div>
<div id="ov-session-duration" style="font-size:11px;color:var(--primary-container);margin-top:2px;"></div>
<div id="ov-session-date" style="font-size:11px;color:var(--outline);margin-top:2px;"></div>
</div>
<div class="overview-card">
<div class="overview-icon" style="background:rgba(209,188,255,0.1);"><span
class="material-symbols-outlined" style="color:var(--secondary);" id="ov-avg-icon">trending_up</span></div>
<div class="overview-label" id="ov-avg-label">Avg Engagement</div>
<div class="overview-value" style="color:var(--secondary);" id="ov-avg"></div>
</div>
<div class="overview-card">
<div class="overview-icon" style="background:rgba(0,240,255,0.1);"><span
class="material-symbols-outlined"
style="color:var(--primary-container);" id="ov-peak-icon">emoji_events</span></div>
<div class="overview-label" id="ov-peak-label">Peak Engagement</div>
<div class="overview-value" style="color:var(--primary-container);" id="ov-peak"></div>
</div>
<div class="overview-card">
<div class="overview-icon" style="background:rgba(255,180,171,0.1);"><span
class="material-symbols-outlined" style="color:var(--error);" id="ov-min-icon">trending_down</span></div>
<div class="overview-label" id="ov-min-label">Min Engagement</div>
<div class="overview-value" style="color:var(--error);" id="ov-min"></div>
</div>
</div>
<!-- Charts & Session History -->
<div class="sections-grid">
<!-- Engagement Over Time Chart -->
<div class="glass-card" style="padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<div>
<div
style="font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--outline);font-weight:600;">
Engagement Timeline</div>
<div style="font-family:var(--font-headline);font-size:1.1rem;font-weight:700;">Historical
Trend</div>
</div>
<span class="material-symbols-outlined" style="color:var(--primary-container);">timeline</span>
</div>
<div class="chart-area">
<canvas id="history-chart"></canvas>
</div>
</div>
<!-- Session History Table -->
<div class="glass-card" style="padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<div>
<div
style="font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--outline);font-weight:600;">
Session Log</div>
<div style="font-family:var(--font-headline);font-size:1.1rem;font-weight:700;">Recent
Sessions</div>
</div>
<span class="material-symbols-outlined" style="color:var(--secondary);">history</span>
</div>
<div style="overflow-y:auto;max-height:240px;" id="session-table-container">
<div class="empty-state">
<span class="material-symbols-outlined">inbox</span>
<div style="font-family:var(--font-headline);font-weight:600;">No sessions yet</div>
<div style="font-size:12px;margin-top:4px;">Start a monitoring session from the Live Pulse
page</div>
</div>
</div>
</div>
</div>
<!-- Performance Records -->
<div class="glass-card" style="padding:24px;margin-bottom:32px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<div>
<div
style="font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--outline);font-weight:600;">
Performance Records</div>
<div style="font-family:var(--font-headline);font-size:1.1rem;font-weight:700;">Detailed History
</div>
</div>
<a href="/api/sessions/export" title="Export to CSV" style="color:var(--tertiary-fixed-dim); text-decoration:none; transition:transform 0.2s; display:flex; align-items:center; gap:8px; padding: 0 16px; height:36px; border-radius:8px; background:rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);" onmouseover="this.style.transform='scale(1.05)'; this.style.background='rgba(255,255,255,0.1)'" onmouseout="this.style.transform='scale(1)'; this.style.background='rgba(255,255,255,0.05)'">
<span class="material-symbols-outlined" style="font-size: 18px; color: #f4d03f;">description</span>
<span style="font-family: var(--font-headline); font-size: 13px; font-weight: 600; color: #f4d03f; letter-spacing: 0.5px;">EXPORT CSV</span>
</a>
</div>
<div id="performance-records" style="overflow-y:auto;max-height:300px;">
<div class="empty-state">
<span class="material-symbols-outlined">folder_off</span>
<div style="font-family:var(--font-headline);font-weight:600;">No performance data</div>
<div style="font-size:12px;margin-top:4px;">Complete a monitoring session to see performance
records</div>
</div>
</div>
</div>
</div>
</main>
<div id="toast" class="toast">User Not Found</div>
<script>
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.className = "toast show";
setTimeout(function () { toast.className = toast.className.replace("show", ""); }, 3000);
}
window.onload = () => {
loadPerformance();
refreshData();
};
async function loadPerformance(event) {
let loadBtn = null;
let originalText = '';
// If triggered by a button click, show loading state on that button
if (event && event.currentTarget) {
loadBtn = event.currentTarget;
originalText = loadBtn.innerHTML;
const isRefresh = originalText.includes('refresh');
loadBtn.innerHTML = `<span class="material-symbols-outlined" style="font-size:18px; animation: spin 1s linear infinite;">${isRefresh ? 'refresh' : 'sync'}</span> ${isRefresh ? 'Refreshing...' : 'Loading...'}`;
loadBtn.disabled = true;
}
let studentId = document.getElementById('student-id-input').value.trim();
if (!studentId) studentId = 'all';
try {
const res = await fetch(`/api/stats/${encodeURIComponent(studentId)}`);
if (res.status === 404) {
showToast("Session Not Found");
throw new Error("Session Not Found");
}
const stats = await res.json();
updateOverview(stats);
// Show success state briefly
if (loadBtn) {
loadBtn.innerHTML = `<span class="material-symbols-outlined" style="font-size:18px;">check</span> Loaded`;
setTimeout(() => {
loadBtn.innerHTML = originalText;
loadBtn.disabled = false;
}, 1500);
}
} catch (e) {
console.error('Failed to load stats:', e);
if (loadBtn) {
loadBtn.innerHTML = '<span class="material-symbols-outlined" style="font-size:18px;">error</span> Failed';
setTimeout(() => {
loadBtn.innerHTML = originalText;
loadBtn.disabled = false;
}, 2000);
}
}
}
async function refreshData(event) {
let loadBtn = null;
let originalText = '';
if (event && event.currentTarget) {
loadBtn = event.currentTarget;
originalText = loadBtn.innerHTML;
loadBtn.innerHTML = `<span class="material-symbols-outlined" style="font-size:18px; animation: spin 1s linear infinite;">refresh</span> Refreshing...`;
loadBtn.disabled = true;
}
try {
const res = await fetch('/api/sessions/latest');
const data = await res.json();
updateSessionTable(data.sessions);
updatePerformanceRecords(data.performance);
drawHistoryChart(data.performance);
if (loadBtn) {
loadBtn.innerHTML = `<span class="material-symbols-outlined" style="font-size:18px;">check</span> Refreshed`;
setTimeout(() => {
loadBtn.innerHTML = originalText;
loadBtn.disabled = false;
}, 1500);
}
} catch (e) {
console.error('Failed to refresh data:', e);
if (loadBtn) {
loadBtn.innerHTML = '<span class="material-symbols-outlined" style="font-size:18px;">error</span> Failed';
setTimeout(() => {
loadBtn.innerHTML = originalText;
loadBtn.disabled = false;
}, 2000);
}
}
}
function updateOverview(stats) {
if (!stats) return;
document.getElementById('ov-sessions').textContent = stats.total_sessions || 0;
document.getElementById('ov-avg').textContent = stats.avg_engagement ? stats.avg_engagement.toFixed(1) + '%' : '—';
document.getElementById('ov-peak').textContent = stats.peak_engagement ? stats.peak_engagement.toFixed(1) + '%' : '—';
document.getElementById('ov-min').textContent = stats.min_engagement ? stats.min_engagement.toFixed(1) + '%' : '—';
// Handle session specific drill-down
if (stats.session_info) {
// Update Card 1 subtitle
document.getElementById('ov-session-id').textContent = 'Session Detailed View';
// Update Card 2 to show Session ID
document.getElementById('ov-avg-label').textContent = 'SESSION ID';
document.getElementById('ov-avg').innerHTML = `<span class="gradient-text-tertiary">#${stats.session_info.id}</span>`;
document.getElementById('ov-avg-icon').textContent = 'tag';
// Update Card 3 to show Duration
document.getElementById('ov-peak-label').textContent = 'SESSION DURATION';
const dur = stats.session_info.duration_mins || "0 seconds";
document.getElementById('ov-peak').innerHTML = `<span style="color:#ff0000 !important; font-size: 2.5rem;">${dur}</span>`;
document.getElementById('ov-peak-icon').textContent = 'timer';
// Update Card 4 to show Date
document.getElementById('ov-min-label').textContent = 'SESSION DATE';
const dateStr = stats.session_info.date_time || '—';
document.getElementById('ov-min').innerHTML = `<span style="font-size: 1.5rem; color:var(--primary-container);">${dateStr}</span>`;
document.getElementById('ov-min-icon').textContent = 'calendar_today';
// Clear old sub-info in card 1
document.getElementById('ov-session-duration').textContent = '';
document.getElementById('ov-session-date').textContent = '';
highlightSessionRow(stats.session_info.id);
} else {
// Reset to Global Labels
document.getElementById('ov-session-id').textContent = 'Global Data';
document.getElementById('ov-session-duration').textContent = '';
document.getElementById('ov-session-date').textContent = '';
document.getElementById('ov-avg-label').textContent = 'Avg Engagement';
document.getElementById('ov-avg-icon').textContent = 'trending_up';
document.getElementById('ov-peak-label').textContent = 'Peak Engagement';
document.getElementById('ov-peak-icon').textContent = 'emoji_events';
document.getElementById('ov-min-label').textContent = 'Min Engagement';
document.getElementById('ov-min-icon').textContent = 'trending_down';
// Reset values will be handled by the code above this if-block
highlightSessionRow(null);
}
}
function highlightSessionRow(sessionId) {
const rows = document.querySelectorAll('#session-table-container tr');
rows.forEach(row => {
row.style.backgroundColor = '';
if (sessionId && row.getAttribute('data-session-id') === String(sessionId)) {
row.style.backgroundColor = 'rgba(0,240,255,0.15)';
// Scroll into view gently
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
}
function updateSessionTable(sessions) {
const container = document.getElementById('session-table-container');
if (!sessions || sessions.length === 0) {
container.innerHTML = '<div class="empty-state"><span class="material-symbols-outlined">inbox</span><div style="font-family:var(--font-headline);font-weight:600;">No sessions yet</div></div>';
return;
}
let html = '<table class="session-table"><thead><tr><th>ID</th><th>Started</th><th>Engagement</th><th>Emotion</th><th>Actions</th></tr></thead><tbody>';
sessions.forEach(s => {
const started = s.start_time ? new Date(s.start_time).toLocaleString() : '—';
html += `<tr data-session-id="${s.id}">
<td>#${s.id}</td>
<td>${started}</td>
<td><span style="color:var(--primary-container);font-weight:700;">${s.avg_engagement ? s.avg_engagement.toFixed(1) + '%' : '—'}</span></td>
<td><span class="emotion-chip ${(s.dominant_emotion || 'neutral').toLowerCase()}">${s.dominant_emotion || 'neutral'}</span></td>
<td>
<button onclick="deleteSession(${s.id})" style="background:transparent;border:none;color:var(--error);cursor:pointer;opacity:0.7;transition:0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7" title="Delete Session">
<span class="material-symbols-outlined" style="font-size: 18px;">delete</span>
</button>
</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
// Re-highlight if needed
let studentId = document.getElementById('student-id-input').value.trim();
if (studentId && studentId !== 'all') highlightSessionRow(studentId);
}
async function deleteSession(sessionId) {
if (!confirm(`Are you sure you want to permanently delete Session #${sessionId}? This will also delete all associated logs and dynamically renumber subsequent sessions.`)) {
return;
}
try {
const res = await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
if (res.ok) {
refreshData(); // Automatically reload the table and total count
// If the currently searched session was deleted or shifted, clear search
let currentSearch = document.getElementById('student-id-input').value.trim();
if (currentSearch == sessionId) {
document.getElementById('student-id-input').value = '';
loadPerformance();
}
} else {
alert("Failed to delete session.");
}
} catch (err) {
console.error(err);
alert("An error occurred while deleting the session.");
}
}
function updatePerformanceRecords(perf) {
const container = document.getElementById('performance-records');
if (!perf || perf.length === 0) {
container.innerHTML = '<div class="empty-state"><span class="material-symbols-outlined">folder_off</span><div style="font-family:var(--font-headline);font-weight:600;">No performance data</div></div>';
return;
}
let html = '<table class="session-table"><thead><tr><th>Session ID</th><th>Date</th><th>Engagement</th><th>Summary</th></tr></thead><tbody>';
perf.forEach(p => {
const sessionLabel = p.session_id ? `#${p.session_id}` : '—';
html += `<tr>
<td style="color:var(--primary-container);font-weight:700;">${sessionLabel}</td>
<td>${p.date || '—'}</td>
<td><span style="color:var(--secondary);font-weight:700;">${p.engagement_score ? p.engagement_score.toFixed(1) + '%' : '—'}</span></td>
<td style="font-size:11px;">${p.overall_summary || '—'}</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function drawHistoryChart(perf) {
const canvas = document.getElementById('history-chart');
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
const w = rect.width;
const h = rect.height;
ctx.clearRect(0, 0, w, h);
if (!perf || perf.length === 0) {
ctx.fillStyle = 'rgba(132,148,149,0.3)';
ctx.font = '13px Inter';
ctx.textAlign = 'center';
ctx.fillText('No data available', w / 2, h / 2);
return;
}
const data = perf.map(p => p.engagement_score || 0).reverse();
if (data.length < 2) return;
// Dynamic Theme Colors
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
const primaryColor = isLight ? '#0284c7' : '#00f0ff';
const primaryRGBA = isLight ? '2, 132, 199' : '0, 240, 255';
const gridColor = isLight ? 'rgba(148,163,184,0.3)' : 'rgba(59,73,75,0.1)';
// Grid lines
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = (i / 4) * h;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
}
// Line chart
ctx.beginPath();
ctx.strokeStyle = primaryColor;
ctx.lineWidth = 2;
for (let i = 0; i < data.length; i++) {
const x = (i / (data.length - 1)) * w;
const y = h - (data[i] / 100) * h;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// Gradient fill
ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, `rgba(${primaryRGBA}, 0.15)`);
grad.addColorStop(1, `rgba(${primaryRGBA}, 0)`);
ctx.fillStyle = grad;
ctx.fill();
// Dots on data points
ctx.fillStyle = primaryColor;
for (let i = 0; i < data.length; i++) {
const x = (i / (data.length - 1)) * w;
const y = h - (data[i] / 100) * h;
ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill();
}
}
</script>
</body>
</html>