From 5708b9eb98ad3bd8ce509388f8425d79e19a6e28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:28:34 +0000 Subject: [PATCH 1/5] Initial plan From fce197ecc1270ce4cd17cb70dbb5f0a9167d2ddc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:36:52 +0000 Subject: [PATCH 2/5] Major UI/UX improvements and bug fixes for NightscoutAI Co-authored-by: code2344 <71059013+code2344@users.noreply.github.com> --- README.md | 114 +++++++++ index.html | 694 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 728 insertions(+), 80 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..918203e --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# 🩸 NightscoutAI - Blood Glucose Prediction Dashboard + +An AI-powered blood glucose prediction tool that trains entirely in your browser using Nightscout CGM data. + +![NightscoutAI Dashboard](https://via.placeholder.com/800x400?text=NightscoutAI+Dashboard) + +## ✨ Features + +- **🤖 Real-time AI Training**: LSTM neural network trains on your Nightscout data directly in the browser +- **📊 Interactive Charts**: Beautiful visualizations of actual vs predicted blood glucose levels +- **💾 Model Persistence**: Save and load trained models locally in your browser +- **🎮 Demo Mode**: Test the application with sample data when Nightscout API is unavailable +- **📱 Responsive Design**: Works on desktop, tablet, and mobile devices +- **🔐 Privacy-First**: All data processing happens locally - no data sent to external servers +- **⚡ Real-time Predictions**: Get instant blood glucose predictions based on historical patterns + +## 🚀 Quick Start + +1. **Open the Application**: Simply open `index.html` in a modern web browser +2. **Enter Your Nightscout URL**: Input your Nightscout site URL (e.g., `https://yoursite.herokuapp.com`) +3. **Fetch & Train**: Click the "🔄 Fetch & Train" button to download data and train the AI model +4. **View Predictions**: See your current BG and AI-predicted next reading + +### Demo Mode + +If you don't have access to a Nightscout site or want to test the application: +1. Click the "🎮 Demo Mode" button +2. The app will use sample blood glucose data to demonstrate functionality + +## 🔧 Technical Details + +### AI Model Architecture +- **Model Type**: LSTM (Long Short-Term Memory) Neural Network +- **Input Features**: Blood glucose, insulin doses, carbohydrate intake +- **Sequence Length**: 10 time points for pattern recognition +- **Training**: Online learning with each data update + +### Technologies Used +- **TensorFlow.js**: Machine learning in the browser +- **Chart.js**: Interactive data visualizations +- **Vanilla JavaScript**: Lightweight, no framework dependencies +- **HTML5/CSS3**: Modern responsive design + +### Data Processing +- Fetches data from Nightscout API (`/api/v1/entries.json`) +- Normalizes blood glucose values (40-400 mg/dL range) +- Filters invalid readings and sorts by timestamp +- Creates sliding windows for sequence-based learning + +## 📊 Statistics Dashboard + +The application provides comprehensive training statistics: +- **Average Loss**: Model training loss (lower is better) +- **Prediction Error**: Average prediction accuracy in mg/dL +- **Training Steps**: Total number of training iterations +- **Data Points**: Total CGM readings processed + +## 💡 Usage Tips + +1. **Data Quality**: More historical data generally leads to better predictions +2. **Regular Updates**: Train periodically with new data for improved accuracy +3. **Model Saving**: Save your trained model to avoid retraining each session +4. **Browser Compatibility**: Works best in Chrome, Firefox, Safari, and Edge + +## ⚠️ Important Notes + +- **Not Medical Advice**: This tool is for educational/research purposes only +- **Supplement, Don't Replace**: Should complement, not replace, medical monitoring +- **Data Privacy**: All processing happens locally in your browser +- **Internet Required**: Needs internet connection to fetch Nightscout data (except demo mode) + +## 🔧 Configuration + +### Adjustable Parameters (in code) +```javascript +const SEQ_LEN = 10; // Sequence length for predictions +const LSTM_UNITS = 16; // Neural network complexity +const BG_MIN = 40; // Minimum BG value for normalization +const BG_MAX = 400; // Maximum BG value for normalization +``` + +## 🐛 Troubleshooting + +**Issue**: External libraries not loading +- **Solution**: The app includes fallback CDNs and demo mode for offline testing + +**Issue**: No data from Nightscout +- **Solution**: Verify your Nightscout URL is correct and accessible + +**Issue**: Poor prediction accuracy +- **Solution**: Ensure sufficient historical data (at least 100+ readings) + +## 🤝 Contributing + +Contributions are welcome! Some areas for improvement: +- Enhanced model architectures +- Additional input features (exercise, stress, sleep) +- Mobile app version +- Advanced visualization options +- Multi-step ahead predictions + +## 📄 License + +This project is open source. Please ensure compliance with medical device regulations in your jurisdiction. + +## 🙏 Acknowledgments + +- Nightscout community for the open CGM platform +- TensorFlow.js team for browser-based machine learning +- Chart.js for excellent visualization capabilities + +--- + +**⚠️ Medical Disclaimer**: This software is not intended for medical diagnosis or treatment. Always consult healthcare professionals for medical decisions. \ No newline at end of file diff --git a/index.html b/index.html index 1767485..018b524 100644 --- a/index.html +++ b/index.html @@ -4,66 +4,413 @@ Nightscout BG Predictor Dashboard - - + + + -

