| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>The Sentinel Interface β AI Engagement Monitor</title> |
| <meta name="description" |
| content="Multisource Emotion Detection and Engagement Optimization for E-Learning. Real-time face, speech, and text analysis."> |
| <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> |
| .hero-section { |
| position: relative; |
| padding: 80px 48px 48px; |
| text-align: center; |
| overflow: hidden; |
| } |
| |
| .hero-title { |
| font-family: var(--font-headline); |
| font-size: 3.5rem; |
| font-weight: 900; |
| line-height: 1.1; |
| margin-bottom: 16px; |
| letter-spacing: -0.03em; |
| } |
| |
| .hero-sub { |
| font-size: 1.1rem; |
| color: var(--on-surface-variant); |
| max-width: 600px; |
| margin: 0 auto 48px; |
| line-height: 1.7; |
| } |
| |
| .hero-glow { |
| position: absolute; |
| width: 600px; |
| height: 600px; |
| border-radius: 50%; |
| filter: blur(120px); |
| opacity: 0.15; |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| .hero-glow.cyan { |
| background: var(--primary-container); |
| top: -200px; |
| left: 20%; |
| } |
| |
| .hero-glow.purple { |
| background: var(--secondary-container); |
| top: -100px; |
| right: 10%; |
| } |
| |
| .hero-glow.gold { |
| background: var(--tertiary-fixed-dim); |
| bottom: -300px; |
| left: 40%; |
| opacity: 0.08; |
| } |
| |
| .cards-section { |
| padding: 0 48px 48px; |
| position: relative; |
| z-index: 1; |
| } |
| |
| .stats-section { |
| padding: 0 48px 48px; |
| } |
| |
| .hero-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| padding: 6px 16px; |
| border-radius: 999px; |
| background: rgba(0, 240, 255, 0.06); |
| border: 1px solid rgba(0, 240, 255, 0.15); |
| font-size: 11px; |
| text-transform: uppercase; |
| letter-spacing: 0.08em; |
| font-weight: 600; |
| color: var(--primary-container); |
| margin-bottom: 24px; |
| } |
| |
| .orbit-ring { |
| position: absolute; |
| border-radius: 50%; |
| border: 1px solid rgba(0, 240, 255, 0.05); |
| animation: rotate-slow 60s linear infinite; |
| pointer-events: none; |
| } |
| |
| .orbit-ring:nth-child(1) { |
| width: 400px; |
| height: 400px; |
| top: -100px; |
| left: calc(50% - 200px); |
| } |
| |
| .orbit-ring:nth-child(2) { |
| width: 600px; |
| height: 600px; |
| top: -200px; |
| left: calc(50% - 300px); |
| animation-duration: 90s; |
| animation-direction: reverse; |
| } |
| |
| .orbit-ring:nth-child(3) { |
| width: 800px; |
| height: 800px; |
| top: -300px; |
| left: calc(50% - 400px); |
| animation-duration: 120s; |
| } |
| |
| .orbit-dot { |
| position: absolute; |
| width: 4px; |
| height: 4px; |
| background: var(--primary-container); |
| border-radius: 50%; |
| box-shadow: 0 0 8px var(--primary-container); |
| } |
| |
| .system-status { |
| display: flex; |
| align-items: center; |
| gap: 24px; |
| padding: 20px 28px; |
| border-radius: 16px; |
| background: rgba(26, 31, 47, 0.5); |
| border: 1px solid rgba(59, 73, 75, 0.1); |
| } |
| |
| .system-status-item { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 12px; |
| color: var(--on-surface-variant); |
| } |
| |
| .system-status-dot { |
| width: 6px; |
| height: 6px; |
| border-radius: 50%; |
| } |
| |
| .system-status-dot.online { |
| background: #34c759; |
| box-shadow: 0 0 6px #34c759; |
| } |
| |
| .system-status-dot.loading { |
| background: var(--tertiary-fixed-dim); |
| animation: pulse-glow 1.5s infinite; |
| } |
| |
| .feature-grid-container { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 24px; |
| margin-bottom: 32px; |
| } |
| |
| .bottom-grid { |
| display: grid; |
| grid-template-columns: 2fr 1fr; |
| gap: 24px; |
| } |
| |
| @media (max-width: 1200px) { |
| .feature-grid-container { |
| grid-template-columns: 1fr; |
| } |
| |
| .bottom-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <canvas id="particles-canvas"></canvas> |
|
|
| |
| <aside class="sidebar"> |
| <div class="sidebar-logo">S</div> |
| <nav class="sidebar-nav"> |
| <a href="/" class="sidebar-link active" data-tooltip="Dashboard"> |
| <span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">dashboard</span> |
| <span class="label">Home</span> |
| </a> |
| <a href="./live.html" class="sidebar-link" data-tooltip="Live Analysis"> |
| <span class="material-symbols-outlined">sensors</span> |
| <span class="label">Live</span> |
| </a> |
| <a href="./scan.html" class="sidebar-link" data-tooltip="Deep Scan"> |
| <span class="material-symbols-outlined">analytics</span> |
| <span class="label">Scan</span> |
| </a> |
| <a href="./stats.html" class="sidebar-link" data-tooltip="Performance"> |
| <span class="material-symbols-outlined">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">The Sentinel Interface</h1> |
| <div class="status-pill"> |
| <span class="status-dot"></span> |
| <span class="status-text">System Online</span> |
| </div> |
| </div> |
| <div style="display:flex;align-items:center;gap:16px;"> |
| <button class="btn-primary" onclick="window.location.href='/live'" id="start-monitoring-btn"> |
| <span class="material-symbols-outlined" style="font-size:18px;">sensors</span> |
| Start Monitoring |
| </button> |
| </div> |
| </header> |
|
|
| |
| <main class="main-content"> |
| |
| <section class="hero-section"> |
| <div class="hero-glow cyan"></div> |
| <div class="hero-glow purple"></div> |
| <div class="hero-glow gold"></div> |
| <div class="orbit-ring"> |
| <div class="orbit-dot" style="top:0;left:50%;"></div> |
| </div> |
| <div class="orbit-ring"> |
| <div class="orbit-dot" style="top:50%;right:0;"></div> |
| </div> |
| <div class="orbit-ring"> |
| <div class="orbit-dot" style="bottom:0;left:30%;"></div> |
| </div> |
| <div style="position:relative;z-index:1;"> |
| <div class="hero-badge"> |
| <span class="material-symbols-outlined" style="font-size:14px;">neurology</span> |
| Multisource AI Emotion Engine |
| </div> |
| <h2 class="hero-title animate-fade-in"> |
| <span class="gradient-text">Multisource Emotion</span><br> |
| <span style="color:var(--on-surface);"> Recognition System</span> |
| </h2> |
| <p class="hero-sub animate-fade-in" style="animation-delay:0.2s;"> |
| Real-time multimodal emotion detection using face recognition, speech prosody analysis, and text |
| sentiment. |
| </p> |
| <div |
| style="display:flex;gap:16px;justify-content:center;animation:fade-in 0.6s ease-out 0.4s forwards;opacity:0;"> |
| <button class="btn-primary" onclick="window.location.href='/live'"> |
| <span class="material-symbols-outlined" style="font-size:18px;">play_arrow</span> |
| Launch Live Pulse |
| </button> |
| <button class="btn-secondary" onclick="window.location.href='/scan'"> |
| <span class="material-symbols-outlined" style="font-size:18px;">folder_open</span> |
| Deep Archive Scan |
| </button> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section class="cards-section stagger-children"> |
| <div class="feature-grid-container"> |
| |
| <div class="feature-card" onclick="window.location.href='/live'"> |
| <div class="feature-card-inner"> |
| <div class="feature-icon primary"> |
| <span class="material-symbols-outlined" |
| style="font-variation-settings:'FILL' 1;">sensors</span> |
| </div> |
| <h3 class="feature-title" style="color:var(--primary);">Sentinel Live Pulse</h3> |
| <p class="feature-desc"> |
| Real-time multimodal analysis engine. Live webcam with 468-point face mesh visualization, |
| audio frequency waveform recording, and instant text sentiment β all fused into a unified |
| engagement score. |
| </p> |
| <div class="feature-footer"> |
| <span class="feature-tag">Real-time</span> |
| <span class="material-symbols-outlined" |
| style="color:var(--primary-container);font-size:20px;">arrow_forward</span> |
| </div> |
| |
| <div |
| style="position:absolute;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent,var(--primary-container),transparent);animation:scan-line 3s linear infinite;opacity:0.3;"> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="feature-card secondary" onclick="window.location.href='/scan'"> |
| <div class="feature-card-inner"> |
| <div class="feature-icon secondary"> |
| <span class="material-symbols-outlined" |
| style="font-variation-settings:'FILL' 1;">query_stats</span> |
| </div> |
| <h3 class="feature-title" style="color:var(--secondary);">Deep Archive Scan</h3> |
| <p class="feature-desc"> |
| Upload and analyze existing recordings. Process face images, audio clips, and text documents |
| through our AI pipeline for offline emotion detection and engagement scoring. |
| </p> |
| <div class="feature-footer"> |
| <span class="feature-tag secondary">Upload & Analyze</span> |
| <span class="material-symbols-outlined" |
| style="color:var(--secondary);font-size:20px;">arrow_forward</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="feature-card tertiary" onclick="window.location.href='/performance'"> |
| <div class="feature-card-inner"> |
| <div class="feature-icon tertiary"> |
| <span class="material-symbols-outlined" |
| style="font-variation-settings:'FILL' 1;">school</span> |
| </div> |
| <h3 class="feature-title" style="color:var(--tertiary-fixed-dim);">Instructor Nexus</h3> |
| <p class="feature-desc"> |
| Teacher control center for student performance. View current session metrics, historical |
| engagement trends, |
| emotion distribution charts, and per-student analytics. |
| </p> |
| <div class="feature-footer"> |
| <span class="feature-tag tertiary">Performance Hub</span> |
| <span class="material-symbols-outlined" |
| style="color:var(--tertiary-fixed-dim);font-size:20px;">arrow_forward</span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bottom-grid"> |
| |
| <div class="glass-card" style="padding:28px;"> |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;"> |
| <div> |
| <div |
| style="font-size:10px;text-transform:uppercase;letter-spacing:0.1em;color:var(--outline);font-weight:600;"> |
| System Status</div> |
| <div style="font-family:var(--font-headline);font-size:1.25rem;font-weight:700;">AI Models & |
| Services</div> |
| </div> |
| <span class="material-symbols-outlined" style="color:var(--primary-container);">memory</span> |
| </div> |
| <div style="display:flex;flex-direction:column;gap:12px;" id="system-status-container"> |
| <div class="system-status"> |
| <div class="system-status-dot loading"></div> |
| <span style="flex:1;">Checking system status...</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-card" style="padding:28px;"> |
| <div |
| style="font-size:10px;text-transform:uppercase;letter-spacing:0.1em;color:var(--outline);font-weight:600;margin-bottom:12px;"> |
| Quick Stats</div> |
| <div style="font-family:var(--font-headline);font-size:1.25rem;font-weight:700;margin-bottom:20px;"> |
| Today's Overview</div> |
| <div style="display:flex;flex-direction:column;gap:12px;" id="quick-stats"> |
| <div class="stat-card"> |
| <div class="stat-label">Total Sessions</div> |
| <div class="stat-value" style="color:var(--primary-container);" id="stat-sessions">0</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-label">Avg Engagement</div> |
| <div class="stat-value" style="color:var(--secondary);" id="stat-engagement">β</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </section> |
| </main> |
|
|
| <script> |
| |
| (function () { |
| const canvas = document.getElementById('particles-canvas'); |
| const ctx = canvas.getContext('2d'); |
| let particles = []; |
| const PARTICLE_COUNT = 60; |
| |
| function resize() { |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| } |
| window.addEventListener('resize', resize); |
| resize(); |
| |
| class Particle { |
| constructor() { this.reset(); } |
| reset() { |
| this.x = Math.random() * canvas.width; |
| this.y = Math.random() * canvas.height; |
| this.vx = (Math.random() - 0.5) * 0.3; |
| this.vy = (Math.random() - 0.5) * 0.3; |
| this.radius = Math.random() * 1.5 + 0.5; |
| this.opacity = Math.random() * 0.3 + 0.1; |
| } |
| update() { |
| this.x += this.vx; |
| this.y += this.vy; |
| if (this.x < 0 || this.x > canvas.width) this.vx *= -1; |
| if (this.y < 0 || this.y > canvas.height) this.vy *= -1; |
| } |
| draw() { |
| const themeColor = document.documentElement.getAttribute('data-theme') === 'light' ? '2, 132, 199' : '0, 240, 255'; |
| ctx.beginPath(); |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); |
| ctx.fillStyle = `rgba(${themeColor}, ${this.opacity})`; |
| ctx.fill(); |
| } |
| } |
| |
| for (let i = 0; i < PARTICLE_COUNT; i++) particles.push(new Particle()); |
| |
| function animate() { |
| const themeColor = document.documentElement.getAttribute('data-theme') === 'light' ? '2, 132, 199' : '0, 240, 255'; |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| particles.forEach(p => { p.update(); p.draw(); }); |
| |
| for (let i = 0; i < particles.length; i++) { |
| for (let j = i + 1; j < particles.length; j++) { |
| const dx = particles[i].x - particles[j].x; |
| const dy = particles[i].y - particles[j].y; |
| const dist = Math.sqrt(dx * dx + dy * dy); |
| if (dist < 120) { |
| ctx.beginPath(); |
| ctx.moveTo(particles[i].x, particles[i].y); |
| ctx.lineTo(particles[j].x, particles[j].y); |
| ctx.strokeStyle = `rgba(${themeColor}, ${0.03 * (1 - dist / 120)})`; |
| ctx.lineWidth = 0.5; |
| ctx.stroke(); |
| } |
| } |
| } |
| requestAnimationFrame(animate); |
| } |
| animate(); |
| })(); |
| |
| |
| document.querySelectorAll('.feature-card').forEach(card => { |
| const inner = card.querySelector('.feature-card-inner'); |
| card.addEventListener('mousemove', (e) => { |
| const rect = card.getBoundingClientRect(); |
| const x = (e.clientX - rect.left) / rect.width; |
| const y = (e.clientY - rect.top) / rect.height; |
| const rotateX = (0.5 - y) * 8; |
| const rotateY = (x - 0.5) * 8; |
| inner.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-4px)`; |
| inner.style.setProperty('--mouse-x', (x * 100) + '%'); |
| inner.style.setProperty('--mouse-y', (y * 100) + '%'); |
| }); |
| card.addEventListener('mouseleave', () => { |
| inner.style.transform = ''; |
| }); |
| }); |
| |
| |
| async function checkSystemStatus() { |
| const container = document.getElementById('system-status-container'); |
| try { |
| const res = await fetch('/api/health'); |
| const data = await res.json(); |
| container.innerHTML = ` |
| <div class="system-status"> |
| <div class="system-status-dot online"></div> |
| <span style="flex:1;font-weight:600;">API Server</span> |
| <span style="font-size:11px;color:var(--primary-container);">Online</span> |
| </div> |
| <div class="system-status"> |
| <div class="system-status-dot ${data.models_loaded?.face ? 'online' : 'loading'}"></div> |
| <span style="flex:1;">Face CNN Model</span> |
| <span style="font-size:11px;color:${data.models_loaded?.face ? 'var(--primary-container)' : 'var(--tertiary-fixed-dim)'};">${data.models_loaded?.face ? 'Loaded' : 'Standby'}</span> |
| </div> |
| <div class="system-status"> |
| <div class="system-status-dot ${data.models_loaded?.text ? 'online' : 'loading'}"></div> |
| <span style="flex:1;">Text Sentiment (DistilBERT)</span> |
| <span style="font-size:11px;color:${data.models_loaded?.text ? 'var(--primary-container)' : 'var(--tertiary-fixed-dim)'};">${data.models_loaded?.text ? 'Loaded' : 'Standby'}</span> |
| </div> |
| <div class="system-status"> |
| <div class="system-status-dot ${data.models_loaded?.speech ? 'online' : 'loading'}"></div> |
| <span style="flex:1;">Speech Analyzer (librosa)</span> |
| <span style="font-size:11px;color:${data.models_loaded?.speech ? 'var(--primary-container)' : 'var(--tertiary-fixed-dim)'};">${data.models_loaded?.speech ? 'Loaded' : 'Standby'}</span> |
| </div> |
| `; |
| } catch (e) { |
| container.innerHTML = ` |
| <div class="system-status"> |
| <div class="system-status-dot" style="background:#ff3b30;"></div> |
| <span style="flex:1;color:var(--error);">Backend Offline</span> |
| <span style="font-size:11px;color:var(--error);">Start server first</span> |
| </div> |
| `; |
| } |
| } |
| |
| |
| async function loadStats() { |
| try { |
| const res = await fetch('/api/performance/default'); |
| const data = await res.json(); |
| document.getElementById('stat-sessions').textContent = data.overall_stats?.total_sessions || 0; |
| const avg = data.overall_stats?.avg_engagement; |
| document.getElementById('stat-engagement').textContent = avg ? avg.toFixed(1) + '%' : 'β'; |
| } catch (e) { |
| |
| } |
| } |
| |
| checkSystemStatus(); |
| loadStats(); |
| setInterval(checkSystemStatus, 15000); |
| </script> |
| </body> |
|
|
| </html> |