(function () { 'use strict'; const HF_SPACE = 'wss://zeetay-anomaly-detection.hf.space'; const useSameHost = location.hostname.endsWith('.hf.space') || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; const WS_URL = (useSameHost ? (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host : HF_SPACE) + '/ws'; const MAX_POINTS = 200; const THRESHOLD = 0.60; // DOM refs const statusEl = document.getElementById('status'); const phaseBadgeEl = document.getElementById('phase-badge'); const phaseDescEl = document.getElementById('phase-desc'); const obsCountEl = document.getElementById('obs-count'); const totalObsEl = document.getElementById('total-obs'); const totalAnomaliesEl = document.getElementById('total-anomalies'); const totalDriftEl = document.getElementById('total-drift'); const precisionEl = document.getElementById('precision'); const recallEl = document.getElementById('recall'); const f1El = document.getElementById('f1'); // Rolling buffer of the last MAX_POINTS messages const buffer = []; // ── helpers ───────────────────────────────────────────────────────────────── function setConnected(ok) { statusEl.textContent = ok ? 'Connected' : 'Disconnected'; statusEl.className = ok ? 'connected' : 'disconnected'; } function updatePhase(phase) { const labels = { A: 'Phase A', B: 'Phase B', C: 'Phase C' }; const descs = { A: 'building baseline', B: 'sensors drifting', C: 'anomalies active' }; phaseBadgeEl.textContent = labels[phase] || phase; phaseBadgeEl.className = 'phase-badge phase-' + phase; if (phaseDescEl) phaseDescEl.textContent = descs[phase] || ''; } function updateCounters(msg) { if (msg.total_anomalies_detected != null) totalAnomaliesEl.textContent = msg.total_anomalies_detected; if (msg.total_drift_events != null) totalDriftEl.textContent = msg.total_drift_events; const obs = (msg.observation_index != null) ? msg.observation_index + 1 : 0; totalObsEl.textContent = obs; obsCountEl.textContent = obs + ' observations'; precisionEl.textContent = (msg.running_precision ?? 0).toFixed(2); recallEl.textContent = (msg.running_recall ?? 0).toFixed(2); f1El.textContent = (msg.running_f1 ?? 0).toFixed(2); if (msg.phase) updatePhase(msg.phase); } // ── shared drift-line plugin ───────────────────────────────────────────────── function makeDriftPlugin() { return { id: 'driftLines', afterDraw: function (chart) { if (!chart.chartArea) return; var driftPts = buffer.filter(function (p) { return p.drift_event; }); if (!driftPts.length) return; var xScale = chart.scales && chart.scales.x; if (!xScale) return; var ctx = chart.ctx; var area = chart.chartArea; ctx.save(); ctx.setLineDash([4, 4]); ctx.strokeStyle = '#eab308'; ctx.lineWidth = 1.5; driftPts.forEach(function (p) { var x = xScale.getPixelForValue(p.index); if (x >= area.left && x <= area.right) { ctx.beginPath(); ctx.moveTo(x, area.top); ctx.lineTo(x, area.bottom); ctx.stroke(); } }); ctx.restore(); } }; } // ── sensor chart ───────────────────────────────────────────────────────────── var sensorChart = new Chart(document.getElementById('chart-sensors').getContext('2d'), { type: 'line', data: { labels: [], datasets: [ { label: 'Temperature', data: [], borderColor: '#38bdf8', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 }, { label: 'Pressure', data: [], borderColor: '#a78bfa', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 }, { label: 'Vibration', data: [], borderColor: '#34d399', backgroundColor: 'transparent', fill: false, tension: 0.15, pointRadius: 0, borderWidth: 2 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, animation: false, scales: { x: { display: true, title: { display: true, text: 'Observation index', color: '#a1a1aa', font: { family: "'DM Sans', system-ui", size: 11 } }, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#71717a', maxTicksLimit: 12 } }, y: { display: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#71717a' } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { afterBody: function (items) { var idx = items[0] && items[0].dataIndex; var p = buffer[idx]; if (!p) return ''; var lines = []; if (p.alert) lines.push('Alert: YES'); if (p.label && p.label !== 'normal') lines.push('Ground truth: ' + p.label.replace('_', ' ')); return lines; } } } } }, plugins: [makeDriftPlugin()] }); // ── anomaly score chart ─────────────────────────────────────────────────────── var thresholdPlugin = { id: 'thresholdLine', afterDraw: function (chart) { if (!chart.chartArea) return; var yScale = chart.scales && chart.scales.y; if (!yScale) return; var y = yScale.getPixelForValue(THRESHOLD); var ctx = chart.ctx; var area = chart.chartArea; ctx.save(); ctx.setLineDash([6, 3]); ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(area.left, y); ctx.lineTo(area.right, y); ctx.stroke(); ctx.restore(); } }; var scoreChart = new Chart(document.getElementById('chart-score').getContext('2d'), { type: 'line', data: { labels: [], datasets: [ { label: 'Anomaly score', data: [], borderColor: '#6366f1', backgroundColor: 'rgba(99,102,241,0.08)', fill: true, tension: 0.2, pointRadius: 0, borderWidth: 1.5 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, animation: false, scales: { x: { display: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#71717a', maxTicksLimit: 12 } }, y: { display: true, min: 0, max: 1, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#71717a', stepSize: 0.25 } } }, plugins: { legend: { display: false } } }, plugins: [makeDriftPlugin(), thresholdPlugin] }); // ── buffer + redraw ─────────────────────────────────────────────────────────── function pushPoint(msg) { buffer.push({ index: msg.observation_index, temperature: msg.temperature, pressure: msg.pressure, vibration: msg.vibration, anomaly_score: msg.anomaly_score, alert: msg.alert, drift_event: msg.drift_event, label: msg.label, phase: msg.phase }); if (buffer.length > MAX_POINTS) buffer.shift(); } function redrawCharts() { if (!buffer.length) return; var labels = buffer.map(function (p) { return p.index; }); var tempData = buffer.map(function (p) { return p.temperature; }); var pressData = buffer.map(function (p) { return p.pressure; }); var vibeData = buffer.map(function (p) { return p.vibration; }); var scoreData = buffer.map(function (p) { return p.anomaly_score; }); var alertRadius = buffer.map(function (p) { return p.alert ? 5 : 0; }); var alertColor = buffer.map(function (p) { return p.alert ? '#ef4444' : 'transparent'; }); // Sensor chart sensorChart.data.labels = labels; sensorChart.data.datasets[0].data = tempData; sensorChart.data.datasets[1].data = pressData; sensorChart.data.datasets[2].data = vibeData; [0, 1, 2].forEach(function (i) { sensorChart.data.datasets[i].pointRadius = alertRadius; sensorChart.data.datasets[i].pointBackgroundColor = alertColor; sensorChart.data.datasets[i].pointBorderColor = alertColor; }); sensorChart.update('none'); // Score chart scoreChart.data.labels = labels; scoreChart.data.datasets[0].data = scoreData; scoreChart.data.datasets[0].pointRadius = alertRadius; scoreChart.data.datasets[0].pointBackgroundColor = alertColor; scoreChart.update('none'); } // ── WebSocket ───────────────────────────────────────────────────────────────── function connect() { var ws = new WebSocket(WS_URL); ws.onopen = function () { setConnected(true); }; ws.onclose = function () { setConnected(false); setTimeout(connect, 2000); }; ws.onerror = function () { setConnected(false); }; ws.onmessage = function (ev) { try { var msg = JSON.parse(ev.data); updateCounters(msg); pushPoint(msg); redrawCharts(); } catch (e) { /* ignore parse errors */ } }; } // ── accordion ──────────────────────────────────────────────────────────────── (function () { var section = document.getElementById('explainer'); var toggle = document.getElementById('explainer-toggle'); if (section && toggle) { toggle.addEventListener('click', function () { section.classList.toggle('open'); }); } })(); connect(); })();