Nightscout BG Predictor Dashboard

- -

Latest BG: -

-

Next predicted BG: -

- -
-

Average Loss: -

-

Average Prediction Error: -

-

Total Training Steps: 0

-

Sequence Length: -

-

LSTM Units: -

-

Total Data Points: -

-
+
+

🩸 Nightscout AI Predictor

+ +
+ + + + +
+ +
+
+ +
+
+

Loading and training model...

+
- - +
+
+
Current BG
+
-
+
mg/dL
+
+
+
Predicted BG
+
-
+
mg/dL
+
+
-
- +
+
+
Average Loss
+
-
+
+
+
Prediction Error
+
-
+
+
+
Training Steps
+
0
+
+
+
Sequence Length
+
-
+
+
+
LSTM Units
+
-
+
+
+
Total Data Points
+
-
+
+
+ +
+ +
From a198354ef41d1c50ea5d1d25b86a89c474515690 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:46:56 +0000 Subject: [PATCH 3/5] Add mmol/L unit support with toggle and enhance historical data training Co-authored-by: code2344 <71059013+code2344@users.noreply.github.com> --- index.html | 123 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/index.html b/index.html index 018b524..1e04f7e 100644 --- a/index.html +++ b/index.html @@ -201,6 +201,7 @@

🩸 Nightscout AI Predictor

+ @@ -218,12 +219,12 @@

🩸 Nightscout AI Predictor

Current BG
-
-
mg/dL
+
mg/dL
Predicted BG
-
-
mg/dL
+
mg/dL
@@ -279,7 +280,13 @@

🩸 Nightscout AI Predictor

const SEQ_LEN = 10; // sliding window length const FEATURES = 3; // [bg, insulin, carbs] const LSTM_UNITS = 16; -const BG_MIN = 40, BG_MAX = 400; // for normalization +const BG_MIN_MGDL = 40, BG_MAX_MGDL = 400; // for normalization in mg/dL +const BG_MIN_MMOL = 2.2, BG_MAX_MMOL = 22.2; // for normalization in mmol/L +const MGDL_TO_MMOL = 0.0555; // Conversion factor: mg/dL to mmol/L +const MMOL_TO_MGDL = 18.016; // Conversion factor: mmol/L to mg/dL + +// Unit preferences +let currentUnit = 'mgdl'; // 'mgdl' or 'mmol' // Check if TensorFlow.js is available, if not use demo mode function initializeApp() { @@ -390,11 +397,11 @@

🩸 Nightscout AI Predictor

}, y: { display: true, - suggestedMin: 40, - suggestedMax: 400, + suggestedMin: currentUnit === 'mmol' ? 2 : 40, + suggestedMax: currentUnit === 'mmol' ? 22 : 400, title: { display: true, - text: 'Blood Glucose (mg/dL)' + text: `Blood Glucose (${getCurrentUnitLabel()})` }, grid: { color: 'rgba(0,0,0,0.1)' @@ -413,8 +420,40 @@

🩸 Nightscout AI Predictor

} // HELPERS -function normalizeBG(bg){ return (bg-BG_MIN)/(BG_MAX-BG_MIN); } -function denormalizeBG(norm){ return norm*(BG_MAX-BG_MIN)+BG_MIN; } +function mgdlToMmol(mgdl) { return mgdl * MGDL_TO_MMOL; } +function mmolToMgdl(mmol) { return mmol * MMOL_TO_MGDL; } + +function normalizeBG(bg) { + if (currentUnit === 'mmol') { + return (bg - BG_MIN_MMOL) / (BG_MAX_MMOL - BG_MIN_MMOL); + } else { + return (bg - BG_MIN_MGDL) / (BG_MAX_MGDL - BG_MIN_MGDL); + } +} + +function denormalizeBG(norm) { + if (currentUnit === 'mmol') { + return norm * (BG_MAX_MMOL - BG_MIN_MMOL) + BG_MIN_MMOL; + } else { + return norm * (BG_MAX_MGDL - BG_MIN_MGDL) + BG_MIN_MGDL; + } +} + +function convertBgToCurrentUnit(bgMgdl) { + return currentUnit === 'mmol' ? mgdlToMmol(bgMgdl) : bgMgdl; +} + +function formatBgValue(bg, unit) { + if (unit === 'mmol') { + return bg.toFixed(1); + } else { + return Math.round(bg).toString(); + } +} + +function getCurrentUnitLabel() { + return currentUnit === 'mmol' ? 'mmol/L' : 'mg/dL'; +} function addReading(reading){ sequenceBuffer.push(reading); if(sequenceBuffer.length > SEQ_LEN) sequenceBuffer.shift(); @@ -439,8 +478,10 @@

🩸 Nightscout AI Predictor

const totalTrainingSteps = seq.length - SEQ_LEN + 1; for(let i=0;i<=seq.length-SEQ_LEN;i++){ - const inputSeq = seq.slice(i,i+SEQ_LEN-1).map(r=>[normalizeBG(r[0]), r[1], r[2]]); - const targetBG = normalizeBG(seq[i+SEQ_LEN-1][0]); + // Convert BG values to current unit for training + const bgInCurrentUnit = convertBgToCurrentUnit(seq[i+SEQ_LEN-1][0]); + const inputSeq = seq.slice(i,i+SEQ_LEN-1).map(r=>[normalizeBG(convertBgToCurrentUnit(r[0])), r[1], r[2]]); + const targetBG = normalizeBG(bgInCurrentUnit); const X = bufferToTensor(inputSeq); const Y = tf.tensor([[targetBG]]); @@ -448,7 +489,7 @@

🩸 Nightscout AI Predictor

const loss = await model.trainOnBatch(X,Y); totalLoss += loss; const pred = denormalizeBG(model.predict(X).dataSync()[0]); - totalPredError += Math.abs(pred - seq[i+SEQ_LEN-1][0]); + totalPredError += Math.abs(pred - bgInCurrentUnit); count++; trainedCount++; } catch (error) { @@ -470,15 +511,17 @@

🩸 Nightscout AI Predictor

if (isDemoMode) { // Simple prediction for demo: average of last few readings with some trend if (sequenceBuffer.length < 3) return null; - const recent = sequenceBuffer.slice(-3).map(r => r[0]); + const recent = sequenceBuffer.slice(-3).map(r => convertBgToCurrentUnit(r[0])); const avg = recent.reduce((a, b) => a + b) / recent.length; const trend = (recent[recent.length - 1] - recent[0]) / recent.length; - return Math.max(60, Math.min(300, avg + trend * 2)); + const minBg = currentUnit === 'mmol' ? 3.3 : 60; + const maxBg = currentUnit === 'mmol' ? 16.7 : 300; + return Math.max(minBg, Math.min(maxBg, avg + trend * 2)); } if(sequenceBuffer.length < SEQ_LEN) return null; try { - const inputSeq = sequenceBuffer.slice(0,SEQ_LEN-1).map(r=>[normalizeBG(r[0]), r[1], r[2]]); + const inputSeq = sequenceBuffer.slice(0,SEQ_LEN-1).map(r=>[normalizeBG(convertBgToCurrentUnit(r[0])), r[1], r[2]]); const X = bufferToTensor(inputSeq); const predNorm = model.predict(X).dataSync()[0]; X.dispose(); @@ -492,13 +535,21 @@

🩸 Nightscout AI Predictor

// UPDATE UI function updateStatsUI(totalPoints){ document.getElementById('avg-loss').textContent = (count>0? (totalLoss/count).toFixed(4) : '-'); - document.getElementById('avg-error').textContent = (count>0? (totalPredError/count).toFixed(2) + ' mg/dL' : '-'); + const errorText = count>0? (totalPredError/count).toFixed(2) + ' ' + getCurrentUnitLabel() : '-'; + document.getElementById('avg-error').textContent = errorText; document.getElementById('train-steps').textContent = count.toLocaleString(); document.getElementById('seq-len').textContent = SEQ_LEN; document.getElementById('lstm-units').textContent = LSTM_UNITS; document.getElementById('total-data').textContent = totalPoints.toLocaleString(); } +function updateUnitLabels() { + const unitLabel = getCurrentUnitLabel(); + document.getElementById('current-unit-label').textContent = unitLabel; + document.getElementById('predicted-unit-label').textContent = unitLabel; + document.getElementById('unit-toggle').textContent = `🔄 ${unitLabel}`; +} + // FETCH ALL NIGHTSCOUT DATA async function fetchAllData(){ try{ @@ -561,12 +612,12 @@

🩸 Nightscout AI Predictor

await trainOnSequence(allData); - const latestBG = allData[allData.length-1][0]; - document.getElementById('latest-bg').textContent = Math.round(latestBG); + const latestBG = convertBgToCurrentUnit(allData[allData.length-1][0]); + document.getElementById('latest-bg').textContent = formatBgValue(latestBG, currentUnit); const nextBG = predictNext(); if(nextBG !== null) { - document.getElementById('predicted-bg').textContent = Math.round(nextBG); + document.getElementById('predicted-bg').textContent = formatBgValue(nextBG, currentUnit); } else { document.getElementById('predicted-bg').textContent = 'Error'; } @@ -581,23 +632,27 @@

🩸 Nightscout AI Predictor

}); chart.data.labels = timeLabels; - chart.data.datasets[0].data = allData.map(d=>Math.round(d[0])); + chart.data.datasets[0].data = allData.map(d=>parseFloat(formatBgValue(convertBgToCurrentUnit(d[0]), currentUnit))); const preds = []; if (isDemoMode) { // Simple predictions for demo mode for(let i=SEQ_LEN-1;i r[0]); + const recent = allData.slice(Math.max(0, i-2), i+1).map(r => convertBgToCurrentUnit(r[0])); const avg = recent.reduce((a, b) => a + b) / recent.length; const trend = recent.length > 1 ? (recent[recent.length - 1] - recent[0]) / recent.length : 0; - preds.push(Math.round(Math.max(60, Math.min(300, avg + trend * 1.5)))); + const minBg = currentUnit === 'mmol' ? 3.3 : 60; + const maxBg = currentUnit === 'mmol' ? 16.7 : 300; + const predValue = Math.max(minBg, Math.min(maxBg, avg + trend * 1.5)); + preds.push(parseFloat(formatBgValue(predValue, currentUnit))); } } else { for(let i=SEQ_LEN-1;i[normalizeBG(r[0]), r[1], r[2]]); + const inputSeq = allData.slice(i-(SEQ_LEN-1),i).map(r=>[normalizeBG(convertBgToCurrentUnit(r[0])), r[1], r[2]]); const X = bufferToTensor(inputSeq); - preds.push(Math.round(denormalizeBG(model.predict(X).dataSync()[0]))); + const predValue = denormalizeBG(model.predict(X).dataSync()[0]); + preds.push(parseFloat(formatBgValue(predValue, currentUnit))); X.dispose(); } catch (error) { preds.push(null); @@ -605,6 +660,11 @@

🩸 Nightscout AI Predictor

} } chart.data.datasets[1].data = Array(SEQ_LEN-1).fill(null).concat(preds); + + // Update chart y-axis range and title + chart.options.scales.y.suggestedMin = currentUnit === 'mmol' ? 2 : 40; + chart.options.scales.y.suggestedMax = currentUnit === 'mmol' ? 22 : 400; + chart.options.scales.y.title.text = `Blood Glucose (${getCurrentUnitLabel()})`; chart.update(); } @@ -656,6 +716,16 @@

🩸 Nightscout AI Predictor

document.getElementById('update-btn').addEventListener('click', updateUI); document.getElementById('save-btn').addEventListener('click', saveModel); document.getElementById('load-btn').addEventListener('click', loadModel); +document.getElementById('unit-toggle').addEventListener('click', function() { + currentUnit = currentUnit === 'mgdl' ? 'mmol' : 'mgdl'; + localStorage.setItem('bg-unit', currentUnit); + updateUnitLabels(); + // Reset training stats when switching units + totalLoss = 0; + totalPredError = 0; + count = 0; + showSuccess(`Switched to ${getCurrentUnitLabel()}. Please retrain the model for accurate predictions.`); +}); // URL validation document.getElementById('nightscout-url').addEventListener('input', function(e) { @@ -666,6 +736,13 @@

🩸 Nightscout AI Predictor

// Initialize UI document.addEventListener('DOMContentLoaded', function() { + // Load saved unit preference + const savedUnit = localStorage.getItem('bg-unit'); + if (savedUnit && (savedUnit === 'mgdl' || savedUnit === 'mmol')) { + currentUnit = savedUnit; + } + updateUnitLabels(); + // Initialize the app setTimeout(initializeApp, 100); // Give time for external scripts to load From ec6c8fd6916a8273de77f62b04f0eb9bb1e2126c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:57:37 +0000 Subject: [PATCH 4/5] Add continuous training and anomaly detection features Co-authored-by: code2344 <71059013+code2344@users.noreply.github.com> --- index.html | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 1e04f7e..c8dcbf8 100644 --- a/index.html +++ b/index.html @@ -203,6 +203,7 @@

🩸 Nightscout AI Predictor

+
@@ -253,6 +254,10 @@

🩸 Nightscout AI Predictor

Total Data Points
-
+
+
Anomalies Detected
+
0
+
@@ -288,6 +293,16 @@

🩸 Nightscout AI Predictor

// Unit preferences let currentUnit = 'mgdl'; // 'mgdl' or 'mmol' +// Auto-training settings +let autoTrainEnabled = false; +let autoTrainInterval = null; +const AUTO_TRAIN_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +// Anomaly detection +let detectedAnomalies = []; +const ANOMALY_THRESHOLD_MGDL = 100; // mg/dL change threshold +const ANOMALY_THRESHOLD_MMOL = 5.5; // mmol/L change threshold + // Check if TensorFlow.js is available, if not use demo mode function initializeApp() { if (typeof tf === 'undefined') { @@ -451,6 +466,93 @@

🩸 Nightscout AI Predictor

} } +// ANOMALY DETECTION +function detectAnomalies(data) { + if (!data || data.length < 3) return []; + + const anomalies = []; + const threshold = currentUnit === 'mmol' ? ANOMALY_THRESHOLD_MMOL : ANOMALY_THRESHOLD_MGDL; + + for (let i = 1; i < data.length - 1; i++) { + const prev = convertBgToCurrentUnit(data[i-1][0]); + const curr = convertBgToCurrentUnit(data[i][0]); + const next = convertBgToCurrentUnit(data[i+1][0]); + + // Check for sudden spike/drop that immediately reverses + const dropThenRise = (prev - curr > threshold) && (next - curr > threshold * 0.8); + const riseThenDrop = (curr - prev > threshold) && (curr - next > threshold * 0.8); + + // Check for extreme values that are outliers + const isExtreme = curr < (currentUnit === 'mmol' ? 2.2 : 40) || curr > (currentUnit === 'mmol' ? 22.2 : 400); + + // Check for impossible rate of change (>5 mmol/L or >90 mg/dL per 5 minutes) + const timeDiffMinutes = (data[i][3] - data[i-1][3]) / (1000 * 60); + const rateOfChange = Math.abs(curr - prev) / Math.max(timeDiffMinutes / 5, 1); + const impossibleRate = rateOfChange > (currentUnit === 'mmol' ? 5 : 90); + + if (dropThenRise || riseThenDrop || isExtreme || impossibleRate) { + anomalies.push({ + index: i, + timestamp: data[i][3], + value: curr, + type: dropThenRise ? 'spike-down' : riseThenDrop ? 'spike-up' : isExtreme ? 'extreme' : 'rate', + reason: dropThenRise ? 'Sudden drop then recovery' : + riseThenDrop ? 'Sudden rise then drop' : + isExtreme ? 'Extreme value' : 'Impossible rate of change' + }); + } + } + + return anomalies; +} + +function filterAnomalies(data, anomalies) { + if (!anomalies || anomalies.length === 0) return data; + + const anomalyIndices = new Set(anomalies.map(a => a.index)); + return data.filter((_, index) => !anomalyIndices.has(index)); +} + +// AUTO-TRAINING FUNCTIONS +function startAutoTraining() { + if (autoTrainInterval) { + clearInterval(autoTrainInterval); + } + + autoTrainInterval = setInterval(async () => { + if (!document.getElementById('update-btn').disabled) { + console.log('Auto-training: Fetching new data...'); + try { + await updateUI(); + console.log('Auto-training: Successfully updated model'); + } catch (error) { + console.error('Auto-training error:', error); + } + } + }, AUTO_TRAIN_INTERVAL_MS); + + showSuccess(`Auto-training enabled! Model will update every ${AUTO_TRAIN_INTERVAL_MS / 60000} minutes.`); +} + +function stopAutoTraining() { + if (autoTrainInterval) { + clearInterval(autoTrainInterval); + autoTrainInterval = null; + } + showSuccess('Auto-training disabled.'); +} + +function updateAutoTrainButton() { + const button = document.getElementById('auto-train-toggle'); + if (autoTrainEnabled) { + button.textContent = '⏰ Auto-Train: ON'; + button.style.background = 'linear-gradient(135deg, #16a34a 0%, #15803d 100%)'; + } else { + button.textContent = '⏰ Auto-Train: OFF'; + button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + } +} + function getCurrentUnitLabel() { return currentUnit === 'mmol' ? 'mmol/L' : 'mg/dL'; } @@ -541,6 +643,7 @@

🩸 Nightscout AI Predictor

document.getElementById('seq-len').textContent = SEQ_LEN; document.getElementById('lstm-units').textContent = LSTM_UNITS; document.getElementById('total-data').textContent = totalPoints.toLocaleString(); + document.getElementById('anomalies-count').textContent = detectedAnomalies.length.toLocaleString(); } function updateUnitLabels() { @@ -565,7 +668,17 @@

🩸 Nightscout AI Predictor

async function fetchAllData(){ if (isDemoMode) { showSuccess('Using demo data for testing'); - return DEMO_DATA; + // Detect anomalies in demo data + detectedAnomalies = detectAnomalies(DEMO_DATA); + + // Filter out anomalies for training (but keep them for display) + const cleanData = filterAnomalies(DEMO_DATA, detectedAnomalies); + + if (detectedAnomalies.length > 0) { + console.log(`Detected ${detectedAnomalies.length} potential anomalies in demo data:`, detectedAnomalies); + } + + return cleanData; } try{ @@ -586,9 +699,21 @@

🩸 Nightscout AI Predictor

throw new Error('No data received from Nightscout API'); } - return data.map(e=>[e.sgv, e.insulin||0, e.carbs||0, new Date(e.dateString || e.date).getTime()]) + const processedData = data.map(e=>[e.sgv, e.insulin||0, e.carbs||0, new Date(e.dateString || e.date).getTime()]) .filter(e => !isNaN(e[0]) && e[0] > 0 && e[0] < 1000) // filter invalid BG readings .sort((a,b)=>a[3]-b[3]); // sort by timestamp + + // Detect anomalies + detectedAnomalies = detectAnomalies(processedData); + + // Filter out anomalies for training (but keep them for display) + const cleanData = filterAnomalies(processedData, detectedAnomalies); + + if (detectedAnomalies.length > 0) { + console.log(`Detected ${detectedAnomalies.length} potential anomalies:`, detectedAnomalies); + } + + return cleanData; }catch(err){ console.error("Error fetching Nightscout:", err); throw err; @@ -668,7 +793,11 @@

🩸 Nightscout AI Predictor

chart.update(); } - showSuccess(`Successfully processed ${allData.length} data points and trained the model!`); + if (detectedAnomalies.length > 0) { + showSuccess(`Successfully processed ${allData.length} data points, detected ${detectedAnomalies.length} anomalies, and trained the model!`); + } else { + showSuccess(`Successfully processed ${allData.length} data points and trained the model!`); + } } catch (error) { showError(`Error: ${error.message}`); @@ -716,10 +845,54 @@

🩸 Nightscout AI Predictor

document.getElementById('update-btn').addEventListener('click', updateUI); document.getElementById('save-btn').addEventListener('click', saveModel); document.getElementById('load-btn').addEventListener('click', loadModel); +document.getElementById('auto-train-toggle').addEventListener('click', function() { + autoTrainEnabled = !autoTrainEnabled; + localStorage.setItem('auto-train', autoTrainEnabled); + updateAutoTrainButton(); + + if (autoTrainEnabled) { + startAutoTraining(); + } else { + stopAutoTraining(); + } +}); document.getElementById('unit-toggle').addEventListener('click', function() { currentUnit = currentUnit === 'mgdl' ? 'mmol' : 'mgdl'; localStorage.setItem('bg-unit', currentUnit); updateUnitLabels(); + + // Update displayed values if we have data + const latestBgEl = document.getElementById('latest-bg'); + const predictedBgEl = document.getElementById('predicted-bg'); + + if (latestBgEl.textContent !== '-' && !isNaN(parseFloat(latestBgEl.textContent))) { + // Convert and update current BG display + let currentBgValue = parseFloat(latestBgEl.textContent); + // If switching from mg/dL to mmol/L, convert + if (currentUnit === 'mmol' && currentBgValue > 30) { + currentBgValue = mgdlToMmol(currentBgValue); + } + // If switching from mmol/L to mg/dL, convert + else if (currentUnit === 'mgdl' && currentBgValue < 30) { + currentBgValue = mmolToMgdl(currentBgValue); + } + latestBgEl.textContent = formatBgValue(currentBgValue, currentUnit); + } + + if (predictedBgEl.textContent !== '-' && !isNaN(parseFloat(predictedBgEl.textContent))) { + // Convert and update predicted BG display + let predictedBgValue = parseFloat(predictedBgEl.textContent); + // If switching from mg/dL to mmol/L, convert + if (currentUnit === 'mmol' && predictedBgValue > 30) { + predictedBgValue = mgdlToMmol(predictedBgValue); + } + // If switching from mmol/L to mg/dL, convert + else if (currentUnit === 'mgdl' && predictedBgValue < 30) { + predictedBgValue = mmolToMgdl(predictedBgValue); + } + predictedBgEl.textContent = formatBgValue(predictedBgValue, currentUnit); + } + // Reset training stats when switching units totalLoss = 0; totalPredError = 0; @@ -743,6 +916,13 @@

🩸 Nightscout AI Predictor

} updateUnitLabels(); + // Load saved auto-train preference + const savedAutoTrain = localStorage.getItem('auto-train'); + if (savedAutoTrain === 'true') { + autoTrainEnabled = true; + } + updateAutoTrainButton(); + // Initialize the app setTimeout(initializeApp, 100); // Give time for external scripts to load @@ -770,6 +950,11 @@

🩸 Nightscout AI Predictor

updateUI(); }); document.querySelector('.controls').appendChild(demoBtn); + + // Start auto-training if enabled + if (autoTrainEnabled) { + startAutoTraining(); + } }); // OPTIONAL: auto-update every 5 min (disabled by default to avoid API spam) From ac6ddd1ccb5af57dde4858308b00ba8568f89aa6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 Aug 2025 06:04:58 +0000 Subject: [PATCH 5/5] Fix chart rendering by implementing fallback canvas chart when Chart.js CDN is blocked Co-authored-by: code2344 <71059013+code2344@users.noreply.github.com> --- index.html | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 207 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index c8dcbf8..c9aebb7 100644 --- a/index.html +++ b/index.html @@ -351,8 +351,8 @@

🩸 Nightscout AI Predictor

// CHART SETUP function initializeChart() { if (typeof Chart === 'undefined') { - console.log('Chart.js not available, charts disabled'); - document.getElementById('chart-container').innerHTML = '

Charts not available - external libraries not loaded

'; + console.log('Chart.js not available, using fallback canvas chart'); + createFallbackChart(); return; } @@ -434,6 +434,204 @@

🩸 Nightscout AI Predictor

document.getElementById('chart-container').style.height = '400px'; } +// FALLBACK CHART IMPLEMENTATION +function createFallbackChart() { + const container = document.getElementById('chart-container'); + container.innerHTML = ` +
+

Blood Glucose Prediction Chart

+
+ +
+ Actual BG +
+ +
+ Predicted BG +
+
+ +
+ `; + + // Initialize fallback chart object + chart = { + data: { labels: [], datasets: [{ data: [] }, { data: [] }] }, + update: updateFallbackChart, + options: { scales: { y: { suggestedMin: 40, suggestedMax: 400 } } } + }; + + container.style.height = '500px'; +} + +function updateFallbackChart() { + const canvas = document.getElementById('fallback-chart'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const padding = 60; + const chartWidth = width - padding * 2; + const chartHeight = height - padding * 2; + + // Clear canvas + ctx.clearRect(0, 0, width, height); + + // Get data + const actualData = chart.data.datasets[0].data; + const predictedData = chart.data.datasets[1].data; + const labels = chart.data.labels; + + if (!actualData || actualData.length === 0) { + ctx.fillStyle = '#64748b'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('No data available yet. Click "Demo Mode" or "Fetch & Train" to see the chart.', width/2, height/2); + return; + } + + // Determine Y axis range + const allValues = [...actualData, ...predictedData.filter(v => v !== null)]; + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const yMin = Math.max(0, minValue - (maxValue - minValue) * 0.1); + const yMax = maxValue + (maxValue - minValue) * 0.1; + + // Draw background + ctx.fillStyle = '#f8fafc'; + ctx.fillRect(padding, padding, chartWidth, chartHeight); + + // Draw grid lines + ctx.strokeStyle = '#e2e8f0'; + ctx.lineWidth = 1; + + // Horizontal grid lines + for (let i = 0; i <= 5; i++) { + const y = padding + (i / 5) * chartHeight; + ctx.beginPath(); + ctx.moveTo(padding, y); + ctx.lineTo(padding + chartWidth, y); + ctx.stroke(); + + // Y axis labels + const value = yMax - (i / 5) * (yMax - yMin); + ctx.fillStyle = '#64748b'; + ctx.font = '12px Arial'; + ctx.textAlign = 'right'; + ctx.fillText(Math.round(value).toString(), padding - 10, y + 4); + } + + // Vertical grid lines + const stepSize = Math.max(1, Math.floor(actualData.length / 10)); + for (let i = 0; i < actualData.length; i += stepSize) { + const x = padding + (i / (actualData.length - 1)) * chartWidth; + ctx.beginPath(); + ctx.moveTo(x, padding); + ctx.lineTo(x, padding + chartHeight); + ctx.stroke(); + } + + // Helper function to get Y coordinate + function getY(value) { + return padding + chartHeight - ((value - yMin) / (yMax - yMin)) * chartHeight; + } + + // Helper function to get X coordinate + function getX(index) { + return padding + (index / (actualData.length - 1)) * chartWidth; + } + + // Draw actual BG line + if (actualData.length > 1) { + ctx.strokeStyle = '#667eea'; + ctx.lineWidth = 3; + ctx.beginPath(); + + for (let i = 0; i < actualData.length; i++) { + const x = getX(i); + const y = getY(actualData[i]); + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + ctx.stroke(); + + // Draw actual BG points + ctx.fillStyle = '#667eea'; + for (let i = 0; i < actualData.length; i++) { + const x = getX(i); + const y = getY(actualData[i]); + ctx.beginPath(); + ctx.arc(x, y, 3, 0, 2 * Math.PI); + ctx.fill(); + } + } + + // Draw predicted BG line + if (predictedData.length > 1) { + ctx.strokeStyle = '#f5576c'; + ctx.lineWidth = 3; + ctx.setLineDash([8, 4]); + ctx.beginPath(); + + let lastValidIndex = -1; + for (let i = 0; i < predictedData.length; i++) { + if (predictedData[i] !== null) { + const x = getX(i); + const y = getY(predictedData[i]); + + if (lastValidIndex === -1) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + lastValidIndex = i; + } + } + ctx.stroke(); + ctx.setLineDash([]); + + // Draw predicted BG points + ctx.fillStyle = '#f5576c'; + for (let i = 0; i < predictedData.length; i++) { + if (predictedData[i] !== null) { + const x = getX(i); + const y = getY(predictedData[i]); + ctx.beginPath(); + ctx.arc(x, y, 3, 0, 2 * Math.PI); + ctx.fill(); + } + } + } + + // Draw axes + ctx.strokeStyle = '#4a5568'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, padding + chartHeight); + ctx.lineTo(padding + chartWidth, padding + chartHeight); + ctx.stroke(); + + // Y axis title + ctx.fillStyle = '#4a5568'; + ctx.font = '14px Arial'; + ctx.save(); + ctx.translate(20, padding + chartHeight/2); + ctx.rotate(-Math.PI/2); + ctx.textAlign = 'center'; + ctx.fillText(`Blood Glucose (${getCurrentUnitLabel()})`, 0, 0); + ctx.restore(); + + // X axis title + ctx.textAlign = 'center'; + ctx.fillText('Time Points', padding + chartWidth/2, height - 20); +} + // HELPERS function mgdlToMmol(mgdl) { return mgdl * MGDL_TO_MMOL; } function mmolToMgdl(mmol) { return mmol * MMOL_TO_MGDL; } @@ -786,10 +984,13 @@

🩸 Nightscout AI Predictor

} chart.data.datasets[1].data = Array(SEQ_LEN-1).fill(null).concat(preds); - // Update chart y-axis range and title - chart.options.scales.y.suggestedMin = currentUnit === 'mmol' ? 2 : 40; - chart.options.scales.y.suggestedMax = currentUnit === 'mmol' ? 22 : 400; - chart.options.scales.y.title.text = `Blood Glucose (${getCurrentUnitLabel()})`; + // Update chart y-axis range and title for Chart.js + if (chart.options && chart.options.scales) { + chart.options.scales.y.suggestedMin = currentUnit === 'mmol' ? 2 : 40; + chart.options.scales.y.suggestedMax = currentUnit === 'mmol' ? 22 : 400; + chart.options.scales.y.title.text = `Blood Glucose (${getCurrentUnitLabel()})`; + } + chart.update(); }